Skip to content

feat(supervisor): IPv6 policy DNS egress and NAT64-aware SSRF checks - #3702

Open
Joffref wants to merge 6 commits into
NVIDIA:mainfrom
Joffref:feat/policy-dns-ipv6-egress
Open

Joffref wants to merge 6 commits into
NVIDIA:mainfrom
Joffref:feat/policy-dns-ipv6-egress

Conversation

@Joffref

@Joffref Joffref commented Sep 25, 2026 •

Copy link
Copy Markdown

Summary

On IPv6-only hosts (NAT64/DNS64), policy DNS never resolved an allowed name because AAAA always got an empty answer. This adds a policy_dns_ipv6_egress setting (auto by default, which only enables AAAA on IPv6-only supervisor namespaces), and makes the SSRF checks treat a NAT64 address as the IPv4 address it embeds, so IPv6 answers get the same protections as IPv4 ones.

Related Issue

Closes #3716

Changes

  • NAT64 classification (openshell-core::net::nat64): RFC 6052 extraction for every prefix length, RFC 7050 discovery from ipv4only.arpa, and an append-only registry of network prefixes. The SSRF checks use it:

    • is_internal_ip and is_always_blocked_ip;
    • the cloud metadata check;
    • allowed_ips matching, so 10.0.0.0/8 covers 64:ff9b::a00:5 like it covers 10.0.0.5.

    64:ff9b::/96 is always known. The rest of 64:ff9b:1::/48 counts as internal.

  • Driver settings: policy_dns_ipv6_egress (auto / enabled / disabled) and nat64_prefixes in the Docker, Podman, Kubernetes and VM driver configs. The drivers validate them at startup and pass them on the supervisor argv (--policy-dns-ipv6-egress, --nat64-prefix).

  • Supervisor startup: configured NAT64 prefixes are registered, and discovery runs, before any egress path starts. The IPv6 mode is resolved once from /proc/net/route and /proc/net/ipv6_route and applied to both policy DNS runtimes.

  • OCSF: one config event for the IPv6 decision (requested mode, result, IPv4/IPv6 default route, route_state: ipv6_only, dual_stack, ipv4_only, no_default_route or route_table_unavailable). A second one for the NAT64 prefixes in use.

  • Docs: docs/how-it-works/gateways/configuration.mdx, docs/how-it-works/policies/overview.mdx, architecture/sandbox.md.

