From 1dfac19c63781cfe473c8d9cb2d21866d1aa4576 Mon Sep 17 00:00:00 2001 From: LUNO Date: Tue, 18 Aug 2026 21:18:19 +0000 Subject: [PATCH 1/5] refactor(network): retire forwarding and RA management --- README.md | 13 +- modules/network-optimize.sh | 157 ++++------------- tests/test-network-optimize-entry.sh | 248 +++++++++++++++++---------- tests/test-network-optimize.sh | 6 +- 4 files changed, 204 insertions(+), 220 deletions(-) diff --git a/README.md b/README.md index 4f3dcc0..e3295ff 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/LucaLin233/Linux/main/linux_ | ---: | --- | --- | --- | | 1 | `system-optimize.sh` | Zram、系统 sysctl、journald、THP、时区和 Chrony | 为 headless VPS 设置 Panic 恢复、日志上限和低干扰 THP 策略;Ubuntu 可能安装内核模块、固件与 CPU 微码 | | 2 | `system-customize.sh` | 动态 MOTD、中文 Locale、可选 XanMod | 可能修改 Locale、欢迎信息和内核 | -| 3 | `network-optimize.sh` | BBR、fq、按需禁用 ECN、动态 TCP/UDP 缓冲区、IPv4/IPv6 转发 | 交互时先询问是否测速,拒绝后手填带宽;单方向 40 GB 或合计 85 GB 时提前停止;保留云平台 IPv6 RA | +| 3 | `network-optimize.sh` | BBR、fq、按需禁用 ECN、动态 TCP/UDP 缓冲区 | 交互时先询问是否测速,拒绝后手填带宽;单方向 40 GB 或合计 85 GB 时提前停止;只读显示 forwarding/RA,不代管转发 | | 4 | `zsh-setup.sh` | Zsh、Oh My Zsh、Powerlevel10k 和插件 | 备份后重写 root 的 `.zshrc`,可修改默认 Shell | | 5 | `mise-setup.sh` | Mise、Python、Node.js 和依赖迁移 | 配置 Shell 集成及每周 Mise 自动更新 | | 6 | `tools-setup.sh` | NextTrace、Speedtest、htop、jq、tree 等 | 可能添加 NextTrace 第三方 APT 源 | @@ -223,6 +223,8 @@ bash <(curl -fsSL "$RAW_BASE/network-optimize.sh") help 默认不写入 `net.ipv4.tcp_ecn`;只有显式传入 `--disable-ecn` 时才持久写入 `0`。 +网络模块不再持久或运行时修改 IPv4/IPv6 forwarding 与 RA;`status` 仅只读显示这些值。升级时,旧受管配置中的相关行会从新配置自然移除,但当前运行值保持不变,直到重启、网络管理器或其他 sysctl 配置接管。Docker、NAT、VPN、Tailscale exit node、WireGuard 路由和 IPv6 路由环境必须由对应组件显式配置转发。`restore previous` 仍可显式恢复含旧 forwarding/RA 行的上次配置;模块不会自动恢复旧运行行为。 + 网络模块默认面向同时承载 TCP、UDP 与 Docker 流量的代理节点:连接队列使用 `somaxconn=65535`、`tcp_max_syn_backlog=16384`,TCP 缓冲起点固定为 2 MiB,长流继续依赖 autotuning。动态最大值按 `2 × BDP + 2 MiB` @@ -428,17 +430,16 @@ sudo bash <(curl -fsSL https://raw.githubusercontent.com/LucaLin233/Linux/main/t | 仅安装 XanMod | `xanmod-install.sh` | 同时让多个脚本反复管理内核源 | `network-optimize.sh` 与 `traffic-shape.sh` 职责不同,可以配合:前者管理 BBR、缓冲区、默认 -`fq` 和按内核能力启用的 TCP 参数。IPv4/IPv6 forwarding 与 RA 管理不是 tcpfit 移植内容, -而是为当前 Docker/VPS 环境保留的下游扩展:`all`/`default` 保留 `accept_ra=1`,实际 IPv6 -默认出口使用 `accept_ra=2`,现有 Docker、veth、CNI 与隧道接口的 RA 在运行时设为 `0` 并 -纳入回滚快照。后者在确实检测到 policer 后才使用 HTB 控制聚合出口速率,并保留 fq 叶子 pacing。 +`fq` 和按内核能力启用的 TCP 参数,只读显示 IPv4/IPv6 forwarding 与 RA。转发应由 Docker、 +NAT、VPN、路由器、网络管理器或独立 sysctl 配置负责。后者在确实检测到 policer 后才使用 HTB +控制聚合出口速率,并保留 fq 叶子 pacing。 ## 高风险提醒 - **SSH**:修改端口或认证前,先放行云安全组/防火墙,并保持当前会话直到新连接验证成功; - **自动更新**:系统或内核更新后可能自动重启; - **系统优化**:首次写入 journald 限额时会重启 `systemd-journald`,并设置 Kernel Panic 30 秒后重启; -- **IPv6 转发**:网络模块会使用 `accept_ra=2` 保留云平台 RA,但仍需确认云安全组和主机防火墙允许预期的 IPv6 流量; +- **转发职责**:网络模块不代管 forwarding/RA;重启后 Docker、NAT、VPN、Tailscale exit node、WireGuard 或 IPv6 路由所需转发必须由对应组件显式启用; - **网络测速**:`network-optimize` 和 tcshape 都可能产生大量流量; - **qdisc**:不要叠加多个整形工具;tcshape 遇到高级或未知 qdisc 会拒绝覆盖; - **内核**:安装新内核前确认磁盘空间、架构和可用的旧内核; diff --git a/modules/network-optimize.sh b/modules/network-optimize.sh index 22779ec..5a52403 100644 --- a/modules/network-optimize.sh +++ b/modules/network-optimize.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash -# linux-setup:name=网络优化(BBR、fq、双栈转发) +# linux-setup:name=网络优化(BBR、fq、TCP 缓冲区) # linux-setup:order=30 # linux-setup:depends= # linux-setup:enabled=true # 网络优化模块 -# TCP 调优仅覆盖 IPv4;IPv6 forwarding/RA 仅维护转发正确性。 +# TCP 调优仅覆盖 IPv4;forwarding/RA 仅在 status 中只读显示。 # 基础调优移植或参考 Kylin010/tcpfit v0.5.6(MIT,提交 67c0bdfb35dd98e86982600298237b6ecc08ebe4)。 -# 事务备份、交互、验证及 Docker/VPS forwarding/RA 为本仓库下游扩展。 -# 功能:配置 BBR、fq、TCP 缓冲区及 RA 安全的双栈转发;主动探测必须明确选择。 +# 事务备份、交互与验证为本仓库下游扩展。 +# 功能:配置 BBR、fq 与 TCP 缓冲区;主动探测必须明确选择。 # # 用法: # bash network-optimize.sh [install] [选项] # 自动计算并应用 @@ -102,7 +102,6 @@ readonly INITCWND_AUTO_UPLOAD_LIMIT_MBPS=100 readonly TCP_BUFFER_DEFAULT_BYTES=$((2 * 1024 * 1024)) readonly IPERF_DEADLINE_GRACE_SECONDS=15 readonly IPERF_KILL_AFTER_SECONDS=3 -readonly IPV6_CONF_ROOT="${NETWORK_OPTIMIZE_IPV6_CONF_ROOT:-/proc/sys/net/ipv6/conf}" # 参数与计算结果。命令行参数优先于自动探测。 COMMAND="install" @@ -2520,101 +2519,6 @@ EOF fi } -is_virtual_ipv6_ra_interface() { - case "$1" in - docker*|br-*|veth*|virbr*|cni*|flannel*|kube*|lxc*|podman*|tailscale*|tun*|tap*|wg*) - return 0 - ;; - *) - return 1 - ;; - esac -} - -capture_virtual_ipv6_ra_values() { - local output_file="$1" - local accept_ra_path - local interface - - for accept_ra_path in "$IPV6_CONF_ROOT"/*/accept_ra; do - [[ -e "$accept_ra_path" ]] || continue - interface=$(basename "$(dirname "$accept_ra_path")") - [[ "$interface" =~ ^[A-Za-z0-9_-]+$ ]] || continue - is_virtual_ipv6_ra_interface "$interface" || continue - printf 'net.ipv6.conf.%s.accept_ra=%s\n' \ - "$interface" "$(cat "$accept_ra_path")" >> "$output_file" - done -} - -normalize_virtual_ipv6_ra() { - local runtime_backup="$1" - local key - local value - local interface - local normalized=0 - - while IFS='=' read -r key value; do - [[ "$key" =~ ^net\.ipv6\.conf\.([A-Za-z0-9_-]+)\.accept_ra$ ]] || continue - interface="${BASH_REMATCH[1]}" - is_virtual_ipv6_ra_interface "$interface" || continue - [[ -e "$IPV6_CONF_ROOT/$interface/accept_ra" ]] || continue - - if ! sysctl -w "$key=0" >/dev/null 2>&1; then - error "无法禁用虚拟接口 IPv6 RA: $interface" - return 1 - fi - if [[ "$(sysctl -n "$key" 2>/dev/null || true)" != "0" ]]; then - error "虚拟接口 IPv6 RA 验证失败: $interface" - return 1 - fi - ((normalized += 1)) - done < "$runtime_backup" - - if (( normalized > 0 )); then - info "已禁用 $normalized 个容器或隧道虚拟接口的 IPv6 RA" - fi -} - -detect_ipv6_default_iface() { - local interface - - interface=$(ip -6 route get 2606:4700:4700::1111 2>/dev/null | route_value_after dev) - if [[ -z "$interface" ]]; then - interface=$(ip -6 route show default 2>/dev/null | route_value_after dev) - fi - [[ "$interface" =~ ^[A-Za-z0-9_-]+$ ]] || return 1 - case "$interface" in all|default|lo) return 1 ;; esac - is_virtual_ipv6_ra_interface "$interface" && return 1 - [[ -e "$IPV6_CONF_ROOT/$interface/accept_ra" ]] || return 1 - printf '%s\n' "$interface" -} - -append_ipv6_forwarding_config() { - local target_file="$1" - local interface="" - - [[ -e "$IPV6_CONF_ROOT/all/forwarding" ]] || return 0 - [[ -e "$IPV6_CONF_ROOT/all/accept_ra" ]] || return 0 - - # forwarding=1 时,accept_ra=1 不再接收 RA;只有实际 IPv6 默认出口需要 2。 - cat >> "$target_file" <<'EOF' - -# IPv6 转发;仅实际默认出口在转发模式下继续接收 Router Advertisement -net.ipv6.conf.all.accept_ra = 1 -net.ipv6.conf.default.accept_ra = 1 -EOF - - interface=$(detect_ipv6_default_iface || true) - if [[ -n "$interface" ]]; then - echo "net.ipv6.conf.${interface}.accept_ra = 2" >> "$target_file" - fi - - cat >> "$target_file" <<'EOF' -net.ipv6.conf.default.forwarding = 1 -net.ipv6.conf.all.forwarding = 1 -EOF -} - create_network_config() { local target_file="$1" local enable_bbr="$2" @@ -2634,31 +2538,28 @@ ${BANDWIDTH_PROBE_NOTE:+# 带宽测量环境: $BANDWIDTH_PROBE_NOTE} # initcwnd 模式: $INITCWND_MODE # initcwnd 策略: $INITCWND_POLICY # 缓冲区依据: $CALCULATION_REASON -# 适用于 Debian 13 代理、转发及中高延迟公网 VPS。 +# 适用于 Debian 13 代理及中高延迟公网 VPS。 -# 1. IPv4 转发 -net.ipv4.ip_forward = 1 - -# 2. 队列调度 +# 1. 队列调度 net.core.default_qdisc = fq -# 3. TCP Fast Open +# 2. TCP Fast Open net.ipv4.tcp_fastopen = 3 -# 4. 连接与接收队列 +# 3. 连接与接收队列 net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 16384 net.core.netdev_max_backlog = 16384 net.core.optmem_max = 65536 -# 5. TCP/UDP 缓冲区;TCP 初始默认固定 2 MiB,长流继续依赖 autotuning +# 4. TCP/UDP 缓冲区;TCP 初始默认固定 2 MiB,长流继续依赖 autotuning # core default 与全局 tcp_mem 保留内核或发行版值,避免放大所有 socket 的内存承诺 net.core.rmem_max = $RMEM_MAX_BYTES net.core.wmem_max = $WMEM_MAX_BYTES net.ipv4.tcp_rmem = 4096 $RMEM_DEFAULT_BYTES $RMEM_MAX_BYTES net.ipv4.tcp_wmem = 4096 $WMEM_DEFAULT_BYTES $WMEM_MAX_BYTES -# 6. 长连接、连接回收与复杂路径 +# 5. 长连接、连接回收与复杂路径 net.ipv4.tcp_fin_timeout = 30 net.ipv4.tcp_slow_start_after_idle = 0 net.ipv4.tcp_keepalive_time = 600 @@ -2674,7 +2575,6 @@ EOF fi append_supported_tcp_settings "$target_file" - append_ipv6_forwarding_config "$target_file" if [[ "$enable_bbr" == "true" ]]; then cat >> "$target_file" <<'EOF' @@ -2693,6 +2593,19 @@ EOF chmod 644 "$target_file" } +managed_config_has_retired_forwarding() { + [[ -f "$NETWORK_CONF" ]] || return 1 + grep -Fq '# 由 network-optimize.sh 自动生成。' "$NETWORK_CONF" || return 1 + grep -Eq '^[[:space:]]*net\.(ipv4\.ip_forward|ipv6\.conf\.[^.]+\.(accept_ra|forwarding))[[:space:]]*=' \ + "$NETWORK_CONF" +} + +warn_retired_forwarding_management() { + managed_config_has_retired_forwarding || return 0 + warn "已停止持久管理 IPv4/IPv6 forwarding 与 RA;当前运行值保持不变。" + warn "后续值由系统、网络管理器或其他 sysctl 配置决定。" +} + prepare_legacy_backup_state() { # 旧版没有 absent 标记。检测到脚本生成的现有文件时,无法证明它在 # 第一次运行前是否存在,因此标记 unknown,绝不把当前受管状态冒充初始状态。 @@ -2929,11 +2842,18 @@ install_optimization() { return 1 } - if ! capture_runtime_values "$temp_config" "$runtime_backup"; then + if [[ -f "$NETWORK_CONF" ]] && + grep -Fq '# 由 network-optimize.sh 自动生成。' "$NETWORK_CONF"; then + capture_runtime_values_from_files \ + "$runtime_backup" "$temp_config" "$NETWORK_CONF" || { + rm -f "$temp_config" "$runtime_backup" + return 1 + } + elif ! capture_runtime_values "$temp_config" "$runtime_backup"; then rm -f "$temp_config" "$runtime_backup" return 1 fi - capture_virtual_ipv6_ra_values "$runtime_backup" + warn_retired_forwarding_management prepare_legacy_backup_state install -d -m 0755 "$NETWORK_OPTIMIZE_STATE_DIR" @@ -2959,11 +2879,6 @@ install_optimization() { rm -f "$temp_config" "$runtime_backup" return 1 fi - if ! normalize_virtual_ipv6_ra "$runtime_backup"; then - restore_runtime_values "$runtime_backup" - rm -f "$temp_config" "$runtime_backup" - return 1 - fi if ! verify_network_config "$temp_config"; then restore_runtime_values "$runtime_backup" rm -f "$temp_config" "$runtime_backup" @@ -3002,7 +2917,7 @@ install_optimization() { warn "无法写入 BBR 模块开机加载配置;当前运行不受影响" fi else - warn "BBR 未启用;其余网络与转发参数已正常应用" + warn "BBR 未启用;其余网络参数已正常应用" fi show_install_summary "$health_before" "$bbr_enabled" @@ -3339,7 +3254,7 @@ show_status() { print_sysctl_rows "TCP Fast Open|net.ipv4.tcp_fastopen|未知" echo - echo "转发与兼容性:" + echo "转发与 RA(只读,本模块不管理):" print_sysctl_rows \ "IPv4 转发|net.ipv4.ip_forward|未知" \ "rp_filter(all)|net.ipv4.conf.all.rp_filter|未知" \ @@ -3445,7 +3360,7 @@ verify 选项: - 只有 --probe 或交互确认后才主动探测并安装缺失依赖 - 探测失败时,交互终端转为手填;非交互终端在写配置、sysctl 或路由前失败 - 自动探测仅测量 IPv4 公网带宽,使用公共 iperf3 与 Cloudflare - - TCP 调优仅覆盖 IPv4;Docker/VPS forwarding/RA 是本仓库保留的下游扩展 + - TCP 调优仅覆盖 IPv4;forwarding 与 RA 只读显示,不由本模块配置 - 自动测速在单方向 40 GB 或合计 85 GB 时提前停止,硬上限仍为 45/90 GB - 流量按实际 IPv4 测速目标的路由接口分别计量并汇总,接口计数包含后台流量 - 默认不持久管理 ECN;只在传入 --disable-ecn 时写入 tcp_ecn=0 @@ -3462,7 +3377,7 @@ verify 选项: 实现来源: - 公共 iperf3、带宽探测、BDP/memory cap 与 initcwnd 策略移植或参考 tcpfit v0.5.6 - - 参数交互、事务备份/恢复、verify 和 Docker/VPS forwarding/RA 为本仓库下游实现 + - 参数交互、事务备份/恢复和 verify 为本仓库下游实现 EOF } diff --git a/tests/test-network-optimize-entry.sh b/tests/test-network-optimize-entry.sh index 4bd7746..30bafbe 100755 --- a/tests/test-network-optimize-entry.sh +++ b/tests/test-network-optimize-entry.sh @@ -7,7 +7,6 @@ trap 'rm -rf "$TEMP_DIR"' EXIT export NETWORK_OPTIMIZE_STATE_DIR="$TEMP_DIR/state" export NETWORK_OPTIMIZE_CONF="$TEMP_DIR/etc/sysctl.d/99-network-optimize.conf" export NETWORK_OPTIMIZE_BBR_MODULES_FILE="$TEMP_DIR/etc/modules-load.d/network-optimize-bbr.conf" -export NETWORK_OPTIMIZE_IPV6_CONF_ROOT="$TEMP_DIR/proc/sys/net/ipv6/conf" export NETWORK_OPTIMIZE_INITCWND_HOOK="$TEMP_DIR/networkd-dispatcher/routable.d/50-network-optimize-initcwnd" # shellcheck source=../modules/network-optimize.sh @@ -365,118 +364,171 @@ for retired_key in \ fail "generated config still owns $retired_key" fi done -assert_eq '1' "$(read_config_value "$generated_config" net.ipv4.ip_forward)" \ - "generated config preserves IPv4 forwarding" -assert_eq '4096 2097152 39845888' \ - "$(read_config_value "$generated_config" net.ipv4.tcp_rmem)" \ - "generated config uses fixed 2 MiB receive default" -assert_eq '4096 2097152 20971520' \ - "$(read_config_value "$generated_config" net.ipv4.tcp_wmem)" \ - "generated config uses fixed 2 MiB send default" -printf 'PASS: default generated config leaves retired global parameters unmanaged\n' - -ECN_DISABLED=true -create_network_config "$generated_config" false -assert_eq '0' "$(read_config_value "$generated_config" net.ipv4.tcp_ecn)" \ - "explicit disable writes tcp_ecn=0" +if grep -Eq '^[[:space:]]*net[.](ipv4[.]ip_forward|ipv6[.]conf[.][^.]+[.](accept_ra|forwarding))[[:space:]]*=' \ + "$generated_config"; then + fail "generated config still owns forwarding or RA" +fi +printf 'PASS: generated config omits forwarding and RA assignments\n' -mkdir -p "$IPV6_CONF_ROOT"/{all,default,eth0,eth1,br0,br-docker,veth123} -touch "$IPV6_CONF_ROOT/all/forwarding" "$IPV6_CONF_ROOT/all/accept_ra" -for ra_iface in default eth0 eth1 br0 veth123; do - touch "$IPV6_CONF_ROOT/$ra_iface/accept_ra" -done -IPV6_ROUTE_GET_IFACE=eth1 -IPV6_DEFAULT_IFACE=eth0 -ip() { - case "$1 $2 $3" in - '-6 route get') - [[ -n "$IPV6_ROUTE_GET_IFACE" ]] && - printf '2606:4700:4700::1111 via 2001:db8::1 dev %s src 2001:db8::2\n' \ - "$IPV6_ROUTE_GET_IFACE" - ;; - '-6 route show') - [[ -n "$IPV6_DEFAULT_IFACE" ]] && - printf 'default via 2001:db8::1 dev %s proto ra metric 100\n' \ - "$IPV6_DEFAULT_IFACE" - ;; - *) return 1 ;; - esac -} -ra_config="$TEMP_DIR/ra.conf" -: > "$ra_config" -append_ipv6_forwarding_config "$ra_config" -for expected_ra_line in \ +old_managed_config="$TEMP_DIR/old-managed-network.conf" +mkdir -p "$(dirname "$NETWORK_CONF")" +printf '%s\n' \ + '# 由 network-optimize.sh 自动生成。' \ + '# legacy bytes must remain unchanged' \ + 'net.ipv4.ip_forward = 1' \ 'net.ipv6.conf.all.accept_ra = 1' \ 'net.ipv6.conf.default.accept_ra = 1' \ + 'net.ipv6.conf.eth0.accept_ra = 2' \ 'net.ipv6.conf.default.forwarding = 1' \ - 'net.ipv6.conf.all.forwarding = 1'; do - grep -Fq "$expected_ra_line" "$ra_config" || - fail "forwarding/RA config misses $expected_ra_line" + 'net.ipv6.conf.all.forwarding = 1' > "$NETWORK_CONF" +cp "$NETWORK_CONF" "$old_managed_config" +migration_warning=$(warn_retired_forwarding_management) +for warning_line in \ + '已停止持久管理 IPv4/IPv6 forwarding 与 RA;当前运行值保持不变。' \ + '后续值由系统、网络管理器或其他 sysctl 配置决定。'; do + assert_eq 1 "$(grep -Fc "$warning_line" <<< "$migration_warning")" \ + "managed legacy config prints migration warning once" done -grep -Fq 'net.ipv6.conf.eth1.accept_ra = 2' "$ra_config" || - fail "actual IPv6 route interface did not receive accept_ra=2" -for excluded_iface in eth0 br0 veth123; do - ! grep -Fq "net.ipv6.conf.${excluded_iface}.accept_ra = 2" "$ra_config" || - fail "non-egress interface $excluded_iface incorrectly received accept_ra=2" -done -printf 'PASS: multiple NICs select only actual IPv6 default egress and exclude bridge/veth\n' - -IPV6_ROUTE_GET_IFACE="" -IPV6_DEFAULT_IFACE=eth0 -: > "$ra_config" -append_ipv6_forwarding_config "$ra_config" -grep -Fq 'net.ipv6.conf.eth0.accept_ra = 2' "$ra_config" || - fail "IPv6 default-route fallback did not select eth0" -printf 'PASS: IPv6 default-route fallback selects one egress\n' - -IPV6_ROUTE_GET_IFACE="" -IPV6_DEFAULT_IFACE="" -: > "$ra_config" -append_ipv6_forwarding_config "$ra_config" -! grep -Eq '^net[.]ipv6[.]conf[.].+[.]accept_ra = 2$' "$ra_config" || - fail "accept_ra=2 was generated without an IPv6 default route" -printf 'PASS: no IPv6 default route generates no per-interface accept_ra=2\n' -unset -f ip - -printf '%s\n' 1 > "$IPV6_CONF_ROOT/br-docker/accept_ra" -printf '%s\n' 2 > "$IPV6_CONF_ROOT/veth123/accept_ra" -virtual_ra_backup="$TEMP_DIR/virtual-ra.runtime" -: > "$virtual_ra_backup" -capture_virtual_ipv6_ra_values "$virtual_ra_backup" -assert_eq $'net.ipv6.conf.br-docker.accept_ra=1\nnet.ipv6.conf.veth123.accept_ra=2' \ - "$(sort "$virtual_ra_backup")" "capture virtual interface RA values" -RA_BR_DOCKER=1 -RA_VETH123=2 +printf '%s\n' 'net.ipv4.ip_forward = 1' > "$NETWORK_CONF" +assert_eq '' "$(warn_retired_forwarding_management)" \ + "unmarked config does not print forwarding migration warning" +cp "$old_managed_config" "$NETWORK_CONF" + +IP_FORWARD_RUNTIME=0 +IPV6_ALL_RA_RUNTIME=2 +IPV6_DEFAULT_RA_RUNTIME=2 +IPV6_ETH0_RA_RUNTIME=1 +IPV6_DEFAULT_FORWARD_RUNTIME=0 +IPV6_ALL_FORWARD_RUNTIME=0 +RETIRED_SYSCTL_WRITES=0 sysctl() { - local key + local key value case "$1" in -n) case "$2" in - net.ipv6.conf.br-docker.accept_ra) printf '%s\n' "$RA_BR_DOCKER" ;; - net.ipv6.conf.veth123.accept_ra) printf '%s\n' "$RA_VETH123" ;; - *) return 1 ;; + net.ipv4.ip_forward) printf '%s\n' "$IP_FORWARD_RUNTIME" ;; + net.ipv6.conf.all.accept_ra) printf '%s\n' "$IPV6_ALL_RA_RUNTIME" ;; + net.ipv6.conf.default.accept_ra) printf '%s\n' "$IPV6_DEFAULT_RA_RUNTIME" ;; + net.ipv6.conf.eth0.accept_ra) printf '%s\n' "$IPV6_ETH0_RA_RUNTIME" ;; + net.ipv6.conf.default.forwarding) printf '%s\n' "$IPV6_DEFAULT_FORWARD_RUNTIME" ;; + net.ipv6.conf.all.forwarding) printf '%s\n' "$IPV6_ALL_FORWARD_RUNTIME" ;; + *) printf '%s\n' 0 ;; esac ;; + -p) + while IFS='=' read -r key value; do + key="${key//[[:space:]]/}" + value="${value//[[:space:]]/}" + [[ -n "$key" && "$key" != \#* ]] || continue + case "$key" in + net.ipv4.ip_forward) + IP_FORWARD_RUNTIME="$value" + ((RETIRED_SYSCTL_WRITES += 1)) + ;; + net.ipv6.conf.all.accept_ra) + IPV6_ALL_RA_RUNTIME="$value" + ((RETIRED_SYSCTL_WRITES += 1)) + ;; + net.ipv6.conf.default.accept_ra) + IPV6_DEFAULT_RA_RUNTIME="$value" + ((RETIRED_SYSCTL_WRITES += 1)) + ;; + net.ipv6.conf.eth0.accept_ra) + IPV6_ETH0_RA_RUNTIME="$value" + ((RETIRED_SYSCTL_WRITES += 1)) + ;; + net.ipv6.conf.default.forwarding) + IPV6_DEFAULT_FORWARD_RUNTIME="$value" + ((RETIRED_SYSCTL_WRITES += 1)) + ;; + net.ipv6.conf.all.forwarding) + IPV6_ALL_FORWARD_RUNTIME="$value" + ((RETIRED_SYSCTL_WRITES += 1)) + ;; + esac + done < "$2" + ;; -w) key="${2%%=*}" + value="${2#*=}" case "$key" in - net.ipv6.conf.br-docker.accept_ra) RA_BR_DOCKER="${2#*=}" ;; - net.ipv6.conf.veth123.accept_ra) RA_VETH123="${2#*=}" ;; - *) return 1 ;; + net.ipv4.ip_forward) IP_FORWARD_RUNTIME="$value" ;; + net.ipv6.conf.all.accept_ra) IPV6_ALL_RA_RUNTIME="$value" ;; + net.ipv6.conf.default.accept_ra) IPV6_DEFAULT_RA_RUNTIME="$value" ;; + net.ipv6.conf.eth0.accept_ra) IPV6_ETH0_RA_RUNTIME="$value" ;; + net.ipv6.conf.default.forwarding) IPV6_DEFAULT_FORWARD_RUNTIME="$value" ;; + net.ipv6.conf.all.forwarding) IPV6_ALL_FORWARD_RUNTIME="$value" ;; + *) : ;; esac + ((RETIRED_SYSCTL_WRITES += 1)) ;; *) return 1 ;; esac } -normalize_virtual_ipv6_ra "$virtual_ra_backup" >/dev/null -assert_eq 0 "$RA_BR_DOCKER" "normalize bridge RA" -assert_eq 0 "$RA_VETH123" "normalize veth RA" -restore_runtime_values "$virtual_ra_backup" -assert_eq 1 "$RA_BR_DOCKER" "rollback restores bridge RA" -assert_eq 2 "$RA_VETH123" "rollback restores veth RA" +upgrade_runtime="$TEMP_DIR/upgrade-previous.runtime" +capture_runtime_values_from_files \ + "$upgrade_runtime" "$generated_config" "$old_managed_config" +for expected_runtime in \ + 'net.ipv4.ip_forward=0' \ + 'net.ipv6.conf.all.accept_ra=2' \ + 'net.ipv6.conf.default.accept_ra=2' \ + 'net.ipv6.conf.eth0.accept_ra=1' \ + 'net.ipv6.conf.default.forwarding=0' \ + 'net.ipv6.conf.all.forwarding=0'; do + grep -Fxq "$expected_runtime" "$upgrade_runtime" || + fail "upgrade runtime snapshot dropped $expected_runtime" +done +printf 'PASS: upgrade runtime snapshot retains retired keys\n' + +rm -f "$NETWORK_PREVIOUS_BACKUP" "$NETWORK_PREVIOUS_ABSENT" +backup_managed_file \ + "$NETWORK_CONF" "$NETWORK_INITIAL_BACKUP" "$NETWORK_PREVIOUS_BACKUP" \ + "$NETWORK_INITIAL_ABSENT" "$NETWORK_PREVIOUS_ABSENT" +cmp -s "$old_managed_config" "$NETWORK_PREVIOUS_BACKUP" || + fail "previous backup did not preserve legacy config byte-for-byte" +printf 'PASS: previous backup preserves legacy config byte-for-byte\n' +atomic_install_file "$generated_config" "$NETWORK_CONF" 0644 +DEBUG=1 +apply_network_config "$NETWORK_CONF" +assert_eq 0 "$RETIRED_SYSCTL_WRITES" \ + "upgraded config applies no forwarding or RA runtime writes" +assert_eq '0|2|2|1|0|0' \ + "$IP_FORWARD_RUNTIME|$IPV6_ALL_RA_RUNTIME|$IPV6_DEFAULT_RA_RUNTIME|$IPV6_ETH0_RA_RUNTIME|$IPV6_DEFAULT_FORWARD_RUNTIME|$IPV6_ALL_FORWARD_RUNTIME" \ + "upgrade preserves forwarding and RA runtime values" + +IP_FORWARD_RUNTIME=9 +IPV6_ALL_RA_RUNTIME=9 +IPV6_DEFAULT_RA_RUNTIME=9 +IPV6_ETH0_RA_RUNTIME=9 +IPV6_DEFAULT_FORWARD_RUNTIME=9 +IPV6_ALL_FORWARD_RUNTIME=9 +apply_network_config "$NETWORK_PREVIOUS_BACKUP" +restore_managed_file \ + "$NETWORK_CONF" "$NETWORK_PREVIOUS_BACKUP" "$NETWORK_PREVIOUS_ABSENT" +apply_runtime_values_strict "$upgrade_runtime" +cmp -s "$old_managed_config" "$NETWORK_CONF" || + fail "restore previous did not restore legacy config byte-for-byte" +assert_eq '0|2|2|1|0|0' \ + "$IP_FORWARD_RUNTIME|$IPV6_ALL_RA_RUNTIME|$IPV6_DEFAULT_RA_RUNTIME|$IPV6_ETH0_RA_RUNTIME|$IPV6_DEFAULT_FORWARD_RUNTIME|$IPV6_ALL_FORWARD_RUNTIME" \ + "restore previous explicitly restores legacy forwarding and RA runtime values" +printf 'PASS: restore previous explicitly restores retired forwarding and RA state\n' +unset DEBUG unset -f sysctl +assert_eq '4096 2097152 39845888' \ + "$(read_config_value "$generated_config" net.ipv4.tcp_rmem)" \ + "generated config uses fixed 2 MiB receive default" +assert_eq '4096 2097152 20971520' \ + "$(read_config_value "$generated_config" net.ipv4.tcp_wmem)" \ + "generated config uses fixed 2 MiB send default" +printf 'PASS: default generated config leaves retired global parameters unmanaged\n' + +ECN_DISABLED=true +create_network_config "$generated_config" false +assert_eq '0' "$(read_config_value "$generated_config" net.ipv4.tcp_ecn)" \ + "explicit disable writes tcp_ecn=0" + SYSCTL_TCP_ECN='1' SYSCTL_CONNTRACK_MAX='262144' SYSCTL_WRITE_FAIL_KEY='' @@ -792,4 +844,18 @@ if grep -Eq 'LEGACY_(KERNEL|SYSCTL)|RETIRED_SYSCTL_KEYS|migrate_legacy_(kernel|s fi printf 'PASS: removed network migration and mode framework is absent\n' +if grep -Eq 'IPV6_CONF_ROOT|is_virtual_ipv6_ra_interface|capture_virtual_ipv6_ra_values|normalize_virtual_ipv6_ra|detect_ipv6_default_iface|append_ipv6_forwarding_config' \ + "$ROOT_DIR/modules/network-optimize.sh"; then + fail "removed forwarding or virtual RA implementation remains" +fi +if grep -Eq '^[[:space:]]*net\.(ipv4\.ip_forward|ipv6\.conf\..*\.(accept_ra|forwarding))[[:space:]]*=' \ + "$ROOT_DIR/modules/network-optimize.sh"; then + fail "network-optimize source still contains forwarding or RA config assignments" +fi +if grep -Eq 'sysctl[[:space:]]+-w.*(ip_forward|accept_ra|forwarding)' \ + "$ROOT_DIR/modules/network-optimize.sh"; then + fail "network-optimize source still writes forwarding or RA at runtime" +fi +printf 'PASS: forwarding and virtual RA management implementation is absent\n' + printf 'All network-optimize entry tests passed.\n' diff --git a/tests/test-network-optimize.sh b/tests/test-network-optimize.sh index b97b1a6..ac5813e 100755 --- a/tests/test-network-optimize.sh +++ b/tests/test-network-optimize.sh @@ -7,7 +7,6 @@ trap 'rm -rf "$TEMP_DIR"' EXIT export NETWORK_OPTIMIZE_STATE_DIR="$TEMP_DIR/state" export NETWORK_OPTIMIZE_CONF="$TEMP_DIR/etc/sysctl.d/99-network-optimize.conf" export NETWORK_OPTIMIZE_BBR_MODULES_FILE="$TEMP_DIR/etc/modules-load.d/network-optimize-bbr.conf" -export NETWORK_OPTIMIZE_IPV6_CONF_ROOT="$TEMP_DIR/proc/sys/net/ipv6/conf" export NETWORK_OPTIMIZE_INITCWND_HOOK="$TEMP_DIR/networkd-dispatcher/routable.d/50-network-optimize-initcwnd" # shellcheck source=../modules/network-optimize.sh source "$ROOT_DIR/modules/network-optimize.sh" @@ -135,7 +134,10 @@ for diagnostic_key in \ fail "status omits read-only diagnostic $diagnostic_key" done printf 'PASS: status includes read-only kernel capacity diagnostics\n' -show_status >/dev/null || fail "status fails when optional diagnostics are unavailable" +status_output=$(show_status) +grep -Fq '转发与 RA(只读,本模块不管理):' <<< "$status_output" || + fail "status does not mark forwarding and RA as unmanaged read-only values" +printf 'PASS: status labels forwarding and RA as unmanaged read-only values\n' printf 'PASS: status succeeds when optional diagnostics are unavailable\n' TUNING_MODE=probe From de38b2be3744ea39c991c33af060de375be398ad Mon Sep 17 00:00:00 2001 From: LUNO Date: Tue, 18 Aug 2026 21:25:12 +0000 Subject: [PATCH 2/5] fix(network): harden initcwnd and BBR persistence --- modules/network-optimize.sh | 82 +++++++++++++++++++++++----------- tests/test-network-optimize.sh | 66 ++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 29 deletions(-) diff --git a/modules/network-optimize.sh b/modules/network-optimize.sh index 5a52403..d45117c 100644 --- a/modules/network-optimize.sh +++ b/modules/network-optimize.sh @@ -144,6 +144,7 @@ declare -a TRAFFIC_IFACES=() declare -A TRAFFIC_RX_START_BY_IFACE=() declare -A TRAFFIC_TX_START_BY_IFACE=() declare -a IPERF_RUNNER_PIDS=() +declare -a INITCWND_ROLLBACK_FAILED_ITEMS=() PREFERRED_IPERF_PORT="" CLOUDFLARE_IPV4="" VERIFY_ASSUME_YES="false" @@ -453,12 +454,7 @@ bbr_available() { } persist_bbr_module() { - local temp_file - - temp_file=$(mktemp "${BBR_MODULES_FILE}.new.XXXXXX") || return 1 - printf '%s\n' tcp_bbr > "$temp_file" - chmod 644 "$temp_file" - mv "$temp_file" "$BBR_MODULES_FILE" + atomic_write_file "$BBR_MODULES_FILE" "tcp_bbr" 0644 || return 1 } ensure_bbr_available() { @@ -928,6 +924,14 @@ strip_route_window_fields() { printf '%s\n' "${clean[*]}" } +create_initcwnd_ownership_marker() { + atomic_install_file /dev/null "$ROUTE_OWNED_MARKER" 0600 || return 1 +} + +remove_initcwnd_ownership_marker() { + rm -f -- "$ROUTE_OWNED_MARKER" || return 1 +} + backup_default_route() { local route="" local query_status=0 @@ -1146,7 +1150,7 @@ write_initcwnd_hook() { } apply_initcwnd() { - local route clean + local route="" clean="" local owned="false" local -a route_args=() @@ -1169,7 +1173,10 @@ apply_initcwnd() { return 1 } fi - rm -f "$ROUTE_OWNED_MARKER" + if ! remove_initcwnd_ownership_marker; then + error "删除 initcwnd ownership marker 失败" + return 1 + fi if ! remove_initcwnd_hook; then error "移除本脚本 initcwnd 持久化钩子失败" return 1 @@ -1194,7 +1201,10 @@ apply_initcwnd() { clean=$(strip_route_window_fields "$route") read -r -a route_args <<< "$clean" if ip -4 route replace "${route_args[@]}" initcwnd 32 initrwnd 32; then - install -D -m 0600 /dev/null "$ROUTE_OWNED_MARKER" + if ! create_initcwnd_ownership_marker; then + error "创建 initcwnd ownership marker 失败" + return 1 + fi if ! write_initcwnd_hook; then error "无法写入 initcwnd 持久化钩子" return 1 @@ -1222,7 +1232,7 @@ restore_default_route() { initcwnd_settings_owned || return 0 route=$(default_ipv4_route) || return 1 if [[ -z "$route" ]]; then - rm -f "$ROUTE_OWNED_MARKER" + remove_initcwnd_ownership_marker || return 1 return 0 fi if grep -Eq '(^| )(initcwnd|initrwnd) [0-9]+( |$)' <<< "$route"; then @@ -1231,7 +1241,7 @@ restore_default_route() { (( ${#route_args[@]} > 0 )) || return 1 ip -4 route replace "${route_args[@]}" || return 1 fi - rm -f "$ROUTE_OWNED_MARKER" + remove_initcwnd_ownership_marker || return 1 return 0 fi @@ -1239,7 +1249,7 @@ restore_default_route() { initcwnd_settings_owned || return 0 route=$(default_ipv4_route) || return 1 if [[ -z "$route" ]]; then - rm -f "$ROUTE_OWNED_MARKER" + remove_initcwnd_ownership_marker || return 1 return 0 fi route=$(strip_route_window_fields "$route") @@ -1249,9 +1259,9 @@ restore_default_route() { (( ${#route_args[@]} > 0 )) || return 1 ip -4 route replace "${route_args[@]}" || return 1 if [[ -n "$owned_file" && -e "$owned_file" ]]; then - install -D -m 0600 /dev/null "$ROUTE_OWNED_MARKER" + create_initcwnd_ownership_marker || return 1 else - rm -f "$ROUTE_OWNED_MARKER" + remove_initcwnd_ownership_marker || return 1 fi } @@ -2805,6 +2815,29 @@ verify_network_config() { success "运行时 sysctl 已与生成配置一致" } +rollback_initcwnd_install() { + INITCWND_ROLLBACK_FAILED_ITEMS=() + + if ! restore_default_route \ + "$ROUTE_PREVIOUS_BACKUP" "$ROUTE_PREVIOUS_OWNED" "$ROUTE_PREVIOUS_ABSENT"; then + INITCWND_ROLLBACK_FAILED_ITEMS+=(route) + fi + if ! restore_managed_file \ + "$INITCWND_ROUTE_HOOK" "$ROUTE_HOOK_PREVIOUS_BACKUP" \ + "$ROUTE_HOOK_PREVIOUS_ABSENT"; then + INITCWND_ROLLBACK_FAILED_ITEMS+=(hook) + fi + if ! apply_runtime_values_strict "$RUNTIME_PREVIOUS_BACKUP"; then + INITCWND_ROLLBACK_FAILED_ITEMS+=(runtime) + fi + if ! restore_managed_file \ + "$NETWORK_CONF" "$NETWORK_PREVIOUS_BACKUP" "$NETWORK_PREVIOUS_ABSENT"; then + INITCWND_ROLLBACK_FAILED_ITEMS+=(config) + fi + + (( ${#INITCWND_ROLLBACK_FAILED_ITEMS[@]} == 0 )) +} + install_optimization() { local temp_config local runtime_backup @@ -2893,15 +2926,10 @@ install_optimization() { fi if ! apply_initcwnd; then - restore_default_route \ - "$ROUTE_PREVIOUS_BACKUP" "$ROUTE_PREVIOUS_OWNED" "$ROUTE_PREVIOUS_ABSENT" || true - restore_managed_file \ - "$INITCWND_ROUTE_HOOK" "$ROUTE_HOOK_PREVIOUS_BACKUP" \ - "$ROUTE_HOOK_PREVIOUS_ABSENT" || true - restore_runtime_values "$RUNTIME_PREVIOUS_BACKUP" - if ! restore_managed_file \ - "$NETWORK_CONF" "$NETWORK_PREVIOUS_BACKUP" "$NETWORK_PREVIOUS_ABSENT"; then - warn "路由优化失败后无法恢复上次持久网络配置" + if rollback_initcwnd_install; then + error "initcwnd 应用失败,已回滚 route、hook、runtime、config" + else + error "initcwnd 应用失败,且回滚不完整:失败项 ${INITCWND_ROLLBACK_FAILED_ITEMS[*]}" fi rm -f "$temp_config" "$runtime_backup" return 1 @@ -2996,9 +3024,9 @@ restore_captured_default_route() { current_route=$(query_default_ipv4_route) || query_status=$? if (( query_status == 2 )); then if [[ -e "$transaction_dir/route-owned" ]]; then - install -D -m 0600 /dev/null "$ROUTE_OWNED_MARKER" + create_initcwnd_ownership_marker || return 1 else - rm -f "$ROUTE_OWNED_MARKER" + remove_initcwnd_ownership_marker || return 1 fi return $? fi @@ -3011,9 +3039,9 @@ restore_captured_default_route() { (( ${#route_args[@]} > 0 )) || return 1 ip -4 route del "${route_args[@]}" || return 1 if [[ -e "$transaction_dir/route-owned" ]]; then - install -D -m 0600 /dev/null "$ROUTE_OWNED_MARKER" + create_initcwnd_ownership_marker || return 1 else - rm -f "$ROUTE_OWNED_MARKER" + remove_initcwnd_ownership_marker || return 1 fi } diff --git a/tests/test-network-optimize.sh b/tests/test-network-optimize.sh index ac5813e..5abd6a3 100755 --- a/tests/test-network-optimize.sh +++ b/tests/test-network-optimize.sh @@ -489,11 +489,13 @@ printf 'PASS: owned cleanup failure propagates\n' # Hook deletion failure must return nonzero and leave the now-inert hook for retry. CURRENT_ROUTE="default via 192.0.2.1 dev eth0 proto dhcp metric 100 initcwnd 32 initrwnd 32" write_initcwnd_hook +eval "$(declare -f remove_initcwnd_hook | sed '1s/remove_initcwnd_hook/remove_initcwnd_hook_real/')" remove_initcwnd_hook() { return 1; } if apply_initcwnd >/dev/null 2>&1; then fail "failed managed hook deletion unexpectedly succeeded" fi -unset -f remove_initcwnd_hook +eval "$(declare -f remove_initcwnd_hook_real | sed '1s/remove_initcwnd_hook_real/remove_initcwnd_hook/')" +unset -f remove_initcwnd_hook_real assert_eq "default via 192.0.2.1 dev eth0 proto dhcp metric 100" \ "$CURRENT_ROUTE" "hook deletion failure does not undo completed route cleanup" [[ -e "$INITCWND_ROUTE_HOOK" ]] || fail "failed hook deletion removed the hook" @@ -501,10 +503,70 @@ assert_eq "default via 192.0.2.1 dev eth0 proto dhcp metric 100" \ rm -f "$INITCWND_ROUTE_HOOK" printf 'PASS: managed hook deletion failure propagates without success\n' +# Marker creation and deletion failures must be observable to callers. +remove_initcwnd_ownership_marker CURRENT_ROUTE="default via 192.0.2.1 dev eth0 proto dhcp metric 100" -install -D -m 0600 /dev/null "$ROUTE_OWNED_MARKER" +INITCWND_ENABLED=true +( + create_initcwnd_ownership_marker() { return 1; } + if apply_initcwnd >/dev/null 2>&1; then + fail "failed ownership marker creation unexpectedly succeeded" + fi + [[ ! -e "$ROUTE_OWNED_MARKER" ]] || + fail "failed ownership marker creation left a marker" +) +printf 'PASS: ownership marker creation failure propagates\n' + +create_initcwnd_ownership_marker +CURRENT_ROUTE="default via 192.0.2.1 dev eth0 proto dhcp metric 100" +INITCWND_ENABLED=false +( + remove_initcwnd_ownership_marker() { return 1; } + if apply_initcwnd >/dev/null 2>&1; then + fail "failed ownership marker deletion unexpectedly succeeded" + fi +) +[[ -e "$ROUTE_OWNED_MARKER" ]] || + fail "failed ownership marker deletion removed the marker" +remove_initcwnd_ownership_marker +printf 'PASS: ownership marker deletion failure propagates\n' + +# BBR persistence must use checked atomic writes even when called in a condition. +( + atomic_write_file() { return 1; } + if persist_bbr_module; then + fail "failed BBR atomic write unexpectedly succeeded" + fi +) +rm -f "$BBR_MODULES_FILE" +persist_bbr_module +assert_eq tcp_bbr "$(cat "$BBR_MODULES_FILE")" "persist BBR module atomically" +assert_eq 644 "$(stat -c '%a' "$BBR_MODULES_FILE")" "persist BBR module mode" + +# Rollback collects every failed initcwnd recovery item in execution order. +( + restore_default_route() { return 1; } + restore_managed_file() { return 1; } + apply_runtime_values_strict() { return 1; } + if rollback_initcwnd_install; then + fail "incomplete initcwnd rollback unexpectedly succeeded" + fi + assert_eq 'route hook runtime config' "${INITCWND_ROLLBACK_FAILED_ITEMS[*]}" \ + "summarize all initcwnd rollback failures" +) +printf 'PASS: initcwnd rollback reports route, hook, runtime, and config failures\n' + +CURRENT_ROUTE="default via 192.0.2.1 dev eth0 proto dhcp metric 100" +create_initcwnd_ownership_marker assert_eq 'drift|ownership marker exists but default route lacks initcwnd/initrwnd 32' \ "$(detect_initcwnd_state)" "detect marker and route drift" +INITCWND_ENABLED=false +apply_initcwnd >/dev/null +assert_eq "default via 192.0.2.1 dev eth0 proto dhcp metric 100" "$CURRENT_ROUTE" \ + "stale marker cleanup preserves route without window fields" +[[ ! -e "$ROUTE_OWNED_MARKER" ]] || fail "stale marker cleanup retained ownership" +printf 'PASS: stale initcwnd marker does not trigger nounset\n' +create_initcwnd_ownership_marker CURRENT_ROUTE="default via 192.0.2.1 dev eth0 proto dhcp metric 100 initcwnd 32 initrwnd 32" assert_eq 'effective|owned default route has initcwnd/initrwnd 32' \ "$(detect_initcwnd_state)" "detect owned initcwnd route as effective" From d7829b12ca001a2666ebeeb225b0ec70ecfb1478 Mon Sep 17 00:00:00 2001 From: LUNO Date: Tue, 18 Aug 2026 21:37:36 +0000 Subject: [PATCH 3/5] fix(network): reap Cloudflare probes per egress --- modules/network-optimize.sh | 95 ++++++++++++++----- tests/test-network-optimize-entry.sh | 136 +++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 26 deletions(-) diff --git a/modules/network-optimize.sh b/modules/network-optimize.sh index d45117c..c8681f0 100644 --- a/modules/network-optimize.sh +++ b/modules/network-optimize.sh @@ -139,11 +139,12 @@ CALCULATION_REASON="pending bandwidth" RMEM_REASON="pending bandwidth" WMEM_REASON="pending bandwidth" PROBE_IFACE="" -PROBE_ENVIRONMENT_SHOWN="false" +declare -A PROBE_ENVIRONMENT_SHOWN_BY_IFACE=() declare -a TRAFFIC_IFACES=() declare -A TRAFFIC_RX_START_BY_IFACE=() declare -A TRAFFIC_TX_START_BY_IFACE=() declare -a IPERF_RUNNER_PIDS=() +declare -a CLOUDFLARE_WORKER_PIDS=() declare -a INITCWND_ROLLBACK_FAILED_ITEMS=() PREFERRED_IPERF_PORT="" CLOUDFLARE_IPV4="" @@ -1282,7 +1283,7 @@ read_iface_counter() { traffic_reset() { PROBE_IFACE="" - PROBE_ENVIRONMENT_SHOWN="false" + PROBE_ENVIRONMENT_SHOWN_BY_IFACE=() TRAFFIC_IFACES=() TRAFFIC_RX_START_BY_IFACE=() TRAFFIC_TX_START_BY_IFACE=() @@ -1293,7 +1294,7 @@ traffic_add_target() { local iface rx tx iface=$(detect_ipv4_iface_for_target "$target") || return 1 - [[ -n "$PROBE_IFACE" ]] || PROBE_IFACE="$iface" + PROBE_IFACE="$iface" if [[ -n "${TRAFFIC_RX_START_BY_IFACE[$iface]+x}" ]]; then return 0 fi @@ -1387,11 +1388,12 @@ unregister_iperf_runner() { terminate_recorded_pid() { local pid="$1" + local kill_after="${2:-$IPERF_KILL_AFTER_SECONDS}" local attempt [[ "$pid" =~ ^[0-9]+$ ]] || return 0 kill -TERM "$pid" 2>/dev/null || true - for ((attempt = 0; attempt < IPERF_KILL_AFTER_SECONDS * 10; attempt++)); do + for ((attempt = 0; attempt < kill_after * 10; attempt++)); do kill -0 "$pid" 2>/dev/null || break sleep 0.1 done @@ -1409,10 +1411,33 @@ cleanup_iperf_runners() { done } -terminate_pid_now() { - local pid="$1" - [[ "$pid" =~ ^[0-9]+$ ]] || return 0 - kill -KILL "$pid" 2>/dev/null || true +register_cloudflare_worker() { + CLOUDFLARE_WORKER_PIDS+=("$1") +} + +unregister_cloudflare_worker() { + local wanted="$1" pid + local -a remaining=() + + for pid in "${CLOUDFLARE_WORKER_PIDS[@]}"; do + [[ "$pid" == "$wanted" ]] || remaining+=("$pid") + done + CLOUDFLARE_WORKER_PIDS=("${remaining[@]}") +} + +cleanup_cloudflare_workers() { + local pid + local -a pids=("${CLOUDFLARE_WORKER_PIDS[@]}") + + for pid in "${pids[@]}"; do + terminate_recorded_pid "$pid" + unregister_cloudflare_worker "$pid" + done +} + +cleanup_probe_processes() { + cleanup_iperf_runners + cleanup_cloudflare_workers } run_iperf_runner() { @@ -1844,7 +1869,12 @@ cloudflare_worker() { local direction="$1" local upload_file="${2:-}" local deadline=$((SECONDS + CLOUDFLARE_DURATION)) - local remaining + local remaining curl_pid="" curl_rc=0 + + trap 'terminate_recorded_pid "${curl_pid:-}" 1' EXIT + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM while (( SECONDS < deadline )); do remaining=$((deadline - SECONDS)) @@ -1855,7 +1885,7 @@ cloudflare_worker() { --resolve "speed.cloudflare.com:443:$CLOUDFLARE_IPV4" \ --header 'Accept-Encoding: identity' \ --connect-timeout 4 --max-time "$remaining" \ - "$SPEED_DOWNLOAD_URL?bytes=$CLOUDFLARE_DOWNLOAD_BYTES" || break + "$SPEED_DOWNLOAD_URL?bytes=$CLOUDFLARE_DOWNLOAD_BYTES" & else curl -4 --noproxy '*' --fail --silent --output /dev/null \ --resolve "speed.cloudflare.com:443:$CLOUDFLARE_IPV4" \ @@ -1863,8 +1893,13 @@ cloudflare_worker() { --header 'Expect:' \ --connect-timeout 4 --max-time "$remaining" \ --request POST --upload-file "$upload_file" \ - "$SPEED_UPLOAD_URL" || break + "$SPEED_UPLOAD_URL" & fi + curl_pid=$! + curl_rc=0 + wait "$curl_pid" || curl_rc=$? + curl_pid="" + (( curl_rc == 0 )) || break done } @@ -1899,7 +1934,9 @@ probe_cloudflare_direction() { for ((index = 0; index < CLOUDFLARE_PARALLEL; index++)); do cloudflare_worker "$direction" "$upload_file" & - pids+=("$!") + pid=$! + pids+=("$pid") + register_cloudflare_worker "$pid" done while true; do @@ -1913,15 +1950,16 @@ probe_cloudflare_direction() { [[ "$alive" == "true" ]] || break if traffic_budget_reached "$direction"; then - for pid in "${pids[@]}"; do - terminate_pid_now "$pid" - done + cleanup_cloudflare_workers break fi sleep 0.05 done for pid in "${pids[@]}"; do - wait "$pid" 2>/dev/null || true + if ! wait "$pid" 2>/dev/null; then + detail "Cloudflare worker $pid 已停止或失败" + fi + unregister_cloudflare_worker "$pid" done [[ -n "$upload_file" ]] && rm -f "$upload_file" @@ -2008,6 +2046,7 @@ round_bandwidth() { } show_probe_environment() { + local iface="${1:-$PROBE_IFACE}" local driver="virtual" local rx_queues local tx_queues @@ -2016,28 +2055,32 @@ show_probe_environment() { local root_qdisc local driver_path - driver_path=$(readlink -f "/sys/class/net/$PROBE_IFACE/device/driver" 2>/dev/null || true) + [[ -n "$iface" ]] || return 1 + driver_path=$(readlink -f "/sys/class/net/$iface/device/driver" 2>/dev/null || true) [[ -n "$driver_path" ]] && driver="${driver_path##*/}" - rx_queues=$(find "/sys/class/net/$PROBE_IFACE/queues" -maxdepth 1 -name 'rx-*' 2>/dev/null | wc -l) - tx_queues=$(find "/sys/class/net/$PROBE_IFACE/queues" -maxdepth 1 -name 'tx-*' 2>/dev/null | wc -l) + rx_queues=$(find "/sys/class/net/$iface/queues" -maxdepth 1 -name 'rx-*' 2>/dev/null | wc -l) + tx_queues=$(find "/sys/class/net/$iface/queues" -maxdepth 1 -name 'tx-*' 2>/dev/null | wc -l) current_cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null || echo unknown) default_qdisc=$(sysctl -n net.core.default_qdisc 2>/dev/null || echo unknown) - root_qdisc=$(tc qdisc show dev "$PROBE_IFACE" 2>/dev/null | awk 'NR == 1 {print $2}') + root_qdisc=$(tc qdisc show dev "$iface" 2>/dev/null | awk 'NR == 1 {print $2}') - detail "测速环境:接口 $PROBE_IFACE / 驱动 $driver / RX-TX 队列 ${rx_queues}-${tx_queues}" + detail "测速环境:接口 $iface / 驱动 $driver / RX-TX 队列 ${rx_queues}-${tx_queues}" detail "测速前网络栈:CC $current_cc / default_qdisc $default_qdisc / root_qdisc ${root_qdisc:-unknown}" if [[ "$root_qdisc" == "htb" && -f "${NETWORK_OPTIMIZE_TCSHAPE_CONFIG_FILE:-/etc/tcshape.conf}" ]]; then BANDWIDTH_PROBE_NOTE="tcshape HTB 整形状态下测得(可能偏低)" - warn "检测到 tcshape HTB 正在限制 $PROBE_IFACE,主动测速结果可能偏低" + warn "检测到 tcshape HTB 正在限制 $iface,主动测速结果可能偏低" warn "建议先执行 tcshape off,再重新运行 network-optimize 主动测速" fi } show_probe_environment_once() { - [[ "$PROBE_ENVIRONMENT_SHOWN" != "true" && -n "$PROBE_IFACE" ]] || return 0 - show_probe_environment - PROBE_ENVIRONMENT_SHOWN="true" + local iface="${PROBE_IFACE:-}" + + [[ -n "$iface" ]] || return 0 + [[ -z "${PROBE_ENVIRONMENT_SHOWN_BY_IFACE[$iface]+x}" ]] || return 0 + show_probe_environment "$iface" + PROBE_ENVIRONMENT_SHOWN_BY_IFACE["$iface"]="true" } probe_bandwidth() { @@ -3472,6 +3515,6 @@ main() { trap 'error "网络优化脚本在第 $LINENO 行执行失败"' ERR if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then - trap 'cleanup_iperf_runners' EXIT + trap 'cleanup_probe_processes' EXIT main "$@" fi diff --git a/tests/test-network-optimize-entry.sh b/tests/test-network-optimize-entry.sh index 30bafbe..f41dc14 100755 --- a/tests/test-network-optimize-entry.sh +++ b/tests/test-network-optimize-entry.sh @@ -317,6 +317,89 @@ NETWORK_TEST_TOTAL=85000000000 assert_ok "active probe budget stops at reserved total threshold" \ traffic_budget_reached upload +cloudflare_fake_bin="$TEMP_DIR/cloudflare-fake-bin" +mkdir -p "$cloudflare_fake_bin" +cat > "$cloudflare_fake_bin/curl" <<'EOF' +#!/usr/bin/env bash +mkdir -p "$TEST_CURL_PID_DIR" +printf '%s\n' "$BASHPID" > "$TEST_CURL_PID_DIR/$BASHPID" +exec sleep 30 +EOF +chmod +x "$cloudflare_fake_bin/curl" + +assert_cloudflare_curls_stopped() { + local pid_file pid found="false" + + for pid_file in "$1"/*; do + [[ -f "$pid_file" ]] || continue + found="true" + pid=$(<"$pid_file") + ! kill -0 "$pid" 2>/dev/null || fail "Cloudflare cleanup left curl PID $pid running" + done + [[ "$found" == "true" ]] || fail "Cloudflare process test did not start curl" +} + +cloudflare_budget_pid_dir="$TEMP_DIR/cloudflare-budget-pids" +mkdir -p "$cloudflare_budget_pid_dir" +( + trap - EXIT + PATH="$cloudflare_fake_bin:$PATH" + TEST_CURL_PID_DIR="$cloudflare_budget_pid_dir" + export PATH TEST_CURL_PID_DIR + CLOUDFLARE_IPV4=192.0.2.80 + CLOUDFLARE_WORKER_PIDS=() + budget_checks=0 + traffic_used_bytes() { printf '%s\n' 0; } + traffic_budget_reached() { + local curl_count + + ((budget_checks += 1)) + (( budget_checks > 1 )) || return 1 + for _ in {1..200}; do + curl_count=$(find "$TEST_CURL_PID_DIR" -type f | wc -l) + (( curl_count >= CLOUDFLARE_PARALLEL )) && return 0 + sleep 0.01 + done + return 0 + } + + cloudflare_rc=0 + probe_cloudflare_direction download >/dev/null 2>&1 || cloudflare_rc=$? + (( cloudflare_rc != 0 )) || fail "budget-stopped Cloudflare probe unexpectedly succeeded" + assert_eq 0 "${#CLOUDFLARE_WORKER_PIDS[@]}" \ + "budget stop unregisters all Cloudflare workers" +) +assert_cloudflare_curls_stopped "$cloudflare_budget_pid_dir" +printf 'PASS: Cloudflare budget stop reaps real curl children\n' + +cloudflare_signal_pid_dir="$TEMP_DIR/cloudflare-signal-pids" +mkdir -p "$cloudflare_signal_pid_dir" +( + trap - EXIT + PATH="$cloudflare_fake_bin:$PATH" + TEST_CURL_PID_DIR="$cloudflare_signal_pid_dir" + export PATH TEST_CURL_PID_DIR + CLOUDFLARE_IPV4=192.0.2.81 + CLOUDFLARE_WORKER_PIDS=() + trap 'cleanup_cloudflare_workers; exit 143' TERM + cloudflare_worker download & + register_cloudflare_worker "$!" + while true; do sleep 0.1; done +) & +cloudflare_wrapper_pid=$! +for _ in {1..200}; do + find "$cloudflare_signal_pid_dir" -type f -print -quit | grep -q . && break + sleep 0.01 +done +find "$cloudflare_signal_pid_dir" -type f -print -quit | grep -q . || + fail "signal cleanup test did not start curl" +kill -TERM "$cloudflare_wrapper_pid" +cloudflare_signal_rc=0 +wait "$cloudflare_wrapper_pid" 2>/dev/null || cloudflare_signal_rc=$? +assert_eq 143 "$cloudflare_signal_rc" "Cloudflare wrapper returns signal-derived status" +assert_cloudflare_curls_stopped "$cloudflare_signal_pid_dir" +printf 'PASS: Cloudflare signal exit reaps real curl children\n' + ROUTE_GET_LOG="$TEMP_DIR/route-get-target" ip() { [[ "$1 $2 $3" == '-4 route get' ]] || return 1 @@ -332,6 +415,59 @@ assert_eq eth7 "$PROBE_IFACE" "traffic accounting uses actual target route inter assert_eq 198.51.100.25 "$(cat "$ROUTE_GET_LOG")" "traffic accounting routes the real IPv4 target" unset -f ip read_iface_counter +multi_iface_qdisc_log="$TEMP_DIR/multi-iface-qdisc.log" +multi_iface_tcshape_config="$TEMP_DIR/multi-iface-tcshape.conf" +printf '%s\n' 'RATE_MBIT=500' > "$multi_iface_tcshape_config" +( + traffic_reset + BANDWIDTH_PROBE_NOTE="" + NETWORK_OPTIMIZE_TCSHAPE_CONFIG_FILE="$multi_iface_tcshape_config" + ip() { + [[ "$1 $2 $3" == '-4 route get' ]] || return 1 + case "$4" in + 192.0.2.10) printf '%s dev eth0 src 192.0.2.1\n' "$4" ;; + 198.51.100.20) printf '%s dev eth1 src 198.51.100.1\n' "$4" ;; + *) return 1 ;; + esac + } + read_iface_counter() { + case "$2" in rx) printf '%s\n' 100 ;; tx) printf '%s\n' 200 ;; *) return 1 ;; esac + } + tc() { + [[ "$1 $2 $3" == 'qdisc show dev' ]] || return 1 + printf '%s\n' "$4" >> "$multi_iface_qdisc_log" + case "$4" in + eth0) printf '%s\n' 'qdisc fq 0: root' ;; + eth1) printf '%s\n' 'qdisc htb 1: root' ;; + *) return 1 ;; + esac + } + sysctl() { + case "${2:-}" in + net.ipv4.tcp_congestion_control) printf '%s\n' bbr ;; + net.core.default_qdisc) printf '%s\n' fq ;; + *) return 1 ;; + esac + } + readlink() { return 1; } + find() { return 0; } + + traffic_add_target 192.0.2.10 + show_probe_environment_once >/dev/null + traffic_add_target 198.51.100.20 + show_probe_environment_once >/dev/null + traffic_add_target 192.0.2.10 + show_probe_environment_once >/dev/null + + expected_qdisc_ifaces=$(printf 'eth0\neth1\n') + assert_eq eth0 "$PROBE_IFACE" "probe interface follows latest actual target route" + assert_eq "$expected_qdisc_ifaces" "$(cat "$multi_iface_qdisc_log")" \ + "probe checks qdisc once for every actual egress interface" + assert_eq 'tcshape HTB 整形状态下测得(可能偏低)' "$BANDWIDTH_PROBE_NOTE" \ + "probe detects tcshape on a later egress interface" +) +printf 'PASS: multi-egress probe checks tcshape and qdisc per interface\n' + generated_config="$TEMP_DIR/generated.conf" prepare_dynamic_case resolve_tuning_values >/dev/null From 1386539ac41badacc0a47eb5f38ff2cc54e064b7 Mon Sep 17 00:00:00 2001 From: LUNO Date: Tue, 18 Aug 2026 21:41:47 +0000 Subject: [PATCH 4/5] chore(network): document shellcheck shadow --- modules/network-optimize.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/network-optimize.sh b/modules/network-optimize.sh index c8681f0..b486653 100644 --- a/modules/network-optimize.sh +++ b/modules/network-optimize.sh @@ -1150,6 +1150,8 @@ write_initcwnd_hook() { fi } +# Generated hook uses clean as an array; apply path intentionally shadows it as a string. +# shellcheck disable=SC2128,SC2178 apply_initcwnd() { local route="" clean="" local owned="false" From b2f48be5f3d81195ee49ba90c144c53d57c13422 Mon Sep 17 00:00:00 2001 From: LUNO Date: Tue, 18 Aug 2026 21:49:42 +0000 Subject: [PATCH 5/5] fix(tests): satisfy forward function lint --- tests/test-network-optimize-entry.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test-network-optimize-entry.sh b/tests/test-network-optimize-entry.sh index f41dc14..39fb363 100755 --- a/tests/test-network-optimize-entry.sh +++ b/tests/test-network-optimize-entry.sh @@ -517,7 +517,8 @@ printf '%s\n' \ 'net.ipv6.conf.eth0.accept_ra = 2' \ 'net.ipv6.conf.default.forwarding = 1' \ 'net.ipv6.conf.all.forwarding = 1' > "$NETWORK_CONF" -cp "$NETWORK_CONF" "$old_managed_config" +# shellcheck disable=SC2218 +command cp "$NETWORK_CONF" "$old_managed_config" migration_warning=$(warn_retired_forwarding_management) for warning_line in \ '已停止持久管理 IPv4/IPv6 forwarding 与 RA;当前运行值保持不变。' \ @@ -528,7 +529,8 @@ done printf '%s\n' 'net.ipv4.ip_forward = 1' > "$NETWORK_CONF" assert_eq '' "$(warn_retired_forwarding_management)" \ "unmarked config does not print forwarding migration warning" -cp "$old_managed_config" "$NETWORK_CONF" +# shellcheck disable=SC2218 +command cp "$old_managed_config" "$NETWORK_CONF" IP_FORWARD_RUNTIME=0 IPV6_ALL_RA_RUNTIME=2 @@ -618,6 +620,8 @@ done printf 'PASS: upgrade runtime snapshot retains retired keys\n' rm -f "$NETWORK_PREVIOUS_BACKUP" "$NETWORK_PREVIOUS_ABSENT" +# Sourced implementation is intentionally overridden by later failure-injection tests. +# shellcheck disable=SC2218 backup_managed_file \ "$NETWORK_CONF" "$NETWORK_INITIAL_BACKUP" "$NETWORK_PREVIOUS_BACKUP" \ "$NETWORK_INITIAL_ABSENT" "$NETWORK_PREVIOUS_ABSENT" @@ -626,6 +630,7 @@ cmp -s "$old_managed_config" "$NETWORK_PREVIOUS_BACKUP" || printf 'PASS: previous backup preserves legacy config byte-for-byte\n' atomic_install_file "$generated_config" "$NETWORK_CONF" 0644 DEBUG=1 +# shellcheck disable=SC2218 apply_network_config "$NETWORK_CONF" assert_eq 0 "$RETIRED_SYSCTL_WRITES" \ "upgraded config applies no forwarding or RA runtime writes" @@ -639,6 +644,7 @@ IPV6_DEFAULT_RA_RUNTIME=9 IPV6_ETH0_RA_RUNTIME=9 IPV6_DEFAULT_FORWARD_RUNTIME=9 IPV6_ALL_FORWARD_RUNTIME=9 +# shellcheck disable=SC2218 apply_network_config "$NETWORK_PREVIOUS_BACKUP" restore_managed_file \ "$NETWORK_CONF" "$NETWORK_PREVIOUS_BACKUP" "$NETWORK_PREVIOUS_ABSENT"