From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-15.8 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER, INCLUDES_PATCH,MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 12F1DC4320A for ; Sun, 8 Aug 2021 23:13:27 +0000 (UTC) Received: from lists.zx2c4.com (lists.zx2c4.com [165.227.139.114]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.kernel.org (Postfix) with ESMTPS id 272E860E78 for ; Sun, 8 Aug 2021 23:13:25 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.1 mail.kernel.org 272E860E78 Authentication-Results: mail.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=rowanthorpe.com Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=lists.zx2c4.com Received: by lists.zx2c4.com (ZX2C4 Mail Server) with ESMTP id c5792244; Sun, 8 Aug 2021 23:00:53 +0000 (UTC) Received: from mail-oo1-xc2d.google.com (mail-oo1-xc2d.google.com [2607:f8b0:4864:20::c2d]) by lists.zx2c4.com (ZX2C4 Mail Server) with ESMTPS id 803cf420 (TLSv1.3:AEAD-AES256-GCM-SHA384:256:NO) for ; Wed, 16 Jun 2021 15:52:52 +0000 (UTC) Received: by mail-oo1-xc2d.google.com with SMTP id r9-20020a4a37090000b029024b15d2fef9so556630oor.7 for ; Wed, 16 Jun 2021 08:52:52 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=rowanthorpe.com; s=base; h=mime-version:from:date:message-id:subject:to; bh=0A5OPLFA3jqXexsvyLfsbzb06AVsa9/fSd2WEPd1gtQ=; b=i5z8wkV5L/9XBlsLq0eUXlMhoUrPVXMklb7Q+ZN88QijVCpOf/D7hlsm2Dlo2od9Gi Ds1L5GyTfZU83PO7E5PokYwpsP3979clXBRRUp4Fw6eBlud0/gvsUneOAxOYiiXtwLR4 r3wOnHlBzihXBQ+BBrWmSdN5xwf+owKMnCaRQ= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:from:date:message-id:subject:to; bh=0A5OPLFA3jqXexsvyLfsbzb06AVsa9/fSd2WEPd1gtQ=; b=As0dedSVXc3sftYjBxgCpncCeFokeM/Oe9AgXdK4URdfHy9zZCASvqc0Y57Da5jDBL F+wjBjD0CLFQBuzJvrkLd5UvKUDzTJMTNMdiGIYKtg3y2GergkungR6AAGAYuIOU4/zB tb2CmudIzzrNfXfVBQ2UMumNGlZekPDF6VEtgHUxrvG7p7Pbbuy+KEz0DLio2nM/EV+h x/0JlL2BByRo6LRShxaCJ2aQwTdwN0KZ+pLsFaluInxeMCOsg46jKDOgo03BYP4hpF7f Fhaov27hiybiQKb0aAwL4+baz9Q3VDbyZLrPDrfgBnbQmEBSxV9ey4nnXREysvw29Xkn DqUw== X-Gm-Message-State: AOAM530qjBWokzWOzstB7QiqWUl9i20unjc91jsRF6CArxpq6fX6+fWH AsqnDRKD97YU3GFGK97uDjHJ3YpYQgDCHZcr0QHEaMDm+l+mU3E8 X-Google-Smtp-Source: ABdhPJyABA0X3OGsUnsamRWuQN3bAzd6QErLRlH0A/h8Jf3np3DFr+zSYpE0Pywm5D09MqjZEZ+KgxgAX+CHUDhHuWk= X-Received: by 2002:a4a:b789:: with SMTP id a9mr555422oop.45.1623858770809; Wed, 16 Jun 2021 08:52:50 -0700 (PDT) MIME-Version: 1.0 From: Rowan Thorpe Date: Wed, 16 Jun 2021 18:52:14 +0300 Message-ID: Subject: [PATCH] wg-quick: add embedded-friendly POSIX-shell version To: wireguard@lists.zx2c4.com Content-Type: text/plain; charset="UTF-8" X-Mailman-Approved-At: Sun, 08 Aug 2021 23:00:51 +0000 X-BeenThere: wireguard@lists.zx2c4.com X-Mailman-Version: 2.1.30rc1 Precedence: list List-Id: Development discussion of WireGuard List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: wireguard-bounces@lists.zx2c4.com Sender: "WireGuard" I had to do extreme configuring to get the latest libreCMC release onto my old (not officially supported any more, 4MB disk) router in order to have Wireguard available & usable, and although I succeeded it resulted in a very bare-bones kernel+OS, even by embedded standards. Aside from Bash obviously being missing (Busybox provides Ash) I also had to disable some of the usual Busybox & POSIX configuration options. I still wanted to use wg-quick though (and assumed others on very small systems may want it too) so I derived an "embedded-friendly posix shellscript" variant (with shims/monkeypatching for missing or deficient executables and for missing Bash-specific builtin functionality). Usually I wouldn't bother but this is one rare case where I think it is justified. I kept its structure as equivalent (and some code identical) to the linux Bash variant as possible, in the hopes it minimises added maintenance-burden. Because it has "lowest common denominator" requirements it can be parity-tested in the same environment as the other variants (but not fully tested there for its unique parts of course). I made it opt-in as one of the "wg-quick" variants by Make env-var (WITH_EMBEDDED + WITH_WGQUICK), but perhaps the preference is just to keep it separate, named something like "wg-quick-mini" in the contrib directory..? If so let me know & I'll re-send in that form. Signed-off-by: Rowan Thorpe --- src/Makefile | 12 +- src/wg-quick/posix.sh | 880 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 890 insertions(+), 2 deletions(-) create mode 100755 src/wg-quick/posix.sh diff --git a/src/Makefile b/src/Makefile index 7b8969a..50553e6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -15,6 +15,7 @@ RUNSTATEDIR ?= /var/run WITH_BASHCOMPLETION ?= WITH_WGQUICK ?= WITH_SYSTEMDUNITS ?= +WITH_EMBEDDED ?= ifeq ($(WITH_BASHCOMPLETION),) ifneq ($(strip $(wildcard $(BASHCOMPDIR))),) @@ -28,6 +29,9 @@ endif ifneq ($(strip $(wildcard $(DESTDIR)/bin/bash)),) WITH_WGQUICK := yes endif +ifeq ($(WITH_EMBEDDED),yes) +WITH_WGQUICK := yes +endif endif ifeq ($(WITH_SYSTEMDUNITS),) ifneq ($(strip $(wildcard $(SYSTEMDUNITDIR))),) @@ -91,10 +95,14 @@ install: wg @[ "$(WITH_BASHCOMPLETION)" = "yes" ] || exit 0; \ install -v -d "$(DESTDIR)$(BASHCOMPDIR)" && install -v -m 0644 completion/wg.bash-completion "$(DESTDIR)$(BASHCOMPDIR)/wg" @[ "$(WITH_WGQUICK)" = "yes" ] || exit 0; \ - install -v -m 0755 wg-quick/$(PLATFORM).bash "$(DESTDIR)$(BINDIR)/wg-quick" && install -v -m 0700 -d "$(DESTDIR)$(SYSCONFDIR)/wireguard" + if [ "$(WITH_EMBEDDED)" = "yes" ]; then \ + install -v -m 0755 wg-quick/posix.sh "$(DESTDIR)$(BINDIR)/wg-quick"; \ + else \ + install -v -m 0755 wg-quick/$(PLATFORM).bash "$(DESTDIR)$(BINDIR)/wg-quick"; \ + fi && install -v -m 0700 -d "$(DESTDIR)$(SYSCONFDIR)/wireguard" @[ "$(WITH_WGQUICK)" = "yes" ] || exit 0; \ install -v -m 0644 man/wg-quick.8 "$(DESTDIR)$(MANDIR)/man8/wg-quick.8" - @[ "$(WITH_WGQUICK)" = "yes" -a "$(WITH_BASHCOMPLETION)" = "yes" ] || exit 0; \ + @[ "$(WITH_WGQUICK)" = "yes" -a "$(WITH_BASHCOMPLETION)" = "yes" -a "$(WITH_EMBEDDED)" != "yes" ] || exit 0; \ install -v -m 0644 completion/wg-quick.bash-completion "$(DESTDIR)$(BASHCOMPDIR)/wg-quick" @[ "$(WITH_WGQUICK)" = "yes" -a "$(WITH_SYSTEMDUNITS)" = "yes" ] || exit 0; \ install -v -d "$(DESTDIR)$(SYSTEMDUNITDIR)" && install -v -m 0644 systemd/* "$(DESTDIR)$(SYSTEMDUNITDIR)/" diff --git a/src/wg-quick/posix.sh b/src/wg-quick/posix.sh new file mode 100755 index 0000000..2ecdcbf --- /dev/null +++ b/src/wg-quick/posix.sh @@ -0,0 +1,880 @@ +#!/bin/sh +# -*- mode: sh; sh-indentation: 2; sh-basic-offset: 2; indent-tabs-mode: nil; fill-column: 100; coding: utf-8-unix; -*- +# +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (C) 2015-2020 Jason A. Donenfeld . All Rights Reserved. +# Austere posix/embedded variant derived by Rowan Thorpe , 2021. +# +# Sanity checked with: +# + shellcheck --check-sourced --external-sources --enable=all --shell=sh posix.sh +# + shfmt -d -ln posix -i 2 -ci posix.sh +# +# TODO: +# * Create local-vars drop-in functionality (without exploding complexity) to +# ensure recursion doesn't shadow vars. + +set -e + +## setup needed before function-definitions + +trap - EXIT +trap 'exit 1' HUP INT QUIT TERM + +# primitive exit-trap stack to keep things manageable +exit_trap() { + case "${1}" in + push) EXIT_TRAP="${2}${EXIT_TRAP:+${NL}${EXIT_TRAP}}" ;; + pop) EXIT_TRAP="$(printf '%s\n' "${EXIT_TRAP}" | tail -n +2)" ;; + *) exit 1 ;; + esac + #shellcheck disable=SC2064 + trap "${EXIT_TRAP:--}" EXIT +} + +# embedded systems without char-classes in "tr" need monkeypatching +if ! [ "$(printf 'aBcD' | tr '[:upper:]' '[:lower:]')" = 'abcd' ]; then + REAL_TR="$(command -v tr 2>/dev/null)" + tr() { + args='' + while [ "${#}" -ne 0 ]; do + case "${1}" in + '[:upper:]') + args="${args:+${args} }$(entity_save '[A-Z]')" + ;; + '[:lower:]') + args="${args:+${args} }$(entity_save '[a-z]')" + ;; + *) + args="${args:+${args} }$(entity_save "${1}")" + ;; + esac + shift + done + eval "${REAL_TR} ${args}" + unset args + } +fi + +# POSIX shells _may_ not have "type -p" so we need this drop-in +#shellcheck disable=SC2039 +if [ -n "$(type -p cat 2>/dev/null || :)" ]; then + type_p() { + type -p "${@}" + } +else + type_p() { + ret=0 + for arg; do + found=0 + for path in $(printf %s "${PATH-}" | tr ':' ' '); do + if [ -x "${path}/${arg}" ]; then + found=1 + break + fi + done + if [ "${found}" -eq 1 ]; then + printf '%s/%s' "${path}" "${arg}" + else + ret=1 + fi + done + unset arg found path + if [ "${ret}" -eq 0 ]; then + unset ret + return 0 + else + unset ret + return 1 + fi + } +fi + +# embedded systems without "stat" need this drop-in +if command -v stat >/dev/null 2>&1; then + stat_octal() { + stat -c '%04a' "${@}" + } +else + stat_octal() { + #shellcheck disable=SC2012 disable=SC2034 + ls -l "${@}" | + sed -ne ' + s/^[-dsbclp]\([-r]\)\([-w]\)\([-xsStT]\)\([-r]\)\([-w]\)\([-xsStT]\)\([-r]\)\([-w]\)\([-xsStT]\) .*$/\1 \2 \3 \4 \5 \6 \7 \8 \9/g + t P + b + : P + p + ' | + while read -r ur uw ux gr gw gx or ow ox; do + out='' + spc_sum=0 + for ctg in u g o; do + sum=0 + for perm in r w x; do + var="${ctg}${perm}" + eval "val=\"\${${var}}\"" + #shellcheck disable=SC2154 + case "${val}" in + r) exp=2 ;; + w) exp=1 ;; + s | t | x) exp=0 ;; + - | S | T) exp=-1 ;; + *) exit 1 ;; + esac + case "${val}" in + - | w | r | x) + spc_exp=-1 + ;; + S | s) + case "${var}" in + u*) spc_exp=2 ;; + g*) spc_exp=1 ;; + *) exit 1 ;; + esac + ;; + T | t) + case "${var}" in + o*) spc_exp=0 ;; + *) exit 1 ;; + esac + ;; + *) + exit 1 + ;; + esac + [ "${exp}" -lt 0 ] || + sum=$((sum + $((1 << exp)))) + [ "${spc_exp}" -lt 0 ] || + spc_sum=$((spc_sum + $((1 << spc_exp)))) + done + out="${out}$(printf %o "${sum}")" + done + printf '%o%s\n' "${spc_sum}" "${out}" + done + unset ur uw ux gr gw gx or ow ox ctg spc_sum out perm sum var val exp spc_exp + } +fi + +## + +e_body_save() { sed -e "s/'/'\\\\''/g"; } + +e_head_save() { sed -e "1s/^/'/"; } + +e_tail_save() { sed -e "\$s/\$/'/"; } + +e_save() { e_body_save | e_head_save | e_tail_save; } + +a_e_wrap() { sed -e '$s/$/ \\/'; } + +a_wrap() { sed -e '$s/$/\n /'; } + +entity_save() { printf '%s\n' "${1}" | e_save; } + +array_save() { + for i; do + entity_save "${i}" | a_e_wrap + done | + a_wrap + unset i +} + +array_append() { + orig_name="${1}" + shift + new=$(array_save "${@}") + eval " + eval \"set -- \${${orig_name}}\" + set -- \"\${@}\" ${new} + ${orig_name}=\$(array_save \"\${@}\") + " + unset orig_name new +} + +get_mtu() { + output="${1}" + existing_mtu="${2}" + shift 2 + mtu_match='' + dev_match='' + mtu_match="$(printf %s "${output}" | sed -ne 's:^.*\.*$:\1:; t P; b; : P; p; q')" + if [ -z "${mtu_match}" ]; then + dev_match="$(printf %s "${output}" | sed -ne 's:^.*\.*$:\1:; t P; b; : P; p; q')" + [ -z "${dev_match}" ] || + mtu_match="$(ip link show dev "${dev_match}" | sed -ne 's:^.*\.*$:\1:; t P; b; : P; p; q')" + fi + if [ -n "${mtu_match}" ] && + [ "${mtu_match}" -gt "${existing_mtu}" ]; then + printf %s "${mtu_match}" + else + printf %s "${existing_mtu}" + fi + unset output existing_mtu mtu_match dev_match +} + +## + +cmd() { + printf '[#] %s\n' "${*}" >&2 + "${@}" +} + +die() { + printf '%s: %s\n' "${PROGRAM}" "${*}" >&2 + exit 1 +} + +parse_options() { + interface_section=0 + line='' + key='' + value='' + stripped='' + v='' + header_line=0 + CONFIG_FILE="${1}" + #shellcheck disable=SC2003 + ! expr match "${CONFIG_FILE}" '[a-zA-Z0-9_=+.-]\{1,15\}$' >/dev/null || + CONFIG_FILE="${CONFIG_FILE_BASE}/${CONFIG_FILE}.conf" + [ -e "${CONFIG_FILE}" ] || + die "\`${CONFIG_FILE}' does not exist" + #shellcheck disable=SC2003 + expr match "${CONFIG_FILE}" '\(.*/\)\?\([a-zA-Z0-9_=+.-]\{1,15\}\)\.conf$' >/dev/null || + die 'The config file must be a valid interface name, followed by .conf' + CONFIG_FILE="$(readlink -f "${CONFIG_FILE}")" + if { + stat_octal "${CONFIG_FILE}" || : + stat_octal "$(printf %s "${CONFIG_FILE}" | sed -e 's:/[^/]*$::')" || : + } 2>/dev/null | grep -vq '0$'; then + printf 'Warning: `%s'\'' is world accessible\n' "${CONFIG_FILE}" >&2 + fi + INTERFACE="$(printf %s "${CONFIG_FILE}" | sed -e 's:^\(.*/\)\?\([^/.]\+\)\.conf$:\2:')" + while read -r line || [ -n "${line}" ]; do + stripped="$(printf %s "${line}" | sed -e 's:#.*$::; /^[[:blank:]]*$/d')" + key="$(printf %s "${stripped}" | sed -e 's#^[[:blank:]]*\([^=[:blank:]]\+\)[[:blank:]]*=.*$#\1#')" + case "${key}" in + '['*) + if [ "${key}" = '[Interface]' ]; then + interface_section=1 + else + interface_section=0 + fi + header_line=1 + ;; + *) + header_line=0 + ;; + esac + if [ "${header_line}" -eq 0 ] && [ "${interface_section}" -eq 1 ]; then + value="$( + printf %s "${stripped}" | + sed -e 's#^[^=]\+=[[:blank:]]*\([^[:blank:]]\(.*[^[:blank:]]\)\?\)\?[[:blank:]]*$#\1#' + )" + case "$(printf %s "${key}" | tr '[:upper:]' '[:lower:]')" in + address) + #shellcheck disable=SC2046 + array_append ADDRESSES $(printf %s "${value}" | tr ',' ' ') + continue + ;; + mtu) + MTU="${value}" + continue + ;; + dns) + for v in $(printf %s "${value}" | tr ',' ' '); do + #shellcheck disable=SC2003 + if expr match "${v}" '[0-9.]\+$' >/dev/null || expr match "${v}" '.*:.*$' >/dev/null; then + array_append DNS "${v}" + else + array_append DNS_SEARCH "${v}" + fi + done + continue + ;; + table) + TABLE="${value}" + continue + ;; + preup) + array_append PRE_UP "${value}" + continue + ;; + predown) + array_append PRE_DOWN "${value}" + continue + ;; + postup) + array_append POST_UP "${value}" + continue + ;; + postdown) + array_append POST_DOWN "${value}" + continue + ;; + saveconfig) + read_bool SAVE_CONFIG "${value}" + continue + ;; + *) + : + ;; + esac + fi + WG_CONFIG="${WG_CONFIG:+${WG_CONFIG}${NL}}${line}" + done <"${CONFIG_FILE}" + unset interface_section line key value stripped v header_line +} + +read_bool() { + case "${2}" in + true) eval "${1}=1" ;; + false) eval "${1}=0" ;; + *) die "\`${2}' is neither true nor false" ;; + esac +} + +#shellcheck disable=SC2120 +auto_su() { + if [ "${UID}" -ne 0 ]; then + eval "set -- ${ARGS}" + exec sudo -p "${PROGRAM} must be run as root. Please enter the password for %u to continue: " -- \ + "${SHELL:-/bin/sh}" -- "${SELF}" "${@}" + fi +} + +add_if() { + ret=0 + if ! cmd ip link add "${INTERFACE}" type wireguard; then + ret=${?} + ! [ -e /sys/module/wireguard ] && command -v "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" >/dev/null || + exit "${ret}" + printf '[!] Missing WireGuard kernel module. Falling back to slow userspace implementation.\n' >&2 + cmd "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" "${INTERFACE}" + fi + unset ret +} + +del_if() { + table='' + [ "${HAVE_SET_DNS-0}" -eq 0 ] || unset_dns + [ "${HAVE_SET_FIREWALL-0}" -eq 0 ] || remove_firewall + #shellcheck disable=SC2003 + if [ -z "${TABLE}" ] || + [ "x${TABLE}" = 'xauto' ] && + get_fwmark table && + expr match "$(wg show "${INTERFACE}" allowed-ips)" '.*/0\( .*\|'"${NL}"'.*\)\?$' >/dev/null; then + for proto in -4 -6; do + while :; do + case "$(ip "${proto}" rule show 2>/dev/null)" in + *"lookup ${table}"*) + cmd ip "${proto}" rule delete table "${table}" + ;; + *) + break + ;; + esac + done + while :; do + case "$(ip "${proto}" rule show 2>/dev/null)" in + *"from all lookup main suppress_prefixlength 0"*) + cmd ip "${proto}" rule delete table main suppress_prefixlength 0 + ;; + *) + break + ;; + esac + done + done + unset proto + fi + cmd ip link delete dev "${INTERFACE}" + unset table +} + +add_addr() { + case "${1}" in + *:*) proto=-6 ;; + *) proto=-4 ;; + esac + cmd ip "${proto}" address add "${1}" dev "${INTERFACE}" + unset proto +} + +set_mtu_up() { + mtu=0 + endpoint='' + v6_addr='' + if [ -n "${MTU}" ]; then + cmd ip link set mtu "${MTU}" up dev "${INTERFACE}" + else + wg show "${INTERFACE}" endpoints | { + while read -r _ endpoint; do + v6_addr="$( + printf %s "${endpoint}" | + sed -ne ' + s%^\[\([a-z0-9:.]\+\)\]:[0-9]\+$%\1% + t P + s%^\([a-z0-9:.]\+\):[0-9]\+$%\1% + t P + b + : P + p + ' + )" + [ -z "${v6_addr}" ] || + mtu="$(get_mtu "$(ip route get "${v6_addr}" || :)" "${mtu}")" + done + [ "${mtu}" -gt 0 ] || + mtu="$(get_mtu "$(ip route show default || :)" "${mtu}")" + [ "${mtu}" -gt 0 ] || mtu=1500 + cmd ip link set mtu $((mtu - 80)) up dev "${INTERFACE}" + } + fi + unset mtu endpoint v6_addr +} + +resolvconf_iface_prefix() { + if ! [ -f /etc/resolvconf/interface-order ]; then + iface='' + while read -r iface; do + #shellcheck disable=SC2003 + expr match "${iface}" '\([A-Za-z0-9-]\+\)\*$' >/dev/null || + continue + printf '%s\n' "${iface}" | + sed -e 's/\*\?$/./' + break + done /dev/null)" ] || + cmd ip "${proto}" route add "${1}" dev "${INTERFACE}" + ;; + esac + fi + unset proto +} + +get_fwmark() { + fwmark="$(wg show "${INTERFACE}" fwmark)" && + [ -n "${fwmark}" ] && + [ "x${fwmark}" != 'xoff' ] || + return 1 + eval "${1}=${fwmark}" + unset fwmark +} + +remove_firewall() { + if type_p nft >/dev/null; then + table='' + nftcmd='' + nft list tables 2>/dev/null | { + while read -r table; do + case "${table}" in + *" wg-quick-${INTERFACE}") + nftcmd="${nftcmd:+${nftcmd}${NL}}delete ${table}" + ;; + *) + : + ;; + esac + done + if [ -n "${nftcmd}" ]; then + printf '%s\n' "${nftcmd}" | + cmd nft -f + fi + } + unset table nftcmd + fi + if type_p iptables >/dev/null; then + iptables='' + for iptables in iptables ip6tables; do + "${iptables}-save" 2>/dev/null | { + restore='' + found=0 + line='' + while read -r line; do + case "${line}" in + \** | COMMIT | '-A '*'-m comment --comment "wg-quick(8) rule for '"${INTERFACE}"'"'*) + case "${line}" in + -A*) + found=1 + ;; + *) + : + ;; + esac + restore="${restore:+${restore}${NL}}-D${line#-A}" + ;; + *) + : + ;; + esac + done + [ "${found}" -ne 1 ] || + printf '%s\n' "${restore}" | + cmd "${iptables}-restore" -n + unset restore found line + } + done + unset iptables + fi +} + +add_default() { + table='' + line='' + proto='' + iptables='' + pf='' + marker='' + restore='' + nftable='' + nftcmd='' + if ! get_fwmark table; then + table=51820 + while [ -n "$(ip -4 route show table "${table}" 2>/dev/null)" ] || + [ -n "$(ip -6 route show table "${table}" 2>/dev/null)" ]; do + table=$((table + 1)) + done + cmd wg set "${INTERFACE}" fwmark "${table}" + fi + case "${1}" in + *:*) + proto='-6' + iptables='ip6tables' + pf='ip6' + ;; + *) + proto='-4' + iptables='iptables' + pf='ip' + ;; + esac + cmd ip "${proto}" route add "${1}" dev "${INTERFACE}" table "${table}" + cmd ip "${proto}" rule add not fwmark "${table}" table "${table}" + cmd ip "${proto}" rule add table main suppress_prefixlength 0 + + marker="-m comment --comment \"wg-quick(8) rule for ${INTERFACE}\"" + restore="*raw${NL}" + nftable="wg-quick-${INTERFACE}" + nftcmd="${nftcmd:+${nftcmd}${NL}}add table ${pf} ${nftable}" + nftcmd="${nftcmd:+${nftcmd}${NL}}add chain ${pf} ${nftable} preraw { type filter hook prerouting priority -300; }" + nftcmd="${nftcmd:+${nftcmd}${NL}}add chain ${pf} ${nftable} premangle { type filter hook prerouting priority -150; }" + nftcmd="${nftcmd:+${nftcmd}${NL}}add chain ${pf} ${nftable} postmangle { type filter hook postrouting priority -150; }" + ip -o "${proto}" addr show dev "${INTERFACE}" 2>/dev/null | { + match='' + while read -r line; do + match="$( + printf %s "${line}" | + sed -ne 's/^.*inet6\? \([0-9a-f:.]\+\)/[0-9]\+.*$/\1/; t P; b; : P; p' + )" + [ -n "${match}" ] || + continue + restore="${restore:+${restore}${NL}}-I PREROUTING ! -i ${INTERFACE} -d ${match} -m addrtype ! --src-type LOCAL -j DROP ${marker}" + nftcmd="${nftcmd:+${nftcmd}${NL}}add rule ${pf} ${nftable} preraw iifname != \"${INTERFACE}\" ${pf} daddr ${match} fib saddr type != local drop" + done + restore="${restore:+${restore}${NL}}COMMIT${NL}*mangle${NL}-I POSTROUTING -m mark --mark ${table} -p udp -j CONNMARK --save-mark ${marker}${NL}-I PREROUTING -p udp -j CONNMARK --restore-mark ${marker}${NL}COMMIT" + nftcmd="${nftcmd:+${nftcmd}${NL}}add rule ${pf} ${nftable} postmangle meta l4proto udp mark ${table} ct mark set mark" + nftcmd="${nftcmd:+${nftcmd}${NL}}add rule ${pf} ${nftable} premangle meta l4proto udp meta mark set ct mark" + ! [ "${proto}" = '-4' ] || + cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1 + if type_p nft >/dev/null; then + printf '%s\n' "${nftcmd}" | + cmd nft -f + else + printf '%s\n' "${restore}" | + cmd "${iptables}-restore" -n + fi + unset match + } + HAVE_SET_FIREWALL=1 + unset table line proto iptables pf marker restore nftable nftcmd +} + +set_config() { + if [ -e /dev/stdin ]; then + printf '%s\n' "${WG_CONFIG}" | + cmd wg setconf "${INTERFACE}" /dev/stdin + else + tempfile="$(mktemp)" + exit_trap push "rm -f \"${tempfile}\"" + printf '%s\n' "${WG_CONFIG}" >"${tempfile}" + cmd wg setconf "${INTERFACE}" "${tempfile}" + rm -f "${tempfile}" + exit_trap pop + unset tempfile + fi +} + +save_config() { + old_umask='' + new_config='' + current_config='' + address='' + cmd='' + addr_match="$( + ip -all -brief address show dev "${INTERFACE}" | + sed -ne 's#^'"${INTERFACE}"' \+[A-Z]\+ \+\(.\+\)$#\1#; t P; b; : P; p' + )" + new_config='[Interface]' + for address in ${addr_match}; do + new_config="${new_config:+${new_config}${NL}}Address = ${address}" + done + { + resolvconf -l "$(resolvconf_iface_prefix)${INTERFACE}" 2>/dev/null || + cat "/etc/resolvconf/run/interface/$(resolvconf_iface_prefix)${INTERFACE}" 2>/dev/null + } | { + while read -r address; do + addr_match="$( + printf %s "${address}" | + sed -ne 's#^nameserver \([a-zA-Z0-9_=+:%.-]\+\)$#\1#; t P; b; : P; p' + )" + [ -z "${addr_match}" ] || + new_config="${new_config:+${new_config}${NL}}DNS = ${addr_match}" + done + if [ -n "${MTU}" ]; then + mtu_match="$( + ip link show dev "${INTERFACE}" | + sed -ne 's/^.*mtu \([0-9]\+\).*$/\1/; t P; b; : P; p' + )" + [ -z "${mtu_match}" ] || + new_config="${new_config:+${new_config}${NL}}MTU = ${mtu_match}" + fi + [ -z "${TABLE}" ] || + new_config="${new_config:+${new_config}${NL}}Table = ${TABLE}" + [ "${SAVE_CONFIG}" -eq 0 ] || + new_config="${new_config:+${new_config}${NL}}SaveConfig = true" + eval "set -- ${PRE_UP}" + for cmd; do + new_config="${new_config:+${new_config}${NL}}PreUp = ${cmd}" + done + eval "set -- ${POST_UP}" + for cmd; do + new_config="${new_config:+${new_config}${NL}}PostUp = ${cmd}" + done + eval "set -- ${PRE_DOWN}" + for cmd; do + new_config="${new_config:+${new_config}${NL}}PreDown = ${cmd}" + done + eval "set -- ${POST_DOWN}" + for cmd; do + new_config="${new_config:+${new_config}${NL}}PostDown = ${cmd}" + done + old_umask="$(umask)" + umask 077 + current_config="$(cmd wg showconf "${INTERFACE}")" + exit_trap push "rm -f \"${CONFIG_FILE}.tmp\"" + printf '%s\n' "${current_config}" | + sed -e "s#\\[Interface\\]\$#$( + printf %s "${new_config}" | + sed -e '$!s/$/\\n/' | + tr -d '\n' + )#" >"${CONFIG_FILE}.tmp" || + die 'Could not write configuration file' + sync "${CONFIG_FILE}.tmp" + mv "${CONFIG_FILE}.tmp" "${CONFIG_FILE}" || + die 'Could not move configuration file' + exit_trap pop + umask "${old_umask}" + unset new_config current_config old_umask cmd mtu_match addr_match address + } +} + +execute_hooks() { + for hook; do + hook="$( + printf %s "${hook}" | + sed -e "s^%i^${INTERFACE}^g" + )" + printf '[#] %s\n' "${hook}" >&2 + (eval "${hook}") + done + unset hook +} + +cmd_usage() { + cat >&2 <<-_EOF + Usage: ${PROGRAM} [ up | down | save | strip ] [ CONFIG_FILE | INTERFACE ] + + CONFIG_FILE is a configuration file, whose filename is the interface name + followed by \`.conf'. Otherwise, INTERFACE is an interface name, with + configuration found at ${CONFIG_FILE_BASE}/INTERFACE.conf. It is to be readable + by wg(8)'s \`setconf' sub-command, with the exception of the following additions + to the [Interface] section, which are handled by ${PROGRAM}: + + - Address: may be specified one or more times and contains one or more + IP addresses (with an optional CIDR mask) to be set for the interface. + - DNS: an optional DNS server to use while the device is up. + - MTU: an optional MTU for the interface; if unspecified, auto-calculated. + - Table: an optional routing table to which routes will be added; if + unspecified or \`auto', the default table is used. If \`off', no routes + are added. + - PreUp, PostUp, PreDown, PostDown: script snippets which will be executed + by bash(1) at the corresponding phases of the link, most commonly used + to configure DNS. The string \`%i' is expanded to INTERFACE. + - SaveConfig: if set to \`true', the configuration is saved from the current + state of the interface upon shutdown. + + See wg-quick(8) for more info and examples. + _EOF +} + +cmd_up() { + i='' + [ -z "$(ip link show dev "${INTERFACE}" 2>/dev/null)" ] || + die "\`${INTERFACE}' already exists" + exit_trap push 'del_if' + eval "execute_hooks ${PRE_UP}" + add_if + set_config + eval "set -- ${ADDRESSES}" + for i; do + add_addr "${i}" + done + set_mtu_up + set_dns + for i in $( + wg show "${INTERFACE}" allowed-ips | + while read -r _ j; do + for k in ${j}; do + #shellcheck disable=SC2003 + ! expr match "${k}" '[0-9a-z:.]\+/[0-9]\+$' >/dev/null || + printf '%s\n' "${k}" + done + done | + sort -nr -k 2 -t / + unset j k + ); do + add_route "${i}" + done + eval "execute_hooks ${POST_UP}" + unset i + exit_trap pop +} + +cmd_down() { + case " $(wg show interfaces) " in + *" ${INTERFACE} "*) : ;; + *) die "\`${INTERFACE}' is not a WireGuard interface" ;; + esac + eval "execute_hooks ${PRE_DOWN}" + [ "${SAVE_CONFIG}" -eq 0 ] || + save_config + del_if + unset_dns || : + remove_firewall || : + eval "execute_hooks ${POST_DOWN}" +} + +cmd_save() { + case " $(wg show interfaces) " in + *" ${INTERFACE} "*) : ;; + *) die "\`${INTERFACE}' is not a WireGuard interface" ;; + esac + save_config +} + +cmd_strip() { printf '%s\n' "${WG_CONFIG}"; } + +## + +EXIT_TRAP='' +LC_ALL=C +SELF="$(readlink -f "${0}")" +PATH="$(printf %s "${SELF}" | sed -e 's:/[^/]*$::'):${PATH}" +export LC_ALL PATH +[ -n "${UID-}" ] || UID="$(id -u)" +[ -n "${CONFIG_FILE_BASE}" ] || + CONFIG_FILE_BASE='/etc/wireguard' +NL=' +' +WG_CONFIG='' +INTERFACE='' +ADDRESSES=$(array_save) +MTU='' +DNS=$(array_save) +DNS_SEARCH=$(array_save) +TABLE='' +PRE_UP=$(array_save) +POST_UP=$(array_save) +PRE_DOWN=$(array_save) +POST_DOWN=$(array_save) +SAVE_CONFIG=0 +CONFIG_FILE='' +PROGRAM="$(printf %s "${0}" | sed -e 's:^.*/\([^/]*\)$:\1:')" +ARGS=$(array_save "${@}") +HAVE_SET_DNS=0 +HAVE_SET_FIREWALL=0 + +# ~~ function override insertion point ~~ + +case "${#}:${1}" in + 1:--help | 1:-h | 1:help) + cmd_usage + ;; + 2:up | 2:down | 2:save | 2:strip) + auto_su + parse_options "${2}" + case "${1}" in + up) + cmd_up + ;; + down) + cmd_down + ;; + save) + cmd_save + ;; + strip) + cmd_strip + ;; + *) + : + ;; + esac + ;; + *) + cmd_usage + exit 1 + ;; +esac -- 2.31.1