| 1 | --- |
| 2 | title: Concepts |
| 3 | description: Understand Portal's relay model, transport modes, and end-to-end TLS design. |
| 4 | --- |
| 5 | |
| 6 | # Concepts |
| 7 | |
| 8 | Portal publishes local services through relay servers. The important design |
| 9 | choice is that the relay is a transport and routing component, not the owner of |
| 10 | your application traffic. |
| 11 | |
| 12 | ## Relay And Tunnel Responsibilities |
| 13 | |
| 14 | The relay owns: |
| 15 | |
| 16 | - lease registration and renewal |
| 17 | - public hostname and port routing |
| 18 | - SNI route lookup for the default stream path |
| 19 | - relay discovery and relay-to-relay forwarding |
| 20 | - admin policy such as approval, bans, and transport limits |
| 21 | |
| 22 | The tunnel process owns: |
| 23 | |
| 24 | - tenant TLS termination for the default HTTPS stream path |
| 25 | - local target proxying |
| 26 | - routed HTTP reverse proxy behavior |
| 27 | - UDP target forwarding |
| 28 | - identity keys and lease signing |
| 29 | - MITM self-probe validation |
| 30 | |
| 31 | This split is why Portal can use public relays without giving relay operators |
| 32 | tenant plaintext. |
| 33 | |
| 34 | ## Default Stream Path |
| 35 | |
| 36 | The default command is: |
| 37 | |
| 38 | ```bash |
| 39 | portal expose 3000 |
| 40 | ``` |
| 41 | |
| 42 | The public URL is HTTPS, but the relay does not terminate tenant TLS. |
| 43 | |
| 44 | ```text |
| 45 | Browser |
| 46 | -> Relay :443 |
| 47 | -> reverse session |
| 48 | -> tunnel process TLS server |
| 49 | -> 127.0.0.1:3000 |
| 50 | ``` |
| 51 | |
| 52 | Flow: |
| 53 | |
| 54 | 1. A browser connects to the relay and sends a TLS ClientHello. |
| 55 | 2. The relay reads the SNI hostname and finds the matching lease. |
| 56 | 3. The relay claims a waiting reverse session from the tunnel process. |
| 57 | 4. The tunnel process performs the tenant TLS handshake locally. |
| 58 | 5. The relay may sign handshake digests through `/v1/sign`, but it does not |
| 59 | receive tenant TLS session keys. |
| 60 | 6. After the handshake, the relay forwards encrypted bytes. |
| 61 | |
| 62 | ## Routed HTTP Mode |
| 63 | |
| 64 | Routed HTTP mode mounts one or more local HTTP upstreams behind one public URL: |
| 65 | |
| 66 | ```bash |
| 67 | portal expose --name myapp \ |
| 68 | --http-route /api=http://127.0.0.1:3001 \ |
| 69 | --http-route /=http://127.0.0.1:5173 |
| 70 | ``` |
| 71 | |
| 72 | This is not relay-side HTTP proxying. The relay still transports the connection. |
| 73 | The tunnel process receives the stream, parses HTTP, and runs the reverse proxy. |
| 74 | |
| 75 | Routed HTTP mode can: |
| 76 | |
| 77 | - match routes longest-prefix-first |
| 78 | - strip the mounted prefix before proxying |
| 79 | - forward `X-Forwarded-*` |
| 80 | - rewrite matching upstream `Location` redirects |
| 81 | - strip loopback cookie domains |
| 82 | - remap cookie paths to route prefixes |
| 83 | |
| 84 | Because HTTP is parsed in the tunnel process, this is the right place for |
| 85 | cooperative HTTP policy such as response headers. It is not a relay-enforced |
| 86 | policy boundary. |
| 87 | |
| 88 | Paid routes are also owned by routed HTTP mode. Add `--x402-pay-to` and attach |
| 89 | the amount to the HTTP route: |
| 90 | |
| 91 | ```bash |
| 92 | portal expose --name paid-app \ |
| 93 | --http-route "/paid=http://127.0.0.1:3001 GET:0.01" \ |
| 94 | --http-route /=http://127.0.0.1:5173 \ |
| 95 | --x402-pay-to 0x... |
| 96 | ``` |
| 97 | |
| 98 | The tunnel serves `/x402/client.js` and `/x402/prepare` on the same public |
| 99 | origin. A browser frontend mounted through the tunnel can import |
| 100 | `/x402/client.js` and call `x402Fetch()` from its own UI, so the Sui wallet flow |
| 101 | stays in the app instead of requiring a separate payment redirect. Native |
| 102 | clients use `/x402/prepare` directly and send the signed payload as |
| 103 | `X-PAYMENT`. The tunnel still verifies and settles the payment before proxying |
| 104 | the protected request. Paid routes use Sui mainnet by default; add |
| 105 | `--x402-testnet` for Sui testnet. |
| 106 | |
| 107 | ## Dedicated Raw TCP |
| 108 | |
| 109 | Use raw TCP when clients need a public TCP port instead of a public HTTPS |
| 110 | hostname: |
| 111 | |
| 112 | ```bash |
| 113 | portal expose localhost:25565 --name minecraft --tcp |
| 114 | ``` |
| 115 | |
| 116 | The relay allocates a port from its configured range and bridges raw TCP to the |
| 117 | tunnel process. This is useful for Minecraft, game servers, and custom TCP |
| 118 | protocols. The raw TCP path does not add TLS; use protocol-level encryption when |
| 119 | needed. |
| 120 | |
| 121 | ## UDP Relay |
| 122 | |
| 123 | Use UDP mode for datagram protocols: |
| 124 | |
| 125 | ```bash |
| 126 | portal expose localhost:8080 --udp --udp-addr localhost:19132 |
| 127 | ``` |
| 128 | |
| 129 | The relay allocates a UDP port and carries datagrams over the tunnel backhaul to |
| 130 | the local UDP target. The positional target is still used for stream traffic; |
| 131 | `--udp-addr` selects the local UDP service. |
| 132 | |
| 133 | ## Multi-Relay And Multi-Hop |
| 134 | |
| 135 | With discovery enabled, Portal starts from the public registry plus explicit |
| 136 | relays, then expands through relay discovery. Explicit relays are always kept |
| 137 | connected separately from the auto-selected relay pool. |
| 138 | |
| 139 | Use a fixed ordered route: |
| 140 | |
| 141 | ```bash |
| 142 | portal expose 3000 --multi-hop https://entry.example.com,https://exit.example.com |
| 143 | ``` |
| 144 | |
| 145 | Or ask Portal to choose one route of a given depth: |
| 146 | |
| 147 | ```bash |
| 148 | portal expose 3000 --multi-hop-depth 3 |
| 149 | ``` |
| 150 | |
| 151 | Multi-hop currently applies to the default SNI TLS stream transport. It is not |
| 152 | combined with UDP or dedicated raw TCP port mode. |
| 153 | |
| 154 | ## MITM Self-Probe |
| 155 | |
| 156 | Portal runs a TLS passthrough self-probe after real stream traffic starts: |
| 157 | |
| 158 | 1. The tunnel opens a client connection to its own public URL. |
| 159 | 2. The tunnel also receives that connection as the tenant TLS server. |
| 160 | 3. Both controlled ends export TLS keying material. |
| 161 | 4. Matching exporter values indicate passthrough for that sampled connection. |
| 162 | 5. A mismatch is treated as suspected relay-side TLS termination. |
| 163 | |
| 164 | By default, `portal expose` logs self-probe detections without banning the relay. |
| 165 | Use `--ban-mitm` when suspected TLS termination should ban the relay. |
| 166 | |
| 167 | The probe is a detection signal, not a mathematical proof for every future |
| 168 | connection. It raises the cost of relay-side termination while preserving the |
| 169 | transport model. |
| 170 | |
| 171 | ## Identity And Lease Authentication |
| 172 | |
| 173 | On first run, Portal creates a local secp256k1 identity at `identity.json` unless |
| 174 | you pass another `--identity-path`. |
| 175 | |
| 176 | Lease registration uses challenge signing. After registration, the relay issues |
| 177 | a lease-scoped access token used for renew, unregister, reverse connect, and |
| 178 | datagram authentication. |
| 179 | |
| 180 | Reusing the same identity path keeps the same tunnel identity across runs. |
| 181 | |
| 182 | Relay admin token login and optional browser wallet login for local agent status |
| 183 | are both separate from tunnel registration. See |
| 184 | [Wallet and ENS](/wallet-and-ens) for the distinction. |
| 185 | |
| 186 | ## Domain Boundary |
| 187 | |
| 188 | The default stream path prevents the relay from safely injecting `robots.txt`, |
| 189 | `noindex`, or arbitrary HTTP headers into user responses. That is a feature of |
| 190 | the trust model, but it also means public multi-tenant relays should use a |
| 191 | separate wildcard tunnel domain instead of a brand or docs domain. |
| 192 | |
| 193 | ## Next Steps |
| 194 | |
| 195 | - [Getting Started](/getting-started): run your first tunnel |
| 196 | - [Portal Agent](/portal-agent): run durable tunnel configs |
| 197 | - [CLI Reference](/cli-reference): command and flag details |
| 198 | - [Architecture](/architecture): protocol-level design notes |