Tag: routing
NAT connection pinning with iproute2 / iptables
by mithrandi on Oct.23, 2011
My home network has a somewhat complicated setup where I have multiple PPPoE sessions across my ADSL connection, with various different ISPs. This allows me to take advantage of varying ISP properties such as cost and latency, by routing different traffic over different connections. Naturally, each of these connections only affords me a single IPv4 address, so I make use of NAT to allow the rest of my network access to the Internet. A potential problem arises, however, when connections go down and come back up. In the simple case, with only one connection, MASQUERADE takes care of all the details; when the interface goes down, all of the NAT entries associated with the connection are removed, so when it comes back up, it’s not a problem that your IP address has changed, because all of the NAT entries associated with the old address are gone. This works just as well in the multiple connections scenario; if an interface goes down resulting in traffic being routed over another interface, all of the old NAT entries have been dropped, so new ones will be established associated with the interface they are now travelling over. The problem arises when the interface that went down comes back up; traffic will now be routed over the first interface again, while still being rewritten to the second interface’s address, and this traffic is almost guaranteed to be dropped by either your ISP, or their upstream provider.
What’s the solution? Well, if you absolutely definitely want to start routing traffic over the first interface as soon as it comes back up, you’re going to need to flush the associated conntrack NAT entries as soon as it comes up, and let all your users reconnect (since their connections will be interrupted); I’m not entirely sure how to do this. In my case, however, I’m more concerned with maintaining existing connections without interruption, even if that means continuing to route them over the “wrong” interface. This also applies to incoming connections; ordinarily if somebody tries to establish a connection to the public IP address of one of your connections, they will need to connect to the same interface that outbound traffic to them would be routed over, which can be somewhat inconvenient.
My solution is something I’m going to call “connection pinning”. The idea is that once an outbound interface has been selected for a particular connection (by the Linux routing table), we “pin” the connection to that interface, so that traffic associated with that connection always travels over that interface even if the routing table changes. In order to achieve this, we can use a combination of Linux policy routing (ip rule), as well as firewall / conntrack packet marking. When a connection is first established, we set a connmark, which is a value stored in the conntrack table entry for that connection. In the case of an incoming connection, we set the mark based on the interface the packet arrived on; in the case of an outgoing connection, we set the mark in POSTROUTING based on the outbound interface already selected by the routing table. Then, for future outgoing traffic associated with that connection (as determined by conntrack), we set an fwmark based on the connmark, and bypass the normal routing table using policy rules for traffic marked thusly.
This is implemented in three parts. Firewall rules added using iptables, for the netfilter/conntrack bits; an ip-up script for establishing policy rules and routes when a PPP connection is established; and an ip-down script for flushing them again when the PPP connection is terminated.
First, the firewall rules (using the excellent ferm tool):
@def $DEV_PRIVATE = eth0;
@def $NET_PRIVATE_V4 = 10.0.0.0/24;
domain ip table mangle {
# Only match new connections; established connections should
# already have a connmark, which should not be overwritten.
chain (INPUT FORWARD) {
# Unfortunately the set-mark rules need to be duplicated for
# each ppp interface we have.
mod conntrack ctstate NEW {
interface ppp0 CONNMARK set-mark 1;
interface ppp1 CONNMARK set-mark 2;
interface ppp2 CONNMARK set-mark 3;
interface ppp3 CONNMARK set-mark 4;
interface ppp4 CONNMARK set-mark 5;
}
}
chain POSTROUTING {
mod conntrack ctstate NEW {
outerface ppp0 CONNMARK set-mark 1;
outerface ppp1 CONNMARK set-mark 2;
outerface ppp2 CONNMARK set-mark 3;
outerface ppp3 CONNMARK set-mark 4;
outerface ppp4 CONNMARK set-mark 5;
}
}
chain PREROUTING {
# Copy the connmark to the fwmark in order to activate the
# policy rules for connection pinning. Only do this for
# traffic originating from the local network; other traffic
# (such as traffic going *to* the local network) should be
# left unmodified, to allow return traffic to be routed over
# the correct interface.
interface $DEV_PRIVATE daddr ! $NET_PRIVATE_V4 CONNMARK restore-mark;
}
chain OUTPUT {
# Same as above, but for locally originating traffic.
daddr ! $NET_PRIVATE_V4 CONNMARK restore-mark;
}
}
# I am assuming you already have something like this:
domain ip table nat {
chain POSTROUTING outerface (ppp0 ppp1 ppp2 ppp3 ppp4) MASQUERADE;
}
If you’re not using ferm, here’s what the raw iptables commands would be (these are exactly what ferm will install given the above, so this is just more verbose):
iptables -t mangle -A FORWARD --match conntrack --ctstate NEW --in-interface ppp0 --jump CONNMARK --set-mark 1 iptables -t mangle -A FORWARD --match conntrack --ctstate NEW --in-interface ppp1 --jump CONNMARK --set-mark 2 iptables -t mangle -A FORWARD --match conntrack --ctstate NEW --in-interface ppp2 --jump CONNMARK --set-mark 3 iptables -t mangle -A FORWARD --match conntrack --ctstate NEW --in-interface ppp3 --jump CONNMARK --set-mark 4 iptables -t mangle -A FORWARD --match conntrack --ctstate NEW --in-interface ppp4 --jump CONNMARK --set-mark 5 iptables -t mangle -A INPUT --match conntrack --ctstate NEW --in-interface ppp0 --jump CONNMARK --set-mark 1 iptables -t mangle -A INPUT --match conntrack --ctstate NEW --in-interface ppp1 --jump CONNMARK --set-mark 2 iptables -t mangle -A INPUT --match conntrack --ctstate NEW --in-interface ppp2 --jump CONNMARK --set-mark 3 iptables -t mangle -A INPUT --match conntrack --ctstate NEW --in-interface ppp3 --jump CONNMARK --set-mark 4 iptables -t mangle -A INPUT --match conntrack --ctstate NEW --in-interface ppp4 --jump CONNMARK --set-mark 5 iptables -t mangle -A POSTROUTING --match conntrack --ctstate NEW --out-interface ppp0 --jump CONNMARK --set-mark 1 iptables -t mangle -A POSTROUTING --match conntrack --ctstate NEW --out-interface ppp1 --jump CONNMARK --set-mark 2 iptables -t mangle -A POSTROUTING --match conntrack --ctstate NEW --out-interface ppp2 --jump CONNMARK --set-mark 3 iptables -t mangle -A POSTROUTING --match conntrack --ctstate NEW --out-interface ppp3 --jump CONNMARK --set-mark 4 iptables -t mangle -A POSTROUTING --match conntrack --ctstate NEW --out-interface ppp4 --jump CONNMARK --set-mark 5 iptables -t mangle -A PREROUTING --in-interface eth0 ! --destination 10.0.0.0/24 --jump CONNMARK --restore-mark iptables -t mangle -A OUTPUT ! --destination 10.0.0.0/24 --jump CONNMARK --restore-mark iptables -t nat -A POSTROUTING --out-interface ppp0 --jump MASQUERADE iptables -t nat -A POSTROUTING --out-interface ppp1 --jump MASQUERADE iptables -t nat -A POSTROUTING --out-interface ppp2 --jump MASQUERADE iptables -t nat -A POSTROUTING --out-interface ppp3 --jump MASQUERADE iptables -t nat -A POSTROUTING --out-interface ppp4 --jump MASQUERADE
Next, the ip-up script (to be placed in /etc/ppp/ip-up.d/ and made executable):
#!/bin/sh
TABLE="$PPP_IFACE"
MARK=$((${PPP_IFACE##ppp} + 1))
ip rule del lookup "$TABLE"
ip route flush table "$TABLE"
ip route add default dev "$PPP_IFACE" table "$TABLE"
ip rule add fwmark "$MARK" table "$TABLE"
Finally, the ip-down script (to be placed in /etc/ppp/ip-down.d/ and made executable):
#!/bin/sh TABLE="$PPP_IFACE" ip rule del lookup "$TABLE" ip route flush table "$TABLE"
There are a couple of changes you will need to make to adapt these for your own network. In particular, you’ll need to duplicate the pppN iptables rules for each of the PPP interfaces you want to apply this to. Also, if you are already doing packet marking for some other reason, you’ll need to change the fwmark values I’ve used to ones that don’t interfere with your existing marks. I suspect there’s a better way to only mark outbound traffic than what I do above, but I wasn’t able to figure it out. If you have any improvements to suggest, feel free to mention them in the comments; I will try to keep this post updated with any improvements I make (either on my own, or based on other people’s suggestions).
Dual-path routing with Quagga
by mithrandi on Mar.21, 2007
tags:
Well, I guess it took me long enough, but as promised, how I setup dual-path routing with Quagga. But first, thanks and credit to Colin and Jonathan for helping me out with this on IRC; as they had done similar work before, it was a lot easier than blazing the trail myself.
So, what the heck am I talking about? Let’s start off with a little background for those of you unfamiliar with the state of ADSL internet access in South Africa. Aside from the costs paid to the local telecommunications monopoly for the physical service provision, there is also an ISP charge for actually getting onto the internet, which includes charging for bandwidth usage (throughput). Exact pricing varies slightly between ISPs, but is mostly the same across the board, so I’ll quote prices from the ISP I use. To cut a long story short, I pay about R70 (US$9.46) per GB of international transfer (in both directions), but I can get local-only connectivity at a much cheaper rate of around R4.33 (US$0.59) per GB. This clearly makes it economically advantageous to do local transfers over a local-only account.
So, how to do it? My internet gateway, elvandar, is naturally running Debian GNU/Linux, so this can easily be setup. Please note that the following instructions are primarily designed for a Debian system, and your mileage may vary with other distributions.
-
Configure your PPP connections appropriately.
Make a copy of your existing
/etc/peers/$PROVIDERfile for the second PPP connection; you will want to change theusernamesetting in the new file, and also remove thedefaultroutesetting, so that the default route will always point at the first PPP connection.. Next, add aunit 0setting to the first file, and a unitunit 1setting to the second (assuming you don’t already have these or similar); this ensures that theppp0andppp1devices (respectively) will always be used, rather than the order of the connections coming up determining how devices are assigned.For reference, my peer files are called
saixandis, and I have the following stanzas in/etc/network/interfacesto bring them up:auto dsl iface dsl inet ppp provider saix auto dsl-is iface dsl-is inet ppp provider is -
Install Quagga.
# aptitude install quagga -
Configure Quagga.
My configuration looks like the following:
hostname elvandar ### TENET # UCT-C1 ip route 196.21.0.0/19 ppp1 ### IS # ISNET-03 ip route 196.26.0.0/16 ppp1 ### Verizon # HC1-20050912 ip route 196.40.96.0/20 ppp1 # HETZNER-ZA ip route 196.22.132.0/22 ppp1 ip route 196.22.136.0/21 ppp1 ### Datapro # GIA-BLK6 ip route 196.41.192.0/19 ppp1
Initially I had plans to retrieve a list of local routes from public-route-server.is.co.za and use that, but I discovered that I was tweaking my routing a lot, so I decided to just stick with setting specific routes for hosts that I wanted them for.
-
Enable the Zebra daemon.
Edit
/etc/quagga/daemonsand setzebra=yes. -
Make sure the configuration changes take effect.
# invoke-rc.d quagga force-reload
And there you have it. Zebra will automatically add the routes as the appropriate interface(s) come up, thus you don’t need to worry about the routes persisting across reconnections etc. (which is a good thing, since currently all ADSL connections are terminated after they have been active for 24 hours… argh!)