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 +```