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:
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.