No description
  • Go 93.6%
  • HTML 2.9%
  • JavaScript 2.4%
  • CSS 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-18 11:53:08 -04:00
browser feat: add -port proxy mode, improve error handling, remove dead external proxy code 2026-07-18 11:47:49 -04:00
test docs: remove stale Phase 2 references from test README 2026-07-18 11:53:08 -04:00
wwc Review fixes: sync-key auth, rate limiter, maxbody, import merge, relay loop, insecure warn, wwc sync, Electron docs 2026-07-13 17:42:29 -04:00
.gitignore feat: ghost-config.json, -relay rename, browser fixes, auto-push dir assets 2026-07-13 21:17:08 -04:00
api.go feat: add -port proxy mode, improve error handling, remove dead external proxy code 2026-07-18 11:47:49 -04:00
browse.go feat: ghost-config.json, -relay rename, browser fixes, auto-push dir assets 2026-07-13 21:17:08 -04:00
browse_daemon.go feat: ghost-config.json, -relay rename, browser fixes, auto-push dir assets 2026-07-13 21:17:08 -04:00
config.go feat: ghost-config.json, -relay rename, browser fixes, auto-push dir assets 2026-07-13 21:17:08 -04:00
domain.go feat: add -port proxy mode, improve error handling, remove dead external proxy code 2026-07-18 11:47:49 -04:00
ghostd_test.go feat: ghost-config.json, -relay rename, browser fixes, auto-push dir assets 2026-07-13 21:17:08 -04:00
go.mod Standalone ghostd with bundled wwc, multi-server peer sync, Electron browser 2026-07-12 13:01:56 -04:00
go.sum Standalone ghostd with bundled wwc, multi-server peer sync, Electron browser 2026-07-12 13:01:56 -04:00
host.go feat: add -port proxy mode, improve error handling, remove dead external proxy code 2026-07-18 11:47:49 -04:00
main.go feat: add -port proxy mode, improve error handling, remove dead external proxy code 2026-07-18 11:47:49 -04:00
rate.go Review fixes: sync-key auth, rate limiter, maxbody, import merge, relay loop, insecure warn, wwc sync, Electron docs 2026-07-13 17:42:29 -04:00
README.md docs: expand tests section in README 2026-07-18 11:51:49 -04:00
server.go feat: ghost-config.json, -relay rename, browser fixes, auto-push dir assets 2026-07-13 21:17:08 -04:00

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.html automatically 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-scan to 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      │
└──────────────┘          └────────────────────┘        └────────────────┘
  1. Start ghostd server on your California VPS (runs 24/7)
  2. In New York, start your Node/React/Go dev server on port 3000
  3. In New York, run: ghostd host -domain myapp.dev -port 3000 -api http://california-vps:7777
  4. 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-running ghostd browse-daemon subprocess per WWC server rather than spawning one ghostd browse per 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 separate ghostd host -path invocations 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 of ghost:// / http(s):// / relative links is handled by a small inline shim we inject at the head of every HTML response (see maybeInjectShim in browser/main.js); the parent listens for ghost-nav / ghost-ext / ghost-rel postMessages whose source is verified against view.contentWindow.
  • CSP-locked hosted pages — strict script-src directives on hosted HTML block our inline click-shim script. Pages with such policies can still be loaded but ghost:// 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)

  1. Encrypted transport via X25519 + AEAD-256-GCM (WireGuard primitives)
  2. Server maps domains to peers (30s TTL, heartbeat-renewed)
  3. Host heartbeats to primary server and all -peer APIs, keeping domains alive across regions
  4. Browser resolves domain via any bootstrap API → /resolve returns relay_addr + wwc_server
  5. Browser connects to the correct WWC server and opens a P2P relay to the host
  6. Content is served over an encrypted AEAD pipe (P2P direct or relay)
  7. NAT hole punching for direct UDP path when possible
  8. 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-Protocol response header on /host/list; a v3 sender collapsing to legacy v1 format via CollapseToV1 is 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=X allocates a relayWorker keyed by X. The worker calls wwc.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.