- 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)
- New ipset.rs module wrapping ipset binary (hash:ip backend)
- ban_ip() and unban_ip() use ipset.add/del instead of per-IP iptables rules
- get_banned_ips() parses ipset list -json
- Optional timeout via BAN_DURATION_SECS (default 3600s = 1h)
- Auto-installs single iptables rule: -m set --match-set banned src -j DROP
- New CLI subcommands: ban-ip, unban-ip (with --timeout)
- 13 unit tests for ipset module + integration test script using netns
- Update Woodpecker CI to install ipset