通过OpenVPN仅针对特定网络名称空间提供所有流量


16

我正在尝试建立一个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)接口分配给网络名称空间 ”(如果为true,则会获得今年对最可笑的不必要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
    

该问题末尾的脚本适用于第七种方法。直接方法的脚本可以在编辑历史记录中找到。脚本中似乎没有先设置变量就使用的变量是由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.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

destination.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>

让我们从显而易见的内容开始:是否支持veth设备?内核模块(veth)是否已加载?
countermode

@countermode grep veth /proc/modules没有列出任何内容,但我不知道这是否定论。Linode实例在OS分区内未安装内核,因此我不确定我是否仍可以加载缺少的模块。
zwol 2014年

是否lsmod产生在所有的任何输出?有目录/lib/modules吗?
countermode

lsmod: command not found。有一个/lib/modules,但其中没有任何模块,只有一堆包含空modules.dep文件的每内核目录。我将在Linode特定的帮助中四处寻找,看看是否应该如此。
zwol 2014年

嗯...很奇怪。我对Linode并不熟悉,但是对我来说好像不支持veth设备。
countermode

Answers:


9

您可以在名称空间内启动OpenVPN链接,然后运行要在名称空间内使用该OpenVPN链接的每个命令。有关如何执行此操作的详细信息(不是我的工作):

http://www.naju.se/articles/openvpn-netns.html

我尝试过,它确实起作用;这个想法是提供一个自定义脚本,以在特定名称空间而不是全局名称空间内执行OpenVPN连接的启动和路由启动阶段。我引用上面的链接,以防万一将来离线:

首先为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 command

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

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

1
您好,欢迎光临本站!我们鼓励用户至少总结(如果可能)粘贴在答案中的链接的内容。如果链接变旧(例如,该站点不再可用),这有助于保持答案的质量。请通过包含链接文章中最重要的部分/说明来改善您的答案。
Erathiel 2015年

很好,但是您需要在单引号分隔符两边加上单引号,以防止Shell扩展所有变量。
ewatt

7

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

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

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

在没有网络名称空间的情况下,一切都可以正常工作,因为openvpn的内置网络配置脚本不会犯此错误。而且没有“作用域链接”,我的原始脚本也可以正常工作。

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


您是否有机会为此撰写指南?我正在尝试设置类似的内容,但是很难说出问题的哪些部分是好的起点,哪些是导致失败的途径。
tremby 2014年

@tremby我不太可能在不久的将来这样做,但是您可能会发现github.com/zackw/tbbscraper/blob/master/scripts/openvpn-netns.c很有用。
zwol 2014年

是的,我不确定1100行C程序是否会有所帮助。最终的配置,脚本和咒语如何为您完成任务呢?...还是那个C程序是您对此的最终实现?
tremby 2014年

@tremby是的,该C程序是我的最终实现。(您知道在我的使用场景中,它必须是setuid的。)您也许可以放下它-如果顶部的大注释没有说明如何使用它,请告诉我。
zwol 2014年

@tremby或者,查看“从openvpn内部执行的脚本”,从github.com/zackw/tbbscraper/blob/master/scripts/…开始,以了解如何设置和删除网络名称空间。而ovpn客户端的实际调用位于github.com/zackw/tbbscraper/blob/master/scripts/…。该代码的其余部分可以看作是一种微型外壳实现,可以使这些操作的编写更加轻松。
zwol 2014年

4

尝试创建第ve个设备时出现错误是由于ip解释命令行参数的方式发生了变化。

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

ip link add name veth0 type veth peer name veth1

namedev

现在,如何将流量从命名空间发送到VPN隧道?由于仅可使用调谐设备,因此“主机”必须路由。即创建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”)听起来好像无法保证后者。


谢谢,这使我更进一步,但是我现在停留在“然后您使用网桥将veth设备连接到隧道”部分上-请参阅修订后的问题。
zwol 2014年

根据我刚刚发布的答案,整个过程都归结为我的原始脚本中的一个愚蠢错误-“作用域链接”并不意味着我认为的含义。但是我要给您赏金,因为您付出了很多工作来帮助我尝试各种可能性,如果您没有,我可能会完全放弃。
zwol

嘿扎克,非常感谢。命名空间和策略路由是一项有趣的研究。如果它本身并不令人兴奋,我确实不会为此付出太多努力。

0

如果知道通过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. 同时为VPN和直接连接添加两个名称服务器条目(在resolv.conf中)。

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


遗憾的是,事先不知道通过VPN访问的网络,因此这对我不起作用。
zwol
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.