6

I want certain apps to access the internet via OpenVPN. I found a solution in the final answer/comment at the bottom of this question/thread here: Feed all traffic through OpenVPN for a specific network namespace only

I am quoting that post, the problem I am having is stated at the bottom:

You can start the OpenVPN link inside a namespace and then run every command you want to use that OpenVPN link inside the namespace. Details on how to do it (not my work) here:

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

I tried it and it does work; the idea is to provide a custom script to carry out the up and route-up phases of the OpenVPN connection inside a specific namespace instead of the global one. I quote from the above link just in case it goes offline in the future:

First create an --up script for OpenVPN. This script will create the VPN tunnel interface inside a network namespace called vpn, instead of the default namespace.

$ 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

Then start OpenVPN and tell it to use our --up script instead of executing ifconfig and route.

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

Now you can start programs to be tunneled like this:

ip netns exec vpn command

The only catch is that you need to be root to invoke ip netns exec ... and maybe you do not want your application to run as root. The solution is simple:

sudo ip netns exec vpn sudo -u $(whoami) command

MY PROBLEM:

When I try to run the openvpn command which calls the netns-up script, I get two errors:

:/etc/openvpn$ sudo openvpn --ifconfig-noexec --route-noexec --up netns-up --route-up netns-up --down netns-up --config za1.nordvpn.com.tcp443.ovpn
(..)

Tue Mar 22 00:10:56 2016 [vpn-za.nordvpn.com] Peer Connection Initiated with [AF_INET]154.127.61.142:443
Tue Mar 22 00:10:59 2016 SENT CONTROL [vpn-za.nordvpn.com]: 'PUSH_REQUEST' (status=1)
Tue Mar 22 00:10:59 2016 PUSH: Received control message: 'PUSH_REPLY,redirect-gateway def1,dhcp-option DNS 78.46.223.24,dhcp-option DNS 162.242.211.137,route 10.7.7.1,topology net30,ping 5,ping-restart 30,ifconfig 10.7.7.102 10.7.7.101'
Tue Mar 22 00:10:59 2016 OPTIONS IMPORT: timers and/or timeouts modified
Tue Mar 22 00:10:59 2016 OPTIONS IMPORT: --ifconfig/up options modified
Tue Mar 22 00:10:59 2016 OPTIONS IMPORT: route options modified
Tue Mar 22 00:10:59 2016 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
Tue Mar 22 00:10:59 2016 ROUTE_GATEWAY 192.168.1.254/255.255.255.0 IFACE=eth0 HWADDR=b8:27:eb:39:7e:46
Tue Mar 22 00:10:59 2016 TUN/TAP device tun0 opened
Tue Mar 22 00:10:59 2016 TUN/TAP TX queue length set to 100
Tue Mar 22 00:10:59 2016 netns-up tun0 1500 1592 10.7.7.102 10.7.7.101 init
Tue Mar 22 00:10:59 2016 WARNING: Failed running command (--up/--down): external program exited with error status: 1
Tue Mar 22 00:10:59 2016 Exiting due to fatal error

I tried re-creating the netns-up script with and without sudo but it didn't help. What am I doing wrong?

5
  • So I had an error in the script (at the bottom "EOF"), the script is now fixed but I still get a fatal error after the VPN connection is initiated. I updated my problem above to include the log.
    – zilexa
    Commented Mar 21, 2016 at 23:06
  • It amazes me this is not a highly popular topic. It relates directly to the use of torrent clients and torrent scrapers, which by now (2016-Q2) for most countries need to be behind VPN. To debug the issue I ran each command of the "UP" case one by one and noticed one command fails because it's syntax is incorrect. I tried a few things but this is way over my head: ip link set dev "$1" up netns vpn mtu "$2" Any help fixing this command would be greatly appreciated.
    – zilexa
    Commented Mar 29, 2016 at 13:28
  • This script popcorntime-vpn.sh with some little modifications worked for me. If you are still interested, I can share it here.
    – Felix
    Commented Sep 14, 2016 at 9:28
  • Yes absolutely!
    – zilexa
    Commented Dec 17, 2016 at 22:58
  • 1
    Re error WARNING: Failed running command (--up/--down)..., to diagnose it edit the script to add set -x near the start but after the top #!/bin/sh line. This should cause all steps and error message to be sent to logs. This is only a starting point to find why it fails.
    – AnyDev
    Commented Apr 20, 2020 at 10:10

