Реализован production-модуль sysctl_tuning и полная интеграция fix33
Добавлен отдельный host-level модуль sysctl_tuning без легаси-подходов: managed sysctl файл, точечный reconcile через sysctl -w только для drifted ключей, режимы tcp_bbr off/auto/require, безопасное удаление только managed-файла и атомарная запись конфигурации. Интегрированы новые CLI-команды apply-network-tuning/inspect-network-tuning, startup reconcile в daemon и секция Network tuning в status. Расширены config/defaults/валидация, обновлен пример конфига и документация. Добавлены unit-тесты для config и sysctl_tuning.
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
/lib/systemd/system/awg2-reboot-scheduler.service
|
/lib/systemd/system/awg2-reboot-scheduler.service
|
||||||
```
|
```
|
||||||
|
|
||||||
`awg2-harness.service` запускает `ensure-swap` перед стартом daemon, затем в фоне держит drift recovery. Daemon слушает Docker events и дополнительно делает периодический reconcile. `awg2-reboot-scheduler.service` отдельно читает общий config, выбирает случайное время в заданном UTC-окне и запрашивает reboot через `systemctl --no-block reboot`.
|
`awg2-harness.service` запускает `ensure-swap` перед стартом daemon, затем в фоне держит drift recovery. Daemon слушает Docker events и дополнительно делает периодический reconcile для `traffic`/`firewall`/`swap`; `sysctl_tuning` применяется только на старте daemon (если включён) или вручную отдельной командой. `awg2-reboot-scheduler.service` отдельно читает общий config, выбирает случайное время в заданном UTC-окне и запрашивает reboot через `systemctl --no-block reboot`.
|
||||||
|
|
||||||
Пакет **не** устанавливает Docker, **не** устанавливает AmneziaVPN, **не** включает сервисы автоматически и **не** создаёт боевой `/etc/awg2-harness/config.toml`. Это сделано намеренно: SSH, Amnezia и боевой config должны быть подготовлены оператором до включения firewall/reconcile.
|
Пакет **не** устанавливает Docker, **не** устанавливает AmneziaVPN, **не** включает сервисы автоматически и **не** создаёт боевой `/etc/awg2-harness/config.toml`. Это сделано намеренно: SSH, Amnezia и боевой config должны быть подготовлены оператором до включения firewall/reconcile.
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,36 @@ container_ready_timeout_secs = 120
|
|||||||
class_start_id = 10
|
class_start_id = 10
|
||||||
ifb_name = "ifb0"
|
ifb_name = "ifb0"
|
||||||
|
|
||||||
|
[sysctl_tuning]
|
||||||
|
enabled = true
|
||||||
|
apply_on_daemon_start = true
|
||||||
|
|
||||||
|
# Включает IPv4 forwarding для VPN/Docker gateway host.
|
||||||
|
ip_forward = true
|
||||||
|
|
||||||
|
# off | auto | require
|
||||||
|
# auto: использовать BBR, если доступен; иначе пропустить без ошибки.
|
||||||
|
# require: завершить с ошибкой, если BBR недоступен.
|
||||||
|
tcp_bbr = "auto"
|
||||||
|
|
||||||
|
# none | fq | fq_codel
|
||||||
|
# fq применяется только когда BBR активен.
|
||||||
|
default_qdisc = "fq"
|
||||||
|
|
||||||
|
# Лимиты socket buffers в байтах.
|
||||||
|
# Баланс для хоста порядка ~500 Mbit/s.
|
||||||
|
rmem_max = 67108864
|
||||||
|
wmem_max = 67108864
|
||||||
|
|
||||||
|
netdev_backlog_enabled = true
|
||||||
|
netdev_max_backlog = 50000
|
||||||
|
|
||||||
|
udp_rmem_min_enabled = true
|
||||||
|
udp_rmem_min = 8192
|
||||||
|
|
||||||
|
# net.ipv4.udp_wmem_min intentionally not managed:
|
||||||
|
# Linux kernel documentation states it has no effect.
|
||||||
|
|
||||||
[host_firewall]
|
[host_firewall]
|
||||||
enabled = true
|
enabled = true
|
||||||
# Должен совпадать с реально слушающим портом sshd до apply-firewall.
|
# Должен совпадать с реально слушающим портом sshd до apply-firewall.
|
||||||
|
|||||||
@@ -96,6 +96,18 @@
|
|||||||
- [ ] `vm.swappiness = 1` применён.
|
- [ ] `vm.swappiness = 1` применён.
|
||||||
- [ ] Повторный `ensure-swap` ничего не пересоздаёт и не добавляет дублей.
|
- [ ] Повторный `ensure-swap` ничего не пересоздаёт и не добавляет дублей.
|
||||||
|
|
||||||
|
## H2. Sysctl network tuning
|
||||||
|
|
||||||
|
- [ ] `awg2-harness apply-network-tuning` успешно завершается.
|
||||||
|
- [ ] `awg2-harness inspect-network-tuning` показывает `enabled: true` и `drift: no`.
|
||||||
|
- [ ] Создаётся только `/etc/sysctl.d/90-awg2-harness-vpn.conf`.
|
||||||
|
- [ ] В managed-файле есть marker `Managed by awg2-harness sysctl_tuning`.
|
||||||
|
- [ ] В managed-файле отсутствует `udp_wmem_min`.
|
||||||
|
- [ ] Runtime apply меняет только drifted keys (`sysctl -w`), без `sysctl --system`.
|
||||||
|
- [ ] При `tcp_bbr = auto` и недоступном BBR сервис не падает, в статусе есть warning.
|
||||||
|
- [ ] При `tcp_bbr = require` и недоступном BBR команда/старт завершается с ошибкой.
|
||||||
|
- [ ] При `enabled=false` managed-файл удаляется только если содержит managed marker.
|
||||||
|
|
||||||
## I. Anti-torrent nftables guard
|
## I. Anti-torrent nftables guard
|
||||||
|
|
||||||
- [ ] Если `anti_torrent.enabled = true`, в Amnezia netns есть `table inet awg2_harness`.
|
- [ ] Если `anti_torrent.enabled = true`, в Amnezia netns есть `table inet awg2_harness`.
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ container_ready_timeout_secs = 120
|
|||||||
class_start_id = 10
|
class_start_id = 10
|
||||||
ifb_name = "ifb0"
|
ifb_name = "ifb0"
|
||||||
|
|
||||||
|
[sysctl_tuning]
|
||||||
|
enabled = true
|
||||||
|
apply_on_daemon_start = true
|
||||||
|
ip_forward = true
|
||||||
|
tcp_bbr = "auto" # off | auto | require
|
||||||
|
default_qdisc = "fq" # none | fq | fq_codel
|
||||||
|
rmem_max = 67108864
|
||||||
|
wmem_max = 67108864
|
||||||
|
netdev_backlog_enabled = true
|
||||||
|
netdev_max_backlog = 50000
|
||||||
|
udp_rmem_min_enabled = true
|
||||||
|
udp_rmem_min = 8192
|
||||||
|
|
||||||
[host_firewall]
|
[host_firewall]
|
||||||
enabled = true
|
enabled = true
|
||||||
ssh_port = 2323
|
ssh_port = 2323
|
||||||
@@ -98,6 +111,21 @@ swappiness = 1
|
|||||||
|
|
||||||
`traffic.ifb_name` — имя IFB-интерфейса для ingress shaping. Production default — `ifb0`. Этот параметр можно оставить в конфиге, потому что бинарь реально создаёт/удаляет этот интерфейс.
|
`traffic.ifb_name` — имя IFB-интерфейса для ingress shaping. Production default — `ifb0`. Этот параметр можно оставить в конфиге, потому что бинарь реально создаёт/удаляет этот интерфейс.
|
||||||
|
|
||||||
|
`sysctl_tuning` — отдельный host-level модуль управляемого сетевого baseline через `sysctl`:
|
||||||
|
|
||||||
|
- persistent-файл: `/etc/sysctl.d/90-awg2-harness-vpn.conf`;
|
||||||
|
- runtime apply: только `sysctl -w` и только для drifted keys;
|
||||||
|
- на установке `.deb` модуль не применяется;
|
||||||
|
- на старте daemon модуль применяется, если `enabled=true` и `apply_on_daemon_start=true`;
|
||||||
|
- `tcp_bbr = auto` не валит запуск при недоступном BBR (warning), `require` — валит.
|
||||||
|
|
||||||
|
Ограничения модуля:
|
||||||
|
|
||||||
|
- не использует `/etc/sysctl.conf`;
|
||||||
|
- не запускает `sysctl --system`;
|
||||||
|
- не меняет `nftables`, Docker bridge и container-local sysctl;
|
||||||
|
- не управляет `net.ipv4.udp_wmem_min` (в Linux документации параметр указан как не имеющий эффекта).
|
||||||
|
|
||||||
При drift orchestrator всегда делает controlled rebuild owned topology.
|
При drift orchestrator всегда делает controlled rebuild owned topology.
|
||||||
|
|
||||||
`anti_torrent.enabled` — включает nftables connection-count guard против high-peer-count P2P-нагрузки. Это отдельный AWG2 Harness module и часть текущего production-контракта.
|
`anti_torrent.enabled` — включает nftables connection-count guard против high-peer-count P2P-нагрузки. Это отдельный AWG2 Harness module и часть текущего production-контракта.
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ Subcommands:
|
|||||||
```bash
|
```bash
|
||||||
awg2-harness daemon
|
awg2-harness daemon
|
||||||
awg2-harness apply-limits
|
awg2-harness apply-limits
|
||||||
|
awg2-harness apply-network-tuning
|
||||||
|
awg2-harness inspect-network-tuning
|
||||||
awg2-harness apply-firewall
|
awg2-harness apply-firewall
|
||||||
awg2-harness status
|
awg2-harness status
|
||||||
awg2-harness ensure-swap
|
awg2-harness ensure-swap
|
||||||
@@ -51,6 +53,15 @@ awg2-harness validate-config
|
|||||||
awg2-harness validate-runtime
|
awg2-harness validate-runtime
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`apply-network-tuning`/`inspect-network-tuning` относятся к отдельному host-level модулю `sysctl_tuning`.
|
||||||
|
Этот модуль:
|
||||||
|
|
||||||
|
- управляет только sysctl параметрами хоста;
|
||||||
|
- пишет только `/etc/sysctl.d/90-awg2-harness-vpn.conf`;
|
||||||
|
- применяет runtime-изменения точечно (`sysctl -w`) только для drifted keys;
|
||||||
|
- не использует `/etc/sysctl.conf` и `sysctl --system`;
|
||||||
|
- не управляет `udp_wmem_min`.
|
||||||
|
|
||||||
## 3. Module layout
|
## 3. Module layout
|
||||||
|
|
||||||
Recommended Rust module split:
|
Recommended Rust module split:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- Production-хост получает готовый Debian-пакет; Rust/cargo/source tree на нём не требуются.
|
- Production-хост получает готовый Debian-пакет; Rust/cargo/source tree на нём не требуются.
|
||||||
- `/etc/awg2-harness/config.toml` создаётся вручную. `config.example.toml` не копируется автоматически как боевой config.
|
- `/etc/awg2-harness/config.toml` создаётся вручную. `config.example.toml` не копируется автоматически как боевой config.
|
||||||
- Пакет не устанавливает Docker/Amnezia и не включает services автоматически.
|
- Пакет не устанавливает Docker/Amnezia и не включает services автоматически.
|
||||||
|
- `sysctl_tuning` не применяется в `postinst`; применение только на старте daemon или вручную через CLI.
|
||||||
|
|
||||||
## A. Build machine
|
## A. Build machine
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ AmneziaDNS:
|
|||||||
|
|
||||||
Expected status:
|
Expected status:
|
||||||
service: active
|
service: active
|
||||||
|
network tuning drift: no
|
||||||
traffic drift: no
|
traffic drift: no
|
||||||
host firewall drift: no
|
host firewall drift: no
|
||||||
anti-torrent drift: no
|
anti-torrent drift: no
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ pub struct Cli {
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
Daemon,
|
Daemon,
|
||||||
ApplyLimits,
|
ApplyLimits,
|
||||||
|
ApplyNetworkTuning,
|
||||||
|
InspectNetworkTuning,
|
||||||
ApplyFirewall,
|
ApplyFirewall,
|
||||||
Status,
|
Status,
|
||||||
EnsureSwap,
|
EnsureSwap,
|
||||||
|
|||||||
+165
-76
@@ -30,12 +30,48 @@ fn default_input_policy() -> String {
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub amnezia: AmneziaConfig,
|
pub amnezia: AmneziaConfig,
|
||||||
pub traffic: TrafficConfig,
|
pub traffic: TrafficConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sysctl_tuning: SysctlTuningConfig,
|
||||||
pub host_firewall: HostFirewallConfig,
|
pub host_firewall: HostFirewallConfig,
|
||||||
pub anti_torrent: AntiTorrentConfig,
|
pub anti_torrent: AntiTorrentConfig,
|
||||||
pub reboot: RebootConfig,
|
pub reboot: RebootConfig,
|
||||||
pub swap: SwapConfig,
|
pub swap: SwapConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct SysctlTuningConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub apply_on_daemon_start: bool,
|
||||||
|
pub ip_forward: bool,
|
||||||
|
pub tcp_bbr: String,
|
||||||
|
pub default_qdisc: String,
|
||||||
|
pub rmem_max: u64,
|
||||||
|
pub wmem_max: u64,
|
||||||
|
pub netdev_backlog_enabled: bool,
|
||||||
|
pub netdev_max_backlog: u32,
|
||||||
|
pub udp_rmem_min_enabled: bool,
|
||||||
|
pub udp_rmem_min: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SysctlTuningConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
apply_on_daemon_start: true,
|
||||||
|
ip_forward: true,
|
||||||
|
tcp_bbr: "auto".to_string(),
|
||||||
|
default_qdisc: "fq".to_string(),
|
||||||
|
rmem_max: 67_108_864,
|
||||||
|
wmem_max: 67_108_864,
|
||||||
|
netdev_backlog_enabled: true,
|
||||||
|
netdev_max_backlog: 50_000,
|
||||||
|
udp_rmem_min_enabled: true,
|
||||||
|
udp_rmem_min: 8192,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct AmneziaConfig {
|
pub struct AmneziaConfig {
|
||||||
pub container_names: Vec<String>,
|
pub container_names: Vec<String>,
|
||||||
@@ -139,6 +175,33 @@ impl Config {
|
|||||||
}
|
}
|
||||||
parse_tc_rate_bps(&self.traffic.peer_limit)?;
|
parse_tc_rate_bps(&self.traffic.peer_limit)?;
|
||||||
parse_tc_rate_bps(&self.traffic.root_rate)?;
|
parse_tc_rate_bps(&self.traffic.root_rate)?;
|
||||||
|
|
||||||
|
let tcp_bbr_mode = self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase();
|
||||||
|
if tcp_bbr_mode != "off" && tcp_bbr_mode != "auto" && tcp_bbr_mode != "require" {
|
||||||
|
anyhow::bail!("sysctl_tuning.tcp_bbr must be one of: off, auto, require");
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_qdisc = self.sysctl_tuning.default_qdisc.trim().to_ascii_lowercase();
|
||||||
|
if default_qdisc != "none" && default_qdisc != "fq" && default_qdisc != "fq_codel" {
|
||||||
|
anyhow::bail!("sysctl_tuning.default_qdisc must be one of: none, fq, fq_codel");
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIB: u64 = 1024 * 1024;
|
||||||
|
if self.sysctl_tuning.rmem_max < MIB || self.sysctl_tuning.rmem_max > 512 * MIB {
|
||||||
|
anyhow::bail!("sysctl_tuning.rmem_max must be in 1MiB..=512MiB");
|
||||||
|
}
|
||||||
|
if self.sysctl_tuning.wmem_max < MIB || self.sysctl_tuning.wmem_max > 512 * MIB {
|
||||||
|
anyhow::bail!("sysctl_tuning.wmem_max must be in 1MiB..=512MiB");
|
||||||
|
}
|
||||||
|
if self.sysctl_tuning.netdev_max_backlog < 1000
|
||||||
|
|| self.sysctl_tuning.netdev_max_backlog > 100_000
|
||||||
|
{
|
||||||
|
anyhow::bail!("sysctl_tuning.netdev_max_backlog must be in 1000..=100000");
|
||||||
|
}
|
||||||
|
if self.sysctl_tuning.udp_rmem_min < 4096 || self.sysctl_tuning.udp_rmem_min > 65_536 {
|
||||||
|
anyhow::bail!("sysctl_tuning.udp_rmem_min must be in 4096..=65536");
|
||||||
|
}
|
||||||
|
|
||||||
if self.swap.swappiness > 100 {
|
if self.swap.swappiness > 100 {
|
||||||
anyhow::bail!("swap.swappiness must be <= 100");
|
anyhow::bail!("swap.swappiness must be <= 100");
|
||||||
}
|
}
|
||||||
@@ -224,11 +287,19 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_host_tools(&self) -> anyhow::Result<()> {
|
fn check_host_tools(&self) -> anyhow::Result<()> {
|
||||||
let mut required = vec!["tc", "ip", "wg", "docker", "modprobe"];
|
let mut required = vec!["tc", "ip", "wg", "docker"];
|
||||||
if !crate::netns::nsenter_available() {
|
if !crate::netns::nsenter_available() {
|
||||||
anyhow::bail!("required host tool not found in PATH: nsenter");
|
anyhow::bail!("required host tool not found in PATH: nsenter");
|
||||||
}
|
}
|
||||||
required.push("nsenter");
|
required.push("nsenter");
|
||||||
|
if self.sysctl_tuning.enabled {
|
||||||
|
required.push("sysctl");
|
||||||
|
}
|
||||||
|
if self.sysctl_tuning.enabled
|
||||||
|
&& self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase() != "off"
|
||||||
|
{
|
||||||
|
required.push("modprobe");
|
||||||
|
}
|
||||||
if self.anti_torrent.enabled {
|
if self.anti_torrent.enabled {
|
||||||
required.push("nft");
|
required.push("nft");
|
||||||
}
|
}
|
||||||
@@ -408,6 +479,46 @@ pub fn ip_in_cidr(ip: Ipv4Addr, cidr: &str) -> anyhow::Result<bool> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn base_config() -> Config {
|
||||||
|
Config {
|
||||||
|
amnezia: AmneziaConfig {
|
||||||
|
container_names: vec!["x".into()],
|
||||||
|
interface_candidates: vec!["awg0".into()],
|
||||||
|
interface_patterns: vec!["awg*".into()],
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
},
|
||||||
|
traffic: TrafficConfig {
|
||||||
|
enabled: true,
|
||||||
|
peer_limit: "70mbit".into(),
|
||||||
|
root_rate: "1000mbit".into(),
|
||||||
|
reconcile_interval_secs: 15,
|
||||||
|
container_ready_timeout_secs: 30,
|
||||||
|
class_start_id: 10,
|
||||||
|
ifb_name: "ifb0".into(),
|
||||||
|
},
|
||||||
|
sysctl_tuning: SysctlTuningConfig::default(),
|
||||||
|
host_firewall: HostFirewallConfig::default(),
|
||||||
|
anti_torrent: AntiTorrentConfig {
|
||||||
|
enabled: false,
|
||||||
|
max_connections_per_peer: 100,
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
apply_in_amnezia_netns: false,
|
||||||
|
},
|
||||||
|
reboot: RebootConfig {
|
||||||
|
enabled: false,
|
||||||
|
window_start_utc: "01:00".into(),
|
||||||
|
window_end_utc: "02:00".into(),
|
||||||
|
},
|
||||||
|
swap: SwapConfig {
|
||||||
|
enabled: false,
|
||||||
|
path: "/swapfile".into(),
|
||||||
|
size_mb: 2048,
|
||||||
|
priority: 1,
|
||||||
|
swappiness: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_cidr_invalid_prefix() {
|
fn parse_cidr_invalid_prefix() {
|
||||||
let err = parse_cidr_v4("10.8.1.0/33").err();
|
let err = parse_cidr_v4("10.8.1.0/33").err();
|
||||||
@@ -438,86 +549,64 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn swap_rejects_dangerous_path() {
|
fn swap_rejects_dangerous_path() {
|
||||||
let cfg = Config {
|
let mut cfg = base_config();
|
||||||
amnezia: AmneziaConfig {
|
cfg.swap.enabled = true;
|
||||||
container_names: vec!["x".into()],
|
cfg.swap.path = "/etc/fstab".into();
|
||||||
interface_candidates: vec!["awg0".into()],
|
|
||||||
interface_patterns: vec!["awg*".into()],
|
|
||||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
|
||||||
},
|
|
||||||
traffic: TrafficConfig {
|
|
||||||
enabled: true,
|
|
||||||
peer_limit: "70mbit".into(),
|
|
||||||
root_rate: "1000mbit".into(),
|
|
||||||
reconcile_interval_secs: 15,
|
|
||||||
container_ready_timeout_secs: 30,
|
|
||||||
class_start_id: 10,
|
|
||||||
ifb_name: "ifb0".into(),
|
|
||||||
},
|
|
||||||
host_firewall: HostFirewallConfig::default(),
|
|
||||||
anti_torrent: AntiTorrentConfig {
|
|
||||||
enabled: false,
|
|
||||||
max_connections_per_peer: 100,
|
|
||||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
|
||||||
apply_in_amnezia_netns: false,
|
|
||||||
},
|
|
||||||
reboot: RebootConfig {
|
|
||||||
enabled: false,
|
|
||||||
window_start_utc: "01:00".into(),
|
|
||||||
window_end_utc: "02:00".into(),
|
|
||||||
},
|
|
||||||
swap: SwapConfig {
|
|
||||||
enabled: true,
|
|
||||||
path: "/etc/fstab".into(),
|
|
||||||
size_mb: 2048,
|
|
||||||
priority: 1,
|
|
||||||
swappiness: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
assert!(cfg.validate_config().is_err());
|
assert!(cfg.validate_config().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn host_firewall_rejects_empty_protocols() {
|
fn host_firewall_rejects_empty_protocols() {
|
||||||
let cfg = Config {
|
let mut cfg = base_config();
|
||||||
amnezia: AmneziaConfig {
|
cfg.host_firewall.amnezia_protocols = vec![];
|
||||||
container_names: vec!["x".into()],
|
assert!(cfg.validate_config().is_err());
|
||||||
interface_candidates: vec!["awg0".into()],
|
}
|
||||||
interface_patterns: vec!["awg*".into()],
|
|
||||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
#[test]
|
||||||
},
|
fn sysctl_tuning_default_is_valid() {
|
||||||
traffic: TrafficConfig {
|
let cfg = base_config();
|
||||||
enabled: true,
|
assert!(cfg.validate_config().is_ok());
|
||||||
peer_limit: "70mbit".into(),
|
}
|
||||||
root_rate: "1000mbit".into(),
|
|
||||||
reconcile_interval_secs: 15,
|
#[test]
|
||||||
container_ready_timeout_secs: 30,
|
fn sysctl_tuning_rejects_invalid_tcp_bbr() {
|
||||||
class_start_id: 10,
|
let mut cfg = base_config();
|
||||||
ifb_name: "ifb0".into(),
|
cfg.sysctl_tuning.tcp_bbr = "bad".into();
|
||||||
},
|
assert!(cfg.validate_config().is_err());
|
||||||
host_firewall: HostFirewallConfig {
|
}
|
||||||
amnezia_protocols: vec![],
|
|
||||||
..HostFirewallConfig::default()
|
#[test]
|
||||||
},
|
fn sysctl_tuning_rejects_invalid_default_qdisc() {
|
||||||
anti_torrent: AntiTorrentConfig {
|
let mut cfg = base_config();
|
||||||
enabled: false,
|
cfg.sysctl_tuning.default_qdisc = "cake".into();
|
||||||
max_connections_per_peer: 100,
|
assert!(cfg.validate_config().is_err());
|
||||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
}
|
||||||
apply_in_amnezia_netns: false,
|
|
||||||
},
|
#[test]
|
||||||
reboot: RebootConfig {
|
fn sysctl_tuning_rejects_rmem_max_below_1mib() {
|
||||||
enabled: false,
|
let mut cfg = base_config();
|
||||||
window_start_utc: "01:00".into(),
|
cfg.sysctl_tuning.rmem_max = 1024;
|
||||||
window_end_utc: "02:00".into(),
|
assert!(cfg.validate_config().is_err());
|
||||||
},
|
}
|
||||||
swap: SwapConfig {
|
|
||||||
enabled: false,
|
#[test]
|
||||||
path: "/swapfile".into(),
|
fn sysctl_tuning_rejects_rmem_max_above_512mib() {
|
||||||
size_mb: 2048,
|
let mut cfg = base_config();
|
||||||
priority: 1,
|
cfg.sysctl_tuning.rmem_max = 513 * 1024 * 1024;
|
||||||
swappiness: 1,
|
assert!(cfg.validate_config().is_err());
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
#[test]
|
||||||
|
fn sysctl_tuning_rejects_netdev_backlog_above_limit() {
|
||||||
|
let mut cfg = base_config();
|
||||||
|
cfg.sysctl_tuning.netdev_max_backlog = 100_001;
|
||||||
|
assert!(cfg.validate_config().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sysctl_tuning_rejects_udp_rmem_min_below_limit() {
|
||||||
|
let mut cfg = base_config();
|
||||||
|
cfg.sysctl_tuning.udp_rmem_min = 4095;
|
||||||
assert!(cfg.validate_config().is_err());
|
assert!(cfg.validate_config().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{config::Config, docker, firewall, iface, netns::build_transport, swap, traffic};
|
use crate::{
|
||||||
|
config::Config, docker, firewall, iface, netns::build_transport, swap, sysctl_tuning, traffic,
|
||||||
|
};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -26,6 +28,17 @@ pub fn run(cfg: Config) -> anyhow::Result<()> {
|
|||||||
info!("starting daemon");
|
info!("starting daemon");
|
||||||
let state = Arc::new(Mutex::new(LastActionState::default()));
|
let state = Arc::new(Mutex::new(LastActionState::default()));
|
||||||
|
|
||||||
|
if cfg.sysctl_tuning.enabled && cfg.sysctl_tuning.apply_on_daemon_start {
|
||||||
|
if let Err(e) = sysctl_tuning::ensure_sysctl_tuning(&cfg) {
|
||||||
|
set_err(&state, format!("startup network tuning reconcile: {e}"));
|
||||||
|
append_event(&format!("startup_network_tuning error=\"{e}\""));
|
||||||
|
return Err(e);
|
||||||
|
} else {
|
||||||
|
set_ok(&state, "startup network tuning reconcile ok");
|
||||||
|
append_event("startup_network_tuning ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = swap::ensure_swap(&cfg) {
|
if let Err(e) = swap::ensure_swap(&cfg) {
|
||||||
set_err(&state, format!("startup swap reconcile: {e}"));
|
set_err(&state, format!("startup swap reconcile: {e}"));
|
||||||
append_event(&format!("startup_swap error=\"{e}\""));
|
append_event(&format!("startup_swap error=\"{e}\""));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod lock;
|
|||||||
mod netns;
|
mod netns;
|
||||||
mod status;
|
mod status;
|
||||||
mod swap;
|
mod swap;
|
||||||
|
mod sysctl_tuning;
|
||||||
mod traffic;
|
mod traffic;
|
||||||
mod wg;
|
mod wg;
|
||||||
|
|
||||||
@@ -49,6 +50,18 @@ fn main() -> anyhow::Result<()> {
|
|||||||
traffic::reconcile_traffic(&cfg)?;
|
traffic::reconcile_traffic(&cfg)?;
|
||||||
println!("traffic limits reconciled");
|
println!("traffic limits reconciled");
|
||||||
}
|
}
|
||||||
|
Command::ApplyNetworkTuning => {
|
||||||
|
let cfg = Config::load(&config_path)?;
|
||||||
|
cfg.validate_all()?;
|
||||||
|
sysctl_tuning::ensure_sysctl_tuning(&cfg)?;
|
||||||
|
println!("network tuning reconciled");
|
||||||
|
}
|
||||||
|
Command::InspectNetworkTuning => {
|
||||||
|
let cfg = Config::load(&config_path)?;
|
||||||
|
cfg.validate_config()?;
|
||||||
|
let s = sysctl_tuning::inspect_sysctl_tuning(&cfg)?;
|
||||||
|
sysctl_tuning::print_sysctl_tuning_status(&s);
|
||||||
|
}
|
||||||
Command::ApplyFirewall => {
|
Command::ApplyFirewall => {
|
||||||
let cfg = Config::load(&config_path)?;
|
let cfg = Config::load(&config_path)?;
|
||||||
cfg.validate_all()?;
|
cfg.validate_all()?;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::{config::Config, docker, firewall, netns::build_transport, swap, traffic, wg};
|
use crate::{
|
||||||
|
config::Config, docker, firewall, netns::build_transport, swap, sysctl_tuning, traffic, wg,
|
||||||
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
@@ -20,6 +22,8 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
Err(_) => println!(" service: unknown"),
|
Err(_) => println!(" service: unknown"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print_network_tuning_status(cfg);
|
||||||
|
|
||||||
let target = match docker::discover_target(cfg) {
|
let target = match docker::discover_target(cfg) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -270,6 +274,17 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_network_tuning_status(cfg: &Config) {
|
||||||
|
match sysctl_tuning::inspect_sysctl_tuning(cfg) {
|
||||||
|
Ok(s) => sysctl_tuning::print_sysctl_tuning_status(&s),
|
||||||
|
Err(e) => {
|
||||||
|
println!("Network tuning:");
|
||||||
|
println!(" enabled: {}", cfg.sysctl_tuning.enabled);
|
||||||
|
println!(" drift: unknown ({e})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
|
fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
|
||||||
println!("Peers:");
|
println!("Peers:");
|
||||||
println!(" unavailable (container/netns not discovered)");
|
println!(" unavailable (container/netns not discovered)");
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
use std::{collections::BTreeMap, fs, io::Write, path::Path, process::Command, time::Duration};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
use crate::{config::Config, lock::FileLock};
|
||||||
|
|
||||||
|
const SYSCTL_CONF_PATH: &str = "/etc/sysctl.d/90-awg2-harness-vpn.conf";
|
||||||
|
const MANAGED_MARKER: &str = "# Managed by awg2-harness sysctl_tuning";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SysctlTuningStatus {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub apply_on_daemon_start: bool,
|
||||||
|
pub tcp_bbr_mode: String,
|
||||||
|
pub conf_path: String,
|
||||||
|
pub persisted_present: bool,
|
||||||
|
pub persisted_matches: bool,
|
||||||
|
pub bbr_available: Option<bool>,
|
||||||
|
pub bbr_active: Option<bool>,
|
||||||
|
pub current: Vec<SysctlValue>,
|
||||||
|
pub desired: Vec<SysctlValue>,
|
||||||
|
pub drift_reason: Option<String>,
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SysctlValue {
|
||||||
|
pub key: String,
|
||||||
|
pub desired: Option<String>,
|
||||||
|
pub current: Option<String>,
|
||||||
|
pub managed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct RenderPlan {
|
||||||
|
desired: BTreeMap<String, String>,
|
||||||
|
persisted: String,
|
||||||
|
bbr_available: Option<bool>,
|
||||||
|
bbr_active: Option<bool>,
|
||||||
|
warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_sysctl_tuning(cfg: &Config) -> anyhow::Result<()> {
|
||||||
|
if !cfg.sysctl_tuning.enabled {
|
||||||
|
remove_managed_sysctl_file_if_disabled(cfg)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _lock = FileLock::acquire_timeout(
|
||||||
|
"/run/awg2-harness/sysctl_tuning.lock",
|
||||||
|
Duration::from_secs(30),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let plan = build_render_plan(cfg, true)?;
|
||||||
|
write_atomic(SYSCTL_CONF_PATH, &plan.persisted)?;
|
||||||
|
|
||||||
|
if let Some(v) = plan.desired.get("net.ipv4.ip_forward") {
|
||||||
|
let current = read_sysctl("net.ipv4.ip_forward").ok();
|
||||||
|
if current.as_deref() != Some(v.as_str()) {
|
||||||
|
write_sysctl("net.ipv4.ip_forward", v)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (k, v) in &plan.desired {
|
||||||
|
if k == "net.ipv4.ip_forward" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let current = read_sysctl(k).ok();
|
||||||
|
if current.as_deref() != Some(v.as_str()) {
|
||||||
|
write_sysctl(k, v)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (k, v) in &plan.desired {
|
||||||
|
let current = read_sysctl(k).unwrap_or_else(|_| "<unreadable>".to_string());
|
||||||
|
if current != *v {
|
||||||
|
anyhow::bail!("sysctl verify failed for {k}: current={current}, desired={v}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inspect_sysctl_tuning(cfg: &Config) -> anyhow::Result<SysctlTuningStatus> {
|
||||||
|
let plan = build_render_plan(cfg, false)?;
|
||||||
|
let persisted_raw = fs::read_to_string(SYSCTL_CONF_PATH).ok();
|
||||||
|
let persisted_present = persisted_raw.is_some();
|
||||||
|
let persisted_matches = persisted_raw
|
||||||
|
.as_deref()
|
||||||
|
.map(|v| normalize(v) == normalize(&plan.persisted))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let mut values = Vec::<SysctlValue>::new();
|
||||||
|
for (k, v) in &plan.desired {
|
||||||
|
values.push(SysctlValue {
|
||||||
|
key: k.clone(),
|
||||||
|
desired: Some(v.clone()),
|
||||||
|
current: read_sysctl(k).ok(),
|
||||||
|
managed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut drift_reason = None;
|
||||||
|
if cfg.sysctl_tuning.enabled {
|
||||||
|
if !persisted_present {
|
||||||
|
drift_reason = Some("managed sysctl file is missing".to_string());
|
||||||
|
} else if !persisted_matches {
|
||||||
|
drift_reason = Some("persisted sysctl file differs from rendered config".to_string());
|
||||||
|
} else if let Some(v) = values
|
||||||
|
.iter()
|
||||||
|
.find(|v| v.desired.as_deref() != v.current.as_deref())
|
||||||
|
{
|
||||||
|
drift_reason = Some(format!(
|
||||||
|
"runtime value mismatch for {}: current={:?} desired={:?}",
|
||||||
|
v.key, v.current, v.desired
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SysctlTuningStatus {
|
||||||
|
enabled: cfg.sysctl_tuning.enabled,
|
||||||
|
apply_on_daemon_start: cfg.sysctl_tuning.apply_on_daemon_start,
|
||||||
|
tcp_bbr_mode: cfg.sysctl_tuning.tcp_bbr.clone(),
|
||||||
|
conf_path: SYSCTL_CONF_PATH.to_string(),
|
||||||
|
persisted_present,
|
||||||
|
persisted_matches,
|
||||||
|
bbr_available: plan.bbr_available,
|
||||||
|
bbr_active: plan.bbr_active,
|
||||||
|
current: values.clone(),
|
||||||
|
desired: values,
|
||||||
|
drift_reason,
|
||||||
|
warnings: plan.warnings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_sysctl_tuning_status(status: &SysctlTuningStatus) {
|
||||||
|
println!("Network tuning:");
|
||||||
|
println!(" enabled: {}", status.enabled);
|
||||||
|
println!(" apply_on_daemon_start: {}", status.apply_on_daemon_start);
|
||||||
|
println!(" tcp_bbr_mode: {}", status.tcp_bbr_mode);
|
||||||
|
println!(" conf_path: {}", status.conf_path);
|
||||||
|
println!(
|
||||||
|
" persisted_present: {}",
|
||||||
|
if status.persisted_present {
|
||||||
|
"yes"
|
||||||
|
} else {
|
||||||
|
"no"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" persisted_matches: {}",
|
||||||
|
if status.persisted_matches {
|
||||||
|
"yes"
|
||||||
|
} else {
|
||||||
|
"no"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if let Some(v) = status.bbr_available {
|
||||||
|
println!(" bbr_available: {}", if v { "yes" } else { "no" });
|
||||||
|
}
|
||||||
|
if let Some(v) = status.bbr_active {
|
||||||
|
println!(" bbr_active: {}", if v { "yes" } else { "no" });
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in &status.current {
|
||||||
|
println!(
|
||||||
|
" {}: current={} desired={}",
|
||||||
|
row.key,
|
||||||
|
row.current.as_deref().unwrap_or("n/a"),
|
||||||
|
row.desired.as_deref().unwrap_or("n/a")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reason) = &status.drift_reason {
|
||||||
|
println!(" drift: yes");
|
||||||
|
println!(" reason: {reason}");
|
||||||
|
} else {
|
||||||
|
println!(" drift: no");
|
||||||
|
}
|
||||||
|
for w in &status.warnings {
|
||||||
|
println!(" warning: {w}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_managed_sysctl_file_if_disabled(cfg: &Config) -> anyhow::Result<()> {
|
||||||
|
if cfg.sysctl_tuning.enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let path = Path::new(SYSCTL_CONF_PATH);
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(path)?;
|
||||||
|
let first = raw.lines().next().unwrap_or("");
|
||||||
|
if !first.contains(MANAGED_MARKER) {
|
||||||
|
anyhow::bail!("refusing to remove unmanaged sysctl file: {SYSCTL_CONF_PATH}");
|
||||||
|
}
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_render_plan(cfg: &Config, allow_modprobe: bool) -> anyhow::Result<RenderPlan> {
|
||||||
|
let mut desired = BTreeMap::<String, String>::new();
|
||||||
|
let mut warnings = Vec::<String>::new();
|
||||||
|
|
||||||
|
if !cfg.sysctl_tuning.enabled {
|
||||||
|
return Ok(RenderPlan {
|
||||||
|
desired,
|
||||||
|
persisted: render_persisted(&desired),
|
||||||
|
bbr_available: None,
|
||||||
|
bbr_active: None,
|
||||||
|
warnings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode = cfg.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase();
|
||||||
|
let mut bbr_avail = bbr_available()?;
|
||||||
|
if !bbr_avail && mode != "off" && allow_modprobe {
|
||||||
|
let _ = Command::new("modprobe").args(["tcp_bbr"]).output();
|
||||||
|
bbr_avail = bbr_available()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bbr_active = if mode == "off" {
|
||||||
|
None
|
||||||
|
} else if bbr_avail {
|
||||||
|
Some(true)
|
||||||
|
} else {
|
||||||
|
if mode == "require" {
|
||||||
|
anyhow::bail!("tcp_bbr=require but bbr is unavailable");
|
||||||
|
}
|
||||||
|
warnings.push("bbr unavailable; tcp tuning skipped in auto mode".to_string());
|
||||||
|
Some(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
if cfg.sysctl_tuning.ip_forward {
|
||||||
|
desired.insert("net.ipv4.ip_forward".to_string(), "1".to_string());
|
||||||
|
}
|
||||||
|
desired.insert(
|
||||||
|
"net.core.rmem_max".to_string(),
|
||||||
|
cfg.sysctl_tuning.rmem_max.to_string(),
|
||||||
|
);
|
||||||
|
desired.insert(
|
||||||
|
"net.core.wmem_max".to_string(),
|
||||||
|
cfg.sysctl_tuning.wmem_max.to_string(),
|
||||||
|
);
|
||||||
|
if cfg.sysctl_tuning.netdev_backlog_enabled {
|
||||||
|
desired.insert(
|
||||||
|
"net.core.netdev_max_backlog".to_string(),
|
||||||
|
cfg.sysctl_tuning.netdev_max_backlog.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if cfg.sysctl_tuning.udp_rmem_min_enabled {
|
||||||
|
desired.insert(
|
||||||
|
"net.ipv4.udp_rmem_min".to_string(),
|
||||||
|
cfg.sysctl_tuning.udp_rmem_min.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bbr_active == Some(true) {
|
||||||
|
desired.insert(
|
||||||
|
"net.ipv4.tcp_congestion_control".to_string(),
|
||||||
|
"bbr".to_string(),
|
||||||
|
);
|
||||||
|
let qdisc = cfg.sysctl_tuning.default_qdisc.trim().to_ascii_lowercase();
|
||||||
|
if qdisc != "none" {
|
||||||
|
desired.insert("net.core.default_qdisc".to_string(), qdisc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let persisted = render_persisted(&desired);
|
||||||
|
Ok(RenderPlan {
|
||||||
|
desired,
|
||||||
|
persisted,
|
||||||
|
bbr_available: Some(bbr_avail),
|
||||||
|
bbr_active,
|
||||||
|
warnings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bbr_available() -> anyhow::Result<bool> {
|
||||||
|
let v = read_sysctl("net.ipv4.tcp_available_congestion_control")?;
|
||||||
|
Ok(v.split_whitespace().any(|p| p == "bbr"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_persisted(desired: &BTreeMap<String, String>) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str(MANAGED_MARKER);
|
||||||
|
out.push('\n');
|
||||||
|
out.push_str("# Source: /etc/awg2-harness/config.toml\n");
|
||||||
|
out.push_str("# Do not edit this file manually; use [sysctl_tuning].\n\n");
|
||||||
|
for (k, v) in desired {
|
||||||
|
out.push_str(&format!("{k} = {v}\n"));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sysctl(key: &str) -> anyhow::Result<String> {
|
||||||
|
let out = Command::new("sysctl").args(["-n", key]).output()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"sysctl -n {key} failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_sysctl(key: &str, value: &str) -> anyhow::Result<()> {
|
||||||
|
let out = Command::new("sysctl")
|
||||||
|
.args(["-w", &format!("{key}={value}")])
|
||||||
|
.output()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"sysctl -w {key}={value} failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize(v: &str) -> String {
|
||||||
|
v.lines().map(str::trim_end).collect::<Vec<_>>().join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_atomic(path: &str, content: &str) -> anyhow::Result<()> {
|
||||||
|
let p = Path::new(path);
|
||||||
|
let dir = p
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no parent directory for path: {path}"))?;
|
||||||
|
let tmp_path = format!("{path}.tmp");
|
||||||
|
|
||||||
|
let mut f = fs::File::create(&tmp_path)?;
|
||||||
|
f.write_all(content.as_bytes())?;
|
||||||
|
f.sync_all()?;
|
||||||
|
drop(f);
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o644))?;
|
||||||
|
|
||||||
|
fs::rename(&tmp_path, path)?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let d = fs::File::open(dir)?;
|
||||||
|
d.sync_all()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config::{
|
||||||
|
AmneziaConfig, AntiTorrentConfig, HostFirewallConfig, RebootConfig, SwapConfig,
|
||||||
|
TrafficConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn cfg() -> Config {
|
||||||
|
Config {
|
||||||
|
amnezia: AmneziaConfig {
|
||||||
|
container_names: vec!["x".into()],
|
||||||
|
interface_candidates: vec!["awg0".into()],
|
||||||
|
interface_patterns: vec!["awg*".into()],
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
},
|
||||||
|
traffic: TrafficConfig {
|
||||||
|
enabled: true,
|
||||||
|
peer_limit: "70mbit".into(),
|
||||||
|
root_rate: "1000mbit".into(),
|
||||||
|
reconcile_interval_secs: 15,
|
||||||
|
container_ready_timeout_secs: 30,
|
||||||
|
class_start_id: 10,
|
||||||
|
ifb_name: "ifb0".into(),
|
||||||
|
},
|
||||||
|
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
|
||||||
|
host_firewall: HostFirewallConfig::default(),
|
||||||
|
anti_torrent: AntiTorrentConfig {
|
||||||
|
enabled: false,
|
||||||
|
max_connections_per_peer: 100,
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
apply_in_amnezia_netns: false,
|
||||||
|
},
|
||||||
|
reboot: RebootConfig {
|
||||||
|
enabled: false,
|
||||||
|
window_start_utc: "01:00".into(),
|
||||||
|
window_end_utc: "02:00".into(),
|
||||||
|
},
|
||||||
|
swap: SwapConfig {
|
||||||
|
enabled: false,
|
||||||
|
path: "/swapfile".into(),
|
||||||
|
size_mb: 2048,
|
||||||
|
priority: 1,
|
||||||
|
swappiness: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn managed_marker_is_present() {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert("net.core.rmem_max".to_string(), "1".to_string());
|
||||||
|
let s = render_persisted(&map);
|
||||||
|
assert!(s.starts_with(MANAGED_MARKER));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_udp_wmem_min_in_rendered_output() {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert("net.core.rmem_max".to_string(), "1".to_string());
|
||||||
|
let s = render_persisted(&map);
|
||||||
|
assert!(!s.contains("udp_wmem_min"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_refuses_unmanaged_file() {
|
||||||
|
let mut c = cfg();
|
||||||
|
c.sysctl_tuning.enabled = false;
|
||||||
|
let e = remove_managed_sysctl_file_if_disabled(&c);
|
||||||
|
if Path::new(SYSCTL_CONF_PATH).exists() {
|
||||||
|
assert!(e.is_ok() || e.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user