Linux

僅通過 OpenVPN 為特定網路命名空間提供所有流量

  • September 22, 2020

我正在嘗試設置一個 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 ... )

這些命令成功執行,但在命名空間內生成的流量(例如 with ip netns exec tns0 traceroute -n 8.8.8.8)陷入了黑洞。 2. 假設“你可以$$ still $$僅將虛擬乙太網 (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

核心是由我的虛擬主機提供商 ( Linode ) 建構的,雖然使用 編譯CONFIG_MODULES=y,但沒有實際的模組——唯一設置為的CONFIG_*變數是,我實際上沒有那個模組(核心儲存在我的文件系統之外;是空的,並表明它沒有以某種方式神奇地載入)。應要求提供的摘錄,但我不想在此處粘貼整個內容。m``/proc/config.gz``CONFIG_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>

事實證明,您可以將隧道介面放入網路命名空間。我的整個問題歸結為調出界面的錯誤:

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 它從隧道描述符中讀取的所有內容,然後手動解碼數據標頭。)

您可以在命名空間內啟動 OpenVPN 連結,然後執行要在命名空間內使用該 OpenVPN 連結的每個命令。Sebastian Thorarensen 的在網路名稱空間內執行 OpenVPN 隧道中介紹瞭如何執行此操作的詳細資訊。

我試過了,它確實有效。這個想法是提供一個自定義腳本來在特定命名空間而不是全域命名空間內執行 OpenVPN 連接的啟動和路由階段。這是基於上述來源的答案,但經過修改以將 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

然後啟動 OpenVPN 並告訴它使用我們的*–up*腳本而不是執行 ifconfig 和 route。

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

現在您可以像這樣啟動要隧道化的程序:

ip netns exec vpn*命令*

唯一的問題是您需要以 root 身份呼叫ip netns exec ...,並且您可能不希望您的應用程序以 root 身份執行。解決方案很簡單:

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

引用自:https://unix.stackexchange.com/questions/149293