Route only specific traffic through VPN

What you are asking for does not exist. This is why you are dissatisfied with the answers you found (some of them, possibly, being mine): all of them have suggested workarounds, not a genuine solution, either simple or complex.

Let me explain. Routing in all OSes is determined by destination address: you may very well have several routes, but the choice between them is not based upon the application invoking the connection, but simply upon the destination address. Full stop.

Let me give you a non-trivial example. When a VPN client has established a connection to its server, it is still possible to route a connection to a given site, say example.org, outside the VPN. But all applications trying to reach that special address will be routed outside the VPN: you cannot have some applications going to example.org thru the VPN while other apps pass outside the VPN.

The situation becomes richer with the Linux kernel, which allows source routing: this means you can have two or more routing tables, and the choice between them is based upon the source address, not the destination address.

A non-trivial example: my pc has two outside lines, with two distinct public IPs. It can be contacted thru either interface, and it is important that my replies to a given connection go thru the same interface that connection came in thru: otherwise they will be discarded as irrelevant when they reach the person who initiated the connection. This is source routing.

Fair enough, what about connections that we start? Some apps allow you to specify the bind address, like the openssh client:

-b bind_address

Use bind_address on the local machine as the source address of the connection. Only useful on systems with more than one address.

For them, there is no problem in having one instance going thru the VPN (say, routing table 1) while another instance will go outside the VPN (say routing table 2). But other apps, like Firefox, not only are notoriously difficult to bind to a specific source ip address (but see here for a very smart workaround), but also are mean and nasty in that they will not allow you to have two copies of themselves running simultaneously, each bound to a different source address. In other words, while thanks to the trick referenced above you can oblige one instance to bind to a source address of your choice, then you cannot have another version of it binding to the other source address.

This explains why we use workarounds: they are all based upon the same idea, that they work with a separate network stack than the rest of the pc. So you can have, in decreasing approximate order of complexity, VMs, dockers, containers, namespaces. In each of them you will have one or more routing tables, but you can have several instances of each (VM/dockers/containers/namespaces) and you can also admix them freely, each one of them running its own app like Firefox happily separated from the other ones.

Perhaps you are still interested in one of the workarounds?

EDIT:

The simplest work-around is a network namespace. The script below handles all necessary aspects of a NNS: put it in a file (you pick your name, I generally use newns, but you do whatever you prefer) in /usr/local/bin, then chmod 755 FILE_NAME, and you can use it as follows:

       newns NAMESPACE_NAME start
       newns NAMESPACE_NAME stop

