feat: implement TLS passthrough with ACME DNS-01 support, enhance documentation and configuration for deployment
gosunuts committed
Feb 25, 2026 at 12:05 UTC
88b0072ebdd3b697746a6090f092b8b18ba12300
9 files changed
+373
-74
AGENTS.md
+43
-8
@@ -8,7 +8,6 @@ Build:
8
- `make build` (all artifacts)
9
- `make build-server` (relay server binary)
10
- `make build-frontend` (React admin UI)
11
-- `make build-wasm` (webclient WASM)
11
- `make build-tunnel` (portal-tunnel binaries)
12
13
Run:
@@ -36,27 +35,25 @@ Frontend dev:
35
Portal is a relay network that connects Apps (service publishers) and Clients (service consumers) through a central relay server without decrypting payloads.
36
37
Core components:
39
-- Relay server: `cmd/relay-server` (HTTP + WS relay, admin UI serving)
38
+- Relay server: `cmd/relay-server` (HTTP + TCP relay, admin UI serving)
39
- Relay core logic: `portal/` (lease manager, connection handlers, forwarding)
40
- Crypto + protocols: `portal/core/`
41
- SDK for Apps: `sdk/`
42
- Tunnel client: `cmd/portal-tunnel/` (exposes local services)
44
-- Webclient: `cmd/webclient/` (WASM + service worker served by relay)
43
- Admin frontend: `cmd/relay-server/frontend/` (built into `cmd/relay-server/dist/app`)
44
45
## Connection Flow (High Level)
46
47
1. App registers a Lease with the relay (identity, ALPN, metadata).
48
2. Client requests connection by Lease ID or name.
51
-3. Relay forwards the request to the App and brokers the connection.
52
-4. RDSEC handshake establishes end-to-end encryption (X25519 + ChaCha20-Poly1305).
53
-5. Yamux multiplexes multiple streams over one relay connection.
49
+3. Relay routes TLS connection by SNI to the appropriate tunnel backend.
50
+4. TLS provides end-to-end encryption (relay does not decrypt).
51
52
## Key Terms
53
54
- Portal / Relay: central mediator; never decrypts payloads.
55
- App: service publisher using SDK or tunnel to register Leases.
59
-- Client: consumer (often browser + WASM) connecting via relay.
56
+- Client: consumer connecting via relay.
57
- Lease: advertising unit; one Lease maps to one public endpoint.
58
59
## Where to Look
@@ -65,9 +62,47 @@ Core components:
62
- `portal/` (core relay logic)
63
- `sdk/` (App integration)
64
- `cmd/portal-tunnel/` (tunnel client)
68
-- `cmd/webclient/` (WASM client)
65
- `docs/architecture.md` and `docs/glossary.md`
66
67
+## Domain Configuration
68
+
69
+Portal uses environment variables for domain and TLS configuration:
70
+
71
+### Core Environment Variables
72
+
73
+| Variable | Description |
74
+|----------|-------------|
75
+| `PORTAL_URL` | Base URL (e.g., `https://portal.example.com`) |
76
+| `BOOTSTRAP_URIS` | Relay API URLs (defaults to `PORTAL_URL`) |
77
+| `SNI_PORT` | SNI router port (default `:443`) |
78
+| `ADMIN_SECRET_KEY` | Admin auth key (auto-generated if unset) |
79
+
80
+### ACME Certificate Management
81
+
82
+For TLS passthrough with automatic certificates:
83
+
84
+| Variable | Description |
85
+|----------|-------------|
86
+| `ACME_DNS_PROVIDER` | `cloudflare` or `route53` |
87
+| `ACME_EMAIL` | Email for ACME registration |
88
+| `CLOUDFLARE_API_TOKEN` | Cloudflare API token (if using cloudflare) |
89
+
90
+### Domain Derivation
91
+
92
+- Service URL: `{name}.{base_domain}` (e.g., `myapp.example.com`)
93
+- Base domain extracted from `PORTAL_URL` via `extractBaseDomain()` in `cmd/relay-server/utils.go`
94
+- SNI routes registered in `portal/utils/sni/router.go`
95
+
96
+### TLS Modes
97
+
98
+1. **HTTP Proxy**: No TLS, relay proxies HTTP to tunnel
99
+2. **TLS Passthrough**: Relay routes TLS by SNI to tunnel backend
100
+ - Tunnel client sends CSR to relay
101
+ - Relay issues certificate via ACME DNS-01
102
+ - End-to-end TLS encryption
103
+
104
+See `docs/portal-deploy-guide.md` for full deployment documentation.
105
+
106
## Repo Basics
107
108
- Module: `gosuda.org/portal`
README.md
+3
-4
@@ -5,6 +5,7 @@
5
</p>
6
7
Portal is a permissionless, open hosting network that transforms your local project into a public web endpoint. [Learn more.](https://gosuda.org/portal/)
8
+
9
## Overview
10
11
Portal connects local applications to web users through a secure relay layer.
@@ -14,9 +15,8 @@ This enables developers to publish local services globally without managing serv
15
## Features
16
17
- 🔄 **Connection Relay**: Connects clients behind NAT or firewalls through the Portal network
17
-- 🔐 **End-to-End Encryption**: Fully encrypted client-to-client communication via RDSEC protocol
18
+- 🔐 **End-to-End Encryption**: TLS passthrough with ACME DNS-01 certificates
19
- 🕊️ **Permissionless Hosting**: Anyone can run their own Portal — no approval needed
19
-- 🚀 **High Performance**: Multiplexed connections using yamux
20
- ⚙️ **Simple Setup**: Quick start with Tunnel client or Go SDK
21
22
## Quick Start
@@ -59,10 +59,9 @@ See [portal-toys](https://github.com/gosuda/portal-toys) for more examples.
59
60
## Architecture
61
62
-- **Relay Server**: HTTP/WebSocket relay, admin UI, lease management
62
+- **Relay Server**: TLS passthrough relay with SNI routing, admin UI, lease management
63
- **SDK**: Go library for native app integration
64
- **Tunnel**: CLI client for exposing local services without code changes
65
-- **WebClient**: WASM-based browser client (served by relay)
65
66
For details, see [docs/architecture.md](docs/architecture.md).
67
cmd/portal-tunnel/README.md
+55
@@ -17,6 +17,27 @@ You can run the tunnel using command-line flags or a configuration file.
17
--thumbnail https://example.com/thumb.png
18
```
19
20
+### TLS Mode (End-to-End Encryption)
21
+
22
+Enable TLS for end-to-end encryption from client to your local service:
23
+
24
+```bash
25
+./bin/portal-tunnel --host localhost:8080 \
26
+ --relay https://portal.example.com \
27
+ --name myapp \
28
+ --tls
29
+```
30
+
31
+When `--tls` is enabled:
32
+- Tunnel generates a private key and CSR locally
33
+- Relay issues certificate via ACME DNS-01 (if configured)
34
+- TLS is terminated at the tunnel, then proxied to your local service via TCP
35
+- Access via `https://myapp.example.com` directly on port 443
36
+
37
+**Requirements:**
38
+- Relay must have ACME DNS-01 configured (`ACME_DNS_PROVIDER`, `ACME_EMAIL`)
39
+- DNS provider credentials must be set on the relay server
40
+
41
## Flags
42
43
```text
@@ -27,6 +48,7 @@ Options:
48
--relay Portal relay server API URLs (comma-separated, http/https) [default: http://localhost:4017] [env: RELAYS]
49
--host Target host to proxy to (host:port or URL) [env: APP_HOST]
50
--name Service name [env: APP_NAME]
51
+ --tls Enable TLS termination on tunnel client (uses relay ACME DNS-01) [env: TLS_ENABLE]
52
--protocols ALPN protocols (comma-separated) [default: http/1.1,h2] [env: APP_PROTOCOLS]
53
--description Service description metadata [env: APP_DESCRIPTION]
54
--tags Service tags metadata (comma-separated) [env: APP_TAGS]
@@ -35,3 +57,36 @@ Options:
57
--hide Hide service from discovery (metadata) [env: APP_HIDE]
58
-h, --help Print this help message and exit
59
```
60
+
61
+## Examples
62
+
63
+### Quick Start (HTTP)
64
+
65
+```bash
66
+# macOS/Linux
67
+curl -fsSL https://portal.example.com/tunnel | HOST=localhost:3000 NAME=myapp sh
68
+
69
+# Windows PowerShell
70
+$env:HOST="localhost:3000"; $env:NAME="myapp"; irm https://portal.example.com/tunnel | iex
71
+```
72
+
73
+### Production (TLS)
74
+
75
+```bash
76
+export RELAYS=https://portal.example.com
77
+export APP_HOST=localhost:3000
78
+export APP_NAME=myapp
79
+export TLS_ENABLE=true
80
+
81
+./bin/portal-tunnel
82
+```
83
+
84
+### Multiple Relays (High Availability)
85
+
86
+```bash
87
+./bin/portal-tunnel \
88
+ --host localhost:3000 \
89
+ --name myapp \
90
+ --relay https://portal1.example.com,https://portal2.example.com \
91
+ --tls
92
+```
cmd/relay-server/manager/bps_manager.go
+1
-1
@@ -118,7 +118,7 @@ func (m *BPSManager) Copy(dst io.Writer, src io.Reader, leaseID string) (int64,
118
}
119
120
// EstablishRelayWithBPS sets up bidirectional relay with BPS limiting.
121
-// In the new TLS passthrough architecture, this uses net.Conn instead of yamux.Stream.
121
+// In the new TLS passthrough architecture, this uses net.Conn
122
func EstablishRelayWithBPS(clientConn, leaseConn net.Conn, leaseID string, bpsManager *BPSManager) {
123
bpsLimit := bpsManager.GetBPSLimit(leaseID)
124
log.Info().
docker-compose.yml
+16
-1
@@ -9,9 +9,24 @@ services:
9
- "--port"
10
- "${PORTAL_PORT:-4017}"
11
environment:
12
+ # Core configuration
13
PORTAL_URL: ${PORTAL_URL:-http://localhost:${PORTAL_PORT:-4017}}
14
BOOTSTRAP_URIS: ${BOOTSTRAP_URIS:-http://localhost:${PORTAL_PORT:-4017}}
15
ADMIN_SECRET_KEY: ${ADMIN_SECRET_KEY:-}
16
+ NOINDEX: ${NOINDEX:-}
17
+
18
+ # SNI router port (for TLS passthrough)
19
+ SNI_PORT: ${SNI_PORT:-:443}
20
+
21
+ # ACME certificate management (optional)
22
+ ACME_DNS_PROVIDER: ${ACME_DNS_PROVIDER:-}
23
+ ACME_EMAIL: ${ACME_EMAIL:-}
24
+ ACME_DIRECTORY: ${ACME_DIRECTORY:-}
25
+
26
+ # DNS provider credentials (set as needed)
27
+ CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN:-}
28
ports:
16
- - "4017:4017"
29
+ - "${PORTAL_PORT:-4017}:${PORTAL_PORT:-4017}"
30
+ - "80:80"
31
+ - "443:443"
32
restart: unless-stopped
docs/architecture.md
+23
-21
@@ -7,14 +7,14 @@ Portal connects local applications to web users through a secure relay layer wit
7
```
8
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
9
│ Browser │◄────►│ Relay Server │◄────►│ App/Tunnel │
10
-│ (WASM SW) │ WS │ (:4017) │ WS │ (SDK/CLI) │
10
+│ │ TLS │ (:4017/443) │ TCP │ (SDK/CLI) │
11
└─────────────┘ └──────────────┘ └─────────────┘
12
- │
13
- ▼
14
- ┌──────────────┐
15
- │ Local HTTP │
16
- │ (:3000 etc) │
17
- └──────────────┘
12
+ │
13
+ ▼
14
+ ┌──────────────┐
15
+ │ Local HTTP │
16
+ │ (:3000 etc) │
17
+ └──────────────┘
18
```
19
20
## Components
@@ -22,14 +22,14 @@ Portal connects local applications to web users through a secure relay layer wit
22
### Relay Server (`cmd/relay-server`)
23
24
- **HTTP Server**: Static files, admin UI, API endpoints
25
-- **WebSocket Relay**: `/api/connect` for reverse tunnel connections
25
+- **Reverse Hub**: Connection management for tunnel backends
26
- **Lease Manager**: Registration, TTL, banning
27
- **SNI Router**: TLS passthrough routing
28
29
### SDK (`sdk/`)
30
31
- **Client**: Bootstrap, health checks, reconnection
32
-- **Listener**: `net.Listener` implementation over WebSocket
32
+- **Listener**: `net.Listener` implementation for tunnel connections
33
- **Types**: Shared API types (`RegisterRequest`, `Metadata`, etc.)
34
35
### Tunnel (`cmd/portal-tunnel`)
@@ -37,29 +37,31 @@ Portal connects local applications to web users through a secure relay layer wit
37
- TCP proxy between relay and local service
38
- No code changes required to expose existing services
39
40
-### WebClient (`cmd/webclient`)
41
-
42
-- WASM-based Service Worker proxy
43
-- Runs in browser for E2EE communication
44
-
40
## Connection Flow
41
42
1. **Register**: App/Tunnel → Relay (`POST /api/register`)
48
-2. **Reverse Connect**: App/Tunnel ← Relay (`WS /api/connect`)
43
+2. **Reverse Connect**: App/Tunnel ← Relay (`TCP reverse tunnel`)
44
3. **Client Request**: Browser → Relay (`GET *.localhost:4017`)
45
4. **Proxy**: Relay ↔ App/Tunnel ↔ Local Service
46
47
## Security
48
54
-- **RDSEC**: X25519 key exchange + ChaCha20-Poly1305 encryption
49
+- **E2EE**: TLS passthrough with ACME certificates - relay routes TLS by SNI without termination
50
- **Tokens**: Per-lease reverse connection tokens
56
-- **SNI Routing**: TLS passthrough without termination
51
+- **SNI Routing**: TLS passthrough without decryption
52
53
## Protocol Stack
54
55
```
61
-HTTP/WebSocket
62
- └── yamux (multiplexing)
63
- └── RDSEC (E2EE)
64
- └── Application Data
56
+TLS (E2EE via ACME certificate)
57
+ └── TCP
58
+ └── Application Data
59
+```
60
+
61
+Or for non-TLS (development only):
62
+
63
+```
64
+HTTP
65
+ └── TCP
66
+ └── Application Data
67
```
docs/development.md
+2
-3
@@ -22,7 +22,6 @@ cmd/
22
relay-server/ # Relay server entrypoint
23
portal-tunnel/ # Tunnel CLI client
24
demo-app/ # Demo application
25
- webclient/ # WASM browser client
25
portal/ # Core relay logic
26
sdk/ # Go SDK for apps
27
utils/ # Shared utilities
@@ -47,6 +46,6 @@ utils/ # Shared utilities
46
47
## Architecture Decisions
48
50
-- **E2EE**: RDSEC protocol (X25519 + ChaCha20-Poly1305)
51
-- **Multiplexing**: yamux over WebSocket
49
+- **E2EE**: TLS passthrough with ACME certificates
50
+- **SNI Routing**: TLS routed by hostname without termination
51
- **No CGO**: pure Go for cross-platform builds
docs/glossary.md
+6
-7
@@ -5,9 +5,8 @@ Portal is unlike traditional server–client architectures, both the App and the
5
The Portal is the relay hub provided by this project.
6
It acts as a central mediator, while other components either advertise themselves to the Portal or discover their counterparts through it.
7
8
-* Participates in the RDSEC handshake using its own Ed25519 credential.
9
-* Manages multiplexed streams with yamux and handles lease registration/deletion/query, connection forwarding, and traffic control (BPS, concurrent streams).
10
-* Does not decrypt payloads and only provides routing. App and Client maintain end-to-end encryption (SecureConnection) even when traversing the Portal.
8
+* Handles lease registration/deletion/query, connection forwarding, and traffic control (BPS).
9
+* Does not decrypt TLS payloads and only provides SNI-based routing. TLS mode provides end-to-end encryption between client and tunnel.
10
11
### App (Service Publisher)
12
@@ -15,16 +14,16 @@ An App is a service-providing entity that publishes services to the Portal.
14
In practice, server-side code that uses the Portal SDK to communicate with the Portal is considered an App.
15
16
* Holds at least one credential, connects to the Portal with it, and registers a Lease that represents the App.
18
-* Maintains a yamux session with the Portal and proxies incoming connection requests to a local service (TCP/HTTP, etc.).
17
+* Maintains a TCP connection with the Portal and proxies incoming connection requests to a local service (TCP/HTTP, etc.).
18
* Updates metadata, ALPN, and other advertising attributes via the Portal Frontend or API to improve discoverability.
19
20
### Client (Service Consumer)
21
22
A Client is the end-user entity that attempts to access services published by an App through the Portal.
23
25
-* Currently, this is primarily the browser (Portal WebClient + Service Worker).
26
-* A Client requests a connection to the Portal by specifying a Lease ID or name. The Portal matches this request to the App owning that Lease, then performs an RDSEC handshake to establish a SecureConnection and exchange data.
27
-* A Client must also possess a credential, which grants connection-request permission and enables mutual authentication with the App (E2EE).
24
+* Currently, this is primarily the browser connecting via TLS.
25
+* A Client requests a connection to the Portal by specifying a Lease ID or name. The Portal matches this request to the App owning that Lease, then routes the TLS connection by SNI.
26
+* TLS mode provides end-to-end encryption between client and tunnel.
27
28
### Lease (Advertising Slot)
29
docs/portal-deploy-guide.md
+224
-29
@@ -1,39 +1,234 @@
1
# Portal Deploy Guide
2
3
-How to run a public Portal relay with DNS, TLS, and wildcard subdomains.
3
+How to run a public Portal relay with TLS Passthrough and ACME DNS-01.
4
5
-Portal does NOT manage TLS, certificates, or DNS. You must place an HTTPS reverse proxy or TLS terminator in front of Portal.
5
+## Overview
6
+
7
+Portal uses SNI-based TLS passthrough: the relay routes TLS connections by SNI to tunnel backends, and each tunnel gets its own certificate via ACME DNS-01.
8
+
9
+```
10
+Client ──TLS──► Relay (SNI Router :443) ──TLS──► Tunnel Backend (TLS mode)
11
+ │
12
+ └── ACME DNS-01 issues cert for each tunnel
13
+```
14
15
## Prerequisites
16
+
17
- A public server (VPS / cloud VM / on-prem with port forwarding)
18
- A domain you can manage
10
-- Ports 80 and 443 open to the Internet
19
+- Ports 80, 443, and 4017 open to the Internet
20
- DNS A/AAAA records pointing to your server:
12
- - `yourdomain.com -> <server IP>`
13
- - `*.yourdomain.com -> <server IP>`
14
-
15
-## TLS & Wildcard Certificates
16
-Portal requires a single wildcard TLS certificate for all app subdomains (`*.yourdomain.com`).
17
-
18
-- Wildcard certificates require DNS-01.
19
- - HTTP-01/TLS-ALPN-01 will not work for `*.` names.
20
-- Use any ACME client that supports DNS-01 (reverse proxy or standalone).
21
-- You must provide DNS credentials so the ACME client can create TXT records at
22
- - `_acme-challenge.yourdomain.com`.
23
-
24
-## Environment (docker compose)
25
-- Set these for public deployment (via `environment:`):
26
- ```
27
- PORTAL_PORT=4017
28
- PORTAL_URL=https://yourdomain.com
29
- BOOTSTRAP_URIS=https://yourdomain.com
30
- ```
31
-
32
-## Deploy
33
-- Run Portal (e.g., `docker compose up -d`) exposing 4017 internally.
34
-- Place an HTTPS reverse proxy in front, terminate TLS with your wildcard cert, and route `yourdomain.com` / `*.yourdomain.com` to Portal on 4017.
35
-- Supply your DNS API credentials to the ACME client so DNS-01 can obtain/renew the wildcard cert.
21
+ - `example.com -> <server IP>`
22
+ - `*.example.com -> <server IP>`
23
+- DNS provider API credentials (Cloudflare or Route53) for ACME DNS-01
24
+
25
+## Quick Start
26
+
27
+### 1. DNS Configuration
28
+
29
+```
30
+# A records (IPv4)
31
+example.com. A 203.0.113.10
32
+*.example.com. A 203.0.113.10
33
+
34
+# AAAA records (IPv6, if applicable)
35
+example.com. AAAA 2001:db8::1
36
+*.example.com. AAAA 2001:db8::1
37
+```
38
+
39
+### 2. Create .env File
40
+
41
+```bash
42
+# Core
43
+PORTAL_URL=https://example.com
44
+ADMIN_SECRET_KEY=your-secure-key-here
45
+
46
+# ACME (Cloudflare example)
47
+ACME_DNS_PROVIDER=cloudflare
48
+ACME_EMAIL=admin@example.com
49
+CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
50
+```
51
+
52
+### 3. Run Portal
53
+
54
+```bash
55
+docker compose up -d
56
+```
57
+
58
+### 4. Run Tunnel
59
+
60
+```bash
61
+portal-tunnel --host localhost:3000 --name myapp --relay https://example.com --tls
62
+```
63
+
64
+### 5. Access
65
+
66
+```
67
+https://myapp.example.com
68
+```
69
+
70
+## Environment Variables
71
+
72
+### Core
73
+
74
+| Variable | Default | Description |
75
+|----------|---------|-------------|
76
+| `PORTAL_URL` | `http://localhost:4017` | Base URL (e.g., `https://example.com`) |
77
+| `BOOTSTRAP_URIS` | (derived) | Relay API URLs |
78
+| `ADMIN_SECRET_KEY` | (auto-generated) | Admin authentication key |
79
+| `SNI_PORT` | `:443` | SNI router port |
80
+
81
+### ACME Certificate Management
82
+
83
+| Variable | Description |
84
+|----------|-------------|
85
+| `ACME_DNS_PROVIDER` | `cloudflare` or `route53` |
86
+| `ACME_EMAIL` | Email for ACME registration |
87
+| `ACME_DIRECTORY` | ACME directory URL (default: Let's Encrypt) |
88
+
89
+### DNS Provider Credentials
90
+
91
+**Cloudflare:**
92
+```bash
93
+CLOUDFLARE_API_TOKEN=your-api-token
94
+```
95
+
96
+**Route53:**
97
+```bash
98
+AWS_ACCESS_KEY_ID=your-access-key
99
+AWS_SECRET_ACCESS_KEY=your-secret-key
100
+AWS_REGION=us-east-1
101
+```
102
+
103
+## docker-compose.yml
104
+
105
+```yaml
106
+services:
107
+ portal:
108
+ image: ghcr.io/gosuda/portal:1
109
+ environment:
110
+ PORTAL_URL: ${PORTAL_URL}
111
+ ADMIN_SECRET_KEY: ${ADMIN_SECRET_KEY}
112
+ ACME_DNS_PROVIDER: ${ACME_DNS_PROVIDER}
113
+ ACME_EMAIL: ${ACME_EMAIL}
114
+ CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
115
+ ports:
116
+ - "4017:4017"
117
+ - "443:443"
118
+ - "80:80"
119
+ restart: unless-stopped
120
+```
121
+
122
+## Tunnel Client
123
+
124
+### TLS Mode (Production)
125
+
126
+```bash
127
+portal-tunnel --host localhost:3000 --name myapp --relay https://example.com --tls
128
+```
129
+
130
+- `--host`: Local service address
131
+- `--name`: Subdomain name (becomes `myapp.example.com`)
132
+- `--relay`: Portal relay URL
133
+- `--tls`: Enable TLS
134
+
135
+How it works:
136
+1. Tunnel generates private key and CSR locally
137
+2. Sends CSR to relay via `/api/csr`
138
+3. Relay issues certificate via ACME DNS-01
139
+4. TLS connections go directly to tunnel on port 443
140
+
141
+### Non-TLS Mode (Development Only)
142
+
143
+```bash
144
+portal-tunnel --host localhost:3000 --name myapp --relay https://example.com
145
+```
146
+
147
+**Not recommended for production.** Use TLS mode instead.
148
+
149
+- No TLS certificate issued
150
+- HTTP requests proxied through relay on port 4017
151
+- No end-to-end encryption between client and tunnel
152
+
153
+## Port Summary
154
+
155
+| Port | Service | Description |
156
+|------|---------|-------------|
157
+| 443 | SNI Router | TLS passthrough to tunnel backends |
158
+| 4017 | HTTP Relay | Admin UI, API, HTTP proxy |
159
+| 80 | HTTP | Redirect to HTTPS (optional) |
160
161
## Validate
38
-- Health: `curl -vk https://yourdomain.com/healthz` → `{"status":"ok"}`.
39
-- Tunnel script fetch: `curl -fsSL https://yourdomain.com/tunnel | head`.
162
+
163
+```bash
164
+# Health check
165
+curl https://example.com/healthz
166
+# Expected: {"status":"ok"}
167
+
168
+# Domain API
169
+curl https://example.com/api/domain
170
+# Expected: {"success":true,"base_domain":"example.com"}
171
+
172
+# Tunnel script
173
+curl -fsSL https://example.com/tunnel | HOST=localhost:3000 NAME=test sh
174
+```
175
+
176
+## Architecture
177
+
178
+```
179
+ ┌─────────────────────────────────────────────────────┐
180
+ │ Portal Relay │
181
+ │ │
182
+ :443 TLS ──────►│ ┌─────────────┐ ┌─────────────────────────────┐ │
183
+ │ │ SNI Router │───►│ Tunnel Backend │ │
184
+ │ │ │ │ w/ ACME certificate │ │
185
+ │ └─────────────┘ └─────────────────────────────┘ │
186
+ │ │
187
+ ├─────────────────────────────────────────────────────┤
188
+ │ │
189
+ :4017 HTTP ────►│ ┌─────────────┐ ┌─────────────────────────────┐ │
190
+ │ │ HTTP Server │───►│ Tunnel (dev mode, no TLS) │ │
191
+ │ │ │ │ or redirect to HTTPS │ │
192
+ │ └─────────────┘ └─────────────────────────────┘ │
193
+ │ │ │
194
+ │ ▼ │
195
+ │ ┌─────────────┐ ┌─────────────┐ │
196
+ │ │ Admin UI │ │ API │ │
197
+ │ │ /admin │ │ /api/* │ │
198
+ │ └─────────────┘ └─────────────┘ │
199
+ │ │
200
+ └─────────────────────────────────────────────────────┘
201
+```
202
+
203
+## Troubleshooting
204
+
205
+### SNI Router Fails to Start
206
+
207
+```bash
208
+# Check if port 443 is in use
209
+sudo netstat -tlnp | grep :443
210
+
211
+# Grant capability to bind privileged port
212
+sudo setcap 'cap_net_bind_service=+ep' ./bin/relay-server
213
+```
214
+
215
+### ACME Certificate Issuance Fails
216
+
217
+```bash
218
+# Verify credentials
219
+echo $CLOUDFLARE_API_TOKEN
220
+echo $ACME_EMAIL
221
+
222
+# Check logs
223
+docker compose logs portal | grep -i acme
224
+```
225
+
226
+### Tunnel Cannot Connect
227
+
228
+```bash
229
+# Check relay is running
230
+curl https://example.com/healthz
231
+
232
+# Run tunnel with verbose logging
233
+portal-tunnel --host localhost:3000 --name test --relay https://example.com --tls
234
+```