mirror of
https://github.com/bivlked/amneziawg-installer.git
synced 2026-10-19 13:13:19 +00:00
fix: prevent split-brain in load_awg_params + atomic export + optimizations (#38)
Post-audit improvements on top of the v5.7.13 fix commit (20ba58d).
The audit identified one HIGH severity bug in the post-#38 code path,
plus self-review found two further improvements.
CRITICAL — split-brain prevention in load_awg_params
The post-#38 load_awg_params silently fell back to safe_load_config
when load_awg_params_from_server_conf returned 1. With a partially
corrupt awg0.conf (e.g. user accidentally removed H4 during a manual
edit) the validation loop saw AWG_* still populated from the init
file and reported success — while the live server actually ran the
new (broken) config. regen / render_client_config would then issue
clients stale J*/S*/H* from the cached init file, producing exactly
the kind of mismatch elvaleto and Klavishnik reported in #38, only
quietly.
Fix: live awg0.conf is now the SOLE source of truth for AWG protocol
parameters whenever the file exists. Atomic-parser failure on a live
config is now an explicit error with multi-line log_error and
return 1 — no fallback. The init file is used for AWG params ONLY
when awg0.conf is missing entirely (bootstrap path of first install).
This is the most important change in this commit — it closes a
regression introduced by the original #38 fix.
Atomic export in load_awg_params_from_server_conf
Self-review: previously the parser exported each AWG_* variable as
soon as it parsed the matching key. On a partially corrupt awg0.conf
that meant AWG_Jc could be updated to a new value while AWG_H4
remained at the stale init value — mixed state in the environment.
The function now accumulates values into local _Jc/_Jmin/.../_H4 and
exports them only after verifying that all 11 required fields were
found. Either everything is exported, or nothing is touched.
Optimization: single /dev/urandom read in generate_awg_h_ranges
Previously the function called rand_range 8 times (8 separate od
subprocess invocations). It now does a single
od -An -N32 -tu4 /dev/urandom read that yields all 8 uint32 values
at once, then parses them with read. Falls back to the per-value
rand_range loop if /dev/urandom is unavailable. ~8x fewer subprocess
invocations on the hot path. Mirrored in install_amneziawg.sh and
both EN scripts.
Explicit return 0 in apply_config success path
The syncconf success path ended with exec apply_fd close and returned
that command exit code implicitly. Added an explicit return 0 so
callers (manage add/remove, check_expired_clients) get a guaranteed
exit code on success. Mirrored in EN.
Tests
Three new bats cases in test_load_awg_params.bats (66 total in the
suite, up from 63):
- split-brain prevention: corrupt server_conf with init present must
return 1 (regression for the audit finding)
- bootstrap path: server_conf missing must fall back to init and
return 0 (sanity check that bootstrap still works)
- atomic export: corrupt server_conf must not pollute env with the
partially parsed values
This commit is contained in:
parent
30116904cf
commit
ece59174c3
5 changed files with 340 additions and 84 deletions
140
awg_common.sh
140
awg_common.sh
|
|
@ -101,17 +101,38 @@ rand_range() {
|
|||
# Минимальная ширина каждого диапазона = 1000.
|
||||
# Печатает 4 строки "low-high" в stdout. Возвращает 1 при неудаче.
|
||||
# Защита от ТСПУ-фингерпринта по статическим H-значениям (#38).
|
||||
#
|
||||
# Оптимизация: один вызов `od -N32 -tu4` читает 32 байта = 8 uint32 значений
|
||||
# одной операцией, вместо 8 отдельных subprocess через rand_range.
|
||||
# Fallback на rand_range если /dev/urandom недоступен.
|
||||
generate_awg_h_ranges() {
|
||||
local attempt=0 max_attempts=20
|
||||
while (( attempt < max_attempts )); do
|
||||
local p1 p2 p3 p4 p5 p6 p7 p8 sorted
|
||||
p1=$(rand_range 0 4294967295); p2=$(rand_range 0 4294967295)
|
||||
p3=$(rand_range 0 4294967295); p4=$(rand_range 0 4294967295)
|
||||
p5=$(rand_range 0 4294967295); p6=$(rand_range 0 4294967295)
|
||||
p7=$(rand_range 0 4294967295); p8=$(rand_range 0 4294967295)
|
||||
sorted=$(printf '%s\n' "$p1" "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" | sort -n)
|
||||
local arr=()
|
||||
while IFS= read -r _line; do arr+=("$_line"); done <<< "$sorted"
|
||||
local raw arr=() _v
|
||||
# Один read 32 байт из /dev/urandom = 8 uint32 значений
|
||||
raw=$(od -An -N32 -tu4 /dev/urandom 2>/dev/null | tr -s ' \n' '\n' | sed '/^$/d')
|
||||
if [[ -n "$raw" ]]; then
|
||||
local count=0
|
||||
while IFS= read -r _v; do
|
||||
[[ "$_v" =~ ^[0-9]+$ ]] || continue
|
||||
arr+=("$_v"); count=$((count + 1))
|
||||
(( count == 8 )) && break
|
||||
done <<< "$raw"
|
||||
fi
|
||||
# Fallback: 8 отдельных вызовов rand_range (если urandom недоступен)
|
||||
if (( ${#arr[@]} != 8 )); then
|
||||
arr=()
|
||||
local _i
|
||||
for _i in 1 2 3 4 5 6 7 8; do
|
||||
arr+=("$(rand_range 0 4294967295)")
|
||||
done
|
||||
fi
|
||||
# Сортировка
|
||||
local sorted
|
||||
sorted=$(printf '%s\n' "${arr[@]}" | sort -n)
|
||||
arr=()
|
||||
while IFS= read -r _v; do arr+=("$_v"); done <<< "$sorted"
|
||||
# Проверка минимальной ширины каждой пары
|
||||
if (( ${arr[1]} - ${arr[0]} >= 1000 )) && \
|
||||
(( ${arr[3]} - ${arr[2]} >= 1000 )) && \
|
||||
(( ${arr[5]} - ${arr[4]} >= 1000 )) && \
|
||||
|
|
@ -165,8 +186,11 @@ safe_load_config() {
|
|||
}
|
||||
|
||||
# Парсер живого серверного конфига AmneziaWG (источник истины для AWG_*).
|
||||
# Читает секцию [Interface] из awg0.conf и экспортирует AWG_* переменные.
|
||||
# Возвращает 0 если нашёл хотя бы Jc и H4, иначе 1.
|
||||
# Читает секцию [Interface] из awg0.conf и экспортирует AWG_* переменные
|
||||
# АТОМАРНО: либо все 11 обязательных параметров (Jc/Jmin/Jmax/S1-S4/H1-H4)
|
||||
# найдены и экспортированы, либо ничего не меняется в окружении и возврат 1.
|
||||
# Это защищает от mixed-state при частично corrupt awg0.conf.
|
||||
# I1, ListenPort — опциональные, экспортируются если нашлись.
|
||||
# Решает баг #38: regen использовал устаревшие значения из init-файла,
|
||||
# а не актуальные из awg0.conf после ручной правки.
|
||||
# shellcheck disable=SC2120 # Опциональный аргумент используется только в тестах
|
||||
|
|
@ -174,6 +198,12 @@ load_awg_params_from_server_conf() {
|
|||
local conf="${1:-$SERVER_CONF_FILE}"
|
||||
[[ -f "$conf" ]] || return 1
|
||||
|
||||
# Локальное накопление — экспортируем всё-или-ничего в конце
|
||||
local _Jc="" _Jmin="" _Jmax=""
|
||||
local _S1="" _S2="" _S3="" _S4=""
|
||||
local _H1="" _H2="" _H3="" _H4=""
|
||||
local _I1="" _Port=""
|
||||
|
||||
local in_iface=0 line key value
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^\[Interface\] ]]; then in_iface=1; continue; fi
|
||||
|
|
@ -188,45 +218,90 @@ load_awg_params_from_server_conf() {
|
|||
# Trim trailing whitespace
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
case "$key" in
|
||||
Jc) export AWG_Jc="$value" ;;
|
||||
Jmin) export AWG_Jmin="$value" ;;
|
||||
Jmax) export AWG_Jmax="$value" ;;
|
||||
S1) export AWG_S1="$value" ;;
|
||||
S2) export AWG_S2="$value" ;;
|
||||
S3) export AWG_S3="$value" ;;
|
||||
S4) export AWG_S4="$value" ;;
|
||||
H1) export AWG_H1="$value" ;;
|
||||
H2) export AWG_H2="$value" ;;
|
||||
H3) export AWG_H3="$value" ;;
|
||||
H4) export AWG_H4="$value" ;;
|
||||
I1) export AWG_I1="$value" ;;
|
||||
ListenPort) export AWG_PORT="$value" ;;
|
||||
Jc) _Jc="$value" ;;
|
||||
Jmin) _Jmin="$value" ;;
|
||||
Jmax) _Jmax="$value" ;;
|
||||
S1) _S1="$value" ;;
|
||||
S2) _S2="$value" ;;
|
||||
S3) _S3="$value" ;;
|
||||
S4) _S4="$value" ;;
|
||||
H1) _H1="$value" ;;
|
||||
H2) _H2="$value" ;;
|
||||
H3) _H3="$value" ;;
|
||||
H4) _H4="$value" ;;
|
||||
I1) _I1="$value" ;;
|
||||
ListenPort) _Port="$value" ;;
|
||||
esac
|
||||
fi
|
||||
done < "$conf"
|
||||
[[ -n "${AWG_Jc:-}" && -n "${AWG_H4:-}" ]]
|
||||
|
||||
# Atomic check: все 11 обязательных полей найдены?
|
||||
[[ -n "$_Jc" && -n "$_Jmin" && -n "$_Jmax" && \
|
||||
-n "$_S1" && -n "$_S2" && -n "$_S3" && -n "$_S4" && \
|
||||
-n "$_H1" && -n "$_H2" && -n "$_H3" && -n "$_H4" ]] || return 1
|
||||
|
||||
# Atomic export — окружение модифицируется только при полном успехе
|
||||
export AWG_Jc="$_Jc" AWG_Jmin="$_Jmin" AWG_Jmax="$_Jmax"
|
||||
export AWG_S1="$_S1" AWG_S2="$_S2" AWG_S3="$_S3" AWG_S4="$_S4"
|
||||
export AWG_H1="$_H1" AWG_H2="$_H2" AWG_H3="$_H3" AWG_H4="$_H4"
|
||||
[[ -n "$_I1" ]] && export AWG_I1="$_I1"
|
||||
[[ -n "$_Port" ]] && export AWG_PORT="$_Port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Загрузка AWG параметров.
|
||||
# Приоритет: live серверный конфиг (awg0.conf) > init-файл (awgsetup_cfg.init).
|
||||
# Это гарантирует что regen после ручной правки awg0.conf работает корректно (#38).
|
||||
#
|
||||
# Семантика источников (важно для предотвращения split-brain между сервером
|
||||
# и клиентскими конфигами, см. #38):
|
||||
#
|
||||
# * init-файл ($CONFIG_FILE = awgsetup_cfg.init) — для НЕ-AWG настроек
|
||||
# (OS_ID, ALLOWED_IPS, AWG_PORT, AWG_ENDPOINT и т.п.). Загружается всегда
|
||||
# если существует.
|
||||
# * Live server config ($SERVER_CONF_FILE = /etc/amnezia/amneziawg/awg0.conf)
|
||||
# — ЕДИНСТВЕННЫЙ источник истины для AWG протокольных параметров
|
||||
# (Jc/Jmin/Jmax/S1-S4/H1-H4/I1) когда файл существует.
|
||||
#
|
||||
# Если live server config существует но НЕ содержит полного набора AWG
|
||||
# параметров (повреждение / неполная ручная правка) — функция возвращает 1
|
||||
# с явной ошибкой. Молчаливый fallback на устаревшие значения из init-файла
|
||||
# создал бы split-brain: сервер живёт по новому awg0.conf, а regen выпускал
|
||||
# бы клиентам старые J*/S*/H*. Это именно тот класс проблем, который
|
||||
# elvaleto и Klavishnik сообщили в Discussion #38.
|
||||
#
|
||||
# Init-файл используется для AWG параметров ТОЛЬКО когда live server config
|
||||
# вообще отсутствует — это путь bootstrap первой установки, когда awg0.conf
|
||||
# ещё не записан, а generate_awg_params уже сохранил значения в init.
|
||||
load_awg_params() {
|
||||
# 1. Базовые параметры из init-файла (OS_ID, ALLOWED_IPS, AWG_PORT, AWG_ENDPOINT и т.п.)
|
||||
# 1. Базовые настройки из init (всегда, для не-AWG ключей)
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
safe_load_config "$CONFIG_FILE" || log_warn "Не удалось загрузить $CONFIG_FILE"
|
||||
fi
|
||||
# 2. AWG протокольные параметры — приоритет live awg0.conf (источник истины)
|
||||
if load_awg_params_from_server_conf; then
|
||||
|
||||
# 2. AWG протокольные параметры
|
||||
if [[ -f "$SERVER_CONF_FILE" ]]; then
|
||||
# Live config существует — он единственный источник истины.
|
||||
# Никакого fallback на init: иначе получим split-brain.
|
||||
if ! load_awg_params_from_server_conf; then
|
||||
log_error "В $SERVER_CONF_FILE отсутствуют обязательные AWG-параметры"
|
||||
log_error "(Jc/Jmin/Jmax/S1-S4/H1-H4). Не использую устаревшие значения"
|
||||
log_error "из $CONFIG_FILE, чтобы не создавать split-brain между сервером"
|
||||
log_error "и клиентскими конфигами. Восстановите [Interface] секцию в"
|
||||
log_error "$SERVER_CONF_FILE или восстановите awg0.conf из бэкапа."
|
||||
return 1
|
||||
fi
|
||||
log_debug "AWG параметры загружены из $SERVER_CONF_FILE (live config)"
|
||||
else
|
||||
log_debug "AWG параметры загружены из $CONFIG_FILE (init fallback)"
|
||||
# Bootstrap: server config ещё не существует (первая установка).
|
||||
# AWG_* должны быть в env через safe_load_config выше.
|
||||
log_debug "$SERVER_CONF_FILE не существует — использую AWG params из $CONFIG_FILE (bootstrap)"
|
||||
fi
|
||||
# Проверка обязательных AWG 2.0 параметров
|
||||
|
||||
# 3. Проверка обязательных AWG 2.0 параметров
|
||||
local missing=0
|
||||
local param
|
||||
for param in AWG_Jc AWG_Jmin AWG_Jmax AWG_S1 AWG_S2 AWG_S3 AWG_S4 AWG_H1 AWG_H2 AWG_H3 AWG_H4; do
|
||||
if [[ -z "${!param:-}" ]]; then
|
||||
log_error "Параметр $param не найден ни в $SERVER_CONF_FILE, ни в $CONFIG_FILE"
|
||||
log_error "Параметр $param не найден"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
|
@ -501,6 +576,7 @@ apply_config() {
|
|||
}
|
||||
log_debug "Конфигурация применена (syncconf)."
|
||||
exec {apply_fd}>&-
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
|
|
|
|||
141
awg_common_en.sh
141
awg_common_en.sh
|
|
@ -101,17 +101,38 @@ rand_range() {
|
|||
# Minimum width per range = 1000.
|
||||
# Prints 4 "low-high" lines to stdout. Returns 1 on failure.
|
||||
# Mitigates Russian DPI fingerprinting of static H values (#38).
|
||||
#
|
||||
# Optimization: a single `od -N32 -tu4` call reads 32 bytes = 8 uint32
|
||||
# values in one operation, instead of 8 separate subprocess calls via
|
||||
# rand_range. Falls back to rand_range if /dev/urandom is unavailable.
|
||||
generate_awg_h_ranges() {
|
||||
local attempt=0 max_attempts=20
|
||||
while (( attempt < max_attempts )); do
|
||||
local p1 p2 p3 p4 p5 p6 p7 p8 sorted
|
||||
p1=$(rand_range 0 4294967295); p2=$(rand_range 0 4294967295)
|
||||
p3=$(rand_range 0 4294967295); p4=$(rand_range 0 4294967295)
|
||||
p5=$(rand_range 0 4294967295); p6=$(rand_range 0 4294967295)
|
||||
p7=$(rand_range 0 4294967295); p8=$(rand_range 0 4294967295)
|
||||
sorted=$(printf '%s\n' "$p1" "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" | sort -n)
|
||||
local arr=()
|
||||
while IFS= read -r _line; do arr+=("$_line"); done <<< "$sorted"
|
||||
local raw arr=() _v
|
||||
# One 32-byte read from /dev/urandom = 8 uint32 values
|
||||
raw=$(od -An -N32 -tu4 /dev/urandom 2>/dev/null | tr -s ' \n' '\n' | sed '/^$/d')
|
||||
if [[ -n "$raw" ]]; then
|
||||
local count=0
|
||||
while IFS= read -r _v; do
|
||||
[[ "$_v" =~ ^[0-9]+$ ]] || continue
|
||||
arr+=("$_v"); count=$((count + 1))
|
||||
(( count == 8 )) && break
|
||||
done <<< "$raw"
|
||||
fi
|
||||
# Fallback: 8 separate rand_range calls (if urandom unavailable)
|
||||
if (( ${#arr[@]} != 8 )); then
|
||||
arr=()
|
||||
local _i
|
||||
for _i in 1 2 3 4 5 6 7 8; do
|
||||
arr+=("$(rand_range 0 4294967295)")
|
||||
done
|
||||
fi
|
||||
# Sort
|
||||
local sorted
|
||||
sorted=$(printf '%s\n' "${arr[@]}" | sort -n)
|
||||
arr=()
|
||||
while IFS= read -r _v; do arr+=("$_v"); done <<< "$sorted"
|
||||
# Minimum width per pair
|
||||
if (( ${arr[1]} - ${arr[0]} >= 1000 )) && \
|
||||
(( ${arr[3]} - ${arr[2]} >= 1000 )) && \
|
||||
(( ${arr[5]} - ${arr[4]} >= 1000 )) && \
|
||||
|
|
@ -165,8 +186,11 @@ safe_load_config() {
|
|||
}
|
||||
|
||||
# Parser for the live AmneziaWG server config (source of truth for AWG_*).
|
||||
# Reads the [Interface] section of awg0.conf and exports AWG_* variables.
|
||||
# Returns 0 if at least Jc and H4 were found, otherwise 1.
|
||||
# Reads the [Interface] section of awg0.conf and exports AWG_* variables
|
||||
# ATOMICALLY: either all 11 required parameters (Jc/Jmin/Jmax/S1-S4/H1-H4)
|
||||
# are found and exported, or nothing changes in the environment and 1
|
||||
# is returned. Protects against mixed state when awg0.conf is partially
|
||||
# corrupt. I1, ListenPort are optional — exported only if found.
|
||||
# Fixes #38: regen used stale values from the init file instead of the
|
||||
# actual awg0.conf after manual edits.
|
||||
# shellcheck disable=SC2120 # Optional argument is only used in tests
|
||||
|
|
@ -174,6 +198,12 @@ load_awg_params_from_server_conf() {
|
|||
local conf="${1:-$SERVER_CONF_FILE}"
|
||||
[[ -f "$conf" ]] || return 1
|
||||
|
||||
# Local accumulation — all-or-nothing export at the end
|
||||
local _Jc="" _Jmin="" _Jmax=""
|
||||
local _S1="" _S2="" _S3="" _S4=""
|
||||
local _H1="" _H2="" _H3="" _H4=""
|
||||
local _I1="" _Port=""
|
||||
|
||||
local in_iface=0 line key value
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^\[Interface\] ]]; then in_iface=1; continue; fi
|
||||
|
|
@ -187,45 +217,91 @@ load_awg_params_from_server_conf() {
|
|||
value="${BASH_REMATCH[2]}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
case "$key" in
|
||||
Jc) export AWG_Jc="$value" ;;
|
||||
Jmin) export AWG_Jmin="$value" ;;
|
||||
Jmax) export AWG_Jmax="$value" ;;
|
||||
S1) export AWG_S1="$value" ;;
|
||||
S2) export AWG_S2="$value" ;;
|
||||
S3) export AWG_S3="$value" ;;
|
||||
S4) export AWG_S4="$value" ;;
|
||||
H1) export AWG_H1="$value" ;;
|
||||
H2) export AWG_H2="$value" ;;
|
||||
H3) export AWG_H3="$value" ;;
|
||||
H4) export AWG_H4="$value" ;;
|
||||
I1) export AWG_I1="$value" ;;
|
||||
ListenPort) export AWG_PORT="$value" ;;
|
||||
Jc) _Jc="$value" ;;
|
||||
Jmin) _Jmin="$value" ;;
|
||||
Jmax) _Jmax="$value" ;;
|
||||
S1) _S1="$value" ;;
|
||||
S2) _S2="$value" ;;
|
||||
S3) _S3="$value" ;;
|
||||
S4) _S4="$value" ;;
|
||||
H1) _H1="$value" ;;
|
||||
H2) _H2="$value" ;;
|
||||
H3) _H3="$value" ;;
|
||||
H4) _H4="$value" ;;
|
||||
I1) _I1="$value" ;;
|
||||
ListenPort) _Port="$value" ;;
|
||||
esac
|
||||
fi
|
||||
done < "$conf"
|
||||
[[ -n "${AWG_Jc:-}" && -n "${AWG_H4:-}" ]]
|
||||
|
||||
# Atomic check: are all 11 required fields present?
|
||||
[[ -n "$_Jc" && -n "$_Jmin" && -n "$_Jmax" && \
|
||||
-n "$_S1" && -n "$_S2" && -n "$_S3" && -n "$_S4" && \
|
||||
-n "$_H1" && -n "$_H2" && -n "$_H3" && -n "$_H4" ]] || return 1
|
||||
|
||||
# Atomic export — environment is modified only on full success
|
||||
export AWG_Jc="$_Jc" AWG_Jmin="$_Jmin" AWG_Jmax="$_Jmax"
|
||||
export AWG_S1="$_S1" AWG_S2="$_S2" AWG_S3="$_S3" AWG_S4="$_S4"
|
||||
export AWG_H1="$_H1" AWG_H2="$_H2" AWG_H3="$_H3" AWG_H4="$_H4"
|
||||
[[ -n "$_I1" ]] && export AWG_I1="$_I1"
|
||||
[[ -n "$_Port" ]] && export AWG_PORT="$_Port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Load AWG parameters.
|
||||
# Priority: live server config (awg0.conf) > init file (awgsetup_cfg.init).
|
||||
# This ensures regen after manual awg0.conf edits works correctly (#38).
|
||||
#
|
||||
# Source semantics (important for preventing split-brain between server
|
||||
# and client configs, see #38):
|
||||
#
|
||||
# * init file ($CONFIG_FILE = awgsetup_cfg.init) — for NON-AWG settings
|
||||
# (OS_ID, ALLOWED_IPS, AWG_PORT, AWG_ENDPOINT etc.). Always loaded
|
||||
# when present.
|
||||
# * Live server config ($SERVER_CONF_FILE = /etc/amnezia/amneziawg/awg0.conf)
|
||||
# — the SOLE source of truth for AWG protocol parameters
|
||||
# (Jc/Jmin/Jmax/S1-S4/H1-H4/I1) when the file exists.
|
||||
#
|
||||
# If the live server config exists but does NOT contain a complete set of
|
||||
# AWG parameters (corruption / incomplete manual edit) — the function
|
||||
# returns 1 with an explicit error. Silently falling back to stale values
|
||||
# from the init file would create split-brain: the server runs the new
|
||||
# awg0.conf while regen would issue clients old J*/S*/H*. This is exactly
|
||||
# the class of issue reported by elvaleto and Klavishnik in Discussion #38.
|
||||
#
|
||||
# The init file is used for AWG parameters ONLY when the live server
|
||||
# config is missing entirely — that is the bootstrap path of the first
|
||||
# install when awg0.conf has not been written yet but generate_awg_params
|
||||
# has already stored values in the init file.
|
||||
load_awg_params() {
|
||||
# 1. Base parameters from init file (OS_ID, ALLOWED_IPS, AWG_PORT, AWG_ENDPOINT etc.)
|
||||
# 1. Base settings from init (always, for non-AWG keys)
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
safe_load_config "$CONFIG_FILE" || log_warn "Failed to load $CONFIG_FILE"
|
||||
fi
|
||||
# 2. AWG protocol parameters — live awg0.conf takes priority (source of truth)
|
||||
if load_awg_params_from_server_conf; then
|
||||
|
||||
# 2. AWG protocol parameters
|
||||
if [[ -f "$SERVER_CONF_FILE" ]]; then
|
||||
# Live config exists — it is the sole source of truth.
|
||||
# No fallback to init: that would create split-brain.
|
||||
if ! load_awg_params_from_server_conf; then
|
||||
log_error "$SERVER_CONF_FILE is missing required AWG parameters"
|
||||
log_error "(Jc/Jmin/Jmax/S1-S4/H1-H4). Refusing to use stale values from"
|
||||
log_error "$CONFIG_FILE, that would create a split-brain between server"
|
||||
log_error "and client configs. Restore the [Interface] section in"
|
||||
log_error "$SERVER_CONF_FILE or restore awg0.conf from a backup."
|
||||
return 1
|
||||
fi
|
||||
log_debug "AWG parameters loaded from $SERVER_CONF_FILE (live config)"
|
||||
else
|
||||
log_debug "AWG parameters loaded from $CONFIG_FILE (init fallback)"
|
||||
# Bootstrap: server config does not exist yet (first install).
|
||||
# AWG_* must be in env via safe_load_config above.
|
||||
log_debug "$SERVER_CONF_FILE missing — using AWG params from $CONFIG_FILE (bootstrap)"
|
||||
fi
|
||||
# Check required AWG 2.0 parameters
|
||||
|
||||
# 3. Check required AWG 2.0 parameters
|
||||
local missing=0
|
||||
local param
|
||||
for param in AWG_Jc AWG_Jmin AWG_Jmax AWG_S1 AWG_S2 AWG_S3 AWG_S4 AWG_H1 AWG_H2 AWG_H3 AWG_H4; do
|
||||
if [[ -z "${!param:-}" ]]; then
|
||||
log_error "Parameter $param not found in $SERVER_CONF_FILE or $CONFIG_FILE"
|
||||
log_error "Parameter $param not found"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
|
@ -500,6 +576,7 @@ apply_config() {
|
|||
}
|
||||
log_debug "Config applied (syncconf)."
|
||||
exec {apply_fd}>&-
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
|
|
|
|||
|
|
@ -485,19 +485,35 @@ rand_range() {
|
|||
# Сортировка гарантирует low ≤ high и непересечение между парами.
|
||||
# Минимальная ширина каждого диапазона = 1000 (для нормальной обфускации).
|
||||
# Печатает 4 строки формата "low-high" в stdout.
|
||||
# Возвращает 1 если за 20 попыток не удалось получить корректные диапазоны
|
||||
# (вероятность близка к нулю при равномерном uint32 распределении).
|
||||
# Возвращает 1 если за 20 попыток не удалось получить корректные диапазоны.
|
||||
#
|
||||
# Оптимизация: один вызов `od -N32 -tu4` читает 32 байта = 8 uint32 значений
|
||||
# одной операцией, вместо 8 отдельных subprocess через rand_range.
|
||||
# Fallback на rand_range если /dev/urandom недоступен.
|
||||
generate_awg_h_ranges() {
|
||||
local attempt=0 max_attempts=20
|
||||
while (( attempt < max_attempts )); do
|
||||
local p1 p2 p3 p4 p5 p6 p7 p8 sorted
|
||||
p1=$(rand_range 0 4294967295); p2=$(rand_range 0 4294967295)
|
||||
p3=$(rand_range 0 4294967295); p4=$(rand_range 0 4294967295)
|
||||
p5=$(rand_range 0 4294967295); p6=$(rand_range 0 4294967295)
|
||||
p7=$(rand_range 0 4294967295); p8=$(rand_range 0 4294967295)
|
||||
sorted=$(printf '%s\n' "$p1" "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" | sort -n)
|
||||
local arr=()
|
||||
while IFS= read -r _line; do arr+=("$_line"); done <<< "$sorted"
|
||||
local raw arr=() _v
|
||||
raw=$(od -An -N32 -tu4 /dev/urandom 2>/dev/null | tr -s ' \n' '\n' | sed '/^$/d')
|
||||
if [[ -n "$raw" ]]; then
|
||||
local count=0
|
||||
while IFS= read -r _v; do
|
||||
[[ "$_v" =~ ^[0-9]+$ ]] || continue
|
||||
arr+=("$_v"); count=$((count + 1))
|
||||
(( count == 8 )) && break
|
||||
done <<< "$raw"
|
||||
fi
|
||||
if (( ${#arr[@]} != 8 )); then
|
||||
arr=()
|
||||
local _i
|
||||
for _i in 1 2 3 4 5 6 7 8; do
|
||||
arr+=("$(rand_range 0 4294967295)")
|
||||
done
|
||||
fi
|
||||
local sorted
|
||||
sorted=$(printf '%s\n' "${arr[@]}" | sort -n)
|
||||
arr=()
|
||||
while IFS= read -r _v; do arr+=("$_v"); done <<< "$sorted"
|
||||
if (( ${arr[1]} - ${arr[0]} >= 1000 )) && \
|
||||
(( ${arr[3]} - ${arr[2]} >= 1000 )) && \
|
||||
(( ${arr[5]} - ${arr[4]} >= 1000 )) && \
|
||||
|
|
|
|||
|
|
@ -484,20 +484,36 @@ rand_range() {
|
|||
# Algorithm: 8 random uint32 → sort → 4 (low, high) pairs.
|
||||
# Sorting guarantees low ≤ high and non-overlap between pairs.
|
||||
# Minimum width per range = 1000 (for proper obfuscation).
|
||||
# Prints 4 "low-high" lines to stdout. Returns 1 on failure
|
||||
# (probability is essentially zero with uniform uint32 distribution).
|
||||
# Prints 4 "low-high" lines to stdout. Returns 1 on failure.
|
||||
# Mitigates Russian DPI fingerprinting of static H values (#38).
|
||||
#
|
||||
# Optimization: a single `od -N32 -tu4` call reads 32 bytes = 8 uint32
|
||||
# values in one operation, instead of 8 separate subprocess calls via
|
||||
# rand_range. Falls back to rand_range if /dev/urandom is unavailable.
|
||||
generate_awg_h_ranges() {
|
||||
local attempt=0 max_attempts=20
|
||||
while (( attempt < max_attempts )); do
|
||||
local p1 p2 p3 p4 p5 p6 p7 p8 sorted
|
||||
p1=$(rand_range 0 4294967295); p2=$(rand_range 0 4294967295)
|
||||
p3=$(rand_range 0 4294967295); p4=$(rand_range 0 4294967295)
|
||||
p5=$(rand_range 0 4294967295); p6=$(rand_range 0 4294967295)
|
||||
p7=$(rand_range 0 4294967295); p8=$(rand_range 0 4294967295)
|
||||
sorted=$(printf '%s\n' "$p1" "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" | sort -n)
|
||||
local arr=()
|
||||
while IFS= read -r _line; do arr+=("$_line"); done <<< "$sorted"
|
||||
local raw arr=() _v
|
||||
raw=$(od -An -N32 -tu4 /dev/urandom 2>/dev/null | tr -s ' \n' '\n' | sed '/^$/d')
|
||||
if [[ -n "$raw" ]]; then
|
||||
local count=0
|
||||
while IFS= read -r _v; do
|
||||
[[ "$_v" =~ ^[0-9]+$ ]] || continue
|
||||
arr+=("$_v"); count=$((count + 1))
|
||||
(( count == 8 )) && break
|
||||
done <<< "$raw"
|
||||
fi
|
||||
if (( ${#arr[@]} != 8 )); then
|
||||
arr=()
|
||||
local _i
|
||||
for _i in 1 2 3 4 5 6 7 8; do
|
||||
arr+=("$(rand_range 0 4294967295)")
|
||||
done
|
||||
fi
|
||||
local sorted
|
||||
sorted=$(printf '%s\n' "${arr[@]}" | sort -n)
|
||||
arr=()
|
||||
while IFS= read -r _v; do arr+=("$_v"); done <<< "$sorted"
|
||||
if (( ${arr[1]} - ${arr[0]} >= 1000 )) && \
|
||||
(( ${arr[3]} - ${arr[2]} >= 1000 )) && \
|
||||
(( ${arr[5]} - ${arr[4]} >= 1000 )) && \
|
||||
|
|
|
|||
|
|
@ -122,3 +122,74 @@ CONF
|
|||
run load_awg_params
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "load_awg_params: split-brain prevention — corrupt server_conf returns 1 even if init has values" {
|
||||
# The exact scenario from the audit Findings.md #1:
|
||||
# init file has GOOD (stale) values, server_conf exists but is missing
|
||||
# required H4. Old behavior would silently fall back to init and pretend
|
||||
# success — server runs new config, regen would emit clients old values.
|
||||
# New behavior: error, return 1, no split-brain possible.
|
||||
create_init_config
|
||||
cat > "$SERVER_CONF_FILE" << 'CONF'
|
||||
[Interface]
|
||||
PrivateKey = TESTKEY
|
||||
Address = 10.9.9.1/24
|
||||
ListenPort = 39743
|
||||
Jc = 99
|
||||
Jmin = 200
|
||||
Jmax = 999
|
||||
S1 = 1
|
||||
S2 = 2
|
||||
S3 = 3
|
||||
S4 = 4
|
||||
H1 = 500-1500
|
||||
H2 = 2000-3000
|
||||
H3 = 4000-5000
|
||||
CONF
|
||||
# H4 deliberately missing
|
||||
run load_awg_params
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "load_awg_params: split-brain prevention — server_conf missing falls back to init (bootstrap)" {
|
||||
# Counterpart to the previous test: when server_conf is missing entirely,
|
||||
# init fallback IS allowed — this is the bootstrap path of first install.
|
||||
create_init_config
|
||||
rm -f "$SERVER_CONF_FILE"
|
||||
run load_awg_params
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "load_awg_params_from_server_conf: atomic — partial corrupt config does not pollute env" {
|
||||
# Init file has GOOD values
|
||||
create_init_config
|
||||
safe_load_config "$CONFIG_FILE"
|
||||
# Sanity: init values loaded
|
||||
[ "$AWG_Jc" = "6" ]
|
||||
[ "$AWG_H4" = "100000000-800000000" ]
|
||||
|
||||
# Now write a CORRUPT server config — missing H2, H3, H4
|
||||
cat > "$SERVER_CONF_FILE" << 'CONF'
|
||||
[Interface]
|
||||
PrivateKey = TESTKEY
|
||||
Address = 10.9.9.1/24
|
||||
ListenPort = 39743
|
||||
Jc = 99
|
||||
Jmin = 200
|
||||
Jmax = 999
|
||||
S1 = 1
|
||||
S2 = 2
|
||||
S3 = 3
|
||||
S4 = 4
|
||||
H1 = 500-1500
|
||||
CONF
|
||||
# Atomic: should return 1 because H2-H4 missing
|
||||
run load_awg_params_from_server_conf "$SERVER_CONF_FILE"
|
||||
[ "$status" -eq 1 ]
|
||||
|
||||
# CRITICAL: env must NOT be partially polluted with new Jc/H1
|
||||
# The init values must remain intact
|
||||
[ "$AWG_Jc" = "6" ]
|
||||
[ "$AWG_H1" = "100000-800000" ]
|
||||
[ "$AWG_H4" = "100000000-800000000" ]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue