2

tl;dr: accessing 0.0.0.0:port (eg. curl http://0.0.0.0:443) gets redirected(internally) to 127.0.0.1:port (where port is any port number) (eg. the previous curl command is the same as curl http://127.0.0.1:443); why does this happen and how to block connections destined to 0.0.0.0 ?

UPDATE2: I've found a way to block it by patching the Linux kernel (version 6.0.9):


--- .orig/usr/src/linux/net/ipv4/route.c
+++ /usr/src/linux/net/ipv4/route.c
@@ -2740,14 +2740,17 @@ struct rtable *ip_route_output_key_hash_
    }
 
    if (!fl4->daddr) {
-       fl4->daddr = fl4->saddr;
+           rth = ERR_PTR(-ENETUNREACH);
+           goto out;
+                        /* commenting out the rest:
+       fl4->daddr = fl4->saddr; // if you did specify src address and dest is 0.0.0.0 then set dest=src addr
        if (!fl4->daddr)
-           fl4->daddr = fl4->saddr = htonl(INADDR_LOOPBACK);
+           fl4->daddr = fl4->saddr = htonl(INADDR_LOOPBACK); // if you didn't specify source address and dest address is 0.0.0.0 then make them both 127.0.0.1
        dev_out = net->loopback_dev;
        fl4->flowi4_oif = LOOPBACK_IFINDEX;
        res->type = RTN_LOCAL;
        flags |= RTCF_LOCAL;
-       goto make_route;
+       goto make_route; END of COMMENTed out block */
    }
 
    err = fib_lookup(net, fl4, res, 0);

Result: Where do packets sent to IP 0.0.0.0 go?:

$ ip route get 0.0.0.0
RTNETLINK answers: Network is unreachable

...they don't!

A client attempts to connect from 127.1.2.18:5000 to 0.0.0.0:80

$ nc -n -s 127.1.2.18 -p 5000 -vvvvvvvv -- 0.0.0.0 80
(UNKNOWN) [0.0.0.0] 80 (http) : Network is unreachable
 sent 0, rcvd 0

