A Go-based tool for building and managing WireGuard mesh networks with support for NAT traversal, automatic endpoint detection, and incremental configuration updates.
- Automatic Mesh Network Creation: Builds full mesh topology where every node can communicate with every other node
- NAT Detection: Automatically detects nodes behind NAT and configures persistent keepalive
- SSH-Based Deployment: Installs and configures WireGuard on remote Ubuntu hosts via SSH
- Incremental Updates: Uses
wg setcommands for online configuration changes without restarting interfaces - Key Management: Generates and stores WireGuard key pairs locally for all nodes
- Routing Table Management: Automatically configures routes for networks behind mesh nodes on all nodes
- Diff-Based Deployment: Only applies configuration changes, minimizing disruption
- Persistent Configuration: Uses systemd and wg-quick for automatic startup after reboot
- Persistent State: Stores mesh configuration in JSON format
- Go 1.23 or later
- WireGuard tools (
wgcommand) on the machine running wgmesh - SSH access to all nodes (root or sudo privileges required)
- Ubuntu-based target systems (tested on Ubuntu 20.04+)
git clone https://github.com/ituoga/wgmeshbuilder.git
cd wgmeshbuilder
go build -o wgmesh./wgmesh -initThis creates a mesh-state.json file with default settings:
- Interface name:
wg0 - Mesh network:
10.99.0.0/16 - Listen port:
51820
# Format: hostname:mesh_ip:ssh_host[:ssh_port]
./wgmesh -add node1:10.99.0.1:192.168.1.10
./wgmesh -add node2:10.99.0.2:203.0.113.50
./wgmesh -add node3:10.99.0.3:198.51.100.20:2222hostname: Node identifier (should match the actual hostname)mesh_ip: IP address within the mesh networkssh_host: SSH connection address (can be IP or hostname)ssh_port: SSH port (optional, defaults to 22)
./wgmesh -listOutput:
Mesh Network: 10.99.0.0/16
Interface: wg0
Listen Port: 51820
Nodes:
node1 (local):
Mesh IP: 10.99.0.1
SSH: 192.168.1.10:22
Public Key: AbCd...Ef12
Endpoint: 192.168.1.10:51820
node2 [NAT]:
Mesh IP: 10.99.0.2
SSH: 203.0.113.50:22
Public Key: GhIj...Kl34
./wgmesh -deployThis will:
- Connect to each node via SSH
- Install WireGuard if not present
- Detect public endpoints and NAT status
- Generate or update WireGuard configuration
- Write configuration to
/etc/wireguard/wg0.conf - Enable and start
wg-quick@wg0systemd service - Apply changes using
wg setcommands for online updates (when possible) - Configure routing tables with routes to all mesh networks
Configuration persists across reboots via systemd service.
./wgmesh -remove node3
./wgmesh -deploy./wgmesh -state /path/to/custom-state.json -listEncrypt the mesh state file to protect private keys. The file will be AES-256-GCM encrypted and base64-encoded, making it safe to store in vaults.
# Initialize with encryption (asks for password twice)
./wgmesh --encrypt -init
Enter encryption password: ********
Confirm password: ********
# All operations require the password when using --encrypt
./wgmesh --encrypt --add node1:10.99.0.1:192.168.1.10
Enter encryption password: ********
./wgmesh --encrypt --list
Enter encryption password: ********
./wgmesh --encrypt --deploy
Enter encryption password: ********Encrypted file format:
U2FsdGVkX1+Qq1RZNlBXMTJHVzR4TVRrMllXNWpaVzkxZEdWd0FsSnZibk5hY0dWaGRHbHZi...
(base64-encoded encrypted data)
Security features:
- AES-256-GCM authenticated encryption
- PBKDF2 key derivation (100,000 iterations)
- Random 32-byte salt per encryption
- Base64-encoded output (vault-friendly)
Store in vault:
# HashiCorp Vault
vault kv put secret/wgmesh state=@mesh-state.json
# Retrieve and use
vault kv get -field=state secret/wgmesh > mesh-state.json
./wgmesh --encrypt --listEdit the mesh-state.json file and add routable networks to a node:
{
"nodes": {
"node1": {
"hostname": "node1",
"mesh_ip": "10.99.0.1",
"routable_networks": ["192.168.10.0/24", "192.168.20.0/24"],
...
}
}
}After editing, run ./wgmesh -deploy to apply the changes.
What happens:
node1gets direct routes:ip route add 192.168.10.0/24 dev wg0andip route add 192.168.20.0/24 dev wg0- All other nodes get routes via node1's mesh IP:
ip route add 192.168.10.0/24 via 10.99.0.1 dev wg0 - Routes are added to both the live routing table and the persistent config file
- If you remove a network from
routable_networks, it will be automatically cleaned up from all nodes on the next deploy
Every node becomes a peer to every other node. For a 4-node mesh:
node1 <----> node2
^ ^
| |
v v
node3 <----> node4
- Nodes with public IPs are configured as endpoints for other nodes
- Nodes behind NAT use persistent keepalive to maintain connections
- The tool detects NAT by comparing SSH host with detected public IP
The tool ensures configuration survives server reboots by:
- Writing WireGuard configuration to
/etc/wireguard/wg0.conf(wg-quick format) - Enabling the
wg-quick@wg0.servicesystemd unit - Including
PostUpcommands in the config to:- Add routes for networks behind other mesh nodes (e.g.,
ip route add 192.168.10.0/24 via 10.99.0.2) - Enable IP forwarding (
sysctl -w net.ipv4.ip_forward=1)
- Add routes for networks behind other mesh nodes (e.g.,
- Including
PreDowncommands to clean up routes on shutdown
When the server reboots, systemd automatically:
- Brings up the WireGuard interface
- Restores all peer connections
- Re-applies all routing table entries
The tool intelligently manages routing tables:
- Reading Current Routes: Uses
ip route show dev wg0to get existing routes - Calculating Diff: Compares current vs desired routes
- Removing Stale Routes: Automatically removes routes that are no longer in the mesh state
- Adding New Routes: Adds routes for newly configured networks
- Persistence: All routes are embedded in the config file via
PostUpcommands
Example scenario:
- You add
"routable_networks": ["192.168.10.0/24"]to node1 - Deploy: All nodes get route
192.168.10.0/24 via 10.99.0.1 - You remove that network from node1's config
- Deploy: All nodes automatically remove the stale route
When deploying changes, the tool:
- Reads current WireGuard configuration using
wg show dump - Reads current routing table using
ip route show - Calculates differences between current and desired state for both peers and routes
- Updates the persistent configuration file
- Applies changes using
wg setcommands for online updates:- Add new peers
- Update existing peers
- Remove old peers
- Applies route changes using
ip routecommands:- Remove stale routes
- Add new routes
- Only restarts the interface if fundamental changes are required (e.g., IP address change)
The tool attempts authentication in this order:
- SSH agent (if
SSH_AUTH_SOCKis set) ~/.ssh/id_rsa~/.ssh/id_ed25519~/.ssh/id_ecdsa
Ensure your SSH keys are added to the authorized_keys file on target hosts.
The mesh-state.json file stores the complete mesh state:
{
"interface_name": "wg0",
"network": "10.99.0.0/16",
"listen_port": 51820,
"local_hostname": "control-node",
"nodes": {
"node1": {
"hostname": "node1",
"mesh_ip": "10.99.0.1",
"public_key": "base64-encoded-public-key",
"private_key": "base64-encoded-private-key",
"ssh_host": "192.168.1.10",
"ssh_port": 22,
"listen_port": 51820,
"public_endpoint": "192.168.1.10:51820",
"behind_nat": false,
"routable_networks": [],
"is_local": true
}
}
}- Private keys in state file: WireGuard private keys are stored in
mesh-state.json- Without encryption: Use file permissions (
chmod 600) and secure storage - With
--encrypt: State file is AES-256-GCM encrypted and base64-encoded - Recommended: Always use
--encryptflag for production deployments
- Without encryption: Use file permissions (
- Password storage: Never store encryption passwords in scripts or environment variables
- The tool uses
InsecureIgnoreHostKeyfor SSH - consider implementing proper host key verification for production - WireGuard traffic is encrypted end-to-end
- Root SSH access is required on target hosts - ensure SSH keys are properly secured
# Test SSH connectivity
ssh root@node-address
# Check WireGuard status on a node
ssh root@node-address wg show# Check if systemd service is enabled and running
ssh root@node-address systemctl status wg-quick@wg0
# View the persistent configuration file
ssh root@node-address cat /etc/wireguard/wg0.conf
# Check if service starts on boot
ssh root@node-address systemctl is-enabled wg-quick@wg0# Check interface status
ssh root@node-address ip addr show wg0
# Check routing table
ssh root@node-address ip route
# Test connectivity through mesh
ssh root@node-address ping -c 3 10.99.0.2# View systemd service logs
ssh root@node-address journalctl -u wg-quick@wg0 -n 50
# Follow logs in real-time
ssh root@node-address journalctl -u wg-quick@wg0 -f# Reboot a node
ssh root@node-address reboot
# Wait for reboot, then check if WireGuard came back up
sleep 30
ssh root@node-address wg show
ssh root@node-address ip route | grep 192.168If something goes wrong, you can force a fresh configuration:
# On each node, stop and disable the service
ssh root@node-address systemctl stop wg-quick@wg0
ssh root@node-address systemctl disable wg-quick@wg0
# Then redeploy
./wgmesh -deploywgmeshbuilder/
├── main.go # CLI interface
├── pkg/
│ ├── mesh/
│ │ ├── types.go # Data structures
│ │ ├── mesh.go # Mesh management (add/remove/list)
│ │ └── deploy.go # Deployment logic
│ ├── wireguard/
│ │ ├── keys.go # Key generation
│ │ ├── config.go # Config parsing and diffing
│ │ ├── apply.go # Configuration application
│ │ └── convert.go # Type conversions
│ └── ssh/
│ ├── client.go # SSH connection management
│ └── wireguard.go # Remote WireGuard operations
└── mesh-state.json # Mesh state (created on init)
Contributions are welcome! Please feel free to submit issues or pull requests.
MIT License - see LICENSE file for details
- Support for multiple mesh networks
- Web UI for mesh management
- Monitoring and health checks
- Support for more Linux distributions
- IPv6 support
- Automatic key rotation
- Integration with service discovery systems