use std::net::Ipv4Addr; use crate::{ config, netns::{DockerExecTransport, NsTransport, NsenterTransport, nsenter_available}, }; #[derive(Debug, Clone)] pub struct Peer { pub ipv4: Ipv4Addr, } pub fn discover_peers(pid: u32, container: &str, peer_cidr: &str) -> anyhow::Result> { let boxed: Box = if nsenter_available() { Box::new(NsenterTransport { pid }) } else { Box::new(DockerExecTransport { container: container.to_string(), }) }; discover_peers_with_transport(boxed.as_ref(), peer_cidr) } pub fn discover_peers_with_transport( transport: &dyn NsTransport, peer_cidr: &str, ) -> anyhow::Result> { let primary = transport .run(&["wg", "show", "all", "allowed-ips"]) .map_err(|e| anyhow::anyhow!("wg show all allowed-ips failed: {e}"))?; let mut peers = parse_allowed_ips(&primary, peer_cidr)?; if peers.is_empty() { let fallback = transport .run(&["wg", "show"]) .map_err(|e| anyhow::anyhow!("wg show fallback failed: {e}"))?; peers = parse_fallback_wg_show(&fallback, peer_cidr)?; } peers.sort_by_key(|p| p.ipv4.to_string()); peers.dedup_by_key(|p| p.ipv4); Ok(peers) } fn parse_allowed_ips(raw: &str, peer_cidr: &str) -> anyhow::Result> { let mut out = Vec::new(); for line in raw.lines() { let cols: Vec<&str> = line.split_whitespace().collect(); if cols.len() < 3 { continue; } for tok in cols[2].split(',') { let tok = tok.trim(); if let Some((ip, pfx)) = tok.split_once('/') { if pfx != "32" { continue; } if let Ok(v4) = ip.parse::() { if config::ip_in_cidr(v4, peer_cidr)? { out.push(Peer { ipv4: v4 }); } } } } } Ok(out) } fn parse_fallback_wg_show(raw: &str, peer_cidr: &str) -> anyhow::Result> { let mut out = Vec::new(); for line in raw.lines() { let t = line.trim(); if !t.starts_with("allowed ips:") { continue; } let rest = t.trim_start_matches("allowed ips:").trim(); for tok in rest.split(',') { let tok = tok.trim(); if let Some((ip, pfx)) = tok.split_once('/') { if pfx != "32" { continue; } if let Ok(v4) = ip.parse::() { if config::ip_in_cidr(v4, peer_cidr)? { out.push(Peer { ipv4: v4 }); } } } } } Ok(out) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_primary() { let raw = "wg0 key1 10.8.1.10/32\nwg0 key2 10.8.1.2/32"; let peers = parse_allowed_ips(raw, "10.8.1.0/24").expect("parse"); assert_eq!(peers.len(), 2); } #[test] fn parse_fallback() { let raw = "allowed ips: 10.8.1.4/32, 10.8.1.5/32\nallowed ips: 192.168.0.1/32"; let peers = parse_fallback_wg_show(raw, "10.8.1.0/24").expect("parse"); assert_eq!(peers.len(), 2); } #[test] fn cidr_filtering_keeps_only_target_subnet() { let raw = "wg0 key1 10.8.1.10/32\nwg0 key2 10.9.1.2/32"; let peers = parse_allowed_ips(raw, "10.8.1.0/24").expect("parse"); assert_eq!(peers.len(), 1); assert_eq!(peers[0].ipv4.to_string(), "10.8.1.10"); } #[test] fn lexicographic_sorting_parity() { let raw = "wg0 key1 10.8.1.2/32\nwg0 key2 10.8.1.10/32"; let mut peers = parse_allowed_ips(raw, "10.8.1.0/24").expect("parse"); peers.sort_by_key(|p| p.ipv4.to_string()); assert_eq!(peers[0].ipv4.to_string(), "10.8.1.10"); assert_eq!(peers[1].ipv4.to_string(), "10.8.1.2"); } }