我正在尝试设置一个 VPN(使用 OpenVPN),以便所有流量,并且仅有的进出特定进程的流量通过 VPN;其他进程应该继续直接使用物理设备。据我了解,在 Linux 中执行此操作的方法是使用网络命名空间。
如果我正常使用 OpenVPN(即漏斗全部来自客户端通过 VPN 的流量),它工作正常。具体来说,我是这样启动 OpenVPN 的:
# openvpn --config destination.ovpn --auth-user-pass credentials.txt
(destination.ovpn 的编辑版本位于本问题的末尾。)
我陷入了下一步,编写将隧道设备限制到命名空间的脚本。我努力了:
将隧道设备直接放入命名空间中
# 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
)落入黑洞。假设“您仍然可以仅将虚拟以太网 (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.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>
答案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 访问的网络已知,您可以编辑路由表来实现您想要的目的。
记下您当前的默认路线。
# ip route | grep default default via 192.168.43.1 dev wlo1 proto static metric 1024
执行VPN,这将引入一个路由条目。
删除当前默认路由(由 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
将自定义路由添加到 VPN 中的网络以通过 tun0 进行路由。
# ip route add <net1>/16 dev tun0
# ip route add <net2>/24 dev tun0
- 添加两个名称服务器条目(在 resolv.conf 中)以及 VPN 和直接连接。
现在,所有 net1 和 net2 连接都将通过 VPN,并且重置将直接进行(在本示例中通过 wlo1)。