Linux: routing based on domain names

On ubuntu 16.04, I would like to route my traffic either through the direct internet eth0 or my VPN tun0 based on the domain name entered into the browser. The reason being local sites are either slow or location dependent.

I realize the kernel routing table is IP based and domain names are usually resolved in the software layer, but with linux being a script friendly platform, I'm hoping for a workaround. Though, I have no idea how to write such a script.

So far I've found the dig example.com +short @8.8.8.8 command will list the IPs associated with a domain, and I've figured out the sudo route add -net 8.8.8.8 netmask 255.255.255.255 gw 192.168.2.1 command will bypass the VPN for a given IP (where 192.168.2.1 is my default eth0). Would somebody be kind enough to template a script which reads a file containing the domain names and enters the route rules upon system boot. Bonus points for allowing masked sub domains *.example.com.

If there is an easier method to this madness I'll accept it as a solution.

Note: I could very easily hard code the IPs into /etc/network/interfaces but then they become hard to manage. I also tried hard coding all known IPs for my country into this file, but it was very hit and miss along with a delayed boot up time.


I'd recommend you to avoid managing routing based on domain names (by the way, it is also impossible to resolve wildcard subdomain, whether it is bonus points for it or not :D)

To be a bit descriptive, you shouldn't do that because:

  1. some domains changes their IPs time to time,

  2. it is impossible to match wildcards in subdomains

  3. it is impossible to know/fetch all subdomains of any domain

  4. any random subdomain can have any random IP address.

So, the solution as browser addon (and/or custom local proxy like squid) is the best option for your issue.

But, I guess, "FoxyProxy" addon (it is originally Firefox addon, but AFAIRC, also a Chrome version exists) is exactly what you want.

And, also, answering to your notice that "FoxyProxy is paid service and you already have your vpn":

FoxyProxyPlus is paid service, but not FoxyProxy.

FoxyProxy is addon, available for major browsers:

Standard Edition (Firefox) | Basic Edition (Firefox)

Standard Edition (Chrom{e,ium}) | Basic Edition (Chrom{e,ium})

So, if you want to go to some domains through VPN, you should:

  1. write rules for foxyproxy to go through you squid instance for the list of domains

  2. and/or write the rules list for squid

  3. capture http/https traffic not owned by squid with iptables and point it to squid by rule like this:

iptables -m owner -m multiport -t nat -A OUTPUT ! -o lo ! --uid-owner $squid_user_id -p tcp --dports 80,443,8080,... -j REDIRECT --to-ports $SQUID_PORT

(--syn option may be needed for -p tcp)

  1. capture http/https traffic owned by squid, and mark it for next routing it to VPN with rule like this:
iptables -A OUTPUT -m owner --uid-owner $squid_user_id -j MARK --set-mark 11
echo 11 forcevpn >> /etc/iproute2/rt_tables
ip rule add fwmark 11 table forcevpn
ip route add default via 10.0.0.1 table forcevpn

where 10.0.0.1 is you gateway inside VPN. Or you can use dev $VPN_IF instead of via 10.0.0.1 if you have no gateway and just want to push all the traffic in the vpn interface.

  1. optionally, you may be need to run sudo sysctl ipv4.conf.all.rp_filter =0

===

And one more thing:

If you want to do same magick with non-http(s) TCP traffic, you'll be need something like proxychains, and perform similar capturing magic.

And, if you want to do that magic with UDP, I've a bad news: I don't know any proxy capable of proxying of UDP (because of the nature of this protocol) :)

⇓⇓⇓ EDIT ⇓⇓⇓

In case, you want the reverse thing (default gw = vpn, and rule some domains directly through ISP), it can be:

  1. write rules for foxyproxy to go through you squid instance for the list of domains

  2. capture traffic owned by squid, and mark it for next routing it another way with rule like this:

iptables -A OUTPUT -m owner --uid-owner $squid_user_id -j MARK --set-mark 11
echo 11 novpn >> /etc/iproute2/rt_tables
ip rule add fwmark 11 table novpn
ip route add default via ${ISP_GW} table novpn

where ISP_GW is the gateway you use to route traffic to your VPN server. Some users may want to use dev ppp0 (or ppp1, ..., pppN) instead of via ${ISP_GW} in case if they use pptp to connect to the internet.


Routing based on destination domain is not impossible, and, with the right tools, not all that hard.

