https://github.com/leonardomb1/pulse
An easy to configure encrypted mesh network with TUN VPN, tag-based ACLs, quality-routed multipath.
https://github.com/leonardomb1/pulse
mesh-networks networking vnet vpn
Last synced: 2 months ago
JSON representation
An easy to configure encrypted mesh network with TUN VPN, tag-based ACLs, quality-routed multipath.
- Host: GitHub
- URL: https://github.com/leonardomb1/pulse
- Owner: leonardomb1
- License: mit
- Created: 2026-04-04T21:54:37.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-05T01:26:48.000Z (2 months ago)
- Last Synced: 2026-04-05T02:26:56.884Z (2 months ago)
- Topics: mesh-networks, networking, vnet, vpn
- Language: Go
- Homepage:
- Size: 221 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# pulse
[](https://github.com/leonardomb1/pulse/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
[](https://go.dev/)
[](https://github.com/leonardomb1/pulse/releases)
[](https://goreportcard.com/report/github.com/leonardomb1/pulse)
A zero-config encrypted mesh network in a single binary. Nodes discover each other via gossip, traffic is routed by measured link quality, and the whole thing is managed from the CLI or an interactive TUI.
## What it does
- **Encrypted mesh** between any number of nodes over QUIC (with WebSocket fallback)
- **Layer 3 VPN** via TUN interface — mesh IPs just work like a LAN
- **NAT traversal** — automatic QUIC hole punching for direct peer-to-peer links
- **Weighted multipath routing** — traffic distributed across paths by inverse-score weighting (better paths get more traffic)
- **Delta-gossip** — only changed entries are sent per gossip round (80-95% bandwidth reduction)
- **FEC** — optional forward error correction on TUN pipes for lossy links (recovers single packet loss without retransmit)
- **Exit nodes** — forward traffic to the internet through designated nodes
- **Tag-based ACL policies** — `tag:dev` can't reach `tag:prod`, first-match firewall rules
- **DNS** for the `.pulse` TLD — `ssh user@db-server.pulse`
- **SOCKS5 proxy** — transparent mesh routing for any application
- **Network isolation** — separate pulse deployments with `--network` IDs
- **Interactive TUI** (`pulse top`) — k9s-style dashboard for managing the mesh
- **Prometheus metrics** — `/metrics` endpoint for Grafana/alerting
## Architecture
```
┌──────────────┐
│ CA Node │ Signs certificates
│ (--ca) │ Handles join flow
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌─────┴─────┐ ┌───┴────┐ ┌────┴──────┐
│ Scribe │ │ Relay │ │ Client │
│ (--scribe)│ │ │ │(--socks │
│ ACLs,DNS, │ │forwards│ │ --dns │
│ tags, │ │traffic │ │ --tun) │
│ tokens │ │ │ │ │
└───────────┘ └────────┘ └───────────┘
```
**Two control roles (can be on the same or different machines):**
- **CA** — certificate authority. Signs node certificates, handles the join flow.
- **Scribe** — network config authority. Manages ACLs, DNS zones, tags, names, revocations, and join tokens. Distributes signed config to all nodes.
Every node is also a **relay** that forwards traffic for other nodes. Roles are composable — a single node can be CA + scribe + relay + exit.
## Quickstart
No config file needed. Everything is flags.
```bash
# Build
go build -o pulse ./cmd/pulse/
# On a server with a public IP — start as CA:
pulse --ca --scribe --addr relay.example.com:443 --listen :443 \
--network mynet --token $(openssl rand -hex 32)
# On another machine — join the mesh:
pulse join relay.example.com:443 --token
# Start with services enabled:
pulse start --socks --dns --tun --network mynet relay.example.com:443
# Check status:
pulse status
pulse top # interactive TUI
```
## CLI Reference
### Lifecycle
```
pulse [flags] [peers...] Start in foreground
pulse start [flags] [peers...] Start as background daemon
pulse stop Graceful shutdown
pulse id Print node ID and mesh IP
pulse cert Show certificate expiry and status
pulse top Interactive TUI dashboard
```
### Mesh
```
pulse join --token Join a mesh (one-time)
pulse status Show mesh status table
pulse tag Add a tag to a node
pulse untag Remove a tag
pulse name Set a friendly name
pulse revoke --node Revoke a node's certificate
```
### Policy
```
pulse acl list Show ACL rules
pulse acl add --from --to Add a rule (--deny, --ports 22,443)
pulse acl remove Remove a rule by index
```
ACL patterns: node ID globs (`a3f2*`), tags (`tag:prod`), names (`db-server`), or `*`.
Rules are evaluated top-to-bottom, first match wins. No rules = open by default. Adding any rule activates policy mode (unmatched = deny).
### Tokens
```
pulse token Show legacy master token (CA only)
pulse token create --ttl 1h Create a time-limited token
pulse token create --max-uses 1 Create a single-use token
pulse token list List all tokens
pulse token revoke Revoke a token
```
### Networking
```
pulse connect --node --dest SSH ProxyCommand tunnel
pulse forward --node --dest --local Port forward
pulse dns list|add|remove Manage DNS records
pulse route list|add|remove Manage exit routes
```
### Admin
```
pulse ca log View CA audit log
pulse ca sign --ca-dir ... Offline cert signing
pulse setup dns Configure systemd-resolved for .pulse
```
## Node Flags
```
--config Path to config.toml (optional)
--data-dir Data directory (default ~/.pulse)
--addr Advertised address (default :8443)
--listen Bind address (default: same as --addr)
--tcp TCP tunnel listener (default :7000)
--network Network isolation ID
--join CA relay address (auto-join on startup)
--token Join token
--log-level debug, info, warn, error (default: info)
```
## Feature Flags
```
--ca Certificate authority
--ca-token Token the CA accepts (defaults to --token)
--scribe Control plane (ACLs, DNS, tags, dashboard)
--scribe-listen Scribe HTTP API (default 127.0.0.1:8080)
--socks SOCKS5 proxy
--socks-listen SOCKS5 address (default 127.0.0.1:1080)
--dns DNS server for .pulse TLD
--dns-listen DNS address (default 127.0.0.1:5353)
--tun TUN interface for layer 3 routing (Linux)
--fec Forward error correction on TUN pipes (lossy links)
--exit Exit node (forwards traffic to internet)
```
## How it works
### Transport
Nodes try QUIC first (no head-of-line blocking, 0-RTT reconnect), fall back to WebSocket+yamux if UDP is blocked. Transport selection is transparent — both return the same `Session` interface. QUIC sessions support connection migration — when a node's NAT rebinds (wifi to 4G), the session survives without reconnecting.
### Gossip (Delta)
Every 10 seconds, each node sends **only changed entries** to its neighbors (delta-gossip). Each table entry is stamped with a version counter; peers track which version they last received. A full table push is forced every 60 seconds as a fallback. Stale entries (not seen in 5 minutes) are pruned. Max hop count: 16.
### Routing (Weighted Multipath)
The router scores each path using:
```
score = latency_ms * (1 + 5*loss_rate) * (1 + 0.3*hop_count)
```
A 2-hop path at 5ms beats a 1-hop path at 200ms. When multiple viable paths exist, traffic is distributed using **inverse-score weighted random selection** — a 5ms path gets ~10x more streams than a 50ms path.
### NAT Traversal
Nodes discover their public address via `/whoami` on a relay, then coordinate simultaneous UDP punches to establish direct QUIC links. The relay remains as fallback.
### TUN (Layer 3 VPN)
Each node gets a deterministic mesh IP (`10.100.x.x`) derived from its node ID. The `pulse0` TUN interface handles routing at the kernel level. Exit node CIDRs are auto-learned from gossip and installed as kernel routes. The packet path uses a single TUN reader with per-peer write queues and batch draining to avoid contention.
Optional **FEC** (forward error correction) can be enabled for lossy links (`[tun] fec = true`). For every 10 data packets, 1 XOR parity packet is sent. The receiver can reconstruct any single lost packet without retransmission — 10% bandwidth overhead for significant latency improvement on lossy links.
### Certificate Lifecycle
- CA cert: 10 years
- Node certs: 90 days, auto-renewed when <30 days remain
- Renewal happens through the mesh (re-join flow) — no downtime
- TLS configs use dynamic callbacks — renewed certs are picked up without restart
### Security
- **mTLS** between all peers using CA-signed ed25519 certificates
- **Constant-time** token comparison (timing attack resistant)
- **Peer identity verification** — nodeID must match SHA256 of public key
- **ACL enforcement at every hop** — not just the terminating relay
- **Signed network config** — ACLs, DNS, tags distributed via ed25519-signed NetworkConfig from the scribe
- **SSRF protection** — tunnel DestAddr validated, cloud metadata IPs blocked
- **Network isolation** — `--network` ID checked in handshake, mismatched peers rejected
- **Audit log** — all CA operations (join attempts, cert issuance, revocations) logged with fsync
## HTTP API
The scribe exposes a REST API (default `127.0.0.1:8080`):
| Endpoint | Methods | Purpose |
|----------|---------|---------|
| `GET /api/status` | GET | Full mesh state |
| `GET /api/nodes` | GET | Peer list |
| `GET/PUT /api/config` | GET, PUT | Raw NetworkConfig |
| `GET/POST/DELETE /api/dns` | * | DNS zone CRUD |
| `GET/POST/DELETE /api/acls` | * | ACL rule CRUD |
| `POST/DELETE /api/tags` | * | Node tag management |
| `PUT /api/name` | PUT | Set node name |
| `GET /api/routes` | GET | Exit route table |
| `POST /api/revoke` | POST | Revoke a node |
| `GET/POST/DELETE /api/tokens` | * | Token management |
| `GET /metrics` | GET | Prometheus metrics |
## Prometheus Metrics
```
pulse_peers_total # known peers
pulse_peers_connected # peers with active sessions
pulse_peer_latency_ms{node_id,name} # per-peer RTT
pulse_peer_loss_ratio{node_id,name} # per-peer packet loss
pulse_cert_expiry_seconds # seconds until cert expires
pulse_acl_rules_total # ACL rule count
pulse_tokens_valid # usable join tokens
pulse_node_info{node_id,network_id} # node metadata labels
```
## Config File (optional)
All settings can be passed as flags. A TOML config file is optional:
```toml
[node]
addr = "relay.example.com:443"
listen = ":443"
network_id = "prod"
log_level = "info"
[ca]
enabled = true
join_token = "your-secret-token"
[scribe]
enabled = true
[tun]
enabled = true
[socks]
enabled = true
[dns]
enabled = true
listen = "127.0.0.1:5353"
```
## Examples
### Home + relay setup
```bash
# Remote server (CA + relay):
pulse --ca --addr relay.example.com:443 --listen :443 \
--network home --token $(openssl rand -hex 32)
# Home machine (scribe + all services):
pulse join relay.example.com:443 --token
pulse start --scribe --socks --dns --tun --network home relay.example.com:443
# Name your nodes:
pulse name relay-01
pulse name home-desktop
pulse tag infra
```
### SSH through the mesh
```bash
# Direct (with TUN enabled):
ssh user@10.100.247.82
# Via DNS:
ssh user@relay-01.pulse
# Via ProxyCommand (no TUN needed):
ssh -o ProxyCommand="pulse connect --node --dest localhost:22" user@relay
```
### Access control
```bash
# Allow infra nodes SSH everywhere:
pulse acl add --from "tag:infra" --to "*" --ports 22
# Block dev from prod:
pulse acl add --from "tag:dev" --to "tag:prod" --deny
# Allow DB access on postgres port only:
pulse acl add --from "*" --to "tag:db" --ports 5432
```
### Time-limited invite
```bash
# Create a 1-hour, single-use token:
pulse token create --ttl 1h --max-uses 1
# Share the token — it self-destructs after one use or one hour
```
## License
MIT