Skip to content

fix(net): canonicalize v4-mapped IPv6 destinations before policy checks#151

Open
congwang-mk wants to merge 1 commit into
mainfrom
fix/v4-mapped-ipv6-bypass
Open

fix(net): canonicalize v4-mapped IPv6 destinations before policy checks#151
congwang-mk wants to merge 1 commit into
mainfrom
fix/v4-mapped-ipv6-bypass

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Problem

Network policy matching is family-exact, but a dual-stack AF_INET6 socket that connects to the v4-mapped form ::ffff:a.b.c.d reaches a.b.c.d over IPv4. The mapped spelling therefore matched neither v4 CIDR rules nor the is_loopback() gates:

  • --net-deny 169.254.169.254 was bypassable via connect(::ffff:169.254.169.254) (fails open).
  • A v4 --net-allow target rejected the same destination written in mapped form (fails closed).
  • ::ffff:127.0.0.1 was treated as non-loopback by the connect supervision and port-remap gates.

Fix

Canonicalize v4-mapped destinations to their V4 form before any policy decision:

  • parse_ip_from_sockaddr (the parse-phase choke point for connect/sendto/sendmsg/sendmmsg) now returns Ipv6Addr::to_canonical(), so every downstream consumer sees the real destination.
  • NetworkPolicy::allows canonicalizes at entry as defense in depth, keeping the pure matcher safe regardless of caller.
  • read_sockaddr_for_event now reuses the shared materialize parsers (dropping its duplicated hand-rolled parsing), so policy_fn callbacks judge the same canonical address the enforcement path does.

Sockaddr rewrites must keep following the socket's family, not the canonical IP's family, so the proxy-redirect plan and the record_orig_dest local-bind probe now use a new sockaddr_is_ipv6 helper that reads the family field directly. A v6 socket connecting to a mapped destination is still redirected with an AF_INET6 sockaddr.

Testing

  • New unit tests at three layers: NetworkPolicy::allows (deny and allow sides), parse_ip_from_sockaddr canonicalization, and a plan_connect_target regression guard for the redirect family. All were written first and failed on the pre-fix code.
  • Full suite passes: 515 lib + 292 integration Rust tests, 386 Python tests.
  • Verified end to end with the CLI: sandlock run --net-deny 169.254.169.254 now refuses connect(::ffff:169.254.169.254) instantly with ECONNREFUSED, while a control run denying an unrelated IP proceeds to the wire and times out.

Known remaining wart (out of scope here): a rule literal written in mapped form, e.g. --net-deny '::ffff:1.2.3.4', still parses as a V6 host and will never match a canonical destination. Canonicalizing single-host mapped literals in IpCidr::parse would be a small follow-up.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant