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 }