Skip to content

bug(network): v4-mapped loopback flows (AF_INET connect to dual-stack listener) get no OCSF DENY event — silent RST or hold-then-timeout #3735

Open

Description

@dlynch90

Summary

OpenShell emits an excellent explicit denial surface for policy-denied egress — NET:REFUSE [MED] DENIED host [reason:policy_dns_ineligible] + NET:DENIED <binary> -> host:port [reason:transparent_tcp_policy_denied] OCSF events, with the client receiving a clean EACCES. However, one flow class falls entirely outside that coverage:

An AF_INET connection to a dual-stack ([::], IPV6_V6ONLY=0) loopback listener — i.e. the default bind of many runtimes (python http.server, node, etc.) — is reset or silently held with no OCSF event and no deny reason.

This flow class is on the critical path of every external access mechanism, because service expose, forward start (ssh relay), and forward service (gRPC) all connect to the target via native 127.0.0.1.

Environment

  • OpenShell 0.1.1 (CLI + gateway, Homebrew tap nvidia/openshell)
  • macOS 15 (Apple Silicon), local Docker compute driver
  • Sandbox image: nvcr.io/nvidia/base/ubuntu:24.04 and python:3.13-alpine

Reproduction

  1. Create a workload whose server binds dual-stack (default):
    openshell sandbox create --name web --from python:3.13-alpine -- python3 -m http.server 8080
    
    /proc/net/tcp6 shows [::]:8080 LISTEN; /proc/net/tcp has no v4 listener.
  2. In-sandbox client to http://127.0.0.1:8080/ → ECONNRESET. openshell logs shows NET:OPEN/NET:CLOSE only — no DENIED event.
  3. openshell service expose web 8080 → the printed URL hangs (relay bridges, TCP opens, zero payload, timeout). Same signature via forward start and forward service.
  4. Contrast — policy-denied egress from the same sandbox:
    [OCSF] NET:REFUSE [MED] DENIED example.com [reason:policy_dns_ineligible]
    [OCSF] NET:OPEN [MED] DENIED /usr/local/bin/python3.13(0) -> example.com:80 [reason:transparent_tcp_policy_denied]
    
    Client gets EACCES — explicit and diagnosable.

Workaround (verified)

Bind the workload natively AF_INET: python3 -m http.server 8080 --bind 0.0.0.0 → in-sandbox 127.0.0.1, the service expose URL, and forwards all return 200.

Expected

Denied v4-mapped flows should behave like other denied flow classes: emit the same OCSF DENIED event with a reason code (e.g. reason:v4_mapped_flow_denied) and close with an explicit error, instead of a silent RST/hold. Alternatively, if v4-mapped flows are intended to work (docs say only loopback/link-local/unspecified destinations are always blocked, yet a native --bind 127.0.0.1 listener on 127.0.0.1:8081 is reachable — so the block is flow-family-specific, not destination-specific), the docs and enforcement should be aligned.

Impact

Any workload using a runtime default dual-stack bind is unreachable via all three external access paths, with an error signature that looks like a routing/relay failure rather than a policy denial — costly to diagnose.

Activity

  1. kvnloo commented on Sep 27, 2026

    @kvnloo

    Before choosing "allow" versus "deny", I think this needs one normalization invariant:

    an IPv4 client connecting to a dual-stack listener through a v4-mapped IPv6 address should reach the same policy decision and audit path as the semantically equivalent native IPv4 flow.

    Right now the address-family representation appears to change which enforcement/audit path is exercised.

    A useful matrix would be:

    • 127.0.0.1 -> native IPv4 listener;
    • 127.0.0.1 -> [::] dual-stack listener;
    • ::1 -> IPv6 listener;
    • v4-mapped ::ffff:127.0.0.1.

    For each one, assert:

    • allow/deny result;
    • reason code;
    • OCSF event presence;
    • normalized destination identity.

    If loopback is intentionally denied, every representation should fail explicitly and emit the same class of denial evidence.

    If this particular loopback path is intended to support service forwarding, normalization should happen before policy classification so the wire representation cannot silently change the decision.

  2. github-actions commented on Oct 15, 2026

    @github-actions

    This issue has had no activity for 14 days and is now marked stale. It may be closed in 14 days if there is no further activity. Comment or remove the state:stale label to keep it open.

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

    state:staleInactive item at risk of automatic closure.state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions