PORTAL v2

CLI Reference

The portal CLI exposes local services through Portal relay servers. The relay provides transport and routing. The tunnel process decides whether a connection is handled as the default HTTPS stream, routed HTTP, raw TCP, or UDP.

Install

macOS / Linux

curl -fsSL https://github.com/gosuda/portal-tunnel/releases/latest/download/install.sh | bash

Windows PowerShell

$ProgressPreference = 'SilentlyContinue'
irm https://github.com/gosuda/portal-tunnel/releases/latest/download/install.ps1 | iex

From A Relay

If your relay publishes its own installer:

curl -sSL https://portal.example.com/api/install.sh | bash

The installer downloads the portal binary and adds it to your PATH. It does not create a config file.

Command Overview

CommandPurpose
portal exposeExpose one local service or one routed HTTP bundle
portal auth issueIssue a Portal-native application access credential
portal listPrint relay URLs resolved for this invocation
portal agentRun a durable local multi-tunnel agent
portal updateReplace the CLI with the latest release
portal versionPrint the current version

portal expose

Expose a local service:

portal expose [flags] <target>

Or run routed HTTP mode:

portal expose [flags] --http-route "PATH=UPSTREAM [METHOD[,METHOD...]:PAYMENT_AMOUNT]" [...]

The payment suffix is optional; omit it for free routes.

Target Formats

FormatExampleResolves to
Bare port3000127.0.0.1:3000
Host and portlocalhost:8080localhost:8080
URL hosthttp://127.0.0.1:3000127.0.0.1:3000

URL inputs are accepted for address parsing. Paths, queries, and fragments are not supported.

Mode Selection

ModeExampleNotes
Default HTTPS streamportal expose 3000Relay routes by SNI; tunnel process terminates tenant TLS
Static siteportal expose --serve ./distServes a directory or an HTML file with SPA fallback
Routed HTTPportal expose --http-route /api=3001 --http-route /=5173Tunnel process runs the HTTP reverse proxy
Dedicated raw TCPportal expose localhost:25565 --tcpRelay allocates a public TCP port
UDP relayportal expose 8080 --udp --udp-addr 19132Relay allocates a public UDP port

Flags

FlagTypeDefaultDescription
--relaysstringregistryAdditional relay API URLs, comma-separated
--discoverybooltrueInclude registry relays and relay discovery expansion
--max-active-relaysint3Maximum auto-selected relays to keep connected; explicit relays are always included
--overlayboolfalsePrefer an IVNP overlay path when available; retains direct fallback
--ban-mitmboolfalseBan relay when the MITM self-probe detects TLS termination
--identity-pathstringidentity.jsonIdentity JSON file path; created automatically when missing
--identity-jsonstringIn-memory identity JSON; takes precedence over --identity-path without reading or writing that file
--namestringautoPublic hostname prefix, normalized to one DNS label of at most 22 ASCII characters
--descriptionstringService description metadata
--tagsstringService tags metadata, comma-separated
--thumbnailstringService thumbnail URL metadata
--ownerstringService owner metadata
--hideboolfalseHide service from relay listing screens
--authstringProtect HTTP application access with siwe or credential; cannot be combined with --cache
--auth-allowstringEthereum wallet allowed to sign in; repeat for multiple wallets (empty allows any wallet); requires --auth siwe
--auth-identity-headersboolfalseSend authenticated X-Portal-User and X-Portal-Auth headers to HTTP upstreams; requires --auth
--x402-pay-tostringPayment recipient address for this tunnel
--x402-testnetboolfalseUse Sui testnet when --x402-network is omitted
--x402-networkstringOptional Sui or Casper CAIP-2 network
--x402-assetstringwCSPR CEP-18 contract hash required by Casper
--x402-endpointstringOptional Sui RPC or Casper facilitator endpoint; repeatable
--x402-facilitator-tokenstringCSPR_CLOUD_API_KEYCasper facilitator authorization token; prefer the environment variable so the secret is not exposed in the process arguments
--http-routestringHTTP route mapping in PATH=UPSTREAM [METHOD[,METHOD...]:PAYMENT_AMOUNT] form; repeatable; route amounts require --x402-pay-to
--strip-request-headerstringClient request header removed before routed HTTP forwards it upstream; repeatable; case-insensitive; applies to HTTP and WebSocket upgrades; Portal-owned headers (Host, X-Forwarded-*) are always rewritten after stripping; requires --http-route or --auth
--servestringServe a local directory or HTML file; unknown paths fall back to the entry HTML
--cacheboolfalseOpt in to relay storage and browser TLS termination for --serve
--cache-ttlduration0Requested offline cache lifetime; 0 uses relay policy; requires --cache
--tcpboolfalseRequest a dedicated raw TCP port on the relay
--udpboolfalseEnable public UDP relay in addition to the default stream path
--udp-addrstringLocal UDP target; defaults to the primary target when --udp is enabled
--metrics-addrstringOptional host:port for Prometheus /metrics

IVNP-backed overlay networking

