commit 8b7fe69ca362bf7f78fee7207ddd10d8697ae02a Author: Freya Murphy Date: Fri Nov 10 19:44:48 2023 -0500 things diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0008476 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine +RUN apk add --no-cache wireguard-tools bind-tools bird openrc udev-init-scripts-openrc +COPY ./wait.initd /etc/init.d/wait +COPY ./inet2.initd /etc/init.d/inet2 +RUN sed -i 's/#rc_sys=""/rc_sys="docker"/' /etc/rc.conf && \ + rc-update add wait && \ + rc-update add inet2 +COPY ./setup.sh /setup.sh +COPY ./bin /usr/local/bin + +ENTRYPOINT ["/setup.sh"] + diff --git a/bin/config.awk b/bin/config.awk new file mode 100755 index 0000000..f4833ae --- /dev/null +++ b/bin/config.awk @@ -0,0 +1,35 @@ +#!/run/current-system/profile/bin/awk -f + +BEGIN { + FS = "[ ]"; # use a single space as field separator and don't trim input + ind = 0; # indentation level + last = ARGC - 3; # last argument index + exitcode = 1; # whether anything has been matched + if(last < 0) { # there should be at least one argument after the filename + exit 1; + } + ARGC = 2; # don't read ARGV[2] and onward as files +} + +END { + exit exitcode; +} + +$0 != "" { # exit when the indentation block is exited + for(i = 0; i < ind; i++) { + if(! sub(/^\t/, "")) { + exit exitcode; + } + } +} + +# if on the last argument, interpret it as a key and print the value +ind == last && $1 == ARGV[ind + 2] { + exitcode = 0; + print substr($0, length($1) + 2); +} +# if not on the last argument, find the string exactly and increment indentation +ind != last && $0 == ARGV[ind + 2] { + ind++; +} + diff --git a/bin/mkwgconfig.sh b/bin/mkwgconfig.sh new file mode 100755 index 0000000..3afa221 --- /dev/null +++ b/bin/mkwgconfig.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env sh +# args: /path/to/interface-config /path/to/output.conf + +inter="$1" +configfile="$3" +if [ -z "$configfile" ]; then + configfile=/run/inet2/inet2.conf +fi + +getval() { + /usr/local/bin/config.awk "$configfile" "$@" +} + +k() { + while read -r line; do + echo "$1 = $line" + done +} + +( + echo "[Interface]" + getval "interface $inter" ListenPort | k ListenPort + getval "interface $inter" PrivateKey | k PrivateKey + getval PrivateKey | k PrivateKey + echo + + getval "interface $inter" peer | while read -r peer; do + echo "[Peer]" + getval "interface $inter" "peer $peer" PublicKey | k PublicKey + getval "interface $inter" "peer $peer" AllowedIPs | k AllowedIPs + + domain="$(getval "interface $inter" "peer $peer" Domain)" + if [ -n "$domain" ]; then + # it doesn't like domain names in the Endpoint field, so resolve dns here + v4="$(dig +short "$domain")" + [ ! "$?" = "0" ] && v4="" + v6="$(dig +short -t aaaa "$domain")" + [ ! "$?" = "0" ] && v6="" + if getval "interface $inter" "peer $peer" IPv4; then + v6="" + fi + addr="[$v6]" + [ "$addr" = "[]" ] && addr="$v4" + echo "Endpoint = $addr:$(getval "interface $inter" "peer $peer" Port)" + else + getval "interface $inter" "peer $peer" Endpoint | k Endpoint + fi + getval "interface $inter" "peer $peer" PersistentKeepalive | k PersistentKeepalive + echo + done +) > "$2" diff --git a/inet2.initd b/inet2.initd new file mode 100755 index 0000000..7824bdc --- /dev/null +++ b/inet2.initd @@ -0,0 +1,108 @@ +#!/sbin/openrc-run +name="inet2" +description="Sets up wireguard interfaces connected via the host's internet connection" + +extra_started_commands="reloadwg" + +run() { + printf '$ \x1b[32;1m%s\x1b[0m\n' "$*" + "$@" +} +step() { + printf '\x1b[34;1m>> %s\x1b[0m\n' "$*" +} + +getval() { + /usr/local/bin/config.awk /run/inet2/inet2.conf "$@" +} + +runscripts() { + if [ -n "$(getval "interface $2" "$1")" ]; then + step "Running $1 for $2" + getval "interface $2" "$1" | while read -r line; do + (eval "$line") + done + fi +} + +start() { + rm -rf /run/inet2/config 2>/dev/null + rm -rf /run/inet2/wg 2>/dev/null + cp /config/inet2.conf /run/inet2/inet2.conf + mkdir /run/inet2/wg + + getval Loopback | while read -r addr; do + run ip addr add "$addr" dev lo + done + + getval interface | while read -r inter; do + step "Generating config for $inter" + run mkwgconfig.sh "$inter" /run/inet2/wg/"$inter" + + # create the wireguard interface *in the default namespace* + step "Adding Wireguard interface $inter" + run ip link add name "$inter" type wireguard + + # set up the new network from the config + step "Setting Wireguard config for $inter" + run wg setconf "$inter" /run/inet2/wg/"$inter" + + # the config doesn't actually add any addresses, do that here + step "Adding host addresses for $inter" + getval "interface $inter" Address | while read -r addr; do + run ip addr add "$addr" dev "$inter" + done + + runscripts PreUp "$inter" + + step "Bringing interface up" + run ip link set dev "$inter" up + + getval "interface $inter" Route | while read -r line; do + read -r route via addr2 < <(printf "%s" "$line") + if [ "$via" = "via" ]; then + run ip route add "$route" via "$addr2" dev "$inter" + else + run ip route add "$route" dev "$inter" + fi + done + + runscripts PostUp "$inter" + done + step "Done!" +} + +stop() { + if [ -f /run/inet2/inet2.conf ]; then + getval Loopback | while read -r addr; do + run ip addr del "$addr" dev lo + done + + getval interface | while read -r inter; do + runscripts PreDown "$inter" + + step "Bringing $inter down" + run ip link del "$inter" + + runscripts PostDown "$inter" + done + + rm -rf /run/inet2/inet2.conf + fi +} + +# just reloads the wireguard configs for existing interfaces +# for if a peer's domain name resolves to a different ip address now +# and it needs to be re-resolved without taking down the connection +reloadwg() { + if [ -f /run/inet2/inet2.conf ]; then + getval interface | while read -r inter; do + step "Generating config for $inter" + run mkwgconfig.sh "$inter" /run/inet2/wg/"$inter" /config/inet2.conf + + step "Setting Wireguard config for $inter" + run wg setconf "$inter" /run/inet2/wg/"$inter" + done + fi +} + diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..c03fb10 --- /dev/null +++ b/setup.sh @@ -0,0 +1,147 @@ +#!/bin/sh + +run() { + printf '$ \x1b[32;1m%s\x1b[0m\n' "$*" + "$@" +} +step() { + printf '\x1b[34;1m>> %s\x1b[0m\n' "$*" +} + +getval() { + /usr/local/bin/config.awk /config/inet2.conf "$@" +} + +haskey() { + getval interface | while read -r inter; do + if getval "interface $inter" "$1"; then + echo "true" + return + fi + done +} + +# ensure the /run/inet2 directory is empty (docker doesn't mount tmpfs to /run) +# /run/inet2 is used for storage during runtime - restarting the container should clear it +rm -rf /run/inet2 2>/dev/null +mkdir /run/inet2 + +# ensure the /var/lib/inet2 directory exists +# /var/lib/inet2 is used for storage for the entire lifetime of the container - restarting the container shouldn't clear it +if [ ! -d /var/lib/inet2 ]; then + mkdir -p /var/lib/inet2 +fi + +# these are disabled in the docker netns +step "Enabling IPv6" +run sysctl net.ipv6.conf.all.disable_ipv6=0 net.ipv6.conf.default.disable_ipv6=0 net.ipv6.conf.all.forwarding=1 + +ospf="$(haskey OSPF)" + +escapebird() { + sed -e 's/\\/\\\\/g;s/"/\\"/g' +} + +if [ -n "$ospf" ]; then + step "Creating Bird configuration" + + touch /var/log/bird.log + chown bird:bird /var/log/bird.log + + selfas=$(getval AS) + ( + cat < /etc/bird.conf + chown root:bird /etc/bird.conf + chmod 640 /etc/bird.conf + + step "Enabling BIRD" + run rc-update add bird +fi + +if [ ! -f /var/lib/inet2/setupDone ]; then + if [ -f /config/setup.sh ]; then + step "Running /config/setup.sh" + /config/setup.sh + fi + touch /var/lib/inet2/setupDone +fi + +if [ -f /config/start.sh ]; then + step "Running /config/start.sh" + /config/start.sh +fi + +if [ "$#" = "0" ]; then + step "Starting OpenRC" + rm -rf /run/openrc 2>/dev/null + mkdir /run/openrc + touch /run/openrc/softlevel + exec /sbin/openrc +else + "$@" +fi + diff --git a/wait.initd b/wait.initd new file mode 100755 index 0000000..b033ac1 --- /dev/null +++ b/wait.initd @@ -0,0 +1,4 @@ +#!/sbin/openrc-run +# this container may need openrc to keep running without any services +# ordinarily that would cause it to stop, so this is a dummy service +start(){ sleep infinity & }