Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
732 lines
22 KiB
Go
732 lines
22 KiB
Go
/*
|
|
Volt Mesh — WireGuard-based encrypted overlay network.
|
|
|
|
Provides peer-to-peer encrypted tunnels between Volt nodes using WireGuard
|
|
(kernel module). Each node gets a unique IP from the mesh CIDR, and peers
|
|
are discovered via the control plane or a shared cluster token.
|
|
|
|
Architecture:
|
|
- WireGuard interface: voltmesh0 (configurable)
|
|
- Mesh CIDR: 10.200.0.0/16 (default, supports ~65K nodes)
|
|
- Each node: /32 address within the mesh CIDR
|
|
- Key management: auto-generated WireGuard keypairs per node
|
|
- Peer discovery: token-based join → control plane registration
|
|
- Config persistence: /etc/volt/mesh/
|
|
|
|
Token format (base64-encoded JSON):
|
|
{
|
|
"mesh_cidr": "10.200.0.0/16",
|
|
"control_endpoint": "198.58.96.144:51820",
|
|
"control_pubkey": "...",
|
|
"join_secret": "...",
|
|
"mesh_id": "..."
|
|
}
|
|
|
|
Copyright (c) Armored Gates LLC. All rights reserved.
|
|
AGPSL v5 — Source-available. Anti-competition clauses apply.
|
|
*/
|
|
package mesh
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
|
|
const (
|
|
DefaultMeshCIDR = "10.200.0.0/16"
|
|
DefaultMeshPort = 51820
|
|
DefaultInterface = "voltmesh0"
|
|
MeshConfigDir = "/etc/volt/mesh"
|
|
MeshStateFile = "/etc/volt/mesh/state.json"
|
|
MeshPeersFile = "/etc/volt/mesh/peers.json"
|
|
WireGuardConfigDir = "/etc/wireguard"
|
|
KeepAliveInterval = 25 // seconds
|
|
)
|
|
|
|
// ── Token ────────────────────────────────────────────────────────────────────
|
|
|
|
// ClusterToken is the join token exchanged out-of-band to bootstrap mesh membership.
|
|
type ClusterToken struct {
|
|
MeshCIDR string `json:"mesh_cidr"`
|
|
ControlEndpoint string `json:"control_endpoint"`
|
|
ControlPublicKey string `json:"control_pubkey"`
|
|
JoinSecret string `json:"join_secret"`
|
|
MeshID string `json:"mesh_id"`
|
|
}
|
|
|
|
// EncodeToken serializes and base64-encodes a cluster token.
|
|
func EncodeToken(t *ClusterToken) (string, error) {
|
|
data, err := json.Marshal(t)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to encode token: %w", err)
|
|
}
|
|
return base64.URLEncoding.EncodeToString(data), nil
|
|
}
|
|
|
|
// DecodeToken base64-decodes and deserializes a cluster token.
|
|
func DecodeToken(s string) (*ClusterToken, error) {
|
|
data, err := base64.URLEncoding.DecodeString(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid token encoding: %w", err)
|
|
}
|
|
var t ClusterToken
|
|
if err := json.Unmarshal(data, &t); err != nil {
|
|
return nil, fmt.Errorf("invalid token format: %w", err)
|
|
}
|
|
if t.MeshCIDR == "" || t.MeshID == "" {
|
|
return nil, fmt.Errorf("token missing required fields (mesh_cidr, mesh_id)")
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// ── Peer ─────────────────────────────────────────────────────────────────────
|
|
|
|
// Peer represents a node in the mesh network.
|
|
type Peer struct {
|
|
NodeID string `json:"node_id"`
|
|
PublicKey string `json:"public_key"`
|
|
Endpoint string `json:"endpoint"` // host:port (public IP + WireGuard port)
|
|
MeshIP string `json:"mesh_ip"` // 10.200.x.x/32
|
|
AllowedIPs []string `json:"allowed_ips"` // CIDRs routed through this peer
|
|
LastSeen time.Time `json:"last_seen"`
|
|
Latency float64 `json:"latency_ms"` // last measured RTT in ms
|
|
Region string `json:"region,omitempty"` // optional region label
|
|
Online bool `json:"online"`
|
|
}
|
|
|
|
// ── Mesh State ───────────────────────────────────────────────────────────────
|
|
|
|
// MeshState is the persistent on-disk state for this node's mesh membership.
|
|
type MeshState struct {
|
|
NodeID string `json:"node_id"`
|
|
MeshID string `json:"mesh_id"`
|
|
MeshCIDR string `json:"mesh_cidr"`
|
|
MeshIP string `json:"mesh_ip"` // this node's mesh IP (e.g., 10.200.0.2)
|
|
PrivateKey string `json:"private_key"`
|
|
PublicKey string `json:"public_key"`
|
|
ListenPort int `json:"listen_port"`
|
|
Interface string `json:"interface"`
|
|
JoinedAt time.Time `json:"joined_at"`
|
|
IsControl bool `json:"is_control"` // true if this node is the control plane
|
|
}
|
|
|
|
// ── Manager ──────────────────────────────────────────────────────────────────
|
|
|
|
// Manager handles mesh lifecycle operations.
|
|
type Manager struct {
|
|
state *MeshState
|
|
peers []*Peer
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewManager creates a mesh manager, loading state from disk if available.
|
|
func NewManager() *Manager {
|
|
m := &Manager{}
|
|
m.loadState()
|
|
m.loadPeers()
|
|
return m
|
|
}
|
|
|
|
// IsJoined returns true if this node is part of a mesh.
|
|
func (m *Manager) IsJoined() bool {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.state != nil && m.state.MeshID != ""
|
|
}
|
|
|
|
// State returns a copy of the current mesh state (nil if not joined).
|
|
func (m *Manager) State() *MeshState {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if m.state == nil {
|
|
return nil
|
|
}
|
|
copy := *m.state
|
|
return ©
|
|
}
|
|
|
|
// Peers returns a copy of the current peer list.
|
|
func (m *Manager) Peers() []*Peer {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
result := make([]*Peer, len(m.peers))
|
|
for i, p := range m.peers {
|
|
copy := *p
|
|
result[i] = ©
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ── Init (Create a new mesh) ────────────────────────────────────────────────
|
|
|
|
// InitMesh creates a new mesh network and makes this node the control plane.
|
|
// Returns the cluster token for other nodes to join.
|
|
func (m *Manager) InitMesh(meshCIDR string, listenPort int, publicEndpoint string) (*ClusterToken, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.state != nil && m.state.MeshID != "" {
|
|
return nil, fmt.Errorf("already part of mesh %q — run 'volt mesh leave' first", m.state.MeshID)
|
|
}
|
|
|
|
if meshCIDR == "" {
|
|
meshCIDR = DefaultMeshCIDR
|
|
}
|
|
if listenPort == 0 {
|
|
listenPort = DefaultMeshPort
|
|
}
|
|
|
|
// Generate WireGuard keypair
|
|
privKey, pubKey, err := generateWireGuardKeys()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate WireGuard keys: %w", err)
|
|
}
|
|
|
|
// Generate mesh ID
|
|
meshID := generateMeshID()
|
|
|
|
// Allocate first IP in mesh CIDR for control plane
|
|
meshIP, err := allocateFirstIP(meshCIDR)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to allocate mesh IP: %w", err)
|
|
}
|
|
|
|
// Generate join secret
|
|
joinSecret, err := generateSecret(32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate join secret: %w", err)
|
|
}
|
|
|
|
// Generate node ID
|
|
nodeID, err := generateNodeID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate node ID: %w", err)
|
|
}
|
|
|
|
m.state = &MeshState{
|
|
NodeID: nodeID,
|
|
MeshID: meshID,
|
|
MeshCIDR: meshCIDR,
|
|
MeshIP: meshIP,
|
|
PrivateKey: privKey,
|
|
PublicKey: pubKey,
|
|
ListenPort: listenPort,
|
|
Interface: DefaultInterface,
|
|
JoinedAt: time.Now().UTC(),
|
|
IsControl: true,
|
|
}
|
|
|
|
// Configure WireGuard interface
|
|
if err := m.configureInterface(); err != nil {
|
|
m.state = nil
|
|
return nil, fmt.Errorf("failed to configure WireGuard interface: %w", err)
|
|
}
|
|
|
|
// Save state
|
|
if err := m.saveState(); err != nil {
|
|
return nil, fmt.Errorf("failed to save mesh state: %w", err)
|
|
}
|
|
|
|
// Build cluster token
|
|
token := &ClusterToken{
|
|
MeshCIDR: meshCIDR,
|
|
ControlEndpoint: publicEndpoint,
|
|
ControlPublicKey: pubKey,
|
|
JoinSecret: joinSecret,
|
|
MeshID: meshID,
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// ── Join ─────────────────────────────────────────────────────────────────────
|
|
|
|
// JoinMesh joins this node to an existing mesh using a cluster token.
|
|
func (m *Manager) JoinMesh(tokenStr string, listenPort int, publicEndpoint string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.state != nil && m.state.MeshID != "" {
|
|
return fmt.Errorf("already part of mesh %q — run 'volt mesh leave' first", m.state.MeshID)
|
|
}
|
|
|
|
token, err := DecodeToken(tokenStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid cluster token: %w", err)
|
|
}
|
|
|
|
if listenPort == 0 {
|
|
listenPort = DefaultMeshPort
|
|
}
|
|
|
|
// Generate WireGuard keypair
|
|
privKey, pubKey, err := generateWireGuardKeys()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate WireGuard keys: %w", err)
|
|
}
|
|
|
|
// Generate node ID
|
|
nodeID, err := generateNodeID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate node ID: %w", err)
|
|
}
|
|
|
|
// Allocate a mesh IP (in production, the control plane would assign this;
|
|
// for now, derive from node ID hash to avoid collisions)
|
|
meshIP, err := allocateIPFromNodeID(token.MeshCIDR, nodeID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to allocate mesh IP: %w", err)
|
|
}
|
|
|
|
m.state = &MeshState{
|
|
NodeID: nodeID,
|
|
MeshID: token.MeshID,
|
|
MeshCIDR: token.MeshCIDR,
|
|
MeshIP: meshIP,
|
|
PrivateKey: privKey,
|
|
PublicKey: pubKey,
|
|
ListenPort: listenPort,
|
|
Interface: DefaultInterface,
|
|
JoinedAt: time.Now().UTC(),
|
|
IsControl: false,
|
|
}
|
|
|
|
// Configure WireGuard interface
|
|
if err := m.configureInterface(); err != nil {
|
|
m.state = nil
|
|
return fmt.Errorf("failed to configure WireGuard interface: %w", err)
|
|
}
|
|
|
|
// Add control plane as first peer
|
|
controlPeer := &Peer{
|
|
NodeID: "control",
|
|
PublicKey: token.ControlPublicKey,
|
|
Endpoint: token.ControlEndpoint,
|
|
MeshIP: "", // resolved dynamically
|
|
AllowedIPs: []string{token.MeshCIDR},
|
|
LastSeen: time.Now().UTC(),
|
|
Online: true,
|
|
}
|
|
m.peers = []*Peer{controlPeer}
|
|
|
|
// Add control plane peer to WireGuard
|
|
if err := m.addWireGuardPeer(controlPeer); err != nil {
|
|
return fmt.Errorf("failed to add control plane peer: %w", err)
|
|
}
|
|
|
|
// Save state
|
|
if err := m.saveState(); err != nil {
|
|
return fmt.Errorf("failed to save mesh state: %w", err)
|
|
}
|
|
if err := m.savePeers(); err != nil {
|
|
return fmt.Errorf("failed to save peer list: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ── Leave ────────────────────────────────────────────────────────────────────
|
|
|
|
// LeaveMesh removes this node from the mesh, tearing down the WireGuard interface.
|
|
func (m *Manager) LeaveMesh() error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.state == nil || m.state.MeshID == "" {
|
|
return fmt.Errorf("not part of any mesh")
|
|
}
|
|
|
|
// Tear down WireGuard interface
|
|
exec.Command("ip", "link", "set", m.state.Interface, "down").Run()
|
|
exec.Command("ip", "link", "del", m.state.Interface).Run()
|
|
|
|
// Clean up config files
|
|
os.Remove(filepath.Join(WireGuardConfigDir, m.state.Interface+".conf"))
|
|
|
|
// Clear state
|
|
m.state = nil
|
|
m.peers = nil
|
|
|
|
// Remove state files
|
|
os.Remove(MeshStateFile)
|
|
os.Remove(MeshPeersFile)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ── Add/Remove Peers ─────────────────────────────────────────────────────────
|
|
|
|
// AddPeer registers a new peer in the mesh and configures the WireGuard tunnel.
|
|
func (m *Manager) AddPeer(peer *Peer) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.state == nil {
|
|
return fmt.Errorf("not part of any mesh")
|
|
}
|
|
|
|
// Check for duplicate
|
|
for _, existing := range m.peers {
|
|
if existing.NodeID == peer.NodeID {
|
|
// Update existing peer
|
|
existing.Endpoint = peer.Endpoint
|
|
existing.PublicKey = peer.PublicKey
|
|
existing.AllowedIPs = peer.AllowedIPs
|
|
existing.LastSeen = time.Now().UTC()
|
|
existing.Online = true
|
|
if err := m.addWireGuardPeer(existing); err != nil {
|
|
return fmt.Errorf("failed to update WireGuard peer: %w", err)
|
|
}
|
|
return m.savePeers()
|
|
}
|
|
}
|
|
|
|
peer.LastSeen = time.Now().UTC()
|
|
peer.Online = true
|
|
m.peers = append(m.peers, peer)
|
|
|
|
if err := m.addWireGuardPeer(peer); err != nil {
|
|
return fmt.Errorf("failed to add WireGuard peer: %w", err)
|
|
}
|
|
|
|
return m.savePeers()
|
|
}
|
|
|
|
// RemovePeer removes a peer from the mesh.
|
|
func (m *Manager) RemovePeer(nodeID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.state == nil {
|
|
return fmt.Errorf("not part of any mesh")
|
|
}
|
|
|
|
var remaining []*Peer
|
|
var removed *Peer
|
|
for _, p := range m.peers {
|
|
if p.NodeID == nodeID {
|
|
removed = p
|
|
} else {
|
|
remaining = append(remaining, p)
|
|
}
|
|
}
|
|
|
|
if removed == nil {
|
|
return fmt.Errorf("peer %q not found", nodeID)
|
|
}
|
|
|
|
m.peers = remaining
|
|
|
|
// Remove from WireGuard
|
|
exec.Command("wg", "set", m.state.Interface,
|
|
"peer", removed.PublicKey, "remove").Run()
|
|
|
|
return m.savePeers()
|
|
}
|
|
|
|
// ── Latency Measurement ──────────────────────────────────────────────────────
|
|
|
|
// MeasureLatency pings all peers and updates their latency values.
|
|
func (m *Manager) MeasureLatency() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for _, peer := range m.peers {
|
|
if peer.MeshIP == "" {
|
|
continue
|
|
}
|
|
// Parse mesh IP (strip /32 if present)
|
|
ip := strings.Split(peer.MeshIP, "/")[0]
|
|
start := time.Now()
|
|
cmd := exec.Command("ping", "-c", "1", "-W", "2", ip)
|
|
if err := cmd.Run(); err != nil {
|
|
peer.Online = false
|
|
peer.Latency = -1
|
|
continue
|
|
}
|
|
peer.Latency = float64(time.Since(start).Microseconds()) / 1000.0
|
|
peer.Online = true
|
|
peer.LastSeen = time.Now().UTC()
|
|
}
|
|
}
|
|
|
|
// ── WireGuard Configuration ──────────────────────────────────────────────────
|
|
|
|
// configureInterface creates and configures the WireGuard network interface.
|
|
func (m *Manager) configureInterface() error {
|
|
iface := m.state.Interface
|
|
meshIP := m.state.MeshIP
|
|
listenPort := m.state.ListenPort
|
|
|
|
// Create WireGuard interface
|
|
if out, err := exec.Command("ip", "link", "add", iface, "type", "wireguard").CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create WireGuard interface: %s", string(out))
|
|
}
|
|
|
|
// Write private key to temp file for wg
|
|
keyFile := filepath.Join(MeshConfigDir, "private.key")
|
|
os.MkdirAll(MeshConfigDir, 0700)
|
|
if err := os.WriteFile(keyFile, []byte(m.state.PrivateKey), 0600); err != nil {
|
|
return fmt.Errorf("failed to write private key: %w", err)
|
|
}
|
|
|
|
// Configure WireGuard
|
|
if out, err := exec.Command("wg", "set", iface,
|
|
"listen-port", fmt.Sprintf("%d", listenPort),
|
|
"private-key", keyFile,
|
|
).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to configure WireGuard: %s", string(out))
|
|
}
|
|
|
|
// Assign mesh IP
|
|
_, meshNet, _ := net.ParseCIDR(m.state.MeshCIDR)
|
|
ones, _ := meshNet.Mask.Size()
|
|
if out, err := exec.Command("ip", "addr", "add",
|
|
fmt.Sprintf("%s/%d", meshIP, ones),
|
|
"dev", iface,
|
|
).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to assign mesh IP: %s", string(out))
|
|
}
|
|
|
|
// Bring up interface
|
|
if out, err := exec.Command("ip", "link", "set", iface, "up").CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to bring up interface: %s", string(out))
|
|
}
|
|
|
|
// Write WireGuard config file for wg-quick compatibility
|
|
m.writeWireGuardConfig()
|
|
|
|
return nil
|
|
}
|
|
|
|
// addWireGuardPeer adds or updates a peer in the WireGuard interface.
|
|
func (m *Manager) addWireGuardPeer(peer *Peer) error {
|
|
args := []string{"set", m.state.Interface, "peer", peer.PublicKey}
|
|
|
|
if peer.Endpoint != "" {
|
|
args = append(args, "endpoint", peer.Endpoint)
|
|
}
|
|
|
|
allowedIPs := peer.AllowedIPs
|
|
if len(allowedIPs) == 0 && peer.MeshIP != "" {
|
|
ip := strings.Split(peer.MeshIP, "/")[0]
|
|
allowedIPs = []string{ip + "/32"}
|
|
}
|
|
if len(allowedIPs) > 0 {
|
|
args = append(args, "allowed-ips", strings.Join(allowedIPs, ","))
|
|
}
|
|
|
|
args = append(args, "persistent-keepalive", fmt.Sprintf("%d", KeepAliveInterval))
|
|
|
|
if out, err := exec.Command("wg", args...).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("wg set peer failed: %s", string(out))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeWireGuardConfig generates a wg-quick compatible config file.
|
|
func (m *Manager) writeWireGuardConfig() error {
|
|
os.MkdirAll(WireGuardConfigDir, 0700)
|
|
|
|
_, meshNet, _ := net.ParseCIDR(m.state.MeshCIDR)
|
|
ones, _ := meshNet.Mask.Size()
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("[Interface]\n")
|
|
sb.WriteString(fmt.Sprintf("PrivateKey = %s\n", m.state.PrivateKey))
|
|
sb.WriteString(fmt.Sprintf("ListenPort = %d\n", m.state.ListenPort))
|
|
sb.WriteString(fmt.Sprintf("Address = %s/%d\n", m.state.MeshIP, ones))
|
|
sb.WriteString("\n")
|
|
|
|
for _, peer := range m.peers {
|
|
sb.WriteString("[Peer]\n")
|
|
sb.WriteString(fmt.Sprintf("PublicKey = %s\n", peer.PublicKey))
|
|
if peer.Endpoint != "" {
|
|
sb.WriteString(fmt.Sprintf("Endpoint = %s\n", peer.Endpoint))
|
|
}
|
|
allowedIPs := peer.AllowedIPs
|
|
if len(allowedIPs) == 0 && peer.MeshIP != "" {
|
|
ip := strings.Split(peer.MeshIP, "/")[0]
|
|
allowedIPs = []string{ip + "/32"}
|
|
}
|
|
if len(allowedIPs) > 0 {
|
|
sb.WriteString(fmt.Sprintf("AllowedIPs = %s\n", strings.Join(allowedIPs, ", ")))
|
|
}
|
|
sb.WriteString(fmt.Sprintf("PersistentKeepalive = %d\n", KeepAliveInterval))
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
confPath := filepath.Join(WireGuardConfigDir, m.state.Interface+".conf")
|
|
return os.WriteFile(confPath, []byte(sb.String()), 0600)
|
|
}
|
|
|
|
// ── Persistence ──────────────────────────────────────────────────────────────
|
|
|
|
func (m *Manager) loadState() {
|
|
data, err := os.ReadFile(MeshStateFile)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var state MeshState
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|
return
|
|
}
|
|
m.state = &state
|
|
}
|
|
|
|
func (m *Manager) saveState() error {
|
|
os.MkdirAll(MeshConfigDir, 0700)
|
|
data, err := json.MarshalIndent(m.state, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(MeshStateFile, data, 0600)
|
|
}
|
|
|
|
func (m *Manager) loadPeers() {
|
|
data, err := os.ReadFile(MeshPeersFile)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var peers []*Peer
|
|
if err := json.Unmarshal(data, &peers); err != nil {
|
|
return
|
|
}
|
|
m.peers = peers
|
|
}
|
|
|
|
func (m *Manager) savePeers() error {
|
|
os.MkdirAll(MeshConfigDir, 0700)
|
|
data, err := json.MarshalIndent(m.peers, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(MeshPeersFile, data, 0600)
|
|
}
|
|
|
|
// ── Key Generation ───────────────────────────────────────────────────────────
|
|
|
|
// generateWireGuardKeys generates a WireGuard keypair using the `wg` tool.
|
|
func generateWireGuardKeys() (privateKey, publicKey string, err error) {
|
|
// Generate private key
|
|
privOut, err := exec.Command("wg", "genkey").Output()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("wg genkey failed: %w", err)
|
|
}
|
|
privateKey = strings.TrimSpace(string(privOut))
|
|
|
|
// Derive public key
|
|
cmd := exec.Command("wg", "pubkey")
|
|
cmd.Stdin = strings.NewReader(privateKey)
|
|
pubOut, err := cmd.Output()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("wg pubkey failed: %w", err)
|
|
}
|
|
publicKey = strings.TrimSpace(string(pubOut))
|
|
|
|
return privateKey, publicKey, nil
|
|
}
|
|
|
|
// generateMeshID creates a random 8-character mesh identifier.
|
|
func generateMeshID() string {
|
|
b := make([]byte, 4)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
// generateNodeID creates a random 16-character node identifier.
|
|
func generateNodeID() (string, error) {
|
|
b := make([]byte, 8)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// generateSecret creates a random secret of the given byte length.
|
|
func generateSecret(length int) (string, error) {
|
|
b := make([]byte, length)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.URLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// ── IP Allocation ────────────────────────────────────────────────────────────
|
|
|
|
// allocateFirstIP returns the first usable IP in a CIDR (x.x.x.1).
|
|
func allocateFirstIP(cidr string) (string, error) {
|
|
ip, _, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid CIDR: %w", err)
|
|
}
|
|
ip4 := ip.To4()
|
|
if ip4 == nil {
|
|
return "", fmt.Errorf("only IPv4 is supported")
|
|
}
|
|
// First usable: network + 1
|
|
ip4[3] = 1
|
|
return ip4.String(), nil
|
|
}
|
|
|
|
// allocateIPFromNodeID deterministically derives a mesh IP from a node ID,
|
|
// using a hash to distribute IPs across the CIDR space.
|
|
func allocateIPFromNodeID(cidr, nodeID string) (string, error) {
|
|
_, ipNet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid CIDR: %w", err)
|
|
}
|
|
|
|
ones, bits := ipNet.Mask.Size()
|
|
hostBits := bits - ones
|
|
maxHosts := (1 << hostBits) - 2 // exclude network and broadcast
|
|
|
|
// Hash node ID to get a host number
|
|
hash := sha256.Sum256([]byte(nodeID))
|
|
hostNum := int(hash[0])<<8 | int(hash[1])
|
|
hostNum = (hostNum % maxHosts) + 2 // +2 to skip .0 (network) and .1 (control)
|
|
|
|
ip := make(net.IP, 4)
|
|
copy(ip, ipNet.IP.To4())
|
|
|
|
// Add host number to network address
|
|
for i := 3; i >= 0 && hostNum > 0; i-- {
|
|
ip[i] += byte(hostNum & 0xFF)
|
|
hostNum >>= 8
|
|
}
|
|
|
|
return ip.String(), nil
|
|
}
|
|
|
|
// ── Status ───────────────────────────────────────────────────────────────────
|
|
|
|
// GetWireGuardStatus retrieves the current WireGuard interface status.
|
|
func (m *Manager) GetWireGuardStatus() (string, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
if m.state == nil {
|
|
return "", fmt.Errorf("not part of any mesh")
|
|
}
|
|
|
|
out, err := exec.Command("wg", "show", m.state.Interface).CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("wg show failed: %s", string(out))
|
|
}
|
|
return string(out), nil
|
|
}
|