portal expose 3000 --overlay

This asks the relay to prefer an available overlay gateway for reverse streams. Portal selects and authorizes the endpoints; IVNP owns the gateway-to-ingress path. Direct reverse transport remains the default and fallback. See the overlay networking concepts for why endpoint policy and network routing are separate, and the architecture for the protocol.

Identity Names

An existing identity file or --identity-json supplies the saved name as well as the key. --name applies only when creating a new identity; it does not rename an existing one. Use a separate --identity-path for a new identity.

Constraints

  • Choose one of <target>, --serve, or --http-route.
  • --serve cannot be combined with --tcp or --udp.
  • --cache requires --serve and cannot be combined with --ban-mitm.
  • --cache-ttl requires --cache; a nonzero value must be between 1s and 8760h and is clamped by the relay.
  • --http-route cannot be combined with --tcp or --udp.
  • --strip-request-header requires --http-route or --auth; it applies to every routed HTTP upstream, including WebSocket upgrades, and Portal-owned headers (Host, X-Forwarded-*) cannot be removed by it.
  • --tcp and --udp require matching transport support on the relay.
  • Route payment amounts are part of --http-route and require a tunnel-owned --x402-pay-to.
  • Tunnel paid routes use Sui mainnet by default. Casper requires an explicit --x402-network casper:... and --x402-asset; it uses the hosted facilitator by default or the first --x402-endpoint override. The default CSPR.cloud facilitator also requires CSPR_CLOUD_API_KEY.

Examples

Expose a local web app:

portal expose 3000

Protect the app with tunnel-local SIWE login:

portal expose 3000 --auth siwe
# Restrict login and pass the verified identity to the upstream.
portal expose 3000 --auth siwe --auth-allow 0x1234... --auth-identity-headers

Protect it without requiring a browser wallet, then issue a host-scoped credential from the tunnel identity:

portal expose 3000 --auth credential
portal auth issue myapp.example.com --subject alice --expires 720h

Portal protects the complete HTTP gateway, including routed, static, and x402 paths. It always strips inbound Portal identity headers. The auth gate cannot be combined with relay cache mode or raw TCP/UDP exposure.

Use a custom name and relay:

portal expose localhost:8080 
  --name myapp 
  --relays https://portal.example.com 
  --discovery=false 
  --description "My web application" 
  --tags webapp,demo

Run routed HTTP mode:

portal expose --name myapp 
  --http-route /api=http://127.0.0.1:3001 
  --http-route /=http://127.0.0.1:5173

