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 DPI-evasion transports when a network blocks plain tunnels.

Linux and Windows. On Linux the mesh interface needs /dev/net/tun and CAP_NET_ADMIN; on Windows it comes up through the meshhold-vpnhelper service (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-route capability. 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 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). 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.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 (198.18.0.0/15) is RFC 2544 special-use and doesn't overlap Tailscale's 100.64/10, so you can run both.