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.