路由和网络命名空间集成

与所有 Linux 网络接口一样,WireGuard 集成到网络命名空间基础架构中。这意味着管理员可以拥有几个完全不同的网络子系统,并选择每个子系统中的接口。

WireGuard 做了一些非常有趣的事情。当创建 WireGuard 接口时(使用ip link add wg0 type wireguard),它会记住创建它的命名空间。“我是在命名空间 A 中创建的。”稍后,WireGuard 可以移动到新的命名空间(“我正在移动到命名空间 B。”),但它仍然会记住它起源于命名空间 A。

WireGuard 使用 UDP 套接字来实际发送和接收加密数据包。此套接字始终位于命名空间 A(原始出生地命名空间)中。这允许一些非常酷的特性。也就是说,您可以在一个命名空间 (A) 中创建 WireGuard 接口,将其移动到另一个命名空间 (B),并让从命名空间 B 发送的明文数据包通过命名空间 A 中的 UDP 套接字以加密形式发送。

(请注意,同样的技术也适用于基于用户空间 TUN 的接口,通过在一个命名空间中创建一个套接字文件描述符,然后切换到另一个命名空间并保持前一个命名空间的文件描述符打开。)

这开辟了一些非常好的可能性。

普通容器化

最明显的用途是给容器(例如 Docker 容器)一个 WireGuard 接口作为其唯一接口。

container # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN group default qlen 1
    link/none 
    inet 192.168.4.33/32 scope global wg0
       valid_lft forever preferred_lft forever

这里,唯一可能访问网络的方式是通过wg0WireGuard 接口。

容器网络命名空间图

完成此类设置的方法如下:

首先我们创建名为“容器”的网络命名空间:

# ip netns add container

接下来,我们在“init”(原始)命名空间中创建一个 WireGuard 接口:

# ip link add wg0 type wireguard

最后,我们将该接口移到新的命名空间中:

# ip link set wg0 netns container

现在我们可以像往常一样配置 wg0,但我们需要指定它的新命名空间:

# ip -n container addr add 192.168.4.33/32 dev wg0
# ip netns exec container wg setconf wg0 /etc/wireguard/wg0.conf
# ip -n container link set wg0 up
# ip -n container route add default dev wg0

瞧,现在访问“容器”的任何网络资源的唯一方式是通过 WireGuard 接口。

请注意,Docker 用户可以指定 Docker 进程的 PID 而不是网络命名空间名称,以使用 Docker 已为其容器创建的网络命名空间:

# ip link set wg0 netns 879

路由所有流量

一个不太明显但非常强大的用途是利用 WireGuard 的这一特性将所有普通互联网流量重定向到 WireGuard。但首先,让我们回顾一下执行此操作的常用解决方案:

经典解决方案

经典解决方案依赖于不同类型的路由表配置。对于所有这些,我们需要为实际的 WireGuard 端点设置一些显式路由。对于这些示例,我们假设 WireGuard 端点是demo.wireguard.com,截至撰写本文时,它解析为。此外,我们假设我们通常使用和 的经典网关163.172.161.0连接到互联网eth0192.168.1.1

替换默认路由

最直接的技术就是替换默认路由,但为 WireGuard 端点添加一条明确的规则:

# ip route del default
# ip route add default dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

这个方法是可行的,而且相对简单,但不幸的是,DHCP 守护进程等喜欢撤消我们刚刚执行的操作。

覆盖默认路由

因此,我们不必替换默认路由,而是可以用两个更具体的规则覆盖它,这两个规则加起来等于默认路由,但在默认路由之前匹配:

# ip route add 0.0.0.0/1 dev wg0
# ip route add 128.0.0.0/1 dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

这样,我们就不会破坏默认路由。这种方法也很好用,但不幸的是,当 eth0 启动和关闭时,显式路由demo.wireguard.com将被遗忘,这很烦人。

基于规则的路由

有些人更喜欢使用基于规则的路由和多个路由表。其工作方式是,我们为 WireGuard 路由创建一个路由表,为纯文本 Internet 路由创建一个路由表,然后添加规则来确定每个路由表使用哪个路由表:

# ip rule add to 163.172.161.0 lookup main pref 30
# ip rule add to all lookup 80 pref 40
# ip route add default dev wg0 table 80

现在,我们能够将路由表分开。不幸的是,缺点是仍然需要添加显式端点规则,并且删除接口时没有清理,现在需要复制更复杂的路由规则。

改进的基于规则的路由

先前的解决方案依赖于我们知道应该免于隧道的显式端点 IP,但 WireGuard 端点可以漫游,这意味着此规则可能会失效。幸运的是,我们能够fwmark在所有从 WireGuard 的 UDP 套接字发出的数据包上设置一个,然后这些数据包将免于隧道:

# wg set wg0 fwmark 1234
# ip route add default dev wg0 table 2468
# ip rule add not fwmark 1234 table 2468
# ip rule add table main suppress_prefixlength 0

我们首先在接口上设置fwmark,并在备用路由表上设置默认路由。然后我们指示没有 的数据包应该fwmark转到这个备用路由表。最后,我们添加了一个仍然访问本地网络的便利功能,即fwmark如果它与其中任何前缀长度大于零的路由(例如非默认本地路由)匹配,我们允许没有 的数据包使用主路由表,而不是 WireGuard 接口的路由表。这是该wg-quick(8)工具使用的技术。

改进经典解决方案

WireGuard 的作者们有兴趣在内核中添加一项名为“notoif”的功能,以涵盖隧道用例。这将允许接口说“不要使用我自己作为接口路由此数据包,以避免路由循环”。WireGuard 将能够添加一行.flowi4_not_oif = wg0_idx,例如,基于用户空间tun的接口将能够在其传出套接字上设置一个选项,例如setsockopt(fd, SO_NOTOIF, tun0_idx);。不幸的是,这还没有合并,但你可以在这里阅读 LKML 线程

新的命名空间解决方案

事实证明,我们可以使用网络命名空间通过 WireGuard 路由所有互联网流量,而不是使用传统的路由表黑客技术。其工作原理是,我们将连接到互联网的接口(如eth0wlan0)移动到命名空间(我们称之为“物理”),然后让 WireGuard 接口成为“init”命名空间中的唯一接口。

物理网络命名空间图

首先我们创建“物理”网络命名空间:

# ip netns add physical

现在我们进入eth0wlan0物理”命名空间:

# ip link set eth0 netns physical
# iw phy phy0 set netns name physical

(请注意,必须使用iw并通过指定物理设备来移动无线设备phy0。)

我们现在在“physical”命名空间中拥有这些接口,而在“init”命名空间中没有任何接口:

# ip -n physical link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000
    link/ether ab:cd:ef:g1:23:45 brd ff:ff:ff:ff:ff:ff
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT group default qlen 1000
    link/ether 01:23:45:67:89:ab brd ff:ff:ff:ff:ff:ff

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

现在我们直接在“物理”命名空间中添加一个 WireGuard 接口:

# ip -n physical link add wg0 type wireguard

的出生地命名空间wg0现在是“物理”命名空间,这意味着密文 UDP 套接字将被分配给eth0和等设备wlan0。我们现在可以进入wg0“init”命名空间;但是,它仍然会记住套接字的出生地。

# ip -n physical link set wg0 netns 1

我们将“1”指定为“init”命名空间,因为这是系统上第一个进程的 PID。现在“init”命名空间有设备wg0

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/none

我们现在可以使用普通工具配置物理设备,但我们在“物理”网络命名空间内启动它们:

# ip netns exec physical dhcpcd wlan0
# ip netns exec physical wpa_supplicant -iwlan0 -c/etc/wpa_supplicant/wpa_supplicant.conf
# ip -n physical addr add 192.168.12.52/24 dev eth0

等等。最后,我们可以wg0像平常一样配置接口,并将其设置为默认路由:

# wg setconf wg0 /etc/wireguard/wg0.conf
# ip addr add 10.2.4.5/32 dev wg0
# ip link set wg0 up
# ip route add default dev wg0

完成!此时,系统上的所有普通进程都将通过“init”命名空间路由其数据包,该命名空间仅包含接口wg0wg0路由。但是,wg0其 UDP 套接字位于“物理”命名空间中,这意味着它将从eth0或发送流量wlan0。普通进程甚至不会知道eth0wlan0,除了dhcpcdwpa_supplicant,它们是在“物理”命名空间内生成的。

但是,有时您可能希望使用“物理”命名空间快速打开网页​​或执行某些操作。例如,也许您计划像往常一样通过 WireGuard 路由所有流量,但您所在的咖啡店要求您使用网站进行身份验证,然后才能为您提供真正的互联网链接。因此,您可以使用“物理”接口执行选择进程(以本地用户身份):

$ sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) chromium

这当然可以做成一个很好的功能.bashrc

physexec() { sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) "$@"; }

现在您可以编写以下内容以chromium在“物理”命名空间中打开。

$ physexec chromium

当您登录完咖啡店网络后,照常打开浏览器,就可以放心地浏览,因为您知道您的所有流量都受到 WireGuard 的保护:

$ chromium

示例脚本

以下示例脚本可以保存为/usr/local/bin/wgphys并用于wgphys upwgphys down和等命令wgphys exec

#!/bin/bash
set -ex

[[ $UID != 0 ]] && exec sudo -E "$(readlink -f "$0")" "$@"

up() {
    killall wpa_supplicant dhcpcd || true
    ip netns add physical
    ip -n physical link add wgvpn0 type wireguard
    ip -n physical link set wgvpn0 netns 1
    wg setconf wgvpn0 /etc/wireguard/wgvpn0.conf
    ip addr add 192.168.4.33/32 dev wgvpn0
    ip link set eth0 down
    ip link set wlan0 down
    ip link set eth0 netns physical
    iw phy phy0 set netns name physical
    ip netns exec physical dhcpcd -b eth0
    ip netns exec physical dhcpcd -b wlan0
    ip netns exec physical wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
    ip link set wgvpn0 up
    ip route add default dev wgvpn0
}

down() {
    killall wpa_supplicant dhcpcd || true
    ip -n physical link set eth0 down
    ip -n physical link set wlan0 down
    ip -n physical link set eth0 netns 1
    ip netns exec physical iw phy phy0 set netns 1
    ip link del wgvpn0
    ip netns del physical
    dhcpcd -b eth0
    dhcpcd -b wlan0
    wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
}

execi() {
    exec ip netns exec physical sudo -E -u \#${SUDO_UID:-$(id -u)} -g \#${SUDO_GID:-$(id -g)} -- "$@"
}

command="$1"
shift

case "$command" in
    up) up "$@" ;;
    down) down "$@" ;;
    exec) execi "$@" ;;
    *) echo "Usage: $0 up|down|exec" >&2; exit 1 ;;
esac

上面的一个小演示:

wgphys 命令