fix8: продакшн host firewall + dual reconcile firewall
- добавлена секция host_firewall в конфиг и defaults\n- реализован host firewall reconcile в отдельной таблице\n- apply-firewall теперь делает host firewall + anti-torrent\n- убрана JSON-инспекция из full-table drift для anti-torrent\n- расширен status вывод drift по host firewall/anti-torrent
This commit is contained in:
+230
-53
@@ -7,6 +7,9 @@ use crate::{
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
|
||||
const ANTITORRENT_TABLE: &str = "awg2_harness";
|
||||
const HOST_FW_TABLE: &str = "awg2_host_firewall";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FirewallActual {
|
||||
table_exists: bool,
|
||||
@@ -25,8 +28,14 @@ pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
let _lock =
|
||||
FileLock::acquire_timeout("/run/awg2-harness/firewall.lock", Duration::from_secs(10))?;
|
||||
|
||||
apply_host_firewall(cfg)?;
|
||||
apply_antitorrent_firewall(cfg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_antitorrent_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
if !cfg.anti_torrent.enabled {
|
||||
apply_firewall_disabled(cfg)?;
|
||||
apply_antitorrent_disabled(cfg)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -39,12 +48,12 @@ pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
|
||||
run_allow_notfound(
|
||||
transport.as_ref(),
|
||||
&["nft", "delete", "table", "inet", "awg2_harness"],
|
||||
&["nft", "delete", "table", "inet", ANTITORRENT_TABLE],
|
||||
)?;
|
||||
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&["nft", "add", "table", "inet", "awg2_harness"],
|
||||
&["nft", "add", "table", "inet", ANTITORRENT_TABLE],
|
||||
)?;
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
@@ -53,7 +62,7 @@ pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
"add",
|
||||
"counter",
|
||||
"inet",
|
||||
"awg2_harness",
|
||||
ANTITORRENT_TABLE,
|
||||
"antitorrent_dropped_v4",
|
||||
],
|
||||
)?;
|
||||
@@ -67,21 +76,21 @@ pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
let verify = inspect_firewall(transport.as_ref(), cfg)?;
|
||||
if firewall_has_drift(&verify) {
|
||||
anyhow::bail!(
|
||||
"firewall verify-after-apply failed: {}",
|
||||
"anti-torrent firewall verify-after-apply failed: {}",
|
||||
firewall_drift_reason(&verify)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_firewall_disabled(cfg: &Config) -> anyhow::Result<()> {
|
||||
fn apply_antitorrent_disabled(cfg: &Config) -> anyhow::Result<()> {
|
||||
if cfg.anti_torrent.apply_in_amnezia_netns {
|
||||
match docker::discover_target(cfg) {
|
||||
Ok(target) => {
|
||||
let transport = build_transport(target.container_pid, &target.container_name);
|
||||
run_allow_notfound(
|
||||
transport.as_ref(),
|
||||
&["nft", "delete", "table", "inet", "awg2_harness"],
|
||||
&["nft", "delete", "table", "inet", ANTITORRENT_TABLE],
|
||||
)?;
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -92,21 +101,157 @@ fn apply_firewall_disabled(cfg: &Config) -> anyhow::Result<()> {
|
||||
let transport = build_host_transport();
|
||||
run_allow_notfound(
|
||||
transport.as_ref(),
|
||||
&["nft", "delete", "table", "inet", "awg2_harness"],
|
||||
&["nft", "delete", "table", "inet", ANTITORRENT_TABLE],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_host_firewall(cfg: &Config) -> anyhow::Result<()> {
|
||||
let transport = build_host_transport();
|
||||
if !cfg.host_firewall.enabled {
|
||||
run_allow_notfound(
|
||||
transport.as_ref(),
|
||||
&["nft", "delete", "table", "inet", HOST_FW_TABLE],
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let policy = cfg
|
||||
.host_firewall
|
||||
.default_input_policy
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
|
||||
run_allow_notfound(
|
||||
transport.as_ref(),
|
||||
&["nft", "delete", "table", "inet", HOST_FW_TABLE],
|
||||
)?;
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&["nft", "add", "table", "inet", HOST_FW_TABLE],
|
||||
)?;
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&[
|
||||
"nft",
|
||||
"add",
|
||||
"chain",
|
||||
"inet",
|
||||
HOST_FW_TABLE,
|
||||
"input",
|
||||
"{",
|
||||
"type",
|
||||
"filter",
|
||||
"hook",
|
||||
"input",
|
||||
"priority",
|
||||
"filter;",
|
||||
"policy",
|
||||
&format!("{};", policy),
|
||||
"}",
|
||||
],
|
||||
)?;
|
||||
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&[
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
HOST_FW_TABLE,
|
||||
"input",
|
||||
"iif",
|
||||
"lo",
|
||||
"accept",
|
||||
],
|
||||
)?;
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&[
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
HOST_FW_TABLE,
|
||||
"input",
|
||||
"ct",
|
||||
"state",
|
||||
"established,related",
|
||||
"accept",
|
||||
],
|
||||
)?;
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&[
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
HOST_FW_TABLE,
|
||||
"input",
|
||||
"tcp",
|
||||
"dport",
|
||||
&cfg.host_firewall.ssh_port.to_string(),
|
||||
"accept",
|
||||
],
|
||||
)?;
|
||||
for proto in &cfg.host_firewall.amnezia_protocols {
|
||||
let p = proto.trim().to_ascii_lowercase();
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&[
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
HOST_FW_TABLE,
|
||||
"input",
|
||||
&p,
|
||||
"dport",
|
||||
&cfg.host_firewall.amnezia_port.to_string(),
|
||||
"accept",
|
||||
],
|
||||
)?;
|
||||
}
|
||||
if cfg.host_firewall.allow_icmp {
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&[
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
HOST_FW_TABLE,
|
||||
"input",
|
||||
"ip",
|
||||
"protocol",
|
||||
"icmp",
|
||||
"accept",
|
||||
],
|
||||
)?;
|
||||
run_step(
|
||||
transport.as_ref(),
|
||||
&[
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
HOST_FW_TABLE,
|
||||
"input",
|
||||
"ip6",
|
||||
"nexthdr",
|
||||
"icmpv6",
|
||||
"accept",
|
||||
],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result<FirewallActual> {
|
||||
let json = transport.run(&["nft", "-j", "list", "table", "inet", "awg2_harness"]);
|
||||
if let Ok(raw) = json {
|
||||
if let Some(actual) = inspect_firewall_json(&raw, cfg)? {
|
||||
return Ok(actual);
|
||||
}
|
||||
}
|
||||
|
||||
let table = match transport.run(&["nft", "list", "table", "inet", "awg2_harness"]) {
|
||||
let table = match transport.run(&["nft", "list", "table", "inet", ANTITORRENT_TABLE]) {
|
||||
Ok(out) => out,
|
||||
Err(e) if is_nft_not_found(&e.to_string()) => {
|
||||
return Ok(FirewallActual {
|
||||
@@ -168,13 +313,79 @@ fn firewall_transport(cfg: &Config) -> anyhow::Result<Box<dyn NsTransport>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn antitorrent_drift_reason(cfg: &Config) -> anyhow::Result<Option<String>> {
|
||||
if !cfg.anti_torrent.enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
let transport = firewall_transport(cfg)?;
|
||||
let actual = inspect_firewall(transport.as_ref(), cfg)?;
|
||||
if firewall_has_drift(&actual) {
|
||||
return Ok(Some(firewall_drift_reason(&actual)));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn host_firewall_drift_reason(cfg: &Config) -> anyhow::Result<Option<String>> {
|
||||
if !cfg.host_firewall.enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
let transport = build_host_transport();
|
||||
let table = match transport.run(&["nft", "list", "table", "inet", HOST_FW_TABLE]) {
|
||||
Ok(out) => out,
|
||||
Err(e) if is_nft_not_found(&e.to_string()) => return Ok(Some("missing nft table".into())),
|
||||
Err(e) => return Err(anyhow::anyhow!("host firewall inspect failed: {e}")),
|
||||
};
|
||||
let nt = normalize_nft_text(&table);
|
||||
let ssh = format!("tcp dport {} accept", cfg.host_firewall.ssh_port);
|
||||
if !nt.contains("chain input") {
|
||||
return Ok(Some("missing input chain".into()));
|
||||
}
|
||||
if !nt.contains("type filter hook input") {
|
||||
return Ok(Some("missing input hook".into()));
|
||||
}
|
||||
let pol = format!(
|
||||
"policy {}",
|
||||
cfg.host_firewall.default_input_policy.to_ascii_lowercase()
|
||||
);
|
||||
if !nt.contains(&pol) {
|
||||
return Ok(Some("wrong input policy".into()));
|
||||
}
|
||||
if !nt.contains("iif lo accept") {
|
||||
return Ok(Some("missing loopback allow".into()));
|
||||
}
|
||||
if !(nt.contains("ct state established,related accept")
|
||||
|| nt.contains("ct state related,established accept"))
|
||||
{
|
||||
return Ok(Some("missing established/related allow".into()));
|
||||
}
|
||||
if !nt.contains(&ssh) {
|
||||
return Ok(Some("missing ssh allow rule".into()));
|
||||
}
|
||||
for proto in &cfg.host_firewall.amnezia_protocols {
|
||||
let p = proto.trim().to_ascii_lowercase();
|
||||
let need = format!("{p} dport {} accept", cfg.host_firewall.amnezia_port);
|
||||
if !nt.contains(&need) {
|
||||
return Ok(Some(format!("missing amnezia allow rule for {p}")));
|
||||
}
|
||||
}
|
||||
if cfg.host_firewall.allow_icmp {
|
||||
if !nt.contains("ip protocol icmp accept") {
|
||||
return Ok(Some("missing icmp allow rule".into()));
|
||||
}
|
||||
if !nt.contains("ip6 nexthdr icmpv6 accept") {
|
||||
return Ok(Some("missing icmpv6 allow rule".into()));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn apply_rule(transport: &dyn NsTransport, cidr: &str, max_conn: u32) -> anyhow::Result<()> {
|
||||
let _ = transport.run(&[
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
"awg2_harness",
|
||||
ANTITORRENT_TABLE,
|
||||
"forward_antitorrent",
|
||||
"ip",
|
||||
"saddr",
|
||||
@@ -213,7 +424,7 @@ fn run_step_chain(transport: &dyn NsTransport) -> anyhow::Result<()> {
|
||||
"add",
|
||||
"chain",
|
||||
"inet",
|
||||
"awg2_harness",
|
||||
ANTITORRENT_TABLE,
|
||||
"forward_antitorrent",
|
||||
"{",
|
||||
"type",
|
||||
@@ -330,40 +541,6 @@ fn normalize_nft_text(input: &str) -> String {
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn inspect_firewall_json(raw: &str, cfg: &Config) -> anyhow::Result<Option<FirewallActual>> {
|
||||
let v: Value = serde_json::from_str(raw)?;
|
||||
let Some(arr) = v.get("nftables").and_then(|x| x.as_array()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut text = String::new();
|
||||
for item in arr {
|
||||
text.push_str(&item.to_string());
|
||||
text.push(' ');
|
||||
}
|
||||
let nt = normalize_nft_text(&text);
|
||||
let table_exists = nt.contains("awg2_harness");
|
||||
if !table_exists {
|
||||
return Ok(None);
|
||||
}
|
||||
let actual = FirewallActual {
|
||||
table_exists,
|
||||
counter_exists: nt.contains("antitorrent_dropped_v4"),
|
||||
chain_exists: nt.contains("forward_antitorrent"),
|
||||
rule_matches: firewall_rule_contract_matches(
|
||||
&nt,
|
||||
&cfg.anti_torrent.peer_ipv4_cidr,
|
||||
cfg.anti_torrent.max_connections_per_peer,
|
||||
),
|
||||
hook_forward_present: nt.contains("hook") && nt.contains("forward"),
|
||||
policy_accept_present: nt.contains("policy") && nt.contains("accept"),
|
||||
ct_state_new_present: nt.contains("ct") && nt.contains("state") && nt.contains("new"),
|
||||
meter_present: nt.contains("awg2_peer_connlimit_v4"),
|
||||
counter_name_used: nt.contains("antitorrent_dropped_v4"),
|
||||
drop_present: nt.contains("drop"),
|
||||
};
|
||||
Ok(Some(actual))
|
||||
}
|
||||
|
||||
pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64)>> {
|
||||
if !cfg.anti_torrent.enabled {
|
||||
return Ok(None);
|
||||
@@ -376,7 +553,7 @@ pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64
|
||||
"list",
|
||||
"counter",
|
||||
"inet",
|
||||
"awg2_harness",
|
||||
ANTITORRENT_TABLE,
|
||||
"antitorrent_dropped_v4",
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user