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
flowchart TB
    subgraph LocalPeer["Local Peer"]
        App[Application]
        Device[TUN/TAP Device]
        MeshNode[Mesh Node]
        P2PManager[P2P Manager]
        Router[Mesh Router]
        Protocol[Routing Protocol]
    end

    subgraph Infrastructure["Infrastructure Services"]
        Discovery[Discovery Server]
        STUN[STUN Servers]
        TURN[TURN Relay]
    end

    subgraph RemotePeers["Remote Peers"]
        PeerA[Peer A]
        PeerB[Peer B]
        PeerC[Peer C]
    end

    App <--> Device
    Device <--> MeshNode
    MeshNode --> P2PManager
    MeshNode --> Router
    Router --> Protocol

    P2PManager <-.->|Register/Heartbeat| Discovery
    P2PManager <-.->|NAT Detection| STUN
    P2PManager <-.->|Relay Allocation| TURN

    P2PManager <-->|Direct UDP| PeerA
    P2PManager <-->|Direct UDP| PeerB
    P2PManager <-.->|Via TURN| PeerC

    style MeshNode fill:#4a90e2,stroke:#2c5aa0,color:#fff
    style P2PManager fill:#7b68ee,stroke:#5a4fcf,color:#fff
    style TURN fill:#ff6b6b,stroke:#cc5555,color:#fff

Bifrost supports three types of peer connections:

Type Description Latency Use Case
Direct UDP hole-punching through NAT Lowest Most connections
Relayed Traffic via TURN server Medium Symmetric NAT
Multi-Hop Traffic via other peers Highest Fallback when TURN unavailable
sequenceDiagram
    participant A as Peer A
    participant D as Discovery Server
    participant S as STUN Server
    participant T as TURN Server
    participant B as Peer B

    Note over A,B: 1. Discovery & NAT Detection
    A->>D: Register with public key & endpoints
    A->>S: STUN Binding Request
    S-->>A: Mapped Address (public IP:port)
    B->>D: Register with public key & endpoints
    B->>S: STUN Binding Request
    S-->>B: Mapped Address (public IP:port)

    Note over A,B: 2. Peer Discovery
    D-->>A: Peer B joined (endpoints, public key)
    D-->>B: Peer A joined (endpoints, public key)

    Note over A,B: 3. Connection Attempt
    A->>B: Try Direct (host candidates)
    A->>B: Try Direct (server-reflexive candidates)

    alt Direct Connection Succeeds
        A<<->>B: Encrypted P2P Connection
    else Direct Fails (Symmetric NAT)
        A->>T: TURN Allocate
        T-->>A: Relay Address
        A->>T: Create Permission for B
        A->>B: Exchange relay candidates
        A<<->>T: Data via TURN
        T<<->>B: Data via TURN
    end

    Note over A,B: 4. Handshake & Encryption
    A->>B: Handshake Init (static + ephemeral public key, timestamp, MAC)
    Note over B: Authorize key, verify MAC, reject replays
    B-->>A: Handshake Response (static + ephemeral public key, timestamp, MAC)
    Note over A,B: Derive per-session keys from the handshake transcript
    A<<->>B: Encrypted data (ChaCha20-Poly1305)

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:

sequenceDiagram
    participant Client
    participant STUN as STUN Server

    Client->>STUN: Binding Request<br/>(Transaction ID)
    Note over STUN: Server sees:<br/>Source IP: 203.0.113.50<br/>Source Port: 54321
    STUN-->>Client: Binding Response<br/>(XOR-Mapped-Address: 203.0.113.50:54321)
    Note over Client: Now knows public endpoint

Default STUN Servers:

mesh:
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"
timeout: 5s

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:

Operation Description
Allocate Request a relay address from the server
CreatePermission Allow a peer IP to send data through relay
ChannelBind Bind a channel number to a peer for efficiency
Refresh Keep allocation alive (default: 10 minutes)
Send/Data Send and receive relayed data

Configuration:

mesh:
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:

Type Priority Description
host 126 Local interface addresses
srflx 100 Server-reflexive (via STUN)
prflx 110 Peer-reflexive (discovered during checks)
relay 0 TURN relay addresses

