Skip to content

if_IPv6addr prefix length parsed as an ASCII string, not an octet #346

Description

@JarryShaw

What is wrong

IPv6InterfaceField.post_process (pcapkit/corekit/fields/ipaddress.py:269) reads the trailing prefix-length octet as if it were an ASCII decimal string:

    def post_process(self, value: 'bytes', packet: 'dict[str, Any]') -> 'IPv6Interface':
        ip = ipaddress.IPv6Address(value[:16])
        mask = int(value[16:])                      # line 269 -- value[16:] is one raw byte

int(b'\x40') is int() on a one-byte bytes, which Python interprets as a decimal literal, not as the number 64.

The field's own pre_process, 14 lines above, writes it as a binary octet:

        prefixlen = cast('int', val._prefixlen)
        return ip.packed + prefixlen.to_bytes(1, 'big')     # line 255

so the field cannot read back what it writes. § 4.2 of draft-ietf-opsawg-pcapng agrees with pre_process: if_IPv6addr is option type 5, fixed length 17, and

The first 16 octets are the IP address and the next octet is the prefix length.

Example: 2001:0db8:85a3:08d3:1319:8a2e:0370:7344/64 is written (in hex) as '20 01 0d b8 85 a3 08 d3 13 19 8a 2e 03 70 73 44 40'.

That trailing 40 is the byte int(b'@') chokes on.

Symptom

The field is used by IF_IPv6AddrOption (pcapkit/protocols/schema/misc/pcapng.py:647), so any Interface Description Block carrying if_IPv6addr is affected. Two outcomes, and the silent one is worse:

  • Raises for every prefix length whose octet is not an ASCII digit — which is all but ten of them, including every common one (/64, /128, /48, /32, /16, /8, /0):

      File ".../pcapkit/corekit/fields/ipaddress.py", line 269, in post_process
        mask = int(value[16:])
    ValueError: invalid literal for int() with base 10: b'@'
    
  • Silently wrong for the ten prefix lengths 48–57, whose octets happen to be 0x30–0x39, i.e. '0'–'9'. Every one of them decodes to the wrong value:

    encoded decoded as
    /48 /0
    /49 /1
    … …
    /56 /8
    /57 /9

    A capture recording 2001:db8:85a3:8d3:1319:8a2e:370:7344/56 comes back as .../8 with no warning at all.

Reproduction

The field on its own:

import ipaddress
from pcapkit.corekit.fields.ipaddress import IPv6InterfaceField

f = IPv6InterfaceField()
for plen in (64, 56):
    raw = f.pre_process(ipaddress.ip_interface(f'2001:db8::1/{plen}'), {})   # what pcapkit writes
    try:
        print(f'/{plen} ->', f.post_process(raw, {}))
    except ValueError as exc:
        print(f'/{plen} ->', type(exc).__name__, exc)

# /64 -> ValueError invalid literal for int() with base 10: b'@'
# /56 -> 2001:db8::1/8

End to end, with the spec's own example address:

import struct, ipaddress

def pad4(b): return b + b'\x00' * ((4 - len(b) % 4) % 4)
def block(t, body):
    body = pad4(body); n = 12 + len(body)
    return struct.pack('<II', t, n) + body + struct.pack('<I', n)
def opt(c, v): return struct.pack('<HH', c, len(v)) + pad4(v)

shb = lambda: block(0x0A0D0D0A, struct.pack('<IHHq', 0x1A2B3C4D, 1, 0, -1))
epb = lambda d: block(0x00000006, struct.pack('<IIIII', 0, 0, 0, len(d), len(d)) + pad4(d))
ETH = bytes.fromhex('ffffffffffff001122334455') + b'\x08\x06' + b'\x00' * 28

import pcapkit
for plen in (64, 56):
    val = ipaddress.IPv6Address('2001:db8:85a3:8d3:1319:8a2e:370:7344').packed + bytes([plen])
    idb = block(0x00000001, struct.pack('<HHI', 1, 0, 0x40000) + opt(5, val) + opt(0, b''))
    open(f'/tmp/if6_{plen}.pcapng', 'wb').write(shb() + idb + epb(ETH))
    try:
        pcapkit.extract(fin=f'/tmp/if6_{plen}.pcapng', store=True, nofile=True)
        print(plen, 'parsed')
    except ValueError as exc:
        print(plen, '->', type(exc).__name__, exc)

/64 raises; /56 "succeeds" and yields IF_IPv6AddrOption(type=if_IPv6addr [5], length=17, interface=2001:db8:85a3:8d3:1319:8a2e:370:7344/8).

What a fix would need to touch

Line 269: value[16], or int.from_bytes(value[16:], 'big'). Then build the interface from the prefix length rather than from a netmask string — ipaddress.ip_interface(f'{ip}/{mask}') at line 271 happens to work for a prefix length, but IPv4InterfaceField.post_process (line 206) deliberately passes a dotted netmask there, so the two paths are not interchangeable and a test pinning both would be worth having.

How it was found

While building deterministic sample captures for the test suite. The generators' module docstrings record it: examples/samples/pcapng.py lists if_IPv6addr among the constructs deliberately left out of the fixtures, because a /64 prefix raises.

Line numbers are from main at 72f950d.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions