仅通过 OpenVPN 为特定网络命名空间提供所有流量

仅通过 OpenVPN 为特定网络命名空间提供所有流量

我正在尝试设置一个 VPN(使用 OpenVPN),以便所有流量,并且仅有的进出特定进程的流量通过 VPN;其他进程应该继续直接使用物理设备。据我了解,在 Linux 中执行此操作的方法是使用网络命名空间。

如果我正常使用 OpenVPN(即漏斗全部来自客户端通过 VPN 的流量),它工作正常。具体来说,我是这样启动 OpenVPN 的:

# openvpn --config destination.ovpn --auth-user-pass credentials.txt

(destination.ovpn 的编辑版本位于本问题的末尾。)

我陷入了下一步,编写将隧道设备限制到命名空间的脚本。我努力了:

  1. 将隧道设备直接放入命名空间中

    # ip netns add tns0
    # ip link set dev tun0 netns tns0
    # ip netns exec tns0 ( ... commands to bring up tun0 as usual ... )
    

    这些命令成功执行,但命名空间内生成的流量(例如ip netns exec tns0 traceroute -n 8.8.8.8)落入黑洞。

  2. 假设“您仍然可以仅将虚拟以太网 (veth) 接口分配给网络命名空间“(如果属实,将获得今年最可笑、不必要的 API 限制奖),创建一个 veth 对和一个网桥,并将 veth 对的一端放入命名空间中。这甚至还没有达到降低流量的程度在地板上:它不会让我把隧道放到桥上[编辑:这似乎只是因为!轻敲设备可以放入网桥中。与无法将任意设备放入网络命名空间不同,这实际上是有道理的,因为桥是以太网层的概念;不幸的是,我的 VPN 提供商不支持 Tap 模式下的 OpenVPN,所以我需要一个解决方法。]

    # ip addr add dev tun0 local 0.0.0.0/0 scope link
    # ip link set tun0 up
    # ip link add name teo0 type veth peer name tei0
    # ip link set teo0 up
    # brctl addbr tbr0
    # brctl addif tbr0 teo0
    # brctl addif tbr0 tun0
    can't add tun0 to bridge tbr0: Invalid argument
    

本问题末尾的脚本适用于 veth 方法。直接方法的脚本可以在编辑历史中找到。脚本中似乎没有先设置就使用的变量是由程序在环境中设置的openvpn——是的,它很草率并且使用小写名称。

请提供有关如何使其发挥作用的具体建议。我痛苦地意识到我正在通过这里的货运邪教进行编程——有任何人为这些东西写过全面的文档吗?我找不到任何东西——所以对脚本的一般代码审查也是值得赞赏的。

如果重要的话:

# uname -srvm
Linux 3.14.5-x86_64-linode42 #1 SMP Thu Jun 5 15:22:13 EDT 2014 x86_64
# openvpn --version | head -1
OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Mar 17 2014
# ip -V
ip utility, iproute2-ss140804
# brctl --version
bridge-utils, 1.5

内核是由我的虚拟主机提供商构建的(易诺德)并且,虽然用 编译CONFIG_MODULES=y,但没有实际的模块 - 唯一CONFIG_*设置为m根据的变量/proc/config.gzCONFIG_XEN_TMEM,而我实际上并没有该模块(内核存储在我的文件系统之外;/lib/modules是空的,并/proc/modules表明它没有以某种方式神奇地加载)。根据要求提供的摘录/proc/config.gz,但我不想将整个内容粘贴到此处。

netns-up.sh

#! /bin/sh

mask2cidr () {
    local nbits dec
    nbits=0
    for dec in $(echo $1 | sed 's/\./ /g') ; do
        case "$dec" in
            (255) nbits=$(($nbits + 8)) ;;
            (254) nbits=$(($nbits + 7)) ;;
            (252) nbits=$(($nbits + 6)) ;;
            (248) nbits=$(($nbits + 5)) ;;
            (240) nbits=$(($nbits + 4)) ;;
            (224) nbits=$(($nbits + 3)) ;;
            (192) nbits=$(($nbits + 2)) ;;
            (128) nbits=$(($nbits + 1)) ;;
            (0)   ;;
            (*) echo "Error: $dec is not a valid netmask component" >&2
                exit 1
                ;;
        esac
    done
    echo "$nbits"
}

mask2network () {
    local host mask h m result
    host="$1."
    mask="$2."
    result=""
    while [ -n "$host" ]; do
        h="${host%%.*}"
        m="${mask%%.*}"
        host="${host#*.}"
        mask="${mask#*.}"
        result="$result.$(($h & $m))"
    done
    echo "${result#.}"
}

maybe_config_dns () {
    local n option servers
    n=1
    servers=""
    while [ $n -lt 100 ]; do
       eval option="\$foreign_option_$n"
       [ -n "$option" ] || break
       case "$option" in
           (*DNS*)
               set -- $option
               servers="$servers
nameserver $3"
               ;;
           (*) ;;
       esac
       n=$(($n + 1))
    done
    if [ -n "$servers" ]; then
        cat > /etc/netns/$tun_netns/resolv.conf <<EOF
# name servers for $tun_netns
$servers
EOF
    fi
}