3 Answers 3

2

Starting openvpn inside the network namespace is safer. I use the following script (fork of Schnouki's) to create namespace, configure firewall, DNS, test connectivity, start openvpn, and finally start a torrent client. I put TODOs in the script where you have to adjust it to your needs.

#!/bin/sh
# start openvpn tunnel and torrent client inside Linux network namespace
#
# this is a fork of schnouki's script, see original blog post
# https://schnouki.net/posts/2014/12/12/openvpn-for-a-single-application-on-linux/
#
# original script can be found here
# https://gist.github.com/Schnouki/fd171bcb2d8c556e8fdf

# ------------ adjust values below ------------
# network namespace
NS_NAME=myVPN
NS_EXEC="ip netns exec $NS_NAME"
# user for starting the torrent client
REGULAR_USER=heinzwurst
# ---------------------------------------------

# exit on unbound variable
set -u

# exit on error
set -e
set -o pipefail

# trace option
#set -x

if [ $USER != "root" ]; then
    echo "This must be run as root."
    exit 1
fi

start_vpn() {
    echo "Add network interface"

    # Create the network namespace
    ip netns add $NS_NAME

    # Start the loopback interface in the namespace
    $NS_EXEC ip addr add 127.0.0.1/8 dev lo
    $NS_EXEC ip link set lo up

    # Create virtual network interfaces that will let OpenVPN (in the
    # namespace) access the real network, and configure the interface in the
    # namespace (vpn1) to use the interface out of the namespace (vpn0) as its
    # default gateway
    ip link add vpn0 type veth peer name vpn1
    ip link set vpn0 up
    ip link set vpn1 netns $NS_NAME up

    ip addr add 10.200.200.1/24 dev vpn0
    $NS_EXEC ip addr add 10.200.200.2/24 dev vpn1
    $NS_EXEC ip link set dev vpn1 mtu 1492
    $NS_EXEC ip route add default via 10.200.200.1 dev vpn1

    # Configure the nameserver to use inside the namespace
    # TODO use VPN-provided DNS servers in order to prevent leaks
    mkdir -p /etc/netns/$NS_NAME
    cat >/etc/netns/$NS_NAME/resolv.conf <<EOF || exit 1
nameserver 8.8.8.8
nameserver 8.8.4.4
EOF

    # IPv4 NAT, you may need to adjust the interface name prefixes 'eth' 'wlan'
    iptables -t nat -A POSTROUTING -o eth+ -m mark --mark 0x29a -j MASQUERADE
    iptables -t nat -A POSTROUTING -o wlan+ -m mark --mark 0x29a -j MASQUERADE
    iptables -t mangle -A PREROUTING -i vpn0 -j MARK --set-xmark 0x29a/0xffffffff

    # TODO create firewall rules for your specific application (torrent)
    # or just comment the line below
    $NS_EXEC iptables-restore < /etc/iptables/iptables-$NS_NAME.rules

    # we should have full network access in the namespace
    $NS_EXEC ping -c 3 www.google.com

    # start OpenVPN in the namespace
    echo "Starting VPN"
    cd /etc/openvpn
    # TODO create openvpn configuration in /etc/openvpn/$NS_NAME.conf
    $NS_EXEC openvpn --config $NS_NAME.conf &

    # wait for the tunnel interface to come up
    while ! $NS_EXEC ip link show dev tun0 >/dev/null 2>&1 ; do sleep .5 ; done
}

stop_vpn() {
    echo "Stopping VPN"
    ip netns pids $NS_NAME | xargs -rd'\n' kill
    # TODO wait for terminate

    # clear NAT
    iptables -t nat -D POSTROUTING -o eth+ -m mark --mark 0x29a -j MASQUERADE
    iptables -t nat -D POSTROUTING -o wlan+ -m mark --mark 0x29a -j MASQUERADE
    iptables -t mangle -D PREROUTING -i vpn0 -j MARK --set-xmark 0x29a/0xffffffff

    echo "Delete network interface"
    rm -rf /etc/netns/$NS_NAME

    ip netns delete $NS_NAME
    ip link delete vpn0
}

# stop VPN on exit (even when error occured)
trap stop_vpn EXIT

start_vpn

# TODO start your favorite torrent client
$NS_EXEC sudo -u $REGULAR_USER transmission-gtk
8
  • @HalosGhost OMG this is exactly what I have been looking for! But I am a noob compared to you. Could you please explain a little bit more for example share your firewall rules for your torrent client? I am using Transmission, with transmission-daemon. Also, should I add the path\filename of my .ovpn file at the end of this line or should $NS_NAME.conf resolve to the actual name of my (NordVPN) conf file? "$NS_EXEC openvpn --config $NS_NAME.conf &"
    – zilexa
    Commented Sep 18, 2016 at 20:06
  • @zilexa, I edited the post only for clarity; you should address clarifying questions to the actual author.
    – HalosGhost
    Commented Sep 18, 2016 at 20:27
  • answers to your questions: (1) use something like this simple stateful firewall configuration and open a single port for your torrent client (2) NS_NAME is without '.ovpn' suffix
    – Felix
    Commented Sep 25, 2016 at 13:33
  • Don't forget to sysctl -w net.ipv4.ip_forward=1 on the host machine! Commented Jul 30, 2017 at 7:14
  • Looks like you have got two completely different interfaces both named tun0 and you get away with it because they're in different namespaces. That's a bad idea. You should call the veth interfaces vethhost and vethvpn. Commented Jan 19, 2019 at 1:50
1

I have been using the forked script provided by Felix for about six months and up until the past week it has been working quite well (once I added the sysctl -w net.ipv4.ip_forward=1 line suggested by Nehal J Wani and commented out the pipefail - as it would crash with it otherwise).

Yet I noticed in the past week it has completely stopped working (Debian 9.4). I spent a lot of time trying to debug what went wrong, as the script is quite useful - but no matter what I tried I couldn't get it working again.

As this is such a useful feature, I wanted to provide an alternative fork of Schnouki's work that's works well in case anyone runs into the same issue.

https://github.com/crasm/vpnshift.sh

#!/bin/bash
#
# Copyright (c) 2016, crasm <[email protected]>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


usage="usage: vpnshift -c <config> [<command> [<arg>...]]
optional:
    -u <user>      Execute <command> as <user>
    -d             Toggle namespace debug shell

if not otherwise specified:
    - The command defaults to the user's shell (${SHELL}).
    - The user must be inferred from sudo.
"

quick_die() {
    format="$1"; shift
    >&2 printf "${format}\n" "$@"
    exit 1
}

die() {
    format="$1"; shift
    >&2 printf "${format}\n" "$@"
    clean_exit 1
}

hush() {
    eval "$@" > /dev/null 2> /dev/null
}

must() {
    eval "$@" || die "failed: %s" "$*"
}

is_running() {
    local pid="$1"
    hush kill -s 0 "${pid}"
}

sig_kill() {
    local pid="$1"
    hush kill -s KILL "${pid}"
}

sig_term() {
    local pid="$1"
    hush kill -s TERM "${pid}"
}

clean_exit() {
    local exit_code="$1"

    if is_running "${openvpn_pid}"; then
        # Kill openvpn.
        sig_term "${openvpn_pid}"
        >&2 printf "stopping openvpn (pid = %d)." "${openvpn_pid}"
        for i in {1..100}; do
            if is_running "${openvpn_pid}"; then
                sleep 0.1
                printf "."
            else
                break
            fi
        done
        printf "\n"

        if is_running "${openvpn_pid}"; then
            >&2 echo "forced to kill openvpn"
            sig_kill "${openvpn_pid}"
        fi
    else
        >&2 echo "openvpn exited"
    fi

    # don't start cleaning up until openvpn is gone
    hush ip netns delete "${namespace}"
    hush rm --recursive --force "${namespace_dir}"
    hush sysctl --quiet net.ipv4.ip_forward="${forward}"
    if hush ps -C 'firewalld'; then
        echo "[firewalld] clearing firewalld state"
        hush systemctl restart firewalld
    else
        echo "${rules}" | hush iptables-restore
    fi

    # Sometimes there's a lag for the veths to be deleted by linux, so we
    # delete it manually.
    hush ip link delete "${veth_default}"
    hush ip link delete "${veth_vpn}"

    exit "${exit_code}"
}

nsdo() {
    ip netns exec "${namespace}" "$@"
}

_debug=0

main() {
    local config=
    local user="${SUDO_USER}"
    while getopts "hdc:u:" opt; do
        case "${opt}" in
            h) quick_die "${usage}" ;;
            d) _debug=1 ;;
            c) config="$(realpath "${OPTARG}")" ;;
            u) user="${OPTARG}" ;;
            *) quick_die "unknown option: %s" "${opt}" ;;
        esac
    done
    shift $(( OPTIND - 1 ))

    if [[ -z "${config}" ]]; then
        quick_die "openvpn config is required"
    fi

    if [[ -z "${user}" ]]; then
        quick_die "user must be provided explicitly via '-u' or implicitly via SUDO_USER"
    fi

    local cmd="$1"; shift

    if [[ -z "${cmd}" ]]; then
        cmd="${SHELL}"
    fi

    must ip netns add vpnshift
    must mkdir --parents "${namespace_dir}"

    # Set up loopback interface

    must nsdo ip address add '127.0.0.1/8' dev lo
    must nsdo ip address add '::1/128' dev lo
    must nsdo ip link set lo up

    # Set up veth tunnel

    must ip link add "${veth_vpn}" type veth peer name "${veth_default}"
    must ip link set "${veth_vpn}" netns "${namespace}"

    must ip link set "${veth_default}" up
    must nsdo ip link set "${veth_vpn}" up

    must ip address add "10.10.10.10/31" dev "${veth_default}"
    must nsdo ip \
        address add "10.10.10.11/31" dev "${veth_vpn}"

    must nsdo ip \
        route add default via "10.10.10.10" dev "${veth_vpn}"

    # Set up NAT and IP forwarding
    must sysctl --quiet net.ipv4.ip_forward=1

    # check if we need to enable masquerading via firewalld for veth_default
    if hush ps -C 'firewalld'; then
        echo "[firewalld] enabling firewalld based masquerading for ${veth_default}"

        if [[ $(firewall-cmd --get-zones | grep "${namespace}") != *"${namespace}"* ]]
        then
            echo "[firewalld] creating permanent new zone ${namespace} with target default"
            must firewall-cmd -q --permanent --new-zone="${namespace}"
            must firewall-cmd -q --permanent --zone="${namespace}" --set-target="default"
            must firewall-cmd -q --reload
        fi

        # add interface to our zone
        echo "[firewalld] adding ${veth_default} and ${veth_vpn} to zone ${namespace}"
        must firewall-cmd -q --zone="${namespace}" --change-interface="${veth_default}"

        # apply our source range to our zone
        echo "[firewalld] adding 10.10.10.10/31 as source for ${namespace}"
        must firewall-cmd -q --zone="${namespace}" --add-source=10.10.10.10/31

        # enable masquerading from our new source range on the default zone
        default_zone=$(firewall-cmd --get-default-zone)
        echo "[firewalld] enabling masquerading on default zone: ${default_zone}"
        must firewall-cmd -q --zone="${default_zone}" --add-masquerade
        must firewall-cmd -q --zone="${default_zone}"  --add-rich-rule=\'rule family="ipv4" source address="10.10.10.10/31" masquerade\'

        # optionally allow ports, services, etc. on our zone

        # enabling desired ports
        #echo "enabling all port traffic on zone ${namespace}"
        #must firewall-cmd -q --zone="${namespace}" --add-port=1025-65535/udp
        #must firewall-cmd -q --zone="${namespace}" --add-port=1025-65535/tcp

        # enable services
        #echo "enabling dns on zone ${namespace}"
        #must firewall-cmd -q --zone="${namespace}" --add-service=dns

    else
        must iptables --table "nat" --append "POSTROUTING" --jump "MASQUERADE" --source "10.10.10.10/31"
    fi

    # Set up DNS inside the new namespace
    printf > "${namespace_dir}/resolv.conf" \
        "nameserver %s\nnameserver %s\n" \
        "108.62.19.131" \
        "104.238.194.235"

    # drop in a shell to debug namespace connectivity ... the exit trap will catch exit from this and clean up
    if [[ "$_debug" == 1 ]]; then
        nsdo "${SHELL}"
    fi

    # Launch openvpn
    local tun="tunvpn"
    nsdo openvpn \
        --cd "$(dirname "${config}")" \
        --config "${config}" \
        --dev "${tun}" \
        --errors-to-stderr &

    openvpn_pid=$(ps --ppid "$!" \
        --format "pid" \
        --no-headers
    )

    >&2 printf "waiting for openvpn (pid = %d)\n" "${openvpn_pid}"

    while ! hush nsdo ip link show "${tun}"; do
        if ! is_running "${openvpn_pid}"; then
            clean_exit 1
        fi
        sleep 0.2
    done

    # Removing the default route protects from exposure if openvpn exits
    # prematurely.
    must nsdo ip \
        route delete default via "10.10.10.10" dev "${veth_vpn}"

    nsdo sudo -u "${user}" "${cmd}" "$@"
}

