docs(portal): align transport and routing guidance

cognitive committed Mar 3, 2026 at 20:03 UTC 45e0752feefd3d27feb892d655d027459a6139ea
4 files changed +74 -68
README.md
+19 -14
@@ -4,31 +4,32 @@
4 <img src="/portal.jpg" alt="Portal logo" width="540" />
5 </p>
6
7 -Expose your local application to the public internet — no ports, no NAT, no DNS setup.
7 +Expose your local application on the public internet without opening inbound ports or managing NAT and DNS.
8
9 -Portal is a **self-hosted**, **permissionless** relay network. Portal is not a SaaS tunnel, but a routing layer you can connect to — or run yourself.
9 +Portal is a self-hosted relay network. You can run your own relay or connect to one that is already running.
10
11 ## Why Portal?
12
13 -Publishing a local service typically requires:
13 +Publishing a local service usually requires:
14
15 - Opening inbound ports
16 - Configuring NAT or firewall rules
17 - Managing DNS records
18 - Terminating TLS at a gateway
19
20 -Portal removes these steps by inverting the connection model.
20 +Portal removes most of this setup by inverting the connection model.
21 Applications establish outbound connections to a relay.
22 The relay runs on a public base domain, assigns each service a subdomain,
23 and routes incoming traffic while preserving end-to-end TLS.
24
25 ## Features
26
27 -- 🔄 **Connection Behind NAT**: Works behind NAT or firewalls without opening inbound ports
28 -- 🌐 **Automatic Subdomain Routing**: Give each app its own subdomain ( your-app.<base-domain> )
29 -- 🔐 **End-to-End Encryption**: TLS passthrough with relay keyless certificates
30 -- 🕊️ **Permissionless Hosting**: Anyone can run their own Portal — no approval needed
31 -- ⚙️ **One-Command Setup**: Expose any local app with a single command
27 +- **NAT-friendly connectivity**: Works behind NAT or firewalls without opening inbound ports
28 +- **Automatic subdomain routing**: Gives each app its own subdomain (`your-app.<base-domain>`)
29 +- **Non-apex `PORTAL_URL` friendly**: Route hosts are derived from the full portal host (e.g., `https://portal.example.com:8443` -> `portal.example.com`), so services become `<name>.portal.example.com`
30 +- **End-to-end encryption**: Supports TLS passthrough with relay keyless certificates
31 +- **Self-hosted by design**: You can run your own Portal relay
32 +- **Fast setup**: Expose a local app with a short command flow
33
34 ## Components
35
@@ -40,7 +41,11 @@ For details, see [docs/glossary.md](docs/glossary.md).
41 ## Protocol Scope
42
43 - Raw TCP reverse-connect is the only supported relay/tunnel transport.
43 -- WebSocket and legacy compatibility paths are intentionally unsupported.
44 +- No websocket compatibility path is provided for transport control or data-plane flow.
45 +
46 +### Routing Notes
47 +
48 +- SNI routing preserves an exact-match fallback for the portal root host. Requests that target the exact `PORTAL_URL` host (for example, `portal.example.com`) are handled by the admin/API listener via the no-route path.
49
50 ## Quick Start
51
@@ -57,11 +62,11 @@ For deployment to a public domain, see [docs/deployment.md](docs/deployment.md).
62 ### Expose Local Service via Tunnel
63
64 1. Run your local service.
60 -2. Open a Portal relay site.
65 +2. Open the Portal relay site.
66 3. Click `Add your server` button.
67 4. Use the generated command to connect your local service.
68
64 -### Use Go SDK (Advanced)
69 +### Use the Go SDK
70
71 See [portal-toys](https://github.com/gosuda/portal-toys) for more examples.
72
@@ -72,11 +77,11 @@ For architecture decisions, see [docs/adr/README.md](docs/adr/README.md).
77
78 ## Contributing
79
75 -We welcome contributions from the community!
80 +Contributions are welcome.
81
82 ### Verification (CI-Aligned)
83
79 -Run the same checks enforced in CI:
84 +Run the same checks enforced in CI (`.github/workflows/ci.yml`):
85
86 ```bash
87 make vet
docs/architecture.md
+18 -25
@@ -3,7 +3,7 @@
3 ## Overview
4
5 Portal is a relay network for publishing local services on public subdomains.
6 -The relay is the control and routing plane; service backends connect outward to the relay (NAT-friendly), and clients connect to the relay domain.
6 +The relay is both the control plane and routing plane. Service backends connect outward to the relay (NAT-friendly), and clients connect to the relay domain.
7
8 High-level path:
9
@@ -41,25 +41,18 @@ Client (Browser)
41 - CLI proxy for existing local apps without code changes
42 - Registers lease via SDK and forwards relay traffic to local `--host`
43
44 -## Data Plane Modes
44 +## Transport Model
45
46 -### TLS mode (`lease.TLS=true`)
46 +### Raw reverse transport (`lease.TLS=true` only)
47
48 -1. Lease is registered with TLS enabled.
49 -2. Relay registers SNI route (`<lease>.<base-domain>`).
50 -3. Client connects via HTTPS to relay SNI port.
51 -4. Relay selects route by SNI and acquires reverse connection from `ReverseHub`.
52 -5. Tunnel-side listener performs TLS handshake (keyless-backed signer), relay forwards raw TCP.
48 +1. Relay requires registered leases to use TLS.
49 +2. Backend opens a raw TCP reverse connection via `GET /sdk/connect?lease_id=...`.
50 +3. Clients connect via HTTPS to relay SNI port.
51 +4. Relay resolves route by SNI and acquires a reverse connection from `ReverseHub`.
52 +5. Tunnel-side listener performs TLS handshake (keyless-backed signer), while relay forwards raw TCP transparently.
53 +6. No websocket or legacy compatibility transport is used.
54
54 -Result: relay does SNI-based routing and forwarding; app payload stays end-to-end encrypted.
55 -
56 -### Non-TLS mode (`lease.TLS=false`)
57 -
58 -1. Client connects over HTTP to relay/admin port.
59 -2. Relay resolves lease by subdomain and acquires reverse connection.
60 -3. Relay proxies HTTP request/response through reverse tunnel.
61 -
62 -Result: simple HTTP proxy path for development or non-TLS services.
55 +Result: the relay handles SNI-based routing and transparent raw TCP forwarding, preserving end-to-end TLS where applicable.
56
57 ## Control Plane Flow
58
@@ -72,11 +65,11 @@ Result: simple HTTP proxy path for development or non-TLS services.
65 - `tls`
66 - `reverse_token`
67 - Relay stores lease and (TLS only) registers SNI route.
75 -- Route hostnames are generated from normalized lease + normalized `PORTAL_URL` host (`scheme/path/port` removed).
68 +- Route hostnames are generated from normalized lease + normalized `PORTAL_URL` host (extract host from URL without scheme/port/path); path segments are ignored, so `https://portal.example.com:8443/admin` and `https://portal.example.com` both map to `portal.example.com`.
69
70 ### 2. Reverse Connect
71
79 -- Backend opens raw TCP reverse channel to `GET /sdk/connect?lease_id=...` and upgrades into a long-lived stream
72 +- Backend opens a raw TCP reverse channel to `GET /sdk/connect?lease_id=...` and streams traffic over that long-lived connection
73 - `X-Portal-Reverse-Token` is validated server-side.
74 - Connection is pooled in `ReverseHub`.
75
@@ -96,10 +89,10 @@ Result: simple HTTP proxy path for development or non-TLS services.
89
90 1. Exact host match
91 2. Single-label wildcard (`*.example.com`)
99 -3. No-route handler (used for portal root-domain fallback)
92 +3. No-route handler (used for exact portal-root host fallback to admin/API listener on the `PORTAL_URL` root host)
93
101 -Note: wildcard does not match apex domain (`example.com`).
102 -For non-apex `PORTAL_URL` values such as `https://portal.example.com:8443/admin`, SNI/public hostnames are normalized to `<lease>.portal.example.com`.
94 +Note: wildcard does not match the portal root host itself (`example.com` or `portal.example.com`), so exact root-host matches trigger fallback to admin/API listener.
95 +`PORTAL_URL` is normalized to its host component (scheme/port/path removed), so non-apex values such as `https://portal.example.com:8443/admin` become `<lease>.portal.example.com` and exact host matches still resolve through no-route fallback.
96
97 ## Keyless and Certificates
98
@@ -109,11 +102,11 @@ For non-apex `PORTAL_URL` values such as `https://portal.example.com:8443/admin`
102 - If materials are missing and Cloudflare token is configured, relay can provision via ACME DNS-01.
103 - SDK/tunnel TLS mode uses keyless signer workflow and `/v1/sign` for remote signatures.
104
112 -## Important Design Properties
105 +## Design Properties
106
114 -- Reverse-only backend connectivity (no inbound port on app host required)
107 +- Reverse-only backend connectivity (no inbound port required on the app host)
108 - Per-lease reverse token authorization
116 -- Separation of control plane (`/sdk/*`) and data plane (SNI/HTTP forwarding)
109 +- Separation of control plane (`/sdk/*`) and data plane (SNI + raw TCP forwarding)
110 - Single transport policy: raw TCP reverse-connect only (no websocket/legacy compatibility mode)
111 - Unified lease abstraction for routing, metadata, and lifecycle
112
docs/deployment.md
+13 -10
@@ -1,7 +1,6 @@
1 # Portal Relay Deployment Guide
2
3 -This guide covers production-style deployment of Portal Relay on a public domain.
4 -It includes DNS setup, environment configuration, startup, validation, and basic operations.
3 +This guide covers production deployment of Portal Relay on a public domain.
4
5 ## 1. Prerequisites
6
@@ -11,7 +10,7 @@ You need:
10 - A public Linux server with a static public IP
11 - Open inbound ports: `443/tcp`, `4017/tcp`
12 - Docker and Docker Compose
14 -- Cloudflare-managed DNS zone for your domain (required for automatic ACME DNS-01 flow)
13 +- A Cloudflare-managed DNS zone (required for ACME DNS-01 automation)
14
15 ## 2. DNS and Cloudflare Setup
16
@@ -37,17 +36,20 @@ Cloudflare Dashboard -> `DNS` -> `Records`:
36 - Content: `<server-ip>`
37 - Proxy status: `DNS only`
38
40 -Expected:
39 +Expected records:
40
41 - `example.com -> <server-ip>`
42 - `*.example.com -> <server-ip>`
43
45 -If you run Portal on a non-apex host (for example, `PORTAL_URL=https://portal.example.com:8443`), use host-specific records instead:
44 +If you deploy on a non-apex host (for example, `PORTAL_URL=https://portal.example.com:8443`), create host-scoped records:
45
46 - `portal.example.com -> <server-ip>`
47 - `*.portal.example.com -> <server-ip>`
48
50 -Portal normalizes `PORTAL_URL` to its host for routing, so service SNI/public hosts become `<lease>.portal.example.com`.
49 +Portal normalizes `PORTAL_URL` to its host for routing, so public service hosts become `<lease>.portal.example.com`.
50 +Requests to the exact `PORTAL_URL` host (for example, `portal.example.com`) are not wildcard-matched; the router uses no-route fallback and forwards them to the admin/API listener.
51 +Backend registration and reverse traffic are raw TCP on `/sdk/connect`.
52 +This build does not include websocket transport compatibility.
53
54 ### 2.3 Create Cloudflare API Token
55
@@ -66,7 +68,7 @@ Save this token for `CLOUDFLARE_TOKEN`.
68
69 ## 3. Run Relay Server
70
69 -### 3-1. Create `.env` at repository root:
71 +### 3.1 Create `.env` at repository root
72
73 ```bash
74 PORTAL_URL=https://example.com
@@ -77,9 +79,10 @@ KEYLESS_DIR=/etc/portal/keyless
79 CLOUDFLARE_TOKEN=cf_xxxxxxxxxxxxxxxxx
80 ```
81
80 -For non-apex deployments, set `PORTAL_URL` and `BOOTSTRAP_URIS` to the same non-apex host value (for example, `https://portal.example.com:8443`).
82 +For non-apex deployments, set `PORTAL_URL` and `BOOTSTRAP_URIS` to the same non-apex host value (for example, `https://portal.example.com:8443`). Keep any path segments only for dashboard use, not for routing.
83 +`PORTAL_URL` path/query segments are ignored for route derivation; only the host component is used.
84
82 -### 3-2. Start Relay
85 +### 3.2 Start Relay
86
87 ```bash
88 docker compose up
@@ -89,7 +92,7 @@ docker compose up
92
93 ### 4.1 Ports blocked
94
92 -Required inbound:
95 +Required inbound ports:
96
97 - `443/tcp`
98 - `4017/tcp`
docs/glossary.md
+24 -19
@@ -1,39 +1,40 @@
1 # Glossary
2
3 -This glossary defines key terms used in Portal.
3 +Key terms used in Portal.
4
5 ## Portal / Relay
6
7 -The central server that handles lease registration, routing, and reverse connection brokering.
8 -It does not terminate end-to-end app payload in TLS passthrough mode.
7 +The central server that handles lease registration, routing, and reverse-connection brokering.
8 +In TLS passthrough mode, it routes transport and does not terminate app payload TLS.
9 +All backend-to-relay ingress uses a long-lived raw TCP reverse-connect channel (`/sdk/connect`); websocket compatibility transport is not supported.
10
11 ## App (Service Publisher)
12
12 -A service provider connected to Portal through Tunnel or Native SDK.
13 +A backend service connected to Portal through Tunnel or Native SDK.
14 An app publishes one or more leases and serves traffic from local services.
15
16 ## Client (Service Consumer)
17
17 -The external user or browser that accesses a published service through the relay domain.
18 +A browser or external caller that accesses a published service through relay-managed domains.
19
20 ## Tunnel
21
21 -The CLI-based publisher path (`cmd/portal-tunnel`).
22 -It exposes existing local apps without code changes by forwarding relay traffic to a local host/port.
22 +The CLI publisher path (`cmd/portal-tunnel`).
23 +It forwards relay traffic to an existing local host/port without app code changes.
24
25 ## Native SDK
26
26 -The code integration path (`sdk/`) for Go applications.
27 -It provides relay-backed listener APIs and metadata control for direct app integration.
27 +The Go integration path (`sdk/`).
28 +It provides relay-backed listener APIs and lease metadata control for direct integration.
29
30 ## Lease
31
31 -The routing and advertisement unit in Portal.
32 +Portal's routing and advertisement unit.
33 Each lease maps to one public endpoint and includes identity, name, metadata, TLS flag, and reverse token.
34
35 ## Lease Name
36
36 -The human-readable lease identifier used for subdomain routing (for example, `myapp` -> `myapp.example.com`).
37 +The human-readable identifier used for subdomain routing (for example, `myapp` -> `myapp.example.com`).
38
39 ## Reverse Token
40
@@ -41,26 +42,30 @@ A per-lease secret used to authenticate reverse connections (`/sdk/connect`) fro
42
43 ## ReverseHub
44
44 -The relay-side pool of authenticated reverse connections, keyed by lease ID.
45 -It provides connections for TLS SNI forwarding and HTTP proxy forwarding.
45 +Relay-side pool of authenticated reverse connections keyed by lease ID.
46 +It supplies raw TCP reverse connections for TLS SNI forwarding.
47
48 ## SNI Router
49
49 -The TCP router on relay SNI port (default `443`) that selects lease routes by TLS Server Name Indication (SNI).
50 +The TCP router on relay SNI port (default `443`) that selects lease routes by TLS SNI.
51 +Exact matches on the portal root host (derived from `PORTAL_URL` host) are intentionally routed via no-route fallback to the admin/API listener.
52
53 ## Keyless TLS
54
53 -A mode where the backend handles TLS with remote signing support via relay signer endpoint (`/v1/sign`), without local private key distribution.
55 +A mode where the backend performs TLS while using the relay signer endpoint (`/v1/sign`) for remote signing.
56 +This avoids distributing private keys to every backend host.
57
58 ## ACME DNS-01
59
57 -The certificate issuance/renewal method used with Cloudflare DNS API token when keyless materials are missing.
60 +Certificate issuance/renewal method used with a Cloudflare DNS API token when keyless materials are missing.
61
62 ## Base Domain
63
61 -The normalized host derived from `PORTAL_URL` and used to build service subdomains.
62 -For non-apex values such as `https://portal.example.com:8443/admin`, the base domain is `portal.example.com` (scheme, path, and port are removed).
64 +The host extracted from `PORTAL_URL` (scheme, port, and path removed) and used to build service subdomains.
65 +For non-apex values such as `https://portal.example.com:8443/admin`, the base host is `portal.example.com`.
66 +The same host is used for exact-match SNI fallback, which routes root-host requests to the admin/API listener.
67
68 ## Admin/API Server
69
66 -The relay HTTP server (default `:4017`) that serves admin UI and SDK/control endpoints such as `/sdk/*`, `/admin`, and `/healthz`.
70 +The relay HTTP server (default `:4017`) serving admin UI and control endpoints such as `/sdk/*`, `/admin`, and `/healthz`.
71 +It also receives root-domain fallback traffic from SNI when no more specific lease route is found.