A distributed mesh networking solution for self-hosted infrastructure named NSL.SH
This repository serves as the root "bookmark" for all Mesh Router projects, bringing together the various components that make up the mesh networking ecosystem.
This project is made possible by:
| Sponsor | Description |
|---|---|
| Yundera | Easy to use cloud server for open source container applications |
| NSL.SH | Free domain for open source projects |
Mesh Router enables secure, distributed routing of traffic between public servers and private instances. It supports both direct IP routing (for low latency) and WireGuard VPN tunneling (for instances behind NAT).
┌─────────────────────────────────────────┐
│ mesh-router-gateway │
│ (public entry point) │
└──────────────────┬──────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Direct Route │ │ VPN Tunnel │
│ (Public IP via │ │ (WireGuard via │
│ agent) │ │ tunnel) │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────┐
│ PCS Instance │
│ (self-hosted services) │
└──────────────────────────────────────────────┘
| Component | Description | Documentation |
|---|---|---|
| mesh-router-backend | Express.js API for domain management and IP resolution | README |
| mesh-router-gateway | OpenResty reverse proxy gateway for wildcard domain routing | README |
| mesh-router-gateway-cf | Cloudflare Worker gateway for edge-based routing | README |
| mesh-router-tunnel | WireGuard-based VPN tunneling for NAT traversal | README |
| mesh-router-agent | Lightweight agent for direct IP registration | README |
| mesh-dashboard | Web dashboard for mesh network management | README |
| mesh-router-nameserver | Authoritative DNS for the nsl.sh zone |
README |
| mesh-router-perf | Performance testing service | README |
On the box — everything below is deployed by the template (see What runs on a box):
| Component | Description | Documentation |
|---|---|---|
| mesh-router-template-root | The box template: install.sh, the nightly self-check, and the mesh / auth / maison stacks it deploys |
README |
| mesh-router-caddy | Local reverse proxy (caddy-docker-proxy) routing each hostname to a container by its caddy_* labels |
README |
| mesh-console | The box's local status and control page | README |
| mesh-router-auth | auth-registrar: registers OIDC clients with the box's Authelia for gated apps | README |
| auth-console | Web UI of the auth stack (Dex + Authelia): who can sign in |
README |
| mail-gateway | SMTP gateway: apps send mail, it relays over HTTPS | README |
| maison | The dashboard and app manager (replaces CasaOS): installs store apps, backups, domains | README, docs |
| files | Web file manager for the box (replaces FileBrowser) | README |
There are two templates, and a managed Yundera PCS runs both: the Yundera layer
installs and drives the FOSS mesh template unmodified. A self-serve box (install.sh)
runs the mesh template alone.
Yundera/template-root /DATA/AppData/yundera managed PCS only
admin app, kopia backup, Yundera login connector, identity push, its own migrations
└── ensure-mesh-installed.sh writes the mesh .env contract, picks the mesh version,
│ runs the mesh self-check every night
▼
mesh-router-template-root /DATA/AppData/mesh every box
stack mesh → /DATA/AppData/mesh tunnel, agent, caddy, smtp, mesh-console
stack auth → /DATA/AppData/auth dex, authelia, auth-registrar, auth-console
stack maison → /DATA/AppData/maison maison (AppShield gate) + maison-app (the dashboard)
└── store apps → /DATA/AppData/<app>
All stacks share the external pcs network; requests arrive through the tunnel or
agent, and caddy routes each hostname by container labels.
Where each version is chosen
| What | Pinned in | Reaches a box |
|---|---|---|
| The maison image | mesh-router-template-root/stacks/maison/docker-compose.yml (ghcr.io/yundera/maison:<x.y.z>) |
next self-check after the template change lands on the box's channel |
| The mesh template | the update channel: MESH_UPDATE_CHANNEL (stable / main); on a managed PCS, template-root picks the branch (or MESH_REF) |
nightly self-check |
| Store apps | the app stores; each app updates from Maison's per-app Update | when the user (or Settings › Updates) applies it |
So shipping a Maison release is: tag maison (the image is built on the tag push —
push tags one at a time, GitHub skips tag events when more than three are pushed at
once), then bump the image in mesh-router-template-root on main, then stable.
Mesh Router supports three gateway configurations:
| Mode | Description | Performance |
|---|---|---|
| CF Worker Only | Edge-based routing via Cloudflare Workers | Best (~249 req/s, 169ms p50) |
| OpenResty Only | Self-hosted reverse proxy | Simpler setup (~159 req/s, 283ms p50) |
| CF + Fallback | CF Worker with OpenResty fallback | Maximum reliability |
For detailed guidance on choosing and configuring gateway modes:
- Gateway Modes Guide - Decision tree and comparison
- Data Flow - How traffic flows through each mode
- Security in Transit - Certificates and encryption
Use these headers to debug routing issues:
Add X-Mesh-Trace: 1 request header to see the routing path in the response:
curl -s -D- -o /dev/null -H "X-Mesh-Trace: 1" https://app.alice.domain.com/ | grep -i x-mesh-route
# Response: x-mesh-route: cf-worker,nip.io,direct,pcsRoute path segments:
cf-worker- Cloudflare Worker processed the requestnip.io- Direct path via nip.io (~4x faster than gateway)gateway-fallback- OpenResty gateway fallbackdirect/agent- Route from mesh-router-agenttunnel- Route from mesh-router-tunnel (WireGuard)pcs- Final destination
Force a specific routing path for testing:
curl -H "X-Mesh-Force: gateway" https://app.alice.domain.com/ # Use gateway fallback
curl -H "X-Mesh-Force: direct" https://app.alice.domain.com/ # Prefer agent routes
curl -H "X-Mesh-Force: tunnel" https://app.alice.domain.com/ # Prefer tunnel routes| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/routes/:userid/:sig |
Ed25519 | Register routes |
GET |
/routes/:userid |
Public | Get routes for user |
DELETE |
/routes/:userid/:sig |
Ed25519 | Delete all routes |
GET |
/resolve/v2/:domain |
Public | Resolve domain to routes |
GET |
/available/:domainName |
Public | Check domain availability |
Example - resolve a domain:
curl https://backend.domain.com/resolve/v2/alice
# Response: {"userId":"...","routes":[...],"routesTtl":580}See mesh-router-backend/README.md for full API documentation.
Architecture diagrams and design documents are available in the doc/architecture folder:
Clone this repository with all submodules:
git clone --recursive https://github.com/Yundera/mesh-router-root.gitOr if you've already cloned:
git submodule update --init --recursiveRefer to each component's README for specific setup instructions.
To run the complete Mesh Router stack locally, see the dev/ folder which includes:
- Docker Compose configuration for all services
- Example environment file
- Caddy reverse proxy configuration
- Step-by-step setup instructions
cd dev
cp .env.example .env
# Edit .env with your credentials
docker compose up -dSee dev/README.md for detailed instructions.
See LICENSE for details.