Thanks for sponsoring this project
Yundera : yundera.com - Easy to use cloud server for open source container applications. NSL.SH : nsl.sh - free domain for opensouce project.
CasaIMG - providing easy management of docker images and compatible with mesh router.
MeshRouter is a WireGuard-based VPN tunneling solution that enables secure routing of traffic between a provider (public server) and requesters (private PCS instances).
┌─────────────────────────────────────────────────────────────────────┐
│ PCS Instance │
│ │
│ ┌──────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ mesh-router │───▶│ Caddy │───▶│ Services (with labels) │ │
│ │ (port 80) │ │ (port 80) │ │ - casaos:8080 │ │
│ │ │ │ │ │ - app1:3000 │ │
│ │ WireGuard │ │ Docker │ │ - app2:8080 │ │
│ │ Tunnel ▲ │ │ Labels │ │ │ │
│ └──────────────┘ └─────────────┘ └─────────────────────────┘ │
│ │ │
└──────────────│───────────────────────────────────────────────────────┘
│
▼
┌─────────────┐
│ Provider │
│ (nsl.sh) │
└─────────────┘
- Mesh Router (Requester): Establishes WireGuard tunnel to provider and forwards all incoming traffic to Caddy
- Caddy: Uses caddy-docker-proxy to discover services via Docker labels and routes traffic accordingly
- Services: Containers with Caddy labels define their own routing rules
- External Request: User requests
https://app.user.nsl.sh - Provider: nsl.sh receives request, looks up user's VPN IP, forwards via WireGuard
- Mesh Router: Receives request on port 80, forwards to Caddy
- Caddy: Matches request against container labels, proxies to appropriate service
- Service: Container handles request and returns response
The tunnel preserves the original request scheme (HTTP vs HTTPS) throughout the entire flow:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SCHEME PRESERVATION FLOW │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ENTRY (Gateway) TUNNEL EXIT (Requester → Caddy) │
│ │ │ │ │
│ HTTP request │ │ │
│ (port 80) │ │ │
│ │ │ WireGuard │ http:// │
│ ├──────────────────────►│◄════════════════════════════►│───────────────► │
│ │ X-Forwarded-Proto: │ (encrypted) │ caddy:80 │
│ │ http │ │ │
│ │ │ │ │
│ HTTPS request │ │ │
│ (port 443, SSL terminated) │ │ │
│ │ │ WireGuard │ https:// │
│ ├──────────────────────►│◄════════════════════════════►│───────────────► │
│ │ X-Forwarded-Proto: │ (encrypted) │ caddy:443 │
│ │ https │ │ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
How it works:
- Gateway terminates SSL for HTTPS requests and sets
X-Forwarded-Proto: httpsheader - Tunnel entry (provider) receives HTTP on port 80 and preserves the
X-Forwarded-Protoheader - Traffic flows through WireGuard as HTTP (WireGuard provides network-layer encryption)
- Tunnel exit (requester) reads
X-Forwarded-Protoand speaks the correct protocol to Caddy:X-Forwarded-Proto: http→http://caddy:80X-Forwarded-Proto: https→https://caddy:443(nginx speaks TLS to Caddy)
Why this design:
- Caddy can keep standard HTTP/HTTPS configuration without special proxy trust settings
- The tunnel handles protocol translation transparently
- WireGuard encrypts all internal traffic regardless of application-layer protocol
- Scheme-aware routing ensures correct behavior for redirects, secure cookies, and HSTS
The mesh-router operates in two modes:
- Provider Mode: Runs on public servers, accepts incoming connections and routes traffic to registered requesters via WireGuard
- Requester Mode: Runs on PCS instances, establishes VPN tunnel to provider and forwards traffic to Caddy for local routing
<name> : Name proposed by the user on registration (if accepted by the provider)
<domain> : The domain of the provider (eg nsl.sh)
Full format: <service>.<name>.<domain> or <service>-<name>.<domain>
Example: nextcloud.mynas.nsl.sh or nextcloud-mynas.nsl.sh
| Variable | Default | Description |
|---|---|---|
PROVIDER |
- | Provider connection string: <url>,<userId>,<signature> (for single provider setup) |
ROUTING_TARGET_HOST |
caddy |
Target container hostname for traffic forwarding |
ROUTING_TARGET_PORT_HTTPS |
443 |
Target container port for HTTPS traffic |
ROUTING_TARGET_PORT_HTTP |
80 |
Target container port for HTTP traffic |
For advanced setups with multiple providers, use a YAML configuration file mounted at /app/config/config.yml:
providers:
- provider: https://nsl.sh,userId,signature
defaultService: casaos
- provider: http://custom-provider.com,userId2
defaultService: myapp
services:
myapp:
defaultPort: '3000'The configuration file is watched for changes and will automatically reconnect to providers when modified.
The requester automatically monitors WireGuard handshakes every 5 minutes. If a connection becomes stale (no handshake within 5 minutes), it will:
- Log the connection issue
- Tear down the WireGuard interface
- Re-register with the provider
- Re-establish the tunnel
This ensures resilient connections without manual intervention.
services:
mesh-router:
image: ghcr.io/yundera/mesh-router-tunnel:latest
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
environment:
- PROVIDER=https://nsl.sh,<userId>,<signature>
- ROUTING_TARGET_HOST=caddy
networks:
- pcs
depends_on:
- caddy
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CADDY_INGRESS_NETWORKS=pcs
networks:
- pcs
casaos:
image: casa-img:latest
labels:
caddy: ":80"
caddy.reverse_proxy: "{{upstreams 8080}}"
networks:
- pcs
networks:
pcs:
driver: bridge
name: pcsLabels define how Caddy routes traffic to each container:
labels:
caddy: ":80" # Match all requests on port 80
caddy.reverse_proxy: "{{upstreams 8080}}" # Proxy to container port 8080This template function:
- Resolves to the container's IP address on the ingress network
- Uses the specified port as the upstream port
- Example:
{{upstreams 8080}}→172.20.0.2:8080
Route all traffic to a service:
labels:
caddy: ":80"
caddy.reverse_proxy: "{{upstreams 3000}}"Route by hostname:
labels:
caddy: "myapp.example.com"
caddy.reverse_proxy: "{{upstreams 8080}}"Route with path matching:
labels:
caddy: ":80"
caddy.0_matcher: "path /api/*"
caddy.0_reverse_proxy: "{{upstreams 8080}}"Provider mode is used on public servers to accept incoming VPN connections.
| Variable | Default | Description |
|---|---|---|
PROVIDER_ANNONCE_DOMAIN |
- | Domain to announce (e.g., nsl.sh) - presence of this variable enables provider mode |
AUTH_API_URL |
- | URL for user authentication API (optional) |
VPN_IP_RANGE |
10.77.0.0/16 |
IP range for VPN clients |
VPN_PORT |
51820 |
WireGuard listen port |
VPN_ENDPOINT_ANNOUNCE |
- | Public endpoint for VPN connections (IP or hostname) |
SSL |
false |
Enable HTTPS with self-signed certificate on port 443 |
The provider exposes an internal API on port 3000 (used by Nginx for routing):
| Endpoint | Method | Description |
|---|---|---|
/api/ping |
GET | Health check - returns ok |
/api/get_ip/<host> |
GET | Resolves domain to backend VPN IP for routing |
/api/register |
POST | Peer registration endpoint for requesters |
Registration Request Body:
{
"userId": "username",
"vpnPublicKey": "WireGuard public key",
"authToken": "signature or auth token"
}Registration Response:
{
"wgConfig": { "interface": {...}, "peers": [...] },
"serverIp": "10.77.0.1",
"serverDomain": "nsl.sh",
"domainName": "username",
"domain": "username.nsl.sh"
}services:
routing:
image: ghcr.io/yundera/mesh-router-tunnel:latest
ports:
- "80:80"
- "443:443"
- "51820:51820/udp"
environment:
- PROVIDER_ANNONCE_DOMAIN=domain.com
- VPN_ENDPOINT_ANNOUNCE=x.x.x.x # Use direct IP (not behind Cloudflare)
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- ./config.lua:/etc/nginx/lua/config.lua # optionalIt is recommended to set up Cloudflare for TLS management and DDoS protection. The provider container will use a self-signed certificate for end-to-end encryption.
Tailscale Funnel Cloudflare Tunnels https://github.com/hintjen/selfhosted-gateway : Docker native self-hosted alternative to Cloudflare Tunnels, Tailscale Funnel, ngrok and others.
To start development, use the scripts in ./dev-scripts/windows/simple folder,
which contains everything needed to run mesh-router in a basic environment.
For detailed instructions, see simple example.