Skip to content

Creating a network namespace that uses wireguard vpn

We will setup a network namespace that uses a wireguard vpn. IPv4 and IPv6 will be routed through the vpn. But it can also be used for IPv4 only or IPv6 only.

Additionally we want to access an service in the namespace from the host.

Bash Script

#!/usr/bin/env bash

set -euo pipefail

NAMESPACE="vpn"
WG_INTERFACE="wg0"
WG_CONFIG="/etc/wireguard/wg0.conf"
WG_IPV4="10.65.186.143/32"
WG_IPV6="fc00:bbbb:bbbb:bb01::2:ba8e/128"
VETH_TO_NS_IP="10.0.187.4/31"
VETH_TO_NS_IPV6="fd00:0:187::4/127"
VETH_FROM_NS_IP="10.0.187.5/31"
VETH_FROM_NS_IPV6="fd00:0:187::5/127"
NAMESERVER="10.64.0.1"
PODMAN_CONTAINER_PORT="8112"
IPTABLES_NAT_PORT="8112"
HOME_SUBNET="192.168.2.0/23"

log() {
    echo "$(date +"%Y-%m-%d %H:%M:%S") - $1"
}

# Process command-line options
while [[ $# -gt 0 ]]; do
    case "$1" in
        -v|--verbose)
            VERBOSE=true
            set -x
            ;;
        *)
            echo "Unknown option: $1"
            exit 1
            ;;
    esac
    shift
done

# Create network namespace
if ! ip netns list | grep -q "$NAMESPACE"; then
    log "Creating network namespace: $NAMESPACE"
    ip netns add "$NAMESPACE"
    ip -n "$NAMESPACE" addr add 127.0.0.1/8 dev lo
    ip -n "$NAMESPACE" addr add ::1/128 dev lo
    ip -n "$NAMESPACE" link set lo up
else
    log "Network namespace $NAMESPACE already exists."
fi

# Create WireGuard interface
if ! ip -n "$NAMESPACE" link show "$WG_INTERFACE" &> /dev/null; then
    log "Creating WireGuard interface: $WG_INTERFACE"
    ip link add "$WG_INTERFACE" type wireguard
    ip link set "$WG_INTERFACE" netns "$NAMESPACE"
    ip netns exec "$NAMESPACE" wg setconf "$WG_INTERFACE" <(wg-quick strip "$WG_CONFIG")
    ip -n "$NAMESPACE" address add "$WG_IPV4" dev "$WG_INTERFACE"
    ip -n "$NAMESPACE" address add "$WG_IPV6" dev "$WG_INTERFACE"
    ip -n "$NAMESPACE" link set "$WG_INTERFACE" up
else
    log "WireGuard interface $WG_INTERFACE already exists."
fi

# Add default route for WireGuard interface if it does not exist
if ! ip -n "$NAMESPACE" route show | grep -q "default dev $WG_INTERFACE"; then
    log "Adding default route for WireGuard interface"
    ip -n "$NAMESPACE" route add default dev "$WG_INTERFACE"
fi

if ! ip -n "$NAMESPACE" -6 route show | grep -q "default dev $WG_INTERFACE"; then
    log "Adding IPv6 default route for WireGuard interface"
    ip -n "$NAMESPACE" -6 route add default dev "$WG_INTERFACE"
fi


# Configure nameserver
log "Configuring nameserver: $NAMESERVER"
echo "nameserver $NAMESERVER" > "/etc/netns/$NAMESPACE/resolv.conf"

