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.