Полный продакшн-фикс: parity с old bash, drift-recovery, status/runtime и builder separation
This commit is contained in:
@@ -10,6 +10,16 @@ pub struct Peer {
|
||||
pub ipv4: Ipv4Addr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PeerRuntime {
|
||||
pub public_key: String,
|
||||
pub allowed_ips: Vec<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub latest_handshake_epoch: Option<u64>,
|
||||
pub transfer_rx_bytes: Option<u64>,
|
||||
pub transfer_tx_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
pub fn discover_peers(pid: u32, container: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer>> {
|
||||
let boxed: Box<dyn NsTransport> = if nsenter_available() {
|
||||
Box::new(NsenterTransport { pid })
|
||||
@@ -43,6 +53,31 @@ pub fn discover_peers_with_transport(
|
||||
Ok(peers)
|
||||
}
|
||||
|
||||
pub fn discover_peer_runtime(
|
||||
pid: u32,
|
||||
container: &str,
|
||||
peer_cidr: &str,
|
||||
) -> anyhow::Result<Vec<PeerRuntime>> {
|
||||
let boxed: Box<dyn NsTransport> = if nsenter_available() {
|
||||
Box::new(NsenterTransport { pid })
|
||||
} else {
|
||||
Box::new(DockerExecTransport {
|
||||
container: container.to_string(),
|
||||
})
|
||||
};
|
||||
discover_peer_runtime_with_transport(boxed.as_ref(), peer_cidr)
|
||||
}
|
||||
|
||||
pub fn discover_peer_runtime_with_transport(
|
||||
transport: &dyn NsTransport,
|
||||
peer_cidr: &str,
|
||||
) -> anyhow::Result<Vec<PeerRuntime>> {
|
||||
let raw = transport
|
||||
.run(&["wg", "show", "all", "dump"])
|
||||
.map_err(|e| anyhow::anyhow!("wg show all dump failed: {e}"))?;
|
||||
parse_wg_show_dump_runtime_peers(&raw, peer_cidr)
|
||||
}
|
||||
|
||||
fn parse_allowed_ips(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer>> {
|
||||
let mut out = Vec::new();
|
||||
for line in raw.lines() {
|
||||
@@ -92,6 +127,76 @@ fn parse_fallback_wg_show(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_wg_show_dump_runtime_peers(
|
||||
raw: &str,
|
||||
peer_cidr: &str,
|
||||
) -> anyhow::Result<Vec<PeerRuntime>> {
|
||||
let mut out = Vec::new();
|
||||
for line in raw.lines() {
|
||||
let cols: Vec<&str> = line.split('\t').collect();
|
||||
if cols.len() < 8 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// peer lines include endpoint/allowed_ips/handshake/rx/tx; interface lines do not match this contract
|
||||
let public_key = cols.get(1).map(|s| s.trim()).unwrap_or_default();
|
||||
let allowed = cols.get(4).map(|s| s.trim()).unwrap_or_default();
|
||||
if public_key.is_empty() || allowed.is_empty() || allowed == "(none)" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let filtered_allowed = filter_allowed_ips_in_cidr(allowed, peer_cidr)?;
|
||||
if filtered_allowed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let endpoint = cols
|
||||
.get(3)
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty() && *s != "(none)")
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let latest_handshake_epoch = cols
|
||||
.get(5)
|
||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
||||
.filter(|v| *v > 0);
|
||||
let transfer_rx_bytes = cols.get(6).and_then(|s| s.trim().parse::<u64>().ok());
|
||||
let transfer_tx_bytes = cols.get(7).and_then(|s| s.trim().parse::<u64>().ok());
|
||||
|
||||
out.push(PeerRuntime {
|
||||
public_key: public_key.to_string(),
|
||||
allowed_ips: filtered_allowed,
|
||||
endpoint,
|
||||
latest_handshake_epoch,
|
||||
transfer_rx_bytes,
|
||||
transfer_tx_bytes,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn filter_allowed_ips_in_cidr(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<String>> {
|
||||
let mut out = Vec::new();
|
||||
for tok in raw.split(',') {
|
||||
let tok = tok.trim();
|
||||
let Some((ip, pfx)) = tok.split_once('/') else {
|
||||
continue;
|
||||
};
|
||||
if pfx != "32" {
|
||||
continue;
|
||||
}
|
||||
let Ok(v4) = ip.parse::<Ipv4Addr>() else {
|
||||
continue;
|
||||
};
|
||||
if config::ip_in_cidr(v4, peer_cidr)? {
|
||||
out.push(v4.to_string());
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out.dedup();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -180,4 +285,34 @@ mod tests {
|
||||
assert_eq!(peers[0].ipv4.to_string(), "10.8.1.4");
|
||||
assert_eq!(peers[1].ipv4.to_string(), "10.8.1.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_wg_show_dump_runtime_peers() {
|
||||
let raw = "wg0\tpriv\tpub\t51820\toff\nwg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0\nwg0\tpubkey2\t(none)\t(none)\t10.8.1.27/32,192.168.1.5/32\t0\t0\t0\t0";
|
||||
let peers = parse_wg_show_dump_runtime_peers(raw, "10.8.1.0/24").expect("parse");
|
||||
assert_eq!(peers.len(), 2);
|
||||
assert_eq!(peers[0].allowed_ips, vec!["10.8.1.10".to_string()]);
|
||||
assert_eq!(peers[1].allowed_ips, vec!["10.8.1.27".to_string()]);
|
||||
assert_eq!(peers[1].latest_handshake_epoch, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_allowed_ip_to_runtime_peer() {
|
||||
let raw = "wg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32,10.8.1.11/32\t1715000000\t123\t456\t0";
|
||||
let peers = parse_wg_show_dump_runtime_peers(raw, "10.8.1.0/24").expect("parse");
|
||||
assert_eq!(peers.len(), 1);
|
||||
assert_eq!(peers[0].allowed_ips.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propagates_wg_dump_command_failure() {
|
||||
let transport = TestTransport::new(vec![(
|
||||
"wg show all dump",
|
||||
Err(anyhow::anyhow!("wg broken")),
|
||||
)]);
|
||||
let err = discover_peer_runtime_with_transport(&transport, "10.8.1.0/24")
|
||||
.err()
|
||||
.expect("err");
|
||||
assert!(err.to_string().contains("wg show all dump failed"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user