Adding and Removing Routes in the Linux Routing Table
We will go through how to add and remove null routes in c++, though there is a lot more you can do with Linux’ routing tables.
For this project I want to use the routing tables like a firewall. I have a home built web server (you’re using it now!) that like all web servers is under constant attack by all manner of automated hacking and span scripts.
In an earlier article I showed how to ban these using TCP_REPAIR. In that case we have to accept the connection first, before silently closing the connection. With routing tables, once we identify an abusive remote IP address, we can avoid connecting at all, start ignoring their packets completely.
Viewing the Linux Routing Table on the Command Line
It is instructive to first understand what the routing table does and how it can be viewed.
The routing table lives in the Linux kernel. When the kernel receives an ethernet packet on any interface, it has to decide what to do with it. Should this packet be processed on the local system? Should it be forwarded? Or dropped? Similarly when writing a packet to the network, the routing tables are used to determine which network interface that packet should be written to ( if you have multiple interfaces ).
One way to view the routing table is with the route -n
command:
user@system: route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 72.93.243.1 0.0.0.0 UG 0 0 0 eth0 72.93.243.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
You have to be root to use the route
command.
Another way is read the file /proc/net/route
. The content looks something like this:
Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT eth0 00000000 01F35D48 0003 0 0 0 00000000 0 0 0 eth0 00F35D48 00000000 0001 0 0 0 00FFFFFF 0 0 0
On most machines with a single network interface you will usually see two routes, an incoming route, and an outgoing route for the one network interface.
While the command line version is more readable, the file version is much closer to what is actually stored in the table. Viewing it this way makes it easier to understand how to code new routes later. For example, it’s easy to see exactly what bits go into the flags
field.
Adding and Removing Null Routes
Null routes will simply ignore the packets that match the route. It’s an effective way to ban the sending and receiving of packets from a problematic IP.
You can do it with route
like this:
user@system: route add 123.123.123.123 reject user@system: route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 72.93.243.1 0.0.0.0 UG 0 0 0 eth0 72.93.243.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 123.123.123.123 - 255.255.255.255 !H 0 - 0 -
Now you will be unable to send of receive packets from IP 123.123.123.123
You remove a null route like this:
user@system: route del 123.123.123.123 reject
Be careful adding null routes, or removing any route from the table. If you remove a route that you are using to ssh
into the system, you cannot recover without logging in at the physical terminal.
The !H
in the flags means that this is a “reject” route to a “host” system.
Changing the Routing Table in C/C++
To add a null route a program, you use ioctl
with the SIOCADDRT
control code, like this:
#include <sys/types.h> #include <sys/socket.h> #include <net/route.h> #include <sys/ioctl.h> bool addNullRoute( long host ) { // create the control socket. int fd = socket( PF_INET, SOCK_DGRAM, IPPROTO_IP ); struct rtentry route; memset( &route, 0, sizeof( route ) ); // set the gateway to 0. struct sockaddr_in *addr = (struct sockaddr_in *)&route.rt_gateway; addr->sin_family = AF_INET; addr->sin_addr.s_addr = 0; // set the host we are rejecting. addr = (struct sockaddr_in*) &route.rt_dst; addr->sin_family = AF_INET; addr->sin_addr.s_addr = htonl(host); // Set the mask. In this case we are using 255.255.255.255, to block a single // IP. But you could use a less restrictive mask to block a range of IPs. // To block and entire C block you would use 255.255.255.0, or 0x00FFFFFFF addr = (struct sockaddr_in*) &route.rt_genmask; addr->sin_family = AF_INET; addr->sin_addr.s_addr = 0xFFFFFFFF; // These flags mean: this route is created "up", or active // The blocked entity is a "host" as opposed to a "gateway" // The packets should be rejected. On BSD there is a flag RTF_BLACKHOLE // that causes packets to be dropped silently. We would use that if Linux // had it. RTF_REJECT will cause the network interface to signal that the // packets are being actively rejected. route.rt_flags = RTF_UP | RTF_HOST | RTF_REJECT; route.rt_metric = 0; // this is where the magic happens.. if ( ioctl( fd, SIOCADDRT, &route ) ) { close( fd ); return false; } // remember to close the socket lest you leak handles. close( fd ); return true; }
One thing to know is that when your program exits, your routes stay active. The changes you are making are global and persist after your program quits. You should remove any routes you don’t need before exiting your program. Routes you add will not persist though a reboot however. If you need that, you will want to use the route
command in one of the startup scripts.
Removing a route is almost identical, except you use SIOCDELRT
:
bool delNullRoute( long host ) { int fd = socket( PF_INET, SOCK_DGRAM, IPPROTO_IP ); struct rtentry route; memset( &route, 0, sizeof( route ) ); struct sockaddr_in *addr = (struct sockaddr_in *)&route.rt_gateway; addr->sin_family = AF_INET; addr->sin_addr.s_addr = 0; addr = (struct sockaddr_in*) &route.rt_dst; addr->sin_family = AF_INET; addr->sin_addr.s_addr = htonl(host); addr = (struct sockaddr_in*) &route.rt_genmask; addr->sin_family = AF_INET; addr->sin_addr.s_addr = 0xFFFFFFFF; route.rt_flags = RTF_UP | RTF_HOST | RTF_REJECT; route.rt_metric = 0; // this time we are deleting the route: if ( ioctl( fd, SIOCDELRT, &route ) ) { close( fd ); return false; } close( fd ); return true; }
Syncing the Routing Table
In my project I have a list of blocked IPs that are managed dynamically as abusive requests come in. If the requests stop coming, eventually the ban times out. Spammers burn out IPs and then move on, and so you don’t usually want to permanently ban any IP. Dynamic management makes it much easier than trying to manually keep track of all the spam IPs.
Normally when I start the server, and periodically while running, I want to look at the state of the routing table, remove any bans that have timed out, and start any new null routes for newly banned IPs.
For this I have a sync function. Some of this code uses my own container classes, but they work in a fairly standard way.
bool syncNullRoutes( const KxVector<long>& hostList ) { // hostlist contains the complete list of remote IPs we want to ban. // IPs on this list that are not already banned will get banned. // IPs that are banned that are not on this list will get unbanned. // read the route table from procfs. KxTokBuf routeTable; KxfPath path( "/proc/net/route" ); if ( !path.readFile( routeTable )) { return false; } KxVector<long> hl = hostList; KxVector<long> ex; hl.sort(); // parse the route table to see which routes already exist. const char* line; KxTokBuf lineBuf; while (( line = routeTable.getToken( "\n", "\r\t " ))) { // consider only rows that affect all interfaces, since our ban // routes all work like that. if ( *line != '*' ) continue; lineBuf.tokenize( line + 1 ); u32 vals[10]; u32 idx = 0; const char* tok; while (( tok = lineBuf.getToken( " \t", " \t" ))) { vals[idx++] = strtol( tok, NULL, 16 ); if ( idx >= 10 ) break; } // at this point, each column in the row has been parsed into vals. // offset 2, is the flags field. Offset 0 is the remote IP. if ( vals[2] == ( RTF_UP | RTF_HOST | RTF_REJECT )) { long ip = htonl( vals[0] ); if ( hl.contains( ip ) ) { // route exists in hostList, and in route table. Add to ex ex.insert( ip ); } else { // route does not exist in hostList, remove from route table. delNullRoute( ip ); } } } // add in all routes that don't exist in route table. ex.sort(); for ( u32 i = 0; i < hl.size(); i++ ) { long ip = hl[i]; if ( ex.contains( ip ) ) addNullRoute( ip ); } return true; }
Resources:
ioctl | Linux documentation for ioctl() |
ioctl()–Perform I/O Control Request | Some good documentation on ioctl form IBM. Does not match Linux exactly. |
Special Route Flags | Contains a section explaining the meaning of RTF_REJECT and RTF_BLACKHOLE on BSD. |
Specify interface when adding default gateway via SIOCADDRT. | Contains some reasonable example code using SIOCADDRT/SIOCDELRT. |
lbuchy / defaultGatewayIoctlSet.c | Another scrap of reasonable code, this time adding a gateway route programatically. |