Полный production-рефакторинг билдера, drift-reconcile и документации по up5
This commit is contained in:
@@ -28,7 +28,6 @@ pub struct TrafficConfig {
|
||||
pub container_ready_timeout_secs: u64,
|
||||
pub class_start_id: u16,
|
||||
pub ifb_name: String,
|
||||
pub full_rebuild_on_drift: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
||||
@@ -65,27 +65,21 @@ pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result<FirewallActual> {
|
||||
let table = transport
|
||||
.run(&["nft", "list", "table", "inet", "awg2_harness"])
|
||||
.unwrap_or_default();
|
||||
let table = inspect_cmd(
|
||||
transport,
|
||||
&["nft", "list", "table", "inet", "awg2_harness"],
|
||||
"firewall table",
|
||||
)?;
|
||||
|
||||
let table_exists = !table.trim().is_empty();
|
||||
let counter_exists = table.contains("counter antitorrent_dropped_v4");
|
||||
let chain_exists = table.contains("chain forward_antitorrent");
|
||||
|
||||
let needle_cidr = format!("ip saddr {}", cfg.anti_torrent.peer_ipv4_cidr);
|
||||
let needle_max = format!(
|
||||
"ct count over {}",
|
||||
cfg.anti_torrent.max_connections_per_peer
|
||||
let rule_matches = firewall_rule_contract_matches(
|
||||
&table,
|
||||
&cfg.anti_torrent.peer_ipv4_cidr,
|
||||
cfg.anti_torrent.max_connections_per_peer,
|
||||
);
|
||||
let rule_matches = table.contains(&needle_cidr)
|
||||
&& table.contains(&needle_max)
|
||||
&& table.contains("ct state new")
|
||||
&& table.contains("meter awg2_peer_connlimit_v4")
|
||||
&& table.contains("counter name antitorrent_dropped_v4")
|
||||
&& table.contains("drop")
|
||||
&& table.contains("hook forward")
|
||||
&& table.contains("policy accept");
|
||||
|
||||
Ok(FirewallActual {
|
||||
table_exists,
|
||||
@@ -185,6 +179,27 @@ fn run_allow_notfound(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Res
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_cmd(transport: &dyn NsTransport, args: &[&str], what: &str) -> anyhow::Result<String> {
|
||||
transport
|
||||
.run(args)
|
||||
.map_err(|e| anyhow::anyhow!("{what} inspect failed: {e}"))
|
||||
}
|
||||
|
||||
fn firewall_rule_contract_matches(table: &str, cidr: &str, max_conn: u32) -> bool {
|
||||
let needle_cidr = format!("ip saddr {cidr}");
|
||||
let needle_max = format!("ct count over {max_conn}");
|
||||
table.contains("table inet awg2_harness")
|
||||
&& table.contains("counter antitorrent_dropped_v4")
|
||||
&& table.contains("chain forward_antitorrent")
|
||||
&& table.contains("type filter hook forward priority filter; policy accept")
|
||||
&& table.contains(&needle_cidr)
|
||||
&& table.contains("ct state new")
|
||||
&& table.contains("meter awg2_peer_connlimit_v4")
|
||||
&& table.contains(&needle_max)
|
||||
&& table.contains("counter name antitorrent_dropped_v4")
|
||||
&& table.contains("drop")
|
||||
}
|
||||
|
||||
pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64)>> {
|
||||
if !cfg.anti_torrent.enabled {
|
||||
return Ok(None);
|
||||
@@ -201,38 +216,25 @@ pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64
|
||||
"antitorrent_dropped_v4",
|
||||
]);
|
||||
|
||||
if let Ok(raw) = json {
|
||||
if let Some(v) = parse_counter_json(&raw)? {
|
||||
return Ok(Some(v));
|
||||
match json {
|
||||
Ok(raw) => {
|
||||
if let Some(v) = parse_counter_json(&raw)? {
|
||||
return Ok(Some(v));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("No such file")
|
||||
|| msg.contains("No such")
|
||||
|| msg.contains("not found")
|
||||
|| msg.contains("not exist")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(anyhow::anyhow!("nft counter inspection failed: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
let table = transport
|
||||
.run(&["nft", "list", "table", "inet", "awg2_harness"])
|
||||
.unwrap_or_default();
|
||||
for line in table.lines() {
|
||||
let t = line.trim();
|
||||
if !t.contains("packets") {
|
||||
continue;
|
||||
}
|
||||
let toks: Vec<&str> = t.split_whitespace().collect();
|
||||
let packets = toks
|
||||
.iter()
|
||||
.position(|x| *x == "packets")
|
||||
.and_then(|i| toks.get(i + 1))
|
||||
.and_then(|s| s.parse::<u64>().ok());
|
||||
let bytes = toks
|
||||
.iter()
|
||||
.position(|x| *x == "bytes")
|
||||
.and_then(|i| toks.get(i + 1))
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
if let Some(p) = packets {
|
||||
return Ok(Some((p, bytes)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn parse_counter_json(raw: &str) -> anyhow::Result<Option<(u64, u64)>> {
|
||||
@@ -262,4 +264,46 @@ mod tests {
|
||||
let got = parse_counter_json(raw).expect("json parse");
|
||||
assert_eq!(got, Some((7, 448)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nft_rule_contract_parser_accepts_expected_rule() {
|
||||
let table = r#"
|
||||
table inet awg2_harness {
|
||||
counter antitorrent_dropped_v4
|
||||
chain forward_antitorrent {
|
||||
type filter hook forward priority filter; policy accept;
|
||||
ip saddr 10.8.1.0/24 ct state new meter awg2_peer_connlimit_v4 { ip saddr ct count over 100 } counter name antitorrent_dropped_v4 drop
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_counter_name_causes_drift() {
|
||||
let table = r#"
|
||||
table inet awg2_harness {
|
||||
counter antitorrent_dropped_v4
|
||||
chain forward_antitorrent {
|
||||
type filter hook forward priority filter; policy accept;
|
||||
ip saddr 10.8.1.0/24 ct state new meter awg2_peer_connlimit_v4 { ip saddr ct count over 100 } drop
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(!firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_max_connection_threshold_causes_drift() {
|
||||
let table = r#"
|
||||
table inet awg2_harness {
|
||||
counter antitorrent_dropped_v4
|
||||
chain forward_antitorrent {
|
||||
type filter hook forward priority filter; policy accept;
|
||||
ip saddr 10.8.1.0/24 ct state new meter awg2_peer_connlimit_v4 { ip saddr ct count over 250 } counter name antitorrent_dropped_v4 drop
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(!firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
|
||||
}
|
||||
}
|
||||
|
||||
+150
-78
@@ -17,6 +17,13 @@ struct TrafficDrift {
|
||||
reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FilterInspect {
|
||||
map: BTreeMap<String, u16>,
|
||||
duplicate_ips: BTreeSet<String>,
|
||||
duplicate_classes: BTreeSet<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct ClassRateCeil {
|
||||
rate: String,
|
||||
@@ -106,13 +113,6 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
|
||||
|
||||
println!("traffic drift detected: {}", drift.reason);
|
||||
|
||||
if !cfg.traffic.full_rebuild_on_drift {
|
||||
anyhow::bail!(
|
||||
"traffic drift detected but full_rebuild_on_drift=false: {}",
|
||||
drift.reason
|
||||
);
|
||||
}
|
||||
|
||||
teardown_owned_topology(
|
||||
transport.as_ref(),
|
||||
&target.interface_name,
|
||||
@@ -174,8 +174,10 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
|
||||
.run(&["tc", "class", "show", "dev", &cfg.traffic.ifb_name])
|
||||
.unwrap_or_default();
|
||||
|
||||
let (actual_map_egress, dup_egress) = extract_filter_ip_class_map(&filters_egress);
|
||||
let (actual_map_ingress, dup_ingress) = extract_filter_ip_class_map(&filters_ingress);
|
||||
let inspect_egress = extract_filter_ip_class_map(&filters_egress);
|
||||
let inspect_ingress = extract_filter_ip_class_map(&filters_ingress);
|
||||
let actual_map_egress = inspect_egress.map;
|
||||
let actual_map_ingress = inspect_ingress.map;
|
||||
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
|
||||
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
|
||||
let expected_map = expected_peer_class_map(&peers, cfg.traffic.class_start_id);
|
||||
@@ -216,10 +218,10 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
|
||||
{
|
||||
reasons.push("ingress rate/ceil mismatch".to_string());
|
||||
}
|
||||
if dup_egress.contains(&ip) {
|
||||
if inspect_egress.duplicate_ips.contains(&ip) {
|
||||
reasons.push("duplicate egress filters".to_string());
|
||||
}
|
||||
if dup_ingress.contains(&ip) {
|
||||
if inspect_ingress.duplicate_ips.contains(&ip) {
|
||||
reasons.push("duplicate ingress filters".to_string());
|
||||
}
|
||||
|
||||
@@ -252,9 +254,11 @@ fn inspect_and_diff(
|
||||
peer_limit: &str,
|
||||
peers: &[wg::Peer],
|
||||
) -> anyhow::Result<TrafficDrift> {
|
||||
let root_qdisc = transport
|
||||
.run(&["tc", "qdisc", "show", "dev", iface])
|
||||
.unwrap_or_default();
|
||||
let root_qdisc = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "qdisc", "show", "dev", iface],
|
||||
"egress qdisc",
|
||||
)?;
|
||||
if !root_qdisc.contains("htb") {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
@@ -262,9 +266,11 @@ fn inspect_and_diff(
|
||||
});
|
||||
}
|
||||
|
||||
let ingress_qdisc = transport
|
||||
.run(&["tc", "qdisc", "show", "dev", iface])
|
||||
.unwrap_or_default();
|
||||
let ingress_qdisc = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "qdisc", "show", "dev", iface],
|
||||
"ingress qdisc",
|
||||
)?;
|
||||
if !ingress_qdisc.contains("ingress") {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
@@ -272,43 +278,65 @@ fn inspect_and_diff(
|
||||
});
|
||||
}
|
||||
|
||||
let ifb_link = transport
|
||||
.run(&["ip", "link", "show", ifb])
|
||||
.unwrap_or_default();
|
||||
let ifb_link = inspect_cmd(transport, &["ip", "link", "show", ifb], "ifb link")?;
|
||||
if ifb_link.trim().is_empty() {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: "missing ifb interface".to_string(),
|
||||
});
|
||||
}
|
||||
if !ifb_link.contains(" UP ") && !ifb_link.contains("state UP") {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: "ifb interface is not UP".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let filters_egress = transport
|
||||
.run(&["tc", "filter", "show", "dev", iface, "parent", "1:"])
|
||||
.unwrap_or_default();
|
||||
let filters_ingress = transport
|
||||
.run(&["tc", "filter", "show", "dev", ifb, "parent", "2:"])
|
||||
.unwrap_or_default();
|
||||
let filters_egress = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "filter", "show", "dev", iface, "parent", "1:"],
|
||||
"egress filters",
|
||||
)?;
|
||||
let filters_ingress = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "filter", "show", "dev", ifb, "parent", "2:"],
|
||||
"ingress filters",
|
||||
)?;
|
||||
|
||||
let (actual_map_egress, dup_egress) = extract_filter_ip_class_map(&filters_egress);
|
||||
let (actual_map_ingress, dup_ingress) = extract_filter_ip_class_map(&filters_ingress);
|
||||
if !dup_egress.is_empty() {
|
||||
let inspect_egress = extract_filter_ip_class_map(&filters_egress);
|
||||
let inspect_ingress = extract_filter_ip_class_map(&filters_ingress);
|
||||
if !inspect_egress.duplicate_ips.is_empty() {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: format!(
|
||||
"duplicate egress filters detected for {} peer(s)",
|
||||
dup_egress.len()
|
||||
inspect_egress.duplicate_ips.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
if !dup_ingress.is_empty() {
|
||||
if !inspect_egress.duplicate_classes.is_empty() {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: "duplicate egress class flowid mapping detected".to_string(),
|
||||
});
|
||||
}
|
||||
if !inspect_ingress.duplicate_ips.is_empty() {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: format!(
|
||||
"duplicate ingress filters detected for {} peer(s)",
|
||||
dup_ingress.len()
|
||||
inspect_ingress.duplicate_ips.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
if !inspect_ingress.duplicate_classes.is_empty() {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: "duplicate ingress class flowid mapping detected".to_string(),
|
||||
});
|
||||
}
|
||||
let actual_map_egress = inspect_egress.map;
|
||||
let actual_map_ingress = inspect_ingress.map;
|
||||
let actual_ips_egress: BTreeSet<String> = actual_map_egress.keys().cloned().collect();
|
||||
let actual_ips_ingress: BTreeSet<String> = actual_map_ingress.keys().cloned().collect();
|
||||
let expected_ips: BTreeSet<String> = peers.iter().map(|p| p.ipv4.to_string()).collect();
|
||||
@@ -358,12 +386,16 @@ fn inspect_and_diff(
|
||||
}
|
||||
|
||||
let expected_limit = normalize_tc_rate(peer_limit);
|
||||
let classes_egress_full = transport
|
||||
.run(&["tc", "class", "show", "dev", iface])
|
||||
.unwrap_or_default();
|
||||
let classes_ingress_full = transport
|
||||
.run(&["tc", "class", "show", "dev", ifb])
|
||||
.unwrap_or_default();
|
||||
let classes_egress_full = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "class", "show", "dev", iface],
|
||||
"egress classes",
|
||||
)?;
|
||||
let classes_ingress_full = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "class", "show", "dev", ifb],
|
||||
"ingress classes",
|
||||
)?;
|
||||
let expected_root = normalize_tc_rate(root_rate);
|
||||
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
|
||||
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
|
||||
@@ -414,32 +446,40 @@ fn inspect_and_diff(
|
||||
}
|
||||
}
|
||||
|
||||
let classes_egress = transport
|
||||
.run(&["tc", "class", "show", "dev", iface, "parent", "1:1"])
|
||||
.unwrap_or_default();
|
||||
let classes_ingress = transport
|
||||
.run(&["tc", "class", "show", "dev", ifb, "parent", "2:1"])
|
||||
.unwrap_or_default();
|
||||
let expected_class_count = peers.len();
|
||||
if count_leaf_classes(&classes_egress) != expected_class_count {
|
||||
let classes_egress = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "class", "show", "dev", iface, "parent", "1:1"],
|
||||
"egress leaf classes",
|
||||
)?;
|
||||
let classes_ingress = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "class", "show", "dev", ifb, "parent", "2:1"],
|
||||
"ingress leaf classes",
|
||||
)?;
|
||||
let expected_set: BTreeSet<u16> = expected_map.values().copied().collect();
|
||||
if collect_leaf_class_ids(&classes_egress) != expected_set {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: "egress per-peer class count mismatch".to_string(),
|
||||
reason: "egress per-peer class set mismatch".to_string(),
|
||||
});
|
||||
}
|
||||
if count_leaf_classes(&classes_ingress) != expected_class_count {
|
||||
if collect_leaf_class_ids(&classes_ingress) != expected_set {
|
||||
return Ok(TrafficDrift {
|
||||
has_drift: true,
|
||||
reason: "ingress per-peer class count mismatch".to_string(),
|
||||
reason: "ingress per-peer class set mismatch".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let qdisc_egress = transport
|
||||
.run(&["tc", "qdisc", "show", "dev", iface])
|
||||
.unwrap_or_default();
|
||||
let qdisc_ingress = transport
|
||||
.run(&["tc", "qdisc", "show", "dev", ifb])
|
||||
.unwrap_or_default();
|
||||
let qdisc_egress = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "qdisc", "show", "dev", iface],
|
||||
"egress leaf qdisc",
|
||||
)?;
|
||||
let qdisc_ingress = inspect_cmd(
|
||||
transport,
|
||||
&["tc", "qdisc", "show", "dev", ifb],
|
||||
"ingress leaf qdisc",
|
||||
)?;
|
||||
for class_id in expected_map.values() {
|
||||
let class_egress = format!("1:{class_id}");
|
||||
let class_ingress = format!("2:{class_id}");
|
||||
@@ -463,9 +503,17 @@ fn inspect_and_diff(
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_filter_ip_class_map(filters: &str) -> (BTreeMap<String, u16>, BTreeSet<String>) {
|
||||
fn inspect_cmd(transport: &dyn NsTransport, args: &[&str], what: &str) -> anyhow::Result<String> {
|
||||
transport
|
||||
.run(args)
|
||||
.map_err(|e| anyhow::anyhow!("{what} inspect failed: {e}"))
|
||||
}
|
||||
|
||||
fn extract_filter_ip_class_map(filters: &str) -> FilterInspect {
|
||||
let mut out = BTreeMap::new();
|
||||
let mut duplicates = BTreeSet::new();
|
||||
let mut duplicate_ips = BTreeSet::new();
|
||||
let mut class_to_ip = BTreeMap::new();
|
||||
let mut duplicate_classes = BTreeSet::new();
|
||||
|
||||
for block in split_filter_blocks(filters) {
|
||||
let mut ip: Option<String> = None;
|
||||
@@ -483,13 +531,20 @@ fn extract_filter_ip_class_map(filters: &str) -> (BTreeMap<String, u16>, BTreeSe
|
||||
|
||||
if let (Some(ip), Some(class_id)) = (ip, class_id) {
|
||||
if out.contains_key(&ip) {
|
||||
duplicates.insert(ip.clone());
|
||||
duplicate_ips.insert(ip.clone());
|
||||
}
|
||||
if class_to_ip.insert(class_id, ip.clone()).is_some() {
|
||||
duplicate_classes.insert(class_id);
|
||||
}
|
||||
out.insert(ip, class_id);
|
||||
}
|
||||
}
|
||||
|
||||
(out, duplicates)
|
||||
FilterInspect {
|
||||
map: out,
|
||||
duplicate_ips,
|
||||
duplicate_classes,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_fq_codel_for_parent(raw_qdisc: &str, parent_class: &str) -> bool {
|
||||
@@ -585,16 +640,24 @@ fn hex_to_ipv4_string(hex: &str) -> Option<String> {
|
||||
Some(Ipv4Addr::new(b1, b2, b3, b4).to_string())
|
||||
}
|
||||
|
||||
fn count_leaf_classes(raw: &str) -> usize {
|
||||
raw.lines()
|
||||
.filter_map(|l| {
|
||||
let line = l.trim();
|
||||
let pos = line.find("class htb ")?;
|
||||
let class = &line[(pos + "class htb ".len())..];
|
||||
let head = class.split_whitespace().next()?;
|
||||
if head.ends_with(":1") { None } else { Some(()) }
|
||||
})
|
||||
.count()
|
||||
fn collect_leaf_class_ids(raw: &str) -> BTreeSet<u16> {
|
||||
let mut out = BTreeSet::new();
|
||||
for l in raw.lines() {
|
||||
let line = l.trim();
|
||||
let Some(pos) = line.find("class htb ") else {
|
||||
continue;
|
||||
};
|
||||
let class = &line[(pos + "class htb ".len())..];
|
||||
let Some(head) = class.split_whitespace().next() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(class_id) = head.split(':').nth(1).and_then(|v| v.parse::<u16>().ok())
|
||||
&& class_id != 1
|
||||
{
|
||||
out.insert(class_id);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn teardown_owned_topology(
|
||||
@@ -876,25 +939,34 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_tc_u32_hex_ipv4() {
|
||||
let raw = " match 0a080105/ffffffff at 16\n flowid 1:42";
|
||||
let (m, dup) = extract_filter_ip_class_map(raw);
|
||||
assert!(dup.is_empty());
|
||||
assert_eq!(m.get("10.8.1.5").copied(), Some(42));
|
||||
let inspect = extract_filter_ip_class_map(raw);
|
||||
assert!(inspect.duplicate_ips.is_empty());
|
||||
assert!(inspect.duplicate_classes.is_empty());
|
||||
assert_eq!(inspect.map.get("10.8.1.5").copied(), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_block_based_filters() {
|
||||
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080103/ffffffff at 16\n flowid 1:11";
|
||||
let (m, dup) = extract_filter_ip_class_map(raw);
|
||||
assert!(dup.is_empty());
|
||||
assert_eq!(m.get("10.8.1.2").copied(), Some(10));
|
||||
assert_eq!(m.get("10.8.1.3").copied(), Some(11));
|
||||
let inspect = extract_filter_ip_class_map(raw);
|
||||
assert!(inspect.duplicate_ips.is_empty());
|
||||
assert!(inspect.duplicate_classes.is_empty());
|
||||
assert_eq!(inspect.map.get("10.8.1.2").copied(), Some(10));
|
||||
assert_eq!(inspect.map.get("10.8.1.3").copied(), Some(11));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_duplicate_filter_ip() {
|
||||
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080102/ffffffff at 16\n flowid 1:10";
|
||||
let (_m, dup) = extract_filter_ip_class_map(raw);
|
||||
assert!(dup.contains("10.8.1.2"));
|
||||
let inspect = extract_filter_ip_class_map(raw);
|
||||
assert!(inspect.duplicate_ips.contains("10.8.1.2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_duplicate_filter_class_flowid() {
|
||||
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080103/ffffffff at 16\n flowid 1:10";
|
||||
let inspect = extract_filter_ip_class_map(raw);
|
||||
assert!(inspect.duplicate_classes.contains(&10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -95,6 +95,34 @@ fn parse_fallback_wg_show(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct TestTransport {
|
||||
replies: Mutex<BTreeMap<String, anyhow::Result<String>>>,
|
||||
}
|
||||
|
||||
impl TestTransport {
|
||||
fn new(entries: Vec<(&str, anyhow::Result<&str>)>) -> Self {
|
||||
let mut replies = BTreeMap::new();
|
||||
for (cmd, out) in entries {
|
||||
let mapped = out.map(|s| s.to_string());
|
||||
replies.insert(cmd.to_string(), mapped);
|
||||
}
|
||||
Self {
|
||||
replies: Mutex::new(replies),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::netns::NsTransport for TestTransport {
|
||||
fn run(&self, args: &[&str]) -> anyhow::Result<String> {
|
||||
let key = args.join(" ");
|
||||
let mut lock = self.replies.lock().expect("lock");
|
||||
lock.remove(&key)
|
||||
.unwrap_or_else(|| Err(anyhow::anyhow!("missing stub for: {key}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_primary() {
|
||||
@@ -126,4 +154,30 @@ mod tests {
|
||||
assert_eq!(peers[0].ipv4.to_string(), "10.8.1.10");
|
||||
assert_eq!(peers[1].ipv4.to_string(), "10.8.1.2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_failure_is_propagated() {
|
||||
let transport = TestTransport::new(vec![(
|
||||
"wg show all allowed-ips",
|
||||
Err(anyhow::anyhow!("wg broken")),
|
||||
)]);
|
||||
|
||||
let err = discover_peers_with_transport(&transport, "10.8.1.0/24").err();
|
||||
assert!(err.is_some());
|
||||
let msg = err.expect("err").to_string();
|
||||
assert!(msg.contains("wg show all allowed-ips failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_primary_output_falls_back_to_wg_show() {
|
||||
let transport = TestTransport::new(vec![
|
||||
("wg show all allowed-ips", Ok("")),
|
||||
("wg show", Ok("allowed ips: 10.8.1.4/32, 10.8.1.5/32")),
|
||||
]);
|
||||
|
||||
let peers = discover_peers_with_transport(&transport, "10.8.1.0/24").expect("peers");
|
||||
assert_eq!(peers.len(), 2);
|
||||
assert_eq!(peers[0].ipv4.to_string(), "10.8.1.4");
|
||||
assert_eq!(peers[1].ipv4.to_string(), "10.8.1.5");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user