if [[ $# == 0 ]]; then
    quick_die "${usage}"
elif [[ "$(id -u)" != 0 ]]; then
    sudo "$0" "$@"
    exit "$?"
fi

# Stuff needed by clean_exit() to restore previous state.
namespace="vpnshift"
namespace_dir="/etc/netns/${namespace}"
forward="$(sysctl --values "net.ipv4.ip_forward")"
rules="$(iptables-save -t nat)"
veth_default="veth_default"
veth_vpn="veth_vpn"

openvpn_pid= # This is set later.

# Enable cleanup routine.
trap 'clean_exit 1' INT TERM
trap 'clean_exit $?' EXIT

main "$@"
1

I have a computer connected to a LAN with wired ethernet, which allowed me to take a bit of a different tack with this: bridging! It allows the LAN's DHCP server to grant separate addresses to the host and the vpn namespace and grants the vpn namespace direct access to the internet! No iptables modifications or manual IP address specifications are required! Here's my whole solution in commands.

# Make the netns.
ip netns add vpn

ip link add br0 type bridge
ip link set br0 up

# Make the inter-namespace pipe and bridge the host end.
ip link add veth.host type veth peer veth.vpn
ip link set veth.host master br0 up
ip link set veth.vpn netns vpn up

# Bridge the wired ethernet.
ip link set eth0 master br0 up

# Start dhcpcd on the bridge for the host to use and on veth.vpn
# for the vpn netns to use. The router will grant separate IP
# addresses to both! (They have different MAC addresses.)
dhcpcd br0
ip netns exec vpn dhcpcd veth.vpn

# OK, now start the VPN.
ip netns exec vpn openvpn --config etc etc etc

Considerably simpler setup.

2
  • 1
    thanks so much! But how do you make sure certain applications only connect to the internet via the namespace in which vpn is running?
    – zilexa
    Commented Mar 3, 2020 at 17:23
  • Use ip netns exec vpn yourcommand and make sure yourcommand is never run by itself. If you want to make completely sure it is never run by itself, you can make a bash script named yourcommand that runs ip netns exec vpn yourcommand and put it at a higher precedence in your PATH. Commented Mar 29, 2020 at 1:55

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.