[PATCH] wg-quick: add embedded-friendly POSIX-shell version

Rowan Thorpe rowan at rowanthorpe.com
Wed Jun 16 15:52:53 UTC 2021


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 <rowan at rowanthorpe.com>
---
 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 <Jason at zx2c4.com>. All
Rights Reserved.
+# Austere posix/embedded variant derived by Rowan Thorpe
<rowan at rowanthorpe.com>, 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:^.*\<mtu
\([0-9]\+\)\>.*$:\1:; t P; b; : P; p; q')"
+  if [ -z "${mtu_match}" ]; then
+    dev_match="$(printf %s "${output}" | sed -ne 's:^.*\<dev \([^
]\+\)\>.*$:\1:; t P; b; : P; p; q')"
+    [ -z "${dev_match}" ] ||
+      mtu_match="$(ip link show dev "${dev_match}" | sed -ne
's:^.*\<mtu \([0-9]\+\)\>.*$:\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 </etc/resolvconf/interface-order
+    unset iface
+  fi
+}
+
+#shellcheck disable=SC2120
+set_dns() {
+  eval "set -- ${DNS}"
+  if [ ${#} -gt 0 ]; then
+    {
+      printf 'nameserver %s\n' "${@}"
+      eval "set -- ${DNS_SEARCH}"
+      [ ${#} -eq 0 ] ||
+        printf 'search %s\n' "${*}"
+    } | cmd resolvconf -a "$(resolvconf_iface_prefix)${INTERFACE}" -m 0 -x
+    HAVE_SET_DNS=1
+  fi
+}
+
+unset_dns() {
+  eval "set -- ${DNS}"
+  [ ${#} -eq 0 ] ||
+    cmd resolvconf -d "$(resolvconf_iface_prefix)${INTERFACE}" -f
+}
+
+add_route() {
+  case "${1}" in
+    *:*) proto=-6 ;;
+    *) proto=-4 ;;
+  esac
+  if [ "${TABLE}" != off ]; then
+    case "${TABLE}:${1}" in
+      :*)
+        :
+        ;;
+      auto:*)
+        cmd ip "${proto}" route add "${1}" dev "${INTERFACE}" table "${TABLE}"
+        ;;
+      *:*/0)
+        add_default "${1}"
+        ;;
+      *)
+        [ -n "$(ip "${proto}" route show dev "${INTERFACE}" match
"${1}" 2>/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


More information about the WireGuard mailing list