291 lines
7.2 KiB
Bash
Executable file
291 lines
7.2 KiB
Bash
Executable file
#!/usr/bin/bash
|
|
# vi:set ft=bash ts=4 sw=4 noet noai:
|
|
|
|
# configuration matches the wg-quick specifications
|
|
|
|
# manual invocation:
|
|
# $ sudo wg-netns vpn-1
|
|
# via systemd:
|
|
# $ sudo systemctl start wg-netns@vpn-1.service
|
|
|
|
# examples:
|
|
# show tunnel statistics
|
|
# $ sudo ip netns exec vpn wg
|
|
|
|
# create launch function
|
|
# $ vpn() { sudo -E ip netns exec vpn setpriv --reuid $(id -u) --regid $(id -g) --clear-groups --reset-env "$@"; }
|
|
|
|
# launch firefox in namespace
|
|
# $ vpn firefox
|
|
|
|
# use firejail instead
|
|
# $ firejail --quiet --noprofile --netns=vpn firefox
|
|
|
|
# or netns
|
|
# $ sudo nsenter --net=/run/netns/vpn-de setpriv --reuid $(id -u) --regid $(id -g) --clear-groups --reset-env firefox
|
|
|
|
set -e -o pipefail
|
|
shopt -s extglob
|
|
shopt -s nullglob
|
|
export LC_ALL=C
|
|
|
|
SELF="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
export PATH="${SELF%/*}:$PATH"
|
|
|
|
WG_CONFIG=""
|
|
INTERFACE=""
|
|
ADDRESSES=( )
|
|
MTU=""
|
|
DNS=( )
|
|
TABLE=""
|
|
PRE_UP=( )
|
|
POST_UP=( )
|
|
PRE_DOWN=( )
|
|
POST_DOWN=( )
|
|
SAVE_CONFIG=0
|
|
CONFIG_FILE=""
|
|
PROGRAM="${0##*/}"
|
|
ARGS=( "$@" )
|
|
|
|
MARK=""
|
|
MTU=1408
|
|
NAMESPACE=vpn
|
|
|
|
die() {
|
|
printf "%s\n" "$PROGRAM: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
auto_su() {
|
|
[[ $UID == 0 ]] || exec sudo -p "$PROGRAM must be run as root. Please enter the password for %u to continue: " -- "$BASH" -- "$SELF" "${ARGS[@]}"
|
|
}
|
|
|
|
parse_options() {
|
|
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)) || printf "Warning: %s is world accessible\n" "$CONFIG_FILE" >&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 ;;
|
|
Namespace) NAMESPACE=$value; continue ;;
|
|
PreUp) PRE_UP+=( "$value" ); continue ;;
|
|
PreDown) PRE_DOWN+=( "$value" ); continue ;;
|
|
PostUp) POST_UP+=( "$value" ); continue ;;
|
|
PostDown) POST_DOWN+=( "$value" ); continue ;;
|
|
SaveConfig) read_bool SAVE_CONFIG "$value"; continue ;;
|
|
esac
|
|
fi
|
|
WG_CONFIG+="$line"$'\n'
|
|
done < "$CONFIG_FILE"
|
|
shopt -u nocasematch
|
|
}
|
|
|
|
read_bool() {
|
|
case "$2" in
|
|
true) printf -v "$1" 1 ;;
|
|
false) printf -v "$1" 0 ;;
|
|
*) die "\`$2' is neither true nor false"
|
|
esac
|
|
}
|
|
|
|
cmd() {
|
|
printf "[#] %s\n" "$*" >&2
|
|
"$@"
|
|
}
|
|
|
|
cmd_add_ns () {
|
|
if ! ip netns | grep "$NAMESPACE"; then
|
|
cmd ip netns add "$NAMESPACE";
|
|
cmd ip -n "$NAMESPACE" link set dev lo up
|
|
fi
|
|
}
|
|
|
|
cmd_del_ns () {
|
|
cmd ip netns del "$NAMESPACE"
|
|
}
|
|
|
|
option_ns () {
|
|
printf -- "-netns %s" "$NAMESPACE"
|
|
}
|
|
|
|
exec_ns () {
|
|
printf "ip netns exec %s\n" "$NAMESPACE"
|
|
}
|
|
|
|
add_addr() {
|
|
local proto=-4
|
|
[[ $1 == *:* ]] && proto=-6
|
|
cmd $(exec_ns) ip $proto address add "$1" dev "$INTERFACE"
|
|
}
|
|
|
|
execute_hooks() {
|
|
local hook
|
|
for hook in "$@"; do
|
|
hook="${hook//%i/$INTERFACE}"
|
|
echo "[#] $hook" >&2
|
|
(eval "$hook")
|
|
done
|
|
}
|
|
|
|
is_up() {
|
|
cmd ip $(option_ns) -4 -o -br link show $INTERFACE &>/dev/null
|
|
}
|
|
|
|
get_fwmark() {
|
|
local fwmark
|
|
fwmark="$(cmd $(exec_ns) wg show $INTERFACE fwmark)" || return 1
|
|
[[ -n $fwmark && $fwmark != off ]] || return 1
|
|
printf -v "$1" "%d" "$fwmark"
|
|
return 0
|
|
}
|
|
|
|
add_dns() {
|
|
if [[ ! -f "/etc/netns/${NAMESPACE}/resolv.conf" ]]; then
|
|
local ns=/run/wg-netns/${NAMESPACE}
|
|
cmd mkdir -p "$ns"
|
|
if [[ -w $ns ]]; then
|
|
cmd touch "${ns}/resolv.conf"
|
|
|
|
(( ${#DNS[@]} > 0 )) && cmd printf 'nameserver %s\n' "${DNS[@]}" | \
|
|
tee >(awk '{print "[#] echo \47"$0"\47 >> '"${ns}"'/resolv.conf"}' >&2) \
|
|
> "${ns}/resolv.conf"
|
|
|
|
anti_leak() {
|
|
cmd cp /etc/nsswitch.conf "${ns}/nsswitch.conf"
|
|
#cmd sed -i 's/\(hosts\: .*\) resolve \(\[.*\]\)\?\(.*\)$/\1\3/ g' "${ns}/nsswitch.conf"
|
|
cmd sed -i 's/ resolve \(\[\!UNAVAIL=return\]\)\?//g; /^[[:blank:]]*#/d' "${ns}/nsswitch.conf"
|
|
}
|
|
anti_leak
|
|
cmd mkdir -p /etc/netns
|
|
cmd ln -sf "${ns}" "/etc/netns/${NAMESPACE}"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
del_dns() {
|
|
if [[ -L /etc/netns/${NAMESPACE} ]]; then
|
|
{
|
|
cmd unlink "/etc/netns/${NAMESPACE}"
|
|
cmd rm -f "/run/wg-netns/${NAMESPACE}/"{resolv,nsswitch}.conf; \
|
|
cmd rmdir /etc/netns "/run/wg-netns/${NAMESPACE}" /run/wg-netns
|
|
} || true
|
|
fi
|
|
}
|
|
|
|
tunnel_exec() {
|
|
sudo -E $(exec_ns) setpriv --reuid $(id -u) --regid $(id -g) --clear-groups --reset-env "$@"
|
|
}
|
|
|
|
teardown() {
|
|
{
|
|
execute_hooks "${PRE_DOWN[@]}"
|
|
del_dns
|
|
# seems necessary if an error is encountered before moving the interface to the namespace
|
|
cmd ip link delete dev $INTERFACE
|
|
cmd ip $(option_ns) link delete dev $INTERFACE
|
|
cmd $(exec_ns) nft delete table inet wgfilter
|
|
cmd_del_ns
|
|
execute_hooks "${POST_DOWN[@]}"
|
|
} || true
|
|
}
|
|
|
|
_term() {
|
|
trap - EXIT
|
|
printf "\nCaught signal! - Cleaning up.\n" >&2
|
|
teardown
|
|
}
|
|
|
|
setup() {
|
|
if is_up; then
|
|
printf "Found existing interface, tearing down %s.\n" $INTERFACE >&2
|
|
teardown
|
|
fi
|
|
teardown 2>/dev/null
|
|
|
|
execute_hooks "${PRE_UP[@]}"
|
|
|
|
cmd ip link add $INTERFACE type wireguard
|
|
cmd_add_ns
|
|
cmd ip link set $INTERFACE netns "$NAMESPACE"
|
|
cmd $(exec_ns) wg setconf $INTERFACE <(printf "%s\n" "$WG_CONFIG")
|
|
|
|
local table
|
|
if ! get_fwmark table; then
|
|
table=51820
|
|
while [[ -n $(cmd ip $(option_ns) -4 route show table $table 2>/dev/null) \
|
|
|| -n $(cmd ip $(option_ns) -6 route show table $table 2>/dev/null) ]]; do
|
|
((table++))
|
|
done
|
|
cmd $(exec_ns) wg set "$INTERFACE" fwmark $table
|
|
fi
|
|
|
|
cmd $(exec_ns) ip link set group $table $INTERFACE
|
|
|
|
firewall() {
|
|
cmd $(exec_ns) nft -f - <<-EOT
|
|
table inet wgfilter {
|
|
chain output {
|
|
type filter hook output priority filter; policy accept;
|
|
oifgroup != $table meta mark != $table fib daddr type != local counter drop
|
|
}
|
|
chain input {
|
|
type filter hook input priority filter; policy drop;
|
|
iif "lo" accept
|
|
iifgroup $table ct state established counter accept
|
|
}
|
|
chain forward { type filter hook forward priority filter; policy drop; }
|
|
}
|
|
EOT
|
|
}
|
|
|
|
for i in "${ADDRESSES[@]}"; do
|
|
add_addr "$i"
|
|
done
|
|
|
|
cmd $(exec_ns) ip link set up dev $INTERFACE
|
|
cmd $(exec_ns) ip link set mtu $MTU up dev $INTERFACE
|
|
cmd $(exec_ns) ip route add default dev $INTERFACE
|
|
cmd $(exec_ns) ip -6 route add default dev $INTERFACE
|
|
|
|
cmd $(exec_ns) sysctl -q net.ipv4.conf.all.src_valid_mark=1 \
|
|
net.ipv4.conf.all.rp_filter=2 \
|
|
net.ipv4.conf.$INTERFACE.rp_filter=2 \
|
|
net.ipv4.ip_forward=0 \
|
|
net.ipv4.ping_group_range="0 2147483647"
|
|
|
|
add_dns
|
|
firewall
|
|
|
|
execute_hooks "${POST_UP[@]}"
|
|
|
|
printf "INTERFACE: %s - NAMESPACE: %s - MTU: %u - MARK: %u\n" \
|
|
$INTERFACE "$NAMESPACE" $MTU $table >&2
|
|
}
|
|
|
|
trap _term EXIT
|
|
|
|
auto_su
|
|
(($# < 1)) && set -- wg0
|
|
parse_options "$1"
|
|
|
|
setup
|
|
|
|
sleep infinity &
|
|
wait
|