From 28081faa3589dc52326ad852c0b9b01004fca9e5 Mon Sep 17 00:00:00 2001 From: Karen Khachatryan Date: Sun, 23 Aug 2026 20:13:24 +0300 Subject: [PATCH] Add AmneziaWG 3.1 support and link diversification - add AWG 3.x rekey, handshake, keepalive, trailer and cookie options - generate deterministic directional Jc/Jmin/Jmax and I1-I5 signatures - keep S1-S4 and H1-H4 shared between both ends of a link - derive stable runtime parameter ranges for infra and access tunnels - add optional HeaderProtectionKey and ContentPaddingAddition support - disable automatic header protection until the upstream kernel issue is fixed - diversify Babel hello/update intervals per local tunnel direction - use a consistent tunnel MTU profile: 1400 for AWG/WG and 1380 for IPIP - propagate AWG 3.1 options through mesh, exit, access, client and server configs - validate AWG 3.x ranges, header protection constraints and generated tunnel configs - update README examples and AWG 3.1 documentation --- README.md | 41 +++- tools/awg_model.py | 380 +++++++++++++++++++++++++++---- tools/common.py | 4 + tools/config_builder.py | 6 +- tools/config_model.py | 13 +- tools/default.py | 58 +++-- tools/generate.py | 26 ++- tools/generate_access.py | 6 +- tools/generate_clients.py | 2 +- tools/generate_exit.py | 10 +- tools/generate_mesh.py | 15 +- tools/generate_network.py | 5 +- tools/generate_server_runtime.py | 15 +- tools/show_unmanaged_collect.py | 6 +- tools/stable_model.py | 40 ++++ tools/validate_access.py | 39 ++-- tools/validate_awg_helpers.py | 17 +- tools/validate_runtime_files.py | 22 +- tools/validate_tunnel_files.py | 23 +- 19 files changed, 593 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 427718d..e229916 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ OpenWrt firmware образы собираются отдельной коман ./build_router_images.py ``` -Проект рассчитан на OpenWrt 25.12+ с `apk` based ImageBuilder и AWG2 пакетами. +Проект рассчитан на OpenWrt 25.12+ с `apk` based ImageBuilder и AmneziaWG 3.1 пакетами. > Текущий `config.json` - демонстрационный пример. Адреса из `203.0.113.0/24` и `198.51.100.0/24` нужно заменить на реальные адреса своей сети перед деплоем. @@ -513,7 +513,7 @@ Source сетью будет subnet access группы, а target роутер Access `port` не должен попадать в `INFRA_AWG_PORT_RANGE`, потому что этот диапазон принадлежит generated infra/exit tunnel ports. -Для `protocol: "amneziawg"` параметры AWG можно задать прямо в access группе. Готовый пример, чтобы был под рукой: +Для `protocol: "amneziawg"` параметры AWG можно задать прямо в access группе. AWG 3.0/3.1 поля поддерживают диапазоны, поэтому полный пример выглядит так: ```json "awg": { @@ -532,11 +532,21 @@ Access `port` не должен попадать в `INFRA_AWG_PORT_RANGE`, по "i2": "", "i3": "", "i4": "", - "i5": "" + "i5": "", + "rekey_after_time": "116-142", + "rekey_timeout": "4-7", + "reject_after_time": "190-217", + "keepalive_timeout": "10-15", + "max_handshake_attempts": "15-21", + "random_trailers": true, + "disable_cookies": false, + "persistent_keepalive": "20-26" } ``` -Этот блок вставляется рядом с `name`, `protocol`, `port`, `subnet` и `users` соответствующей AmneziaWG access группы. +Этот блок вставляется рядом с `name`, `protocol`, `port`, `subnet` и `users` соответствующей AmneziaWG access группы. Если новые AWG 3.x поля не указаны явно, builder детерминированно выводит их из access group key. + +`HeaderProtectionKey` и `ContentPaddingAddition` также поддерживаются как `header_protection_key` и `content_padding_addition`, но в текущем infra профиле не включаются: header protection по умолчанию отключен, а `ContentPaddingAddition` оставлен пустым, чтобы transport padding делал `RandomTrailers`. ## Быстрый старт @@ -608,6 +618,27 @@ Reverse exit без `listen_ip` не принимает входящие tunnel На infra линках не включается обычный default route. Они используются как транспорт для Babel и служебной маршрутизации. +### AWG 3.1 diversification + +Infra AWG профиль генерируется детерминированно, но разделен на shared per-link и local per-direction параметры. Это дает воспроизводимые конфиги без одинакового fingerprint у всех направлений. + +| Параметры | Модель | +| --- | --- | +| `S1-S4`, `H1-H4` | shared per-link: одинаковы на обоих концах одного линка | +| `Jc/Jmin/Jmax` | per-direction: `A -> B` и `B -> A` получают разные значения | +| `I1-I5` | per-direction; генерируется 1-2 signature packets размером 64-256 bytes | +| `RekeyAfterTime`, `RekeyTimeout`, `RejectAfterTime` | per-direction ranges | +| `KeepaliveTimeout`, `MaxHandshakeAttempts` | per-direction ranges | +| `PersistentKeepalive` | per-direction range внутри 20-30 seconds | +| `RandomTrailers` | включен | +| `DisableCookies` | выключен | +| `ContentPaddingAddition` | не задается | +| `HeaderProtectionKey` | поддерживается моделью, но auto-generation сейчас выключен | + +Babel timers также диверсифицированы per local tunnel direction: `hello_interval` выбирается в диапазоне 2-4 seconds, а `update_interval` - 8-14 seconds с привязкой к hello interval. + +Для всех generated tunnel interfaces используется единый MTU profile: AWG/WG `1400`, IPIP `1380`. Физические Ethernet/PPPoE MTU builder этим не меняет. + ## Служебная адресация Основные пулы задаются в `tools/default.py`. @@ -1545,7 +1576,7 @@ Profile name является безопасным ASCII identifier. 1. читает и валидирует `config.json` 1. создает `routers/` из `routers/example` -1. скачивает AWG2 `.apk`, если не указан `--skip-awg-download` +1. скачивает AmneziaWG `.apk`, если не указан `--skip-awg-download` 1. скачивает `libcares` из `c-ares-openwrt-package`, если не указан `--skip-cares-download` 1. синхронизирует per-router `packages/`, если не указан `--skip-package-sync` 1. синхронизирует шаблонные файлы из `routers/example` diff --git a/tools/awg_model.py b/tools/awg_model.py index 510caac..e9ff297 100755 --- a/tools/awg_model.py +++ b/tools/awg_model.py @@ -2,6 +2,9 @@ import sys sys.dont_write_bytecode = True +import base64 +import hashlib +import hmac import random import re @@ -26,11 +29,11 @@ try: AWG_INFRA_AUTO_S3_MIN, AWG_INFRA_AUTO_S4_MAX, AWG_INFRA_AUTO_S4_MIN, - AWG_INFRA_I1, - AWG_INFRA_I2, - AWG_INFRA_I3, - AWG_INFRA_I4, - AWG_INFRA_I5, + AWG_INFRA_SIGNATURE_COUNT_MAX, + AWG_INFRA_SIGNATURE_COUNT_MIN, + AWG_INFRA_SIGNATURE_SIZE_MAX, + AWG_INFRA_SIGNATURE_SIZE_MIN, + AWG_INFRA_SIGNATURE_TAGS, AWG_JC_MAX, AWG_JC_MIN, AWG_JUNK_SIZE_MAX, @@ -70,11 +73,11 @@ except ImportError: AWG_INFRA_AUTO_S3_MIN, AWG_INFRA_AUTO_S4_MAX, AWG_INFRA_AUTO_S4_MIN, - AWG_INFRA_I1, - AWG_INFRA_I2, - AWG_INFRA_I3, - AWG_INFRA_I4, - AWG_INFRA_I5, + AWG_INFRA_SIGNATURE_COUNT_MAX, + AWG_INFRA_SIGNATURE_COUNT_MIN, + AWG_INFRA_SIGNATURE_SIZE_MAX, + AWG_INFRA_SIGNATURE_SIZE_MIN, + AWG_INFRA_SIGNATURE_TAGS, AWG_JC_MAX, AWG_JC_MIN, AWG_JUNK_SIZE_MAX, @@ -94,6 +97,51 @@ except ImportError: from process import die # type: ignore from stable_model import random_free_slots, stable_seed_u64 # type: ignore +try: + from .default import ( + AWG_CONTENT_PADDING_ADDITION, + AWG_DISABLE_COOKIES, + AWG_HEADER_PROTECTION_ENABLED, + AWG_KEEPALIVE_TIMEOUT_MAX, + AWG_KEEPALIVE_TIMEOUT_MIN, + AWG_MAX_HANDSHAKE_ATTEMPTS_MAX, + AWG_MAX_HANDSHAKE_ATTEMPTS_MIN, + AWG_PERSISTENT_KEEPALIVE_MAX, + AWG_PERSISTENT_KEEPALIVE_MIN, + AWG_RANDOM_TRAILERS, + AWG_REJECT_AFTER_TIME_MAX, + AWG_REJECT_AFTER_TIME_MIN, + AWG_REKEY_AFTER_TIME_MAX, + AWG_REKEY_AFTER_TIME_MIN, + AWG_REKEY_TIMEOUT_MAX, + AWG_REKEY_TIMEOUT_MIN, + CONFIG_KEY_MATERIALS_KEY_PATH, + CONFIG_PATH, + ) + from .secrets import master_key +except ImportError: + from default import ( # type: ignore + AWG_CONTENT_PADDING_ADDITION, + AWG_DISABLE_COOKIES, + AWG_HEADER_PROTECTION_ENABLED, + AWG_KEEPALIVE_TIMEOUT_MAX, + AWG_KEEPALIVE_TIMEOUT_MIN, + AWG_MAX_HANDSHAKE_ATTEMPTS_MAX, + AWG_MAX_HANDSHAKE_ATTEMPTS_MIN, + AWG_PERSISTENT_KEEPALIVE_MAX, + AWG_PERSISTENT_KEEPALIVE_MIN, + AWG_RANDOM_TRAILERS, + AWG_REJECT_AFTER_TIME_MAX, + AWG_REJECT_AFTER_TIME_MIN, + AWG_REKEY_AFTER_TIME_MAX, + AWG_REKEY_AFTER_TIME_MIN, + AWG_REKEY_TIMEOUT_MAX, + AWG_REKEY_TIMEOUT_MIN, + CONFIG_KEY_MATERIALS_KEY_PATH, + CONFIG_PATH, + ) + from secrets import master_key # type: ignore + AWG_KEYS = { "jc", @@ -112,6 +160,16 @@ AWG_KEYS = { "i3", "i4", "i5", + "header_protection_key", + "content_padding_addition", + "rekey_after_time", + "rekey_timeout", + "reject_after_time", + "keepalive_timeout", + "max_handshake_attempts", + "random_trailers", + "disable_cookies", + "persistent_keepalive", } @@ -214,9 +272,54 @@ def validate_awg_runtime_ranges(awg: AwgOptions, where: str) -> None: ) +def _parse_u16_range( + value: str, where: str, *, allow_empty: bool = False +) -> tuple[int, int] | None: + value = value.strip() + if not value: + if allow_empty: + return None + die(f"{where} must not be empty") + m = re.fullmatch(r"(\d+)(?:-(\d+))?", value) + if not m: + die(f"{where} must be N or N-M") + start = int(m.group(1)) + end = int(m.group(2) or m.group(1)) + if start < 0 or end > 65535 or start > end: + die(f"{where} must be within 0..65535 and start <= end") + return start, end + + +def validate_awg_v3_options(awg: AwgOptions, where: str) -> None: + for key in ( + "rekey_after_time", + "rekey_timeout", + "reject_after_time", + "keepalive_timeout", + "max_handshake_attempts", + "persistent_keepalive", + ): + _parse_u16_range(getattr(awg, key), f"{where}.{key}") + _parse_u16_range( + awg.content_padding_addition, + f"{where}.content_padding_addition", + allow_empty=True, + ) + if awg.header_protection_key: + try: + decoded = base64.b64decode(awg.header_protection_key, validate=True) + except Exception: + die(f"{where}.header_protection_key must be base64") + if len(decoded) != 32: + die(f"{where}.header_protection_key must decode to 32 bytes") + if min(awg.s1, awg.s2, awg.s3, awg.s4) < 12: + die(f"{where}: HeaderProtectionKey requires S1-S4 >= 12") + + def validate_awg_options(awg: AwgOptions, where: str) -> None: validate_awg_runtime_ranges(awg, where) validate_awg_h_ranges(awg, where) + validate_awg_v3_options(awg, where) def validate_awg_auto_ranges() -> None: @@ -256,25 +359,61 @@ def validate_awg_auto_ranges() -> None: die("bad AWG_INFRA_AUTO_S4_MIN/AWG_INFRA_AUTO_S4_MAX") -def stable_awg_runtime_params( - link_key: str, -) -> tuple[int, int, int, int, int, int, int]: - # Derive per-link AWG runtime parameters from the same stable link key - # family as ports/link addresses/H-ranges. This keeps generation - # deterministic without forcing identical AWG fingerprints on all links. +def stable_awg_shared_runtime_params(link_key: str) -> tuple[int, int, int, int]: + """Parameters that must describe the same wire format on both ends.""" validate_awg_auto_ranges() + # Preserve the S1-S4 values produced by the original builder. + # It used one RNG for J and S, so consume the legacy J draws before + # deriving S. Only J moves to a directional seed. rng = random.Random(stable_seed_u64(f"awg-runtime:{link_key}")) - - jc = rng.randint(AWG_INFRA_AUTO_JC_MIN, AWG_INFRA_AUTO_JC_MAX) - j_left = rng.randint(AWG_INFRA_AUTO_JUNK_SIZE_MIN, AWG_INFRA_AUTO_JUNK_SIZE_MAX) - j_right = rng.randint(AWG_INFRA_AUTO_JUNK_SIZE_MIN, AWG_INFRA_AUTO_JUNK_SIZE_MAX) - jmin, jmax = sorted((j_left, j_right)) - + rng.randint(AWG_INFRA_AUTO_JC_MIN, AWG_INFRA_AUTO_JC_MAX) + rng.randint(AWG_INFRA_AUTO_JUNK_SIZE_MIN, AWG_INFRA_AUTO_JUNK_SIZE_MAX) + rng.randint(AWG_INFRA_AUTO_JUNK_SIZE_MIN, AWG_INFRA_AUTO_JUNK_SIZE_MAX) s1 = rng.randint(AWG_INFRA_AUTO_S1_MIN, AWG_INFRA_AUTO_S1_MAX) s2 = rng.randint(AWG_INFRA_AUTO_S2_MIN, AWG_INFRA_AUTO_S2_MAX) s3 = rng.randint(AWG_INFRA_AUTO_S3_MIN, AWG_INFRA_AUTO_S3_MAX) s4 = rng.randint(AWG_INFRA_AUTO_S4_MIN, AWG_INFRA_AUTO_S4_MAX) + return s1, s2, s3, s4 + +def stable_awg_directional_runtime_params( + link_key: str, src: str, dst: str +) -> tuple[int, int, int]: + """Local/send-side junk parameters; intentionally differ by direction.""" + validate_awg_auto_ranges() + direction = f"{link_key}:{src}->{dst}" + rng = random.Random(stable_seed_u64(f"awg-direction-runtime:v1:{direction}")) + jc = rng.randint(AWG_INFRA_AUTO_JC_MIN, AWG_INFRA_AUTO_JC_MAX) + j_left = rng.randint(AWG_INFRA_AUTO_JUNK_SIZE_MIN, AWG_INFRA_AUTO_JUNK_SIZE_MAX) + j_right = rng.randint(AWG_INFRA_AUTO_JUNK_SIZE_MIN, AWG_INFRA_AUTO_JUNK_SIZE_MAX) + jmin, jmax = sorted((j_left, j_right)) + return jc, jmin, jmax + + +def stable_awg_signature_packets( + link_key: str, src: str, dst: str +) -> tuple[str, str, str, str, str]: + direction = f"{link_key}:{src}->{dst}" + rng = random.Random(stable_seed_u64(f"awg-signatures:v1:{direction}")) + count = rng.randint(AWG_INFRA_SIGNATURE_COUNT_MIN, AWG_INFRA_SIGNATURE_COUNT_MAX) + values: list[str] = [] + for _ in range(count): + tag = rng.choice(AWG_INFRA_SIGNATURE_TAGS) + size = rng.randint(AWG_INFRA_SIGNATURE_SIZE_MIN, AWG_INFRA_SIGNATURE_SIZE_MAX) + values.append(f"<{tag} {size}>") + values.extend([""] * (5 - len(values))) + return values[0], values[1], values[2], values[3], values[4] + + +def stable_awg_runtime_params( + link_key: str, src: str | None = None, dst: str | None = None +) -> tuple[int, int, int, int, int, int, int]: + # Compatibility helper: callers that do not provide a direction get a + # deterministic pseudo-direction based on the link key itself. + src = src or link_key + dst = dst or link_key + jc, jmin, jmax = stable_awg_directional_runtime_params(link_key, src, dst) + s1, s2, s3, s4 = stable_awg_shared_runtime_params(link_key) return jc, jmin, jmax, s1, s2, s3, s4 @@ -314,9 +453,105 @@ def stable_awg_h_ranges(link_key: str) -> tuple[str, str, str, str]: return ranges[0], ranges[1], ranges[2], ranges[3] -def awg_for_infra_link(link_key: str) -> AwgOptions: +def _stable_subrange( + link_key: str, + purpose: str, + minimum: int, + maximum: int, + width_min: int, + width_max: int, +) -> str: + if minimum < 0 or maximum > 65535 or minimum > maximum: + die(f"bad AWG {purpose} envelope") + span = maximum - minimum + if span == 0: + return str(minimum) + width_min = max(1, min(width_min, span)) + width_max = max(width_min, min(width_max, span)) + rng = random.Random(stable_seed_u64(f"awg-{purpose}:{link_key}")) + width = rng.randint(width_min, width_max) + start = rng.randint(minimum, maximum - width) + return f"{start}-{start + width}" + + +def stable_awg_header_protection_key(link_key: str) -> str: + root = master_key(CONFIG_KEY_MATERIALS_KEY_PATH, config_path=CONFIG_PATH) + msg = b"openwrt-mesh-builder:awg-header-protection:v1\0" + link_key.encode("utf-8") + return base64.b64encode(hmac.new(root, msg, hashlib.sha256).digest()).decode( + "ascii" + ) + + +def stable_awg_v3_params( + link_key: str, direction_key: str | None = None +) -> dict[str, object]: + local_key = direction_key or link_key + return { + "header_protection_key": ( + stable_awg_header_protection_key(link_key) + if AWG_HEADER_PROTECTION_ENABLED + else "" + ), + "content_padding_addition": AWG_CONTENT_PADDING_ADDITION, + "rekey_after_time": _stable_subrange( + local_key, + "rekey-after-time", + AWG_REKEY_AFTER_TIME_MIN, + AWG_REKEY_AFTER_TIME_MAX, + 20, + 30, + ), + "rekey_timeout": _stable_subrange( + local_key, + "rekey-timeout", + AWG_REKEY_TIMEOUT_MIN, + AWG_REKEY_TIMEOUT_MAX, + 2, + 3, + ), + "reject_after_time": _stable_subrange( + local_key, + "reject-after-time", + AWG_REJECT_AFTER_TIME_MIN, + AWG_REJECT_AFTER_TIME_MAX, + 20, + 30, + ), + "keepalive_timeout": _stable_subrange( + local_key, + "keepalive-timeout", + AWG_KEEPALIVE_TIMEOUT_MIN, + AWG_KEEPALIVE_TIMEOUT_MAX, + 4, + 6, + ), + "max_handshake_attempts": _stable_subrange( + local_key, + "max-handshake-attempts", + AWG_MAX_HANDSHAKE_ATTEMPTS_MIN, + AWG_MAX_HANDSHAKE_ATTEMPTS_MAX, + 4, + 7, + ), + "random_trailers": AWG_RANDOM_TRAILERS, + "disable_cookies": AWG_DISABLE_COOKIES, + "persistent_keepalive": _stable_subrange( + local_key, + "persistent-keepalive", + AWG_PERSISTENT_KEEPALIVE_MIN, + AWG_PERSISTENT_KEEPALIVE_MAX, + 5, + 8, + ), + } + + +def awg_for_infra_direction(link_key: str, src: str, dst: str) -> AwgOptions: h1, h2, h3, h4 = stable_awg_h_ranges(link_key) - jc, jmin, jmax, s1, s2, s3, s4 = stable_awg_runtime_params(link_key) + s1, s2, s3, s4 = stable_awg_shared_runtime_params(link_key) + jc, jmin, jmax = stable_awg_directional_runtime_params(link_key, src, dst) + i1, i2, i3, i4, i5 = stable_awg_signature_packets(link_key, src, dst) + direction_key = f"{link_key}:{src}->{dst}" awg = AwgOptions( jc=jc, jmin=jmin, @@ -329,16 +564,23 @@ def awg_for_infra_link(link_key: str) -> AwgOptions: h2=h2, h3=h3, h4=h4, - i1=AWG_INFRA_I1, - i2=AWG_INFRA_I2, - i3=AWG_INFRA_I3, - i4=AWG_INFRA_I4, - i5=AWG_INFRA_I5, + i1=i1, + i2=i2, + i3=i3, + i4=i4, + i5=i5, + **stable_awg_v3_params(link_key, direction_key), ) - validate_awg_options(awg, f"infra AWG {link_key}") + validate_awg_options(awg, f"infra AWG {direction_key}") return awg +def awg_for_infra_link(link_key: str) -> AwgOptions: + # Backward-compatible deterministic profile for tooling that has no local + # endpoint context. New infra generation should use awg_for_infra_direction. + return awg_for_infra_direction(link_key, link_key, link_key) + + def peer_endpoint( *, listen_ip: str, @@ -347,13 +589,17 @@ def peer_endpoint( return listen_ip, port -def load_awg_options(raw: object, where: str) -> AwgOptions: +def load_awg_options( + raw: object, where: str, *, auto_key: str | None = None +) -> AwgOptions: if raw is None: die(f"{where}.awg is required for AmneziaWG links") if not isinstance(raw, dict): die(f"{where}.awg must be an object") _require_known_keys(raw, f"{where}.awg", AWG_KEYS) + auto = stable_awg_v3_params(auto_key) if auto_key else {} + def get_int(key: str) -> int: if key not in raw: die(f"{where}.awg.{key} is required") @@ -363,27 +609,25 @@ def load_awg_options(raw: object, where: str) -> AwgOptions: die(f"{where}.awg.{key} must be an integer") def get_str(key: str, default: str = "") -> str: - value = raw.get(key, default) + value = raw.get(key, auto.get(key, default)) if value is None: return default return str(value).strip() - jc = get_int("jc") - jmin = get_int("jmin") - jmax = get_int("jmax") - s1 = get_int("s1") - s2 = get_int("s2") - s3 = get_int("s3") - s4 = get_int("s4") + def get_bool(key: str, default: bool) -> bool: + value = raw.get(key, auto.get(key, default)) + if not isinstance(value, bool): + die(f"{where}.awg.{key} must be a boolean") + return value awg = AwgOptions( - jc=jc, - jmin=jmin, - jmax=jmax, - s1=s1, - s2=s2, - s3=s3, - s4=s4, + jc=get_int("jc"), + jmin=get_int("jmin"), + jmax=get_int("jmax"), + s1=get_int("s1"), + s2=get_int("s2"), + s3=get_int("s3"), + s4=get_int("s4"), h1=get_str("h1"), h2=get_str("h2"), h3=get_str("h3"), @@ -393,6 +637,16 @@ def load_awg_options(raw: object, where: str) -> AwgOptions: i3=get_str("i3"), i4=get_str("i4"), i5=get_str("i5"), + header_protection_key=get_str("header_protection_key"), + content_padding_addition=get_str("content_padding_addition"), + rekey_after_time=get_str("rekey_after_time"), + rekey_timeout=get_str("rekey_timeout"), + reject_after_time=get_str("reject_after_time"), + keepalive_timeout=get_str("keepalive_timeout"), + max_handshake_attempts=get_str("max_handshake_attempts"), + random_trailers=get_bool("random_trailers", AWG_RANDOM_TRAILERS), + disable_cookies=get_bool("disable_cookies", AWG_DISABLE_COOKIES), + persistent_keepalive=get_str("persistent_keepalive"), ) validate_awg_options(awg, f"{where}.awg") return awg @@ -416,6 +670,23 @@ def awg_uci_options(awg: AwgOptions) -> dict[str, str]: **({"awg_i3": awg.i3} if awg.i3 else {}), **({"awg_i4": awg.i4} if awg.i4 else {}), **({"awg_i5": awg.i5} if awg.i5 else {}), + **( + {"awg_header_protection_key": awg.header_protection_key} + if awg.header_protection_key + else {} + ), + **( + {"awg_content_padding_addition": awg.content_padding_addition} + if awg.content_padding_addition + else {} + ), + "awg_rekey_after_time": awg.rekey_after_time, + "awg_rekey_timeout": awg.rekey_timeout, + "awg_reject_after_time": awg.reject_after_time, + "awg_keepalive_timeout": awg.keepalive_timeout, + "awg_max_handshake_attempts": awg.max_handshake_attempts, + "awg_random_trailers": "1" if awg.random_trailers else "0", + "awg_disable_cookies": "1" if awg.disable_cookies else "0", } @@ -437,4 +708,21 @@ def awg_conf_lines(awg: AwgOptions) -> list[str]: *([f"I3 = {awg.i3}"] if awg.i3 else []), *([f"I4 = {awg.i4}"] if awg.i4 else []), *([f"I5 = {awg.i5}"] if awg.i5 else []), + *( + [f"HeaderProtectionKey = {awg.header_protection_key}"] + if awg.header_protection_key + else [] + ), + *( + [f"ContentPaddingAddition = {awg.content_padding_addition}"] + if awg.content_padding_addition + else [] + ), + f"RekeyAfterTime = {awg.rekey_after_time}", + f"RekeyTimeout = {awg.rekey_timeout}", + f"RejectAfterTime = {awg.reject_after_time}", + f"KeepaliveTimeout = {awg.keepalive_timeout}", + f"MaxHandshakeAttempts = {awg.max_handshake_attempts}", + f"RandomTrailers = {1 if awg.random_trailers else 0}", + f"DisableCookies = {1 if awg.disable_cookies else 0}", ] diff --git a/tools/common.py b/tools/common.py index c62b43b..2d6216b 100755 --- a/tools/common.py +++ b/tools/common.py @@ -216,6 +216,7 @@ try: from .stable_model import ( random_free_slots, ring_link_pairs, + stable_babel_intervals, stable_hash_u32, stable_port_avoiding_for, stable_port_for, @@ -298,6 +299,7 @@ except ImportError: from stable_model import ( # type: ignore random_free_slots, ring_link_pairs, + stable_babel_intervals, stable_hash_u32, stable_port_avoiding_for, stable_port_for, @@ -401,6 +403,7 @@ try: from .awg_model import ( AWG_KEYS, awg_conf_lines, + awg_for_infra_direction, awg_for_infra_link, awg_uci_options, infra_awg_port_range, @@ -421,6 +424,7 @@ except ImportError: from awg_model import ( # type: ignore AWG_KEYS, awg_conf_lines, + awg_for_infra_direction, awg_for_infra_link, awg_uci_options, infra_awg_port_range, diff --git a/tools/config_builder.py b/tools/config_builder.py index 9983eb9..0a642c1 100755 --- a/tools/config_builder.py +++ b/tools/config_builder.py @@ -178,7 +178,11 @@ def build_config_data(raw_cfg: dict[str, object]) -> ConfigData: policy = load_access_policy(raw.get(CONFIG_KEY_POLICY), f"{where}.policy") awg: AwgOptions | None = None if protocol == PROTOCOL_AMNEZIAWG: - awg = load_awg_options(raw.get(CONFIG_KEY_AWG), where) + awg = load_awg_options( + raw.get(CONFIG_KEY_AWG), + where, + auto_key=f"access:{router_name}:{iface_name}", + ) elif raw.get(CONFIG_KEY_AWG) is not None: die(f"{where}.awg is only valid when protocol is amneziawg") diff --git a/tools/config_model.py b/tools/config_model.py index c28999e..638f0b9 100755 --- a/tools/config_model.py +++ b/tools/config_model.py @@ -80,6 +80,16 @@ class AwgOptions: i3: str = "" i4: str = "" i5: str = "" + header_protection_key: str = "" + content_padding_addition: str = "" + rekey_after_time: str = "" + rekey_timeout: str = "" + reject_after_time: str = "" + keepalive_timeout: str = "" + max_handshake_attempts: str = "" + random_trailers: bool = True + disable_cookies: bool = False + persistent_keepalive: str = "" @dataclass(frozen=True) @@ -197,7 +207,8 @@ class MeshLinkState: client_peer_section_name: str link: LinkParams keys: KeyMaterial - awg: AwgOptions + server_awg: AwgOptions + client_awg: AwgOptions @dataclass diff --git a/tools/default.py b/tools/default.py index 427e01b..329a1db 100755 --- a/tools/default.py +++ b/tools/default.py @@ -185,9 +185,12 @@ SERVER_ENV_IPSET_NAME = "exit_direct" IPIP_SERVER_IFACE = "ipip-exit" NODE_SERVER_IFACE = "awg-node" -# IPIP MTU must be <= AWG/WG MTU - 20. -# With AWG/WG MTU 1420, IPIP MTU 1400 is the maximum. -IPIP_DEFAULT_MTU: int | None = 1400 +# Use one conservative tunnel MTU profile on every node, regardless of +# whether the underlay is Ethernet or PPPoE. 1400 leaves room for the +# AWG transport overhead plus the configured S4 padding on a 1480-byte +# IPv4 PPPoE path, while remaining safe on ordinary 1500-byte Ethernet. +# IPIP adds a 20-byte outer IPv4 header, so its MTU is 20 bytes lower. +IPIP_DEFAULT_MTU: int | None = 1380 # Generated compact exit service marker prefixes. # 10.254.0.0/24 is split into stable hash-selected /31 prefixes by exit name. @@ -199,7 +202,7 @@ EXIT_ANNOUNCE_PREFIXLEN = 31 EXIT_NODE_SUPERNET4 = "10.254.1.0/24" EXIT_NODE_PREFIXLEN = 31 -TUNNEL_MTU: int | None = None +TUNNEL_MTU: int | None = 1400 PPPOE_DEFAULT_MTU = 1480 PPPOE_MTU_MIN = 1280 @@ -295,8 +298,12 @@ BABELD_LOG_FILE = "/dev/null" BABELD_UBUS_BINDINGS = "true" BABELD_TUNNEL_TYPE = "tunnel" BABELD_SPLIT_HORIZON = "true" -BABELD_HELLO_INTERVAL = 2 -BABELD_UPDATE_INTERVAL = 10 +# Babel intervals are deterministic per local tunnel direction/interface. +# Keep the spread narrow so convergence stays close to the old 2s/10s profile. +BABELD_HELLO_INTERVAL_MIN = 2 +BABELD_HELLO_INTERVAL_MAX = 4 +BABELD_UPDATE_INTERVAL_MIN = 8 +BABELD_UPDATE_INTERVAL_MAX = 14 BABELD_LAN_IFACE = "br-lan" SERVER_BABELD_CONF_PREFIX = "/etc/babel" @@ -455,6 +462,9 @@ AWG_INFRA_AUTO_JC_MIN = AWG_JC_MIN AWG_INFRA_AUTO_JC_MAX = AWG_JC_MAX AWG_INFRA_AUTO_JUNK_SIZE_MIN = AWG_JUNK_SIZE_MIN AWG_INFRA_AUTO_JUNK_SIZE_MAX = AWG_JUNK_SIZE_MAX +# Keep the legacy S1-S4 envelope while HeaderProtectionKey is disabled. +# HPK has an additional S1-S4 >= 12 requirement and should be enabled in a +# separate coordinated rollout once the kernel implementation is fixed. AWG_INFRA_AUTO_S1_MIN = AWG_S1_MIN AWG_INFRA_AUTO_S1_MAX = AWG_S1_MAX AWG_INFRA_AUTO_S2_MIN = AWG_S2_MIN @@ -464,6 +474,29 @@ AWG_INFRA_AUTO_S3_MAX = AWG_S3_MAX AWG_INFRA_AUTO_S4_MIN = AWG_S4_MIN AWG_INFRA_AUTO_S4_MAX = AWG_S4_MAX +# AWG 3.x timing/keepalive envelopes. A deterministic per-link sub-range +# is selected inside each envelope; AWG then samples from that range at runtime. +AWG_REKEY_AFTER_TIME_MIN = 105 +AWG_REKEY_AFTER_TIME_MAX = 145 +AWG_REKEY_TIMEOUT_MIN = 4 +AWG_REKEY_TIMEOUT_MAX = 7 +AWG_REJECT_AFTER_TIME_MIN = 175 +AWG_REJECT_AFTER_TIME_MAX = 220 +AWG_KEEPALIVE_TIMEOUT_MIN = 8 +AWG_KEEPALIVE_TIMEOUT_MAX = 15 +AWG_MAX_HANDSHAKE_ATTEMPTS_MIN = 15 +AWG_MAX_HANDSHAKE_ATTEMPTS_MAX = 24 +AWG_PERSISTENT_KEEPALIVE_MIN = 20 +AWG_PERSISTENT_KEEPALIVE_MAX = 30 + +AWG_RANDOM_TRAILERS = True +AWG_DISABLE_COOKIES = False +AWG_CONTENT_PADDING_ADDITION = "" # omitted/off so RandomTrailers is used + +# v3.1.20260812 kernel mode currently has upstream issue #216 with HPK. +# Flip to True after the header-protection locking fix is present in kmod-amneziawg. +AWG_HEADER_PROTECTION_ENABLED = False + # AWG H range generator defaults. AWG_H_COUNT = 4 AWG_H_SPAN_MIN = 65_536 @@ -472,13 +505,12 @@ AWG_H_MIN = 0 AWG_H_MAX = 4_294_967_295 AWG_H_GAP = 0 -# Infra links derive J/S/H parameters from the stable per-link hash. -# I1-I5 are static CPS templates used for all generated infra links. -AWG_INFRA_I1 = "" -AWG_INFRA_I2 = "" -AWG_INFRA_I3 = "" -AWG_INFRA_I4 = "" -AWG_INFRA_I5 = "" +# Infra links keep S/H shared per link; J and I1-I5 are derived per direction. +AWG_INFRA_SIGNATURE_COUNT_MIN = 1 +AWG_INFRA_SIGNATURE_COUNT_MAX = 2 +AWG_INFRA_SIGNATURE_SIZE_MIN = 64 +AWG_INFRA_SIGNATURE_SIZE_MAX = 256 +AWG_INFRA_SIGNATURE_TAGS = ("r", "rc", "rd") # ============================================================ # TOPOLOGY RENDER DEFAULTS diff --git a/tools/generate.py b/tools/generate.py index 38669eb..1d47454 100755 --- a/tools/generate.py +++ b/tools/generate.py @@ -190,7 +190,8 @@ def main(argv: list[str] | None = None) -> None: right_alias = build_exit_exit_alias(cfg, right_hub.name, left_hub.name) link = compute_exit_exit_link_params(cfg, left_hub, right_hub) key = exit_exit_link_key(left_hub.name, right_hub.name) - awg = awg_for_infra_link(key) + left_awg = awg_for_infra_direction(key, left_hub.name, right_hub.name) + right_awg = awg_for_infra_direction(key, right_hub.name, left_hub.name) keys = build_material_for_exit_exit( cfg=cfg, left_hub=left_hub, @@ -200,11 +201,11 @@ def main(argv: list[str] | None = None) -> None: write( server_client_conf_path(left_hub.name, left_alias), - build_exit_exit_server_conf(left_hub, right_hub, link, keys, awg), + build_exit_exit_server_conf(left_hub, right_hub, link, keys, left_awg), ) write( server_client_conf_path(right_hub.name, right_alias), - build_exit_exit_server_conf(right_hub, left_hub, link, keys, awg), + build_exit_exit_server_conf(right_hub, left_hub, link, keys, right_awg), ) exit_exit_aliases_by_hub[left_hub.name].append(left_alias) exit_exit_aliases_by_hub[right_hub.name].append(right_alias) @@ -224,7 +225,9 @@ def main(argv: list[str] | None = None) -> None: if exit_hub_is_public(hub): client_alias = build_exit_client_alias(cfg, hub.name, router_name) link = compute_exit_link_params(cfg, hub, router_name) - awg = awg_for_infra_link(exit_link_key(hub.name, router_name)) + key = exit_link_key(hub.name, router_name) + router_awg = awg_for_infra_direction(key, router_name, hub.name) + server_awg = awg_for_infra_direction(key, hub.name, router_name) iface_name = exit_out_iface_name(hub.name) keys = build_material_for_exit( @@ -238,10 +241,10 @@ def main(argv: list[str] | None = None) -> None: write( server_client_conf_path(hub.name, client_alias), - build_server_direct_conf(client_alias, hub, link, keys, awg), + build_server_direct_conf(client_alias, hub, link, keys, server_awg), ) blocks.append( - build_exit_out_network_interface_block(hub, link, keys, awg) + build_exit_out_network_interface_block(hub, link, keys, router_awg) ) router_exit_ifaces[router_name].append(iface_name) @@ -252,7 +255,7 @@ def main(argv: list[str] | None = None) -> None: hub=hub, link=link, keys=keys, - awg=awg, + awg=server_awg, ) ) @@ -268,7 +271,8 @@ def main(argv: list[str] | None = None) -> None: ) link = compute_exit_reverse_link_params(cfg, hub, router_name) key = exit_reverse_link_key(hub.name, router_name) - awg = awg_for_infra_link(key) + router_awg = awg_for_infra_direction(key, router_name, hub.name) + server_awg = awg_for_infra_direction(key, hub.name, router_name) iface_name = exit_in_iface_name(hub.name) keys = build_material_for_exit_reverse( @@ -282,11 +286,11 @@ def main(argv: list[str] | None = None) -> None: write( server_client_conf_path(hub.name, client_alias), build_server_reverse_conf( - cfg, router_name, client_alias, hub, link, keys, awg + cfg, router_name, client_alias, hub, link, keys, server_awg ), ) blocks.append( - build_exit_in_network_interface_block(hub, link, keys, awg) + build_exit_in_network_interface_block(hub, link, keys, router_awg) ) router_exit_ifaces[router_name].append(iface_name) @@ -297,7 +301,7 @@ def main(argv: list[str] | None = None) -> None: hub=hub, link=link, keys=keys, - awg=awg, + awg=server_awg, ) ) diff --git a/tools/generate_access.py b/tools/generate_access.py index c25a191..2005e10 100755 --- a/tools/generate_access.py +++ b/tools/generate_access.py @@ -58,7 +58,11 @@ def build_access_peer_block(state: AccessPeerState) -> str: "public_key": state.client_public, "description": f"{state.user_name}.conf", "route_allowed_ips": "1", - "persistent_keepalive": str(KEEPALIVE), + "persistent_keepalive": ( + state.awg.persistent_keepalive + if state.protocol == PROTOCOL_AMNEZIAWG and state.awg is not None + else str(KEEPALIVE) + ), }, lists={"allowed_ips": [state.client_ip4]}, ) diff --git a/tools/generate_clients.py b/tools/generate_clients.py index cb1e915..1c2618f 100755 --- a/tools/generate_clients.py +++ b/tools/generate_clients.py @@ -142,7 +142,7 @@ def build_wireguard_client_conf( f"PublicKey = {server_public}\n" f"Endpoint = {remote_host}:{remote_port}\n" f"AllowedIPs = {DEFAULT_ALLOWED_IPS_TEXT}\n" - f"PersistentKeepalive = {KEEPALIVE}\n" + f"PersistentKeepalive = {awg.persistent_keepalive if awg is not None else KEEPALIVE}\n" ) diff --git a/tools/generate_exit.py b/tools/generate_exit.py index 2a662a8..b616384 100755 --- a/tools/generate_exit.py +++ b/tools/generate_exit.py @@ -171,7 +171,7 @@ def build_exit_out_network_interface_block( "description": f"{iface_name}.conf", "public_key": keys.server_public, "route_allowed_ips": "1", - "persistent_keepalive": str(KEEPALIVE), + "persistent_keepalive": awg.persistent_keepalive, "endpoint_host": endpoint_host, "endpoint_port": str(endpoint_port), }, @@ -207,7 +207,7 @@ def build_exit_in_network_interface_block( "description": f"{iface_name}.conf", "public_key": keys.client_public, "route_allowed_ips": "1", - "persistent_keepalive": str(KEEPALIVE), + "persistent_keepalive": awg.persistent_keepalive, }, lists={"allowed_ips": DEFAULT_ALLOWED_IPS}, ) @@ -293,7 +293,7 @@ def build_server_direct_conf( "[Peer]", f"PublicKey = {keys.client_public}", f"AllowedIPs = {DEFAULT_ALLOWED_IPS_TEXT}", - f"PersistentKeepalive = {KEEPALIVE}", + f"PersistentKeepalive = {awg.persistent_keepalive}", ] return "\n".join(lines) + "\n" @@ -325,7 +325,7 @@ def build_server_reverse_conf( f"PublicKey = {keys.server_public}", f"AllowedIPs = {DEFAULT_ALLOWED_IPS_TEXT}", f"Endpoint = {endpoint_host}:{endpoint_port}", - f"PersistentKeepalive = {KEEPALIVE}", + f"PersistentKeepalive = {awg.persistent_keepalive}", ] return "\n".join(lines) + "\n" @@ -430,7 +430,7 @@ def build_exit_exit_server_conf( port=peer_port, ) peer_lines.append(f"Endpoint = {endpoint_host}:{endpoint_port}") - peer_lines.append(f"PersistentKeepalive = {KEEPALIVE}") + peer_lines.append(f"PersistentKeepalive = {awg.persistent_keepalive}") lines = [ "[Interface]", diff --git a/tools/generate_mesh.py b/tools/generate_mesh.py index ae11d72..8abc9ef 100755 --- a/tools/generate_mesh.py +++ b/tools/generate_mesh.py @@ -75,7 +75,7 @@ def build_mesh_server_block(state: MeshLinkState) -> str: "listen_port": str(state.link.port), "defaultroute": "0", **mtu_uci_options(), - **awg_uci_options(state.awg), + **awg_uci_options(state.server_awg), }, lists={"addresses": [state.link.srv_ip4, state.link.srv_ll]}, ) @@ -86,7 +86,7 @@ def build_mesh_server_block(state: MeshLinkState) -> str: "description": f"{state.server_iface_name}.conf", "public_key": state.keys.client_public, "route_allowed_ips": "1", - "persistent_keepalive": str(KEEPALIVE), + "persistent_keepalive": state.server_awg.persistent_keepalive, }, lists={"allowed_ips": DEFAULT_ALLOWED_IPS}, ) @@ -107,7 +107,7 @@ def build_mesh_client_block(state: MeshLinkState) -> str: "private_key": state.keys.client_private, "defaultroute": "0", **mtu_uci_options(), - **awg_uci_options(state.awg), + **awg_uci_options(state.client_awg), }, lists={"addresses": [state.link.cli_ip4, state.link.cli_ll]}, ) @@ -118,7 +118,7 @@ def build_mesh_client_block(state: MeshLinkState) -> str: "description": f"{state.client_iface_name}.conf", "public_key": state.keys.server_public, "route_allowed_ips": "1", - "persistent_keepalive": str(KEEPALIVE), + "persistent_keepalive": state.client_awg.persistent_keepalive, "endpoint_host": endpoint_host, "endpoint_port": str(endpoint_port), }, @@ -163,7 +163,12 @@ def build_mesh_state( client_iface_name, force, ), - awg=awg_for_infra_link(mesh_link_key(hub.name, target_router)), + server_awg=awg_for_infra_direction( + mesh_link_key(hub.name, target_router), hub.name, target_router + ), + client_awg=awg_for_infra_direction( + mesh_link_key(hub.name, target_router), target_router, hub.name + ), ) network_blocks[hub.name] += build_mesh_server_block(state) diff --git a/tools/generate_network.py b/tools/generate_network.py index 1c42867..601d351 100755 --- a/tools/generate_network.py +++ b/tools/generate_network.py @@ -88,13 +88,14 @@ def build_babeld_text( ] for iface in mesh_ifaces + exit_ifaces: + hello_interval, update_interval = stable_babel_intervals(router_name, iface) lines += [ "config interface", f" option ifname '{iface}'", f" option type '{BABELD_TUNNEL_TYPE}'", f" option split_horizon '{BABELD_SPLIT_HORIZON}'", - f" option hello_interval '{BABELD_HELLO_INTERVAL}'", - f" option update_interval '{BABELD_UPDATE_INTERVAL}'", + f" option hello_interval '{hello_interval}'", + f" option update_interval '{update_interval}'", "", ] diff --git a/tools/generate_server_runtime.py b/tools/generate_server_runtime.py index 3ebb5a5..8adc390 100755 --- a/tools/generate_server_runtime.py +++ b/tools/generate_server_runtime.py @@ -17,14 +17,15 @@ def build_server_babeld_conf( states: list[RouterExitState], exit_exit_aliases: list[str], ) -> str: - _ = hub ifaces = sorted({st.client_alias for st in states} | set(exit_exit_aliases)) - lines = [ - f"interface {iface} type {BABELD_TUNNEL_TYPE} " - f"hello-interval {BABELD_HELLO_INTERVAL} " - f"update-interval {BABELD_UPDATE_INTERVAL}" - for iface in ifaces - ] + lines = [] + for iface in ifaces: + hello_interval, update_interval = stable_babel_intervals(hub.name, iface) + lines.append( + f"interface {iface} type {BABELD_TUNNEL_TYPE} " + f"hello-interval {hello_interval} " + f"update-interval {update_interval}" + ) lines += [ "", "install allow", diff --git a/tools/show_unmanaged_collect.py b/tools/show_unmanaged_collect.py index 01c46c2..4148462 100755 --- a/tools/show_unmanaged_collect.py +++ b/tools/show_unmanaged_collect.py @@ -122,7 +122,8 @@ def expected_router_generation_state( if exit_hub_is_public(hub): client_alias = build_exit_client_alias(cfg, hub.name, router_name) link = compute_exit_link_params(cfg, hub, router_name) - awg = awg_for_infra_link(exit_link_key(hub.name, router_name)) + key = exit_link_key(hub.name, router_name) + awg = awg_for_infra_direction(key, router_name, hub.name) iface_name = exit_out_iface_name(hub.name) keys = build_material_for_exit( router_name=router_name, @@ -142,7 +143,8 @@ def expected_router_generation_state( cfg, hub.name, router_name ) link = compute_exit_reverse_link_params(cfg, hub, router_name) - awg = awg_for_infra_link(exit_reverse_link_key(hub.name, router_name)) + key = exit_reverse_link_key(hub.name, router_name) + awg = awg_for_infra_direction(key, router_name, hub.name) iface_name = exit_in_iface_name(hub.name) keys = build_material_for_exit_reverse( hub=hub, diff --git a/tools/stable_model.py b/tools/stable_model.py index fa64811..5da128a 100755 --- a/tools/stable_model.py +++ b/tools/stable_model.py @@ -130,6 +130,46 @@ def stable_seed_u64(seed: str) -> int: return int.from_bytes(digest, "big") +def stable_int(seed: str, minimum: int, maximum: int) -> int: + if minimum > maximum: + raise ValueError(f"invalid stable integer range {minimum}..{maximum}") + return minimum + (stable_seed_u64(seed) % (maximum - minimum + 1)) + + +def stable_babel_intervals(local_node: str, iface_name: str) -> tuple[int, int]: + # Babel timers are local interface properties, so the two directions of a + # tunnel intentionally get different values. + try: + from .default import ( + BABELD_HELLO_INTERVAL_MIN, + BABELD_HELLO_INTERVAL_MAX, + BABELD_UPDATE_INTERVAL_MIN, + BABELD_UPDATE_INTERVAL_MAX, + ) + except ImportError: + from default import ( # type: ignore + BABELD_HELLO_INTERVAL_MIN, + BABELD_HELLO_INTERVAL_MAX, + BABELD_UPDATE_INTERVAL_MIN, + BABELD_UPDATE_INTERVAL_MAX, + ) + + direction = f"{local_node}:{iface_name}" + hello = stable_int( + f"babel-hello:v1:{direction}", + BABELD_HELLO_INTERVAL_MIN, + BABELD_HELLO_INTERVAL_MAX, + ) + # Keep update close to Babel's conventional ~4x hello cadence while + # still varying each local direction. + update_jitter = stable_int(f"babel-update-jitter:v1:{direction}", -2, 2) + update = max( + BABELD_UPDATE_INTERVAL_MIN, + min(BABELD_UPDATE_INTERVAL_MAX, 4 * hello + update_jitter), + ) + return hello, update + + def random_free_slots(rng: random.Random, total_free: int, slots: int) -> list[int]: if slots <= 1: return [total_free] diff --git a/tools/validate_access.py b/tools/validate_access.py index 72b1d02..646e912 100755 --- a/tools/validate_access.py +++ b/tools/validate_access.py @@ -59,24 +59,10 @@ def require_awg_client_options( if awg is None: die(f"{where}: missing awg options in config model") - expected = { - "Jc": str(awg.jc), - "Jmin": str(awg.jmin), - "Jmax": str(awg.jmax), - "S1": str(awg.s1), - "S2": str(awg.s2), - "S3": str(awg.s3), - "S4": str(awg.s4), - "H1": awg.h1, - "H2": awg.h2, - "H3": awg.h3, - "H4": awg.h4, - } - for key in ("I1", "I2", "I3", "I4", "I5"): - value = getattr(awg, key.lower()) - if value: - expected[key] = value - + expected = {} + for line in awg_conf_lines(awg): + key, value = line.split(" = ", 1) + expected[key] = value for key, value in expected.items(): require_option(opts, key, value, where) @@ -157,7 +143,12 @@ def validate_wireguard_access( require_option( peer_opts, "persistent_keepalive", - str(KEEPALIVE), + ( + group.awg.persistent_keepalive + if group.protocol == PROTOCOL_AMNEZIAWG + and group.awg is not None + else str(KEEPALIVE) + ), f"access {router_name}/{group.name}/{user_name}", ) require_list( @@ -192,7 +183,15 @@ def validate_wireguard_access( str(client_conf), ) require_option( - client_peer, "PersistentKeepalive", str(KEEPALIVE), str(client_conf) + client_peer, + "PersistentKeepalive", + ( + group.awg.persistent_keepalive + if group.protocol == PROTOCOL_AMNEZIAWG + and group.awg is not None + else str(KEEPALIVE) + ), + str(client_conf), ) host, port = parse_endpoint( client_peer.get("Endpoint", ""), str(client_conf) diff --git a/tools/validate_awg_helpers.py b/tools/validate_awg_helpers.py index 0bf820e..d6ceaa6 100755 --- a/tools/validate_awg_helpers.py +++ b/tools/validate_awg_helpers.py @@ -52,6 +52,15 @@ def awg_conf_key_map() -> dict[str, str]: "awg_i3": "I3", "awg_i4": "I4", "awg_i5": "I5", + "awg_header_protection_key": "HeaderProtectionKey", + "awg_content_padding_addition": "ContentPaddingAddition", + "awg_rekey_after_time": "RekeyAfterTime", + "awg_rekey_timeout": "RekeyTimeout", + "awg_reject_after_time": "RejectAfterTime", + "awg_keepalive_timeout": "KeepaliveTimeout", + "awg_max_handshake_attempts": "MaxHandshakeAttempts", + "awg_random_trailers": "RandomTrailers", + "awg_disable_cookies": "DisableCookies", } @@ -121,13 +130,17 @@ def validate_router_awg_interface_common( def validate_awg_peer_common( - block: dict[str, object], expected_public_key: str, where: str + block: dict[str, object], + expected_public_key: str, + where: str, + *, + awg: AwgOptions, ) -> None: opts = block.get("options", {}) lists = block.get("lists", {}) require_option(opts, "public_key", expected_public_key, where) require_option(opts, "route_allowed_ips", "1", where) - require_option(opts, "persistent_keepalive", str(KEEPALIVE), where) + require_option(opts, "persistent_keepalive", awg.persistent_keepalive, where) require_list(lists, "allowed_ips", DEFAULT_ALLOWED_IPS, where) diff --git a/tools/validate_runtime_files.py b/tools/validate_runtime_files.py index 30c32df..739484b 100755 --- a/tools/validate_runtime_files.py +++ b/tools/validate_runtime_files.py @@ -145,7 +145,8 @@ def validate_exit_server_confs(cfg: ConfigData) -> None: for router_name in cfg.router_names: client_alias = build_exit_client_alias(cfg, hub.name, router_name) link = compute_exit_link_params(cfg, hub, router_name) - awg = awg_for_infra_link(exit_link_key(hub.name, router_name)) + key = exit_link_key(hub.name, router_name) + awg = awg_for_infra_direction(key, hub.name, router_name) conf_path = server_client_conf_path(hub.name, client_alias) conf = parse_tunnel_conf(conf_path) require_tunnel_sections(conf, conf_path, {"Interface", "Peer"}) @@ -168,13 +169,17 @@ def validate_exit_server_confs(cfg: ConfigData) -> None: if "Endpoint" in peer: die(f"{conf_path}: unexpected Endpoint") require_option( - peer, "PersistentKeepalive", str(KEEPALIVE), str(conf_path) + peer, + "PersistentKeepalive", + awg.persistent_keepalive, + str(conf_path), ) for router_name in cfg.mesh_hubs_by_name: client_alias = build_exit_reverse_client_alias(cfg, hub.name, router_name) link = compute_exit_reverse_link_params(cfg, hub, router_name) - awg = awg_for_infra_link(exit_reverse_link_key(hub.name, router_name)) + key = exit_reverse_link_key(hub.name, router_name) + awg = awg_for_infra_direction(key, hub.name, router_name) conf_path = server_client_conf_path(hub.name, client_alias) conf = parse_tunnel_conf(conf_path) require_tunnel_sections(conf, conf_path, {"Interface", "Peer"}) @@ -198,13 +203,15 @@ def validate_exit_server_confs(cfg: ConfigData) -> None: require_option( peer, "Endpoint", f"{endpoint_host}:{endpoint_port}", str(conf_path) ) - require_option(peer, "PersistentKeepalive", str(KEEPALIVE), str(conf_path)) + require_option( + peer, "PersistentKeepalive", awg.persistent_keepalive, str(conf_path) + ) for left_name, right_name in exit_exit_link_pairs(cfg): left_hub = cfg.exit_hubs_by_name[left_name] right_hub = cfg.exit_hubs_by_name[right_name] link = compute_exit_exit_link_params(cfg, left_hub, right_hub) - awg = awg_for_infra_link(exit_exit_link_key(left_hub.name, right_hub.name)) + key = exit_exit_link_key(left_hub.name, right_hub.name) pairs = ( ( @@ -235,6 +242,7 @@ def validate_exit_server_confs(cfg: ConfigData) -> None: local_port, peer_port, ) in pairs: + awg = awg_for_infra_direction(key, local_hub.name, peer_hub.name) conf_path = server_client_conf_path(local_hub.name, local_alias) conf = parse_tunnel_conf(conf_path) require_tunnel_sections(conf, conf_path, {"Interface", "Peer"}) @@ -260,7 +268,9 @@ def validate_exit_server_confs(cfg: ConfigData) -> None: ) elif "Endpoint" in peer: die(f"{conf_path}: unexpected Endpoint") - require_option(peer, "PersistentKeepalive", str(KEEPALIVE), str(conf_path)) + require_option( + peer, "PersistentKeepalive", awg.persistent_keepalive, str(conf_path) + ) def parse_env_file(path: Path) -> dict[str, str]: diff --git a/tools/validate_tunnel_files.py b/tools/validate_tunnel_files.py index 98a82ed..bc17076 100755 --- a/tools/validate_tunnel_files.py +++ b/tools/validate_tunnel_files.py @@ -82,7 +82,9 @@ def validate_mesh_pair_confs( hub_parsed = existing[hub.name] target_parsed = existing[target_name] link = compute_mesh_link_params(cfg, hub, target_name) - awg = awg_for_infra_link(mesh_link_key(hub.name, target_name)) + key = mesh_link_key(hub.name, target_name) + server_awg = awg_for_infra_direction(key, hub.name, target_name) + client_awg = awg_for_infra_direction(key, target_name, hub.name) validate_link_is_31_pair( link.srv_ip4, link.cli_ip4, f"mesh {hub.name}->{target_name}" ) @@ -119,13 +121,13 @@ def validate_mesh_pair_confs( server_public = validate_router_awg_interface_common( server_iface, - awg, + server_awg, [link.srv_ip4, link.srv_ll], f"mesh {hub.name}->{target_name} server iface {server_iface_name}", ) client_public = validate_router_awg_interface_common( client_iface, - awg, + client_awg, [link.cli_ip4, link.cli_ll], f"mesh {hub.name}->{target_name} client iface {client_iface_name}", ) @@ -134,11 +136,13 @@ def validate_mesh_pair_confs( server_peer, client_public, f"mesh {hub.name}->{target_name} server peer {server_peer_type}", + awg=server_awg, ) validate_awg_peer_common( client_peer, server_public, f"mesh {hub.name}->{target_name} client peer {client_peer_type}", + awg=client_awg, ) client_opts = client_peer.get("options", {}) @@ -196,7 +200,9 @@ def validate_exit_pair_confs( if exit_hub_is_public(hub): link = compute_exit_link_params(cfg, hub, router_name) - awg = awg_for_infra_link(exit_link_key(hub.name, router_name)) + key = exit_link_key(hub.name, router_name) + router_awg = awg_for_infra_direction(key, router_name, hub.name) + server_awg = awg_for_infra_direction(key, hub.name, router_name) validate_link_is_31_pair( link.srv_ip4, link.cli_ip4, f"exit {hub.name}->{router_name}" ) @@ -214,7 +220,7 @@ def validate_exit_pair_confs( ) router_public = validate_router_awg_interface_common( router_iface, - awg, + router_awg, [link.cli_ip4, link.cli_ll], f"exit-out {router_name}->{hub.name} iface {router_iface_name}", ) @@ -233,6 +239,7 @@ def validate_exit_pair_confs( router_peer, server_public, f"exit-out {router_name}->{hub.name} peer {router_peer_type}", + awg=router_awg, ) router_peer_opts = router_peer.get("options", {}) expected_host, expected_port = peer_endpoint( @@ -260,7 +267,8 @@ def validate_exit_pair_confs( if router_is_public_mesh_hub(cfg, router_name): link = compute_exit_reverse_link_params(cfg, hub, router_name) key = exit_reverse_link_key(hub.name, router_name) - awg = awg_for_infra_link(key) + router_awg = awg_for_infra_direction(key, router_name, hub.name) + server_awg = awg_for_infra_direction(key, hub.name, router_name) validate_link_is_31_pair( link.srv_ip4, link.cli_ip4, @@ -280,7 +288,7 @@ def validate_exit_pair_confs( ) router_public = validate_router_awg_interface_common( router_iface, - awg, + router_awg, [link.srv_ip4, link.srv_ll], f"exit-in {hub.name}->{router_name} iface {router_iface_name}", ) @@ -306,6 +314,7 @@ def validate_exit_pair_confs( router_peer, server_public, f"exit-in {hub.name}->{router_name} peer {router_peer_type}", + awg=router_awg, ) if "endpoint_host" in router_peer.get("options", {}): die(f"exit-in {hub.name}->{router_name}: unexpected endpoint_host")