I'll present a few methods that require little or no special client side configuration. These all assume you are using OpenVPN to connect. This should be achievable with other VPNs, but may require more manual configuration after the VPN is brought up.

For example purposes, I'll use the domains "example.com", "us1.example.com", "us2.example.com", and "geoblocked.com" for the domains we want to route through the non-VPN interface.

All commands should be run as root.

Method 1 - OpenVPN

I would only recommend this if you are certain the IP addresses you are routing have static IPs that never change.

Pros:

  • Extremely simple

Cons:

  • Only reliable for domains with IPs that never change
  • Need an explicit entry for every domain and subdomain

Method:

Add the following lines to your OpenVPN configuration:

route example.com     255.255.255.255 net_gateway
route us1.example.com 255.255.255.255 net_gateway
route us2.example.com 255.255.255.255 net_gateway
route geoblocked.com  255.255.255.255 net_gateway

Restart OpenVPN.

That's it, but you'll have to restart VPN again if those IP addresses ever change.

NOTE: Some sources say you also need to specify allow-pull-fqdn, but that did not seem to be the case in my experience. YMMV.

Method 2 - Policy based routing

Policy based routing is the ability to route based on certain criteria; commonly a source address or protocol, but in this case we will inspect the destination domain name prior to routing and use marked packets ("fwmark").

So what we need to do first is create a separate table for your VPN routed packets, so that we can mark those that go through the VPN, and passing marked packets through the non-VPN interface. (Keep in mind, this is one approach and there are many other ways to approach this, such as letting the VPN do its routing as normal through the main table and creating a separate table for non-VPN traffic.)

Your kernel must be recent enough and have the proper modules, although modern systems probably have them in their default kernels.

The name "vpn_table" (the routing table name), and the numbers "201" (routing table ID) and "3" (fwmark) are arbitrarily chosen.

Create the new routing table (as root):

echo 201 vpn_table >> /etc/iproute2/rt_tables

Configure OpenVPN:

Create the following script somewhere (I am calling it "/etc/openvpn/client/setup-routing") and make it executable:

#!/bin/bash
ip route add 0.0.0.0/1 via $route_vpn_gateway dev $dev scope global table vpn_table
ip route add 128.0.0.0/1 via $route_vpn_gateway dev $dev scope global table vpn_table
sysctl -w net.ipv4.conf.$dev.rp_filter=2

# You can optionally leave the next two lines out but run the `ip rule add`
# command at each boot instead
ip rule del fwmark 3 table vpn_table &>/dev/null # This might fail but that's ok
ip rule add fwmark 3 table vpn_table

The variables in the above script will be populated as environment variables by OpenVPN. Also note this sets up routing to all addresses through the VPN gateway in the "vpn_table" routing table. If your VPN setup requires more complex routing, refer to the OpenVPN documentation and adjust accordingly.

Add the following to your OpenVPN configuration:

## Policy routing
route-noexec
script-security 2
route-up /etc/openvpn/client/setup-routing

The "route-noexec" line permits OpenVPN to fetch the route from the server, but prevents it from actually populating the routes. Instead the route-up script is called. "script-security 2" is necessary to call a user-defined script.

That is all the necessary set up to route the marked packets, but we need to set up a way to actually mark the packets. Two options are using dnsmasq with ipset, or setting up a squid proxy.

Method 2a - Policy based routing using ipset and dnsmasq

I would recommend this method if you are already running this on a dnsmasq-based router or your clients do not support proxy config. This is effectively the same as a caching DNS that updates the routing table whenever a domain name is looked up.

Pros:

  • Handles subdomains
  • Works on devices that can't access proxies (do those exist?)

Cons:

  • Does not handle referrer field (see Method 2b)
  • Needs complicated ipset and iptables configs
  • Requires the VPN connected system to be set up as a router (needs dedicated interface)
  • I have no idea how scalable ipset is (my use case is for a whole ccTLD)

This assumes you already have dnsmasq configured and set up, and acting as a gateway and DNS server for clients connected to a dedicated interface "eth1".

Create the ipset:

ipset create SKIP_VPN_IPSET iphash

Tell iptables to mark the ipset packets (n.b., this must be done after creating the ipset list):

# Mark ALL packets coming in on eth1 - change this to the interface dnsmasq listens on
iptables -A PREROUTING -i eth1 -t mangle -j MARK --set-mark 3