It will open an xterm for you (that's because I like xterm to work, but you can change it if you wish to use anything else), which belongs to the new namespace. From inside the xterm you may, if you wish, start your vpn, and then start your game. You may easily check that you are using the VPN thru the following command:

    wget 216.146.38.70:80 -O - -o /dev/null | cut -d" " -f6 | sed 's/<\/body><\/html>//'

which returns you your public IP. After setting up the VPN in the xterm, you may check that your public IP is different in your other windows. You may open up to 254 xterms, with 254 different NNSes, and different connections.

#!/bin/bash

#
# This script will setup an internal network 10.173.N.0/24; if this causes
# any conflict, change the statement below.

export IP_BASE=10.173

# It will open an xterm window in the new network namespace; if anything
# else is required, change the statement below.

export XTERM=/usr/bin/xterm

# The script will temporarily activate ip forwarding for you. If you
# do not wish to retain this feature, you will have to issue, at the 
# end of this session, the command
# echo 0 > /proc/sys/net/ipv4/ip_forward 
# yourself. 

 ###############################################################################

 WHEREIS=/usr/bin/whereis

 # First of all, check that the script is run by root:


 [ "root" != "$USER" ] && exec sudo $0 "$@"

 if [ $# != 2 ]; then
    echo "Usage $0 name action"
    echo "where name is the network namespace name,"
    echo " and action is one of start| stop| reload."
    exit 1
 fi

 # Do we have all it takes?

 IERROR1=0
 IERROR2=0
 IERROR3=0
 export IP=$($WHEREIS -b ip | /usr/bin/awk '{print $2}')
 if [ $? != 0 ]; then
    echo "please install the iproute2 package"
    IERROR1=1
 fi

 export IPTABLES=$($WHEREIS -b iptables | /usr/bin/awk '{print $2}')
 if [ $? != 0 ]; then
    echo "please install the iptables package"
    IERROR2=1
 fi

 XTERM1=$($WHEREIS -b $XTERM | /usr/bin/awk '{print $2}')
 if [ $? != 0 ]; then
    echo "please install the $XTERM package"
    IERROR3=1
 fi
 if [ IERROR1 == 1 -o IERROR2 == 1 -o IERROR3 == 1 ]; then
    exit 1
 fi

 prelim() {

 # Perform some preliminary setup. First, clear the proposed 
 # namespace name of blank characters; then create a directory
 # for logging info, and a pid file in it; then determine 
 # how many running namespaces already exist, for the purpose
 # of creating a unique network between the bridge interface (to 
 # be built later) and the new namespace interface. Lastly, 
 # enable IPv4 forwarding. 

    VAR=$1
    export NNSNAME=${VAR//[[:space:]]}

    export OUTDIR=/var/log/newns/$NNSNAME

    if [ ! -d $OUTDIR ]; then
            /bin/mkdir -p $OUTDIR
    fi
    export PID=$OUTDIR/pid$NNSNAME

    # Find a free subnet

    ICOUNTER=0
    while true; do
            let ICOUNTER=ICOUNTER+1
            ip addr show | grep IP_BASE.$ICOUNTER.1 2>&1 1> /dev/null
            if [ ! $? == 0 -a $ICOUNTER -lt 255 ]; then
                    export Nns=$ICOUNTER
                    break
            elif [ ! $? == 0 -a $ICOUNTER -gt 254 ]; then
                    echo "Too many open network namespaces"
                    exit 1
            fi
    done
    if [ $Nns == 1 ]; then
            echo 1 > /proc/sys/net/ipv4/ip_forward
    fi

 }

 start_nns() {

 # Check whether a namespace with the same name already exists. 

    $IP netns list | /bin/grep $1 2> /dev/null
    if [ $? == 0 ]; then
            echo "Network namespace $1 already exists,"
            echo "please choose another name"
            exit 1
    fi

    # Here we take care of DNS

    /bin/mkdir -p /etc/netns/$1
    echo "nameserver 8.8.8.8" > /etc/netns/$1/resolv.conf
    echo "nameserver 8.8.4.4" >> /etc/netns/$1/resolv.conf
                                                                           

    # The following creates the new namespace, the veth interfaces, and
    # the bridge between veth1 and a new virtual interface, tap0.
    # It also assigns an IP address to the bridge, and brings everything up

    $IP netns add $1
    $IP link add veth-a$1 type veth peer name veth-b$1
    $IP link set veth-a$1 up
    $IP tuntap add tap$1 mode tap user root
    $IP link set tap$1 up
    $IP link add br$1 type bridge
    $IP link set tap$1 master br$1
    $IP link set veth-a$1 master br$1
    $IP addr add $IP_BASE.$Nns.1/24 dev br$1
    $IP link set br$1 up

    # We need to enable NAT on the default namespace

    $IPTABLES -t nat -A POSTROUTING -j MASQUERADE

    # This assigns the other end of the tunnel, veth2, to the new 
    # namespace, gives it an IP address in the same net as the bridge above, 
    # brings up this and the (essential) lo interface, sets up the 
    # routing table by assigning the bridge interface in the default namespace
    # as the default gateway, creates a new terminal in the new namespace and 
    # stores its pid for the purpose of tearing it cleanly, later. 

    $IP link set veth-b$1 netns $1
    $IP netns exec $1 $IP addr add $IP_BASE.$Nns.2/24 dev veth-b$1
    $IP netns exec $1 $IP link set veth-b$1 up
    $IP netns exec $1 $IP link set dev lo up
    $IP netns exec $1 $IP route add default via $IP_BASE.$Nns.1
    $IP netns exec $1 su -c $XTERM $SUDO_USER &
    $IP netns exec $1 echo "$!" > $PID



}

stop_nns() {

# Check that the namespace to be torn down really exists

    $IP netns list | /bin/grep $1 2>&1 1> /dev/null
    if [ ! $? == 0 ]; then
            echo "Network namespace $1 does not exist,"
            echo "please choose another name"
            exit 1
    fi

    # This kills the terminal in the separate namespace, 
    # removes the file and the directory where it is stored, and tears down
    # all virtual interfaces (veth1, tap0, the bridge, veth2 is automatically
    # torn down when veth1 is), and the NAT rule of iptables. 

    /bin/kill -TERM $(cat $PID) 2> /dev/null 1> /dev/null
    /bin/rm $PID
    /bin/rmdir $OUTDIR
    $IP link set br$1 down
    $IP link del br$1
    $IP netns del $1
    $IP link set veth-a$1 down
    $IP link del veth-a$1
    $IP link set tap$1 down
    $IP link del tap$1
    $IPTABLES -t nat -D POSTROUTING -j MASQUERADE
    /bin/rm /etc/netns/$1/resolv.conf
    /bin/rmdir /etc/netns/$1

}


case $2 in
    start)
            prelim "$1"
            start_nns $NNSNAME
            ;;
    stop)
            prelim "$1"
            stop_nns $NNSNAME
            ;;
    reload)
            prelim "$1"
            stop_nns $NNSNAME
            prelim "$1"
            start_nns $NNSNAME
            ;;
    *)
 # This removes the absolute path from the command name

            NAME1=$0
            NAMESHORT=${NAME1##*/}

            echo "Usage:" $NAMESHORT "name action,"
            echo "where name is the name of the network namespace,"
            echo "and action is one of start|stop|reload"
            ;;
 esac

If you want, you can even start a whole desktop inside the new network namespace, by means of

            sudo startx -- :2 

then you can search for it using Alt+Ctrl+Fn, where Fn is one of F1,F2,....-

I need to add one caveat: DNS handling inside namespaces is a bit buggy, be patient.


I know the question is tagged fedora, but I just got the newns script from the excellent answer by @MariusMatutiae to work on Ubuntu 18.04 - so I thought I should jot down my notes, as there are several steps I found non-trivial.

As it is in the answer, newns seems to work fine on Ubuntu 18.04 at first; however, one can quickly realize, that after one has ran the command newns MYNS start and the new xterm has been raised, only the xterm still has network connectivity (rather, DNS resolution, since I got ping: google.com: Name or service not known; some things I tried by IP still seemed to work fine), while the rest of the system does not - and that situation persists, until you shutdown with newns MYNS start.

The thing is, I connect to the Internet from my Ubuntu 18.04 over Wi-Fi, and the Wi-Fi router (or Ubuntu?) assigns both IPv6 and IPv4 addresses to the Wi-Fi network adapter (which, ifconfig tells me, is called wlp2s0 in my Ubuntu). So at first I thought this is a problem due to IPv6, but it seems to not be - as eventually I found Use VPN connection only for selected applications ( more specifically https://superuser.com/a/1262250 ), with the critical comment being:

As my default interface is a wireless one, I use wl+ (which may match wlan0, wlp3s0, etc.) in iptables for the outgoing interface; if you use a wired interface you should probably use en+ (or br+ for a bridged interface)

So, I did this replacement in the original newns:

@@ -134,7 +134,8 @@
 
     # We need to enable NAT on the default namespace
 
-    $IPTABLES -t nat -A POSTROUTING -j MASQUERADE
+    #$IPTABLES -t nat -A POSTROUTING -j MASQUERADE
+    $IPTABLES -t nat -A POSTROUTING -s $IP_BASE.$Nns.1/24 -o wl+ -j MASQUERADE
 
     # This assigns the other end of the tunnel, veth2, to the new
     # namespace, gives it an IP address in the same net as the bridge above,
@@ -181,7 +182,8 @@
     $IP link del veth-a$1
     $IP link set tap$1 down
     $IP link del tap$1
-    $IPTABLES -t nat -D POSTROUTING -j MASQUERADE
+    #$IPTABLES -t nat -D POSTROUTING -j MASQUERADE
+    $IPTABLES -t nat -D POSTROUTING -s $IP_BASE.$Nns.1/24 -o wl+ -j MASQUERADE
     /bin/rm /etc/netns/$1/resolv.conf
     /bin/rmdir /etc/netns/$1

If you replace only the -A POSTROUTING line (without changing the -D POSTROUTING line), you will get iptables: No chain/target/match by that name. when you try to run newns MYNS stop after newns has been started.

Another relevant note here may be, that I had stored the VPN I want to connect to in Gnome's Network Manager. However, it seems that you can only call the command line openvpn in the xterm from newns (if you try starting the VPN via Network Manager desktop GUI for rest of the system, in hopes that the xterm will remain "direct"/non-VPNed, then no network connection will resolve from anywhere, until you shutdown VPN from Network Manager).

So, here is the problem: Network Manager actually saves the VPN settings as a "network connection" in /etc/NetworkManager/system-connections ( https://askubuntu.com/questions/27168/config-import-on-network-manager-openvpn ), and I have no idea where it saves related passwords; and this can definitely not be used by command-line openvpn client directly. Luckily, I had saved the original .ovpn file (that I had imported in Network Manager to create the VPN connection there), so then I have to do, in the xterm started from newns:

sudo openvpn --config /path/to/myfile.ovpn

... upon which I'm asked for username and password - and when entered successfully, openvpn blocks.

And that is the other problem - since if openvpn blocks, then I cannot start any other program in that xterm :) So one idea would be to run openvpn in background (as "service"), however, just appending ampersand actually runs sudo in background, and you cannot enter username and password (that, assuming you cached the sudo password previously):

user@PC:~$ sudo openvpn --config /path/to/file.ovpn &
[6] 3113
user@PC:~$ Thu Aug 20 20:29:51 2020 OpenVPN 2.4.4 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on May 14 2019
Thu Aug 20 20:29:51 2020 library versions: OpenSSL 1.1.1  11 Sep 2018, LZO 2.08
Enter Auth Username: RemoteUser

[6]+  Stopped                 sudo openvpn --config /path/to/file.ovpn
RemoteUser: command not found

... and neither you can wrap the whole thing in bash -c:

user@PC:~$ sudo bash -c 'openvpn --config /path/to/file.ovpn &'
user@PC:~$ Thu Aug 20 20:35:05 2020 OpenVPN 2.4.4 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on May 14 2019
Thu Aug 20 20:35:05 2020 library versions: OpenSSL 1.1.1  11 Sep 2018, LZO 2.08

Broadcast message from root@MyPCName (Thu 2020-08-20 20:35:05 CEST):

Password entry required for 'Enter Auth Username:' (PID 3261).
Please enter password with the systemd-tty-ask-password-agent tool!

... so actually, what I have to do is this:

  • run newns MYNS start - it starts an xterm
  • From that xterm, first run xterm & - to get another xterm, with the same ("newns") network settings
  • Now in the first xterm, run sudo openvpn --config /path/to/file.ovpn - it will block, but that is no problem now, because we can:
  • Switch to second xterm, confirm with ifconfig that the tunnel for openvpn has been made, then with wget -qO- ifconfig.co that you have the VPN public IP in that terminal - then you can run whatever network using programs that you want to run through the VPN

So all good - the only problem for me, is that I cannot run gnome-terminal, which I prefer, from the second xterm - rather, I can, but then it connects to the system Network Manager and apparently uses its network, and not that of the parent xterm that spawned it. (EDIT: posted about this here https://unix.stackexchange.com/questions/605485/possible-to-start-gnome-terminal-from-xterm-inheriting-xterms-network-settings - turns out, starting mate-terminal & from this xterm preserves the VPN network configuration - and so do subsequent tabs opened in this mate-terminal window... and also changing export XTERM=/usr/bin/mate-terminal in the newns script also seems to work)