chore: update architecture and readme

Kim committed Mar 19, 2026 at 15:46 UTC f3df91bd63a7f4278c914b92a2deb3e33f3ef4f6
2 files changed +53 -15
README.md
+1
@@ -25,6 +25,7 @@ Unlike other tunneling services, Portal is self-hosted and permissionless. You c
25 - **End-to-end encryption**: Supports TLS passthrough with relay keyless certificates
26 - **Permissionless Hosting**: Anyone can run their own Portal — no approval needed
27 - **One-Command Setup**: Expose any local app with a single command
28 +- **UDP Relay (Experimental)**: Supports raw UDP relay use cases, but the transport model and operational behavior may still change
29
30 ## Components
31
docs/architecture.md
+52 -15
@@ -2,17 +2,23 @@
2
3 ## Overview
4
5 -Portal publishes local services on public subdomains through a relay.
6 -Backends connect outward to the relay, the relay routes inbound client traffic by SNI, and tenant TLS remains end-to-end between the browser and the SDK/tunnel endpoint.
5 +Portal publishes local services on public subdomains and optional UDP ports through a relay.
6 +Backends connect outward to the relay. Stream traffic is routed by SNI, and tenant TLS remains end-to-end between the client and the SDK/tunnel endpoint for the stream path.
7
8 High-level path:
9
10 ```text
11 -Client (Browser)
11 +Stream client
12 -> Relay SNI listener (:443 by default)
13 -> Claimed reverse session
14 -> SDK / portal-tunnel
15 -> Local service
16 +
17 +UDP client
18 + -> Relay lease UDP port (29900-29999 by default)
19 + -> Internal QUIC tunnel
20 + -> SDK / portal-tunnel
21 + -> Local UDP service
22 ```
23
24 ## Architecture Invariants
@@ -23,7 +29,7 @@ Client (Browser)
29 - Do not introduce websocket or legacy compatibility paths unless a new ADR supersedes ADR-0002.
30 - Derive lease hostnames from the full normalized `PORTAL_URL` host, not from apex extraction.
31 - Preserve explicit root-host fallback through SNI no-route handling to the admin/API listener.
26 -- All leases are TLS-only. Registration does not negotiate or preserve a non-TLS mode.
32 +- Stream ingress is TLS-only. UDP exposure, when enabled, is raw UDP.
33
34 ### TLS and Identity
35
@@ -61,7 +67,7 @@ Client (Browser)
67
68 ## Connection Model
69
64 -Portal has two distinct network roles:
70 +Portal has three distinct network roles:
71
72 - **Control-plane HTTP requests**
73 - `POST /sdk/register`
@@ -72,9 +78,13 @@ Portal has two distinct network roles:
78 - `GET /sdk/connect?lease_id=...`
79 - HTTP/1.1 only
80 - hijacked into a long-lived raw TCP session
75 - - starts idle in the relay broker, then becomes the tenant data path when claimed
81 + - starts idle in the per-lease stream ready queue, then becomes the tenant data path when claimed
82 +- **Internal datagram tunnel**
83 + - QUIC on `API_PORT/udp`
84 + - authenticated by a QUIC control stream
85 + - carries relay-to-SDK/tunnel datagram traffic only
86
77 -That distinction matters because `/sdk/connect` stops being ordinary HTTP once hijacked.
87 +That distinction matters because `/sdk/connect` stops being ordinary HTTP once hijacked, while the UDP backhaul is a separate internal QUIC carrier.
88
89 ## Core Components
90
@@ -91,8 +101,8 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
101
102 - `Server`: owns listeners, lease registry, API handlers, and shutdown lifecycle
103 - `routeTable`: exact + single-label wildcard hostname lookup
94 -- `leaseBroker`: per-lease ready queue for reverse sessions
95 -- `reverseSession`: idle keepalive + activation state machine for one reverse TCP connection
104 +- `transport.RelayStream`: per-lease ready queue for reverse stream sessions
105 +- `transport.RelayDatagram`: per-lease raw UDP port and datagram backhaul runtime
106 - `acme`: Cloudflare/Route53-backed root/wildcard A-record sync + certificate provisioning/renewal for the relay root host and wildcard
107 - `keyless`: admin/API TLS attach helpers and tenant-side signer integration
108
@@ -101,7 +111,7 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
111 - `WithDefaultRelayURLs`: fetches the default Portal relay list from the repository-root `registry.json`, appends explicit relay inputs, and normalizes the combined list
112 - Entry points can opt out of registry defaults and call `utils.NormalizeRelayURLs` directly when they need explicit relay inputs only
113 - `Listener`: validates one relay URL locally, then starts relay compatibility checks, lease registration, reverse session maintenance, and lease renewal in the background until ready
104 -- `relayclient.go`: internal relay transport helper for control-plane requests and reverse session dialing
114 +- `api_client.go`: internal relay client for control-plane requests, reverse session dialing, and internal QUIC tunnel setup
115 - `ListenerConfig.RetryCount <= 0` means retry forever; positive values close the listener after the retry budget is exhausted
116 - Default app flow is `WithDefaultRelayURLs -> NewListener -> PublicURL -> http.Server.Serve(listener)` or `WithDefaultRelayURLs -> Expose -> PublicURLs -> http.Server.Serve(exposure)`, with an opt-out path for explicit relay inputs only
117 - `expose.go`: optional `RunHTTP` helper for serving one handler on both a local HTTP port and the relay listener
@@ -116,6 +126,7 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
126 - Creates one SDK listener per relay through the SDK and consumes one aggregate listener
127 - Accepts claimed tenant connections from the relay
128 - Proxies raw TCP to a local target passed to `portal expose`
129 +- Optionally proxies raw UDP to a separate local UDP target passed with `--udp-addr`
130 - Returns an HTTP 503 response when the local target is unavailable
131
132 ## Transport Model
@@ -124,15 +135,40 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
135
136 1. SDK/tunnel registers one lease per relay with `POST /sdk/register`.
137 2. SDK opens one or more reverse sessions per registered lease with `GET /sdk/connect?lease_id=...`.
127 -3. Each relay hijacks `/sdk/connect` requests and places the connection in the per-lease broker ready queue.
138 +3. Each relay hijacks `/sdk/connect` requests and places the connection in the per-lease stream ready queue.
139 4. While idle, the relay writes `0x00` keepalive markers.
129 -5. A browser connects to the relay SNI listener.
130 -6. Relay extracts SNI from ClientHello, resolves a lease, and waits up to `ClaimTimeout` for one reverse session from that lease broker.
140 +5. A stream client connects to the relay SNI listener.
141 +6. Relay extracts SNI from ClientHello, resolves a lease, and waits up to `ClaimTimeout` for one reverse session from that lease stream queue.
142 7. Relay writes `0x02` to activate the claimed session.
143 8. SDK/tunnel receives `0x02`, starts tenant TLS locally using the relay-backed keyless signer, and the relay bridges raw encrypted bytes end-to-end.
144
145 Result: the relay decides routing, but tenant TLS termination still happens at the SDK/tunnel side.
146
147 +### UDP datagram transport
148 +
149 +1. SDK/tunnel registers a lease with `udp_enabled=true`.
150 +2. Relay validates that the datagram plane is enabled and allocates one public UDP port for that lease from the configured `UDP_PORT_MIN` to `UDP_PORT_MAX` range.
151 +3. SDK/tunnel opens an internal QUIC tunnel to the relay on `API_PORT/udp` and authenticates it with a QUIC control stream.
152 +4. A UDP client sends raw UDP packets to the lease `UDPAddr`.
153 +5. Relay receives those packets on the lease UDP port, maps client traffic to a flow ID, and forwards payloads over QUIC DATAGRAM frames.
154 +6. SDK/tunnel receives the datagrams and either:
155 + - delivers them to application code through `sdk.Exposure.AcceptDatagram()`, or
156 + - proxies them to a local UDP service in `portal-tunnel`.
157 +7. Replies return over the same QUIC tunnel and are written back to the original client through the lease UDP port.
158 +
159 +### UDP Characteristics and Constraints
160 +
161 +- UDP support is currently experimental. Expect transport details and operational behavior to keep changing while the model is being validated.
162 +- Public UDP ingress is raw UDP only. Portal does not currently provide public QUIC or HTTP/3 ingress.
163 +- The QUIC tunnel is internal only. It is a relay-to-SDK/tunnel backhaul, not a public client entry point.
164 +- Public UDP and internal QUIC serve different roles:
165 + - public UDP keeps the external interface compatible with existing raw UDP clients
166 + - internal QUIC gives the backhaul one outbound, multiplexed, TLS-protected carrier
167 +- Each UDP-enabled lease gets one dedicated public UDP port. The configured UDP port range therefore limits the number of concurrent UDP-enabled leases, not the number of clients behind one lease.
168 +- The public UDP leg is not relay-terminated TLS. Only the internal QUIC backhaul is TLS-protected by default.
169 +- UDP is intended for native UDP clients and services such as game servers, DNS-like protocols, and custom UDP daemons. It is not a browser-native public interface.
170 +- In the current public contract, stream/TCP remains available and UDP is additive when enabled.
171 +
172 ## Control Plane Flow
173
174 ### 1. Register
@@ -144,7 +180,7 @@ Result: the relay decides routing, but tenant TLS termination still happens at t
180 - `reverse_token`
181 - optional `metadata`
182 - optional `ttl`
147 -- No non-TLS mode is accepted or negotiated
183 +- optional `udp_enabled` (experimental)
184 - `name` must be a valid single DNS label and relay publishes the lease at `<name>.<root host>`
185 - Registration reserves the hostname and publishes the route immediately; if no reverse session is ready yet, inbound SNI claims wait up to `ClaimTimeout`
186 - `PORTAL_URL` is normalized to its host component only; path/query segments are ignored for routing
@@ -237,10 +273,11 @@ Relay-local frontend asset filenames stay in `cmd/relay-server`, not `types/`.
273
274 - Reverse-only backend connectivity
275 - One canonical raw TCP reverse transport
276 +- Raw public UDP exposure with an internal QUIC datagram backhaul
277 - SNI-based routing with root-host fallback
278 - End-to-end tenant TLS with relay-backed keyless signing
279 - Per-lease reverse token authorization for reverse session lifecycle
243 -- Lease-local reverse session ownership through `leaseBroker`
280 +- Lease-local stream and datagram ownership through per-lease transport runtimes
281
282 ## ADRs
283