ICE Candidate Gathering:

flowchart LR
    subgraph Gathering["Candidate Gathering"]
        Host[Gather Host<br/>Candidates]
        SRFLX[Gather Server-Reflexive<br/>via STUN]
        Relay[Gather Relay<br/>via TURN]
    end

    subgraph Candidates["Local Candidates"]
        C1["192.168.1.100:5000<br/>(host)"]
        C2["203.0.113.50:54321<br/>(srflx)"]
        C3["198.51.100.10:49152<br/>(relay)"]
    end

    Host --> C1
    SRFLX --> C2
    Relay --> C3

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 Type Mapping Filtering Direct Connection
None N/A N/A Always works
Full Cone Endpoint-independent Endpoint-independent Always works
Restricted Cone Endpoint-independent Address-dependent Works with hole-punching
Port Restricted Endpoint-independent Address+port-dependent Works with hole-punching
Symmetric Endpoint-dependent Address+port-dependent Requires TURN relay
flowchart TD
    Start[Start Detection] --> STUN1[Send to STUN Server 1]
    STUN1 --> Addr1[Get Mapped Address 1]
    Addr1 --> STUN2[Send to STUN Server 2]
    STUN2 --> Addr2[Get Mapped Address 2]
    Addr2 --> Compare{Same Mapping?}

    Compare -->|Yes| Friendly[Cone NAT<br/>Direct possible]
    Compare -->|No| Symmetric[Symmetric NAT<br/>Relay required]

    style Friendly fill:#50c878,stroke:#3a9d5f,color:#fff
    style Symmetric fill:#ff6b6b,stroke:#cc5555,color:#fff
// 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:

sequenceDiagram
    participant Peer
    participant Discovery as Discovery Server
    participant WebSocket as WS Events

    Peer->>Discovery: POST /api/v1/mesh/networks/{id}/peers<br/>{id, name, public_key, endpoints}
    Discovery-->>Peer: {success: true, virtual_ip, peers: [...]}

    Peer->>WebSocket: Connect to /events

    loop Heartbeat (every 30s)
        Peer->>Discovery: POST .../peers/{id}/heartbeat
    end

    Note over Discovery: New peer joins
    Discovery-->>WebSocket: {type: "join", peer: {...}}

    Note over Discovery: Peer leaves
    Discovery-->>WebSocket: {type: "leave", peer: {...}}

API Endpoints:

Method Endpoint Description
POST /api/v1/mesh/networks/{id}/peers Register peer
GET /api/v1/mesh/networks/{id}/peers List all peers
PATCH /api/v1/mesh/networks/{id}/peers/{peer} Update endpoints
DELETE /api/v1/mesh/networks/{id}/peers/{peer} Deregister
POST .../peers/{peer}/heartbeat Keep alive
WS /api/v1/mesh/networks/{id}/events Real-time events

The coordinator lives in the server binary and is configured through the mesh block of the server config:

