[ndp] neighsol(): optional strict validation of IPv6 Hop Limit (==255) per RFC 4861
Summary
This PR adds an optional strict mode to scapy.layers.inet6.neighsol() to validate the IPv6 Hop Limit (HLIM) == 255 on received Neighbor Advertisement (NA) replies. When enabled, the function first narrows capture with a BPF filter and then performs a second, protocol-level check in Python before returning the packet.
Motivation / Rationale
Per RFC 4861 (Neighbor Discovery for IPv6), ND control messages MUST be sent with Hop Limit = 255, and receivers MUST verify HLIM=255 to prevent off-link spoofing. Today neighsol() sets hlim=255 on the sent NS but does not validate HLIM on received NA packets. This can accept responses that traversed a router (HLIM<255) or were injected off-link. Enforcing (optionally) HLIM==255 improves standards compliance and hardens neighbor discovery. (See Scapy’s contribution guidelines for tests and coding style; tox instructions used for local testing. )
Codecov Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 80.90%. Comparing base (29433fc) to head (ce406ff).
:warning: Report is 1 commits behind head on master.
Additional details and impacted files
@@ Coverage Diff @@
## master #4829 +/- ##
==========================================
- Coverage 80.90% 80.90% -0.01%
==========================================
Files 366 366
Lines 90132 90132
==========================================
- Hits 72921 72918 -3
- Misses 17211 17214 +3
| Files with missing lines | Coverage Δ | |
|---|---|---|
| scapy/layers/inet6.py | 88.57% <ø> (ø) |
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
Do you have a use case in mind that requires this check in BPF?
The hop limit check could be done in pure Python.
Do you have a use case in mind that requires this check in BPF?
The hop limit check could be done in pure Python.
Thanks a lot for the suggestion, @guedou! I refactored the change to remove the BPF dependency and do the hop-limit validation in pure Python. Concretely: I switched neighsol() to srp and added a stop_filter so we only treat responses as valid when ICMPv6ND_NA is present and IPv6.hlim == 255. Please take another look.
Is there a need for that change ?
Is there a need for that change ?
This is Qualcomm Device Dial NIC and response 2 NA messages. If don't switch from srp1 to srp, will get wrong packet.
Do you have any other better suggestions?
Is there a need for that change ?
This is not a real world ipv6 address, so i directly paste this image. This is Qualcomm Device Dial NIC and response 2 NA messages. If don't switch from srp1 to srp, will get wrong packet.
Do you have any other better suggestions?
In this scenario, using srp1 with a stop_filter still returns the wrong Ethernet frame. Using srp without a stop_filter waits until the timeout occurs. The reliable approach is to use srp with a stop_filter and then iterate over the answers to select the first compliant packet (e.g., ICMPv6ND_NA with IPv6.hlim == 255).
I personally disagree with @guedou, the BPF idea was smart, and I feel more elegant
For readability, lfilter that only check the hlim value seems like a good choice.
I am unsure that we need any performance improvement there.
@itchenfei I think that what you really need is to filter hlim == 255. Scapy should take care of the rest.
@itchenfei can you share the complete neighsol() call that you used?
@itchenfei can you share the complete
neighsol()call that you used?
I just sent an ICMPv6 packet, and the NDP was triggered automatically.
import os
from scapy.arch import get_if_addr6
from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest
from scapy.sendrecv import sr1
from scapy.config import conf
PING_DEST = "2001:da8:d800:642::248"
PING_INTERFACE = "eth0"
packet_id = os.getpid() & 0xFFFF
ip_layer = IPv6(
dst=PING_DEST,
src=get_if_addr6(PING_INTERFACE)
)
protocol_layer = ICMPv6EchoRequest(
data="A" * 50,
id=packet_id,
seq=1,
)
packet = ip_layer / protocol_layer
reply = sr1(packet, timeout=1, verbose=0)
if reply:
reply.show()
print(f"ipv6 neighbor: {conf.netcache.in6_neighbor}")
I personally disagree with @guedou, the BPF idea was smart, and I feel more elegant
For readability, lfilter that only check the hlim value seems like a good choice.
I am unsure that we need any performance improvement there.
@itchenfei I think that what you really need is to filter hlim == 255. Scapy should take care of the rest.
I’m not sure whether BPF will have any compatibility issues on the NT kernel, but using sniff right now seems easier to understand and maintain. c5885981c110df6fb88558c091a25123e3cc4a2a
@itchenfei your code is not calling neighsol()at all.
The following works fine for me: neighsol("fe80::ba26:6cff:fe5f:4eee", "fe80:1d::1c61:245:1e6e:8092", "en7")
@itchenfei your code is not calling
neighsol()at all.The following works fine for me:
neighsol("fe80::ba26:6cff:fe5f:4eee", "fe80:1d::1c61:245:1e6e:8092", "en7")
When an ICMPv6 packet is sent, NDP is automatically triggered and the neighbor’s MAC address is appended as part of the ICMPv6 packet.
@itchenfei thanks for your answer. Can you share a pcap files containing the NS / NA that were not matched?
Your initial fix looks like the more efficient (i.e. using a BPF filter), but I suspect a deeper bug in Scapy. Sorry for the back and forth, I should have made it clear that I am trying to understand for the root cause.
@itchenfei thanks for your answer. Can you share a pcap files containing the NS / NA that were not matched?
Your initial fix looks like the more efficient (i.e. using a BPF filter), but I suspect a deeper bug in Scapy. Sorry for the back and forth, I should have made it clear that I am trying to understand for the root cause.
Thanks for digging into this! I’ve zipped the capture since GitHub only accepts archives:
pcap: icmp6.zip (attached above) It contains an NS followed by two NAs where the first has IPv6.hlim = 254 and the second IPv6.hlim = 255.
Finally, big thanks to all maintainers for the helpful feedback and for keeping Scapy efficient and reliable—digging into this taught me a lot about how filter and send/receive paths work, which will definitely help my future usage.
Thanks for sharing. These NA packets do look strange. Is the following patch fixing your issue?
% git diff
diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py
index b585c92e..2745291d 100644
--- a/scapy/layers/inet6.py
+++ b/scapy/layers/inet6.py
@@ -2258,6 +2258,10 @@ class ICMPv6ND_NA(_ICMPv6NDGuessPayload, _ICMPv6, Packet):
return bytes_encode(self.tgt) + self.payload.hashret()
def answers(self, other):
+ if not isinstance(self.underlayer, IPv6):
+ return False
+ if isinstance(self.underlayer.hlim != 255:
+ return False
return isinstance(other, ICMPv6ND_NS) and self.tgt == other.tgt
# associated possible options : target link-layer option, Redirected header
This is not a real world ipv6 address, so i directly paste this image.
This is Qualcomm Device Dial NIC and response 2 NA messages. If don't switch from srp1 to srp, will get wrong packet.