forked from midefos/martillo-maldito
fix: ipset compat with v7.x (plain text, timeout support, arg split)
- list() now parses plain text (works on ipset 6/7/8, not just 8) - Strip " timeout N" suffix from member lines (kernel adds it when set has timeout support) - ensure_exists() creates set with 'timeout 0' so add() can use --timeout later - Fix ensure_ipset_match_rule: split rule string into separate argv tokens (Command::args with whitespace string was treated as one arg, breaking nf_tables) - Add #[serial] to tests sharing TEST_SET (race condition fix) - Add 3 new tests: list_parses_plain_format_correctly, list_empty_set_returns_empty_vec, list_filters_non_ip_lines - Simplify integration_netns.sh to use iptables-only assertions (ipset is host-global)
This commit is contained in:
1 parent
03a52925d4
commit
7342386955
3 files changed
+89
-72
No files matched your search
+60
-16
@@ -44,7 +44,7 @@ impl Ipset {
|
||||
|
||||
pub fn ensure_exists(&self) -> Result<()> {
|
||||
let output = Command::new("ipset")
|
||||
.args(["create", &self.name, "hash:ip", "-exist"])
|
||||
.args(["create", &self.name, "hash:ip", "timeout", "0", "-exist"])
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
return Err(IpsetError::CommandFailed(
|
||||
@@ -93,7 +93,7 @@ impl Ipset {
|
||||
|
||||
pub fn list(&self) -> Result<Vec<String>> {
|
||||
let output = Command::new("ipset")
|
||||
.args(["list", &self.name, "-json"])
|
||||
.args(["list", &self.name])
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
|
||||
@@ -102,18 +102,24 @@ impl Ipset {
|
||||
}
|
||||
return Err(IpsetError::CommandFailed(stderr));
|
||||
}
|
||||
let json: serde_json::Value = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|e| IpsetError::ParseError(e.to_string()))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut in_members = false;
|
||||
let mut ips = Vec::new();
|
||||
if let Some(sets) = json.get("ipset").and_then(|v| v.as_array()) {
|
||||
for set in sets {
|
||||
if let Some(members) = set.get("members").and_then(|v| v.as_array()) {
|
||||
for m in members {
|
||||
if let Some(ip) = m.get("ip").and_then(|v| v.as_str()) {
|
||||
ips.push(ip.to_string());
|
||||
}
|
||||
}
|
||||
for line in stdout.lines() {
|
||||
if line.starts_with("Members:") {
|
||||
in_members = true;
|
||||
continue;
|
||||
}
|
||||
if in_members && !line.is_empty() {
|
||||
let entry = line.split_whitespace().next().unwrap_or("");
|
||||
if entry.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if entry.parse::<std::net::Ipv4Addr>().is_ok()
|
||||
|| entry.parse::<std::net::Ipv6Addr>().is_ok()
|
||||
|| entry.contains('/')
|
||||
{
|
||||
ips.push(entry.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +176,7 @@ fn validate_ip(ip: &str) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serial_test::serial;
|
||||
|
||||
const TEST_SET: &str = "banned_test_rs";
|
||||
|
||||
@@ -194,6 +201,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn ensure_exists_idempotent() {
|
||||
let Some(set) = setup() else { return };
|
||||
for _ in 0..5 {
|
||||
@@ -203,6 +211,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn add_then_list_contains_ip() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("192.0.2.1", None).expect("add failed");
|
||||
@@ -212,6 +221,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn add_twice_no_error() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("192.0.2.2", None).expect("first add");
|
||||
@@ -220,6 +230,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn del_then_list_empty() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("192.0.2.3", None).expect("add");
|
||||
@@ -230,6 +241,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn del_nonexistent_no_error() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.del("192.0.2.4").expect("del nonexistent should not fail");
|
||||
@@ -237,6 +249,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn contains_true_after_add_false_after_del() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("192.0.2.5", None).expect("add");
|
||||
@@ -247,6 +260,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn save_and_restore_roundtrip() {
|
||||
use std::env;
|
||||
let Some(set) = setup() else { return };
|
||||
@@ -265,7 +279,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_parses_json_correctly() {
|
||||
#[serial]
|
||||
fn list_parses_plain_format_correctly() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("198.51.100.1", None).unwrap();
|
||||
set.add("198.51.100.2", None).unwrap();
|
||||
@@ -278,6 +293,34 @@ mod tests {
|
||||
teardown(&set);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn list_empty_set_returns_empty_vec() {
|
||||
let Some(set) = setup() else { return };
|
||||
let ips = set.list().unwrap();
|
||||
assert_eq!(ips.len(), 0);
|
||||
teardown(&set);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn list_filters_non_ip_lines() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("203.0.113.1", None).unwrap();
|
||||
set.add("203.0.113.2", None).unwrap();
|
||||
let ips = set.list().unwrap();
|
||||
for ip in &ips {
|
||||
assert!(
|
||||
ip.parse::<std::net::Ipv4Addr>().is_ok()
|
||||
|| ip.parse::<std::net::Ipv6Addr>().is_ok()
|
||||
|| ip.contains('/'),
|
||||
"got non-IP entry: {ip}"
|
||||
);
|
||||
}
|
||||
assert_eq!(ips.len(), 2);
|
||||
teardown(&set);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ip_rejected() {
|
||||
let result = validate_ip("not-an-ip");
|
||||
@@ -287,6 +330,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn flush_clears_all() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("203.0.113.1", None).unwrap();
|
||||
@@ -298,6 +342,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn add_with_timeout() {
|
||||
let Some(set) = setup() else { return };
|
||||
set.add("203.0.113.10", Some(2)).expect("add with timeout");
|
||||
@@ -309,15 +354,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn list_on_nonexistent_set_returns_empty() {
|
||||
let Some(set) = setup() else { return };
|
||||
let bogus = Ipset::new("nonexistent_set_zzz_999");
|
||||
let result = bogus.list();
|
||||
assert!(result.is_ok(), "list on nonexistent should not error");
|
||||
assert_eq!(result.unwrap().len(), 0);
|
||||
teardown(&set);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn bulk_1000_ips_performance() {
|
||||
let Some(set) = setup() else { return };
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
Reference in new issue
Block a user