Serving Localhost Ports Over Skywire¶
Expose local TCP ports over the Skywire network (skynet and/or DMSG). Remote visors can connect via skynet routes or DMSG.
This is the generic recipe for running any app over skynet/dmsg — no public IP, no DNS, no CA. Two halves:
- Far end — the host running the app forwards its port (
serve add), making it reachable at<base32-pk>.dmsg/.skynet. For HTTP you can forward straight to the app (built-in reverse-proxy) or via a real reverse proxy like Caddy for multiple vhosted sites — see below. - Near end — the client reaches it with
skywire cli got, or points a browser at the resolving proxy and visits the.dmsg/.skynethostname.
As long as the far end has forwarded the port, the near end just needs the resolving proxy and the hostname.
The CLI tree lives under skywire cli serve. The legacy
skywire cli skynet port {add,ls,rm} commands still work but are
deprecated.
Managing Served Ports¶
Add a port¶
Flags:
- --to <host:port|port> — local target (127.0.0.1:9883, or just
9883 → localhost:9883). Required for non-default forwarding.
- -l, --label — label shown on the visor landing page
- -d, --desc — description on the landing page
- --skynet / --dmsg — expose over each network face (default: both)
- --landing — show a link on the visor landing page (default: true)
- --whitelist — comma-separated PKs allowed to access the port
(empty = allow all peers)
Examples:
# Forward port 8080 with defaults (skynet + DMSG, landing-page link)
skywire cli serve add 8080 --to 8080
# Same thing with metadata
skywire cli serve add 8080 --to 8080 --label "My App" --desc "Web dashboard"
# Skynet only
skywire cli serve add 3000 --to 3000 --dmsg=false
# Replace the landing page on the bare name with your own site (vhosts,
# below, serve other names on port 80)
skywire cli serve add 80 --to 127.0.0.1:3000 --label "My Website"
--to accepts either host:port (only 127.0.0.1 / localhost for raw
TCP forwarding) or a bare port. Port 80 wires up an HTTP reverse-proxy
through the dmsghttp logserver; every other port is raw TCP.
Remove a port¶
List served ports¶
Output columns: PORT, TO, LABEL, SKYNET, DMSG, LANDING,
DESCRIPTION.
Hosting a Website¶
To serve a static website on your visor's port 80:
# Start a local file server (the static-file helper)
skywire cli util serve /path/to/site
# Output: Serving /path/to/site on http://127.0.0.1:43210
# Reverse-proxy port 80 to it
skywire cli serve add 80 --to 127.0.0.1:43210 --label "My Site"
The visor's system endpoints (/health, /node-info, /services) are
always reachable — the website only replaces unmatched routes.
Note (WebSockets): the port-80 reverse-proxy serves plain HTTP requests via the dmsghttp logserver. WebSocket upgrades are not currently fully supported on port 80; if your app needs WebSockets, serve it on its native port (e.g.
serve add 8085 --to 8085) and let clients connect over.skynet:8085/.dmsg:8085directly.
Without a reverse proxy — one app, no extra software¶
The example above is the zero-dependency path: the visor has a built-in
port-80 HTTP reverse-proxy, so a single site needs nothing but the one
serve add command. The visor forwards port-80 HTTP straight to your local
app. Good for a single dashboard, API, or static site.
Many sites by name: vhosts¶
The visor's port 80 picks a site by the name it was asked for. The resolving
proxy turns http://example.com.<label>.skynet/ into a request for
example.com, so one visor can serve many named sites. The bare
<label>.skynet is the visor's own name and keeps its landing page and its
routes (/health, /ping, /services, /node-info).
A vhost serves one name from a local service, with no reverse proxy needed:
skywire cli serve vhost add status --to 8092
skywire cli serve vhost add blog --to 127.0.0.1:4000 --alias blog.example.org
The vhost named * takes every name no other vhost has. Point it at Caddy or
nginx and that server chooses the site by Host, as it does on clearnet:
skywire cli serve vhost lists them and serve vhost rm <name> removes one.
Your own landing page¶
The bare name's page is set apart from the vhosts. serve add 80 replaces the
landing page with a local site of your own, and the vhosts stay as they are:
The site can be anything local, including the same Caddy on a second port:
The visor's routes still answer on the bare name. serve rm 80 brings the
default landing page back and keeps the vhosts.
Older setups sent port 80 straight to Caddy with serve add 80 --to
127.0.0.1:80 --preserve-host. That still works, but Caddy then also gets the
bare name, so the landing page is gone. To move to a catch-all vhost:
HTTPS and per-PK auth → "websites over skywire"¶
That's a whole topic of its own — websites over skywire — covered in depth elsewhere so it isn't re-explained here:
- PK-aware websites over skynet / dmsg —
the Caddy config, serving on skynet + clearnet at once,
inject_pk, and the trust boundary. (node.skycoin.comserves its whole stack — node API, explorer, blog — from one visor this way.) - Resolver TLS mode — HTTPS for
.dmsg/.skynetbrowser requests, and the base32 hostname format.
Rule of thumb: one HTTP app → the built-in proxy above; a few named
sites → vhosts; many sites / HTTPS / auth → a reverse proxy behind the *
vhost and those two guides.
Access Control¶
Served ports support a PK whitelist. When set, only visors with listed public keys can access the port. Empty whitelist = open to all authenticated peers.
Port 80 has three tiers of access control:
- /health, /services — open to everyone
- /node-info, /visor.log, /debug/pprof — survey whitelist
- Website (everything else) — the served port's --whitelist
Reaching a Served App¶
Once a port is served, any peer visor can reach it. There's nothing app-specific about this — it's the same for a website, an API, a database, or SSH.
From the CLI¶
skywire cli got is a unified HTTP client that speaks http://,
https://, skynet:// and dmsg://. The skywire schemes are
routed through the local visor's RPC.
skywire cli got skynet://<public-key>/path
skywire cli got skynet://<public-key>:<port>/path
skywire cli got dmsg://<public-key>:<port>/path
skywire cli got req POST skynet://<public-key>/endpoint -D '{"key":"val"}'
skywire cli got dl skynet://<public-key>/large-file -o output.file
From a browser (or any app) — the resolving proxy¶
Run the resolving proxy and point the browser (or any SOCKS-aware app) at
it; then use a readable mesh hostname and browse .dmsg / .skynet
domains directly — no IP, no DNS, no CA:
# A resolving SOCKS5 proxy that dials <pk>.dmsg / <pk>.skynet over the mesh.
skywire dmsg web
# Point the browser at it, e.g. socks5h://127.0.0.1:<port> (socks5h so the
# PROXY resolves the hostname — the browser must not try to). Then visit:
# http://<base32-pk>.dmsg/ (a bare visor)
# http://<name>.<base32-pk>.dmsg/ (a vhost behind a reverse proxy)
The <base32-pk> label is the DNS-safe form of a visor's public key:
skywire cli visor pk dnslabel <hex-pk>
# → aong2hr4en7v6bnxr3az5hzruad7qsbv27xr5ajioyicfaor3n2mc
# so node.skycoin.com is reachable at
# node.skycoin.com.aong2hr4en7v6bnxr3az5hzruad7qsbv27xr5ajioyicfaor3n2mc.dmsg
The wasm-visor's built-in iframe browser does this in-tab (see the skynet browser); a native desktop browser uses the standalone resolving proxy above. For HTTPS over the mesh, see Resolver TLS mode.
Persistence¶
Served-port configuration lives in local/forwarded_ports.json and
persists across visor restarts. It is separate from the visor config
file.
RPC Integration¶
Applications can manage served ports programmatically: