The Flat Mesh LAN
Reach every node by a stable virtual IP.
Give every node a stable virtual IP and reach any other node's local
services by that IP — ssh user@10.x.x.x, a database, an SMB share, a
web admin panel — the same way you would on one office LAN, except the
"LAN" is your encrypted MeshHold mesh and the nodes can be anywhere.
This is the same idea as Tailscale or ZeroTier, with two differences
that matter for a self-hosted setup: there is no coordination server
(membership is a shared key, distributed by you), and the traffic can
ride MeshHold's port-sharing transports, which put a node on the
443 or 22 it already exposes instead of opening one of its own.
One interface for mesh + exits. The mesh overlay rides the same system-VPN interface as any exit routing (one adapter, not two): on Linux a daemon-owned
/dev/net/tun(needsCAP_NET_ADMIN), on Windows themeshhold-vpnhelperWintun adapter, on Android theVpnServiceTUN. A node that can't create the interface logs a warning and keeps running — it just won't carry the overlay.
How it works
- A mesh network is defined by a single Mesh LAN key — a
management key with the
meshlancapability (a shared, symmetric credential, distinct from the per-nodetunnel/cameragrant keys). Holding the key is membership. - Each member derives its own virtual IP deterministically from the
key plus its node id, so there is no address-assignment protocol and
no server. Every node is given an IPv4 address from
10.0.0.0/8(configurable). The addresses are effectively static — they only change if two nodes happen to collide on the same address, which resolves itself automatically. - The key's port scope is each node's own choice of which local ports it exposes to the other members. The default is deny everything — a fresh network reaches nothing until a node opens a port.
- Two members talk over an end-to-end-encrypted mesh circuit; relay nodes in between never hold the key and only forward ciphertext.
You can run several independent networks at once by holding several Mesh LAN keys — handy to wire together just two machines in isolation from everyone else. Each network gets its own address range.
1. Hold a key, then turn the system VPN on
Membership is automatic — holding a Mesh LAN (mesh-route) key is membership (steps 2–3 below). The overlay's datapath, though, rides the unified system-VPN interface, so it becomes reachable from the host once the system VPN is up:
- Now, from the CLI:
meshhold vpn up(with no exit) brings up the mesh overlay plus any configured exits on one interface. - At boot, headless: set
vpn.enabled: true(or the Network page's "Start on boot" toggle) so the daemon brings it up itself. - From the UI: the master VPN toggle on the Network page.
On a packaged Linux server make sure the daemon has CAP_NET_ADMIN (it does
when run as root or via the shipped systemd unit); on Windows approve the
one-time meshhold-vpnhelper prompt.
config.yaml is only needed to change the address range (optional):
meshlan:
# ipv4_range: 10.0.0.0/8 # optional; must be a /8. Coexists with
# Tailscale (100.64/10); may overlap a 10.x LAN
2. Create the network key on the first serving node
Pick the node that will expose services first and mint a meshlan
key, opening the ports you want reachable:
meshhold mgmt-keys add \
--name "Home network" --caps meshlan --never-expires \
--allow tcp:22 --allow tcp:445 --allow udp:5353
--allow takes proto:port or proto:lo-hi (e.g. tcp:8000-8100), and
repeats. With no --allow the node joins the network but exposes
nothing — exactly what you want for a laptop or phone that only reaches
other nodes.
Copy the printed key value — that secret is the network. Treat it like
a password.
Change which ports are open, later
The allow set isn't fixed at creation. mgmt-keys update edits it in
place — per node, without touching the secret, so members that already
hold the key keep working. It applies live when the daemon is running (no
restart):
# open one more service
meshhold mgmt-keys update <id> --add-allow tcp:8096
# close one
meshhold mgmt-keys update <id> --rm-allow tcp:445
# replace the whole set at once
meshhold mgmt-keys update <id> --allow tcp:22 --allow udp:5353
Get <id> from meshhold mgmt-keys list. --add-allow / --rm-allow
adjust the current set; --allow replaces it wholesale. Removing every
rule leaves the node at deny-all. Run it on the node whose ports you're
changing — the open-port set is each node's own decision.
3. Join the other nodes
On each other member, import the same secret as its own key and choose which of its ports to open (often none):
# A machine that only connects out — opens nothing:
meshhold mgmt-keys add --name "Home network" --caps meshlan \
--never-expires --key <secret-from-step-2>
# A NAS that should expose SMB + a web UI:
meshhold mgmt-keys add --name "Home network" --caps meshlan \
--never-expires --key <secret-from-step-2> --allow tcp:445 --allow tcp:80
--key accepts either the bare secret or the full
meshhold://join?…&mgmt_key=… invite (the share URL from
mgmt-keys share or the Web UI) — paste the whole link and the secret and
--caps are pulled from it, so you can drop --caps meshlan.
Restart each daemon after importing.
To revoke access you rotate the key: delete it everywhere and issue a new one to the members you still trust. A shared key can't be revoked from a single member without re-keying — the same trade-off as every other shared credential in MeshHold.
4. Find your addresses and connect
meshhold meshlan status
running: yes
my IPs: 10.x.y.z
peers:
10.a.b.c 12D3KooW…nasNodeId
Now use the peer's mesh IP with any normal tool:
ssh user@10.a.b.c
smbclient //10.a.b.c/share
Bind services to
0.0.0.0(or the mesh IP), not just127.0.0.1. A peer reaches a service through the mesh, so a daemon listening only on loopback on the far node still answers — MeshHold dials it on loopback for you — but a service bound to a specific non-loopback LAN IP won't. When in doubt,0.0.0.0is safest.
Names: ssh nas.mesh
You don't have to memorise IPs. Each node also answers under its display
name in the .mesh zone — a node named "My NAS" is reachable as
my-nas.mesh (lowercased, spaces become hyphens), so ssh user@my-nas.mesh
just works.
While the system VPN is up the daemon makes the in-mesh resolver your
system DNS, so every lookup flows through it: .mesh names are
answered locally, ad/tracker domains are blocked (NXDOMAIN — see the ad
blocker), and everything
else is forwarded to your original DNS server, so your provider still
resolves the rest. It's wired Linux via systemd-resolved (resolvectl,
the interface set as the default DNS route), Windows via an NRPT rule
(all names), and Android via the VpnService DNS. (On a Linux box without
resolvectl, host tools fall back to the system DNS and .mesh names won't
resolve — everything still works by IP.)
ssh user@my-nas.mesh
- If two nodes pick the same name, the one with the lower node id keeps the bare name; rename one to disambiguate.
- The zone suffix is configurable (
meshlan.dns_suffix). Non-mesh names are forwarded to the host's original resolver (captured at bring-up);meshlan.dns_upstreamis the fallback used only when that can't be detected.
Forwarded LAN devices (subnet router)
A node can also front devices on its LAN that aren't running MeshHold —
a printer, a camera, a switch's web UI — so other members reach them by a
virtual mesh IP / .mesh name without putting the device on the mesh.
List them under meshlan.forwards in the node's config:
meshlan:
forwards:
- name: Office Printer # -> office-printer.mesh
target: 192.168.1.50 # the real LAN address this node dials
- name: Lobby Camera
target: 192.168.1.51
Each device gets its own virtual IP in this node's block (and a
<name>.mesh name) in every network the node belongs to. Reach it like
any mesh host:
ssh admin@office-printer.mesh # or by IP
curl http://lobby-camera.mesh/
Which ports are reachable still follows the meshlan key's open-port
scope — exactly like the node's own services — and the destination port
is preserved (a key that opens tcp:9100 reaches the printer's 9100).
meshhold meshlan status lists the forwarded devices and their targets.
You don't have to edit config.yaml for every change: the web UI manages
forwards live. Open the Network page → Mesh LAN panel to add or
remove a forwarded device by name and LAN target — it applies immediately,
no restart. Devices declared in config.yaml show a config badge there
and are edited in the file (the runtime ones are stored separately and
survive restarts).
Editing a network in the Web UI
Mesh LAN keys are created / shared / joined from Profile → Network →
Keys — the same Add-key flow as any other management key (pick the
Mesh LAN key type). Everything network-side then lives on the
Network page → Mesh LAN panel: the address this node holds on each
network, an Open local ports table per network (tcp/udp + port or
range), and the forwarded LAN devices. The key dialog itself carries key
info only.
The Keys tab is scoped to the active saved network: it lists the keys filed under the network you're connected to (plus untagged keys, which show everywhere), so switching networks switches the keys on show. A new key is filed under the active network automatically; use a row's Move to network… action to refile one, or the All networks toggle to see every key at once. The network tag also scopes verification: this node only honours one of its own keys while connected to that key's network — a mesh key filed under network A won't authorise a circuit while the node is on network B (untagged keys are honoured everywhere). The swarm key is still the outer gate; this is defence-in-depth for a node on several networks. File a key under All networks (global) to have it honoured everywhere.
Notes & limits
ping nas.meshworks and reflects real reachability: the daemon answers only after a live round-trip to the owning node, so a node that's down makes the ping time out instead of getting a misleading local reply. Pinging a forwarded device probes the real device too (a node that's down — or a device that's off — fails). Pinging a mesh-range IP that no node owns returns Destination Host Unreachable right away rather than hanging. (The device check is a real ICMP echo to the device, so one that drops ICMP reads as down.)- Source IP seen by the destination service is loopback, not the caller's mesh IP — fine for credential-authenticated services, the same as Tailscale's userspace mode.
- No broadcast / mDNS crosses the overlay, so devices don't auto-discover; reach them by their IP.
- Coexists with Tailscale: the default range (
10.0.0.0/8) doesn't overlap Tailscale's100.64/10, so you can run both. - Make sure the range doesn't overlap your LAN. The default
10.0.0.0/8is RFC 1918 — the same private space many home and office LANs use — so a clash is possible. If your node's own DNS server or default gateway falls inside the mesh range, the overlay would otherwise capture traffic to it and break name resolution (raw IPs keep working). On Linux, MeshHold guards against this automatically: at bring-up it pins each such address to the real network with a more-specific/32route and logs a warning, so DNS and the uplink keep working. If a whole LAN subnet clashes, pointmeshlan.ipv4_rangeat another/8you control (the layout needs a full/8). Windows and Android get the same auto-guard in a later release; until then, set a non-overlapping range there if needed.