I am trying to set up a VPN (using OpenVPN) such that all of the traffic, and only the traffic, to/from specific processes goes through the VPN; other processes should continue to use the physical device directly. It is my understanding that the way to do this in Linux is with network namespaces.
If I use OpenVPN normally (i.e. funnelling all traffic from the client through the VPN), it works fine. Specifically, I start OpenVPN like this:
# openvpn --config destination.ovpn --auth-user-pass credentials.txt
(A redacted version of destination.ovpn is at the end of this question.)
I'm stuck on the next step, writing scripts that restrict the tunnel device to namespaces. I have tried:
Putting the tunnel device directly in the namespace with
# ip netns add tns0# ip link set dev tun0 netns tns0# ip netns exec tns0 ( ... commands to bring up tun0 as usual ... )
These commands execute successfully, but traffic generated inside the namespace (e.g. with
ip netns exec tns0 traceroute -n 8.8.8.8
) falls into a black hole.On the assumption that "you can [still] only assign virtual Ethernet (veth) interfaces to a network namespace" (which, if true, takes this year's award for most ridiculously unnecessary API restriction), creating a veth pair and a bridge, and putting one end of the veth pair in the namespace. This doesn't even get as far as dropping traffic on the floor: it won't let me put the tunnel into the bridge! [EDIT: This appears to be because only tap devices can be put into bridges. Unlike the inability to put arbitrary devices into a network namespace, that actually makes sense, what with bridges being an Ethernet-layer concept; unfortunately, my VPN provider does not support OpenVPN in tap mode, so I need a workaround.]
# 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 tun0can't add tun0 to bridge tbr0: Invalid argument
The scripts at the end of this question are for the veth approach. The scripts for the direct approach may be found in the edit history. Variables in the scripts that appear to be used without setting them first are set in the environment by the openvpn
program -- yes, it's sloppy and uses lowercase names.
Please offer specific advice on how to get this to work. I'm painfully aware that I'm programming by cargo cult here -- has anyone written comprehensive documentation for this stuff? I can't find any -- so general code review of the scripts is also appreciated.
In case it matters:
# uname -srvmLinux 3.14.5-x86_64-linode42 #1 SMP Thu Jun 5 15:22:13 EDT 2014 x86_64# openvpn --version | head -1OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Mar 17 2014# ip -Vip utility, iproute2-ss140804# brctl --versionbridge-utils, 1.5
The kernel was built by my virtual hosting provider (Linode) and, although compiled with CONFIG_MODULES=y
, has no actual modules -- the only CONFIG_*
variable set to m
according to /proc/config.gz
was CONFIG_XEN_TMEM
, and I do not actually have that module (the kernel is stored outside my filesystem; /lib/modules
is empty, and /proc/modules
indicates that it was not magically loaded somehow). Excerpts from /proc/config.gz
provided on request, but I don't want to paste the entire thing here.
netns-up.sh
#! /bin/shmask2cidr () { 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="$serversnameserver $3" ;; (*) ;; esac n=$(($n + 1)) done if [ -n "$servers" ]; then cat > /etc/netns/$tun_netns/resolv.conf <<EOF# name servers for $tun_netns$serversEOF 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/binexport PATHset -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=$devtun_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;;esacif [ $# -eq 1 ] && [ $1 = "INSIDE_NETNS" ]; then [ $(ip netns identify $$) = $tun_netns ] || exit 1 config_inside_netnselse 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 "" 0fi
netns-down.sh
#! /bin/shPATH=/sbin:/bin:/usr/sbin:/usr/binexport PATHset -extun_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 1pids=$(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 fifi# this automatically cleans up the the routes and the veth device pairip netns delete "$tun_netns"rm -rf /etc/netns/$tun_netns# the bridge and the tunnel device must be torn down separatelyip link set $dev downbrctl delbr $tun_bridg
destination.ovpn
clientauth-user-passping 5dev tunresolv-retry infinitenobindpersist-keypersist-tunns-cert-type serververb 3route-metric 1proto tcpping-exit 90remote [REDACTED]<ca>[REDACTED]</ca><cert>[REDACTED]</cert><key>[REDACTED]</key>