Skip to content

IPv6 fragmented traffic stays blackholed on the direct path (Fragment header stops the ext-header walk) #779

Description

@kasnder

Background

PR #778 (fixing #769) extracted the IPv6 extension-header walk into ip6_skip_ext_headers() (app/src/main/jni/netguard/ip6_ext.{h,c}). One of its deliberate design decisions: the walk stops when it reaches a Fragment header (44) instead of walking past it, because a non-first fragment has no upper-layer header at all and Fragment's second byte is a reserved field, not a length (#769 flagged exactly this trap). That is correct and safe — but it leaves a known gap behind, tracked here so it isn't forgotten.

Current behavior (after #778)

For a direct-routed (non-WireGuard) IPv6 flow whose packets carry a Fragment header:

  1. The walk stops at the Fragment header; protocol = 44, which matches none of handle_ip()'s TCP/UDP/ICMP dispatch branches.
  2. If the destination is blocked: dropped correctly, logged with the right protocol.
  3. If the destination is allowed: the generic IP-level decision passes, but no handler fires — so the packet is silently blackholed: never forwarded, never logged as blocked. This is unchanged from pre-Fix IPv6 extension header walk in handle_ip() #778 master (which never dispatched chained-extension packets either), i.e. no regression, just an accepted limitation that now has a precise name.
  4. WireGuard-routed flows are unaffected: write_wireguard_packet() forwards the raw packet regardless of the walk result.

Parity note: IPv4 is no better today — handle_ip() drops every IPv4 packet with IP_MF set (ip.c, the frag_off & IP_MF guard), including first fragments. So post-#778 both families treat fragmented traffic the same way on the direct path: drop.

Why not fixed in #778

Dispatching a first fragment (offset == 0) to handle_tcp/handle_udp alone would be wrong: the subsequent non-first fragments carry no L4 header, cannot be attributed to the session by the same dispatch, and would be dropped piecemeal — the state machines would see a SYN or data segment whose continuation never arrives. A real fix needs fragment reassembly before dispatch.

Options

  • A — Reassemble before dispatch (full fix). Hold non-first fragments per (src, dst, ident) up to a bounded size/time, dispatch once the first fragment yields the L4 header. Costs memory and battery for state that legitimate traffic rarely produces (PMTUD keeps most flows unfragmented), and adds engine complexity.
  • B — Keep dropping, document as a known limitation. Same treatment as UID-global attribution: stated, deliberate, revisited if real-world reports appear. WireGuard users are unaffected either way.
  • C — Dispatch first fragments without reassembly. Listed only to reject it explicitly: creates half-open sessions and unpredictable drops mid-flow. Correctly walk IPv6 extension headers in handle_ip() (ip.c) #769 already warned against parsing non-first fragments; this variant fails differently but just as badly.

Related, smaller: ESP (50) stops the walk for the same reason (encrypted payload, nothing parseable behind it); direct-routed ESP flows share the silent-drop-if-allowed fate. Any decision here should say whether ESP stays a documented limitation permanently (it probably does).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions