Skip to content

Mesh Networking

Bifrost supports optional Hamachi-like mesh networking for creating virtual LANs between peers. This feature enables direct peer-to-peer connectivity with automatic NAT traversal, encryption, and routing.

The mesh networking feature provides:

  • Virtual LAN: Create private networks between distributed peers
  • NAT Traversal: Automatic hole-punching with STUN/TURN/ICE support
  • P2P Encryption: All traffic encrypted with ChaCha20-Poly1305
  • Mesh Routing: Automatic route discovery and multi-hop relaying
  • TAP/TUN Device Support: Layer 2 (Ethernet) and Layer 3 (IP) networking

Bifrost supports three types of peer connections:

TypeDescriptionLatencyUse Case
DirectUDP hole-punching through NATLowestMost connections
RelayedTraffic via TURN serverMediumSymmetric NAT
Multi-HopTraffic via other peersHighestFallback when TURN unavailable

STUN (Session Traversal Utilities for NAT)

Section titled “STUN (Session Traversal Utilities for NAT)”

STUN is used to discover your public IP address and port as seen from the internet.

Implementation details (internal/p2p/stun.go):

  • Supports RFC 5389 STUN Binding Requests
  • Parses both MAPPED-ADDRESS and XOR-MAPPED-ADDRESS attributes
  • Default servers: Google’s public STUN servers
  • Configurable timeout (default: 5 seconds)

How it works:

Default STUN Servers:

stun:
servers:
- "stun:stun.l.google.com:19302"
- "stun:stun1.l.google.com:19302"
- "stun:stun2.l.google.com:19302"
- "stun:stun3.l.google.com:19302"
- "stun:stun4.l.google.com:19302"

TURN provides relay services when direct connections are impossible (e.g., symmetric NAT).

Implementation details (internal/p2p/turn.go):

  • Supports RFC 5766 TURN protocol
  • Long-term credentials with HMAC-SHA1 authentication
  • Channel binding for efficient data transfer
  • Automatic allocation refresh

TURN Operations:

OperationDescription
AllocateRequest a relay address from the server
CreatePermissionAllow a peer IP to send data through relay
ChannelBindBind a channel number to a peer for efficiency
RefreshKeep allocation alive (default: 10 minutes)
Send/DataSend and receive relayed data

Configuration:

turn:
enabled: true
servers:
- url: "turn:turn.example.com:3478"
username: "user"
password: "secret"

ICE (Interactive Connectivity Establishment)

Section titled “ICE (Interactive Connectivity Establishment)”

ICE coordinates STUN and TURN to find the best connection path.

Implementation details (internal/p2p/ice.go):

Candidate Types:

TypePriorityDescription
host126Local interface addresses
srflx100Server-reflexive (via STUN)
prflx110Peer-reflexive (discovered during checks)
relay0TURN relay addresses

ICE Candidate Gathering:

Connectivity Checks:

ICE pairs local and remote candidates, then tests connectivity in priority order:

// Priority calculation (RFC 8445)
priority = (typePref << 24) | (localPref << 8) | (256 - componentID)
// Pair priority
pairPriority = (1 << 32) * min(localPri, remotePri) + 2 * max(localPri, remotePri)

Implementation details (internal/p2p/nat.go):

NAT TypeMappingFilteringDirect Connection
NoneN/AN/AAlways works
Full ConeEndpoint-independentEndpoint-independentAlways works
Restricted ConeEndpoint-independentAddress-dependentWorks with hole-punching
Port RestrictedEndpoint-independentAddress+port-dependentWorks with hole-punching
SymmetricEndpoint-dependentAddress+port-dependentRequires TURN relay
// Recommended strategy based on NAT types
func RecommendedTraversalStrategy(nat1, nat2 NATType) string {
if nat1 == NATTypeNone || nat2 == NATTypeNone {
return "direct"
}
if nat1 == NATTypeSymmetric || nat2 == NATTypeSymmetric {
if nat1 == NATTypeFullCone || nat2 == NATTypeFullCone {
return "direct_to_full_cone"
}
return "relay" // Both need TURN
}
return "hole_punch" // Standard NAT traversal
}

The discovery server coordinates peer registration and endpoint exchange.

Implementation details (internal/mesh/discovery.go):

Registration Flow:

API Endpoints:

MethodEndpointDescription
POST/api/v1/mesh/networks/{id}/peersRegister peer
GET/api/v1/mesh/networks/{id}/peersList all peers
PATCH/api/v1/mesh/networks/{id}/peers/{peer}Update endpoints
DELETE/api/v1/mesh/networks/{id}/peers/{peer}Deregister
POST.../peers/{peer}/heartbeatKeep alive
WS/api/v1/mesh/networks/{id}/eventsReal-time events

Each peer advertises:

{
"id": "peer-abc123",
"name": "laptop-home",
"public_key": "base64-encoded-ed25519-pubkey",
"virtual_ip": "10.100.0.5",
"endpoints": [
{"address": "192.168.1.100", "port": 51820, "type": "local", "priority": 100},
{"address": "203.0.113.50", "port": 54321, "type": "reflexive", "priority": 50},
{"address": "198.51.100.10", "port": 49152, "type": "relay", "priority": 10}
]
}

When direct connections fail, traffic is relayed through a TURN server.

Implementation details (internal/p2p/relay.go):

Channel Data Format:

+------+------+-------------------+
| Chan | Len | Data Payload |
| (2) | (2) | (variable) |
+------+------+-------------------+

When TURN is unavailable, traffic can be relayed through other connected peers.

Implementation details (internal/p2p/relay.go):

Relay Message Format:

+------+----------+-----------+
| Type | Dest Len | Dest ID | Payload
| (1) | (1) | (var) | (var)
+------+----------+-----------+

Configuration:

connection:
direct_connect: true # Try direct first
relay_enabled: true # Enable TURN relay
relay_via_peers: true # Enable peer relaying
connect_timeout: 30s
keep_alive_interval: 25s

Implementation details (internal/p2p/crypto.go):

All P2P connections use:

  • Key Generation: Curve25519 key pairs
  • Key Exchange: ECDH (Elliptic Curve Diffie-Hellman)
  • Encryption: ChaCha20-Poly1305 AEAD
  • Key Derivation: Separate send/receive keys

Handshake Protocol:

Key Derivation:

// Simplified - production uses HKDF
sendKey = H(sharedSecret || "send")
recvKey = H(sharedSecret || "recv")
// Direction determined by public key comparison
if localPubKey > remotePubKey {
sendKey, recvKey = recvKey, sendKey
}

Implementation details (internal/mesh/protocol.go, internal/mesh/router.go):

The mesh uses a distance-vector routing protocol similar to RIP.

Message Types:

TypePurpose
RouteAnnounceShare known routes with neighbors
RouteRequestRequest routes from neighbors
RouteWithdrawNotify route is no longer available
HelloPeriodic keepalive
HelloAckRTT measurement
LinkStateLink state updates

Route Metric Calculation:

Metric = Latency(ms) + (HopCount * 100)

Split Horizon:

Routes are not announced back to the peer they were learned from, preventing routing loops:

if config.SplitHorizon && route.NextHop == peerID {
continue // Don't announce back
}
mesh:
enabled: true
network_id: "my-network"
network_cidr: "10.100.0.0/16"
discovery:
server: "bifrost.example.com:7080"
mesh:
enabled: true
network_id: "corporate-vpn"
network_cidr: "10.100.0.0/16"
peer_name: "laptop-john"
device:
type: tap # Layer 2 networking
name: "mesh0"
mtu: 1400
mac_address: "" # Auto-generated
discovery:
server: "bifrost.example.com:7080"
heartbeat_interval: 30s
peer_timeout: 90s
token: "${MESH_TOKEN}"
stun:
servers:
- "stun:stun.l.google.com:19302"
- "stun:stun1.l.google.com:19302"
- "stun:stun.cloudflare.com:3478"
timeout: 5s
turn:
enabled: true
servers:
- url: "turn:turn.example.com:3478"
username: "${TURN_USER}"
password: "${TURN_PASS}"
- url: "turns:turn.example.com:5349" # TLS
username: "${TURN_USER}"
password: "${TURN_PASS}"
connection:
direct_connect: true
relay_enabled: true
relay_via_peers: true
connect_timeout: 30s
keep_alive_interval: 25s
security:
private_key: "" # Auto-generated if empty
require_encryption: true
allowed_peers: [] # Empty = allow all

Using coturn:

Terminal window
# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
realm=turn.example.com
server-name=turn.example.com
# Authentication
lt-cred-mech
user=meshuser:meshpass
# Certificates for TURNS
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem

Problem: Peers discovered but not connecting

Section titled “Problem: Peers discovered but not connecting”

Diagnosis:

Terminal window
# Check NAT type detection
curl http://localhost:7082/api/v1/p2p/nat
# Response: {"type": "symmetric", "mapped_address": "..."}
# Check discovered endpoints
curl http://localhost:7082/api/v1/mesh/networks/my-network/peers

Solutions:

  1. Symmetric NAT detected: Ensure TURN is configured
  2. No reflexive candidates: Check STUN server connectivity
  3. Firewall blocking: Allow UDP on ephemeral ports (32768-65535)

Diagnosis:

Terminal window
# Test STUN connectivity
nc -u stun.l.google.com 19302
# Check local firewall
sudo iptables -L -n | grep -i drop

Solutions:

  1. Increase connect_timeout
  2. Add more STUN servers
  3. Check corporate firewall/proxy

Diagnosis:

Terminal window
# Test TURN server
turnutils_stunclient -p 3478 turn.example.com

Solutions:

  1. Verify credentials are correct
  2. Check server is reachable on port 3478/5349
  3. Ensure realm matches configuration

Expected behavior: Relay adds latency due to extra hop.

Mitigation:

  1. Use geographically close TURN servers
  2. Enable peer relaying for shorter paths
  3. Consider multiple TURN servers

Diagnosis:

Terminal window
# Check route table
curl http://localhost:7082/api/v1/mesh/routes
# Check peer connections
curl http://localhost:7082/api/v1/p2p/connections

Solutions:

  1. Verify peer is actually connected
  2. Check MTU settings (reduce if fragmentation)
  3. Ensure routing protocol is running

Symptoms: Packets bounce between peers, high CPU usage.

Solutions:

  1. Enable split horizon (default)
  2. Check TTL is being decremented
  3. Verify sequence numbers prevent duplicate processing

Linux:

Terminal window
# Check if tun module is loaded
lsmod | grep tun
# Load if missing
sudo modprobe tun
# Check permissions
ls -la /dev/net/tun
# Should be: crw-rw-rw- 1 root root 10, 200

macOS:

Terminal window
# Install tuntaposx
brew install --cask tuntap
# Or use system extension (macOS 10.15+)

Windows:

Terminal window
# Install TAP-Windows adapter
# Download from OpenVPN or WireGuard
Terminal window
# Linux
ip addr add 10.100.0.5/16 dev mesh0
ip link set mesh0 up
# macOS
sudo ifconfig mesh0 10.100.0.5 10.100.0.1 up
  1. Use TUN instead of TAP (less overhead)
  2. Reduce keepalive frequency
  3. Limit broadcast TTL
  1. Check MTU (try 1280 for maximum compatibility)
  2. Verify UDP buffer sizes
  3. Check for network congestion
Terminal window
# Linux: Increase UDP buffers
sudo sysctl -w net.core.rmem_max=26214400
sudo sysctl -w net.core.wmem_max=26214400
Terminal window
# Node status
curl http://localhost:7082/api/v1/mesh/status
# Peer list
curl http://localhost:7082/api/v1/mesh/networks/my-network/peers
# P2P statistics
curl http://localhost:7082/api/v1/p2p/stats
Terminal window
# Linux
ip link show mesh0
ip addr show mesh0
ip route show dev mesh0
# macOS
ifconfig mesh0
netstat -rn | grep mesh0
# Windows
netsh interface show interface "mesh0"
route print
Terminal window
# Ping another peer's virtual IP
ping 10.100.0.2
# With verbose output
ping -c 5 10.100.0.2
PlatformTUNTAPNotes
LinuxFullFullNative kernel support
macOSFullFullRequires tuntaposx or Network Extension
WindowsFullFullRequires wintun or TAP-Windows driver
FreeBSDFullFullNative support
OpenWrtFullPartialMay need additional packages
type NodeStats struct {
Status NodeStatus `json:"status"`
PeerCount int `json:"peer_count"`
ConnectedPeers int `json:"connected_peers"`
DirectConnections int `json:"direct_connections"`
RelayedConnections int `json:"relayed_connections"`
BytesSent int64 `json:"bytes_sent"`
BytesReceived int64 `json:"bytes_received"`
PacketsSent int64 `json:"packets_sent"`
PacketsReceived int64 `json:"packets_received"`
Uptime time.Duration `json:"uptime"`
}
type Stats struct {
ActiveConnections int
DirectConnections int
RelayedConnections int
NATType NATType
LocalEndpoints []netip.AddrPort
}
type NATInfo struct {
Type NATType `json:"type"`
MappedAddress netip.AddrPort `json:"mapped_address"`
LocalAddress netip.AddrPort `json:"local_address"`
IsBehindNAT bool `json:"is_behind_nat"`
Hairpin bool `json:"hairpin"`
DetectedAt time.Time `json:"detected_at"`
}