rants, tirades, ruminations
wireless bridge with proxy ARP, nftables, WireGuard, a network namespace and veth pair posted Sun, 12 Mar 2023 12:56:43 UTC
I recently had to move my home NAS device onto a wireless connection. In doing so, I wanted to maintain my use of a network namespaces and veth pair to isolate all of my WireGuard VPN traffic. However, under Linux, setting up what is effectively a bridged connection on a wireless interface has never been as easy as some other operating systems. As I’m not sure when I might finally be able to get a decent wired connection hooked back up to my NAS, it came time to figure out how to make all of this work together.
The starting point was the Bridging Network Connections with Proxy ARP page on the Debian wiki. It had the very basics of getting this set up, but was missing most of the details. After messing with things a bit, I finally ended up with a working configuration. Let’s start with the very basics covered on the wiki. Since I’m old and crotchety, I’m still using /etc/sysctl.conf
directly:
net.ipv4.ip_forward=1
net.ipv4.conf.all.proxy_arp=1
My motherboard happened to come with an Intel AX200 wireless interface. I’m currently using wpa_supplicant via systemd to statically configure my device for my home wireless network. I also have iwd currently installed although disabled, so my wireless interface’s normal name is being overridden to wlan0
thanks to the iwd package dropping /lib/systemd/network/80-iwd.link
in place. When configuring wpa_supplicant@wlan0.service
in systemd, it’s looking for the corresponding configuration file at /etc/wpa_supplicant/wpa_supplicant-wlan0.conf
:
network={
ssid='Super Secret SSID'
bssid=12:34:56:78:9a:bc
key_mgmt=SAE
sae_password="super secret password"
ieee80211w=2
}
I had to enable that service obviously with systemctl enable wpa_supplicant@wlan0.service
since it isn’t configured by default.
Next up was to configure the interface itself. As previously mentioned, since I’m an old man, I’m also still using /etc/network/interfaces
, which contains the following relevant section now:
allow-hotplug wlan0
iface wlan0 inet static
address 192.168.99.2
gateway 192.168.99.1
netmask 255.255.255.0
post-up ip netns add vpn
post-up ip link add veth.host type veth peer veth.vpn
post-up ip link set dev veth.host up
post-up ip link set veth.vpn netns vpn up
post-up ip -n vpn address add 192.168.99.3/24 dev veth.vpn
post-up ip route add 192.168.99.3/32 dev veth.host
post-up ip link add wg1 type wireguard
post-up ip link set wg1 netns vpn
post-up ip -n vpn -4 address add 172.24.0.10/32 dev wg1
post-up ip netns exec vpn wg setconf wg1 /etc/wireguard/wg1.conf
post-up ip -n vpn link set wg1 up
post-up ip -n vpn route add default dev wg1
post-up ip netns exec vpn nft -f /etc/nftables-vpn.conf
The DHCP range starts higher up in my LAN, so I’m using the first couple of addresses at the bottom of the network after my gateway device for my NAS and this separate, VPN only network namespace. The WireGuard interface is configured within the namespace using the appropriate static address as supplied by my VPN provider. I have a separate WireGuard wg0
already configured outside the namespace, so wg1
is what we’re using inside of it.
And the final piece listed their at the end of the interface setup loads in my firewall rules for the namespace from /etc/nftables-vpn.conf
:
# VPN firewall
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# established/related connections
ct state established,related accept
# invalid connections
ct state invalid drop
# loopback interface
iif lo accept
# ICMP (routers may also want: mld-listener-query, nd-router-solicit)
#ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, echo-reply, echo-request, nd-neighbor-advert, nd-neighbor-solicit, nd-router-advert, packet-too-big, parameter-problem, time-exceeded } accept
ip protocol icmp icmp type { destination-unreachable, echo-reply, echo-request, parameter-problem, router-advertisement, source-quench, time-exceeded } accept
# services
iif veth.vpn tcp dport 9091 accept # Transmission
iif veth.vpn tcp dport 9117 accept # Jackett
iifname wg1 tcp dport { 49152-65535 } accept # Transmission
}
chain output {
type filter hook output priority filter; policy drop;
# explicitly allow my DNS traffic without VPN
skuid nipsy ip daddr 192.168.99.1 tcp dport domain accept
skuid nipsy ip daddr 192.168.99.1 udp dport domain accept
# explicitly allow my Transmission or Jackett RPC traffic without VPN
oifname veth.vpn skuid nipsy tcp sport 9091 accept
oifname veth.vpn skuid nipsy tcp sport 9117 accept
# allow any traffic out through VPN
oifname wg1 accept
# drop everything else
counter drop
}
chain forward {
type filter hook forward priority filter; policy drop;
}
}
Both Jackett and Transmssion are configured via pretty basic systemd service files you might find anywhere else, with the notable exception that both include the following in their [Service]
section:
NetworkNamespacePath=/run/netns/vpn
which seems to do the trick for running them in the namespace correctly.
Lastly, if you create the namespace before setting the kernel parameters above via sysctl, the namespace will not inherit those settings. You’ll probably need to either restart or delete and recreate the namespace for those values to be inherited properly. I’m honestly not sure if they’re specifically relevant within the context of the namespace itself since I think they only apply to what’s happening at the host level directly on wlan0
, but it’s worth mentioning if everything else looks right and it’s still not working for you. Best of luck!