The real-origin mesh browser¶
The visor's in-app browser (both the native hypervisor UI and the in-browser
WASM visor) can open a .dmsg / .skynet site — or a clearnet site over
skysocks — as a normal, isolated, secure web page: its own origin, its own
cookies / localStorage / service worker / secure context, native subresource
loading, redirects, streaming and WASM. The visor only proxies the transport;
the browser does everything else, exactly as it would for any website.
This document explains what "real origin" means here, the two different ways a mesh site can become a real browser origin, how the zero-config in-app browser achieves it, and — because it was not obvious — how the origin naming scheme evolved from encoding the target into the hostname to hashing it.
Two ways to get a real origin (they are not the same thing)¶
There are two distinct mechanisms, and it is easy to conflate them:
1. The SOCKS5 resolving proxy (dmsgweb) — needs browser configuration¶
If you point your browser's SOCKS5 proxy at the visor's resolving proxy, the
browser itself resolves http://<pk>.dmsg (and <pk>.skynet, aliases,
name-vhosts, clearnet-over-skysocks) through the proxy → the visor → the mesh.
In this mode <pk>.dmsg is a genuine, distinct browser origin with real
cookies/storage — no extra machinery required.
The cost: every user must configure their browser's proxy settings. That is fine for an operator, but not for someone who just opens a hosted page.
2. The in-app iframe browser — zero configuration¶
The visor's built-in browser (the WinBox "browser" window) is for users who have
not configured a proxy — most importantly, a visitor who just opens a hosted
wasm visor like theskywirenetwork.net. That browser cannot point an iframe at
http://<pk>.dmsg, because the visitor's browser has no way to resolve .dmsg.
Historically it worked around this by fetching the page with the visor and
injecting the HTML into a sandboxed srcdoc iframe — which has an opaque
origin: no persistent cookies/storage, no service workers, logins do not survive
a reload. Correct isolation, broken platform.
The real-origin browser replaces that: it gives the iframe a genuine, per-site origin without any proxy configuration, so the browser does native cookie/storage/SW/subresource handling. The rest of this document is about that mechanism.
TL;DR — if you have the SOCKS proxy configured,
.dmsg/.skynetalready work as real origins and none of this is needed. The real-origin browser exists for the zero-config, in-page case (the hosted PWA).
How the zero-config real origin works¶
Each mesh site is served from its own isolated browse origin B
(<id>.<browse-domain>), separate from the visor app origin V (the page
that holds the in-tab visor and its key). Because B ≠ V (different host), the
browser treats them as cross-origin: a mesh site can never read the visor's
storage or key.
- Native HV UI:
Bis a loopback reverse-proxy origin on the operator's own machine (pkg/visor/meshproxy.go) — content flows through their transports. - WASM visor (hosted):
Bis served by a tiny bootstrap. The server hands the browser only a static Service-Worker bootstrap (browse-bootstrap.html browse-sw.js). The SW then becomesB's network layer: it relays every fetch through apostMessagebridge toV's first-party in-tab visor, which fetches over dmsg/skynet/skysocks. The host never serves content — only the bootstrap.Vholds the key;Bmay only ask "fetch this URL".
The trust boundary is browse-responder.js, injected into V: it validates the
B origin, hands B a private MessagePort, and services fetches via V's own
visor. B never touches the key.
Networks¶
The target network is explicit in the address you type, and the visor fetches accordingly:
| You type | Network | How it's fetched |
|---|---|---|
<pk>.dmsg, home.dmsg, <vhost>.<pk>.dmsg |
dmsg | fetchDmsg (resolving proxy) |
<pk>.skynet |
skynet | fetchDmsg (skynet route) |
https://skycoin.com |
clearnet | fetchClearnet via skysocks-lite (anonymous exit) |
A public key may be given in the 66-char hex or the 53-char base32 form; both resolve to the same site.
Origin naming — why it ends in a hash, and the road not taken¶
Giving B a real origin over HTTPS requires a TLS certificate for B's
hostname. Issuing a cert per site does not scale (CA rate limits), so we want a
single wildcard cert to cover every site. Getting there took a wrong turn
worth recording.
First attempt: encode the target into the hostname¶
The obvious scheme was to put the target in the name:
This works for a bare public key. But it runs straight into how TLS wildcards are defined:
- A wildcard certificate matches exactly one label, and only as the left-most label (RFC 6125 §6.4.3; CA/Browser Forum Baseline Requirements).
*.*.<domain>is not a valid certificate — no CA issues it and no browser accepts it. A*never spans a dot.
So any multi-label target has no cert:
- a clearnet subdomain —
explorer.skycoin.com→explorer.skycoin.com.skysocks.<domain>; - a dmsg name-vhost —
magnetosphere.net.<base32pk>.dmsg, where the 53-char base32 PK already nearly fills the 63-char DNS-label limit, sovhost + PKcannot even be collapsed into a single label.
Dead ends we considered and rejected:
- Per-depth wildcards (
*.dmsg,*.skysocks,*.com.skysocks, …): unbounded for clearnet — you would need one wildcard per distinct prefix path, forever. - On-demand per-host TLS: readable, but issues a new certificate per site on first visit → Let's Encrypt rate limits (~50/week per registered domain) break under normal browsing.
- Hyphen-escaping the dots into one label (
skycoin-com, real-→--): reversible and readable, but avhost + 53-char base32 PKstill blows past the 63-char label cap. Handles clearnet, not name-vhosts.
The fix: content-addressed origins¶
Stop encoding the target in the name at all. Make B's origin a short stable
hash of the target, and keep the map in the visor:
- The visor keeps
id → target(globalThis.__meshOrigins), populated when the browser navigates.B's bootstrap sends itsidat handshake;V's responder looks up the target and binds it to the port — an untrustedBcannot ask for a different site's content. - The
idis a deterministic hash, so the same site always gets the same origin → per-site cookies/storage persist across sessions. Hex and base32 PK inputs canonicalize to the sameid. - One wildcard (
*.<browse-domain>) now covers every site — any PK length, any clearnet subdomain depth, any dmsg name-vhost — because the label no longer scales with the target.
The trade-off is that the origin is opaque (k3f9x2q7m1a8b4c0zzzz.<domain>), not
skycoin.com…. That is purely cosmetic: the browser's address bar shows the
true URL (https://skycoin.com, https://magnetosphere.net); the hash is an
internal handle the user never types or sees.
Hosted deployment¶
V (the wasm-visor PWA) and B (the browse origins) must be different
registrable domains so untrusted mesh content is fully cross-site from the
visor app (no shared cookies/storage). For example V = theskywirenetwork.net,
B = *.<browse-domain> on a separate eTLD+1.
A single process serves both, on two ports, behind one reverse proxy:
Caddy: V-domain → 127.0.0.1:7999 # the visor PWA + browse-responder.js
*.<browse-domain> → 127.0.0.1:7998 # the browse-origin bootstrap (static)
skywire cli hv serve \
--addr 127.0.0.1:7999 \
--browse-origin 127.0.0.1:7998 \
--browse-suffix .<browse-domain> \
--v-origin https://<V-domain>
or via the visor config (hypervisor.wasm_serve):
"wasm_serve": {
"addr": ":7999",
"browse_origin_addr": "127.0.0.1:7998",
"browse_suffix": ".<browse-domain>",
"browse_v_origin": "https://<V-domain>"
}
Sharing one browse domain between apps¶
browse_v_origin (and --v-origin) names the app origins a browse origin
will accept content from. It is the only thing deciding who may feed a browse
origin, since the bootstrap serves a static shell and the content arrives from
whichever parent completes the handshake.
"browse_v_origin": "https://<V-domain>" one app
"browse_v_origin": "https://<V-domain>,https://<other-app>" several
"browse_v_origin": "*" any parent
A second app on a different registrable domain — a store whose in-page server
wants its own pages rendered natively, say — either gets added to the list or
the deployment is opened with *.
* is a decision rather than a shortcut. An uninvited page cannot reach a
visitor's browsing state: third-party storage partitioning keys storage by
(top-level site, frame origin), so a page embedding a browse origin gets its
own empty partition rather than the one the visor filled. What it can do is
serve its own content from a subdomain of your browse domain, which makes the
domain's reputation everyone's to spend. Open a browse domain you are willing
to share; list origins on one you are not.
The *.<browse-domain> wildcard needs a wildcard TLS cert, which requires the
DNS-01 ACME challenge (HTTP-01 cannot issue wildcards). With Caddy and the
caddy-dns/cloudflare provider that is a few lines; one wildcard covers every
content-addressed origin.
The bootstrap server never proxies content — it serves only the embedded
browse-bootstrap.html + browse-sw.js; the SW relays all fetches to the
visitor's own in-tab visor.
See also¶
pkg/wasmhv/browseui/RFC-real-origin-browser.md— the design RFC.pkg/visor/meshproxy.go— the native reverse-proxy origin.pkg/wasmhv/browse-{bootstrap.html,sw.js,responder.js}— the hosted bridge.docs/deploy/standalone-wasm-visor/— deploy recipe.