IPv6 source address selection regression
Brief description
I'm trying to generate an IPv6 MLD query using scapy. With scapy 2.4.4 (in Debian 11) it works as expected. With 2.5.0 (Debian 12) and current git a05013cf source address selection is weird. If I'm not specifying IPv6 and MAC source addresses explicitly, they're both all 0s. With scapy 2.4.4, I got the IPv6 link-local address of the sending interface (and the associated MAC address) as expected.
In addition, I'm getting 3 of these warnings:
WARNING: No route found for IPv6 destination ff02::1 (no default route?)
True, there is no default route (and also no global IPv6 address), but I don't see why I'd need one - MLD query is a link-local multicast packet, so the source address should be link-local as well.
BTW, this might be related to #4304 (I'm seeing the same warning messages), but in this case here, functionality is impacted, so it's not just a warning that can be safely ignored.
Scapy version
2.5.0.dev300
Python version
3.11.2
Operating system
Debian bookworm 12, kernel 6.6.13-1~bpo12+1
Additional environment information
Tested in a Linux network namespace with only a veth pair, completely isolated from the rest of the network stack. The environment I noticed this originally was also in a netns, but with a few more interfaces and a bridge.
How to reproduce
Use this script to create a "testns" namespace, set up a veth pair, run scapy and monitor packets with tcpdump:
#!/bin/bash
set -e
# Run inside netns so that it's properly isolated
if ! [ "$IN_NETNS" ]; then
ip netns del testns 2> /dev/null || true
ip netns add testns
ip netns exec testns ip link set lo up
IN_NETNS=1 exec ip netns exec testns "$0"
fi
ip link add veth0a type veth peer name veth0b
ip link set veth0a up
ip link set veth0b up
ip -6 addr
ip -6 route
# Wait some time until ND/RD after link up have been sent to avoid
# irrelevant packets in tcpdump output
sleep 20
tcpdump -nel -i veth0a &
sleep 1
scapy <<"EOF"
frame = Ether() / IPv6(dst="ff02::1", hlim=1) / IPv6ExtHdrHopByHop(options=RouterAlert()) / ICMPv6MLQuery2()
sendp(frame, iface="veth0a")
EOF
sleep 1
kill %%
Actual result
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: veth0b@veth0a: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fe80::98d4:a0ff:fe6d:83ee/64 scope link tentative
valid_lft forever preferred_lft forever
3: veth0a@veth0b: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fe80::dc66:dff:fe95:9e74/64 scope link tentative
valid_lft forever preferred_lft forever
fe80::/64 dev veth0b proto kernel metric 256 pref medium
fe80::/64 dev veth0a proto kernel metric 256 pref medium
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth0a, link-type EN10MB (Ethernet), snapshot length 262144 bytes
aSPY//YASa
apyyyyCY//////////YCa |
sY//////YSpcs scpCY//Pp | Welcome to Scapy
ayp ayyyyyyySCP//Pp syY//C | Version 2.5.0
AYAsAYYYYYYYY///Ps cY//S |
pCCCCY//p cSSps y//Y | https://github.com/secdev/scapy
SPPPP///a pP///AC//Y |
A//A cyP////C | Have fun!
p///Ac sC///a |
P////YCpc A//A | Craft packets like it is your last
scccccp///pSP///p p//Y | day on earth.
sY/////////y caa S//P | -- Lao-Tze
cayCyayP//Ya pY/Ya |
sY/PsY////YCc aC//Yp
sc sccaCY//PCypaapyCP//YSs
spCPY//////YPSps
ccaacs
using IPython 8.5.0
>>> >>> WARNING: No route found for IPv6 destination ff02::1 (no default route?)
WARNING: No route found for IPv6 destination ff02::1 (no default route?)
WARNING: more No route found for IPv6 destination ff02::1 (no default route?)
.
Sent 1 packets.
>>> 10:00:51.203947 00:00:00:00:00:00 > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 90: :: > ff02::1: HBH ICMP6, multicast listener query v2 [gaddr ::], length 28
1 packet captured
1 packet received by filter
0 packets dropped by kernel
Note the bogus source addresses in the tcpdump output.
Expected result
This is with the same script and scapy 2.4.4:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: veth0b@veth0a: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fe80::e4f2:75ff:fe98:da6c/64 scope link tentative
valid_lft forever preferred_lft forever
3: veth0a@veth0b: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fe80::4c5c:b6ff:fea9:c272/64 scope link tentative
valid_lft forever preferred_lft forever
fe80::/64 dev veth0b proto kernel metric 256 pref medium
fe80::/64 dev veth0a proto kernel metric 256 pref medium
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth0a, link-type EN10MB (Ethernet), snapshot length 262144 bytes
aSPY//YASa
apyyyyCY//////////YCa |
sY//////YSpcs scpCY//Pp | Welcome to Scapy
ayp ayyyyyyySCP//Pp syY//C | Version 2.4.4
AYAsAYYYYYYYY///Ps cY//S |
pCCCCY//p cSSps y//Y | https://github.com/secdev/scapy
SPPPP///a pP///AC//Y |
A//A cyP////C | Have fun!
p///Ac sC///a |
P////YCpc A//A | We are in France, we say Skappee.
scccccp///pSP///p p//Y | OK? Merci.
sY/////////y caa S//P | -- Sebastien Chabal
cayCyayP//Ya pY/Ya |
sY/PsY////YCc aC//Yp
sc sccaCY//PCypaapyCP//YSs
spCPY//////YPSps
ccaacs
using IPython 7.20.0
>>> >>> .
Sent 1 packets.
>>> 11:08:55.127271 e6:f2:75:98:da:6c > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 90: fe80::e4f2:75ff:fe98:da6c > ff02::1: HBH ICMP6, multicast listener query v2 [gaddr ::], length 28
1 packet captured
1 packet received by filter
0 packets dropped by kernel
I can get a result like this also with scapy 2.5.0 if I explicitly specify source addresses in Ether() and IPv6(), but I don't think that should be necessary.
Related resources
Note that this is from a different run (scapy git a05013cf again), so addresses are different:
(venv) root@debkvm:~# ip netns exec testns scapy
[...]
>>> conf.auto_crop_tables=False
>>> conf.ifaces
Source Index Name MAC IPv4 IPv6
sys 1 lo 00:00:00:00:00:00 127.0.0.1 ::1
sys 2 veth0b 5a:84:c1:3a:c7:ce
sys 3 veth0a 36:39:b4:53:45:2d
>>> conf.route6
Destination Next Hop Iface Src candidates Metr
ic
fe80::/64 :: veth0a fe80::3439:b4ff:fe53:452d 256
::1/128 :: lo ::1 0
fe80::3439:b4ff:fe53:452d/128 :: veth0a fe80::3439:b4ff:fe53:452d 0
fe80::5884:c1ff:fe3a:c7ce/128 :: veth0b fe80::5884:c1ff:fe3a:c7ce 0
Thanks for your detailed message. Can your share the content to conf.iface ? Can you try to set it to veth0a if not alteady set to this interface?
It's "lo" by default:
>>> conf.iface
<NetworkInterface lo [UP+LOOPBACK+RUNNING]>
>>>
If I set it to "veth0a", the warnings disappear and I get proper source addresses for the MLD packet.
Thanks for your answer! I was expecting this result.
Scapy uses conf.iface to build Packets, while the send() function iface argument is used to specify the output interface. With IPv6 and link-local addresses, it is mandatory to setup both conf.iface and iface to the same value to get the desired result, as Scapy cannot find which interface to use for link-local communications. Previous defaults were not satisfactory and we change the previous default behavior.
Currently, get_working_if() is the culprit and returns the loopback interface when no default IPv4 route is found.
We could add some IPv6 logic but it will be tricky for link-local address and only work fine with a single network interface when no IPv4 route exists:
- get the IPv6 interface with a default route, and return it
- get the first interface (excluding the loopback) with a fe80::/64 prefix and return it
Thanks for your explanation! And thanks a lot for such a useful tool, BTW!
I wasn't aware of the different purposes of conf.iface and sendp(iface=XXX). It's clear that the sendp() argument is used for the actual transmission, but I assumed that this would also be used to build the packet, because with IPv6 link-local traffic, there's no way you can build a correct packet without taking the actual outgoing interface into account. I guess if I would have had a default route on an interface other than veth0a, scapy wouldn't have complained but would have silently built a packet with the wrong source address, right?
May I suggest to mention this important difference in the docs (in Usage/Sending Packets and possibly also in the API reference)? And if I understand correctly, send()is affected as well. Just gave it a try and there I even don't see a packet being transmitted without setting conf.iface properly. So I guess conf.iface should be much more prominent in the docs. :wink:
BTW, I also gave scapy 2.4.4 another try: There it picked veth0b as its default interface, so it used the wrong source for sending on veth0a as well. But it didn't look as wrong an "all 0s" address, so the IP stack accepted it.