docs: add TCP port routing to README, architecture, and CLI docs

- Add TCP port routing feature and comparison table row to README (en/zh) - Add TCP port transport section with flow diagram to architecture.md - Add --tcp flag, usage example, and relay requirements to CLI README - Update control plane fields, error codes, and design properties Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Yechan Kim committed Apr 3, 2026 at 02:39 UTC 9a55d919db1624e850ed83989a0ee60173b42028
4 files changed +52 -4
README.md
+2
@@ -15,6 +15,7 @@
15 - **Relay discovery and pools**: Use discovered relays as a pool, with multi-relay access and failover
16 - **No login, no API keys**: Authenticate ownership using SIWE, with ENS-based identity support
17 - **Raw TCP and UDP transport**: Native TCP reverse sessions with optional UDP (no SSH or WebSocket)
18 +- **TCP port routing**: Dedicated TCP ports for non-TLS services (e.g., Minecraft, game servers) without SNI-based routing
19
20 ## Comparison
21
@@ -27,6 +28,7 @@
28 | Multi-relay failover | **Yes** | Managed | Built-in multi-DC | No |
29 | Custom domain | **Yes** | Paid plans | Yes | Yes |
30 | Transport | Raw TCP / UDP | HTTP/S, TCP, TLS | HTTP/S, TCP, UDP | HTTP/S, TCP, UDP |
31 +| Non-TLS TCP port routing | **Yes** | Paid plans | No | Yes |
32 | Open source | **MIT** | No | Client only (Apache 2.0) | Apache 2.0 |
33 | Account required | **No** (SIWE) | Yes | Yes | No |
34
README.zh-CN.md
+2
@@ -15,6 +15,7 @@
15 - **中继发现与中继池**:将发现到的中继作为中继池使用,支持多中继访问与故障切换
16 - **无需登录、无需 API Key**:使用 SIWE 验证所有权,并支持基于 ENS 的身份
17 - **原生 TCP 与 UDP 传输**:原生 TCP 反向会话,并可选支持 UDP(不依赖 SSH 或 WebSocket)
18 +- **TCP 端口路由**:为非 TLS 服务(如 Minecraft、游戏服务器)分配专用 TCP 端口,无需基于 SNI 的路由
19
20 ## 对比
21
@@ -27,6 +28,7 @@
28 | 多中继故障切换 | **是** | 托管 | 内置多数据中心 | 否 |
29 | 自定义域名 | **是** | 付费方案 | 是 | 是 |
30 | 传输协议 | 原生 TCP / UDP | HTTP/S, TCP, TLS | HTTP/S, TCP, UDP | HTTP/S, TCP, UDP |
31 +| 非 TLS TCP 端口路由 | **是** | 付费方案 | 否 | 是 |
32 | 开源 | **MIT** | 否 | 仅客户端(Apache 2.0) | Apache 2.0 |
33 | 需要账号 | **否**(SIWE) | 是 | 是 | 否 |
34
cmd/portal-tunnel/README.md
+9
@@ -47,6 +47,12 @@ portal expose localhost:8080 \
47 --owner "Portal Operator"
48 ```
49
50 +TCP port routing example (e.g., Minecraft server):
51 +
52 +```text
53 +portal expose localhost:25565 --name minecraft --tcp
54 +```
55 +
56 Multi-port HTTP aggregation example:
57
58 ```text
@@ -68,6 +74,7 @@ portal expose --name myapp \
74 - `--relays` adds explicit relay API URLs for that run.
75 - `--discovery=false` disables the public registry seed list and the runtime relay discovery expansion loop for that run. With `--discovery=false`, only the explicit `--relays` values are used.
76 - `--ban-mitm` enables strict rejection when the TLS self-probe detects termination in the path.
77 +- `--tcp` requests a dedicated TCP port on the relay for raw TCP services that do not use TLS (e.g., Minecraft, game servers).
78
79 Flags:
80
@@ -82,6 +89,7 @@ Flags:
89 --thumbnail Service thumbnail URL metadata
90 --owner Service owner metadata
91 --hide Hide service from relay listing screens
92 +--tcp Request a dedicated TCP port on the relay for raw TCP services (no TLS)
93 --http-route HTTP route mapping in PATH=UPSTREAM form; repeat for multiple routes
94 ```
95
@@ -119,4 +127,5 @@ Legacy execution compatibility has been removed:
127 - Tenant TLS is provisioned automatically through the relay keyless signer. The SDK fetches the relay certificate chain and uses `/v1/sign` for remote signing.
128 - `portal expose` enables MITM strict enforcement by default. Use `--ban-mitm=false` to keep warning-only behavior when the TLS self-probe suspects relay termination.
129 - When the local service is unreachable, the tunnel returns an HTTP 503 page.
130 +- `--tcp` allocates a dedicated TCP port (starting from 40000) on the relay. The relay bridges raw TCP connections to the local target without TLS. Requires `TCP_PORT_COUNT > 0` on the relay and TCP port enabled in the admin panel.
131 - `--http-route` mode is HTTP-only and cannot be combined with `--udp`.
docs/architecture.md
+39 -4
@@ -2,7 +2,7 @@
2
3 ## Overview
4
5 -Portal publishes local services on public subdomains and optional UDP ports through a relay.
5 +Portal publishes local services on public subdomains, optional dedicated TCP ports, 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 or tunnel endpoint for the stream path.
7
8 High-level path:
@@ -14,6 +14,12 @@ Stream client
14 -> SDK / portal-tunnel
15 -> Local service
16
17 +TCP port client
18 + -> Relay lease TCP port (40000+ by default)
19 + -> Claimed reverse session (raw TCP, no TLS)
20 + -> SDK / portal-tunnel
21 + -> Local service
22 +
23 UDP client
24 -> Relay lease UDP port (50000+ by default)
25 -> Internal QUIC tunnel
@@ -50,6 +56,7 @@ UDP client
56 - Reverse TCP marker bytes remain protocol state:
57 - `0x00` = idle keepalive
58 - `0x02` = TLS passthrough activation
59 + - `0x03` = raw TCP activation (non-TLS port routing)
60 - `/sdk/connect` remains HTTP/1.1 only.
61
62 ### JSON and Shared Contract
@@ -109,8 +116,9 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
116 - `Server`: owns listeners, lease registry, API handlers, discovery, and shutdown lifecycle
117 - `routeTable`: exact + single-label wildcard hostname lookup
118 - `transport.RelayStream`: per-lease ready queue for reverse stream sessions
119 +- `transport.RelayTCPPort`: per-lease TCP listener on an allocated port; bridges incoming connections to reverse sessions using raw TCP (no TLS)
120 - `transport.RelayDatagram`: per-lease raw UDP socket plus QUIC DATAGRAM bridge runtime
113 -- `transport.PortAllocator`: count-based UDP port allocator with sticky name-based reservation and grace period
121 +- `transport.PortAllocator`: count-based port allocator with sticky name-based reservation and grace period (shared by UDP and TCP port transport)
122 - `transport.datagramSession`: internal QUIC DATAGRAM bind/send/receive primitive shared by relay and SDK datagram runtimes
123 - `acme`: Cloudflare/Google Cloud DNS/Route53-backed root/wildcard A-record sync + certificate provisioning/renewal for the relay root host and wildcard
124 - `keyless`: admin/API TLS attach helpers and tenant-side signer integration
@@ -150,8 +158,10 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
158 - Creates one SDK listener per relay through the SDK and consumes one aggregate listener
159 - Accepts claimed tenant connections from the relay
160 - Proxies raw TCP to a local target passed to `portal expose`
161 +- Optionally requests a dedicated TCP port on the relay for raw TCP services when `--tcp` is enabled
162 - Optionally proxies raw UDP to a separate local UDP target when `--udp` is enabled
163 - Returns an HTTP 503 response when the local target is unavailable
164 +- `--tcp` flag (bool, default `false`): requests a dedicated TCP port on the relay for non-TLS services (e.g., Minecraft, game servers)
165 - `--udp` flag (bool, default `false`): enables UDP relay in addition to TCP
166 - `--udp-addr` flag (string): local UDP target address (`host:port` or port only); required when `--udp` is enabled
167 - `--ban-mitm` flag (bool, default `false`): when enabled, TLS self-probe mismatches ban the relay for the current exposure instead of only logging
@@ -186,6 +196,24 @@ Result: the relay decides routing, but tenant TLS termination still happens at t
196
197 Result: this is a detect-only signal by default. It raises the cost of adaptive relay-side TLS termination, but it does not prove passthrough for every user connection. Callers that need stricter behavior can opt into relay banning.
198
199 +### TCP Port Transport (non-TLS)
200 +
201 +1. SDK/tunnel requests a register challenge with `tcp_enabled=true`, signs the returned SIWE message, and then completes `POST /sdk/register`.
202 +2. Relay validates that the TCP port plane is enabled (server has `TCP_PORT_COUNT > 0` and admin has enabled TCP port), allocates a TCP port via `PortAllocator`, and creates a `transport.RelayTCPPort` for the lease.
203 +3. Registration response includes `tcp_addr` (public TCP endpoint, e.g., `hostname:40001`).
204 +4. `RelayTCPPort` starts a TCP listener on the allocated port.
205 +5. An external TCP client connects to `tcp_addr`.
206 +6. `RelayTCPPort.acceptLoop` accepts the connection and claims a reverse session from the lease `RelayStream` using `ClaimRaw` (writes `0x03` marker instead of `0x02`).
207 +7. SDK-side `ClientStream.runSession` receives `0x03`, calls `activateRaw` which passes the raw connection directly without TLS handshake.
208 +8. `RelayTCPPort.bridgeConns` copies data bidirectionally between the external client and the reverse session.
209 +
210 +```text
211 +Client --TCP--> [:40000+ Relay] --raw TCP--> [RelayTCPPort] --ClaimRaw--> [reverse session] --0x03--> [ClientStream] --> Local Service
212 + <--bidirectional bridge--
213 +```
214 +
215 +Result: the relay allocates a dedicated TCP port per lease and bridges raw TCP without TLS. This is ideal for non-TLS protocols like Minecraft, game servers, or any raw TCP service.
216 +
217 ### UDP/QUIC Datagram Transport
218
219 1. SDK/tunnel requests a register challenge with `udp_enabled=true`, signs the returned SIWE message, and then completes `POST /sdk/register`.
@@ -239,6 +267,7 @@ Wire format (`types/transport.go`): `[flowID uvarint][payload bytes]`
267 - `metadata`
268 - `ttl`
269 - `udp_enabled`
270 + - `tcp_enabled`
271 - Challenge response fields:
272 - `challenge_id`
273 - `expires_at`
@@ -255,6 +284,7 @@ Wire format (`types/transport.go`): `[flowID uvarint][payload bytes]`
284 - `expires_at`
285 - `access_token`
286 - optional `udp_addr`
287 + - optional `tcp_addr`
288 - `access_token` is a relay-issued ES256K JWT signed by the relay identity key and validated with:
289 - `iss = PORTAL_URL`
290 - `aud = portal-sdk`
@@ -266,6 +296,10 @@ Wire format (`types/transport.go`): `[flowID uvarint][payload bytes]`
296 - `APIErrorCodeUDPDisabled` (HTTP 403) when UDP is disabled by admin policy
297 - `APIErrorCodeUDPCapacityExceeded` (HTTP 503) when the admin-configured max UDP lease limit is reached
298 - `APIErrorCodeUDPPortExhausted` (HTTP 503) when the UDP port pool is exhausted
299 +- TCP port registration requires two conditions: server must have `TCP_PORT_COUNT > 0` and admin must enable TCP port in the admin panel
300 +- `APIErrorCodeTCPPortDisabled` (HTTP 403) when TCP port is disabled by admin policy
301 +- `APIErrorCodeTCPPortCapacityExceeded` (HTTP 503) when the admin-configured max TCP port lease limit is reached
302 +- `APIErrorCodeTCPPortExhausted` (HTTP 503) when the TCP port pool is exhausted
303 - `PORTAL_URL` is normalized to its host component only; path/query segments are ignored for routing
304
305 ### 2. Reverse Connect
@@ -334,7 +368,7 @@ Cross-package public contract lives in:
368 - shared request/response DTOs
369 - lease metadata
370 - `types/error.go`
337 - - shared API error codes
371 + - shared API error codes (including TCP port: `tcp_port_disabled`, `tcp_port_exhausted`, `tcp_port_capacity_exceeded`)
372 - shared MITM self-probe reason codes
373 - `types/types.go`
374 - shared headers
@@ -367,6 +401,7 @@ Relay-local frontend asset filenames stay in `cmd/relay-server`, not `types/`.
401
402 - Reverse-only backend connectivity
403 - One canonical raw TCP reverse transport
404 +- Dedicated TCP port allocation for non-TLS services with raw TCP bridging
405 - Raw public UDP exposure with an internal QUIC datagram backhaul
406 - Optional WireGuard relay overlay for relay discovery and peer synchronization
407 - SNI-based routing with root-host fallback
@@ -375,7 +410,7 @@ Relay-local frontend asset filenames stay in `cmd/relay-server`, not `types/`.
410 - SIWE identity proof for registration plus relay-issued ES256K JWT access tokens for the lease lifecycle
411 - Lease-local stream and datagram ownership through per-lease transport runtimes
412 - Optional QUIC/UDP datagram transport coexisting with TCP on the same lease
378 -- Per-lease UDP port allocation with sticky name-based reservation
413 +- Per-lease UDP and TCP port allocation with sticky name-based reservation
414 - QUIC tunnel authentication via control stream (`access_token`)
415
416 ## ADRs