- Go 93.6%
- HTML 2.9%
- JavaScript 2.4%
- CSS 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| browser | ||
| test | ||
| wwc | ||
| .gitignore | ||
| api.go | ||
| browse.go | ||
| browse_daemon.go | ||
| config.go | ||
| domain.go | ||
| ghostd_test.go | ||
| go.mod | ||
| go.sum | ||
| host.go | ||
| main.go | ||
| rate.go | ||
| README.md | ||
| server.go | ||
gHOSTd — General Hosting Over Systematic Technology Directive
A Go daemon for hosting and browsing ghost:// domains over an encrypted P2P overlay network.
Replaces the centralized Node.js hub with a WWC-powered (World Web Connectivity) P2P daemon.
No port forwarding, no central server needed.
Build
ghostd is self-contained with its own go.mod and vendored wwc source in ghostd/wwc/.
Build from the repo root (requires go.work):
go build -o ghostd.exe ./ghostd/
Or build directly from ghostd/ (no repo context needed — copy this folder anywhere):
cd ghostd && go build -o ghostd.exe .
Cross-compile:
cd ghostd
GOOS=linux GOARCH=amd64 go build -o ghostd .
GOOS=darwin GOARCH=arm64 go build -o ghostd .
Quick Start
# Terminal 1: Bootstrap server (domain registry + relay)
./ghostd server
# Terminal 2: Host a domain
./ghostd host -server 127.0.0.1:9000 -domain wrld.dev -file index.html
# Terminal 3: Browse (CLI)
./ghostd browse wrld.dev
# Terminal 4: Browse (GUI browser)
cd ghostd/browser
npx electron .
Type wrld.dev in the browser URL bar and hit Enter. The host registers with the server;
the browser resolves and fetches content over an encrypted P2P relay.
How It Works
ghostd has three roles:
| Role | Command | Runs on | What it does |
|---|---|---|---|
| Server | ghostd server |
VPS (24/7) | Domain registry + relay + API |
| Host | ghostd host |
Your machine | Pushes content to server |
| Browser | ghostd browse or Electron |
Anyone | Fetches content from server |
Content flows: Host → Server → Browser
Commands
ghostd server
Starts a bootstrap node: WWC server + HTTP API with domain registry.
./ghostd server -api :7777 -wwc-addr :9000
Peer with other bootstrap servers for distribution:
./ghostd server -peer 10.0.0.2:7777 -peer 10.0.0.3:7777
Servers pull domain entries from peers every 15s. Synced entries include relay addresses and WWC server info, enabling cross-region P2P routing.
Authenticate peer sync between bootstrap servers with a shared HMAC key:
./ghostd server -sync-key "shared-secret" -peer 10.0.0.2:7777 -peer 10.0.0.3:7777
ghostd host
Registers a domain and serves content. Tries P2P relay first; falls back to server-side API.
Option 1: Static files
./ghostd host -server 127.0.0.1:9000 -domain wrld.dev -file index.html
./ghostd host -server 127.0.0.1:9000 -domain wrld.dev -html "<h1>Hello</h1>"
Option 2: Port proxy (live server)
# Your dev server is running on port 3000
npm run dev
# Proxy it through ghost
./ghostd host -domain myapp.dev -port 3000
The host polls your server every 5 seconds and pushes the latest content to the ghostd server. Works with React, Next.js, Go, Python, anything.
Port proxy across networks:
# Server running on a different machine
./ghostd host -domain myapp.dev -port 192.168.1.5:3000
./ghostd host -domain myapp.dev -port http://myserver.example.com:3000
Heartbeat to additional bootstrap servers so the domain stays alive across regions:
./ghostd host -server eu.example.com:9000 -api http://eu.example.com:7777 \
-peer http://us.example.com:7777 -peer http://asia.example.com:7777 \
-domain wrld.dev -file index.html -relay
ghostd browse
Resolves a domain and fetches content. Outputs HTML to stdout.
Warning:
ghostd host -file index.htmlautomatically scans the file's directory and pushes all sibling files (CSS, JS, images, etc.) as sub-resources. If your directory contains sensitive files (.env,.git, private keys, database files), they will be pushed to the server and served to anyone browsing your domain. Use-no-scanto disable directory scanning and only push the specified file.
./ghostd browse wrld.dev
./ghostd browse wrld.dev /about
The /resolve API response includes wwc_server — the browser auto-connects to the
correct WWC server for P2P relay, even when resolving through a different bootstrap node.
Example: Full Cross-Network Setup
California (VPS) New York (your machine) New Jersey (friend)
┌──────────────┐ ┌────────────────────┐ ┌────────────────┐
│ ghostd server│ │ npm run dev (:3000)│ │ Electron │
│ -api :7777 │◄─push────│ ghostd host -port │ │ browser │
│ -wwc-addr │ │ 3000 -domain │ │ browse │
│ │──serve──►│ myapp.dev │◄─fetch─│ myapp.dev │
└──────────────┘ └────────────────────┘ └────────────────┘
- Start
ghostd serveron your California VPS (runs 24/7) - In New York, start your Node/React/Go dev server on port 3000
- In New York, run:
ghostd host -domain myapp.dev -port 3000 -api http://california-vps:7777 - In New Jersey, open the Electron browser and type
myapp.dev
Your friend in New Jersey is now viewing your local dev server through the ghost network.
ghost-config.json
Auto-detected in the host directory, or specify with -config path/to/config.json.
Controls which files get pushed when scanning a directory. Uses glob patterns (*, ?, [...]).
{
"include": ["*.html", "*.css", "*.js", "*.png", "*.jpg", "images/*"],
"exclude": [".env", ".git", "*.secret", "node_modules", "*.bak"]
}
- include — if set, only matching files are pushed (empty = everything)
- exclude — matching files are never pushed (runs after include)
Flags
| Flag | Default | Description |
|---|---|---|
-server |
127.0.0.1:9000 |
Bootstrap WWC server address |
-api |
:7777 |
HTTP API listen address (server mode) |
-wwc-addr |
:9000 |
WWC server listen address (server mode) |
-key |
server_key.pem |
X25519 identity key file |
-domain |
— | Domain to host/resolve |
-file |
— | HTML file to host |
-html |
— | Inline HTML string |
-dir |
auto | Directory of assets to push (auto-detected from -file) |
-no-scan |
false | Disable automatic directory scanning (only push -file) |
-config |
auto | Path to ghost-config.json (include/exclude filters) |
-port |
— | Proxy a server (port, host:port, or full URL like http://1.2.3.4:3000) |
-path |
"" |
Sub-path for content |
-relay |
false | Serve content via P2P relay instead of HTTP push |
-peer |
— | Peer bootstrap API (repeatable, server sync + host heartbeat) |
-sync-key |
— | Shared HMAC secret for peer sync (protects /host/list and /host/sync) |
-insecure |
true | Disable server fingerprint pinning (-insecure=false or -secure to enable) |
-secure |
— | Re-enable fingerprint verification (shorthand for -insecure=false) |
-server-fingerprint |
— | Pin a specific server fingerprint (implies -insecure=false) |
-print-fingerprint |
— | Print server fingerprint on first connect, verify on subsequent |
-stealth |
— | Stealth mode: off, low, medium, high, max |
-auth-secret |
— | HMAC secret for handshake auth |
GUI Browser
The Electron browser is in ghostd/browser/. It spawns ghostd browse as a subprocess.
Features
- Dark/Light toggle (moon/sun button in nav bar, persists via localStorage)
- New tab with Google-like Ghost homepage and search bar
- Tab history per tab with back/forward (click or Ctrl+R to reload)
- Long-press back — hold the back button (~400ms) or right-click to see last 12 page titles in a dropdown
- Ctrl+H — opens history modal with search and per-entry delete
- Scrollable tabs — mouse wheel or click-drag when tabs overflow
- Iframe isolation — hosted pages render with their own CSS (no parent style leaking)
- Error pages — ghost icon + domain + clean error message, respects theme
Running
cd ghostd/browser
npm install
npx electron .
Known Limitations
- Capped history to 1000 entries — prevents localStorage bloat on long-running sessions.
- Sub-resource proxy cost (mitigated by daemon pool) — Browser sub-resources (CSS, JS,
images) hit a local HTTP proxy that first tries
/resolve(cached content, ~ms). On cache miss with relay-info JSON in the response, the proxy now reuses a long-runningghostd browse-daemonsubprocess per WWC server rather than spawning oneghostd browseper fetch. The daemon opens a single WWC connection plus per-domain keep-alive P2P relays and multiplexes framed requests over stdin/stdout, so a page with 50 sub-resources on one domain now costs 1 handshake + 50 HTTP round-trips rather than 50 handshakes + 50 round-trips. Daemon self-exits after 60s idle; proxy respawns on next use. An in-memory LRU (100 entries / 50 MB) still amortizes the cost across repeat navigations. Hosts that pre-push every sub-resource via separateghostd host -pathinvocations bypass the daemon entirely. Frame protocol and lifecycle are documented in the Browse daemon section below. - Iframe sandbox uses
allow-scripts allow-same-origin allow-forms— hosted pages run with parent-privileged access for form submissions, while click interception ofghost:///http(s):/// relative links is handled by a small inline shim we inject at the head of every HTML response (seemaybeInjectShiminbrowser/main.js); the parent listens forghost-nav/ghost-ext/ghost-relpostMessages whose source is verified againstview.contentWindow. - CSP-locked hosted pages — strict
script-srcdirectives on hosted HTML block our inline click-shim script. Pages with such policies can still be loaded butghost://link interception inside them is disabled (links resolve through normal browser navigation).
Keyboard shortcuts
| Shortcut | Action |
|---|---|
| Enter (in URL bar) | Navigate |
| Ctrl+H | Toggle history modal |
| Ctrl+R | Reload current page (prevents Electron window reload) |
| Escape | Close history modal / back dropdown |
How It Works (Technical)
- Encrypted transport via X25519 + AEAD-256-GCM (WireGuard primitives)
- Server maps domains to peers (30s TTL, heartbeat-renewed)
- Host heartbeats to primary server and all
-peerAPIs, keeping domains alive across regions - Browser resolves domain via any bootstrap API →
/resolvereturnsrelay_addr+wwc_server - Browser connects to the correct WWC server and opens a P2P relay to the host
- Content is served over an encrypted AEAD pipe (P2P direct or relay)
- NAT hole punching for direct UDP path when possible
- Bootstrap servers pull and push domain entries (incl. relay address + WWC server) to/from
peers every 15s. Wire format negotiation uses the
X-Ghostd-Sync-Protocolresponse header on/host/list; a v3 sender collapsing to legacy v1 format viaCollapseToV1is detectable in peer logs when needed.
Security
All WWC traffic is encrypted with AEAD-256-GCM (authenticated encryption)
with forward secrecy, regardless of the -insecure flag.
-insecure (default: on) only skips server X25519 key fingerprint
pinning on the initial handshake. This means:
| Scenario | MITM risk | Notes |
|---|---|---|
Localhost (127.0.0.1) |
None | No network path for interception |
| Trusted VPS | Near zero | Attacker needs network hop access |
| Public Wi-Fi → remote server | Low | Possible during first connect only; content is public anyway |
For production deployments, pin the server fingerprint explicitly:
# Print server fingerprint, then reconnect with it pinned
ghostd browse wrld.dev -insecure -print-fingerprint
ghostd browse wrld.dev -server-fingerprint abc123...
Or use the TOFU flow — first connect saves the fingerprint to
fingerprint.txt, subsequent connects verify against it automatically.
Tests
cd ghostd && go test -count=1 .
Covers domain registry, server API endpoints, sync protocol (list/import/collapse), browse flag parsing, frame protocol round-trips, config include/exclude patterns, and max body enforcement.
Browse daemon
ghostd browse-daemon is the long-running subprocess the Electron browser proxy
spawns (one per WWC server) to amortize handshake cost across many concurrent
sub-resource fetches. Lives in browse_daemon.go. The daemon opens a single WWC
connection to the bootstrap server, then creates per-domain keep-alive P2P
relays on demand. Requests come in via length-prefixed JSON frames on stdin;
responses come out via JSON header + raw-bytes body on stdout.
Frame protocol
Request (proxy → daemon):
[4 bytes LE uint32 headerLen]
[JSON header: {"id": <int>, "domain": "<host>", "path": "<path>"}]
// no body in v1 of the protocol
Response (daemon → proxy):
[4 bytes LE uint32 headerLen]
[JSON header: {"id": <int>, "error": "<msg>"?, "mime": "<mime>"?, "body_len": <int>?}]
[raw body bytes, length = header.body_len] // omitted if error or body_len=0
Maximum header size: 1 MiB (frameHeaderCap). Maximum body size: 100 MiB
(bodyCap). Chunked transfer-encoding is not currently supported — the host's
serveViaRelay must include Content-Length.
Lifecycle
- One WWC client + one wwc connection per daemon.
- A request for
domain=Xallocates arelayWorkerkeyed byX. The worker callswwc.OpenP2PRelay(X)lazily on the first request and reuses the relay for subsequent requests to the same domain. Different domains run in parallel worker goroutines; within one domain requests queue FIFO through a buffered channel (capacity 64). - Each demuxer dispatch has a 60 s timeout (
dispatchTimeout) so a stuck or panicked worker cannot pin the demuxer goroutine forever. - After 60 s with no requests (
daemonIdleTimeout), the daemon self-exits. The proxy respawns on next use. - On stdin EOF (proxy hangup), the daemon closes all open relays and exits.
- Max 128 concurrent
relayWorkers (maxConcurrentRelays) — beyond that the daemon emits an error frame ("too many concurrent domains") so a flood of unique domains cannot unboundedly allocate P2PRelays.
Flags
The daemon accepts the same flag set as browse: -server, -key,
-insecure/-secure/-server-fingerprint, -stealth, -auth-secret,
-print-fingerprint. Positional args are not meaningful for the daemon; pass
-domain and -path only if you want them logged on startup but the daemon
will not act on them.
Backwards-compat fallback
The proxy keeps spawnGhostdBrowse as a fallback path: if the daemon pool fails
to spawn (e.g. ghostd binary not on PATH) or crashes mid-session, the proxy
falls back to one-shot ghostd browse invocations. Browsing keeps working in
degraded mode, just without the multiplexing.