# Create veth pair
if ! ip link show "to-ns-$NAMESPACE" &> /dev/null; then
    log "Creating veth pair: to-ns-$NAMESPACE and from-ns-$NAMESPACE"
    ip link add "to-ns-$NAMESPACE" type veth peer name "from-ns-$NAMESPACE" netns "$NAMESPACE"
    ip address add "$VETH_TO_NS_IP" dev "to-ns-$NAMESPACE"
    ip address add "$VETH_TO_NS_IPV6" dev "to-ns-$NAMESPACE"
    ip link set "to-ns-$NAMESPACE" up
    ip -n "$NAMESPACE" address add "$VETH_FROM_NS_IP" dev "from-ns-$NAMESPACE"
    ip -n "$NAMESPACE" address add "$VETH_FROM_NS_IPV6" dev "from-ns-$NAMESPACE"
    ip -n "$NAMESPACE" link set "from-ns-$NAMESPACE" up
else
    log "Veth pair to-ns-$NAMESPACE and from-ns-$NAMESPACE already exists."
fi

# Add route to home network
if ! ip -n "$NAMESPACE" route show | grep -q "$HOME_SUBNET"; then
   log "Adding route to home subnet"
   IFS='/' read -r IP _ <<< "$VETH_TO_NS_IP"
   ip -n "$NAMESPACE" route add "$HOME_SUBNET" via "$IP"
fi

# Enable IP forwarding in the host
log "Enabling IP forwarding in the host"
echo 1 > /proc/sys/net/ipv4/ip_forward
echo 1 > /proc/sys/net/ipv6/conf/all/forwarding

log "Setting up port forwarding from the host to the Podman container"

# Extract only the IP address from the CIDR notation
IFS='/' read -r IP _ <<< "$VETH_FROM_NS_IP"
iptables-nft -t nat -I PREROUTING -p tcp --dport "$IPTABLES_NAT_PORT" -j DNAT --to-destination "$IP:$PODMAN_CONTAINER_PORT"
iptables-nft -I FORWARD -d "$IP" -p tcp --dport "$PODMAN_CONTAINER_PORT" -j ACCEPT

IFS='/' read -r IP6 _ <<< "$VETH_FROM_NS_IPV6"
ip6tables-nft -t nat -I PREROUTING -p tcp --dport "$IPTABLES_NAT_PORT" -j DNAT --to-destination "$IP6:$PODMAN_CONTAINER_PORT"
ip6tables-nft -I FORWARD -d "$IP6" -p tcp --dport "$PODMAN_CONTAINER_PORT" -j ACCEPT

Systemd Unit

Put into /etc/systemd/system/ns-vpn-setup.service

[Unit]
Description=service to setup vpn namespace
After=network-online.target nss-lookup.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/ns-vpn-setup
RemainAfterExit=true
StandardOutput=journal

[Install]
WantedBy=multi-user.target

Run the service

systemctl daemon-reload
systemctl start ns-vpn-setup

Tests

Verify the wireguard interface

# show the interface
ip -n vpn addr show wg0
ip -n vpn route show

# ping cloudflare
ip netns exec vpn ping 1.1.1.1
ip netns exec vpn ping 2606:4700:4700::1111

Test the connection between the namespaces:

ping -c 1 10.0.187.5
ping -c 1 fd00:0:187::5

ip netns exec vpn ping -c 1 10.0.187.4
ip netns exec vpn ping -c 1 fd00:0:187::4

Test DNS Server

# verify resolving
ip netns exec vpn curl -4 https://ipv4.ipleak.net/json/
ip netns exec vpn curl -6 https://ipv6.ipleak.net/json/

# show which dns server is actually used
session=$(openssl rand -hex 20)
random=$(openssl rand -hex 10)
ip netns exec vpn curl -4 -s "https://$session-$random.ipleak.net/dnsdetection/"
ip netns exec vpn curl -6 -s "https://$session-$random.ipleak.net/dnsdetection/"

Example Podman Pod

Quadlet file in /etc/containers/systemd/deluge.kube

Notice the After and Network directives.

[Install]
WantedBy=default.target

[Unit]
After=ns-vpn-setup.service

[Kube]
Yaml=/opt/container/deluge/deluge.kube.yaml
Network=ns:/var/run/netns/vpn

[Service]
# Restart service when sleep finishes
Restart=always
# Extend Timeout to allow time to pull the image
TimeoutStartSec=900