# REMOVE mark on any addresses that match our ipset
iptables -A PREROUTING -t mangle -m set --match-set SKIP_VPN_IPSET dst -j MARK --set-mark 0/3

NOTE: The above commands (ipset and iptables) must be run at each boot. Alternatively, your OS may provide some options for saving/restoring iptable rules and ipsets.

NOTE2: There is documented an inverse ! --match-set but that resulted in all packets just disappearing when I tried it.

Add the following to your dnsmasq.conf:

ipset=/example.com/geoblocked.com/SKIP_VPN_IPSET

Obviously adjust that line too whichever domain names you want routed. This will also add ALL subdomains to the ipset, so you do not need to explicitly specify them. Even using a TLD will work.

Restart dnsmasq and set up your clients to use the VPN connected system as both a gateway and DNS (which should be implied if it is set up as a DHCP server).

Method 2b - Policy based routing using squid

This is my favorite method and works well with my PS4 and other devices I use to connect.

Pros:

  • Handles subdomains
  • Handles referrer field
  • Does not require replacing your existing router
  • Clients (browsers) can optionally use it or not

Cons:

  • Clients must support proxy connection

This assumes you have a working squid setup and a basic knowledge of squid configuration.

Add the following lines to squid.conf:

# redirect example domains
acl domain_to_remote_proxy dstdomain .example.com
acl ref_to_remote_proxy referer_regex [^.]*\.example.com.*

# redirect geoblocked domain
acl domain_to_remote_proxy dstdomain .geoblocked.com
acl ref_to_remote_proxy referer_regex [^.]*\.geoblocked.com.*

# mark packets that we want routed through the VPN
tcp_outgoing_mark 0x03 !ref_to_remote_proxy !domain_to_remote_proxy

Note there are 2 lines per domain, and subdomains are matched. The first line checks the destination domain, and the second matches the "Referer" header. This is useful because browsers send the referer when fetching content on a webpage such as images, CSS, or javascript; this means that content requested by the site will also route through the non-VPN address even if it is hosted on a different domain (e.g., example-cdn.com).

On the clients, set up the connection like normal but set the proxy settings to use the proxy server and port for this system. Most devices (including game consoles) allow system-wide configuration. On PCs, most browsers can be configured to use a proxy independently of system settigs.

Final note - My use case is actually to route specific domains through the VPN and everything else through the non-VPN. The methods are similar to the above, but inverted.


Squid does not support socks (like ssh tunnel)... there is an option to build squid with socks support, but it's hard to get it to work.

Privoxy can do the job

  • Support parent socks
  • Support http/https proxy
  • Support referrer
  • etc.

Privoxy Setup:

  1. Install privoxy

  2. Edit the config file (remove everything on /etc/privoxy and add /etc/privoxy/config)

    user-manual /usr/share/doc/privoxy/webserver/user-manual
    confdir /etc/privoxy
    logdir /var/log/privoxy
    actionsfile default.action
    filterfile default.filter
    logfile logfile
    toggle  1
    enable-remote-toggle  0
    enable-remote-http-toggle  0
    enable-edit-actions 0
    enforce-blocks 0
    buffer-limit 4096
    enable-proxy-authentication-forwarding 0
    forwarded-connect-retries  0
    accept-intercepted-requests 0
    allow-cgi-request-crunching 0
    split-large-forms 0
    keep-alive-timeout 5
    tolerate-pipelining 1
    socket-timeout 300
    listen-address  127.0.0.1:8888
    forward-socks5 .whatismyipaddress.com 127.0.0.1:8080 . 
    forward-socks5 .whatismyip.com 127.0.0.1:8080 . 
    
  3. Restart the service

    systemctl start privoxy

  4. Setup the privoxy proxy on the client application

  5. If you want to route referrer as well add default.action and default.filter you can test that with http://www.play-hookey.com/htmltest/ with the html code

    <a href="http://amibehindaproxy.com/">test-ip</a></br>
    <a href="http://www.stardrifter.org/cgi-bin/ref.cgi">test-referrer</a>  
    
  6. default.action and default.filter

    default.action

    {+client-header-tagger{referer}}
    /
    
    {+forward-override{forward-socks5 127.0.0.1:8080 .}}
    TAG:.*?hookey.com 
    

    default.filter

    CLIENT-HEADER-TAGGER: referer
    s@^Referer:.*?$@$0@i
    
  7. Restart privoxy service