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 DPI-evasion transports when a network blocks plain tunnels.
Linux and Windows. On Linux the mesh interface needs
/dev/net/tunandCAP_NET_ADMIN; on Windows it comes up through themeshhold-vpnhelperservice (a dedicated Wintun adapter, alongside any exit-VPN one). Android comes in a later release. A node that can't create the interface logs a warning and keeps running — it just won't join the overlay.
How it works
- A mesh network is defined by a single mesh-route key — an
ordinary management key with the
mesh-routecapability. 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
198.18.0.0/15(configurable). The addresses are effectively static — they only change if two nodes happen to collide on the same block, 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-route keys — handy to wire together just two machines in isolation from everyone else. Each network gets its own address range.
1. Enable the overlay
On every node that should join, add to config.yaml:
meshlan:
enabled: true
# ipv4_range: 198.18.0.0/15 # optional; the default coexists with
# Tailscale (100.64/10) and home LANs
Restart the daemon. 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).
2. Create the network key on the first serving node
Pick the node that will expose services first and mint a mesh-route
key, opening the ports you want reachable:
meshhold mgmt-keys add \
--name "Home network" --caps mesh-route --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.
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 mesh-route \
--never-expires --key <secret-from-step-2>
# A NAS that should expose SMB + a web UI:
meshhold mgmt-keys add --name "Home network" --caps mesh-route \
--never-expires --key <secret-from-step-2> --allow tcp:445 --allow tcp:80
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: 198.18.x.y, fd12:....::1
peers:
198.18.a.b 12D3KooW…nasNodeId
fd12:….::1 12D3KooW…nasNodeId
Now use the peer's mesh IP with any normal tool:
ssh user@198.18.a.b
smbclient //198.18.a.b/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). The daemon routes only
the .mesh suffix to the in-mesh resolver, leaving the rest of your DNS
untouched, so ssh user@my-nas.mesh just works: Linux via
systemd-resolved (resolvectl), Windows via an NRPT rule, and
Android via the VpnService's DNS. (On a Linux box without
resolvectl, names won't resolve for host tools, but everything still
works by IP. On Android, when the mesh rides an exit VPN, names stay
mesh-only — use IPs there.)
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), as is the upstream that non-mesh names are forwarded to (meshlan.dns_upstream, used only on platforms that route all DNS to the daemon).
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:
enabled: true
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 mesh-route 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 panel → kebab menu → Mesh devices…
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
Everything above also works from Network → Keys in the web app: a
mesh-route key's open ports are an "Open local ports (mesh)" table on the
key's edit dialog (tcp/udp + port or range), and each mesh-route key
row shows the address this node holds on that network. Creating / sharing
/ joining keys is the same Add-key flow as any other management key.
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 (
198.18.0.0/15) is RFC 2544 special-use and doesn't overlap Tailscale's100.64/10, so you can run both.