| 1 | # Portal - Self-Hostable Relay Tunnel for Localhost |
| 2 | |
| 3 | [English](./README.md) | [简体中文](./README.zh-CN.md) |
| 4 | |
| 5 | <p align="center"><img width="800" alt="Portal Demo" src="./portal.gif" /></p> |
| 6 | |
| 7 | <p align="center"><b>Expose local services through self-hosted or public relays.</b><br/>No port forwarding. No inbound firewall rules. No manual DNS setup. No accounts.</p> |
| 8 | |
| 9 | ## Why Portal? |
| 10 | |
| 11 | Portal is a local tunnel runtime and relay network for publishing services to the agentic web. |
| 12 | It publishes local apps, APIs, tools, and agents through self-hosted or public relays, |
| 13 | keeps routing and x402 payment policy in the tunnel process, and avoids requiring a hosted vendor account. |
| 14 | |
| 15 | - **Self-Hostable, Fully Open Source** - Run your own relay with a single |
| 16 | command. The relay is MIT-licensed with no enterprise tier, no feature gating, |
| 17 | and no call-home. Your relay, your rules. |
| 18 | |
| 19 | - **Anonymous Relay Network** - Connect to public relays without a hosted |
| 20 | account or central operator. Combine self-hosted relays with public relays in |
| 21 | a pool to split trust across independent operators you choose. |
| 22 | |
| 23 | - **End-to-End Tenant TLS And ECH** - Because relays are trustless, Portal |
| 24 | terminates tenant TLS at the user's endpoint instead of the relay. Portal also |
| 25 | provides ECH to avoid exposing the real hostname in plaintext SNI. |
| 26 | |
| 27 | - **Built-in MITM Detection** - Portal actively self-probes its own connection |
| 28 | after real traffic begins. It compares TLS keying material exported on both |
| 29 | sides and treats a mismatch as suspected relay-side TLS termination. |
| 30 | |
| 31 | - **Multi-Hop Relay Routing** - Chain multiple relays together so no single |
| 32 | relay knows both the origin and the destination. Use `--multi-hop-depth 3` to |
| 33 | select a three-hop route automatically. |
| 34 | |
| 35 | - **No Accounts, No API Keys** - Authentication uses SIWE-compatible signing |
| 36 | with a locally generated secp256k1 key pair. No email, no registration, no |
| 37 | vendor lock-in. |
| 38 | |
| 39 | - **Built-in x402 Payments** - Routed HTTP paths can require Sui gasless |
| 40 | USDC x402 payment before proxying. Browser apps can import `/x402/client.js`, |
| 41 | and native clients can call `/x402/prepare` directly and send `X-PAYMENT`. |
| 42 | |
| 43 | ## Comparison |
| 44 | |
| 45 | | | Portal | ngrok | Cloudflare Tunnel | frp | |
| 46 | |---|---|---|---|---| |
| 47 | | Public localhost URL | **Yes** | Yes | Yes | Yes | |
| 48 | | Self-hostable | **Yes** | Enterprise only | No | Yes | |
| 49 | | Open source | **MIT** | No | Client only | Apache 2.0 | |
| 50 | | Custom domain | **Yes** | Paid plans | Yes | Yes | |
| 51 | | End-to-end tenant TLS | **Yes** | No | No | No | |
| 52 | | SNI hiding (ECH) | **Yes** | No | No | No | |
| 53 | | MITM self-probe | **Built-in** | No | No | No | |
| 54 | | Multi-relay failover | **Yes** | Managed | Built-in | No | |
| 55 | | Multi-hop routing | **Yes** | No | No | No | |
| 56 | | Account required | **No** | Yes | Yes | No | |
| 57 | | Native x402 payments | **Yes** | No | No | No | |
| 58 | |
| 59 | ## Quick Start |
| 60 | |
| 61 | ### Expose a local service |
| 62 | |
| 63 | **macOS / Linux:** |
| 64 | |
| 65 | ```bash |
| 66 | curl -fsSL https://github.com/gosuda/portal-tunnel/releases/latest/download/install.sh | bash |
| 67 | portal expose 3000 |
| 68 | ``` |
| 69 | |
| 70 | **Windows (PowerShell):** |
| 71 | |
| 72 | ```powershell |
| 73 | $ProgressPreference = 'SilentlyContinue' |
| 74 | irm https://github.com/gosuda/portal-tunnel/releases/latest/download/install.ps1 | iex |
| 75 | portal expose 3000 |
| 76 | ``` |
| 77 | |
| 78 | Portal prints a public HTTPS URL for your local app instantly. More examples: |
| 79 | |
| 80 | ```bash |
| 81 | # Custom name and relay |
| 82 | portal expose 3000 --name myapp --relays https://portal.example.com --discovery=false |
| 83 | |
| 84 | # Mount frontend and API behind one URL |
| 85 | portal expose --name myapp \ |
| 86 | --http-route /api=http://127.0.0.1:3001 \ |
| 87 | --http-route /=http://127.0.0.1:5173 |
| 88 | |
| 89 | # Require Sui USDC x402 payment before proxying a route |
| 90 | portal expose --name paid-app \ |
| 91 | --http-route "/paid=http://127.0.0.1:3001 GET:0.01" \ |
| 92 | --http-route /=http://127.0.0.1:5173 \ |
| 93 | --x402-pay-to 0x... |
| 94 | |
| 95 | # Raw TCP port (Minecraft, databases, SSH) |
| 96 | portal expose localhost:25565 --name minecraft --tcp |
| 97 | |
| 98 | # Three-hop route for maximum anonymity |
| 99 | portal expose 3000 --multi-hop-depth 3 |
| 100 | ``` |
| 101 | |
| 102 | See [CLI Reference](cmd/portal-tunnel/README.md) for the full route syntax and |
| 103 | [API Reference](docs/src/routes/api-reference/+page.md#payments) for the x402 |
| 104 | helper endpoints. |
| 105 | |
| 106 | ### Keep tunnels running with Portal Agent |
| 107 | |
| 108 | Use `portal agent run` when tunnels should keep running outside your terminal. |
| 109 | It runs as a local OS service, keeps every tunnel in one TOML config alive, and |
| 110 | provides a dashboard for relay and multi-hop management. |
| 111 | |
| 112 | ```bash |
| 113 | portal agent run --config config.toml |
| 114 | portal agent dashboard --config config.toml |
| 115 | portal agent restart |
| 116 | portal agent stop |
| 117 | |
| 118 | # Foreground mode skips OS service installation. |
| 119 | portal agent run --config config.toml --foreground |
| 120 | ``` |
| 121 | |
| 122 | See [Portal Agent](docs/src/routes/portal-agent/+page.md) for the config format. |
| 123 | |
| 124 | ### Run your own relay |
| 125 | |
| 126 | ```bash |
| 127 | git clone https://github.com/gosuda/portal-tunnel |
| 128 | cd portal-tunnel && cp .env.example .env |
| 129 | docker compose up |
| 130 | ``` |
| 131 | |
| 132 | For public deployment with DNS automation (ACME), TCP/UDP port ranges, and relay |
| 133 | policy, see [Deployment](docs/src/routes/deployment/+page.md). |
| 134 | |
| 135 | ## How End-to-End Encryption Works |
| 136 | |
| 137 | ```text |
| 138 | Browser |
| 139 | -> Relay SNI router (reads only routing token, forwards raw bytes) |
| 140 | -> Reverse session |
| 141 | -> Portal tunnel (performs TLS handshake locally, derives session keys) |
| 142 | -> Local service |
| 143 | ``` |
| 144 | |
| 145 | 1. The relay accepts the incoming connection and reads only the TLS ClientHello |
| 146 | for SNI-based routing. |
| 147 | 2. It forwards the raw encrypted stream over the reverse session without |
| 148 | terminating TLS. |
| 149 | 3. The Portal tunnel on your side completes the TLS handshake locally. Session |
| 150 | keys are derived on your machine. |
| 151 | 4. For relay-hosted domains, the tunnel obtains certificate signatures via |
| 152 | `/v1/sign`, using the relay only as a keyless signing oracle. The relay signs |
| 153 | handshake digests but never receives session keys. |
| 154 | 5. After the handshake, the relay continues forwarding ciphertext without access |
| 155 | to plaintext. |
| 156 | |
| 157 | When ECH is enabled, the relay also cannot see the actual tenant hostname. It |
| 158 | routes by an opaque token derived from the tunnel identity, while the real SNI |
| 159 | stays inside the ECH-protected ClientHello. |
| 160 | |
| 161 | ## How Multi-Hop Routing Works |
| 162 | |
| 163 | ```text |
| 164 | Browser |
| 165 | -> Entry relay (sees only the opaque route hostname) |
| 166 | -> Middle relay (sees only the next-hop token) |
| 167 | -> Exit relay (sees only the reverse session token) |
| 168 | -> Portal tunnel |
| 169 | -> Local service |
| 170 | ``` |
| 171 | |
| 172 | Each relay in the chain knows only its immediate neighbors. No single relay |
| 173 | holds the full path. Tenant TLS still terminates only on your side, so no relay |
| 174 | in the chain receives tenant TLS plaintext. |
| 175 | |
| 176 | ## Public Relay Registry |
| 177 | |
| 178 | Portal's official public relay registry is: |
| 179 | |
| 180 | ```text |
| 181 | https://raw.githubusercontent.com/gosuda/portal-tunnel/main/registry.json |
| 182 | ``` |
| 183 | |
| 184 | Tunnel clients include this registry by default. If you operate a public Portal |
| 185 | relay, open a pull request to add your relay URL to `registry.json`. |
| 186 | |
| 187 | ## Documentation |
| 188 | |
| 189 | - [CLI Reference](cmd/portal-tunnel/README.md) |
| 190 | - [Concepts](docs/src/routes/concepts/+page.md) |
| 191 | - [Portal Agent](docs/src/routes/portal-agent/+page.md) |
| 192 | - [Wallet and ENS](docs/src/routes/wallet-and-ens/+page.md) |
| 193 | - [Security Model](docs/src/routes/security-model/+page.md) |
| 194 | - [Architecture](docs/src/routes/architecture/+page.md) |
| 195 | - [Deployment](docs/src/routes/deployment/+page.md) |
| 196 | - [Configuration Reference](docs/src/routes/configuration/+page.md) |
| 197 | |
| 198 | ## Contributing |
| 199 | |
| 200 | 1. Fork the repository. |
| 201 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`). |
| 202 | 3. Make the change with focused tests or docs. |
| 203 | 4. Open a pull request. |
| 204 | |
| 205 | ## License |
| 206 | |
| 207 | MIT License - see [LICENSE](LICENSE). |