Route matching is longest-prefix-first. /api matches /api/* and strips the /api prefix before proxying to the upstream.

Expose a Minecraft server:

portal expose localhost:25565 --name minecraft --tcp

Enable UDP alongside the default stream target:

portal expose localhost:8080 --udp --udp-addr localhost:19132 --name game

Ban relays on MITM probe detection:

portal expose 3000 --ban-mitm

Publish a paid HTTP route:

portal expose --name paid-app 
  --http-route "/paid=http://127.0.0.1:3001 GET:0.01" 
  --http-route /=http://127.0.0.1:5173 
  --x402-pay-to 0x...

The optional method list limits which methods require payment; without it, every method on that route prefix is paid.

The routed HTTP handler also serves /x402/client.js and /x402/prepare on the public tunnel origin. Frontends served by one of the routes can use the shared browser-only Sui wallet client for an in-page payment flow:

import { getSuiWallets, x402Fetch } from '/x402/client.js';

const [wallet] = getSuiWallets();
if (!wallet) {
  throw new Error('Install a Sui wallet');
}

const [account] = await wallet.accounts();
if (!account) {
  throw new Error('Connect a Sui account');
}

const response = await x402Fetch('/paid/photo', { method: 'GET' }, {
  wallet,
  account,
  onEvent: (event) => console.log(event.type, event.message),
});

x402Fetch() is a convenience wrapper: it asks /x402/prepare for the payment transaction, asks the wallet to sign it, then retries the protected request with an X-PAYMENT header. onEvent receives structured progress events; the older onStatus(message) callback is still accepted for simple UIs. Routed HTTP payments use Sui mainnet by default; pass --x402-testnet when exposing the tunnel and use network: 'sui:testnet' in wallet clients that need an explicit network. For mainnet, omit network or pass sui:mainnet.

Native clients should not load /x402/client.js. Call POST /x402/prepare with { "sender": "...", "method": "GET", "path": "/paid/photo" }, execute prepareTransaction.transaction first when present, sign paymentTransaction.transaction, and send the resulting x402 payload as the X-PAYMENT header on the protected request:

const payload = {
  x402Version: prepared.x402Version,
  payload: {
    signature,
    transaction: prepared.paymentTransaction.transaction,
  },
  accepted: prepared.paymentRequirements,
  resource: prepared.resource,
};
const header = base64(JSON.stringify(payload));

The frontend integration is optional. Requests without a valid X-PAYMENT header still receive x402 payment-required responses from the tunnel.

Serve A Static Site

portal expose --serve ./dist
# An HTML file serves its containing folder with that file as the SPA entry.
portal expose --serve ./site/index.html

Unknown paths fall back to the entry HTML file. Keep private files outside the served directory. Path traversal (..) is refused, but symlinks inside the folder that point outside it are followed, so only serve folders you trust. Static serving is available in both expose and agent TOML; the agent format supports serve but not relay cache options (cache or cache_ttl).

To opt in to storage and TLS termination at one selected relay:

portal expose --serve ./dist --cache --cache-ttl 1h 
  --relays https://gosunuts.xyz --discovery=false

The selected relay must advertise cache support and admit the snapshot. Otherwise the live origin tunnel remains in use. A cached connection trusts the relay with the files and HTTP traffic, even if it falls back to the live origin. Only the identity-bound canonical hostname is cached; the friendly hostname continues to require a live origin. The requested TTL is an upper request subject to relay policy, not a hosting guarantee; cached files may be evicted or lost on relay restart. See cache configuration and the TLS boundary.

portal auth issue

Issue a host-scoped credential using an existing tunnel identity:

portal auth issue [flags] <host>
portal auth issue myapp.example.com --subject alice --expires 720h
FlagTypeDefaultDescription
--subjectstringrequiredSubject placed in the credential and optional upstream identity header
--expiresduration720hCredential lifetime
--identity-pathstringidentity.jsonExisting tunnel identity; no identity is created by this command
--identity-jsonstringIn-memory existing tunnel identity; IDENTITY_JSON environment variable; takes precedence over the path

The command prints a bearer credential and an HTTPS redeem URL. Use it with a tunnel started via portal expose ... --auth credential. API clients can send the credential in X-Portal-Access-Credential; Portal validates and removes that header before forwarding the request upstream.

portal list

Print relay URLs resolved for the current invocation:

portal list [flags]
FlagTypeDefaultDescription
--relaysstringregistryAdditional relay URLs
--default-relaysbooltrueInclude public registry relays

portal list does not run the runtime relay discovery expansion loop. It only resolves the registry seed list plus explicit relay URLs.

portal agent

Run a durable local agent that owns multiple tunnels from one config file:

portal agent run
portal agent dashboard
portal agent stop
portal agent restart
CommandDescription
portal agent runInstall or update and start the managed agent service
portal agent run --config config.toml --foregroundRun the agent in the current terminal
portal agent dashboardOpen the local TUI for tunnels, relays, and settings
portal agent stopGracefully stop the agent and disable or stop the OS service
portal agent restartStop the current agent if present, install or update the service, and start it again

The local control API binds only to loopback and uses a token in the agent state directory. See Portal Agent for the workflow and Configuration Reference for the config.toml format.

Agent flags:

CommandFlagDefaultDescription
portal agent run--configplatform defaultAgent TOML config path
portal agent run--foregroundfalseRun in the current process without installing the OS service
portal agent run--servicefalseInternal service entrypoint used by the installed OS service
portal agent dashboard--configplatform defaultConfig path used for display and state-dir discovery
portal agent dashboard--state-dirconfig/defaultAgent state directory to attach to
portal agent stop--configplatform defaultConfig path used to resolve state dir and service name
portal agent stop--state-dirconfig/defaultAgent state directory to stop
portal agent restart--configplatform defaultConfig path used to reinstall and restart the service

portal update

Update the CLI binary:

portal update

The updater resolves the latest GitHub release, compares it with the installed version, downloads the matching asset, verifies its SHA256 checksum, and replaces the current executable.

portal version

portal version

Prints the installed version string and exits.

Behavior Notes

  • portal expose and portal list check the latest published GitHub Release in the background. A main merge or branch artifact is not offered to installed clients until the release is created with matching binary and checksum assets.
  • portal expose loads or creates a signing identity at identity.json or --identity-path.
  • Multiple relay URLs are registered independently. A failed relay does not stop healthy relays from serving.
  • With discovery enabled, the tunnel consumes relay /discovery results and reconciles its relay pool.
  • MITM self-probing logs suspected termination by default; relay banning requires --ban-mitm.
  • When the local stream target is unreachable, the tunnel returns an HTTP 503 page to browser-style clients.
  • Routed HTTP mode is HTTP-only and runs inside the tunnel process.
  • Routed HTTP mode keeps the browser’s Host for every upstream and sends X-Forwarded-Proto: https; a local dev server that checks Host (such as Vite’s server.allowedHosts) must allow the public hostname.
  • --tcp requires relay TCP port transport, a valid MIN_PORT/MAX_PORT range, and TCP port transport enabled in the admin panel.
  • --udp requires relay UDP transport, a valid MIN_PORT/MAX_PORT range, UDP enabled in the admin panel, and the public PORTAL_URL port reachable over UDP for the QUIC backhaul.
  • Bare portal [flags] is not accepted; use portal expose explicitly.
  • Runtime APP_*, RELAYS, and DEFAULT_RELAYS environment variable fallbacks are not used.

Next Steps