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

zwo*_*wol 17 linux vpn openvpn network-namespaces

我正在尝试建立一个 VPN(使用 OpenVPN),以便所有流量,只有流量,去往/来自特定进程,通过 VPN;其他进程应该继续直接使用物理设备。我的理解是,在 Linux 中执行此操作的方法是使用网络命名空间。

如果我正常使用 OpenVPN(即通过 VPN 从客户端汇集所有流量),它工作正常。具体来说,我像这样启动 OpenVPN:

# openvpn --config destination.ovpn --auth-user-pass credentials.txt
Run Code Online (Sandbox Code Playgroud)

(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 ... )
    
    Run Code Online (Sandbox Code Playgroud)

    这些命令成功执行,但在命名空间内产生的流量(例如 with 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
    
    Run Code Online (Sandbox Code Playgroud)

此问题末尾的脚本适用于 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
Run Code Online (Sandbox Code Playgroud)

内核是由我的虚拟主机提供商 ( Linode )构建的,尽管使用 编译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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

目的地.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>
Run Code Online (Sandbox Code Playgroud)

小智 10

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

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

首先为 OpenVPN创建一个--up脚本。此脚本将在名为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
Run Code Online (Sandbox Code Playgroud)

然后启动 OpenVPN 并告诉它使用我们的--up脚本,而不是执行 ifconfig 和 route。

openvpn --ifconfig-noexec --route-noexec --up netns-up --route-up netns-up --down netns-up
Run Code Online (Sandbox Code Playgroud)

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

ip netns exec vpn命令

唯一的问题是您需要以 root 身份进行调用,ip netns exec ...而且您可能不希望您的应用程序以 root 身份运行。解决方法很简单:

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


zwo*_*wol 8

事实证明,您可以将隧道接口放入网络命名空间。我的整个问题都归结为打开界面的错误:

ip addr add dev $tun_tundv \
    local $ifconfig_local/$ifconfig_cidr \
    broadcast $ifconfig_broadcast \
    scope link
Run Code Online (Sandbox Code Playgroud)

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

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

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