3

I'm currently dealing with a VPN which connection endpoint lies within the subnet which prefix shall be tunneled via that specific VPN.

Essentially the problem thus boils down to match against a (larger) set of destination addresses (/16 mask), except for a wholly contained (small) subset of specific destinations that shall not routed that way (and instead go via the default route).

If the problem was about filtering, in Linux using this could be implemented using a ipset set up like

ipset create MyVPN hash:net
ipset add MyVPN $MYVPNNET/$MYVPNMASK
ipset add MyVPN $MYVPNENDPOINT nomatch

However such ipsets can only be used inside netfilter.

Now my question is, how I could set up something equivalent using Linux Advanced IP Routing, i.e. the ip route family of commands?

datenwolf
  • 289
  • 1
  • 11
  • Questions seeking installation, configuration or diagnostic help must include the desired end state, the specific problem or error, sufficient information about the configuration and environment to reproduce it, and attempted solutions. Questions without a clear problem statement are not useful to other readers and are unlikely to get good answers. – djdomi Mar 15 '22 at 04:55
  • 1
    @djdomi: This is not seeking help for a specific configuration. I'm looking for a way to define routes (in a generalized way) by removing certain subsets from route table rule. This is something that could be applied to all sorts of situations. – datenwolf Mar 15 '22 at 09:43

2 Answers2

3

As it turns out, in Linux one can use ipset matching to select routing tables. The extra ingredient is using a netfilter rule that will match the outgoing packets and apply a fwmark to them, which then can be used to select a dedicated routing table. This dedicated routing table then will contain just a single default route through the VPN.

Here's in script form the general idea, how to set this up.

# Subnet(s) to route through the TUN

DSTNETS=.../.. .../..

# Exclude these destinations from routing through the TUN

EXCLUDE=... ...

# TUN device to use for this connection, and its parameters Those usually are supplied by the VPN daemon

TUNDEV=...
TUNADDR=.../..
TUNPEER=...

# Name for the ipset used to match destinations. Base on the TUN name

IPSET=${TUNDEV}ipset

# We need a netfilter fwmark and a iproute2 table. fwmarks and tables are 32 bit values, limited to the signed integer range, i.e. [0, 2³¹-1] fwmarks may be used as bitmasks signalling multiple flags, so depending on our needs either set a single particular bit (or few), or a very specific value that is then compared for equality.

FWMARK=0x...
TABLE=0x...

# Create the ipset and populate it with the IP address ranges and subnets to match to and not to.

ipset create $IPSET hash:net
for d in $DSTNETS ; do ipset add $IPSET $d ; done
for x in $EXCLUDE ; do ipset add $IPSET $x nomatch ; done

# Create a new iproute2 ruletable that will be used for route lookup for all packets that have our fwmark of choice set

ip rule add fwmark $FWMARK table $TABLE

# This is where the magic happens: Create a netfilter rule that will match packets originating on this host, for destinations that match the ipset we just created and mark them with our chosen fwmark. Due to the rule we just created before, those packets will then be routed using that specific table, instead the global ones.

iptables -t mangle -A OUTPUT -m set --match-set $IPSET dst -j MARK --set-mark $FWMARK

# Also add a netfilter rule to overwrite the source address for these packets since the destination network will likely reject them, if they don't match the address range used for the VPN

iptables -t nat -A POSTROUTING -m set --match-set $IPSET dst -j SNAT --to-source $INTERNAL_IP4_ADDRESS

# Now we can set up the actual TUN device. Strictly speaking those steps could have been done before, but then for a short time between the TUN coming up and establishing the routing rules some packets might have ended up in limbo

ip link set dev $TUNDEV up
ip addr add $TUNADDR peer $TUNPEER dev $TUNDEV 

# Finally establish a default route going through the TUN in our dedicated routing table which due to the fwmark rule will be hit only by packets matching to our ipset

ip route add default dev $TUNDEV table $TABLE

To tear everthing down, just follow the script in reverse, deleting stuff; the ipset can be destroyed with a single command.

datenwolf
  • 289
  • 1
  • 11
2

Routes are calculated on a most-specific-first approach. So if your small subnet needs the default route, put that first and then add the others:

Default Gateway in this example is 10.0.0.1 VPN is 10.0.0.2

ip route add 10.0.1.0/24 via 10.0.0.1
ip route add 10.2.0.0/16 via 10.0.0.2
ip route add default via 10.0.0.1

Would route traffic to the subnet (10.0.1.0) via default gateway, VPN via 10.0.0.2, and other default traffic via default gateway.

shearn89
  • 3,403
  • 2
  • 15
  • 39
  • 1
    Yes, this is how most VPN scripts are doing it. But there's a problem: By adding a narrow route via a specific gateway, such a setup is incapable to seamlessly deal with changes of the default route. For example when switching networks in a roaming situation, or maybe even something as simple as docking your laptop (switching from W-LAN to wired LAN). This needlessly overcomplicates things, since if done as you suggested, there'd have to be a continuous tracking of networking state, detecting if specific routes have to be altered, and the routing tables being adjusted. – datenwolf Mar 15 '22 at 09:41
  • 1
    What I'm looking for is an equivalent of *ipset* `nomatch` to avoid all that hassle. – datenwolf Mar 15 '22 at 09:42
  • Ah, okay - I understand! I'll leave the answer up to retain the comments, since they're important clarifications. – shearn89 Mar 15 '22 at 10:25
  • @shearn89 remind if it solves your issue, you need to accept it, else we will be remembered until the end of any decade... – djdomi Mar 15 '22 at 12:14
  • @djdomi I'm not the OP... – shearn89 Mar 15 '22 at 13:53
  • 1
    yupp nott scrolled enough above. @datawolf you were meaned with my comment – djdomi Mar 15 '22 at 17:45
  • @djdomi: See the answer I wrote, regarding the method I figured out. I'll make that one the accepted answer after the grace period passed. – datenwolf Mar 16 '22 at 00:11