266 lines
7.0 KiB
Rust
266 lines
7.0 KiB
Rust
use crate::{
|
|
config::Config,
|
|
docker,
|
|
netns::{NsTransport, build_host_transport, build_transport},
|
|
};
|
|
use serde_json::Value;
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct FirewallActual {
|
|
table_exists: bool,
|
|
counter_exists: bool,
|
|
chain_exists: bool,
|
|
rule_matches: bool,
|
|
}
|
|
|
|
pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
|
|
let transport = firewall_transport(cfg)?;
|
|
|
|
if !cfg.anti_torrent.enabled {
|
|
run_allow_notfound(
|
|
transport.as_ref(),
|
|
&["nft", "delete", "table", "inet", "awg2_harness"],
|
|
)?;
|
|
return Ok(());
|
|
}
|
|
|
|
let actual = inspect_firewall(transport.as_ref(), cfg)?;
|
|
if actual.table_exists && actual.counter_exists && actual.chain_exists && actual.rule_matches {
|
|
return Ok(());
|
|
}
|
|
|
|
run_allow_notfound(
|
|
transport.as_ref(),
|
|
&["nft", "delete", "table", "inet", "awg2_harness"],
|
|
)?;
|
|
|
|
run_step(
|
|
transport.as_ref(),
|
|
&["nft", "add", "table", "inet", "awg2_harness"],
|
|
)?;
|
|
run_step(
|
|
transport.as_ref(),
|
|
&[
|
|
"nft",
|
|
"add",
|
|
"counter",
|
|
"inet",
|
|
"awg2_harness",
|
|
"antitorrent_dropped_v4",
|
|
],
|
|
)?;
|
|
run_step_chain(transport.as_ref())?;
|
|
apply_rule(
|
|
transport.as_ref(),
|
|
&cfg.anti_torrent.peer_ipv4_cidr,
|
|
cfg.anti_torrent.max_connections_per_peer,
|
|
)?;
|
|
|
|
let verify = inspect_firewall(transport.as_ref(), cfg)?;
|
|
if !(verify.table_exists && verify.counter_exists && verify.chain_exists && verify.rule_matches)
|
|
{
|
|
anyhow::bail!("firewall verify-after-apply failed");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
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_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 = 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,
|
|
counter_exists,
|
|
chain_exists,
|
|
rule_matches,
|
|
})
|
|
}
|
|
|
|
fn firewall_transport(cfg: &Config) -> anyhow::Result<Box<dyn NsTransport>> {
|
|
if cfg.anti_torrent.apply_in_amnezia_netns {
|
|
let target = docker::discover_target(cfg)?;
|
|
Ok(build_transport(
|
|
target.container_pid,
|
|
&target.container_name,
|
|
))
|
|
} else {
|
|
Ok(build_host_transport())
|
|
}
|
|
}
|
|
|
|
fn apply_rule(transport: &dyn NsTransport, cidr: &str, max_conn: u32) -> anyhow::Result<()> {
|
|
let _ = transport.run(&[
|
|
"nft",
|
|
"add",
|
|
"rule",
|
|
"inet",
|
|
"awg2_harness",
|
|
"forward_antitorrent",
|
|
"ip",
|
|
"saddr",
|
|
cidr,
|
|
"ct",
|
|
"state",
|
|
"new",
|
|
"meter",
|
|
"awg2_peer_connlimit_v4",
|
|
"{",
|
|
"ip",
|
|
"saddr",
|
|
"ct",
|
|
"count",
|
|
"over",
|
|
&max_conn.to_string(),
|
|
"}",
|
|
"counter",
|
|
"name",
|
|
"antitorrent_dropped_v4",
|
|
"drop",
|
|
])?;
|
|
Ok(())
|
|
}
|
|
|
|
fn run_step(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Result<()> {
|
|
let _ = transport.run(args)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn run_step_chain(transport: &dyn NsTransport) -> anyhow::Result<()> {
|
|
run_step(
|
|
transport,
|
|
&[
|
|
"nft",
|
|
"add",
|
|
"chain",
|
|
"inet",
|
|
"awg2_harness",
|
|
"forward_antitorrent",
|
|
"{",
|
|
"type",
|
|
"filter",
|
|
"hook",
|
|
"forward",
|
|
"priority",
|
|
"filter;",
|
|
"policy",
|
|
"accept;",
|
|
"}",
|
|
],
|
|
)
|
|
}
|
|
|
|
fn run_allow_notfound(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Result<()> {
|
|
match transport.run(args) {
|
|
Ok(_) => Ok(()),
|
|
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(());
|
|
}
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64)>> {
|
|
if !cfg.anti_torrent.enabled {
|
|
return Ok(None);
|
|
}
|
|
|
|
let transport = firewall_transport(cfg)?;
|
|
let json = transport.run(&[
|
|
"nft",
|
|
"-j",
|
|
"list",
|
|
"counter",
|
|
"inet",
|
|
"awg2_harness",
|
|
"antitorrent_dropped_v4",
|
|
]);
|
|
|
|
if let Ok(raw) = json {
|
|
if let Some(v) = parse_counter_json(&raw)? {
|
|
return Ok(Some(v));
|
|
}
|
|
}
|
|
|
|
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)>> {
|
|
let v: Value = serde_json::from_str(raw)?;
|
|
let Some(arr) = v.get("nftables").and_then(|x| x.as_array()) else {
|
|
return Ok(None);
|
|
};
|
|
for item in arr {
|
|
if let Some(counter) = item.get("counter") {
|
|
let packets = counter.get("packets").and_then(|x| x.as_u64());
|
|
let bytes = counter.get("bytes").and_then(|x| x.as_u64());
|
|
if let (Some(p), Some(b)) = (packets, bytes) {
|
|
return Ok(Some((p, b)));
|
|
}
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_nft_counter_json() {
|
|
let raw = r#"{"nftables":[{"metainfo":{"json_schema_version":1}},{"counter":{"family":"inet","table":"awg2_harness","name":"antitorrent_dropped_v4","handle":2,"packets":7,"bytes":448}}]}"#;
|
|
let got = parse_counter_json(raw).expect("json parse");
|
|
assert_eq!(got, Some((7, 448)));
|
|
}
|
|
}
|