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
This commit is contained in:
Karen Khachatryan 2026-08-23 20:13:24 +03:00
parent 9bff15b5f2
commit 28081faa35
19 changed files with 593 additions and 135 deletions

View file

@ -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`

View file

@ -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}",
]

View file

@ -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,

View file

@ -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")

View file

@ -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

View file

@ -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 = "<r 128>"
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

View file

@ -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,
)
)

View file

@ -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]},
)

View file

@ -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"
)

View file

@ -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]",

View file

@ -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)

View file

@ -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}'",
"",
]

View file

@ -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",

View file

@ -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,

View file

@ -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]

View file

@ -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)

View file

@ -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)

View file

@ -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]:

View file

@ -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")