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