Scenario

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 (needs CAP_NET_ADMIN), on Windows the meshhold-vpnhelper Wintun adapter, on Android the VpnService TUN. 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 meshlan capability (a shared, symmetric credential, distinct from the per-node tunnel/camera grant 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 just 127.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.0 is 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_upstream is 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.mesh works 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's 100.64/10, so you can run both.
  • Make sure the range doesn't overlap your LAN. The default 10.0.0.0/8 is 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 /32 route and logs a warning, so DNS and the uplink keep working. If a whole LAN subnet clashes, point meshlan.ipv4_range at another /8 you 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.