Добавлен fallback диагностики FORWARD через nft и расширен вывод status
This commit is contained in:
@@ -146,6 +146,8 @@
|
|||||||
- [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке.
|
- [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке.
|
||||||
- [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика.
|
- [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика.
|
||||||
- [ ] `status` показывает блок `Docker/Amnezia port path` с полями `docker_userland_proxy`, `published_udp_port` и note про Docker-managed `FORWARD`.
|
- [ ] `status` показывает блок `Docker/Amnezia port path` с полями `docker_userland_proxy`, `published_udp_port` и note про Docker-managed `FORWARD`.
|
||||||
|
- [ ] `status` показывает поля `host_forward_policy_drop` и `docker_forward_rules_present` в блоке `Docker/Amnezia port path`.
|
||||||
- [ ] Если `docker_userland_proxy=false` и `FORWARD policy DROP`, при отсутствии явных Docker accept-признаков `status` выводит warning, но `awg2-harness` не изменяет `FORWARD` автоматически.
|
- [ ] Если `docker_userland_proxy=false` и `FORWARD policy DROP`, при отсутствии явных Docker accept-признаков `status` выводит warning, но `awg2-harness` не изменяет `FORWARD` автоматически.
|
||||||
- [ ] Сверено соответствие: `config host_firewall.amnezia_port` ↔ опубликованный Docker UDP host port ↔ `docker exec amnezia-awg2 wg show` (`listening port`).
|
- [ ] Сверено соответствие: `config host_firewall.amnezia_port` ↔ опубликованный Docker UDP host port ↔ `docker exec amnezia-awg2 wg show` (`listening port`).
|
||||||
- [ ] При нескольких UDP bindings без однозначного соответствия `host_firewall.amnezia_port` диагностика `published_udp_port` показывает `unknown` (ambiguous), а не произвольный первый порт.
|
- [ ] При нескольких UDP bindings без однозначного соответствия `host_firewall.amnezia_port` диагностика `published_udp_port` показывает `unknown` (ambiguous), а не произвольный первый порт.
|
||||||
|
- [ ] Для диагностики FORWARD используется best-effort проверка: `iptables -S FORWARD` и fallback через `nft list ruleset`; `status` не падает при недоступности одной из команд.
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ swappiness = 1
|
|||||||
|
|
||||||
Для диагностического блока `Docker/Amnezia port path` в `status` приоритет сопоставления такой: сначала ищется published UDP binding с `HostPort == host_firewall.amnezia_port`, затем fallback на ключ `"<amnezia_port>/udp"` как container port, затем (если UDP binding ровно один) берётся он. Если UDP bindings несколько и однозначного совпадения нет, статус показывает `published_udp_port: unknown`.
|
Для диагностического блока `Docker/Amnezia port path` в `status` приоритет сопоставления такой: сначала ищется published UDP binding с `HostPort == host_firewall.amnezia_port`, затем fallback на ключ `"<amnezia_port>/udp"` как container port, затем (если UDP binding ровно один) берётся он. Если UDP bindings несколько и однозначного совпадения нет, статус показывает `published_udp_port: unknown`.
|
||||||
|
|
||||||
|
Диагностика `FORWARD` в этом блоке является best-effort: сначала используется `iptables -S FORWARD`, затем fallback через `nft list ruleset`; результаты только отображаются в `status` и не изменяют host `FORWARD`/Docker NAT.
|
||||||
|
|
||||||
`host_firewall.amnezia_protocols` — протоколы для порта Amnezia (`udp`, `tcp` или оба).
|
`host_firewall.amnezia_protocols` — протоколы для порта Amnezia (`udp`, `tcp` или оба).
|
||||||
|
|
||||||
Важно: `awg2-harness` не управляет host `FORWARD`/Docker NAT-цепочками. Проект сознательно не добавляет универсальные правила в `FORWARD` и не меняет Docker-managed forwarding path.
|
Важно: `awg2-harness` не управляет host `FORWARD`/Docker NAT-цепочками. Проект сознательно не добавляет универсальные правила в `FORWARD` и не меняет Docker-managed forwarding path.
|
||||||
|
|||||||
+120
-1
@@ -118,7 +118,7 @@ pub fn collect_port_path_diagnostics(
|
|||||||
let docker_userland_proxy = detect_userland_proxy().unwrap_or(TriState::Unknown);
|
let docker_userland_proxy = detect_userland_proxy().unwrap_or(TriState::Unknown);
|
||||||
let published_udp_port = discover_published_udp_port(container_name, expected_udp_port).ok();
|
let published_udp_port = discover_published_udp_port(container_name, expected_udp_port).ok();
|
||||||
let (host_forward_policy_drop, docker_forward_rules_present) =
|
let (host_forward_policy_drop, docker_forward_rules_present) =
|
||||||
inspect_iptables_forward_state().unwrap_or((TriState::Unknown, TriState::Unknown));
|
inspect_forward_state().unwrap_or((TriState::Unknown, TriState::Unknown));
|
||||||
|
|
||||||
PortPathDiagnostics {
|
PortPathDiagnostics {
|
||||||
docker_userland_proxy,
|
docker_userland_proxy,
|
||||||
@@ -146,6 +146,42 @@ fn inspect_iptables_forward_state() -> anyhow::Result<(TriState, TriState)> {
|
|||||||
Ok(parse_forward_policy_and_docker_rules(&fwd))
|
Ok(parse_forward_policy_and_docker_rules(&fwd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inspect_forward_state() -> anyhow::Result<(TriState, TriState)> {
|
||||||
|
let iptables_state =
|
||||||
|
inspect_iptables_forward_state().unwrap_or((TriState::Unknown, TriState::Unknown));
|
||||||
|
let nft_state = inspect_nft_forward_state().unwrap_or((TriState::Unknown, TriState::Unknown));
|
||||||
|
Ok(merge_forward_states(iptables_state, nft_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inspect_nft_forward_state() -> anyhow::Result<(TriState, TriState)> {
|
||||||
|
let ruleset = run_capture(&["nft", "list", "ruleset"])?;
|
||||||
|
Ok(parse_nft_forward_policy_and_docker_rules(&ruleset))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_forward_states(a: (TriState, TriState), b: (TriState, TriState)) -> (TriState, TriState) {
|
||||||
|
(merge_policy_drop(a.0, b.0), merge_docker_rules(a.1, b.1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_policy_drop(a: TriState, b: TriState) -> TriState {
|
||||||
|
if a == TriState::True || b == TriState::True {
|
||||||
|
TriState::True
|
||||||
|
} else if a == TriState::False || b == TriState::False {
|
||||||
|
TriState::False
|
||||||
|
} else {
|
||||||
|
TriState::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_docker_rules(a: TriState, b: TriState) -> TriState {
|
||||||
|
if a == TriState::True || b == TriState::True {
|
||||||
|
TriState::True
|
||||||
|
} else if a == TriState::False && b == TriState::False {
|
||||||
|
TriState::False
|
||||||
|
} else {
|
||||||
|
TriState::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_userland_proxy_from_docker_info(raw: &str) -> TriState {
|
fn parse_userland_proxy_from_docker_info(raw: &str) -> TriState {
|
||||||
let Ok(v) = serde_json::from_str::<Value>(raw) else {
|
let Ok(v) = serde_json::from_str::<Value>(raw) else {
|
||||||
return TriState::Unknown;
|
return TriState::Unknown;
|
||||||
@@ -279,6 +315,34 @@ fn parse_forward_policy_and_docker_rules(raw: &str) -> (TriState, TriState) {
|
|||||||
(policy_drop, docker_rules)
|
(policy_drop, docker_rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_nft_forward_policy_and_docker_rules(raw: &str) -> (TriState, TriState) {
|
||||||
|
let nt = normalize_nft_forward_text(raw);
|
||||||
|
|
||||||
|
let policy_drop = if nt.contains("hook forward") && nt.contains("policy drop") {
|
||||||
|
TriState::True
|
||||||
|
} else if nt.contains("hook forward") && nt.contains("policy accept") {
|
||||||
|
TriState::False
|
||||||
|
} else {
|
||||||
|
TriState::Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
let docker_rules = if nt.contains("docker") && nt.contains("forward") {
|
||||||
|
TriState::True
|
||||||
|
} else {
|
||||||
|
TriState::Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
(policy_drop, docker_rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_nft_forward_text(raw: &str) -> String {
|
||||||
|
raw.to_ascii_lowercase()
|
||||||
|
.replace(['\n', '\t', '{', '}', ';', '"'], " ")
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -432,6 +496,61 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nft_forward_policy_and_docker_rules_works() {
|
||||||
|
let raw = r#"
|
||||||
|
table inet docker_filter {
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority filter; policy accept;
|
||||||
|
jump DOCKER-USER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_nft_forward_policy_and_docker_rules(raw),
|
||||||
|
(TriState::False, TriState::True)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nft_forward_policy_drop_detects_drop_forward_chain() {
|
||||||
|
let raw = r#"
|
||||||
|
table inet filter {
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority filter; policy drop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_nft_forward_policy_and_docker_rules(raw),
|
||||||
|
(TriState::True, TriState::Unknown)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_forward_states_preserves_nft_docker_rules() {
|
||||||
|
assert_eq!(
|
||||||
|
merge_forward_states(
|
||||||
|
(TriState::Unknown, TriState::Unknown),
|
||||||
|
(TriState::False, TriState::True)
|
||||||
|
),
|
||||||
|
(TriState::False, TriState::True)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_forward_states_is_conservative_on_drop_policy() {
|
||||||
|
assert_eq!(
|
||||||
|
merge_forward_states(
|
||||||
|
(TriState::True, TriState::Unknown),
|
||||||
|
(TriState::False, TriState::True)
|
||||||
|
),
|
||||||
|
(TriState::True, TriState::True)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warning_is_emitted_for_risky_forward_state() {
|
fn warning_is_emitted_for_risky_forward_state() {
|
||||||
let diag = PortPathDiagnostics {
|
let diag = PortPathDiagnostics {
|
||||||
|
|||||||
@@ -313,6 +313,8 @@ fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
println!("Docker/Amnezia port path:");
|
println!("Docker/Amnezia port path:");
|
||||||
println!(" docker_userland_proxy: unknown");
|
println!(" docker_userland_proxy: unknown");
|
||||||
println!(" published_udp_port: unknown");
|
println!(" published_udp_port: unknown");
|
||||||
|
println!(" host_forward_policy_drop: unknown");
|
||||||
|
println!(" docker_forward_rules_present: unknown");
|
||||||
println!(" note: FORWARD is Docker-managed; awg2-harness does not modify host FORWARD");
|
println!(" note: FORWARD is Docker-managed; awg2-harness does not modify host FORWARD");
|
||||||
|
|
||||||
println!("Anti-torrent:");
|
println!("Anti-torrent:");
|
||||||
@@ -465,6 +467,14 @@ fn print_docker_amnezia_port_path(diag: &docker::PortPathDiagnostics) {
|
|||||||
),
|
),
|
||||||
None => println!(" published_udp_port: unknown"),
|
None => println!(" published_udp_port: unknown"),
|
||||||
}
|
}
|
||||||
|
println!(
|
||||||
|
" host_forward_policy_drop: {}",
|
||||||
|
diag.host_forward_policy_drop.as_str()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" docker_forward_rules_present: {}",
|
||||||
|
diag.docker_forward_rules_present.as_str()
|
||||||
|
);
|
||||||
println!(" note: FORWARD is Docker-managed; awg2-harness does not modify host FORWARD");
|
println!(" note: FORWARD is Docker-managed; awg2-harness does not modify host FORWARD");
|
||||||
if let Some(w) = diag.warning() {
|
if let Some(w) = diag.warning() {
|
||||||
println!(" warning: {w}");
|
println!(" warning: {w}");
|
||||||
|
|||||||
Reference in New Issue
Block a user