mesh:
# Mount the /api/v1/mesh coordinator routes. Default: true.
# Set to false to remove the endpoints entirely.
enabled: true
# Persist networks and peers so they survive a server restart.
# When unset the coordinator keeps state in memory only and every network
# and peer registration is lost on restart.
state_path: "/var/lib/bifrost/mesh-state.json"
Option Type Default Description
enabled bool true Mount the coordinator REST/WebSocket routes. When false, all /api/v1/mesh/* paths return 404.
state_path string (empty) File the coordinator persists networks and peers to. Empty means in-memory only.

Notes on persistence:

  • The state file is written atomically (temp file plus rename) with mode 0600 and holds network IDs/CIDRs plus each peer’s ID, name, public key, virtual IP, endpoints and metadata. It contains no private keys.
  • Peer virtual IPs are re-pinned on startup, so restarting the server does not renumber a running mesh.
  • Entries that can no longer be parsed (an invalid CIDR or virtual IP) are skipped with a warning rather than aborting startup.
  • A write failure is logged but does not fail the request that triggered it: an in-memory registration that already succeeded is not rolled back.
  • mesh changes require a server restart.

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):

flowchart LR
    subgraph PeerA["Peer A (Symmetric NAT)"]
        AppA[Application]
        RA[Relay Manager]
    end

    subgraph TURN["TURN Server"]
        Alloc[Allocations]
        Perm[Permissions]
        Chan[Channels]
    end

    subgraph PeerB["Peer B (Symmetric NAT)"]
        RB[Relay Manager]
        AppB[Application]
    end

    AppA --> RA
    RA <-->|Channel Data| TURN
    TURN <-->|Channel Data| RB
    RB --> AppB

    style TURN fill:#ff6b6b,stroke:#cc5555,color:#fff

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):

flowchart LR
    A[Peer A] <-->|Direct| B[Peer B]
    B <-->|Direct| C[Peer C]
    A -.->|Via B| C

    style B fill:#7b68ee,stroke:#5a4fcf,color:#fff

Relay Message Format:

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

Configuration:

mesh:
connection:
direct_connect: true # Try direct first
relay_enabled: true # Enable TURN relay
relay_via_peers: false # Multi-hop peer relaying is NOT implemented; setting true is rejected at startup
connect_timeout: 30s
keep_alive_interval: 25s

Note: relay_via_peers (multi-hop relaying through other mesh peers) is not yet implemented on the data plane. The server rejects the config at startup if it is set to true. Leave it false.

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

All P2P connections use:

  • Key Generation: X25519 (Curve25519) static key pairs, plus a fresh ephemeral key pair per handshake
  • Key Exchange: static-static X25519 for peer authentication, ephemeral-ephemeral X25519 for per-session key material and forward secrecy
  • Handshake Authentication: HMAC-SHA256 over every handshake message, keyed from the static-static shared secret
  • Encryption: ChaCha20-Poly1305 AEAD
  • Key Derivation: HKDF-SHA256, separate send/receive keys, bound to the full handshake transcript
  • Replay Protection: strictly increasing handshake timestamps, plus a sliding-window bitmap over data-frame nonces

Handshake Protocol:

Both handshake messages are a fixed 105 bytes:

[0] message type (0x01 init, 0x02 response)
[1:33] sender's static X25519 public key
[33:65] sender's per-session ephemeral X25519 public key
[65:73] handshake timestamp (chosen by the initiator, echoed by the responder)
[73:105] HMAC-SHA256 authenticator over bytes [0:73]
sequenceDiagram
    participant A as Initiator
    participant B as Responder

    Note over A: Generate ephemeral keypair<br/>Compute static-static secret<br/>Derive MAC key
    A->>B: Handshake Init<br/>[0x01 | StaticPub | EphPub | Timestamp | MAC]

    Note over B: Verify MAC (proves A holds<br/>its static private key)<br/>Reject replayed timestamp<br/>Generate ephemeral keypair
    B-->>A: Handshake Response<br/>[0x02 | StaticPub | EphPub | EchoedTimestamp | MAC]

    Note over A: Verify MAC and timestamp echo
    Note over A,B: Compute ephemeral-ephemeral secret<br/>Derive send/receive keys from transcript

    A->>B: Encrypted Data<br/>[0x03 | Nonce | Ciphertext | Tag]
    B-->>A: Encrypted Data<br/>[0x03 | Nonce | Ciphertext | Tag]

Key Derivation:

staticShared = X25519(localStaticPriv, remoteStaticPub) // authenticates the peer
ephemeralShared = X25519(localEphPriv, remoteEphPub) // fresh every handshake
// Both static public keys, both ephemeral public keys (initiator first) and the
// handshake timestamp. Binding the transcript means the two ends derive matching
// keys only if they agree on every handshake input.
transcript = initStaticPub || respStaticPub || initEphPub || respEphPub || timestamp
sendKey = HKDF-SHA256(ikm: staticShared, salt: ephemeralShared, info: "send" || transcript)
recvKey = HKDF-SHA256(ikm: staticShared, salt: ephemeralShared, info: "recv" || transcript)
// Direction determined by public key comparison
if localPubKey > remotePubKey {
sendKey, recvKey = recvKey, sendKey
}

Because ephemeralShared is fresh for every handshake, session keys are unique per connection even though staticShared is fixed for a peer pair. This is what makes it safe for the frame nonce counter to restart at 0 on each reconnect: a (key, nonce) pair is never reused. Discarding the ephemeral private keys after the handshake also gives each session forward secrecy — compromising a static private key later does not decrypt recorded traffic.

Inbound sessions are authorized before the handshake is accepted, and fail closed. A peer must clear all of the following:

  1. Known to discovery. Only public keys this node learned from the discovery server can open an inbound session. A host that merely knows a peer’s public key — which is not a secret, since discovery distributes it — is refused and logged as rejecting handshake from unknown peer.
  2. On the allowlist, when security.allowed_peers is set. See below.
  3. Proves possession of the static private key, via the handshake authenticator. A spoofed identity is refused and logged as rejecting unauthenticated handshake.
  4. Presents a fresh handshake timestamp. A replayed initiation is refused and logged as rejecting replayed handshake initiation. Without this check an attacker could resend a captured handshake, take over the victim’s connection slot, and blackhole it.

Authorization is revoked when a peer leaves the mesh, so a departed peer’s key cannot be reused to open new sessions.

Only frames from a peer that cleared all four checks are decrypted and written to the local TUN/TAP device.

security.allowed_peers narrows trust from “whatever the discovery server announces” to an explicit set of base64 public keys. An empty list means “no explicit allowlist”, not “allow anybody”: peers still have to be announced by discovery and still have to authenticate. Set it when you do not fully trust the discovery server — a compromised coordinator can otherwise announce a peer of its choosing.

security:
allowed_peers:
- "Uy8x5m2Nc0h1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p="
- "9dJk3Lm4Np5Qr6St7Uv8Wx9Yz0Ab1Cd2Ef3Gh4Ij5K="

Peers outside the list are ignored at discovery time and logged as ignoring peer not in allowed_peers list. At startup the node logs which stance is in effect (mesh peer authorization: ...).

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

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

Message Types:

Type Purpose
RouteAnnounce Share known routes with neighbors
RouteRequest Request routes from neighbors
RouteWithdraw Notify route is no longer available
Hello Periodic keepalive
HelloAck RTT measurement
LinkState Link 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
}
flowchart TB
    subgraph RouteTable["Route Table"]
        R1["Peer B: Direct, metric=50"]
        R2["Peer C: via B, metric=150"]
        R3["Peer D: via C, metric=250"]
    end

    subgraph Selection["Best Route Selection"]
        Sort[Sort by Metric]
        Best[Select Lowest]
    end

    RouteTable --> Sort --> Best
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: false # Not implemented; must be false (true is rejected at startup)
connect_timeout: 30s
keep_alive_interval: 25s
security:
private_key: "" # Auto-generated if empty
require_encryption: true # Always on; false is ignored with a warning
allowed_peers: [] # Empty = no explicit allowlist (discovery-announced
# peers only, each still authenticated). See
# "Peer Authorization" above.

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

The inbound handshake path fails closed, so a peer that cannot be authorized is dropped rather than connected. The log line names the reason:

Log message Meaning Fix
rejecting handshake from unknown peer The peer’s public key was never announced by discovery, or was revoked when the peer left Confirm both peers use the same network_id and discovery server, and that the peer is currently registered
ignoring peer not in allowed_peers list security.allowed_peers is set and does not contain the peer’s key Add the peer’s base64 public key to the list, or clear the list
rejecting unauthenticated handshake The sender does not hold the static private key for the public key it claimed — a spoofing attempt, or mismatched private_key config Verify the peer’s security.private_key matches the key it publishes via discovery
rejecting replayed handshake initiation A handshake initiation was seen twice, which is a replay Expected when traffic is being replayed; otherwise check for a middlebox duplicating UDP
invalid handshake init: unexpected length (incompatible peer version?) The peer speaks an older, incompatible handshake format Upgrade all mesh peers to the same version

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
Platform TUN TAP Notes
Linux Full Full Native kernel support
macOS Full Full Requires tuntaposx or Network Extension
Windows Full Full Requires wintun or TAP-Windows driver
FreeBSD Full Full Native support
OpenWrt Full Partial May 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"`
}