From 28eab322f96133edfc3d368a0b1d7299f4bd0a72 Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Sun, 20 Sep 2026 12:27:32 +0000 Subject: [PATCH 1/4] Fix UDP checksum over the padding of an encapsulated layer The Padding of an inner Ethernet frame sits inside the UDP datagram, as reported in #4906, but post_build() only sees the payload build and left those bytes out of the checksum. --- scapy/layers/inet.py | 12 ++++++++-- test/scapy/layers/inet.uts | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index 2486ba0515b..02f474426b1 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -843,14 +843,22 @@ def post_build(self, p, pay): tmp_len = len(p) p = p[:4] + struct.pack("!H", tmp_len) + p[6:] if self.chksum is None: + # A Padding layer that belongs to an encapsulated protocol (for + # instance Ethernet over VXLAN) is data as far as UDP is + # concerned. The len field tells how many of the following + # padding bytes are inside the datagram, the remaining ones + # being padding of an under layer. + ck_p = p + if tmp_len > len(p): + ck_p += self.payload.build_padding()[:tmp_len - len(p)] if isinstance(self.underlayer, IP): - ck = in4_chksum(socket.IPPROTO_UDP, self.underlayer, p) + ck = in4_chksum(socket.IPPROTO_UDP, self.underlayer, ck_p) # According to RFC768 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501 if ck == 0: ck = 0xFFFF p = p[:6] + struct.pack("!H", ck) + p[8:] elif isinstance(self.underlayer, scapy.layers.inet6.IPv6) or isinstance(self.underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501 - ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_UDP, self.underlayer, p) # noqa: E501 + ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_UDP, self.underlayer, ck_p) # noqa: E501 # According to RFC2460 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501 if ck == 0: ck = 0xFFFF diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index 06d49630721..429272b8752 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -543,6 +543,55 @@ assert bpkt.chksum == 0x70bd and bpkt.payload.chksum == 0xbb17 conf.debug_dissector = True += UDP checksum covers the padding of an encapsulated layer +~ udp +# The Padding of an inner Ethernet frame is data for the outer UDP layer. +# GH#4906: Ether / Dot1Q / IPv6 / UDP / VXLAN / Dot3 / LLC / data / Padding +pkt_data = bytes.fromhex("ffffffffffffffffffffffff8100000086dd60000000010e1100000000000000000000000000000000000000000000000000000000000000000018bd2118010e01a8738a32362d5473c879f89f16a8f9cd673fec8d6b000d9a2976054be71268024a7817f64d63fad1d4eb2db918f004c198dc03e7ec4e836ca554e1b4f0c157c61728ae722bd55eac05d698f04a0df32b8d21f031ae70a7ad530d027915763af0e736fa6d61a334289b3aef64855b44ed09f02de48485a3337575d409561fe192bca02214a8a5b10cc66c756992eadaeaf56f8eeeb58c18cce9f36fd8983ff7890dedc28ed254080c180231440188d5afc32bff2193a8f94a1b683b1e850a613eaf42ccd37e5af706d93b11e31649a1b9f39c613ce5a4263fcfe7873da6fdd9d09c90f15b687efc3342189cbac70ab686d1dc5f000015128c4d3400080000034584f14d8e217811d6cee444") +pkt = Ether(pkt_data) +assert pkt[UDP].chksum == 0x1a8 +del pkt[UDP].chksum +assert Ether(raw(pkt))[UDP].chksum == 0x1a8 + += UDP checksum covers the padding of an encapsulated layer - IPv4 +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IP(raw(IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +assert isinstance(pkt[LLC].payload.payload, Padding) +assert pkt[UDP].chksum == 0x94fb +del pkt[UDP].chksum +assert IP(raw(pkt))[UDP].chksum == 0x94fb + += UDP checksum covers the padding of an encapsulated layer - IPv6 +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IPv6(raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +assert pkt[UDP].chksum == 0x499a +del pkt[UDP].chksum +assert IPv6(raw(pkt))[UDP].chksum == 0x499a + += UDP checksum ignores the padding of an under layer (control) +~ udp +# The trailer of the outer Ethernet frame is not part of the datagram +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55", src="66:77:88:99:aa:bb")/IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1, dport=2)/Raw(b"ab")) + b"\x00" * 16) +assert pkt[UDP].len == 10 and isinstance(pkt[Raw].payload, Padding) +del pkt[UDP].chksum +assert Ether(raw(pkt))[UDP].chksum == 0x8e61 + += UDP checksum with a len field larger than the available data (control) +~ udp +with no_debug_dissector(): + pkt = IP(raw(IP()/UDP(len=100)/Raw(b"AAAA"))) + +assert pkt[UDP].chksum == 0x7e8f + += UDP checksum with a len field smaller than the payload (control) +~ udp +with no_debug_dissector(): + pkt = IP(raw(IP()/UDP(len=10)/Raw(b"AAAAAAAAAAAA"))) + +assert pkt[UDP].chksum == 0x79dc + = IP with forced-length 0 p = IP()/TCP() p[IP].len = 0 From 959e14b78d9926863251bd791dd739937de7b245 Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:50:31 +0000 Subject: [PATCH 2/4] Add UDP checksum padding tests for the mixed and boundary cases Cover an inner padding together with an outer frame trailer, an IPv6 extension header under layer, a len field covering a part of the padding or claiming more than the capture holds, and a len field below the payload as a control. --- test/scapy/layers/inet.uts | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index 429272b8752..57be70b43df 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -592,6 +592,55 @@ with no_debug_dissector(): assert pkt[UDP].chksum == 0x79dc += UDP checksum covers the inner padding but not the trailer of the outer frame +~ udp +# Both kinds of padding at once: the inner Ethernet frame is padded inside the +# datagram, the outer Ethernet frame has a trailer outside of it. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55", src="66:77:88:99:aa:bb")/IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner)) + b"TTTTTTTTTTTTTTTT") +assert pkt[UDP].len == 45 and len([x for x in pkt.iterpayloads() if isinstance(x, Padding)]) == 2 +del pkt[UDP].chksum +assert Ether(raw(pkt))[UDP].chksum == 0x94fb + += UDP checksum covers the padding of an encapsulated layer - IPv6 extension header +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IPv6(raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/IPv6ExtHdrDestOpt()/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +assert isinstance(pkt[UDP].underlayer, IPv6ExtHdrDestOpt) +assert pkt[UDP].chksum == 0x499a +del pkt[UDP].chksum +assert IPv6(raw(pkt))[UDP].chksum == 0x499a + += UDP checksum with a len field covering only a part of the padding +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IP(raw(IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +# One single byte of the padding is inside the datagram +pkt[UDP].len = 38 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IP(raw(pkt))[UDP].chksum == 0xd5f3 + += UDP checksum with a len field claiming more padding than the capture holds +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IP(raw(IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 49 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IP(raw(pkt))[UDP].chksum == 0x94f7 + += UDP checksum with a len field below the payload while padding follows (control) +~ udp +# The len field is not an authority to remove data, only to add padding: the +# checksum must be the one of the payload that is built. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IP(raw(IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 36 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IP(raw(pkt))[UDP].chksum == 0xd645 + = IP with forced-length 0 p = IP()/TCP() p[IP].len = 0 From 7b427f2fb97f26ae545cd884f43fd13fa4084722 Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:14:16 +0000 Subject: [PATCH 3/4] Add UDP checksum padding tests for the IPv6 boundary lengths in6_chksum computes the length of the pseudo header from the buffer it receives, where in4_chksum uses the length of the IP layer, so the IPv4 cases do not cover the IPv6 arm when the len field disagrees with the available data. --- test/scapy/layers/inet.uts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index 57be70b43df..352005be982 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -621,6 +621,18 @@ del pkt[UDP].chksum with no_debug_dissector(): assert IP(raw(pkt))[UDP].chksum == 0xd5f3 += UDP checksum with a len field covering only a part of the padding - IPv6 +~ udp +# in6_chksum takes the length of the pseudo header from the buffer it is +# given, where in4_chksum uses the one of the IP layer: both arms of the +# dispatch need the padding bytes that the len field puts in the datagram. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IPv6(raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 38 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IPv6(raw(pkt))[UDP].chksum == 0x8a99 + = UDP checksum with a len field claiming more padding than the capture holds ~ udp inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" @@ -630,6 +642,15 @@ del pkt[UDP].chksum with no_debug_dissector(): assert IP(raw(pkt))[UDP].chksum == 0x94f7 += UDP checksum with a len field claiming more padding than the capture holds - IPv6 +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IPv6(raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 49 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IPv6(raw(pkt))[UDP].chksum == 0x4996 + = UDP checksum with a len field below the payload while padding follows (control) ~ udp # The len field is not an authority to remove data, only to add padding: the From b28bb4bc84e553d39ba73b077cb8eb5e36c8e2a4 Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:32:18 +0000 Subject: [PATCH 4/4] Add UDP checksum padding tests for the remaining len boundaries --- test/scapy/layers/inet.uts | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index 352005be982..80a11c4a013 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -662,6 +662,97 @@ del pkt[UDP].chksum with no_debug_dissector(): assert IP(raw(pkt))[UDP].chksum == 0xd645 += UDP checksum with a len field one short of the whole padding +~ udp +# One byte of the inner padding is left out of the datagram by the len field. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IP(raw(IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 44 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IP(raw(pkt))[UDP].chksum == 0xe4fc + += UDP checksum with a len field one short of the whole padding - IPv6 +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IPv6(raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 44 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IPv6(raw(pkt))[UDP].chksum == 0x999c + += UDP checksum with the maximum len field +~ udp +# 0xffff is the largest value the field holds: the slice bound is far past the +# padding, which build_padding() then returns whole. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IP(raw(IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 65535 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IP(raw(pkt))[UDP].chksum == 0x9528 + += UDP checksum with the maximum len field - IPv6 +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IPv6(raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 65535 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IPv6(raw(pkt))[UDP].chksum == 0x49c7 + += UDP checksum with a len field covering only a part of the padding - IPv6 extension header +~ udp +# The boundary lengths on the second operand of the under layer dispatch: an +# extension header derives the pseudo header the same way IPv6 does. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IPv6(raw(IPv6(src="2001:db8::1", dst="2001:db8::2")/IPv6ExtHdrDestOpt()/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 38 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IPv6(raw(pkt))[UDP].chksum == 0x8a99 + += UDP checksum with a len field reaching into the trailer of the outer frame +~ udp +# build_padding() walks every following Padding layer, so a len field larger +# than the inner padding keeps taking bytes from the outer trailer. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55", src="66:77:88:99:aa:bb")/IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner)) + b"TTTTTTTTTTTTTTTT") +assert pkt[UDP].payload.build_padding() == b"PPPPPPPP" + b"T" * 16 +pkt[UDP].len = 53 +del pkt[UDP].chksum +with no_debug_dissector(): + assert Ether(raw(pkt))[UDP].chksum == 0x43a2 + += UDP checksum with a len field past every padding layer available +~ udp +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = Ether(raw(Ether(dst="00:11:22:33:44:55", src="66:77:88:99:aa:bb")/IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner)) + b"TTTTTTTTTTTTTTTT") +pkt[UDP].len = 70 +del pkt[UDP].chksum +with no_debug_dissector(): + assert Ether(raw(pkt))[UDP].chksum == 0xf23f + += UDP checksum with a len field equal to the payload while padding follows (control) +~ udp +# tmp_len == len(p) leaves the guard false: the padding stays out of the +# checksum, as it does for a packet built from scratch. +inner = raw(Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")) + b"PPPPPPPP" +pkt = IP(raw(IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Raw(inner))) +pkt[UDP].len = 37 +del pkt[UDP].chksum +with no_debug_dissector(): + assert IP(raw(pkt))[UDP].chksum == 0xd644 + += UDP checksum of a packet built from scratch with an explicit Padding (control) +~ udp +# Nothing changes when post_build() computes the len field itself: tmp_len is +# then len(p) and the padding is outside of the datagram. +pkt = IP(src="1.2.3.4", dst="5.6.7.8")/UDP(sport=1111, dport=4789)/VXLAN(flags=0x08, vni=42)/Dot3(dst="11:22:33:44:55:66", src="aa:bb:cc:dd:ee:ff", len=7)/LLC(dsap=0x06, ssap=0x06, ctrl=3)/Raw(b"abcd")/Padding(b"PPPPPPPP") +bpkt = IP(raw(pkt)) +assert bpkt[UDP].len == 37 +assert bpkt[UDP].chksum == 0xd64c + = IP with forced-length 0 p = IP()/TCP() p[IP].len = 0