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
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:
- Create a TCP server on
[::]:0, which means the operating system should assign an ephemeral listening port.
- Look up which port was actually bound.
- 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!