#!/bin/bash # SPDX-License-Identifier: GPL-2.0 # # Copyright (C) 2015-2020 Jason A. Donenfeld . All Rights Reserved. # Copyright (C) 2021-2022 Daniel afx # #set -e -o pipefail shopt -s extglob export LC_ALL=C SELF="$(readlink -f "${BASH_SOURCE[0]}")" export PATH="${SELF%/*}:$PATH" WG_CONFIG="" CONFIG_FILE="" PROGRAM="${0##*/}" ARGS=( "$@" ) EXTERNAL_NETWORK_TEST_IP="1.1.1.1" cmd_usage() { cat >&2 <<-_EOF Usage: $PROGRAM [ CONFIG_NAME ] CONFIG_NAME is the name of a configuration file, which is also the interface name followed by \`.conf'. It should be a configuration found at /etc/wireguard/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. If for some reason the interface is already up, you could use: $PROGRAM [ CONFIG_NAME ] down _EOF } # Helper Functions auto_su() { [[ $UID == 0 ]] || exec sudo -p "$PROGRAM must be run as root. Please enter the password for %u to continue: " -- "$BASH" -- "$SELF" "${ARGS[@]}" } say() { echo " ] $*" } die() { echo "!] $*" >&2 exit 1 } cmd() { echo "#] $*" >&2 "$@" } execute_hooks() { local hook for hook in "$@"; do hook="${hook//%i/$INTERFACE}" echo "#] $hook" >&2 (eval "$hook") done } parse_options() { INTERFACE="" ADDRESSES=( ) MTU="" DNS=( ) DNS_SEARCH=( ) TABLE="" PRE_UP=( ) POST_UP=( ) PRE_DOWN=( ) POST_DOWN=( ) local interface_section=0 line key value stripped v CONFIG_FILE="$1" [[ $CONFIG_FILE =~ ^[a-zA-Z0-9_=+.-]{1,15}$ ]] && CONFIG_FILE="/etc/wireguard/$CONFIG_FILE.conf" [[ -e $CONFIG_FILE ]] || die "\`$CONFIG_FILE' does not exist" [[ $CONFIG_FILE =~ (^|/)([a-zA-Z0-9_=+.-]{1,15})\.conf$ ]] || die "The config file must be a valid interface name, followed by .conf" CONFIG_FILE="$(readlink -f "$CONFIG_FILE")" ((($(stat -c '0%#a' "$CONFIG_FILE") & $(stat -c '0%#a' "${CONFIG_FILE%/*}") & 0007) == 0)) || echo "Warning: \`$CONFIG_FILE' is world accessible" >&2 INTERFACE="${BASH_REMATCH[2]}" shopt -s nocasematch while read -r line || [[ -n $line ]]; do stripped="${line%%\#*}" key="${stripped%%=*}"; key="${key##*([[:space:]])}"; key="${key%%*([[:space:]])}" value="${stripped#*=}"; value="${value##*([[:space:]])}"; value="${value%%*([[:space:]])}" [[ $key == "["* ]] && interface_section=0 [[ $key == "[Interface]" ]] && interface_section=1 if [[ $interface_section -eq 1 ]]; then case "$key" in Address) ADDRESSES+=( ${value//,/ } ); continue ;; MTU) MTU="$value"; continue ;; DNS) for v in ${value//,/ }; do [[ $v =~ (^[0-9.]+$)|(^.*:.*$) ]] && DNS+=( $v ) || DNS_SEARCH+=( $v ) done; continue ;; Table) TABLE="$value"; continue ;; PreUp) PRE_UP+=( "$value" ); continue ;; PreDown) PRE_DOWN+=( "$value" ); continue ;; PostUp) POST_UP+=( "$value" ); continue ;; PostDown) POST_DOWN+=( "$value" ); continue ;; esac fi WG_CONFIG+="$line"$'\n' done < "$CONFIG_FILE" shopt -u nocasematch } set_config() { cmd wg setconf "$INTERFACE" <(echo "$WG_CONFIG") } # Setup Interface and Address add_if() { local ret 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 echo "!] Missing WireGuard kernel module. Falling back to slow userspace implementation." >&2 cmd "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" "$INTERFACE" fi } del_if() { local table #[[ $HAVE_SET_DNS -eq 0 ]] || unset_dns #[[ $HAVE_SET_FIREWALL -eq 0 ]] || remove_firewall if [[ -z $TABLE || $TABLE == auto ]] && get_fwmark table && [[ $(wg show "$INTERFACE" allowed-ips) =~ /0(\ |$'\n'|$) ]]; then while [[ $(ip -4 rule show 2>/dev/null) == *"lookup $table"* ]]; do cmd ip -4 rule delete table $table done while [[ $(ip -4 rule show 2>/dev/null) == *"from all lookup main suppress_prefixlength 0"* ]]; do cmd ip -4 rule delete table main suppress_prefixlength 0 done while [[ $(ip -6 rule show 2>/dev/null) == *"lookup $table"* ]]; do cmd ip -6 rule delete table $table done while [[ $(ip -6 rule show 2>/dev/null) == *"from all lookup main suppress_prefixlength 0"* ]]; do cmd ip -6 rule delete table main suppress_prefixlength 0 done fi cmd ip link delete dev "$INTERFACE" } add_addr() { local proto=-4 [[ $1 == *:* ]] && proto=-6 cmd ip $proto address add "$1" dev "$INTERFACE" } set_mtu_up() { local mtu=0 endpoint output if [[ -n $MTU ]]; then cmd ip link set mtu "$MTU" up dev "$INTERFACE" return fi while read -r _ endpoint; do [[ $endpoint =~ ^\[?([a-z0-9:.]+)\]?:[0-9]+$ ]] || continue output="$(ip route get "${BASH_REMATCH[1]}" || true)" [[ ( $output =~ mtu\ ([0-9]+) || ( $output =~ dev\ ([^ ]+) && $(ip link show dev "${BASH_REMATCH[1]}") =~ mtu\ ([0-9]+) ) ) && ${BASH_REMATCH[1]} -gt $mtu ]] && mtu="${BASH_REMATCH[1]}" done < <(wg show "$INTERFACE" endpoints) if [[ $mtu -eq 0 ]]; then read -r output < <(ip route show default || true) || true [[ ( $output =~ mtu\ ([0-9]+) || ( $output =~ dev\ ([^ ]+) && $(ip link show dev "${BASH_REMATCH[1]}") =~ mtu\ ([0-9]+) ) ) && ${BASH_REMATCH[1]} -gt $mtu ]] && mtu="${BASH_REMATCH[1]}" fi [[ $mtu -gt 0 ]] || mtu=1500 cmd ip link set mtu $(( mtu - 80 )) up dev "$INTERFACE" } # Checks the active internet connection interface transport_interface() { local netiface netiface=$(ip route get ${EXTERNAL_NETWORK_TEST_IP} | grep -Po '(?<=dev\s)\w+' | cut -f1 -d ' ') if [ -z ${netiface} ]; then die "Unable to reach ${EXTERNAL_NETWORK_TEST_IP}. Check the Internet connection" exit 1 else echo "${netiface}" fi } # Setup DNS HAVE_SET_DNS=0 set_dns() { [[ ${#DNS[@]} -gt 0 ]] || return 0 #{ printf 'nameserver %s\n' "${DNS[@]}" # [[ ${#DNS_SEARCH[@]} -eq 0 ]] || printf 'search %s\n' "${DNS_SEARCH[*]}" #} | cmd resolvconf -a "$(resolvconf_iface_prefix)$INTERFACE" -m 0 -x cmd resolvectl dns ${INTERFACE} "${DNS[@]}" cmd resolvectl domain ${INTERFACE} "~." cmd resolvectl domain $(transport_interface) "lan" HAVE_SET_DNS=1 } unset_dns() { [[ ${#DNS[@]} -gt 0 ]] || return 0 cmd resolvectl domain $(transport_interface) "lan" cmd resolvectl domain $(transport_interface) "~." } # Setup Routes and Firewall add_route() { local proto=-4 [[ $1 == *:* ]] && proto=-6 [[ $TABLE != off ]] || return 0 if [[ -n $TABLE && $TABLE != auto ]]; then cmd ip $proto route add "$1" dev "$INTERFACE" table "$TABLE" elif [[ $1 == */0 ]]; then add_default "$1" else [[ -n $(ip $proto route show dev "$INTERFACE" match "$1" 2>/dev/null) ]] || cmd ip $proto route add "$1" dev "$INTERFACE" fi } get_fwmark() { local fwmark fwmark="$(wg show "$INTERFACE" fwmark)" || return 1 [[ -n $fwmark && $fwmark != off ]] || return 1 printf -v "$1" "%d" "$fwmark" return 0 } HAVE_SET_FIREWALL=0 add_default() { local table line 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++)) done cmd wg set "$INTERFACE" fwmark $table fi local proto=-4 iptables=iptables pf=ip [[ $1 == *:* ]] && proto=-6 iptables=ip6tables pf=ip6 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 local marker="-m comment --comment \"wg-quick(8) rule for $INTERFACE\"" restore=$'*raw\n' nftable="wg-quick-$INTERFACE" nftcmd printf -v nftcmd '%sadd table %s %s\n' "$nftcmd" "$pf" "$nftable" printf -v nftcmd '%sadd chain %s %s preraw { type filter hook prerouting priority -300; }\n' "$nftcmd" "$pf" "$nftable" printf -v nftcmd '%sadd chain %s %s premangle { type filter hook prerouting priority -150; }\n' "$nftcmd" "$pf" "$nftable" printf -v nftcmd '%sadd chain %s %s postmangle { type filter hook postrouting priority -150; }\n' "$nftcmd" "$pf" "$nftable" while read -r line; do [[ $line =~ .*inet6?\ ([0-9a-f:.]+)/[0-9]+.* ]] || continue printf -v restore '%s-I PREROUTING ! -i %s -d %s -m addrtype ! --src-type LOCAL -j DROP %s\n' "$restore" "$INTERFACE" "${BASH_REMATCH[1]}" "$marker" printf -v nftcmd '%sadd rule %s %s preraw iifname != "%s" %s daddr %s fib saddr type != local drop\n' "$nftcmd" "$pf" "$nftable" "$INTERFACE" "$pf" "${BASH_REMATCH[1]}" done < <(ip -o $proto addr show dev "$INTERFACE" 2>/dev/null) printf -v restore '%sCOMMIT\n*mangle\n-I POSTROUTING -m mark --mark %d -p udp -j CONNMARK --save-mark %s\n-I PREROUTING -p udp -j CONNMARK --restore-mark %s\nCOMMIT\n' "$restore" $table "$marker" "$marker" printf -v nftcmd '%sadd rule %s %s postmangle meta l4proto udp mark %d ct mark set mark \n' "$nftcmd" "$pf" "$nftable" $table printf -v nftcmd '%sadd rule %s %s premangle meta l4proto udp meta mark set ct mark \n' "$nftcmd" "$pf" "$nftable" [[ $proto == -4 ]] && cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1 if type -p nft >/dev/null; then cmd nft -f <(echo -n "$nftcmd") else echo -n "$restore" | cmd $iptables-restore -n fi HAVE_SET_FIREWALL=1 return 0 } remove_firewall() { if type -p nft >/dev/null; then local table nftcmd while read -r table; do [[ $table == *" wg-quick-$INTERFACE" ]] && printf -v nftcmd '%sdelete %s\n' "$nftcmd" "$table" done < <(nft list tables 2>/dev/null) [[ -z $nftcmd ]] || cmd nft -f <(echo -n "$nftcmd") fi if type -p iptables >/dev/null; then local line iptables found restore for iptables in iptables ip6tables; do restore="" found=0 while read -r line; do [[ $line == "*"* || $line == COMMIT || $line == "-A "*"-m comment --comment \"wg-quick(8) rule for $INTERFACE\""* ]] || continue [[ $line == "-A"* ]] && found=1 printf -v restore '%s%s\n' "$restore" "${line/#-A/-D}" done < <($iptables-save 2>/dev/null) [[ $found -ne 1 ]] || echo -n "$restore" | cmd $iptables-restore -n done fi } # Up/Down Functions cmd_up() { local i #[[ -z $(ip link show dev "$INTERFACE" 2>/dev/null) ]] || die "\`$INTERFACE' already exists" [[ -z $(ip link show dev "$INTERFACE" 2>/dev/null) ]] || cmd_down trap 'del_if; exit' INT TERM EXIT say "Starting UP the ${INTERFACE} interface ..." execute_hooks "${PRE_UP[@]}" add_if set_config for i in "${ADDRESSES[@]}"; do add_addr "$i" done set_mtu_up set_dns for i in $(while read -r _ i; do for i in $i; do [[ $i =~ ^[0-9a-z:.]+/[0-9]+$ ]] && echo "$i"; done; done < <(wg show "$INTERFACE" allowed-ips) | sort -nr -k 2 -t /); do add_route "$i" done execute_hooks "${POST_UP[@]}" trap - INT TERM EXIT sleep 3 } cmd_down() { say "Bringing DOWN the ${INTERFACE} interface ..." [[ " $(wg show interfaces) " == *" $INTERFACE "* ]] || die "$INTERFACE is not a WireGuard interface" execute_hooks "${PRE_DOWN[@]}" del_if [[ $HAVE_SET_DNS -eq 0 ]] || unset_dns [[ $HAVE_SET_FIREWALL -eq 0 ]] || remove_firewall #unset_dns || true #remove_firewall || true execute_hooks "${POST_DOWN[@]}" sleep 1 } # Main if [[ $# -eq 0 ]]; then cmd_usage exit 1 elif [[ $# -eq 1 ]]; then auto_su parse_options "$1" cmd_up while true; do clear say "wg-rapid by deflax :]" echo " " say "[q] to stop the VPN connection." say "[d] or close the terminal window to keep the VPN connection setup configured" say "[o] forward all networking through the VPN tunnel" say "[p] forward the predefined routes only through the VPN tunnel" echo " " wg show echo " " read -t 1 -N 1 input if [[ $input = "q" ]] || [[ $input = "Q" ]]; then echo break fi if [[ $input = "d" ]] || [[ $input = "D" ]]; then echo say "$PROGRAM detached." exit 0 fi if [[ $input = "p" ]] || [[ $input = "P" ]]; then cmd_down LINE_ROUTE_LOCAL=`grep -n 'Route only vpn trafic through vpn' /etc/wireguard/$1.conf | cut -d ':' -f 1` ((LINE_ROUTE_LOCAL=LINE_ROUTE_LOCAL+1)) LINE_ROUTE_ALL=`grep -n 'Route ALL traffic through vpn' /etc/wireguard/$1.conf | cut -d ':' -f 1` ((LINE_ROUTE_ALL=LINE_ROUTE_ALL+1)) sed -i "${LINE_ROUTE_LOCAL} s/^##*//" /etc/wireguard/$1.conf sed -i "${LINE_ROUTE_ALL} s/^/#/" /etc/wireguard/$1.conf parse_options "$1" cmd_up fi if [[ $input = "o" ]] || [[ $input = "O" ]]; then cmd_down sleep 1 LINE_ROUTE_LOCAL=`grep -n 'Route only vpn trafic through vpn' /etc/wireguard/$1.conf | cut -d ':' -f 1` ((LINE_ROUTE_LOCAL=LINE_ROUTE_LOCAL+1)) LINE_ROUTE_ALL=`grep -n 'Route ALL traffic through vpn' /etc/wireguard/$1.conf | cut -d ':' -f 1` ((LINE_ROUTE_ALL=LINE_ROUTE_ALL+1)) sed -i "${LINE_ROUTE_LOCAL} s/^/#/" /etc/wireguard/$1.conf sed -i "${LINE_ROUTE_ALL} s/^##*//" /etc/wireguard/$1.conf parse_options "$1" cmd_up fi done cmd_down elif [[ $# -eq 2 ]]; then auto_su parse_options "$1" if [ "$2" == "down" ]; then cmd_down exit 0 else say "$PROGRAM [ CONFIG_NAME ] [ down ]" fi fi exit 0