mirror of
https://github.com/karen07/openwrt-mesh-builder.git
synced 2026-10-19 07:23:32 +00:00
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:
parent
9bff15b5f2
commit
28081faa35
19 changed files with 593 additions and 135 deletions
41
README.md
41
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`
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}'",
|
||||
"",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue