update readme and landing
Kim committed
Apr 2, 2026 at 16:07 UTC
e020d45b397e059491e4e3ce4d0786483cea9ad6
4 files changed
+128
-148
README.md
+36
-58
@@ -4,68 +4,21 @@
4
5
<p align="center"><img width="800" alt="Portal Demo" src="./portal.gif" /></p>
6
7
-<p align="center">Expose your local application to the public internet - no port forwarding, no NAT, no DNS setup.<br />Portal is a self-hosted relay network with end-to-end encryption (E2EE). You can connect to any relay or run your own.</p><br />
8
-
9
-## Why Portal?
10
-
11
-Publishing a local service to the internet is often complicated.
12
-It usually requires opening inbound ports, configuring NAT or firewalls, managing DNS, and terminating TLS.
13
-
14
-Portal removes this complexity by inverting the connection model.
15
-Applications establish outbound connections to a relay, which exposes the service to the public internet and routes incoming traffic back to the application while preserving end-to-end TLS.
16
-
17
-Unlike other tunneling services, Portal is self-hosted and permissionless. You can run your own relay on your domain or connect to any relay.
7
+<p align="center">Expose your local application to the public internet - no port forwarding, no NAT, no DNS setup.<br />Portal is a trustless relay network where relays cannot access your traffic. Connect to any relay or run your own.</p><br />
8
9
## Features
10
21
-- **NAT-friendly connectivity**: Works behind NAT or firewalls without opening inbound ports
22
-- **Automatic subdomain routing**: Gives each app its own subdomain (`your-app.<base-domain>`)
23
-- **End-to-end tenant TLS**: Relay routes by SNI, while tenant TLS terminates on your side with relay-backed keyless signing
24
-- **Permissionless hosting**: Anyone can run their own Portal, no approval needed
25
-- **One-command setup**: Expose any local app with a single command
26
-- **UDP relay (experimental)**: Supports raw UDP relay
27
-
28
-## How Portal Provides End-to-End Encryption
29
-
30
-Portal is designed so that tenant TLS terminates on your side rather than at the relay. In the normal data path, the relay forwards encrypted traffic without access to tenant TLS plaintext.
31
-
32
-1. The relay accepts the public connection and reads only the TLS ClientHello required for SNI-based routing.
33
-2. It forwards the tenant connection as raw encrypted bytes over the reverse session without terminating tenant TLS.
34
-3. The Portal client on your side acts as the TLS server and completes the tenant handshake locally.
35
-4. For relay-hosted domains, the Portal client obtains certificate signatures via `/v1/sign`, using the relay only as a keyless signing oracle.
36
-5. Session keys are derived entirely on your side. The relay provides certificate signatures only and does not receive tenant traffic secrets.
37
-6. After the handshake, the relay continues forwarding ciphertext without needing tenant TLS plaintext to keep routing traffic.
38
-
39
-Portal also checks that the relay is preserving TLS passthrough. The Portal client connects to its own public endpoint and compares TLS exporter values observed on both client-controlled ends. If they differ, `portal expose` rejects the relay by default.
40
-
41
-## Components
42
-
43
-- **Relay**: A server that routes public requests to the right connected app.
44
-- **Tunnel**: A CLI agent that proxies your local app through the relay.
11
+- **Public HTTPS for localhost**: NAT-friendly publishing via TCP passthrough (no port forwarding)
12
+- **End-to-end TLS**: TLS terminates on your side with built-in MITM detection, so relays cannot access plaintext
13
+- **One-command setup**: Start relays and tunnels with minimal setup
14
+- **Self-hosted relays**: Connect to public relays or run your own
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
19
## Quick Start
20
48
-### Run Portal Relay
49
-
50
-```bash
51
-git clone https://github.com/gosuda/portal
52
-cd portal && cp .env.example .env
53
-docker compose up
54
-```
55
-
56
-The Docker setup persists both the relay identity JSON and relay certificates under `./.portal-certs`. Keep that directory on persistent storage if you want a stable relay address and certificate state across restarts.
57
-
58
-For public domains, you can either:
59
-
60
-- place `fullchain.pem` and `privatekey.pem` in `./.portal-certs` and leave `ACME_DNS_PROVIDER` empty, or
61
-- set `ACME_DNS_PROVIDER=cloudflare|gcloud|route53` and let Portal manage DNS-01 + renewal
62
-
63
-If you want Portal-managed ENS TXT/DNSSEC while keeping manual certificate files, place the certs in `./.portal-certs`, set `ACME_DNS_PROVIDER`, and enable `ENS_GASLESS_ENABLED=true`.
64
-For deployment to a public domain, see [docs/deployment.md](docs/deployment.md).
65
-
66
-### Expose Local Service via Tunnel
67
-
68
-Install the tunnel from the official GitHub release assets:
21
+### Expose your local app:
22
23
```bash
24
curl -fsSL https://github.com/gosuda/portal/releases/latest/download/install.sh | bash
@@ -77,9 +30,21 @@ $ProgressPreference = 'SilentlyContinue'
30
irm https://github.com/gosuda/portal/releases/latest/download/install.ps1 | iex
31
portal expose 3000
32
```
80
-For CLI usage and install details, see [cmd/portal-tunnel/README.md](cmd/portal-tunnel/README.md).
33
82
-### Use the Go SDK (Advanced)
34
+Then access your app via a public HTTPS URL.
35
+For install details, see [cmd/portal-tunnel/README.md](cmd/portal-tunnel/README.md).
36
+
37
+### Run your own relay
38
+
39
+```bash
40
+git clone https://github.com/gosuda/portal
41
+cd portal && cp .env.example .env
42
+docker compose up
43
+```
44
+
45
+For deployment to a public domain, see [docs/deployment.md](docs/deployment.md).
46
+
47
+### Run native app (Advanced)
48
49
See [portal-toys](https://github.com/gosuda/portal-toys) for more examples.
50
@@ -105,6 +70,19 @@ Portal tunnel clients can include this registry by default, and the relay UI als
70
71
If you operate a public Portal relay, open a Pull Request to add your relay URL to `registry.json`. Keeping the registry updated makes public relays easier for the community to discover.
72
73
+## How Portal Provides End-to-End Encryption
74
+
75
+Portal is designed so that tenant TLS terminates on your side rather than at the relay. In the normal data path, the relay forwards encrypted traffic without access to tenant TLS plaintext.
76
+
77
+1. The relay accepts the public connection and reads only the TLS ClientHello required for SNI-based routing.
78
+2. It forwards the tenant connection as raw encrypted bytes over the reverse session without terminating tenant TLS.
79
+3. The Portal client on your side acts as the TLS server and completes the tenant handshake locally.
80
+4. For relay-hosted domains, the Portal client obtains certificate signatures via `/v1/sign`, using the relay only as a keyless signing oracle.
81
+5. Session keys are derived entirely on your side. The relay provides certificate signatures only and does not receive tenant traffic secrets.
82
+6. After the handshake, the relay continues forwarding ciphertext without needing tenant TLS plaintext to keep routing traffic.
83
+
84
+Portal also checks that the relay is preserving TLS passthrough. The Portal client connects to its own public endpoint and compares TLS exporter values observed on both client-controlled ends. If they differ, `portal expose` rejects the relay by default.
85
+
86
## Contributing
87
88
We welcome contributions from the community!
README.zh-CN.md
+47
-61
@@ -4,60 +4,21 @@
4
5
<p align="center"><img width="800" alt="Portal Demo" src="./portal.gif" /></p>
6
7
-<p align="center">将你的本地应用暴露到公网,无需端口转发、NAT 配置或 DNS 设置。<br />Portal 是一个自托管、端到端加密(E2EE)的中继网络。你既可以连接任意中继,也可以自己部署。</p><br />
7
+<p align="center">将本地应用暴露到公网,无需端口转发、NAT 配置或 DNS 设置。<br />Portal 是一个无需信任的中继网络,中继无法访问你的流量。你可以连接任意中继,也可以自行部署自己的中继。</p><br />
8
9
-## 为什么选择 Portal?
9
+## 功能特性
10
11
-将本地服务发布到互联网通常很复杂。
12
-通常需要开放入站端口、配置 NAT 或防火墙、管理 DNS,并终止 TLS。
13
-
14
-Portal 通过反转连接模型来消除这些复杂性。
15
-应用主动向中继建立出站连接,由中继把服务暴露到公网,并将传入流量路由回应用,同时保持端到端 TLS。
16
-
17
-与其他隧道服务不同,Portal 是自托管且无需许可的。你可以在自己的域名上运行中继,也可以连接任意中继。
18
-
19
-## 特性
20
-
21
-- **NAT 友好连接**:无需开放入站端口,也能在 NAT 或防火墙后工作
22
-- **自动子域路由**:为每个应用分配独立子域(`your-app.<base-domain>`)
23
-- **租户端到端 TLS**:中继通过 SNI 路由,而租户 TLS 在你的侧边通过 relay-backed keyless signing 终止
24
-- **无需许可的托管**:任何人都可以运行自己的 Portal,无需审批
25
-- **单命令启动**:用一条命令暴露任意本地应用
26
-- **UDP Relay(实验性)**:支持原始 UDP 转发
27
-
28
-## Portal 如何提供端到端加密
29
-
30
-Portal 的设计目标是让租户 TLS 终止在你的侧边,而不是在中继侧。在正常数据路径中,中继只转发加密流量,无法访问租户 TLS 明文。
31
-
32
-1. 中继接收公网连接,并且只读取 SNI 路由所需的 TLS ClientHello。
33
-2. 中继通过反向会话转发原始加密字节,不会终止租户 TLS。
34
-3. 你侧边的 Portal 客户端作为 TLS 服务器,在本地完成租户握手。
35
-4. 对于 relay-hosted domain,Portal 客户端通过 `/v1/sign` 获取证书签名,把中继仅作为 keyless signing oracle 使用。
36
-5. 会话密钥完全在你的侧边派生。中继只提供证书签名,不会获得租户流量密钥。
37
-6. 握手完成后,中继继续转发密文,无需访问租户 TLS 明文即可持续路由流量。
38
-
39
-Portal 还会检查中继是否真正保留了 TLS passthrough。Portal 客户端会连接自己的公网端点,并比较由两端客户端控制的 TLS exporter 值。如果两者不同,`portal expose` 默认会拒绝该中继。
40
-
41
-## 组件
42
-
43
-- **Relay**:负责把公网请求路由到正确已连接应用的服务器
44
-- **Tunnel**:通过中继代理本地应用的 CLI 代理
11
+- **为 localhost 提供公网 HTTPS**:通过 TCP 透传实现 NAT 友好的发布方式(无需端口转发)
12
+- **端到端 TLS**:TLS 在你这一侧终止,并内置 MITM 检测,因此中继无法访问明文
13
+- **一条命令即可启动**:以最少配置启动中继和隧道
14
+- **自托管中继**:既可以连接公共中继,也可以运行你自己的中继
15
+- **中继发现与中继池**:将发现到的中继作为中继池使用,支持多中继访问与故障切换
16
+- **无需登录、无需 API Key**:使用 SIWE 验证所有权,并支持基于 ENS 的身份
17
+- **原生 TCP 与 UDP 传输**:原生 TCP 反向会话,并可选支持 UDP(不依赖 SSH 或 WebSocket)
18
19
## 快速开始
20
48
-### 运行 Portal Relay
49
-
50
-```bash
51
-git clone https://github.com/gosuda/portal
52
-cd portal
53
-docker compose up
54
-```
55
-
56
-如果要部署到公网域名,请参见 [docs/deployment.md](docs/deployment.md)。
57
-
58
-### 通过 Tunnel 暴露本地服务
59
-
60
-先从官方 GitHub release asset 安装 tunnel:
21
+### 公开你的本地应用:
22
23
```bash
24
curl -fsSL https://github.com/gosuda/portal/releases/latest/download/install.sh | bash
@@ -69,9 +30,21 @@ $ProgressPreference = 'SilentlyContinue'
30
irm https://github.com/gosuda/portal/releases/latest/download/install.ps1 | iex
31
portal expose 3000
32
```
72
-CLI 用法和安装细节请参见 [cmd/portal-tunnel/README.md](cmd/portal-tunnel/README.md)。
33
74
-### 使用 Go SDK(高级)
34
+然后你就可以通过一个公网 HTTPS URL 访问你的应用。
35
+安装细节请参见 [cmd/portal-tunnel/README.md](cmd/portal-tunnel/README.md)。
36
+
37
+### 运行你自己的中继
38
+
39
+```bash
40
+git clone https://github.com/gosuda/portal
41
+cd portal && cp .env.example .env
42
+docker compose up
43
+```
44
+
45
+部署到公网域名时,请参见 [docs/deployment.md](docs/deployment.md)。
46
+
47
+### 运行原生应用(高级)
48
49
更多示例请参见 [portal-toys](https://github.com/gosuda/portal-toys)。
50
@@ -84,29 +57,42 @@ CLI 用法和安装细节请参见 [cmd/portal-tunnel/README.md](cmd/portal-tunn
57
58
| 示例 | 说明 |
59
|---------|-------------|
87
-| [nginx reverse proxy](docs/examples/nginx-proxy/) | 将 Portal 部署在 nginx 后面,使用 L4 SNI 路由和 TLS 终止 |
88
-| [nginx + multi-service](docs/examples/nginx-proxy-multi-service/) | 在同一个 nginx 后面同时运行 Portal 和其他 Web 服务 |
60
+| [nginx reverse proxy](docs/examples/nginx-proxy/) | 在 nginx 后部署 Portal,并使用 L4 SNI 路由和 TLS 终止 |
61
+| [nginx + multi-service](docs/examples/nginx-proxy-multi-service/) | 在同一个 nginx 实例后,将 Portal 与其他 Web 服务一起运行 |
62
90
-## 公共 Relay Registry
63
+## 公共中继注册表
64
92
-Portal 的官方公共 relay registry 是:
65
+Portal 官方公共中继注册表为:
66
67
`https://raw.githubusercontent.com/gosuda/portal/main/registry.json`
68
96
-Portal tunnel 客户端可以默认包含这个 registry,relay UI 也会从同一路径读取它,以展示官方 relay 列表。
69
+Portal 隧道客户端可以默认包含这个注册表,Relay UI 也会从同一路径读取官方中继列表。
70
+
71
+如果你正在运营公共 Portal 中继,请提交一个 Pull Request,将你的中继 URL 添加到 `registry.json`。持续维护这个注册表可以让社区更容易发现公共中继。
72
+
73
+## Portal 如何提供端到端加密
74
+
75
+Portal 的设计目标是让租户 TLS 在你这一侧终止,而不是在中继侧终止。在正常数据路径中,中继只转发加密流量,无法访问租户 TLS 明文。
76
+
77
+1. 中继接收公网连接,并且只读取基于 SNI 路由所需的 TLS ClientHello。
78
+2. 它通过反向会话将租户连接作为原始加密字节转发,而不会终止租户 TLS。
79
+3. 你这一侧的 Portal 客户端充当 TLS 服务器,并在本地完成租户握手。
80
+4. 对于由中继托管的域名,Portal 客户端通过 `/v1/sign` 获取证书签名,此时中继只作为无密钥签名预言机使用。
81
+5. 会话密钥完全在你这一侧派生。中继只提供证书签名,不会接收租户流量密钥。
82
+6. 握手完成后,中继继续转发密文,无需租户 TLS 明文即可保持流量路由。
83
98
-如果你在运营公共 Portal relay,欢迎提交 Pull Request,把你的 relay URL 添加到 `registry.json`。保持 registry 更新有助于社区更容易发现公共 relay。
84
+Portal 还会检查中继是否真的保持了 TLS 透传。Portal 客户端会连接到自己的公网端点,并比较由客户端控制的两端观察到的 TLS exporter 值。如果两者不一致,`portal expose` 默认会拒绝该中继。
85
86
## 贡献
87
88
欢迎社区贡献!
89
104
-1. Fork 本仓库
90
+1. Fork 此仓库
91
2. 创建功能分支(`git checkout -b feature/amazing-feature`)
106
-3. 提交修改(`git commit -m 'Add amazing feature'`)
107
-4. 推送分支(`git push origin feature/amazing-feature`)
92
+3. 提交你的修改(`git commit -m 'Add amazing feature'`)
93
+4. 推送到分支(`git push origin feature/amazing-feature`)
94
5. 创建 Pull Request
95
96
## 许可证
97
112
-MIT License,参见 [LICENSE](LICENSE)
98
+MIT License,详见 [LICENSE](LICENSE)
docs/architecture.md
+4
-1
@@ -115,7 +115,7 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
115
- `acme`: Cloudflare/Google Cloud DNS/Route53-backed root/wildcard A-record sync + certificate provisioning/renewal for the relay root host and wildcard
116
- `keyless`: admin/API TLS attach helpers and tenant-side signer integration
117
- `auth`: SIWE register challenge creation/verification plus lease access token issue/verify
118
-- `discovery`: signed relay descriptor publication and relay-set synchronization
118
+- `discovery`: relay descriptor publication over relay HTTPS plus relay-set synchronization
119
- `wireguard`: optional relay overlay network used to reach peer relay APIs over internal overlay IPs and keep relay peer state synchronized
120
- `Server` additionally owns `quicTunnel` (QUIC listener, ALPN `portal-tunnel`) when UDP transport is enabled
121
@@ -207,6 +207,9 @@ Wire format (`types/transport.go`): `[flowID uvarint][payload bytes]`
207
## WireGuard Overlay and Discovery
208
209
- Discovery starts from bootstrap relay URLs over normal public HTTPS.
210
+- Discovery descriptors are currently transport-authenticated by the queried relay endpoint, not by embedded descriptor signatures.
211
+- Current discovery validation covers protocol version, descriptor normalization, required fields, expiry, target URL/identity matching, and overlay field sanity only.
212
+- Descriptor `identity.address` is a relay claim inside discovery. Independent `domain -> address` verification comes from optional ENS/DNSSEC evidence, not from the discovery payload itself.
213
- Each relay publishes a descriptor over relay HTTPS that may advertise:
214
- `wireguard_public_key`
215
- `wireguard_endpoint`
frontend/src/components/LandingHero.tsx
+41
-28
@@ -33,29 +33,40 @@ const heroDifferentiatorCards = [
33
34
const heroFeatures = [
35
{
36
- title: "No setup. No port forwarding.",
36
+ eyebrow: "HTTPS",
37
+ title: "Public HTTPS for localhost",
38
description:
38
- "Works instantly, even behind NAT and firewalls.",
39
+ "Publish local apps through TCP passthrough without opening inbound ports.",
40
},
41
{
41
- title: "End-to-end TLS",
42
+ eyebrow: "TLS",
43
+ title: "End-to-end TLS on your side",
44
description:
43
- "End-to-end TLS via SNI routing, keyless TLS, and built-in MITM detection.",
45
+ "Tenant TLS terminates locally with MITM detection, so relays cannot access plaintext.",
46
},
47
{
46
- title: "Permissionless hosting",
48
+ eyebrow: "Setup",
49
+ title: "One-command setup",
50
description:
48
- "Attach to arbitrary relays - no accounts, no approval, no trust required.",
51
+ "Start relays and tunnels with minimal setup and a short copy-paste path.",
52
},
53
{
51
- title: "UDP support",
54
+ eyebrow: "Relay",
55
+ title: "Self-hosted relays and pools",
56
description:
53
- "Expose web apps and arbitrary protocols through the same tunnel.",
57
+ "Connect to public relays, use discovered relays as a pool with failover, or run your own.",
58
},
59
{
56
- title: "One command. Done.",
60
+ eyebrow: "Transport",
61
+ title: "Raw TCP with optional UDP",
62
description:
58
- "Install and expose your app in a single copy-paste.",
63
+ "Carry web traffic and arbitrary protocols without SSH or WebSocket overlays.",
64
+ },
65
+ {
66
+ eyebrow: "Identity",
67
+ title: "SIWE ownership with ENS support",
68
+ description:
69
+ "Authenticate ownership with SIWE and keep identity portable with ENS-based naming support.",
70
},
71
] as const;
72
@@ -392,34 +403,36 @@ export function LandingHero() {
403
</div>
404
405
<div className="relative -mx-4 w-auto sm:-mx-6 md:-mx-8">
406
+ <div className="mx-auto max-w-6xl px-4 pb-5 text-left sm:px-6">
407
+ <div className="space-y-2">
408
+ <p className="text-sm font-semibold uppercase tracking-[0.3em] text-primary">
409
+ Core features
410
+ </p>
411
+ <h2 className="text-3xl font-semibold tracking-tight text-foreground">
412
+ Built for real localhost publishing
413
+ </h2>
414
+ </div>
415
+ </div>
416
<div className="overflow-hidden border-t border-border/80 bg-border/70">
417
<div className="grid gap-px sm:grid-cols-2 lg:grid-cols-3">
397
- <div className="flex min-h-46 bg-background/88 p-6 text-left sm:min-h-49 sm:p-7">
398
- <div className="space-y-2">
399
- <p className="text-sm font-semibold uppercase tracking-[0.3em] text-primary">
400
- Core features
401
- </p>
402
- <h2 className="whitespace-nowrap text-[1.2rem] font-semibold tracking-tight text-foreground sm:text-[1.32rem] sm:leading-none">
403
- Make localhost public
404
- </h2>
405
- <p className="max-w-[28ch] text-[0.95rem] leading-6 text-text-muted">
406
- Turn any local app into a shareable HTTPS URL in seconds.
407
- </p>
408
- </div>
409
- </div>
410
-
411
- {heroFeatures.map(({ title, description }) => (
418
+ {heroFeatures.map(({ eyebrow, title, description }) => (
419
<article
420
key={title}
414
- className="flex min-h-46 bg-background/88 p-6 text-left transition-colors duration-200 hover:bg-background/92 sm:min-h-49 sm:p-7"
421
+ className="flex min-h-52 bg-background/88 p-6 text-left transition-colors duration-200 hover:bg-background/92 sm:min-h-56 sm:p-7"
422
>
416
- <div className="space-y-2">
417
- <h3 className="whitespace-nowrap text-[1.2rem] font-semibold tracking-tight text-foreground sm:text-[1.32rem] sm:leading-none">
423
+ <div className="flex h-full flex-col space-y-3">
424
+ <p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-primary/80">
425
+ {eyebrow}
426
+ </p>
427
+ <h3 className="text-[1.2rem] font-semibold tracking-tight text-foreground sm:text-[1.32rem] sm:leading-tight">
428
{title}
429
</h3>
430
<p className="max-w-[28ch] text-[0.95rem] leading-6 text-text-muted">
431
{description}
432
</p>
433
+ <div className="mt-auto pt-4">
434
+ <div className="h-px w-12 bg-linear-to-r from-primary/55 to-transparent" />
435
+ </div>
436
</div>
437
</article>
438
))}