(if you didn't apply kernel patch, you will need a server like the following for the above client to be able to successfully connect: (as root, in bash)while true; do nc -n -l -p 80 -s 127.1.2.18 -vvvvvvvv -- 127.1.2.18 5000; echo "------------------$(date)";sleep 1; done)

Patched ping(ie. a ping that doesn't set destination address to be the same as the source address when destination address is 0.0.0.0, ie. comment out the 2 lines under // special case for 0 dst address that you see here):

$ ping -c1 0.0.0.0
ping: connect: Network is unreachable

instant. However, if specifying source address, it takes a timeout(of 10 sec) until it finishes:

$ ping -I 127.1.2.3 -c1 -- 0.0.0.0
PING 0.0.0.0 (0.0.0.0) from 127.1.2.3 : 56(84) bytes of data.

--- 0.0.0.0 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

UPDATE1:

The why part is explained here but I'm expecting a little bit more details as to why does this happen, for example(thanks to user with nickname anyone on liberachat #kernel channel):

$ ip route get 0.0.0.0
local 0.0.0.0 dev lo src 127.0.0.1 uid 1000
    cache <local>

This shows that somehow packets destined for 0.0.0.0 get routed to the localhost interface lo and they get source ip 127.0.0.1 (if I'm interpreting this right) and because that route doesn't appear in this list:

$ ip route list table local
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
local 169.254.6.5 dev em1 proto kernel scope host src 169.254.6.5
broadcast 169.254.6.255 dev em1 proto kernel scope link src 169.254.6.5
local 192.168.0.17 dev em1 proto kernel scope host src 192.168.0.17
broadcast 192.168.255.255 dev em1 proto kernel scope link src 192.168.0.17

it means that it must be somehow internal to the Linux kernel. ie. hardcoded

To give you an idea, here's how it looks for an IP that's on the internet (I used quad1 as an example IP):

$ ip route get 1.1.1.1
1.1.1.1 via 192.168.1.1 dev em1 src 192.168.0.17 uid 1000
    cache

where 192.168.1.1 is my gateway, ie.:

$ ip route
default via 192.168.1.1 dev em1 metric 2
169.254.6.0/24 dev em1 proto kernel scope link src 169.254.6.5
192.168.0.0/16 dev em1 proto kernel scope link src 192.168.0.17

Because iptables cannot be used to sense (and thus block/drop) such connections destined to 0.0.0.0 that get somehow routed to 127.0.0.1, it might prove difficult to find a way to block them... but I'll definitely try to find a way, unless someone already knows one.

@Stephen Kitt (in the comments) suggested a way to block hostnames that reside in /etc/hosts, so instead of:
0.0.0.0 someblockedhostname
you can have
127.1.2.3 someblockedhostname
127.1.2.3 someOTHERblockedhostname
(anything other than 127.0.0.1, but you can use the same IP for every blocked hostname, unless you want to differentiate)
which IP you can then block using iptables.

However if your DNS resolver (ie. NextDNS, or 1.1.1.3) returns 0.0.0.0 for blocked hostnames (instead of NXDOMAIN) then you cannot do this (unless, of course, you want to add each host manually in /etc/hosts, because /etc/hosts takes precedence - assuming you didn't change the line hosts: files dns from /etc/nsswitch.conf)


OLD: (though edited)

On Linux (I tried Gentoo and Pop OS!, latest) if you have this line in /etc/hosts:

0.0.0.0 somehosthere

and you run this as root (to emulate a localhost server listening on port 443)
# nc -l -p 443 -s 127.0.0.1
then you go into your browser (Firefox and Chrome/Chromium tested) and put this in address bar:
https://somehosthere
or
0.0.0.0:443
or
https://0.0.0.0

then the terminal where you started nc(aka netcat) shows a connection attempt (some garbage text including the plaintext somehosthere if you used it in the url)

or instead of the browser, you can try:
curl https://somehosthere
or if you want to see the plaintext request:
curl http://somehosthere:443

This doesn't seem to be mitigable even when using dnsmasq as long as that 0.0.0.0 somehosthere is in /etc/hosts, but when using dnsmasq and your DNS resolver (ie. NextDNS or Cloudflare's 1.1.1.3) returns 0.0.0.0 instead of NXDOMAIN (true at the time of this writing) and that hostname isn't in your /etc/hosts(AND in what you told dnsmasq is the /etc/hosts to use) then there are two ways to mitigate it(either or both will work):

  1. use dnsmasq arg --stop-dns-rebind
       --stop-dns-rebind
              Reject (and log) addresses from upstream nameservers which are in
              the private ranges. This blocks an attack where a browser  behind
              a  firewall  is  used to probe machines on the local network. For
              IPv6, the private range covers the IPv4-mapped addresses in  pri‐
              vate  space  plus  all  link-local  (LL) and site-local (ULA) ad‐
              dresses.
  1. use line bogus-nxdomain=0.0.0.0 in /etc/dnsmasq.conf which makes dnsmasq itself return NXDOMAIN for any hostname that resolved to 0.0.0.0 (except, once again, if that hostname was in /etc/hosts (bypasses dnsmasq) and what you told dnsmasq to use as /etc/hosts (if you did))

So, the second part of this question is how to disallow accesses to 0.0.0.0 from being redirected to 127.0.0.1 ? I want this because when using NextDNS (or cloudflare's 1.1.1.3) as DNS resolver, it returns 0.0.0.0 for blocked hostnames, instead of NXDOMAIN, thus when loading webpages, parts of them(that are located on blocked hostnames) will try to access my localhost server running on port 443 (if any) and load pages from it instead of just being blocked.

Relevant browser-specific public issues being aware of this(that 0.0.0.0 maps to 127.0.0.1):
Chrome/Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=1300021 Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1672528#c17

  • [Connecting to IP 0.0.0.0 succeeds. How? Why?](https://unix.stackexchange.com/q/419880/86440) explains why this happens. – Stephen Kitt Nov 18 '22 at 12:59
  • Thanks! It seems that this translation from `0.0.0.0` to `127.0.0.1` happens inside the linux kernel and it doesn't reach iptables; somewhere inside `tcp_v4_connect()` maybe the `nexthop` thing. – correabuscar Nov 18 '22 at 13:57
  • @ilkkachu it kind of answer the "why" but not the "how to disallow it" part of the question. It is the same link that Stephen Kitt posted before. I'm still looking for some way to prevent this from happening, even if it means recompiling the kernel. – correabuscar Nov 18 '22 at 17:49
  • @StephenKitt I don't want blocked hosts(because they're resolved to 0.0.0.0 by NextDNS and by 1.1.1.3) to redirect to my localhost(imagine I host a https server locally) while browsing in my browser. Btw, someone on liberachat irc on #kernel realized that it's a routing thing by looking at the output of `ip r g 0.0.0.0` and yet not in the local table `ip r l table local` thus likely hardcoded in the kernel source or something. – correabuscar Nov 18 '22 at 18:27
  • @correabuscar ah, right, I block DNS entries by resolving them to a specific address on my network which blocks everything apart from port 80 (where it displays a ”blocked” page). And yes, this is implemented somewhere in the kernel, I remember finding where when I wrote up my answer to the linked question, but I can’t remember where it was :-(. – Stephen Kitt Nov 18 '22 at 18:30
  • @StephenKitt can you find it by searching for `RTN_LOCAL` ? maybe something related to `fib` stuff? Btw, the `why` of the question is definitely because the output of `ip r g 0.0.0.0` says so. Oh and that's a very good way to block blockedhosts in /etc/hosts but I can't do that with NextDNS or cloudflare's 1.1.1.3 as they always return 0.0.0.0 and aren't configurable to return say 127.3.4.5 which I could `iptables` block. – correabuscar Nov 18 '22 at 18:32
  • Isn't this "all interfaces" in some notations? – davolfman Nov 18 '22 at 22:51
  • I'm making great progress, slow, but still great. For example `0.0.0.0` doesn't transform into `127.0.0.1` but rather into the source of the packet(or of the connection attempt, since I'm only looking at tcp ipv4) which happens to be `127.0.0.1` but can be different ie. client:`$ nc -n -s 127.1.2.18 -p 5000 -vvvvvvvv -- 0.0.0.0 80` will attempt to connect to 127.1.2.18(because that 0.0.0.0 becomes the source IP), so a server like this is needed: `# nc -n -l -p 80 -s 127.1.2.18 -vvvvvvvv -- 127.1.2.18 5000`. I haven't found exactly where the transform happens,but it's inside `ip_route_connect` – correabuscar Nov 19 '22 at 00:56
  • @correabuscar, I know it's the same one, I just ticked it through the system, because it did _seem_ like it was pretty much the same issue. I'm sorry if I missed a part there. – ilkkachu Nov 19 '22 at 10:18
  • @StephenKitt I think I found where the destination address, if 0.0.0.0, gets overwritten with the source address(if set to non 0.0.0.0, or else both get set to 127.0.0.1), in net/ipv4/route.c function *ip_route_output_key_hash_rcu() where there's a `if (!fl4->daddr) {`, ie. `ping -I 127.1.2.3 -c1 0.0.0.0` will get src=dst=127.1.2.3 and also `nc -n -s 127.1.2.18 -p 5000 -vvvvvvvv -- 0.0.0.0 80` will try to connect to src=dst=127.1.2.18 , so a server like this will work `nc -n -l -p 80 -s 127.1.2.18 -vvvvvvvv -- 127.1.2.18 5000`(I restricted source port to 5000 on the server but not needed) – correabuscar Nov 19 '22 at 10:41
  • @ilkkachu hey, no worries! and thank you! @StephenKitt is that the same place where you found it in the past, maybe? Also I used a ping that has commented out the part where dest (if 0.0.0.0) gets overwritten with source(the `// special case for 0 dst address`), even before I've seen this ping [answer](https://unix.stackexchange.com/a/99346/543696) so it does try to ping 0.0.0.0 and reaches(it seems) the same kernel routing code path(mention in my prev. comment) as the tcp ipv4 connect does. – correabuscar Nov 19 '22 at 10:59
  • @StephenKitt I have confirmed that that was the place indeed and I could make it return `Network is unreachable` for both ping and netcat, which is what I want in order to block it! (and I'm currently using that kernel to write this) Now to finesse it in such a way as to be able to write an answer out of it... and maybe optimize it a bit. (should be done by tomorrow night, at worst) – correabuscar Nov 19 '22 at 11:43
  • @correabuscar that doesn’t ring any bells, but it certainly explains the behaviour. I’ll go and fix my answer, thanks! And I look forward to yours. – Stephen Kitt Nov 19 '22 at 13:37
  • @StephenKitt I've to admit, I'm kinda lazy to write an answer (because, I'm pretty bad at being concise and explaining stuff, as you can see from all the verbosity present in my question), so I would appreciate it if someone(you? pretty please? well, only if you want) would write an answer, even though you would be using my patch that I've included already in the question (or some variant of that patch). But if no one writes an answer in a while, maybe I will do it anyway, but man, I'm so reluctant to do it I swear... If anyone wants to do it just answer the why and how(with the kernel patch). – correabuscar Nov 19 '22 at 13:45
  • I’ve written an answer up as a community wiki. Feel free to clean up your question ;-) (or I can do that too). Incidentally, the blocking approach I use doesn’t involve `/etc/hosts`; my local DNS resolver returns the address I specified for all blocked domains. – Stephen Kitt Nov 19 '22 at 13:59

2 Answers2

2

Why this happens is explained in Connecting to IP 0.0.0.0 succeeds. How? Why? — in short, packets with no destination address (0.0.0.0) have their source address copied into their destination address, and packets with no source or destination have their source and destination addresses set to the loopback address (INADDR_LOOPBACK, 127.0.0.1); the resulting packet is sent out on the loopback interface.

As you determined, this behaviour is hard-coded in the Linux kernel’s IPv4 networking stack, and the only way to change it is to patch the kernel:

diff --git a/net/ipv4/route.c b/net/ipv4/route.c
index 795cbe1de912..df15a685f04c 100644
--- a/net/ipv4/route.c
+++ b/net/ipv4/route.c
@@ -2740,14 +2740,8 @@ struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4,
        }
 
        if (!fl4->daddr) {
-               fl4->daddr = fl4->saddr;
-               if (!fl4->daddr)
-                       fl4->daddr = fl4->saddr = htonl(INADDR_LOOPBACK);
-               dev_out = net->loopback_dev;
-               fl4->flowi4_oif = LOOPBACK_IFINDEX;
-               res->type = RTN_LOCAL;
-               flags |= RTCF_LOCAL;
-               goto make_route;
+               rth = ERR_PTR(-ENETUNREACH);
+               goto out;
        }
 
        err = fib_lookup(net, fl4, res, 0);

This patch shows the original implementation, explaining the “why?” part above: if the packet has no destination address (i.e. it’s 0.0.0.0):

  • the source address is copied to the destination address
  • if the packet still has no destination address, i.e. it also has no source address, both addresses are set to the loopback address (127.0.0.1);
  • in all cases, the outgoing device is set to the loopback device, and the route is constructed accordingly.

The patch changes this behaviour to return a “network unreachable” error instead.

Stephen Kitt
  • 411,918
  • 54
  • 1,065
  • 1,164
0

The short answer is it doesn't. It is much, much, worse than that.

0.0.0.0 maps to the IP address of any interface on your system. So if netstat or ip say that 0.0.0.0:22 is open, then your system is accepting incoming SSH connections over loopback, and ethernet, and wifi, and whatever other network interface you may have up. It's just that loopback is the only one listening on your port, or gets priority among the interfaces.

davolfman
  • 479
  • 2
  • 7
  • 2
    You're referring to the "binding" part of [this](https://unix.stackexchange.com/a/419881/543696) answer, but I'm strictly referring to the "target" part, ie. when `0.0.0.0` is the target host, for example when you try to `curl http://0.0.0.0:443` whilst you're already running the server on 127.0.0.1:443 via eg. `nc -l -p 443 -s 127.0.0.1` (as root) – correabuscar Nov 18 '22 at 23:18
  • I don't think there's a difference in Linux. Even local packets are going to get routed, just to see which NIC to send them out on. Localhost can answer for 0.0.0.0 by definition, and probably has a better metric than anything else. – davolfman Nov 28 '22 at 17:40