401 lines
13 KiB
Text
401 lines
13 KiB
Text
|
#!/bin/bash
|
||
|
# SPDX-License-Identifier: GPL-2.0
|
||
|
#
|
||
|
# Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||
|
# Copyright (C) 2021-2022 Daniel afx <daniel@deflax.net>
|
||
|
#
|
||
|
|
||
|
#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 afx. ver.2204"
|
||
|
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
|