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.