You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
connect outbound to one or more public coordinator relays over TLS on a commonly reachable port;
authenticate using a signed challenge;
advertise only transit capabilities and operator limits;
maintain a multiplexed, bidirectional session with keepalive and reconnect;
accept coordinator-opened Portal hop streams over that established session;
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.
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.
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 signedRelayDescriptor, 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/udpendpoint. It therefore cannot publish theAPIHTTPSAddrand 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.
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:RelayDescriptorrequiresAPIHTTPSAddr; overlay-capable relays advertise a WireGuard public key and port.portal/identity/store.go: descriptor validation requires WireGuard metadata whenSupportsOverlayis set.portal/discovery/refresher.go: discovery refresh and self-announce contact relay HTTPS endpoints directly.portal/overlay/:OpenHopStreamreaches the next hop through its derived WireGuard overlay address.portal/api_server.go:/sdk/hopinstalls a route on the publicly reachable receiving relay and requires the next relay's signed overlay descriptor.portal/server.go:bridgeLeaseConnforwards 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
RelayDescriptorwould 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:
The worker should:
The existing
/sdk/connectreverse-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
APIHTTPSAddror WireGuard endpoint. Add a separate signed contract, for exampleTransitDescriptoror an explicit reachability variant, containing fields such as:middleonly for the first version);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:
Either approach must make the coordinator/gateway explicit so route planning never tries to dial the worker directly.
3. Brokered route installation
Current
/sdk/hopsetup assumes that the control request can be sent directly to every hop. Add coordinator-mediated route setup for outbound workers:Each installed route should be:
4. Data-plane boundary
Generalize the real hop-stream boundary so
bridgeLeaseConncan open a stream through either: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:
Security and privacy requirements
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.MVP scope
For a first implementation:
Acceptance criteria
Non-goals
Relationship to existing issues