Skip to content

feat(relay): support NATed outbound-only transit workers #358

Description

@DPS0340

Problem

Portal relays can already join the public mesh when they have a publicly reachable HTTPS origin and WireGuard UDP endpoint. With DISCOVERY=true, a relay builds a signed RelayDescriptor, announces it to bootstrap relays, synchronizes discovery state, and can act as a multi-hop entry, middle, or exit.

That model excludes volunteer capacity behind NAT or CGNAT. A home-lab node may have reliable outbound Internet access and spare bandwidth, but no domain, public IP, port forwarding, or reachable 51820/udp endpoint. It therefore cannot publish the APIHTTPSAddr and WireGuard endpoint currently required for a relay hop.

Add an outbound-only, transit-only worker mode that lets such a node contribute encrypted stream forwarding without becoming a public ingress relay or exit endpoint.

Today

entry relay ── WireGuard/yamux ── public middle relay ── exit relay
                                  ^ public HTTPS + UDP required

Proposed

entry relay ── public coordinator ── established outbound session ── NATed transit worker
                                                                    │
                                                                    └─ outbound path to next public hop

The worker must never accept arbitrary Internet connections. It should carry only authenticated Portal hop streams over connections it initiated.

Current constraints in the code

The existing contracts assume that every discoverable hop is directly reachable:

  • types/identity.go: RelayDescriptor requires APIHTTPSAddr; overlay-capable relays advertise a WireGuard public key and port.
  • portal/identity/store.go: descriptor validation requires WireGuard metadata when SupportsOverlay is set.
  • portal/discovery/refresher.go: discovery refresh and self-announce contact relay HTTPS endpoints directly.
  • portal/overlay/: OpenHopStream reaches the next hop through its derived WireGuard overlay address.
  • portal/api_server.go: /sdk/hop installs a route on the publicly reachable receiving relay and requires the next relay's signed overlay descriptor.
  • portal/server.go: bridgeLeaseConn forwards a hop stream through the direct overlay runtime.
  • sdk/listener.go: route setup treats each hop as a relay URL and sends control requests to those public endpoints.

A NATed worker has none of those inbound addresses, so representing it as an ordinary RelayDescriptor would advertise an endpoint that peers cannot dial.

Proposed model

Introduce a distinct transit-worker role rather than weakening the public relay contract.

1. Outbound worker session

Add a command or mode such as:

portal transit \
  --coordinator https://relay.example.com \
  --identity-path transit-identity.json \
  --max-connections 128 \
  --max-bps 10000000

The worker should:

  1. create or load its own signing identity;
  2. connect outbound to one or more public coordinator relays over TLS on a commonly reachable port;
  3. authenticate using a signed challenge;
  4. advertise only transit capabilities and operator limits;
  5. maintain a multiplexed, bidirectional session with keepalive and reconnect;
  6. accept coordinator-opened Portal hop streams over that established session;
  7. open only protocol-authorized outbound connections for the next hop.

The existing /sdk/connect reverse-session pattern may be reusable conceptually, but transit needs a long-lived multiplexed session that allows the coordinator to open many independently authenticated hop streams.

2. Separate reachability from relay identity

Do not give the worker a fake APIHTTPSAddr or WireGuard endpoint. Add a separate signed contract, for example TransitDescriptor or an explicit reachability variant, containing fields such as:

  • worker identity/address;
  • protocol version and expiry;
  • coordinator relay identity/URL;
  • supported role (middle only for the first version);
  • transport capabilities;
  • operator connection/bandwidth limits;
  • current coarse capacity;
  • signature.

A signature proves identity continuity, not trust or Sybil resistance. Coordinators must retain local admission, allow/deny, quota, and revocation policy.

An architectural decision is needed on how this capacity is exposed:

  • advertise individually brokered transit workers through discovery; or
  • let the public coordinator advertise aggregate transit capacity and select a worker internally.

Either approach must make the coordinator/gateway explicit so route planning never tries to dial the worker directly.

3. Brokered route installation

Current /sdk/hop setup assumes that the control request can be sent directly to every hop. Add coordinator-mediated route setup for outbound workers:

route owner
   -> public coordinator
      -> authenticated worker session
         -> install short-lived hop token and next-hop constraint

Each installed route should be:

  • signed by the route owner under the existing hop-route trust model;
  • bound to one worker identity, one next hop, and one short expiry;
  • replay-resistant and idempotently removable;
  • unusable as a general TCP proxy;
  • removed when the worker session disappears or the lease expires.

4. Data-plane boundary

Generalize the real hop-stream boundary so bridgeLeaseConn can open a stream through either:

  • the existing direct WireGuard overlay; or
  • a coordinator-owned outbound worker session.

