The Private Mesh VPN
Tunnel your phone through a trusted exit.
Tunnel your phone through a trusted exit. The same encrypted transport that carries your files routes arbitrary TCP — and UDP — traffic.
Concepts
- Entry: where you start the tunnel (your phone, your laptop).
- Middle hops (optional): relay nodes that can't decrypt anything.
- Exit: the node whose IP you appear to be using.
ChaCha20-Poly1305 AEAD runs end-to-end between entry and exit; yamux multiplexes many TCP sub-streams inside one chain.
1. Issue a tunnel-capable mgmt key on the exit node
meshhold mgmt-keys add --name="Phone" --caps=tunnel --expires-in=30d
Copy the resulting QR / string.
2. Pair the phone
Scan the QR. The phone stores the key locally and uses it when initiating tunnels.
3. Pick a transport mode
HTTP CONNECT proxy
The daemon exposes a local proxy on 127.0.0.1:<random>. Configure your
browser to use it.
System VPN (Android)
From NetworkPage, hit "🛡 Start system VPN". The daemon brings up a
TUN device through VpnService.Builder + gVisor netstack. Loopback +
RFC1918 LAN ranges are excluded so your local network stays reachable.
4. Forward / reverse TCP & UDP tunnels
The same tunnel primitive powers SSH-style -L / -R port-forwarding —
on top of TCP and UDP. Useful for game servers, DNS, VoIP, OpenVPN,
custom binary protocols, anything most overlay tools would skip.
Two directions, mirroring ssh:
| Direction | Who binds the listener? | Who dials the destination? | Mental model |
|---|---|---|---|
forward (-L) |
this node (local) | the peer | "Bring a remote LAN service to my localhost." Like ssh -L 1194:10.0.0.5:1194 peer. |
reverse (-R) |
the peer | this node (local) | "Publish my local service through a public peer." Like ssh -R 16261:192.168.1.50:16261 peer or an ngrok-style exit. |
Both directions ride the same encrypted tunnel transport, so they inherit multi-hop routing, libp2p Circuit Relay v2 fallback, and the REALITY / SSH masquerade transports if you've enabled them.
Prerequisites
You need the same pieces as a regular tunnel (steps 1–2 above):
- The peer mints a
tunnel-capable mgmt key and shares its QR / string with you. - You scan it on this node, so its peer mgmt key store has an authorised credential for the counter-party.
The forward refuses to come up without an authorised peer key, even if the swarm-PSK is correct.
4a. CLI — four canonical recipes
The CLI shape is the same in every case; only the flags change.
Forward TCP — reach a peer's internal web service on your localhost:
meshhold forwards add \
--name peer-grafana --forward --proto tcp \
--peer-node 12D3KooW…HOME --peer-key home-tunnel \
--listen 127.0.0.1:3000 --remote 192.168.1.10:3000
Forward UDP — dial a peer's OpenVPN over UDP through this node:
meshhold forwards add \
--name peer-openvpn --forward --proto udp \
--peer-node 12D3KooW…HOME --peer-key home-tunnel \
--listen 127.0.0.1:1194 --remote 10.0.0.5:1194
Reverse TCP — expose a LAN SSH server through a public VPS peer:
meshhold forwards add \
--name lan-ssh-via-vps --reverse --proto tcp \
--peer-node 12D3KooW…VPS --peer-key vps-tunnel \
--listen :2222 --remote 192.168.1.50:22
Reverse UDP — publish a Project Zomboid server through a public VPS:
meshhold forwards add \
--name pz-via-vps --reverse --proto udp \
--peer-node 12D3KooW…VPS --peer-key vps-tunnel \
--listen :16261 --remote 192.168.1.50:16261
--listen is parsed as a Go bind string: :PORT becomes 127.0.0.1:PORT
for forward and 0.0.0.0:PORT for reverse; pass an explicit host
when you want something else.
4b. Lifecycle
meshhold forwards list # table + live phase / counters
meshhold forwards stop pf-pz-via-vps # pause without losing config
meshhold forwards start pf-pz-via-vps # bring it back
meshhold forwards rm pf-pz-via-vps # forget completely
The default id is pf-<slug-of-name> — override with --id on add
if you want something stable. --no-autostart registers an entry that
stays dormant until you explicitly start it.
Full flag reference: see meshhold forwards.
4c. Opening the router port (optional)
Tack on --open-via-upnp on add, or flip the Router toggle in
the web UI, to ask the listener-side router to map the port for you:
- on a
forward, that's this node's router; - on a
reverse, that's the peer's router.
This is a best-effort UPnP IGD call. If the router answers, the
external host:port shows up in meshhold forwards list (and in the
"Public" line on the panel card); if it refuses or there's no IGD,
the listener still works LAN-only and no error is raised.
4d. Web UI — Forwards panel
Everything above is also available without touching the CLI.
- Open the web UI on the node that owns the local end of the forward (i.e. the side you're issuing commands from).
- Go to Settings → Forwards. The panel lists every registered
forward with its phase (
active/error/stopped), protocol chip (TCP/UDP), direction chip (Forward (ssh -L)/Reverse (ssh -R)), peer, listen/remote pair, and live byte counters. - Click Add forward. The dialog asks for:
- Name — free-form label.
- Direction — two buttons, Forward (-L) / Reverse (-R).
- Protocol — TCP / UDP.
- Listen address — label changes per direction (Local listen address on forward, Peer listen address on reverse) so it's clear which side actually binds.
- Remote dial address — label flips the same way.
- Peer node ID — paste the counter-party's
12D3KooW…ID. - Peer mgmt key — dropdown populated from your imported peer keys; if it's empty, pair with the peer's "Tunnel" key from Profile → Mgmt keys first.
- Router — checkbox toggling the UPnP request described above.
- Submit. The forward is persisted to
config.yaml'sport_forwardslist and (unless you cleared autostart) goes live immediately.
Each card has inline Start / Stop / Delete controls and a confirm
prompt on delete. The whole panel is just a thin client over
/api/v1/forwards,
so anything you can do in the UI you can also script with curl.
4e. Other surfaces
Same primitive, three entry points — pick whichever fits the workflow:
- REST —
/api/v1/forwards(GET/POST/PATCH/DELETE+start/stop). Body matches a single entry ofport_forwards[]. Full surface: REST API → port forwards. config.yaml— declarativeport_forwards: [...]list, useful on headless/VPS nodes where you don't want to run the web UI to bootstrap a forward. Schema: config reference →port_forwards.- CLI — the
meshhold forwardsfamily above, equivalent to the REST calls.
5. REALITY / SSH-banner masquerade (optional)
Enable in config.yaml:
node:
obfs:
reality:
enabled: true
port: 8443
dest: "www.microsoft.com:443"
private_key_file: "/etc/meshhold/reality.key"
order: ["plain", "reality", "ssh"]
meshhold keygen-reality generates the X25519 keypair.