Skip to content

Commit 4f12c80

Browse files
committed
dns: fix EDNS0ClientSubnet address truncation for non-octet-aligned prefix lengths
When source_plen is set to a value whose ceil(plen/8) byte count exceeds the number of non-zero bytes in the address (e.g. source_plen=23 for 101.132.0.0), _pack_subnet incorrectly stripped the trailing zero byte, producing a 2-byte address instead of the required 3 bytes. Per RFC 7871, the ADDRESS field MUST be truncated to ceil(source_plen/8) bytes. Fix _pack_subnet to accept an optional plen parameter and apply RFC-compliant truncation when it is provided. i2m and i2len now pass pkt.source_plen so the correct byte count is used during packet building. Fixes: #4942
1 parent 9ae49f8 commit 4f12c80

2 files changed

Lines changed: 44 additions & 6 deletions

File tree

scapy/layers/dns.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -558,9 +558,16 @@ def m2i(self, pkt, x):
558558
x = x[: operator.floordiv(self.af_length, 8)]
559559
return inet_ntop(self.af_familly, x)
560560

561-
def _pack_subnet(self, subnet):
562-
# type: (bytes) -> bytes
561+
def _pack_subnet(self, subnet, plen=None):
562+
# type: (bytes, Optional[int]) -> bytes
563563
packed_subnet = inet_pton(self.af_familly, plain_str(subnet))
564+
if plen is not None:
565+
# RFC 7871: ADDRESS MUST be truncated to the number of bits
566+
# indicated by SOURCE PREFIX-LENGTH, padded with 0 bits to the
567+
# end of the last octet needed. Use ceil(plen / 8) bytes.
568+
num_bytes = operator.floordiv(plen + 7, 8)
569+
return packed_subnet[:num_bytes]
570+
# When prefix length is not known, strip trailing zero bytes.
564571
for i in list(range(operator.floordiv(self.af_length, 8)))[::-1]:
565572
if packed_subnet[i] != 0:
566573
i += 1
@@ -571,21 +578,23 @@ def i2m(self, pkt, x):
571578
# type: (Optional[Packet], Optional[Union[str, Net]]) -> bytes
572579
if x is None:
573580
return self.af_default
581+
plen = getattr(pkt, 'source_plen', None)
574582
try:
575-
return self._pack_subnet(x)
583+
return self._pack_subnet(x, plen)
576584
except (OSError, socket.error):
577585
pkt.family = 2
578-
return ClientSubnetv6("", "")._pack_subnet(x)
586+
return ClientSubnetv6("", "")._pack_subnet(x, plen)
579587

580588
def i2len(self, pkt, x):
581589
# type: (Packet, Any) -> int
582590
if x is None:
583591
return 1
592+
plen = getattr(pkt, 'source_plen', None)
584593
try:
585-
return len(self._pack_subnet(x))
594+
return len(self._pack_subnet(x, plen))
586595
except (OSError, socket.error):
587596
pkt.family = 2
588-
return len(ClientSubnetv6("", "")._pack_subnet(x))
597+
return len(ClientSubnetv6("", "")._pack_subnet(x, plen))
589598

590599

591600
class ClientSubnetv6(ClientSubnetv4):

test/scapy/layers/dns_edns0.uts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,35 @@ assert raw(d) == raw_d
154154
d = DNSRROPT(raw_d)
155155
assert EDNS0ClientSubnet in d.rdata[0] and d.rdata[0].family == 2 and d.rdata[0].address == "2001:db8::"
156156

157+
= source_plen with trailing-zero address byte (GH #4942)
158+
# RFC 7871: ADDRESS must be truncated to ceil(source_plen/8) bytes.
159+
# For source_plen=23, ceil(23/8)=3 bytes are required even when the
160+
# 3rd byte is 0x00 (101.132.0.0 -> 0x65 0x84 0x00).
161+
162+
b = EDNS0ClientSubnet(source_plen=23, address='101.132.0.0')
163+
raw_b = raw(b)
164+
# optlen must be 7 (4 fixed bytes + 3 address bytes)
165+
assert raw_b == b'\x00\x08\x00\x07\x00\x01\x17\x00\x65\x84\x00', repr(raw_b)
166+
167+
b2 = EDNS0ClientSubnet(raw_b)
168+
assert b2.source_plen == 23
169+
assert b2.address == '101.132.0.0'
170+
171+
= source_plen with non-zero last byte (sanity check)
172+
# For source_plen=24 on 10.20.30.0, exactly 3 bytes are needed.
173+
b = EDNS0ClientSubnet(source_plen=24, address='10.20.30.0')
174+
raw_b = raw(b)
175+
assert raw_b == b'\x00\x08\x00\x07\x00\x01\x18\x00\x0a\x14\x1e', repr(raw_b)
176+
b2 = EDNS0ClientSubnet(raw_b)
177+
assert b2.source_plen == 24
178+
assert b2.address == '10.20.30.0'
179+
180+
= source_plen=0 produces empty address
181+
b = EDNS0ClientSubnet(source_plen=0, address='0.0.0.0')
182+
raw_b = raw(b)
183+
# optlen == 4 (no address bytes), source_plen=0
184+
assert raw_b == b'\x00\x08\x00\x04\x00\x01\x00\x00', repr(raw_b)
185+
157186

158187
+ EDNS0 - Cookie
159188

0 commit comments

Comments
 (0)