Уточнена диагностика UDP port mapping для Docker и обновлены checklist/docs
This commit is contained in:
@@ -117,22 +117,6 @@
|
|||||||
- [ ] Известно, как вернуть `amnezia-tc.service` и `amnezia-tc-watcher.service`.
|
- [ ] Известно, как вернуть `amnezia-tc.service` и `amnezia-tc-watcher.service`.
|
||||||
- [ ] Rollback проверен на тестовом сервере.
|
- [ ] Rollback проверен на тестовом сервере.
|
||||||
|
|
||||||
## L. Host firewall (ports/SSH safety)
|
|
||||||
|
|
||||||
- [ ] `awg2-harness validate-config` падает, если отсутствует секция `[host_firewall]`.
|
|
||||||
- [ ] `apply-firewall` не блокирует SSH на `host_firewall.ssh_port` (default `2323`).
|
|
||||||
- [ ] После `apply-firewall` порт `host_firewall.amnezia_port` (default `443`) доступен по всем протоколам из `host_firewall.amnezia_protocols`.
|
|
||||||
- [ ] Если эксплуатационно требуется закрыть legacy SSH `22`, это явно подтверждено проверкой доступа на `ssh_port` перед закрытием `22`.
|
|
||||||
- [ ] При `host_firewall.enabled = false` удаляется только таблица `inet awg2_host_firewall`.
|
|
||||||
- [ ] Повторный `apply-firewall` идемпотентен и не вносит лишних изменений в healthy state.
|
|
||||||
- [ ] При невалидном `host_firewall.amnezia_protocols` (`icmp`, `foo`, дубли) `validate-config` завершается ошибкой.
|
|
||||||
- [ ] После `docker restart` anti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой.
|
|
||||||
- [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке.
|
|
||||||
- [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика.
|
|
||||||
- [ ] `status` показывает блок `Docker/Amnezia port path` с полями `docker_userland_proxy`, `published_udp_port` и note про Docker-managed `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`).
|
|
||||||
|
|
||||||
## K. Packaging / release
|
## K. Packaging / release
|
||||||
|
|
||||||
- [ ] Package собирается на отдельной Debian 13 amd64 build-machine.
|
- [ ] Package собирается на отдельной Debian 13 amd64 build-machine.
|
||||||
@@ -148,3 +132,20 @@
|
|||||||
- [ ] `apt install ./awg2-harness_<version>_amd64.deb` проходит на clean Debian 13 Amnezia host.
|
- [ ] `apt install ./awg2-harness_<version>_amd64.deb` проходит на clean Debian 13 Amnezia host.
|
||||||
- [ ] После ручной установки config `awg2-harness validate-config` проходит.
|
- [ ] После ручной установки config `awg2-harness validate-config` проходит.
|
||||||
- [ ] Сервис стартует только после явного `systemctl enable --now awg2-harness.service`.
|
- [ ] Сервис стартует только после явного `systemctl enable --now awg2-harness.service`.
|
||||||
|
|
||||||
|
## L. Host firewall (ports/SSH safety)
|
||||||
|
|
||||||
|
- [ ] `awg2-harness validate-config` падает, если отсутствует секция `[host_firewall]`.
|
||||||
|
- [ ] `apply-firewall` не блокирует SSH на `host_firewall.ssh_port` (default `2323`).
|
||||||
|
- [ ] После `apply-firewall` порт `host_firewall.amnezia_port` (default `443`) доступен по всем протоколам из `host_firewall.amnezia_protocols`.
|
||||||
|
- [ ] Если эксплуатационно требуется закрыть legacy SSH `22`, это явно подтверждено проверкой доступа на `ssh_port` перед закрытием `22`.
|
||||||
|
- [ ] При `host_firewall.enabled = false` удаляется только таблица `inet awg2_host_firewall`.
|
||||||
|
- [ ] Повторный `apply-firewall` идемпотентен и не вносит лишних изменений в healthy state.
|
||||||
|
- [ ] При невалидном `host_firewall.amnezia_protocols` (`icmp`, `foo`, дубли) `validate-config` завершается ошибкой.
|
||||||
|
- [ ] После `docker restart` anti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой.
|
||||||
|
- [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке.
|
||||||
|
- [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика.
|
||||||
|
- [ ] `status` показывает блок `Docker/Amnezia port path` с полями `docker_userland_proxy`, `published_udp_port` и note про Docker-managed `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`).
|
||||||
|
- [ ] При нескольких UDP bindings без однозначного соответствия `host_firewall.amnezia_port` диагностика `published_udp_port` показывает `unknown` (ambiguous), а не произвольный первый порт.
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ swappiness = 1
|
|||||||
|
|
||||||
`host_firewall.amnezia_port` — внешний UDP/TCP порт Amnezia, который AWG2 открывает в host `INPUT` (в зависимости от `host_firewall.amnezia_protocols`). Значение должно совпадать с портом, указанным в панели/инсталляторе AmneziaWG. В старых инсталляциях это мог быть `44344`, в новой целевой конфигурации default — `443`.
|
`host_firewall.amnezia_port` — внешний UDP/TCP порт Amnezia, который AWG2 открывает в host `INPUT` (в зависимости от `host_firewall.amnezia_protocols`). Значение должно совпадать с портом, указанным в панели/инсталляторе AmneziaWG. В старых инсталляциях это мог быть `44344`, в новой целевой конфигурации default — `443`.
|
||||||
|
|
||||||
|
Для диагностического блока `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`.
|
||||||
|
|
||||||
`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.
|
||||||
|
|||||||
+124
-16
@@ -187,6 +187,34 @@ fn parse_udp_port_path_from_inspect(
|
|||||||
.and_then(|x| x.as_object())
|
.and_then(|x| x.as_object())
|
||||||
.ok_or_else(|| anyhow::anyhow!("docker inspect has no NetworkSettings.Ports"))?;
|
.ok_or_else(|| anyhow::anyhow!("docker inspect has no NetworkSettings.Ports"))?;
|
||||||
|
|
||||||
|
let mut udp_bindings: Vec<(u16, u16)> = Vec::new();
|
||||||
|
for (k, v) in ports {
|
||||||
|
if !k.ends_with("/udp") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(host_port) = extract_host_port_from_binding(v) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let container_port = k
|
||||||
|
.split('/')
|
||||||
|
.next()
|
||||||
|
.and_then(|x| x.parse::<u16>().ok())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("invalid udp port key in docker inspect: {k}"))?;
|
||||||
|
udp_bindings.push((host_port, container_port));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((host_port, container_port)) = udp_bindings
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|(host_port, _)| *host_port == expected_udp_port)
|
||||||
|
{
|
||||||
|
return Ok(PublishedUdpPortPath {
|
||||||
|
host_port,
|
||||||
|
container_ip,
|
||||||
|
container_port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let expected_key = format!("{expected_udp_port}/udp");
|
let expected_key = format!("{expected_udp_port}/udp");
|
||||||
if let Some(host_port) = extract_host_port_for_udp_key(ports, &expected_key) {
|
if let Some(host_port) = extract_host_port_for_udp_key(ports, &expected_key) {
|
||||||
return Ok(PublishedUdpPortPath {
|
return Ok(PublishedUdpPortPath {
|
||||||
@@ -196,22 +224,19 @@ fn parse_udp_port_path_from_inspect(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (k, v) in ports {
|
if udp_bindings.len() == 1 {
|
||||||
if !k.ends_with("/udp") {
|
let (host_port, container_port) = udp_bindings[0];
|
||||||
continue;
|
return Ok(PublishedUdpPortPath {
|
||||||
}
|
host_port,
|
||||||
if let Some(host_port) = extract_host_port_from_binding(v) {
|
container_ip,
|
||||||
let container_port = k
|
container_port,
|
||||||
.split('/')
|
});
|
||||||
.next()
|
}
|
||||||
.and_then(|x| x.parse::<u16>().ok())
|
|
||||||
.unwrap_or(expected_udp_port);
|
if udp_bindings.len() > 1 {
|
||||||
return Ok(PublishedUdpPortPath {
|
return Err(anyhow::anyhow!(
|
||||||
host_port,
|
"ambiguous udp published ports in docker inspect"
|
||||||
container_ip,
|
));
|
||||||
container_port,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow::anyhow!(
|
Err(anyhow::anyhow!(
|
||||||
@@ -286,6 +311,20 @@ mod tests {
|
|||||||
"443/udp": [{ "HostIp": "0.0.0.0", "HostPort": "443" }]
|
"443/udp": [{ "HostIp": "0.0.0.0", "HostPort": "443" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_udp_port_prefers_host_port_match() {
|
||||||
|
let raw = r#"[
|
||||||
|
{
|
||||||
|
"NetworkSettings": {
|
||||||
|
"Networks": {
|
||||||
|
"bridge": { "IPAddress": "172.29.172.2" }
|
||||||
|
},
|
||||||
|
"Ports": {
|
||||||
|
"44344/udp": [{ "HostIp": "0.0.0.0", "HostPort": "443" }],
|
||||||
|
"443/udp": [{ "HostIp": "0.0.0.0", "HostPort": "5443" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]"#;
|
]"#;
|
||||||
let got = parse_udp_port_path_from_inspect(raw, 443).expect("parse inspect");
|
let got = parse_udp_port_path_from_inspect(raw, 443).expect("parse inspect");
|
||||||
@@ -294,11 +333,80 @@ mod tests {
|
|||||||
PublishedUdpPortPath {
|
PublishedUdpPortPath {
|
||||||
host_port: 443,
|
host_port: 443,
|
||||||
container_ip: "172.29.172.2".to_string(),
|
container_ip: "172.29.172.2".to_string(),
|
||||||
|
container_port: 44344,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_udp_port_fallbacks_to_expected_key() {
|
||||||
|
let raw = r#"[
|
||||||
|
{
|
||||||
|
"NetworkSettings": {
|
||||||
|
"Networks": {
|
||||||
|
"bridge": { "IPAddress": "172.29.172.2" }
|
||||||
|
},
|
||||||
|
"Ports": {
|
||||||
|
"443/udp": [{ "HostIp": "0.0.0.0", "HostPort": "5443" }],
|
||||||
|
"44344/udp": [{ "HostIp": "0.0.0.0", "HostPort": "15443" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"#;
|
||||||
|
let got = parse_udp_port_path_from_inspect(raw, 443).expect("parse inspect");
|
||||||
|
assert_eq!(
|
||||||
|
got,
|
||||||
|
PublishedUdpPortPath {
|
||||||
|
host_port: 5443,
|
||||||
|
container_ip: "172.29.172.2".to_string(),
|
||||||
container_port: 443,
|
container_port: 443,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_udp_port_uses_single_udp_binding() {
|
||||||
|
let raw = r#"[
|
||||||
|
{
|
||||||
|
"NetworkSettings": {
|
||||||
|
"Networks": {
|
||||||
|
"bridge": { "IPAddress": "172.29.172.2" }
|
||||||
|
},
|
||||||
|
"Ports": {
|
||||||
|
"44344/udp": [{ "HostIp": "0.0.0.0", "HostPort": "44344" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"#;
|
||||||
|
let got = parse_udp_port_path_from_inspect(raw, 443).expect("parse inspect");
|
||||||
|
assert_eq!(
|
||||||
|
got,
|
||||||
|
PublishedUdpPortPath {
|
||||||
|
host_port: 44344,
|
||||||
|
container_ip: "172.29.172.2".to_string(),
|
||||||
|
container_port: 44344,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_udp_port_returns_ambiguous_on_multiple_without_match() {
|
||||||
|
let raw = r#"[
|
||||||
|
{
|
||||||
|
"NetworkSettings": {
|
||||||
|
"Networks": {
|
||||||
|
"bridge": { "IPAddress": "172.29.172.2" }
|
||||||
|
},
|
||||||
|
"Ports": {
|
||||||
|
"10000/udp": [{ "HostIp": "0.0.0.0", "HostPort": "10000" }],
|
||||||
|
"20000/udp": [{ "HostIp": "0.0.0.0", "HostPort": "20000" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"#;
|
||||||
|
let err = parse_udp_port_path_from_inspect(raw, 443).expect_err("must be ambiguous");
|
||||||
|
assert!(err.to_string().contains("ambiguous udp published ports"));
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_forward_policy_and_docker_rules_works() {
|
fn parse_forward_policy_and_docker_rules_works() {
|
||||||
let raw = r#"
|
let raw = r#"
|
||||||
|
|||||||
Reference in New Issue
Block a user