config_inside_netns () {
    local ifconfig_cidr ifconfig_network

    ifconfig_cidr=$(mask2cidr $ifconfig_netmask)
    ifconfig_network=$(mask2network $ifconfig_local $ifconfig_netmask)

    ip link set dev lo up

    ip addr add dev $tun_vethI \
        local $ifconfig_local/$ifconfig_cidr \
        broadcast $ifconfig_broadcast \
        scope link
    ip route add default via $route_vpn_gateway dev $tun_vethI
    ip link set dev $tun_vethI mtu $tun_mtu up
}

PATH=/sbin:/bin:/usr/sbin:/usr/bin
export PATH

set -ex

# For no good reason, we can't just put the tunnel device in the
# subsidiary namespace; we have to create a "virtual Ethernet"
# device pair, put one of its ends in the subsidiary namespace,
# and put the other end in a "bridge" with the tunnel device.

tun_tundv=$dev
tun_netns=tns${dev#tun}
tun_bridg=tbr${dev#tun}
tun_vethI=tei${dev#tun}
tun_vethO=teo${dev#tun}

case "$tun_netns" in
     (tns[0-9] | tns[0-9][0-9] | tns[0-9][0-9][0-9]) ;;
     (*) exit 1;;
esac

if [ $# -eq 1 ] && [ $1 = "INSIDE_NETNS" ]; then
    [ $(ip netns identify $$) = $tun_netns ] || exit 1
    config_inside_netns
else

    trap "rm -rf /etc/netns/$tun_netns ||:
          ip netns del $tun_netns      ||:
          ip link del $tun_vethO       ||:
          ip link set $tun_tundv down  ||:
          brctl delbr $tun_bridg       ||:
         " 0

    mkdir /etc/netns/$tun_netns
    maybe_config_dns

    ip addr add dev $tun_tundv local 0.0.0.0/0 scope link
    ip link set $tun_tundv mtu $tun_mtu up

    ip link add name $tun_vethO type veth peer name $tun_vethI
    ip link set $tun_vethO mtu $tun_mtu up

    brctl addbr $tun_bridg
    brctl setfd $tun_bridg 0
    #brctl sethello $tun_bridg 0
    brctl stp $tun_bridg off

    brctl addif $tun_bridg $tun_vethO
    brctl addif $tun_bridg $tun_tundv
    ip link set $tun_bridg up

    ip netns add $tun_netns
    ip link set dev $tun_vethI netns $tun_netns
    ip netns exec $tun_netns $0 INSIDE_NETNS

    trap "" 0
fi

netns-down.sh

#! /bin/sh

PATH=/sbin:/bin:/usr/sbin:/usr/bin
export PATH

set -ex

tun_netns=tns${dev#tun}
tun_bridg=tbr${dev#tun}

case "$tun_netns" in
     (tns[0-9] | tns[0-9][0-9] | tns[0-9][0-9][0-9]) ;;
     (*) exit 1;;
esac

[ -d /etc/netns/$tun_netns ] || exit 1

pids=$(ip netns pids $tun_netns)
if [ -n "$pids" ]; then
    kill $pids
    sleep 5
    pids=$(ip netns pids $tun_netns)
    if [ -n "$pids" ]; then
        kill -9 $pids
    fi
fi

# this automatically cleans up the the routes and the veth device pair
ip netns delete "$tun_netns"
rm -rf /etc/netns/$tun_netns

# the bridge and the tunnel device must be torn down separately
ip link set $dev down
brctl delbr $tun_bridg

目的地.ovpn

client
auth-user-pass
ping 5
dev tun
resolv-retry infinite
nobind
persist-key
persist-tun
ns-cert-type server
verb 3
route-metric 1
proto tcp
ping-exit 90
remote [REDACTED]
<ca>
[REDACTED]
</ca>
<cert>
[REDACTED]
</cert>
<key>
[REDACTED]
</key>

答案1

您可以在命名空间内启动 OpenVPN 链接,然后运行要在命名空间内使用该 OpenVPN 链接的每个命令。有关如何执行此操作的详细信息,请参阅在网络命名空间内运行 OpenVPN 隧道,作者:Sebastian Thorarensen

我尝试了一下,确实有效。这个想法是提供一个自定义脚本来执行特定命名空间(而不是全局命名空间)内 OpenVPN 连接的启动和路由阶段。这是基于上述来源的答案,但经过修改以将 Google DNS 添加到 resolv.conf.

首先创建一个- 向上OpenVPN 的脚本。该脚本将在名为的网络命名空间内创建 VPN 隧道接口VPN,而不是默认的命名空间。

$ cat > netns-up << 'EOF'
#!/bin/sh
case $script_type in
        up)
                ip netns add vpn
                ip netns exec vpn ip link set dev lo up
                mkdir -p /etc/netns/vpn
                echo "nameserver 8.8.8.8" > /etc/netns/vpn/resolv.conf
                ip link set dev "$1" up netns vpn mtu "$2"
                ip netns exec vpn ip addr add dev "$1" \
                        "$4/${ifconfig_netmask:-30}" \
                        ${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"}
                test -n "$ifconfig_ipv6_local" && \
                        ip netns exec vpn ip addr add dev "$1" \
                                "$ifconfig_ipv6_local"/112
                ;;
        route-up)
                ip netns exec vpn ip route add default via "$route_vpn_gateway"
                test -n "$ifconfig_ipv6_remote" && \
                        ip netns exec vpn ip route add default via \
                                "$ifconfig_ipv6_remote"
                ;;
        down)
                ip netns delete vpn
                ;;
esac
EOF

然后启动 OpenVPN 并告诉它使用我们的- 向上脚本而不是执行 ifconfig 和路由。

openvpn --ifconfig-noexec --route-noexec --up netns-up --route-up netns-up --down netns-up

现在您可以像这样启动要进行隧道传输的程序:

ip netns 执行 VPN命令

唯一的问题是您需要成为 root 才能调用,ip netns exec ...并且也许您不希望应用程序以 root 身份运行。解决方案很简单:

sudo ip netns exec vpn sudo -u $(whoami)命令

答案2

原来你将隧道接口放入网络命名空间中。我的整个问题都归因于打开界面时的错误:

ip addr add dev $tun_tundv \
    local $ifconfig_local/$ifconfig_cidr \
    broadcast $ifconfig_broadcast \
    scope link

问题是“范围链接”,我误解为只影响路由。它使内核将发送到隧道的所有数据包的源地址设置为0.0.0.0;据推测,OpenVPN 服务器会根据 RFC1122 将它们视为无效而丢弃;即使没有,目的地显然也无法回复。

在没有网络命名空间的情况下一切都正常工作,因为 openvpn 的内置网络配置脚本没有犯这个错误。如果没有“范围链接”,我的原始脚本也可以工作。

(您问我是如何发现这一点的?通过strace在 openvpn 进程上运行,设置为 hexdump 从隧道描述符中读取的所有内容,然后手动解码数据包标头。)

答案3

尝试创建 veth 设备时出现的错误是由ip命令行参数解释方式的更改引起的。

ip创建一对 veth 设备的正确调用是

ip link add name veth0 type veth peer name veth1

name代替dev

现在,如何将流量从命名空间传输到 VPN 隧道?由于您只有 tun 设备可供使用,因此“主机”必须进行路由。即创建 veth 对并将其放入命名空间中。通过路由将另一个连接到隧道。因此,启用转发,然后添加必要的路由。

举例来说,假设eth0是您的主接口,tun0是您的 VPN 隧道接口,并且veth0/veth1其中的一对接口veth1位于命名空间中。在命名空间内,您只需添加veth1.

在需要使用策略路由的主机上,请参阅这里例如。你需要做什么:

添加/附加条目,例如

1   vpn

/etc/iproute2/rt_tables。这样您就可以按名称调用(尚未创建的)表。

然后使用以下语句:

ip rule add iif veth0 priority 1000 table vpn
ip rule add iif tun0 priority 1001 table vpn
ip route add default via <ip-addr-of-tun0> table vpn
ip route add <ns-network> via <ip-addr-of-veth0> table vpn

我无法在这里尝试使用像您这样的设置,但这应该完全符合您的要求。您可以通过数据包过滤规则来增强它,这样 VPN 和“访客”网络都不会受到干扰。

注意:tun0首先进入名称空间看起来是正确的做法。但和你一样,我也没有做到这一点。策略路由看起来是下一个正确的做法。如果您了解 VPN 背后的网络,Mahendra 的解决方案就适用所有其他应用程序将永远不会访问这些网络。但是你的初始条件(“所有的流量,以及仅有的进出特定进程的流量通过 VPN”)听起来好像后者无法得到保证。

答案4

如果您通过 VPN 访问的网络已知,您可以编辑路由表来实现您想要的目的。

  1. 记下您当前的默认路线。

    # ip route | grep default default via 192.168.43.1 dev wlo1 proto static metric 1024

  2. 执行VPN,这将引入一个路由条目。

  3. 删除当前默认路由(由 VPN 添加),其中先前的默认路由将成为表中的第一个默认条目。

    # ip route | grep default default dev tun0 scope link default via 192.168.43.1 dev wlo1 proto static metric 1024

    # ip route del default dev tun0 scope link

  4. 将自定义路由添加到 VPN 中的网络以通过 tun0 进行路由。

    # ip route add <net1>/16 dev tun0

    # ip route add <net2>/24 dev tun0

  5. 添加两个名称服务器条目(在 resolv.conf 中)以及 VPN 和直接连接。

现在,所有 net1 和 net2 连接都将通过 VPN,并且重置将直接进行(在本示例中通过 wlo1)。

相关内容