ping ff02::1
repairing misconfigured hosts using link-local IPv6 addresses and ad-hoc networking
TL;DR: Link local addresses are great and can be used for lots of things, like:
- finding and connecting to hosts that didn’t get an address via DHCP
- transferring files directly between two computers via Ethernet with almost no extra configuration
To enumerate every IPv6-supporting device on your LAN, even if the device isn’t DHCP’d or SLAAC’d correctly, you can run a command like this:
ping ff02::1%$interface
For example:
% ping ff02::1%enp4s0
PING ff02::1%enp4s0(ff02::1%enp4s0) 56 data bytes
64 bytes from fe80::54a4:74f4:3735:33ad%enp4s0: icmp_seq=1 ttl=64 time=0.023 ms
64 bytes from fe80::da9d:67ff:fe26:463f%enp4s0: icmp_seq=1 ttl=64 time=0.243 ms
64 bytes from fe80::28c7:cb09:9bf6:1062%enp4s0: icmp_seq=1 ttl=64 time=0.293 ms
64 bytes from fe80::a02:8eff:fe9e:cf67%enp4s0: icmp_seq=1 ttl=64 time=0.796 ms
64 bytes from fe80::5054:ff:fed6:96c%enp4s0: icmp_seq=1 ttl=64 time=0.888 ms
64 bytes from fe80::768e:f8ff:feed:b700%enp4s0: icmp_seq=1 ttl=64 time=3.45 ms
^C
--- ff02::1%enp4s0 ping statistics ---
1 packets transmitted, 1 received, +5 duplicates, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.023/0.949/3.453/1.160 ms
Some applications, like modern web browsers, don’t support the %$interface
syntax, because they are made by stupid people. However, you can still work
around it with socat, like so in the case of TCP:
socat TCP6-LISTEN:$LOCAL_PORT_TO_FORWARD,fork "TCP6:[$LINK_LOCAL_IPV6%$YOUR_INTERFACE_NAME]:$REMOTE_TCP_PORT"
Examples:
socat TCP6-LISTEN:8000,fork "TCP6:[fe80::dead:beef%enowhatever]:80"
firefox http://localhost:8000
socat TCP6-LISTEN:8000,fork "TCP6:[fe80::dead:beef%enowhatever]:443"
firefox https://localhost:8000
The rest of the article will dive deeper into IPv6 link-local and multicast addresses, and demonstrate a way to transfer files to your friend’s computer with directly-attached ethernet with absolutely no prior configuration besides IPv6.
Link local addresses
In IPv6, every device almost always has at least one address on every interface
it has – the
link-local address.
Technically, the block fe80::/10
is reserved for link-local addresses, but in
practice, only fe80::/64
is really used.
Computers almost always assign themselves a link-local address when they set up the network interface, unlike in IPv4, where computers rarely do this.1 Sometimes, the address is based on the MAC address, sometimes it’s completely random, sometimes you can even statically assign yourself a link-local address!
You can see your link-local address on a modern Linux computer if you type in
ip address
or any of its various aliases:
% ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host noprefixroute valid_lft forever preferred_lft forever 2: enp4s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether a8:a1:59:d6:b1:67 brd ff:ff:ff:ff:ff:ff inet 192.168.1.102/24 brd 192.168.1.255 scope global dynamic noprefixroute enp4s0 valid_lft 6292sec preferred_lft 6292sec <snip> inet6 fe80::54a4:74f4:3735:33ad/64 scope link noprefixroute valid_lft forever preferred_lft forever
It’s marked as scope link
, indicating it’s a link local address. Even if you
already have a DHCP IPv4 address, or a SLAAC-assigned IPv6 address, you probably
still have a link-local IPv6 most of the time.
Let’s try pinging it from another host on the subnet.
% ping fe80::54a4:74f4:3735:33ad
ping: Warning: IPv6 link-local address on ICMP datagram socket may require ifname or scope-id => use: address%<ifname|scope-id>
PING fe80::54a4:74f4:3735:33ad(fe80::54a4:74f4:3735:33ad) 56 data bytes
^C
--- fe80::54a4:74f4:3735:33ad ping statistics ---
8 packets transmitted, 0 received, 100% packet loss, time 7167ms
Uh oh, it didn’t work. Why not?
The problem is that link-local addresses are meant to be restricted to a
specific link. One fe80::whatever
on one link won’t be the same on another
link, and these addresses are to never cross router boundaries, either. So,
that’s why link-local addresses must be associated with an interface.
As ping
suggests, you can instead try address%<ifname|scope-id>
, like so:
% ping fe80::54a4:74f4:3735:33ad%eno1
PING fe80::54a4:74f4:3735:33ad%eno1(fe80::54a4:74f4:3735:33ad%eno1) 56 data bytes
64 bytes from fe80::54a4:74f4:3735:33ad%eno1: icmp_seq=1 ttl=64 time=0.182 ms
64 bytes from fe80::54a4:74f4:3735:33ad%eno1: icmp_seq=2 ttl=64 time=0.203 ms
64 bytes from fe80::54a4:74f4:3735:33ad%eno1: icmp_seq=3 ttl=64 time=0.178 ms
64 bytes from fe80::54a4:74f4:3735:33ad%eno1: icmp_seq=4 ttl=64 time=0.214 ms
^C
--- fe80::54a4:74f4:3735:33ad%eno1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3052ms
rtt min/avg/max/mdev = 0.178/0.194/0.214/0.014 ms
The value after the %
is called the zone index.
Besides ping, most network applications do support link-local addresses:
ssh fe80::da9d:67ff:fe26:463f%enp4s0
scp ./a.out '[fe80::da9d:67ff:fe26:463f%enp4s0]:~'
rsync ./a.out '[fe80::da9d:67ff:fe26:463f%enp4s0]:~'
nc fe80::da9d:67ff:fe26:463f%enp4s0 80
curl 'http://[fe80::da9d:67ff:fe26:463f%enp4s0]'
However, some applications do not support link-local addresses. In fact, most web browsers explicitly do not support this, which is a damn shame, especially if you have to access an admin page. However, there is a workaround – more on that later.
Link-local addresses themselves are not that useful. The way they are auto-configured, and the way you can scan for them, is.
Easily finding other link-local addresses on your subnet
Suppose you have a server or Raspberry Pi or whatever, you can’t connect a monitor to, and you’ve fucked up the network configuration and it’s not getting an IP address from DHCP. Or maybe your server has an out-of-band management page that isn’t DHCPing.
Out of luck, right? Nope!
If it supports IPv6, it probably assigned itself a link-local address. The only
question now is, which of the
addresses in fe80::/64
did it give
itself?
Thankfully, link-local multicast addresses2 are here to save the day! Simply run:
ping ff02::1%$YOUR_INTERFACE_NAME
and that will send a ping to every link-local address on your subnet. And then, every host will reply:
% ping ff02::1%enp4s0
PING ff02::1%enp4s0(ff02::1%enp4s0) 56 data bytes
64 bytes from fe80::54a4:74f4:3735:33ad%enp4s0: icmp_seq=1 ttl=64 time=0.023 ms
64 bytes from fe80::da9d:67ff:fe26:463f%enp4s0: icmp_seq=1 ttl=64 time=0.243 ms
64 bytes from fe80::28c7:cb09:9bf6:1062%enp4s0: icmp_seq=1 ttl=64 time=0.293 ms
64 bytes from fe80::a02:8eff:fe9e:cf67%enp4s0: icmp_seq=1 ttl=64 time=0.796 ms
64 bytes from fe80::5054:ff:fed6:96c%enp4s0: icmp_seq=1 ttl=64 time=0.888 ms
64 bytes from fe80::768e:f8ff:feed:b700%enp4s0: icmp_seq=1 ttl=64 time=3.45 ms
^C
--- ff02::1%enp4s0 ping statistics ---
1 packets transmitted, 1 received, +5 duplicates, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.023/0.949/3.453/1.160 ms
Now you can sort through them and determine which host is which.
You don’t even need to have an ethernet switch between the two machines! I’ve
even saved a friend’s Raspberry Pi just by directly connecting its ethernet port
directly into my computer, pinging ff02::1
to find the link-local IP it gave
itself, and SSHing into the discovered IP.
Workaround for web browsers not supporting link-local addresses
Let’s say I’ve determined that fe80::5054:ff:fed6:96c%enp4s0
is a router with
an admin page. Technically, URLs actually do support this syntax, you just need
to escape the %
as a %25
, like so:
http://[fe80::5054:ff:fed6:96c%25enp4s0]
However, if you’re using a modern web browser, this is most likely completely
unsupported. Try clicking on it. I’m quite sure that it won’t work for you.
Firefox simply refuses to take me to it, and Chrome sends me to
about:blank#blocked
.
As a workaround, you can forward the port with a socat command like this:
socat TCP6-LISTEN:8000,fork "TCP6:[fe80::5054:ff:fed6:96c%enp4s0]:443"
Explanation of arguments:
TCP6-LISTEN:8000,fork
- listen on TCP/IPv6 port 8000. when you see a new connection, fork a process to handle it and keep listening.TCP6:[fe80::5054:ff:fed6:96c%enp4s0]:443
- after you’ve got a connection on port8000
, open a connection to[fe80::5054:ff:fed6:96c%enp4s0]:443
, and forward data to it.
Once you have that running in the background, you can go to https://localhost:8000 and look at your beautiful admin page.
Sending files over an ethernet cable with minimal configuration
Tired of using USB sticks to transfer files between laptops? Connect your two computers directly like this:
You can determine the link-local IP by typing ip a
on both computers and
manually typing 26 characters in… or you can be lazy, ping ff02::1
, and
copy/paste:
% ping ff02::1%enp0s20f0u1c2
PING ff02::1%enp0s20f0u1c2(ff02::1%enp0s20f0u1c2) 56 data bytes
64 bytes from fe80::8a40:4905:d926:861c%enp0s20f0u1c2: icmp_seq=1 ttl=64 time=0.112 ms
64 bytes from fe80::29b8:771a:dd16:fb28%enp0s20f0u1c2: icmp_seq=1 ttl=64 time=1.60 ms # this is the other computer, time was much longer
^C
--- ff02::1%enp0s20f0u1c2 ping statistics ---
1 packets transmitted, 1 received, +1 duplicates, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.112/0.855/1.598/0.743 ms
% rsync -rv my-silly-files/ [fe80::29b8:771a:dd16:fb28%enp0s20f0u1c2]:/wherever
hang on i didn’t set up any infrastructure up how can i do this help
What if you don’t have SSH perms on the other machine because it’s your friend’s
machine, and you have a device-local firewall blocking any attempts at running
nc -l
, and you’re too lazy to set up a firewall allow rule?
Well, you can do a little “TCP hole-punching” trick using netcat. Simultaneously on both computers, run the following command to perform a TCP simultaneous-open:
nc -v $IP%$INTERFACE $PORT -p $PORT
where:
$IP
is the other machine’s IP$INTERFACE
is the interface you’re using$PORT
is agreed upon between BOTH OF YOU. List it TWICE.3-p
tells netcat to use that specific source port.- The
-v
is useful because it will tell you how successfull it was.
Example:
astrid@shai-hulud% nc -v fe80::29b8:771a:dd16:fb28%enp0s20f0u1c2 10000 -p 10000
Connection to fe80::29b8:771a:dd16:fb28%enp0s20f0u1c2 10000 port [tcp/ndmp] succeeded!
alia@stargazer% nc -v fe80::8a40:4905:d926:861c%enp0s31f6 10000 -p 10000
Connection to fe80::8a40:4905:d926:861c%enp0s31f6 10000 port [tcp/ndmp] succeeded!
And congratulations, you now have a shitty version of IRC running over that ethernet cable.
If you want to transfer files, just pipe them in on the source:
nc -v $IP%$INTERFACE $PORT -p $PORT < ./my-silly-file
and pipe them out on the destination:
nc -v $IP%$INTERFACE $PORT -p $PORT > ./my-silly-file
If you have multiple files, you can tar
them. If you want to go faster, you
could gzip
them. The possibilities are endless.
Conclusion
ipv6 gives you lots of cool auto-configured goodies
ff02::1
basically just a broadcast?
Appendix: hang on, isn’t In general, IPv6 multicast requires usage of
Multicast Listener Discovery (MLD)
– essentially, hosts ask the router to subscribe to a multicast address, so we
can minimize the number of packets we transmit. But ff02::1
is special – it
does not need MLD or any kind of subscription to work.
So what distinguishes it from a broadcast?
Here’s Wireshark’s dissection of that ICMP request to ff02::1
from earlier:
Frame 449: 118 bytes on wire (944 bits), 118 bytes captured (944 bits) on interface enp4s0, id 0 Ethernet II, Src: ASRockIn_d6:b1:67 (a8:a1:59:d6:b1:67), Dst: IPv6mcast_01 (33:33:00:00:00:01) Destination: IPv6mcast_01 (33:33:00:00:00:01) Address: IPv6mcast_01 (33:33:00:00:00:01) .... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default) .... ...1 .... .... .... .... = IG bit: Group address (multicast/broadcast) Source: ASRockIn_d6:b1:67 (a8:a1:59:d6:b1:67) Address: ASRockIn_d6:b1:67 (a8:a1:59:d6:b1:67) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...0 .... .... .... .... = IG bit: Individual address (unicast) Type: IPv6 (0x86dd) Internet Protocol Version 6, Src: fe80::54a4:74f4:3735:33ad, Dst: ff02::1 0110 .... = Version: 6 .... 0000 0000 .... .... .... .... .... = Traffic Class: 0x00 (DSCP: CS0, ECN: Not-ECT) .... 0010 0000 0011 0111 0011 = Flow Label: 0x20373 Payload Length: 64 Next Header: ICMPv6 (58) Hop Limit: 1 Source Address: fe80::54a4:74f4:3735:33ad Destination Address: ff02::1 Internet Control Message Protocol v6 Type: Echo (ping) request (128) Code: 0 Checksum: 0x7e2a [correct] [Checksum Status: Good] Identifier: 0x0001 Sequence: 1 Data (56 bytes)
This is sent to the MAC address 33:33:00:00:00:01
. Where the hell does that
come from?
IETF RFC 2464 §7,
which specifies how to map multicast IPv6’s into multicast MAC addresses:
An IPv6 packet with a multicast destination address DST, consisting of the sixteen octets DST[1] through DST[16], is transmitted to the Ethernet multicast address whose first two octets are the value 3333 hexadecimal and whose last four octets are the last four octets of DST.
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |0 0 1 1 0 0 1 1|0 0 1 1 0 0 1 1| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | DST[13] | DST[14] | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | DST[15] | DST[16] | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Further note the bolded Group Address bit from above. That specific bit being 1
signals to ethernet switches “hey, this needs to be sent to multiple hosts.”
It’s not just ff:ff:ff:ff:ff:ff
that has this behavior, any MAC with the group
bit set will be treated like this.
According to IETF RFC 4541 §1, most “dumb” switches will effectively treat any packet with that bit enabled, multicast or broadcast, as a broadcast, and simply forward the packet to every single device on the layer 2 network. Of course, this would suck if you are using IPv6, because it takes advantage of multicast very heavily to reduce congestion. IGMP snooping was introduced to have switches (layer 2 devices) “snoop” on the IP headers above them (layer 3 data) and not forward multicasted packets to devices that don’t care about it.
However, §3 recommends
that ff02::1
specifically should be ignored for IGMP purposes and be treated
as simply a broadcast:
In IPv6, the data forwarding rules are more straight forward because MLD is mandated for addresses with scope 2 (link-scope) or greater. The only exception is the address FF02::1 which is the all hosts link-scope address for which MLD messages are never sent. Packets with the all hosts link-scope address should be forwarded on all ports.
So yes, even though it is technically a multicast, and it isn’t addressed to
ff:ff:ff:ff:ff:ff
, it is effectively a broadcast in all but the most painful
of vendor hardware.
As for why they didn’t just send it to ff:ff:ff:ff:ff:ff
, it’s probably to
keep consistent with RFC 2464. I would imagine that 33:33:00:00:00:01
would
theoretically be beneficial over ff:ff:ff:ff:ff:ff
because switches could
simply not forward those to IPv4-only hosts. In practice, I don’t know how often
that’s used, or if it’s used at all.
The reply packet, however, is merely unicasted, and does not have to deal with any of these technicalities.
Frame 452: 118 bytes on wire (944 bits), 118 bytes captured (944 bits) on interface enp4s0, id 0 Ethernet II, Src: HewlettP_26:46:3f (d8:9d:67:26:46:3f), Dst: ASRockIn_d6:b1:67 (a8:a1:59:d6:b1:67) Destination: ASRockIn_d6:b1:67 (a8:a1:59:d6:b1:67) Address: ASRockIn_d6:b1:67 (a8:a1:59:d6:b1:67) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...0 .... .... .... .... = IG bit: Individual address (unicast) Source: HewlettP_26:46:3f (d8:9d:67:26:46:3f) Address: HewlettP_26:46:3f (d8:9d:67:26:46:3f) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...0 .... .... .... .... = IG bit: Individual address (unicast) Type: IPv6 (0x86dd) Internet Protocol Version 6, Src: fe80::da9d:67ff:fe26:463f, Dst: fe80::54a4:74f4:3735:33ad 0110 .... = Version: 6 .... 0000 0000 .... .... .... .... .... = Traffic Class: 0x00 (DSCP: CS0, ECN: Not-ECT) .... 1001 1100 0000 0101 0001 = Flow Label: 0x9c051 Payload Length: 64 Next Header: ICMPv6 (58) Hop Limit: 64 Source Address: fe80::da9d:67ff:fe26:463f Destination Address: fe80::54a4:74f4:3735:33ad [Source SLAAC MAC: HewlettP_26:46:3f (d8:9d:67:26:46:3f)] Internet Control Message Protocol v6 Type: Echo (ping) reply (129) Code: 0 Checksum: 0xf6a9 [correct] [Checksum Status: Good] Identifier: 0x0001 Sequence: 1 Data (56 bytes)
TL;DR: you can only call it a broadcast if it comes from the
ff:ff:ff:ff:ff:ff
region of France, otherwise it’s just sparkling multicast
-
Yes, there are link-local IPv4 addresses in
169.254.0.0/16
. Most IPv4 devices do not assign themselves one automatically, which is one of many other ways IPv4 sucks. We will pretend these don’t exist because they only really seem to be used by routers for BGP extended-next-hop, and not normal people’s computers. ↩ -
ff02::1
is not the only link-local multicast address out there, but it is one of the more useful ones for debugging. IANA has a whole list of other standard multicast addresses. ↩ -
You can technically do it asymmetrically (i.e. having one computer’s port be 10003 and another computer’s port be 10004), but having the same port is just easier. ↩