When IPv6 listeners accept IPv4 connections

Here’s a simple experiment you can perform easily on any Linux or Mac system.

First start a listener on all IPv6 interfaces, port 45000.

$ nc -nlv :: 45000
Listening on :: 45000

Then in another terminal on the same machine, connect to it via IPv4.

$ nc -v 127.0.0.1 45000
Connection to 127.0.0.1 45000 port [tcp/*] succeeded!

Back on the first terminal you will see that it accepted your connection:

Connection received on ::ffff:127.0.0.1 56760

Two interesting things happened here. Firstly, we listened on an IPv6 address and connected to an IPv4 address, and this worked. Secondly, from the perspective of the server, the client had a rather strange-looking address shown here as ::ffff:127.0.0.1.

I first observed this behaviour quite a while ago and it’s been very useful. Why bind a server to 0.0.0.0 when I can bind it to [::] and accept both IPv4 and IPv6 connections?

Well. A few months back I was debugging a rare test failure in some code. One in several thousand times this particular test would fail. This test was doing a few things in sequence:

  1. Create a TCP server on [::]:0, which means the operating system should assign an ephemeral listening port.
  2. Look up which port was actually bound.
  3. Set up a TCP client connecting to 127.0.0.1:that_port, and test the protocol stuff that actually matters.

After running this test in a loop many, many times I noticed that when it failed it was always on port 49874. I ran netstat and realised that there was already a listener on 127.0.0.1:49874 used by Spotify!

If the IPv4 counterpart of the port was already in use when the test ran its server, that server only got the IPv6 connections. The IPv4 test client was connecting to Spotify instead. I changed the test server to also use an IPv4 address and the problem went away.

For some time I didn’t understand this mechanism. What would determine whether this v6-to-v4 forwarding happens or not? Well, recently I found the documentation in the ipv6 man page.

       IPV6_V6ONLY (since Linux 2.4.21 and 2.6)
              If this flag is set to true (nonzero), then the socket is
              restricted to sending and receiving IPv6 packets only.  In
              this case, an IPv4 and an IPv6 application can bind to a
              single port at the same time.

              If this flag is set to false (zero), then the socket can
              be used to send and receive packets to and from an IPv6
              address or an IPv4-mapped IPv6 address.

              The argument is a pointer to a boolean value in an
              integer.

              The default value for this flag is defined by the contents
              of the file /proc/sys/net/ipv6/bindv6only.  The default
              value for that file is 0 (false).

The behaviour is configurable using a setsockopt option, or a sysctl. These ::FFFF:-prefixed addresses are called “IPv4-mapped-on-IPv6”.

I guess I needed to bind both sockets after all!