This should be a narrow transport boundary around authenticated hop streams, not a generic networking abstraction. The direct public-relay path should remain unchanged.

5. Selection and lifecycle

Discovery and route selection must account for reachability and transient availability:

  • never choose a disconnected or draining worker;
  • expire worker advertisements quickly after session loss;
  • avoid using coordinator-to-worker keepalive RTT as public ingress RTT;
  • apply connection, byte-rate, queue, and circuit-duration limits;
  • support graceful drain so existing circuits finish while new ones stop;
  • expose enough status and metrics for both the worker and coordinator operator.

Security and privacy requirements

  • Transit only: the first version must not allocate a public hostname or raw TCP/UDP port and must not act as the final application exit.
  • No open proxy: outbound dialing must be restricted to the next Portal hop authorized by a valid route, not an arbitrary host supplied by stream data.
  • Tenant confidentiality: preserve the current tenant TLS passthrough model; the worker handles ciphertext, not tenant TLS keys or HTTP plaintext.
  • Mutual authentication: bind the worker session and every circuit to signed Portal identities and short-lived tokens.
  • Resource isolation: enforce operator-configured connection, bandwidth, queue, and idle-time limits before admitting work.
  • Abuse response: coordinators need worker allow/deny, disconnect, quarantine, and revocation controls.
  • Honest metadata: document that the coordinator sees the worker IP and timing/volume metadata, and that the worker sees its immediate Portal neighbors.
  • No Sybil claim: worker signatures provide continuity only; reputation or economic admission is outside the initial implementation.

Suggested implementation areas

  • cmd/portal-tunnel/: add the transit-worker CLI/configuration and lifecycle.
  • types/: add stable worker registration, capability, session, and brokered-route contracts.
  • portal/auth/: sign and verify worker registration/session challenges and brokered route messages.
  • portal/api_server.go: add coordinator endpoints for worker registration/session establishment and route control.
  • portal/server.go: own connected worker sessions and dispatch/bridge authenticated hop streams.
  • portal/overlay/: expose the minimal hop-stream transport boundary needed by both direct overlay and outbound sessions.
  • portal/discovery/ and route planning: represent brokered reachability and exclude disconnected workers.
  • sdk/listener.go: create broker-aware route setup/cleanup without treating workers as directly reachable relay URLs.
  • cmd/relay-server/config.go, .env.example, and deployment docs: add coordinator admission, quotas, and observability settings.
  • tests around authentication, replay, reconnect, expiry, selection, backpressure, and fallback to direct relays.

MVP scope

For a first implementation:

  • stream multi-hop only;
  • middle-hop role only;
  • one public coordinator per worker;
  • outbound TLS session initiated by the worker;
  • no inbound worker ports, domain, DNS, or public certificate;
  • explicit coordinator opt-in and conservative admission limits;
  • direct public relay mesh remains the default and fallback;
  • no payment or reputation mechanism.

Acceptance criteria

  • A worker behind NAT/CGNAT, with no inbound port forwarding, can register through an outbound connection to an opted-in public coordinator.
  • A stream multi-hop route can select that worker as a middle transit hop and pass end-to-end traffic through it.
  • The worker cannot be selected as public ingress or application exit.
  • Disconnecting the worker stops new route selection, expires its advertised availability, and cleans up its routes without affecting direct relay paths.
  • Invalid, expired, replayed, or wrong-worker route messages are rejected.
  • Operator connection/bandwidth limits are enforced under load and the worker can enter graceful drain mode.
  • Existing public relay discovery, direct WireGuard multi-hop, and single-relay exposure continue to work unchanged.
  • Documentation includes the trust/metadata model and a minimal NATed-worker deployment example.

Non-goals

  • Replacing the public relay data plane.
  • Turning Portal into a general-purpose proxy or VPN.
  • Raw TCP/UDP transit in the first version.
  • Public ingress, DNS, ACME, or lease hosting on the NATed worker.
  • Solving relay reputation, incentives, payments, or Sybil resistance.
  • Hiding the worker's source IP from its coordinator.

Relationship to existing issues

  • feat(discovery): add IVNP-backed overlay discovery #324 adds an optional IVNP/I2P relay discovery transport and explicitly does not replace the tenant data plane or multi-hop transport. It may later provide another way to locate peers, but it does not create the reverse transit session or brokered hop path required here.
  • feat(exposure): support IVNP/I2P as an endpoint transport #350 proposed IVNP/I2P as an application endpoint exposure transport. This issue is not application exposure and does not create an I2P service endpoint.
  • Existing public relay discovery and multi-hop already cover nodes with publicly reachable HTTPS and WireGuard endpoints. This issue is specifically for transit capacity that has outbound connectivity only.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions