Linux Kernel route_localnet setting



The Linux kernel has a setting for network interfaces that allows routing local net (127.0.0.0/8 for IP version 4, also known as the loopback address because it typically is assigned to the lo loopback interface). It can be accessed via:

/proc/sys/net/ipv4/conf/eth0/route_localnet

for the typical eth0 network interface. The values which can be written there are either 0 (the default) for no routing or 1 for routing. Each network interface has its own special file. If a setting should be made persistent this can usually be done by configuring a setting in /etc/sysctl.conf on most Linux distributions. The setting takes the form of the above path where the prefix /proc/sys/ is removed and all / characters are replaced by a dot. In the example above this would be written as:

net.ipv4.conf.eth0.route_localnet = 1

Now what does this setting achieve?

In the following I'm using examples from IP version 4 but the same applies to the local host (loopback) address ::1 for IP version 6.

Typically in the Linux network stack IP packets which are either originating from local net 127.0.0.0/8 or have a destination in that network are dropped when the come in over an interface that is not configured for this address range. We'll first verify this in the following.

For testing we use a simple UDP server in python that receives UDP packets and prints their source IP address and port (and length):

import socket
[...]
sock = socket.socket (socket.AF_INET, socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
sock.bind ((args.address, args.port))
while True:
    buf, adr = sock.recvfrom (1024)
    l = len (buf)
    print ('Received %(l)s bytes from %(adr)s' % locals ())

All the values in args come from the command line via ArgumentParser. The address to bind to is 0.0.0.0 in the following, meaning it accepts packets from all network interfaces.

Now we can first test with a simple client. In the following we have two machines, .9 is the sending machine and .5 is the receiving machine.

When sending a UDP packet we can use the following simple code:

import socket
sock = socket.socket (socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.sendto (b'This is a test', (args.address, args.port))

Again we can specify the parameters on the command line. Now when we send from .9 to .5 we get:

Received 14 bytes from ('XXXXXXX.9', 38232)

where the source port 38232 is a random number created by the sending Linux kernel and the XXXXXXX part represents the high bytes of the IP address of the sender. When sending from the receiving machine to 127.0.0.1 we get:

Received 14 bytes from ('127.0.0.1', 58196)

We see that the simple server listens to both, the real ethernet interface and to the loopback interface.

Nothing new so far. Lets see what happens when we fake addresses. For creating arbitrary IP packets (or even non-IP packets) the Python package Scapy is used in the following.

Consider the following little script:

p = Ether (dst = args.mac_address, src = args.mac_address) \
    / IP (dst = args.address, src = args.source_address) \
    / UDP (dport = args.port, sport = args.source_port) \
    / Raw (b'This is a test')
sendp (p, iface = args.interface)

This builds an Ethernet (Ether) packet with an IP payload. The IP packet contains a UDP payload, which in turn contains a Raw string. This packet is then sent via a raw (network layer 2) network socket. Again all parameters come from the command line. Default for all the ports is 22222, the source IP is the XXXXXXX.9 address and the destination IP is the XXXXXXX.5 address. The destination and source mac addresses are both set to the mac address of the destination machine. We can replicate what we did with our simple client above:

sudo scapyclient.py

which results in the server printing:

Received 14 bytes from ('XXXXXXX.9', 22222)

It will always be port 22222 unless we specify a port option since this is the default in the settings of the script. We need to use sudo because only the superuser is allowed to send arbitrary packets on layer-2 of the network stack.

Now what happens when we start faking packets? For this we start tcpdump on both, the sending machine and the receiving machine as follows:

sudo tcpdump -n -i eth0 udp port 22222

Of course if your network interface is not called eth0 you would replace the parameter to the -i option above. The -n option tells tcpdump to not attempt DNS lookups of received IP addresses. Now lets send a packet with a localhost address as the source address:

sudo scapyclient.py --source-address=127.0.0.1

We see it on both machines in the tcpdump output:

TT:TT:TT.TTTTTT IP 127.0.0.1.22222 > 10.23.5.5.22222: UDP, length 14

where TT:TT:TT.TTTTTT is a timestamp. So the packet is going out via the network interface of the sending machine and it is being received by the receiving machine (because the destination mac address matches) but it is not printed by our simple server.

The same happens when we fake the destination address:

sudo scapyclient.py --address=127.0.0.1

The tcpdump output is:

TT:TT:TT.TTTTTT IP 10.23.5.9.22222 > 127.0.0.1.22222: UDP, length 14

It is also printed by both machines by tcpdump and again it is not printed by our simple server.

Now lets see what happens if we enable route_localnet for our ethernet interface (the following command only works as root, of course):

echo 1 > /proc/sys/net/ipv4/conf/eth0/route_localnet

We again fake the sender address:

sudo python3.11 scapyclient.py --source-address=127.0.0.1

Again the packet is printed by both running tcpdump processes but again not by our simple server. But faking the destination address:

sudo python3.11 scapyclient.py --address=127.0.0.1

Apart from the packet being logged by tcpdump we get an answer from our server:

Received 14 bytes from ('10.23.5.9', 22222)

Note that this works even if we tell our simple server to only listen to localhost:

python3 server.py --address=127.0.0.1

Still outputs our faked packet although this did not come in via the loopback interface:

Received 14 bytes from ('10.23.5.9', 22222)

Security implications

Some applications are using local servers that listen only to the loopback address and expect these to be able to only receive packets via the loopback interface – from local processes running on the same machine as the server.

This is always a dangerous assumption, so you should never do dangerous things that rely on this type of (non-) authentication. At least on Linux there is some safeguard as long as you keep the setting of the route_localnet to 0. On other operating systems you may not be so lucky. And even on Linux this behaviour can be turned off.

Why would you want to set this?

Sometimes you want to forward connections originating on the local machine that try to connect to local port to a remote machine, e.g. you have in /etc/hosts

127.0.0.1 some.remote.example.com

and you want to connect to that machine with a process from localhost. You can add firewall rules that rewrite packets to achieve this:

iptables -t nat -A OUTPUT -d 127.0.0.1 -p tcp --dport 443 \
    -j DNAT --to-destination XXXXXXX.5:1443
iptables -t nat -A POSTROUTING -d XXXXXXX.5 -p tcp --dport 1443 \
    -j SNAT --to-source XXXXXXX.9

The first rule is doing the destination address (and port) rewriting while the second rule makes sure that answer packets reach us by rewriting the source address. But without setting route_localnet on the outgoing ethernet interface this will not work, the packets will be dropped.

But when setting this beware of the explanation in the Security implications section. Because it has also an impact on what packets are received from the network.