orama/pkg/inspector/checks/wireguard.go
2026-02-11 09:53:46 +02:00

271 lines
8.7 KiB
Go

package checks
import (
"fmt"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/inspector"
)
func init() {
inspector.RegisterChecker("wireguard", CheckWireGuard)
}
const wgSub = "wireguard"
// CheckWireGuard runs all WireGuard health checks.
func CheckWireGuard(data *inspector.ClusterData) []inspector.CheckResult {
var results []inspector.CheckResult
for _, nd := range data.Nodes {
if nd.WireGuard == nil {
continue
}
results = append(results, checkWGPerNode(nd, data)...)
}
results = append(results, checkWGCrossNode(data)...)
return results
}
func checkWGPerNode(nd *inspector.NodeData, data *inspector.ClusterData) []inspector.CheckResult {
var r []inspector.CheckResult
wg := nd.WireGuard
node := nd.Node.Name()
// 5.1 Interface up
if wg.InterfaceUp {
r = append(r, inspector.Pass("wg.interface_up", "WireGuard interface up", wgSub, node,
fmt.Sprintf("wg0 up, IP=%s", wg.WgIP), inspector.Critical))
} else {
r = append(r, inspector.Fail("wg.interface_up", "WireGuard interface up", wgSub, node,
"wg0 interface is DOWN", inspector.Critical))
return r
}
// 5.2 Service active
if wg.ServiceActive {
r = append(r, inspector.Pass("wg.service_active", "wg-quick@wg0 service active", wgSub, node,
"service is active", inspector.Critical))
} else {
r = append(r, inspector.Warn("wg.service_active", "wg-quick@wg0 service active", wgSub, node,
"service not active (interface up but service not managed by systemd?)", inspector.High))
}
// 5.5 Correct IP in 10.0.0.0/24
if wg.WgIP != "" && strings.HasPrefix(wg.WgIP, "10.0.0.") {
r = append(r, inspector.Pass("wg.correct_ip", "WG IP in expected range", wgSub, node,
fmt.Sprintf("IP=%s (10.0.0.0/24)", wg.WgIP), inspector.Critical))
} else if wg.WgIP != "" {
r = append(r, inspector.Warn("wg.correct_ip", "WG IP in expected range", wgSub, node,
fmt.Sprintf("IP=%s (not in 10.0.0.0/24)", wg.WgIP), inspector.High))
}
// 5.4 Listen port
if wg.ListenPort == 51820 {
r = append(r, inspector.Pass("wg.listen_port", "Listen port is 51820", wgSub, node,
"port=51820", inspector.Critical))
} else if wg.ListenPort > 0 {
r = append(r, inspector.Warn("wg.listen_port", "Listen port is 51820", wgSub, node,
fmt.Sprintf("port=%d (expected 51820)", wg.ListenPort), inspector.High))
}
// 5.7 Peer count
expectedNodes := countWGNodes(data)
expectedPeers := expectedNodes - 1
if expectedPeers < 0 {
expectedPeers = 0
}
if wg.PeerCount >= expectedPeers {
r = append(r, inspector.Pass("wg.peer_count", "Peer count matches expected", wgSub, node,
fmt.Sprintf("peers=%d (expected=%d)", wg.PeerCount, expectedPeers), inspector.High))
} else if wg.PeerCount > 0 {
r = append(r, inspector.Warn("wg.peer_count", "Peer count matches expected", wgSub, node,
fmt.Sprintf("peers=%d (expected=%d)", wg.PeerCount, expectedPeers), inspector.High))
} else {
r = append(r, inspector.Fail("wg.peer_count", "Peer count matches expected", wgSub, node,
fmt.Sprintf("peers=%d (isolated!)", wg.PeerCount), inspector.Critical))
}
// 5.29 MTU
if wg.MTU == 1420 {
r = append(r, inspector.Pass("wg.mtu", "MTU is 1420", wgSub, node,
"MTU=1420", inspector.High))
} else if wg.MTU > 0 {
r = append(r, inspector.Warn("wg.mtu", "MTU is 1420", wgSub, node,
fmt.Sprintf("MTU=%d (expected 1420)", wg.MTU), inspector.High))
}
// 5.35 Config file exists
if wg.ConfigExists {
r = append(r, inspector.Pass("wg.config_exists", "Config file exists", wgSub, node,
"/etc/wireguard/wg0.conf present", inspector.High))
} else {
r = append(r, inspector.Warn("wg.config_exists", "Config file exists", wgSub, node,
"/etc/wireguard/wg0.conf NOT found", inspector.High))
}
// 5.36 Config permissions
if wg.ConfigPerms == "600" {
r = append(r, inspector.Pass("wg.config_perms", "Config file permissions 600", wgSub, node,
"perms=600", inspector.Critical))
} else if wg.ConfigPerms != "" && wg.ConfigPerms != "000" {
r = append(r, inspector.Warn("wg.config_perms", "Config file permissions 600", wgSub, node,
fmt.Sprintf("perms=%s (expected 600)", wg.ConfigPerms), inspector.Critical))
}
// Per-peer checks
now := time.Now().Unix()
neverHandshaked := 0
staleHandshakes := 0
noTraffic := 0
for _, peer := range wg.Peers {
// 5.20 Each peer has exactly one /32 allowed IP
if !strings.Contains(peer.AllowedIPs, "/32") {
r = append(r, inspector.Warn("wg.peer_allowed_ip", "Peer has /32 allowed IP", wgSub, node,
fmt.Sprintf("peer %s...%s has allowed_ips=%s", peer.PublicKey[:8], peer.PublicKey[len(peer.PublicKey)-4:], peer.AllowedIPs), inspector.High))
}
// 5.23 No peer has 0.0.0.0/0
if strings.Contains(peer.AllowedIPs, "0.0.0.0/0") {
r = append(r, inspector.Fail("wg.peer_catch_all", "No catch-all route peer", wgSub, node,
fmt.Sprintf("peer %s...%s has 0.0.0.0/0 (route hijack!)", peer.PublicKey[:8], peer.PublicKey[len(peer.PublicKey)-4:]), inspector.Critical))
}
// 5.11-5.12 Handshake freshness
if peer.LatestHandshake == 0 {
neverHandshaked++
} else {
age := now - peer.LatestHandshake
if age > 300 {
staleHandshakes++
}
}
// 5.13 Transfer stats
if peer.TransferRx == 0 && peer.TransferTx == 0 {
noTraffic++
}
}
if len(wg.Peers) > 0 {
// 5.12 Never handshaked
if neverHandshaked == 0 {
r = append(r, inspector.Pass("wg.handshake_all", "All peers have handshaked", wgSub, node,
fmt.Sprintf("%d/%d peers handshaked", len(wg.Peers), len(wg.Peers)), inspector.Critical))
} else {
r = append(r, inspector.Fail("wg.handshake_all", "All peers have handshaked", wgSub, node,
fmt.Sprintf("%d/%d peers never handshaked", neverHandshaked, len(wg.Peers)), inspector.Critical))
}
// 5.11 Stale handshakes
if staleHandshakes == 0 {
r = append(r, inspector.Pass("wg.handshake_fresh", "All handshakes recent (<5m)", wgSub, node,
"all handshakes within 5 minutes", inspector.High))
} else {
r = append(r, inspector.Warn("wg.handshake_fresh", "All handshakes recent (<5m)", wgSub, node,
fmt.Sprintf("%d/%d peers with stale handshake (>5m)", staleHandshakes, len(wg.Peers)), inspector.High))
}
// 5.13 Transfer
if noTraffic == 0 {
r = append(r, inspector.Pass("wg.peer_traffic", "All peers have traffic", wgSub, node,
fmt.Sprintf("%d/%d peers with traffic", len(wg.Peers), len(wg.Peers)), inspector.High))
} else {
r = append(r, inspector.Warn("wg.peer_traffic", "All peers have traffic", wgSub, node,
fmt.Sprintf("%d/%d peers with zero traffic", noTraffic, len(wg.Peers)), inspector.High))
}
}
return r
}
func checkWGCrossNode(data *inspector.ClusterData) []inspector.CheckResult {
var r []inspector.CheckResult
type nodeInfo struct {
name string
wg *inspector.WireGuardData
}
var nodes []nodeInfo
for _, nd := range data.Nodes {
if nd.WireGuard != nil && nd.WireGuard.InterfaceUp {
nodes = append(nodes, nodeInfo{name: nd.Node.Name(), wg: nd.WireGuard})
}
}
if len(nodes) < 2 {
return r
}
// 5.8 Peer count consistent
counts := map[int]int{}
for _, n := range nodes {
counts[n.wg.PeerCount]++
}
if len(counts) == 1 {
for c := range counts {
r = append(r, inspector.Pass("wg.peer_count_consistent", "Peer count consistent across nodes", wgSub, "",
fmt.Sprintf("all nodes have %d peers", c), inspector.High))
}
} else {
var parts []string
for c, num := range counts {
parts = append(parts, fmt.Sprintf("%d nodes have %d peers", num, c))
}
r = append(r, inspector.Warn("wg.peer_count_consistent", "Peer count consistent across nodes", wgSub, "",
strings.Join(parts, "; "), inspector.High))
}
// 5.30 MTU consistent
mtus := map[int]int{}
for _, n := range nodes {
if n.wg.MTU > 0 {
mtus[n.wg.MTU]++
}
}
if len(mtus) == 1 {
for m := range mtus {
r = append(r, inspector.Pass("wg.mtu_consistent", "MTU consistent across nodes", wgSub, "",
fmt.Sprintf("all nodes MTU=%d", m), inspector.High))
}
} else if len(mtus) > 1 {
r = append(r, inspector.Warn("wg.mtu_consistent", "MTU consistent across nodes", wgSub, "",
fmt.Sprintf("%d different MTU values", len(mtus)), inspector.High))
}
// 5.50 Public key uniqueness
allKeys := map[string][]string{}
for _, n := range nodes {
for _, peer := range n.wg.Peers {
allKeys[peer.PublicKey] = append(allKeys[peer.PublicKey], n.name)
}
}
dupeKeys := 0
for _, names := range allKeys {
if len(names) > len(nodes)-1 {
dupeKeys++
}
}
// If all good, the same key should appear at most N-1 times (once per other node)
if dupeKeys == 0 {
r = append(r, inspector.Pass("wg.key_uniqueness", "Public keys unique across nodes", wgSub, "",
fmt.Sprintf("%d unique peer keys", len(allKeys)), inspector.Critical))
}
return r
}
func countWGNodes(data *inspector.ClusterData) int {
count := 0
for _, nd := range data.Nodes {
if nd.WireGuard != nil {
count++
}
}
return count
}