Testing

  • Unit tests pass for the touched crates: openshell-core, openshell-supervisor-network, openshell-supervisor, and the Docker, Podman, Kubernetes, VM and gateway crates. The Linux-only runtime path builds for x86_64-unknown-linux-gnu.

  • New tests:

    • NAT64: RFC 6052 examples at all six lengths, discovery, and the SSRF predicates and allowed_ips matching with NAT64 addresses (well-known, configured, local-use, CIDRs).

    • Route detection: fixtures for the supervisor namespace of each backend:

      • Docker (bridge, IPv6 network);
      • Podman (netavark, pasta);
      • Kubernetes (Calico);
      • VM (gvproxy);
      • a table captured on an IPv6-only host.

      Each is covered IPv4-only, dual-stack and IPv6-only where it applies, plus unreadable tables and down/reject routes.

    • mediated_policy_dns_ipv6_end_to_end: PolicyDnsRuntime::start_mediated with a fake NetworkMediationSource and a fake DNS64 upstream. It covers:

      • AAAA over UDP and TCP, and the synthetic IPv6 mapping;
      • the allowed TCP open dialing the pinned upstream IPv6;
      • denials for an unlisted binary, a missing identity, the real upstream IPv6, an unallocated synthetic address, the wrong port, and a stale policy generation;
      • NAT64 answers per SSRF tier (exact host, allowed_ips, wildcard, metadata, configured prefix).
    • Resolver failure: a second mediated test with an unreachable trusted resolver.

    • Store: IPv6 wrong port, stale generation and expiry.

    • Drivers: each driver renders the supervisor args from its config.

    • E2E: e2e/rust/tests/policy_dns_ipv6_egress.rs checks the decision event on each driver lane. OPENSHELL_E2E_EXPECT_ROUTE_STATE pins the expected state on lanes with a known network. It compiles, but I haven't run it on a driver lane.

  • Not covered yet: the sandbox-side path from an IPv6 socket to the relay, and an IPv6-only CI lane (both tracked in feat(network): support policy DNS IPv6 egress across the sandbox–supervisor boundary #3716).

  • mise run pre-commit passes. mise isn't installed here, so I ran the same checks by hand: cargo fmt --all --check, the SPDX header check, and clippy on the touched crates.

  • Unit tests added/updated

  • E2E tests added/updated (if applicable)

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

@copy-pr-bot

copy-pr-bot Bot commented Sep 25, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

All contributors have signed the DCO ✍️ ✅
Posted by the DCO Assistant Lite bot.

@github-actions

Copy link
Copy Markdown

Thank you for your interest in contributing to OpenShell, @Joffref.

This project uses a vouch system for first-time contributors. Before submitting a pull request, you need to be vouched by a maintainer.

To get vouched:

  1. Open a Vouch Request discussion.
  2. Describe what you want to change and why.
  3. Write in your own words — do not have an AI generate the request.
  4. A maintainer will comment /vouch if approved.
  5. Once vouched, open a new PR (preferred) or reopen this one after a few minutes.

See CONTRIBUTING.md for details.

@github-actions github-actions Bot closed this Sep 25, 2026
@johntmyers johntmyers reopened this Sep 25, 2026
@johntmyers johntmyers added this to the OpenShell 0.1.1 milestone Sep 25, 2026
@johntmyers

Copy link
Copy Markdown
Collaborator

This is a good start but I'd like to see some security hardening and better testing:

  • Embedded IPv4 addresses in NAT64 prefixes need to conform to the existing SSRF protections we have for IPv4
  • We need e2e mediated runtime tests covering AAA resolution, synthetic IPv6 mapping, policy authorization, and pinned IPv6 TCP connections (can use a fake NetworkMediationSource to send UDP and TCP AAAA queries through PolicyDNSRuntime::start_mediated
  • Need tests to validate that auto mode route detection works for Docker, Podman, K8s, and VM supervisor network namespaces - both for IPv6 only and dual-stack cases
  • enabled / disabled do not appear to be plumbed through - everything uses auto exclusively
  • add OCSF logging that shows things like the requested mode, resolved boolean, detected v4/v6 route state and a some enum like ipv6_only, dual_stack or an error like route_table_unavailable
  • there should really be an initial issue that describes the problem and solution with proposed testing prior to a PR

@Joffref

Joffref commented Sep 25, 2026

Copy link
Copy Markdown
Author

I have read the DCO document and I hereby sign the DCO.

@Joffref

Joffref commented Sep 25, 2026

Copy link
Copy Markdown
Author

recheck

@Joffref Joffref changed the title feat(supervisor): make policy DNS IPv6 egress configurable feat(supervisor): IPv6 policy DNS egress and NAT64-aware SSRF checks Sep 25, 2026
Joffref added a commit to blaxel-ai/openshell-blaxel that referenced this pull request Sep 25, 2026
…rnel

Point the upstream IPv6 policy DNS references at NVIDIA/OpenShell#3702
and issue #3716 instead of the fork branch, and note that the PR also
fixes the NAT64 SSRF gap. Add a control sandbox section to KERNEL.md:
why it uses the tun + iptables variant, and how to check TUN and
ip6tables NAT since the guest exposes no kernel config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mediated policy DNS always answered AAAA queries with NOERROR/NODATA and
resolved A queries upstream as A only. On IPv6-only hosts behind
NAT64/DNS64 the trusted resolver returns no A records, so every
policy-allowed name failed with policy_dns_upstream_no_data and native
TCP egress was unusable.

Add a driver-owned supervisor flag, --policy-dns-ipv6-egress
{auto,enabled,disabled}. The default auto mode enables AAAA answers only
when the supervisor network namespace has an IPv6 default route and no
IPv4 default route, so dual-stack and IPv4-only hosts keep the current
A-record fallback. AAAA answers use the existing epoch-scoped synthetic
IPv6 pool and are pinned and dialed through the same resolved-endpoint
store and destination validation as IPv4 answers.

Signed-off-by: Joffref <mjoffre@blaxel.ai>
…ings

A DNS64 answer like 64:ff9b::a00:5 is really 10.0.0.5, but the SSRF
checks treated it as a public IPv6 address. Addresses inside a NAT64
prefix are now checked as the IPv4 address they embed, in policy DNS
and in the CONNECT path. The well-known prefix is always known; other
prefixes come from the new nat64_prefixes driver setting, or from
ipv4only.arpa discovery when the supervisor starts.

policy_dns_ipv6_egress and nat64_prefixes can now be set in the Docker,
Podman, Kubernetes and VM driver configs. Before this, only auto was
reachable in practice.

The supervisor logs its IPv6 egress decision (requested mode, result,
default routes, route state) and the NAT64 prefixes it uses as OCSF
config events.

Tests: route detection fixtures per backend, a start_mediated test with
a fake mediation source and DNS64 upstream, driver arg tests, and an
e2e check of the decision event.

Signed-off-by: Joffref <mjoffre@blaxel.ai>
…S runtime

The listener-based runtime still started with IPv6 answers off, so an
explicit mode only reached the mediated runtime. Resolve the mode once and
use it for both.

Signed-off-by: Joffref <mjoffre@blaxel.ai>
…swers

allowed_ips entries now match a NAT64 address through the IPv4 address
it embeds, so 10.0.0.0/8 covers 64:ff9b::a00:5 the same way it covers
10.0.0.5. Loopback, link-local and metadata stay blocked first.

The mediated IPv6 test now also checks that these are denied: the real
upstream IPv6, an unallocated synthetic address, the right address on
the wrong port, an open without a binary identity, and the old mapping
after a policy reload. A second test covers an unreachable trusted
resolver. The store gets the IPv6 version of the wrong port / stale
generation / expiry test.

Signed-off-by: Joffref <mjoffre@blaxel.ai>
@Joffref
Joffref force-pushed the feat/policy-dns-ipv6-egress branch from 94841e8 to f7f1f99 Compare September 28, 2026 00:24

@johntmyers johntmyers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

PR Review Status

Thanks @johntmyers; I checked the NAT64/IPv4 SSRF parity concern against the new registry and found one ordering defect that can select the wrong embedded IPv4 address when prefixes overlap. The linked maintainer-authored issue makes the cross-cutting change project-valid, and the Fern documentation covers the new operator settings.

Action required: @Joffref, make NAT64 classification choose the most-specific matching prefix and add the overlapping-prefix regression coverage described inline.

Blocking findings:

  • GATOR-f7f1f994-01: overlapping NAT64 prefixes are interpreted in registration order rather than by the route's most-specific prefix.

Carried findings:

  • None

Non-blocking suggestions:

  • GATOR-f7f1f994-02: extend E2E coverage beyond startup diagnostics to exercise sandbox AAAA resolution and the native IPv6 TCP relay path; this does not block the current revision.
Gator metadata
  • Validation: Project-valid through maintainer-authored issue #3716, which defines the IPv6 policy-DNS contract and acceptance criteria.
  • Docs: Fern configuration and network-policy documentation updated; navigation changes are not needed for edits to existing pages.
  • Checks: DCO and available current-head checks pass, but code review has a blocking finding and required branch gates are not yet complete.
  • E2E: test:e2e is required for the network-policy and sandbox-runtime behavior; dispatch is deferred until blocking review feedback is resolved.
  • Head SHA: f7f1f9943d1a932d738def0750f912ba69886104
  • Base SHA: c9da461a588f04b4ae1cc9f46aff98e30ca1bf44
  • Merge base SHA: c9da461a588f04b4ae1cc9f46aff98e30ca1bf44
  • Patch ID: acb665354181ae083ca011ac010062fdd27207f9
  • Gator payload: 9
  • Review mode: initial
  • Previous reviewed SHA: none
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:in-review

Comment thread crates/openshell-core/src/net/nat64.rs Outdated
@johntmyers johntmyers added the gator:in-review Gator is reviewing or awaiting PR review feedback label Sep 28, 2026
embedded_ipv4 returned the first registered prefix that contained the
address. With a configured 2001:db8::/32 and a later-discovered
2001:db8:8c52:7003::/96, 2001:db8:8c52:7003::7f00:1 was read through the
/32 as 140.82.112.3 and passed the SSRF checks, while the /96 route
translates it to 127.0.0.1.

Pick the longest matching prefix, the well-known prefix included, the
same way the route that carries the packet does. The tests register the
broad prefix first and check loopback, metadata and private destinations
under the nested one. They use RFC 9637 documentation space because the
registry is process-wide and a 2001:db8::/32 would change how other
tests classify their 2001:db8 addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Joffref <mjoffre@blaxel.ai>
@Joffref

Joffref commented Sep 28, 2026

Copy link
Copy Markdown
Author

Fixed in 1134d60: embedded_ipv4 now picks the longest matching prefix (well-known prefix included). The regression tests register the broad /32 first, then the nested /96, and check that loopback and metadata stay always-blocked and private addresses stay internal under the /96. I used RFC 9637 3fff:: space instead of 2001:db8::/32: the registry is process-wide, so a 2001:db8::/32 would change how other tests running in parallel classify their 2001:db8 addresses. The E2E suggestion (GATOR-02) is left for a follow-up.

@drew drew modified the milestones: OpenShell 0.1.1, OpenShell 0.1.5 Sep 28, 2026
@johntmyers johntmyers added the test:e2e Requires end-to-end coverage label Sep 28, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/3702 does not exist yet. A maintainer needs to comment /ok to test 1134d606020780fa11dd9dd14524a0b873d29f08 to mirror this PR. Once the mirror exists, re-apply the label or re-run Branch E2E Checks from the Actions tab.

@johntmyers

Copy link
Copy Markdown
Collaborator

/ok to test 1134d60

@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Re-check After Author Update

Thanks @Joffref. I re-evaluated latest head 1134d606020780fa11dd9dd14524a0b873d29f08 after your September 28 response about the overlapping-prefix fix and the use of RFC 9637 test space.

What I checked: the author-only delta now selects the longest matching NAT64 prefix, including the well-known prefix, and its regression coverage registers the broad prefix first before checking loopback, metadata, private, and allowed-network classification through the nested prefix. The independent follow-up review found no new blocking issues, and the prior Gator thread is resolved.

Disposition: resolved.

Remaining items:

  • No blocking review items remain. test:e2e is applied, the mirror is current, and Branch Checks, Helm Lint, and Branch E2E Checks are queued or running.
Gator metadata
  • Validation: Project-valid through maintainer-authored issue feat(network): support policy DNS IPv6 egress across the sandbox–supervisor boundary #3716.
  • Docs: Fern configuration and network-policy documentation updated; navigation changes are not needed for edits to existing pages.
  • Checks: Current-head required workflows are queued or running.
  • E2E: test:e2e applied; /ok to test created the current-head mirror and Branch E2E Checks is running.
  • Head SHA: 1134d606020780fa11dd9dd14524a0b873d29f08
  • Base SHA: c9da461a588f04b4ae1cc9f46aff98e30ca1bf44
  • Merge base SHA: c9da461a588f04b4ae1cc9f46aff98e30ca1bf44
  • Patch ID: c9ccec4eafa80e5af210fa11cb15cbc5a8300a58
  • Gator payload: 9
  • Review mode: follow_up
  • Previous reviewed SHA: f7f1f9943d1a932d738def0750f912ba69886104
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:watch-pipeline

@johntmyers johntmyers added gator:watch-pipeline Gator is monitoring PR CI/CD status gator:in-review Gator is reviewing or awaiting PR review feedback and removed gator:in-review Gator is reviewing or awaiting PR review feedback gator:watch-pipeline Gator is monitoring PR CI/CD status labels Sep 28, 2026
…test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Joffref <mjoffre@blaxel.ai>
@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

PR Review Status

The follow-up review found no blocking issues in the current-head delta: the test-only duration rewrite is behaviorally equivalent, and the previously resolved NAT64 prefix finding remains resolved.

Blocking findings:

  • No blocking findings remain.

Carried findings:

  • None.
Gator metadata
  • Validation: Project-valid through maintainer-authored issue feat(network): support policy DNS IPv6 egress across the sandbox–supervisor boundary #3716.
  • Docs: Fern configuration and network-policy documentation were updated previously; this test-only delta needs no additional documentation.
  • Checks: Current-head required workflows require trusted dispatch before pipeline monitoring can resume.
  • E2E: test:e2e is required and present; current-head mirror authorization and workflow dispatch are pending.
  • Head SHA: 9ab8076a9c0cbf3db4f877b7693040ff68007571
  • Base SHA: c9da461a588f04b4ae1cc9f46aff98e30ca1bf44
  • Merge base SHA: c9da461a588f04b4ae1cc9f46aff98e30ca1bf44
  • Patch ID: bd27ddb73445e1d003cd37f1c6f137fb29bda392
  • Gator payload: 9
  • Review mode: follow_up
  • Previous reviewed SHA: 1134d606020780fa11dd9dd14524a0b873d29f08
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:in-review

@johntmyers

Copy link
Copy Markdown
Collaborator

/ok to test 9ab8076

@johntmyers johntmyers added gator:watch-pipeline Gator is monitoring PR CI/CD status gator:approval-needed Gator completed review; maintainer approval needed and removed gator:in-review Gator is reviewing or awaiting PR review feedback gator:watch-pipeline Gator is monitoring PR CI/CD status labels Sep 28, 2026

This branch has not been deployed

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

Labels

gator:approval-needed Gator completed review; maintainer approval needed test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(network): support policy DNS IPv6 egress across the sandbox–supervisor boundary

3 participants