Полный продакшн-фикс: hardened build/install, drift/firewall/wg проверки и актуализация документации
This commit is contained in:
+14
-7
@@ -31,29 +31,36 @@ nft
|
|||||||
Сборку лучше выполнять на отдельной build-машине с тем же целевым ABI.
|
Сборку лучше выполнять на отдельной build-машине с тем же целевым ABI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build --release
|
./tools/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Альтернатива (ручной запуск cargo из корня репозитория):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --locked --manifest-path traffic_core/Cargo.toml --release
|
||||||
```
|
```
|
||||||
|
|
||||||
Проверки перед переносом:
|
Проверки перед переносом:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo fmt --check
|
cargo fmt --manifest-path traffic_core/Cargo.toml --all --check
|
||||||
cargo clippy --all-targets --all-features -- -D warnings
|
cargo clippy --locked --manifest-path traffic_core/Cargo.toml --all-targets -- -D warnings
|
||||||
cargo test --release
|
cargo test --locked --manifest-path traffic_core/Cargo.toml --release
|
||||||
file target/release/awg2-harness
|
file traffic_core/target/release/awg2-harness
|
||||||
ldd target/release/awg2-harness
|
ldd traffic_core/target/release/awg2-harness
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Установка файлов
|
## 3. Установка файлов
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
install -m 0755 target/release/awg2-harness /usr/local/bin/awg2-harness
|
install -m 0755 traffic_core/target/release/awg2-harness /usr/local/bin/awg2-harness
|
||||||
install -d -m 0755 /etc/awg2-harness
|
install -d -m 0755 /etc/awg2-harness
|
||||||
install -m 0644 config.toml /etc/awg2-harness/config.toml
|
install -m 0644 config.toml /etc/awg2-harness/config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
Важно: production-установка **не должна** автоматически копировать [`config.example.toml`](config.example.toml) как боевой конфиг.
|
Важно: production-установка **не должна** автоматически копировать [`config.example.toml`](config.example.toml) как боевой конфиг.
|
||||||
Если используется [`tools/build.sh`](tools/build.sh), режим `--install` завершится ошибкой при отсутствии `/etc/awg2-harness/config.toml`.
|
Если используется [`tools/build.sh`](tools/build.sh), режим `--install` завершится ошибкой при отсутствии `/etc/awg2-harness/config.toml`.
|
||||||
|
Режим `--install` копирует только бинарь и unit, выполняет `systemctl daemon-reload`, но не делает `systemctl enable --now`.
|
||||||
Сначала подготовьте осознанный production-конфиг вручную.
|
Сначала подготовьте осознанный production-конфиг вручную.
|
||||||
|
|
||||||
Проверка конфига:
|
Проверка конфига:
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.85.0"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
+54
-2
@@ -1,2 +1,54 @@
|
|||||||
На будущее:
|
# Build tooling
|
||||||
Тут должен быть билдер на баш, который проверяет совместимость и зависимости если их нет доставляет и все собирает сам, то есть тупо запустили он сам все сделал.
|
|
||||||
|
[`build.sh`](tools/build.sh) — production build gate для [`traffic_core`](traffic_core/Cargo.toml).
|
||||||
|
|
||||||
|
## Что делает
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
|
||||||
|
1. Проверяет OS и архитектуру (Debian/Ubuntu, amd64/x86_64).
|
||||||
|
2. Проверяет наличие инструментов: `cargo`, `rustc`, `gcc`, `pkg-config`, `file`, `ldd`.
|
||||||
|
3. Проверяет минимальную версию Rust (`rustc >= 1.85.0`).
|
||||||
|
4. Запускает quality gate:
|
||||||
|
- `cargo fmt --check` (если не указан `--skip-fmt`),
|
||||||
|
- `cargo clippy --locked --all-targets -- -D warnings`,
|
||||||
|
- `cargo test --locked --release`,
|
||||||
|
- `cargo build --locked --release`.
|
||||||
|
5. Проверяет собранный бинарь: `file`, `ldd`, `--help`, `sha256sum`.
|
||||||
|
|
||||||
|
## Что НЕ делает
|
||||||
|
|
||||||
|
- Не устанавливает Rust/toolchain автоматически.
|
||||||
|
- Не устанавливает системные зависимости через apt.
|
||||||
|
- Не создаёт production-конфиг автоматически.
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
Из корня репозитория:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Опции:
|
||||||
|
|
||||||
|
- `--skip-fmt` — пропустить шаг `cargo fmt --check`.
|
||||||
|
- `--install` — после успешной сборки установить бинарь и systemd unit.
|
||||||
|
|
||||||
|
## Режим `--install`
|
||||||
|
|
||||||
|
`--install`:
|
||||||
|
|
||||||
|
1. Копирует бинарь в `/usr/local/bin/awg2-harness`.
|
||||||
|
2. Проверяет, что `/etc/awg2-harness/config.toml` уже существует.
|
||||||
|
3. Копирует [`awg2-harness.service`](awg2-harness.service).
|
||||||
|
4. Выполняет `systemd-analyze verify` (если доступно).
|
||||||
|
5. Выполняет `systemctl daemon-reload`.
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- Если скрипт запущен от root, команды установки выполняются напрямую.
|
||||||
|
- Если не от root — используется `sudo` (и проверяется его наличие).
|
||||||
|
- Скрипт **не** выполняет `systemctl enable --now` автоматически.
|
||||||
|
|
||||||
|
Это intentional: включение сервиса и запуск остаются осознанным эксплуатационным шагом.
|
||||||
|
|||||||
+34
-7
@@ -4,6 +4,7 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
CRATE_MANIFEST="$ROOT_DIR/traffic_core/Cargo.toml"
|
CRATE_MANIFEST="$ROOT_DIR/traffic_core/Cargo.toml"
|
||||||
TARGET_BIN="$ROOT_DIR/traffic_core/target/release/awg2-harness"
|
TARGET_BIN="$ROOT_DIR/traffic_core/target/release/awg2-harness"
|
||||||
|
MIN_RUSTC="1.85.0"
|
||||||
|
|
||||||
AUTO_INSTALL=false
|
AUTO_INSTALL=false
|
||||||
SKIP_FMT=false
|
SKIP_FMT=false
|
||||||
@@ -20,6 +21,12 @@ require_cmd() {
|
|||||||
command -v "$1" >/dev/null 2>&1 || { echo "Missing command: $1"; exit 1; }
|
command -v "$1" >/dev/null 2>&1 || { echo "Missing command: $1"; exit 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
version_ge() {
|
||||||
|
local have="$1"
|
||||||
|
local need="$2"
|
||||||
|
[[ "$(printf '%s\n' "$need" "$have" | sort -V | head -n1)" == "$need" ]]
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -f /etc/os-release ]]; then
|
if [[ -f /etc/os-release ]]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source /etc/os-release
|
source /etc/os-release
|
||||||
@@ -40,6 +47,14 @@ if ! command -v cargo >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
require_cmd rustc
|
||||||
|
|
||||||
|
RUSTC_VERSION="$(rustc --version | awk '{print $2}')"
|
||||||
|
if ! version_ge "$RUSTC_VERSION" "$MIN_RUSTC"; then
|
||||||
|
echo "rustc $RUSTC_VERSION is too old. Require >= $MIN_RUSTC"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
for cmd in gcc pkg-config; do
|
for cmd in gcc pkg-config; do
|
||||||
require_cmd "$cmd"
|
require_cmd "$cmd"
|
||||||
done
|
done
|
||||||
@@ -48,9 +63,9 @@ if [[ "$SKIP_FMT" != true ]]; then
|
|||||||
cargo fmt --manifest-path "$CRATE_MANIFEST" --all --check
|
cargo fmt --manifest-path "$CRATE_MANIFEST" --all --check
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cargo clippy --manifest-path "$CRATE_MANIFEST" --all-targets -- -D warnings
|
cargo clippy --locked --manifest-path "$CRATE_MANIFEST" --all-targets -- -D warnings
|
||||||
cargo test --manifest-path "$CRATE_MANIFEST" --release
|
cargo test --locked --manifest-path "$CRATE_MANIFEST" --release
|
||||||
cargo build --manifest-path "$CRATE_MANIFEST" --release
|
cargo build --locked --manifest-path "$CRATE_MANIFEST" --release
|
||||||
|
|
||||||
require_cmd file
|
require_cmd file
|
||||||
require_cmd ldd
|
require_cmd ldd
|
||||||
@@ -64,16 +79,28 @@ ls -lh "$TARGET_BIN"
|
|||||||
|
|
||||||
if [[ "$AUTO_INSTALL" == true ]]; then
|
if [[ "$AUTO_INSTALL" == true ]]; then
|
||||||
require_cmd install
|
require_cmd install
|
||||||
sudo install -m 0755 "$TARGET_BIN" /usr/local/bin/awg2-harness
|
|
||||||
sudo mkdir -p /etc/awg2-harness
|
RUN_AS_ROOT=( )
|
||||||
|
if [[ "$(id -u)" -eq 0 ]]; then
|
||||||
|
RUN_AS_ROOT=( )
|
||||||
|
else
|
||||||
|
require_cmd sudo
|
||||||
|
RUN_AS_ROOT=(sudo)
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${RUN_AS_ROOT[@]}" install -m 0755 "$TARGET_BIN" /usr/local/bin/awg2-harness
|
||||||
|
"${RUN_AS_ROOT[@]}" mkdir -p /etc/awg2-harness
|
||||||
if [[ ! -f /etc/awg2-harness/config.toml ]]; then
|
if [[ ! -f /etc/awg2-harness/config.toml ]]; then
|
||||||
echo "Missing /etc/awg2-harness/config.toml"
|
echo "Missing /etc/awg2-harness/config.toml"
|
||||||
echo "Refusing to install example config as production config."
|
echo "Refusing to install example config as production config."
|
||||||
echo "Create config manually from $ROOT_DIR/config.example.toml and re-run with --install."
|
echo "Create config manually from $ROOT_DIR/config.example.toml and re-run with --install."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sudo install -m 0644 "$ROOT_DIR/awg2-harness.service" /etc/systemd/system/awg2-harness.service
|
"${RUN_AS_ROOT[@]}" install -m 0644 "$ROOT_DIR/awg2-harness.service" /etc/systemd/system/awg2-harness.service
|
||||||
sudo systemctl daemon-reload
|
if command -v systemd-analyze >/dev/null 2>&1; then
|
||||||
|
"${RUN_AS_ROOT[@]}" systemd-analyze verify /etc/systemd/system/awg2-harness.service
|
||||||
|
fi
|
||||||
|
"${RUN_AS_ROOT[@]}" systemctl daemon-reload
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Build completed: $TARGET_BIN"
|
echo "Build completed: $TARGET_BIN"
|
||||||
|
|||||||
Generated
+28
-2
@@ -91,6 +91,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -233,9 +234,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.0"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@@ -283,6 +284,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.98"
|
version = "0.3.98"
|
||||||
@@ -474,6 +481,19 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.9"
|
version = "0.6.9"
|
||||||
@@ -823,3 +843,9 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ chrono = { version = "0.4", features = ["clock"] }
|
|||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::{
|
|||||||
docker,
|
docker,
|
||||||
netns::{NsTransport, build_host_transport, build_transport},
|
netns::{NsTransport, build_host_transport, build_transport},
|
||||||
};
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct FirewallActual {
|
struct FirewallActual {
|
||||||
@@ -77,7 +78,14 @@ fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result
|
|||||||
"ct count over {}",
|
"ct count over {}",
|
||||||
cfg.anti_torrent.max_connections_per_peer
|
cfg.anti_torrent.max_connections_per_peer
|
||||||
);
|
);
|
||||||
let rule_matches = table.contains(&needle_cidr) && table.contains(&needle_max);
|
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 {
|
Ok(FirewallActual {
|
||||||
table_exists,
|
table_exists,
|
||||||
@@ -90,7 +98,10 @@ fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result
|
|||||||
fn firewall_transport(cfg: &Config) -> anyhow::Result<Box<dyn NsTransport>> {
|
fn firewall_transport(cfg: &Config) -> anyhow::Result<Box<dyn NsTransport>> {
|
||||||
if cfg.anti_torrent.apply_in_amnezia_netns {
|
if cfg.anti_torrent.apply_in_amnezia_netns {
|
||||||
let target = docker::discover_target(cfg)?;
|
let target = docker::discover_target(cfg)?;
|
||||||
Ok(build_transport(target.container_pid, &target.container_name))
|
Ok(build_transport(
|
||||||
|
target.container_pid,
|
||||||
|
&target.container_name,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(build_host_transport())
|
Ok(build_host_transport())
|
||||||
}
|
}
|
||||||
@@ -180,25 +191,75 @@ pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64
|
|||||||
}
|
}
|
||||||
|
|
||||||
let transport = firewall_transport(cfg)?;
|
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
|
let table = transport
|
||||||
.run(&["nft", "list", "table", "inet", "awg2_harness"])
|
.run(&["nft", "list", "table", "inet", "awg2_harness"])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
for line in table.lines() {
|
for line in table.lines() {
|
||||||
if line.contains("counter antitorrent_dropped_v4") && line.contains("packets") {
|
let t = line.trim();
|
||||||
let toks: Vec<&str> = line.split_whitespace().collect();
|
if !t.contains("packets") {
|
||||||
if let Some(idx) = toks.iter().position(|t| *t == "packets") {
|
continue;
|
||||||
if let Some(v) = toks.get(idx + 1).and_then(|s| s.parse::<u64>().ok()) {
|
}
|
||||||
|
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
|
let bytes = toks
|
||||||
.iter()
|
.iter()
|
||||||
.position(|t| *t == "bytes")
|
.position(|x| *x == "bytes")
|
||||||
.and_then(|i| toks.get(i + 1))
|
.and_then(|i| toks.get(i + 1))
|
||||||
.and_then(|s| s.parse::<u64>().ok())
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
return Ok(Some((v, bytes)));
|
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)
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,11 +47,17 @@ impl NsTransport for DockerExecTransport {
|
|||||||
|
|
||||||
impl NsTransport for HostTransport {
|
impl NsTransport for HostTransport {
|
||||||
fn run(&self, args: &[&str]) -> anyhow::Result<String> {
|
fn run(&self, args: &[&str]) -> anyhow::Result<String> {
|
||||||
let mut cmd = Command::new(args.first().ok_or_else(|| anyhow::anyhow!("empty command"))?);
|
let mut cmd = Command::new(
|
||||||
|
args.first()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("empty command"))?,
|
||||||
|
);
|
||||||
cmd.args(&args[1..]);
|
cmd.args(&args[1..]);
|
||||||
let out = cmd.output()?;
|
let out = cmd.output()?;
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
anyhow::bail!("host transport failed: {}", String::from_utf8_lossy(&out.stderr));
|
anyhow::bail!(
|
||||||
|
"host transport failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&out.stdout).to_string())
|
Ok(String::from_utf8_lossy(&out.stdout).to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use chrono::{Datelike, Duration, NaiveDateTime, NaiveTime, Timelike, Utc};
|
use chrono::{Duration, NaiveDateTime, NaiveTime, Timelike, Utc};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use std::{process::Command, thread, time::Duration as StdDuration};
|
use std::{process::Command, thread, time::Duration as StdDuration};
|
||||||
|
|
||||||
|
|||||||
+176
-18
@@ -58,6 +58,7 @@ pub fn inspect_drift_summary(cfg: &Config) -> anyhow::Result<Option<String>> {
|
|||||||
&target.interface_name,
|
&target.interface_name,
|
||||||
&cfg.traffic.ifb_name,
|
&cfg.traffic.ifb_name,
|
||||||
cfg.traffic.class_start_id,
|
cfg.traffic.class_start_id,
|
||||||
|
&cfg.traffic.root_rate,
|
||||||
&cfg.traffic.peer_limit,
|
&cfg.traffic.peer_limit,
|
||||||
&peers,
|
&peers,
|
||||||
)?;
|
)?;
|
||||||
@@ -93,6 +94,7 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
&target.interface_name,
|
&target.interface_name,
|
||||||
&cfg.traffic.ifb_name,
|
&cfg.traffic.ifb_name,
|
||||||
cfg.traffic.class_start_id,
|
cfg.traffic.class_start_id,
|
||||||
|
&cfg.traffic.root_rate,
|
||||||
&cfg.traffic.peer_limit,
|
&cfg.traffic.peer_limit,
|
||||||
&peers,
|
&peers,
|
||||||
)?;
|
)?;
|
||||||
@@ -104,7 +106,13 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
println!("traffic drift detected: {}", drift.reason);
|
println!("traffic drift detected: {}", drift.reason);
|
||||||
|
|
||||||
if cfg.traffic.full_rebuild_on_drift {
|
if !cfg.traffic.full_rebuild_on_drift {
|
||||||
|
anyhow::bail!(
|
||||||
|
"traffic drift detected but full_rebuild_on_drift=false: {}",
|
||||||
|
drift.reason
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
teardown_owned_topology(
|
teardown_owned_topology(
|
||||||
transport.as_ref(),
|
transport.as_ref(),
|
||||||
&target.interface_name,
|
&target.interface_name,
|
||||||
@@ -117,7 +125,6 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
&cfg.traffic.ifb_name,
|
&cfg.traffic.ifb_name,
|
||||||
&peers,
|
&peers,
|
||||||
)?;
|
)?;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -139,10 +146,26 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
|
|||||||
}
|
}
|
||||||
|
|
||||||
let filters_egress = transport
|
let filters_egress = transport
|
||||||
.run(&["tc", "filter", "show", "dev", &target.interface_name, "parent", "1:"])
|
.run(&[
|
||||||
|
"tc",
|
||||||
|
"filter",
|
||||||
|
"show",
|
||||||
|
"dev",
|
||||||
|
&target.interface_name,
|
||||||
|
"parent",
|
||||||
|
"1:",
|
||||||
|
])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let filters_ingress = transport
|
let filters_ingress = transport
|
||||||
.run(&["tc", "filter", "show", "dev", &cfg.traffic.ifb_name, "parent", "2:"])
|
.run(&[
|
||||||
|
"tc",
|
||||||
|
"filter",
|
||||||
|
"show",
|
||||||
|
"dev",
|
||||||
|
&cfg.traffic.ifb_name,
|
||||||
|
"parent",
|
||||||
|
"2:",
|
||||||
|
])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let classes_egress_full = transport
|
let classes_egress_full = transport
|
||||||
.run(&["tc", "class", "show", "dev", &target.interface_name])
|
.run(&["tc", "class", "show", "dev", &target.interface_name])
|
||||||
@@ -151,8 +174,8 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
|
|||||||
.run(&["tc", "class", "show", "dev", &cfg.traffic.ifb_name])
|
.run(&["tc", "class", "show", "dev", &cfg.traffic.ifb_name])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let actual_map_egress = extract_filter_ip_class_map(&filters_egress);
|
let (actual_map_egress, dup_egress) = extract_filter_ip_class_map(&filters_egress);
|
||||||
let actual_map_ingress = extract_filter_ip_class_map(&filters_ingress);
|
let (actual_map_ingress, dup_ingress) = extract_filter_ip_class_map(&filters_ingress);
|
||||||
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
|
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
|
||||||
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
|
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
|
||||||
let expected_map = expected_peer_class_map(&peers, cfg.traffic.class_start_id);
|
let expected_map = expected_peer_class_map(&peers, cfg.traffic.class_start_id);
|
||||||
@@ -193,6 +216,12 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
|
|||||||
{
|
{
|
||||||
reasons.push("ingress rate/ceil mismatch".to_string());
|
reasons.push("ingress rate/ceil mismatch".to_string());
|
||||||
}
|
}
|
||||||
|
if dup_egress.contains(&ip) {
|
||||||
|
reasons.push("duplicate egress filters".to_string());
|
||||||
|
}
|
||||||
|
if dup_ingress.contains(&ip) {
|
||||||
|
reasons.push("duplicate ingress filters".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
out.push(PeerTrafficStatus {
|
out.push(PeerTrafficStatus {
|
||||||
ip,
|
ip,
|
||||||
@@ -219,6 +248,7 @@ fn inspect_and_diff(
|
|||||||
iface: &str,
|
iface: &str,
|
||||||
ifb: &str,
|
ifb: &str,
|
||||||
class_start_id: u16,
|
class_start_id: u16,
|
||||||
|
root_rate: &str,
|
||||||
peer_limit: &str,
|
peer_limit: &str,
|
||||||
peers: &[wg::Peer],
|
peers: &[wg::Peer],
|
||||||
) -> anyhow::Result<TrafficDrift> {
|
) -> anyhow::Result<TrafficDrift> {
|
||||||
@@ -259,8 +289,26 @@ fn inspect_and_diff(
|
|||||||
.run(&["tc", "filter", "show", "dev", ifb, "parent", "2:"])
|
.run(&["tc", "filter", "show", "dev", ifb, "parent", "2:"])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let actual_map_egress = extract_filter_ip_class_map(&filters_egress);
|
let (actual_map_egress, dup_egress) = extract_filter_ip_class_map(&filters_egress);
|
||||||
let actual_map_ingress = extract_filter_ip_class_map(&filters_ingress);
|
let (actual_map_ingress, dup_ingress) = extract_filter_ip_class_map(&filters_ingress);
|
||||||
|
if !dup_egress.is_empty() {
|
||||||
|
return Ok(TrafficDrift {
|
||||||
|
has_drift: true,
|
||||||
|
reason: format!(
|
||||||
|
"duplicate egress filters detected for {} peer(s)",
|
||||||
|
dup_egress.len()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !dup_ingress.is_empty() {
|
||||||
|
return Ok(TrafficDrift {
|
||||||
|
has_drift: true,
|
||||||
|
reason: format!(
|
||||||
|
"duplicate ingress filters detected for {} peer(s)",
|
||||||
|
dup_ingress.len()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
let actual_ips_egress: BTreeSet<String> = actual_map_egress.keys().cloned().collect();
|
let actual_ips_egress: BTreeSet<String> = actual_map_egress.keys().cloned().collect();
|
||||||
let actual_ips_ingress: BTreeSet<String> = actual_map_ingress.keys().cloned().collect();
|
let actual_ips_ingress: BTreeSet<String> = actual_map_ingress.keys().cloned().collect();
|
||||||
let expected_ips: BTreeSet<String> = peers.iter().map(|p| p.ipv4.to_string()).collect();
|
let expected_ips: BTreeSet<String> = peers.iter().map(|p| p.ipv4.to_string()).collect();
|
||||||
@@ -316,9 +364,33 @@ fn inspect_and_diff(
|
|||||||
let classes_ingress_full = transport
|
let classes_ingress_full = transport
|
||||||
.run(&["tc", "class", "show", "dev", ifb])
|
.run(&["tc", "class", "show", "dev", ifb])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let expected_root = normalize_tc_rate(root_rate);
|
||||||
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
|
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
|
||||||
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
|
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
|
||||||
|
|
||||||
|
let root_egress = parse_root_class_rate_ceil(&classes_egress_full, "1:1");
|
||||||
|
let root_ingress = parse_root_class_rate_ceil(&classes_ingress_full, "2:1");
|
||||||
|
if root_egress
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| !rate_ceil_matches_limit(v, &expected_root))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
return Ok(TrafficDrift {
|
||||||
|
has_drift: true,
|
||||||
|
reason: "egress root class 1:1 rate/ceil mismatch".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if root_ingress
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| !rate_ceil_matches_limit(v, &expected_root))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
return Ok(TrafficDrift {
|
||||||
|
has_drift: true,
|
||||||
|
reason: "ingress root class 2:1 rate/ceil mismatch".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for class_id in expected_map.values() {
|
for class_id in expected_map.values() {
|
||||||
let class_egress = format!("1:{class_id}");
|
let class_egress = format!("1:{class_id}");
|
||||||
let class_ingress = format!("2:{class_id}");
|
let class_ingress = format!("2:{class_id}");
|
||||||
@@ -330,8 +402,10 @@ fn inspect_and_diff(
|
|||||||
reason: format!("missing rate/ceil class details for peer class_id={class_id}"),
|
reason: format!("missing rate/ceil class details for peer class_id={class_id}"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if e.map(|v| !rate_ceil_matches_limit(v, &expected_limit)).unwrap_or(true)
|
if e.map(|v| !rate_ceil_matches_limit(v, &expected_limit))
|
||||||
|| i.map(|v| !rate_ceil_matches_limit(v, &expected_limit)).unwrap_or(true)
|
.unwrap_or(true)
|
||||||
|
|| i.map(|v| !rate_ceil_matches_limit(v, &expected_limit))
|
||||||
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
return Ok(TrafficDrift {
|
return Ok(TrafficDrift {
|
||||||
has_drift: true,
|
has_drift: true,
|
||||||
@@ -347,27 +421,51 @@ fn inspect_and_diff(
|
|||||||
.run(&["tc", "class", "show", "dev", ifb, "parent", "2:1"])
|
.run(&["tc", "class", "show", "dev", ifb, "parent", "2:1"])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let expected_class_count = peers.len();
|
let expected_class_count = peers.len();
|
||||||
if count_leaf_classes(&classes_egress) < expected_class_count {
|
if count_leaf_classes(&classes_egress) != expected_class_count {
|
||||||
return Ok(TrafficDrift {
|
return Ok(TrafficDrift {
|
||||||
has_drift: true,
|
has_drift: true,
|
||||||
reason: "missing egress per-peer classes".to_string(),
|
reason: "egress per-peer class count mismatch".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if count_leaf_classes(&classes_ingress) < expected_class_count {
|
if count_leaf_classes(&classes_ingress) != expected_class_count {
|
||||||
return Ok(TrafficDrift {
|
return Ok(TrafficDrift {
|
||||||
has_drift: true,
|
has_drift: true,
|
||||||
reason: "missing ingress per-peer classes".to_string(),
|
reason: "ingress per-peer class count mismatch".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let qdisc_egress = transport
|
||||||
|
.run(&["tc", "qdisc", "show", "dev", iface])
|
||||||
|
.unwrap_or_default();
|
||||||
|
let qdisc_ingress = transport
|
||||||
|
.run(&["tc", "qdisc", "show", "dev", ifb])
|
||||||
|
.unwrap_or_default();
|
||||||
|
for class_id in expected_map.values() {
|
||||||
|
let class_egress = format!("1:{class_id}");
|
||||||
|
let class_ingress = format!("2:{class_id}");
|
||||||
|
if !has_fq_codel_for_parent(&qdisc_egress, &class_egress) {
|
||||||
|
return Ok(TrafficDrift {
|
||||||
|
has_drift: true,
|
||||||
|
reason: format!("missing fq_codel for egress class {class_egress}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !has_fq_codel_for_parent(&qdisc_ingress, &class_ingress) {
|
||||||
|
return Ok(TrafficDrift {
|
||||||
|
has_drift: true,
|
||||||
|
reason: format!("missing fq_codel for ingress class {class_ingress}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(TrafficDrift {
|
Ok(TrafficDrift {
|
||||||
has_drift: false,
|
has_drift: false,
|
||||||
reason: "no drift".to_string(),
|
reason: "no drift".to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_filter_ip_class_map(filters: &str) -> BTreeMap<String, u16> {
|
fn extract_filter_ip_class_map(filters: &str) -> (BTreeMap<String, u16>, BTreeSet<String>) {
|
||||||
let mut out = BTreeMap::new();
|
let mut out = BTreeMap::new();
|
||||||
|
let mut duplicates = BTreeSet::new();
|
||||||
|
|
||||||
for block in split_filter_blocks(filters) {
|
for block in split_filter_blocks(filters) {
|
||||||
let mut ip: Option<String> = None;
|
let mut ip: Option<String> = None;
|
||||||
@@ -384,11 +482,46 @@ fn extract_filter_ip_class_map(filters: &str) -> BTreeMap<String, u16> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(ip), Some(class_id)) = (ip, class_id) {
|
if let (Some(ip), Some(class_id)) = (ip, class_id) {
|
||||||
|
if out.contains_key(&ip) {
|
||||||
|
duplicates.insert(ip.clone());
|
||||||
|
}
|
||||||
out.insert(ip, class_id);
|
out.insert(ip, class_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
out
|
(out, duplicates)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_fq_codel_for_parent(raw_qdisc: &str, parent_class: &str) -> bool {
|
||||||
|
raw_qdisc.lines().map(str::trim).any(|line| {
|
||||||
|
line.contains("fq_codel") && line.contains("parent") && line.contains(parent_class)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_root_class_rate_ceil(raw: &str, class_id: &str) -> Option<ClassRateCeil> {
|
||||||
|
for line in raw.lines() {
|
||||||
|
let t = line.trim();
|
||||||
|
if !t.contains("class htb")
|
||||||
|
|| !t.contains(class_id)
|
||||||
|
|| !t.contains(" rate ")
|
||||||
|
|| !t.contains(" ceil ")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let toks: Vec<&str> = t.split_whitespace().collect();
|
||||||
|
let rate = toks
|
||||||
|
.iter()
|
||||||
|
.position(|v| *v == "rate")
|
||||||
|
.and_then(|i| toks.get(i + 1))
|
||||||
|
.map(|s| s.to_string())?;
|
||||||
|
let ceil = toks
|
||||||
|
.iter()
|
||||||
|
.position(|v| *v == "ceil")
|
||||||
|
.and_then(|i| toks.get(i + 1))
|
||||||
|
.map(|s| s.to_string())?;
|
||||||
|
return Some(ClassRateCeil { rate, ceil });
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_filter_blocks(raw: &str) -> Vec<String> {
|
fn split_filter_blocks(raw: &str) -> Vec<String> {
|
||||||
@@ -649,6 +782,7 @@ fn apply_full_topology(
|
|||||||
iface,
|
iface,
|
||||||
ifb,
|
ifb,
|
||||||
cfg.traffic.class_start_id,
|
cfg.traffic.class_start_id,
|
||||||
|
&cfg.traffic.root_rate,
|
||||||
&cfg.traffic.peer_limit,
|
&cfg.traffic.peer_limit,
|
||||||
peers,
|
peers,
|
||||||
)?;
|
)?;
|
||||||
@@ -742,18 +876,42 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parse_tc_u32_hex_ipv4() {
|
fn parse_tc_u32_hex_ipv4() {
|
||||||
let raw = " match 0a080105/ffffffff at 16\n flowid 1:42";
|
let raw = " match 0a080105/ffffffff at 16\n flowid 1:42";
|
||||||
let m = extract_filter_ip_class_map(raw);
|
let (m, dup) = extract_filter_ip_class_map(raw);
|
||||||
|
assert!(dup.is_empty());
|
||||||
assert_eq!(m.get("10.8.1.5").copied(), Some(42));
|
assert_eq!(m.get("10.8.1.5").copied(), Some(42));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_block_based_filters() {
|
fn parse_block_based_filters() {
|
||||||
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080103/ffffffff at 16\n flowid 1:11";
|
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080103/ffffffff at 16\n flowid 1:11";
|
||||||
let m = extract_filter_ip_class_map(raw);
|
let (m, dup) = extract_filter_ip_class_map(raw);
|
||||||
|
assert!(dup.is_empty());
|
||||||
assert_eq!(m.get("10.8.1.2").copied(), Some(10));
|
assert_eq!(m.get("10.8.1.2").copied(), Some(10));
|
||||||
assert_eq!(m.get("10.8.1.3").copied(), Some(11));
|
assert_eq!(m.get("10.8.1.3").copied(), Some(11));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_duplicate_filter_ip() {
|
||||||
|
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080102/ffffffff at 16\n flowid 1:10";
|
||||||
|
let (_m, dup) = extract_filter_ip_class_map(raw);
|
||||||
|
assert!(dup.contains("10.8.1.2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_fq_codel_parent() {
|
||||||
|
let raw = "qdisc fq_codel 8001: dev wg0 parent 1:10 limit 10240p";
|
||||||
|
assert!(has_fq_codel_for_parent(raw, "1:10"));
|
||||||
|
assert!(!has_fq_codel_for_parent(raw, "1:11"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_root_rate_ceil() {
|
||||||
|
let raw = "class htb 1:1 root rate 1000Mbit ceil 1000Mbit";
|
||||||
|
let rc = parse_root_class_rate_ceil(raw, "1:1").expect("root class exists");
|
||||||
|
assert_eq!(normalize_tc_rate(&rc.rate), "1000mbit");
|
||||||
|
assert_eq!(normalize_tc_rate(&rc.ceil), "1000mbit");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_class_rate_ceil() {
|
fn parse_class_rate_ceil() {
|
||||||
let raw = "class htb 1:10 parent 1:1 rate 70Mbit ceil 70Mbit burst 1599b cburst 1599b\nclass htb 1:1 root rate 1000Mbit ceil 1000Mbit";
|
let raw = "class htb 1:10 parent 1:1 rate 70Mbit ceil 70Mbit burst 1599b cburst 1599b\nclass htb 1:1 root rate 1000Mbit ceil 1000Mbit";
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ pub fn discover_peers_with_transport(
|
|||||||
) -> anyhow::Result<Vec<Peer>> {
|
) -> anyhow::Result<Vec<Peer>> {
|
||||||
let primary = transport
|
let primary = transport
|
||||||
.run(&["wg", "show", "all", "allowed-ips"])
|
.run(&["wg", "show", "all", "allowed-ips"])
|
||||||
.unwrap_or_default();
|
.map_err(|e| anyhow::anyhow!("wg show all allowed-ips failed: {e}"))?;
|
||||||
|
|
||||||
let mut peers = parse_allowed_ips(&primary, peer_cidr)?;
|
let mut peers = parse_allowed_ips(&primary, peer_cidr)?;
|
||||||
if peers.is_empty() {
|
if peers.is_empty() {
|
||||||
let fallback = transport.run(&["wg", "show"]).unwrap_or_default();
|
let fallback = transport
|
||||||
|
.run(&["wg", "show"])
|
||||||
|
.map_err(|e| anyhow::anyhow!("wg show fallback failed: {e}"))?;
|
||||||
peers = parse_fallback_wg_show(&fallback, peer_cidr)?;
|
peers = parse_fallback_wg_show(&fallback, peer_cidr)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user