todo/amnezia-2-0/reference.md
loop-uh 0ad0bcc8dd
Some checks failed
Published content check / validate (push) Failing after 5s
seo+зеркало: [!mirror]-плашка со ссылкой на вики и description во frontmatter всех заметок
Каждая публикуемая заметка получила callout-шапку со ссылкой на свою страницу
wiki.zapret.moe (на самой вики она вырезается транформером RemoveMirrorCallout,
видна только на зеркале Obsidian Publish и в Forgejo) и SEO-поле description —
1–2 предложения для meta description обоих сайтов.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 22:05:57 +03:00

1080 lines
56 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
date: 2026-03-25
tags:
- amneziawg
- wireguard
- обфускация
- обход-блокировок
aliases:
- AmneziaWG 2.0
- AWG 2.0
- Амнезия 2.0
- АмнезияВГ 2.0
- Амнезия ВПН параметры конфига
- Что означают Jc Jmin Jmax в конфиге
- Что такое S1 S2 S3 S4 и H1 H2 H3 H4
- Сигнатурные пакеты I1-I5 и язык CPS
link: https://github.com/amnezia-vpn/amneziawg-go
description: "Справочник всех 17 параметров AmneziaWG 2.0 по исходному коду: Jc/Jmin/Jmax, padding S1S4, заголовки H1H4 и сигнатурные пакеты I1I5."
---
> [!mirror] Резервное зеркало
> Актуальная версия этой страницы — на основной вики: [wiki.zapret.moe/amnezia-2-0/reference](https://wiki.zapret.moe/amnezia-2-0/reference)
# AmneziaWG 2.0 — полный справочник параметров
> Составлен на основе анализа исходного кода [amneziawg-go](https://github.com/amnezia-vpn/amneziawg-go)
> Верифицировано по исходникам: 2026-03-25
> [!info] У протокола есть третье поколение
> Справочник описывает AmneziaWG 2.0. 24 июля 2026 года вышло третье поколение — AmneziaWG 3.0 с шифрованием заголовков пакетов: обзор в [[amnezia-3-0/reference|заметке про AmneziaWG 3.0]], побайтовый разбор по коду — в [[amnezia-3-0/internals|описании внутреннего устройства]], сопутствующий релиз приложения — в [[amnezia-3-0/client-5-0-0-5|AmneziaVPN 5.0.0.5]]. Для своих серверов третье поколение на 20 августа 2026 года всё ещё не выпущено (поддержка смержена в ветку разработки приложения 6 августа, но релиза с ней нет), поэтому параметры ниже остаются актуальными для self-hosted установок.
## Обзор
AmneziaWG (*AWG, АмнезияВГ 2.0, Амнезия ВПН VPN*) — модифицированный WireGuard с обфускацией трафика для обхода DPI-блокировок.
**Версия 1.0** (2023): мусорные пакеты, padding handshake, фиксированные заголовки.
**Версия 1.5** (июль 2025): signature packets i1-i5 — мимикрия под QUIC, DNS и SIP.
**Версия 2.0** (сентябрь 2025): range-based заголовки, padding для всех типов пакетов, CPS-язык для описания сигнатур.
**Версия 3.0** (июль 2026): шифрование заголовков, content padding, диапазонные тайминги — см. [[amnezia-3-0/reference|отдельную заметку]].
Клиент: AmneziaVPN 4.8.12.9+ (desktop, Android). Self-hosted only для AWG 2.0.
---
## Все параметры протокола (17 штук)
### 1. Junk Packets — мусорные пакеты перед handshake
| Параметр | Тип | Валидация | Описание |
|----------|-----|-----------|----------|
| **Jc** | int | > 0, строго положительное | Количество мусорных пакетов |
| **Jmin** | int | > 0, строго положительное | Минимальный размер пакета (байт) |
| **Jmax** | int | > 0, строго положительное | Максимальный размер пакета (байт) |
Дефолт: 0 (отключено). Сериализуется в вывод только если != 0.
- Отправляются **только** при handshake initiation (~раз в 2 минуты)
- Размер каждого пакета: `crypto/rand.Int(Jmax - Jmin + 1) + Jmin`
- Данные: `crypto/rand.Read` (криптографически безопасные)
- Рекомендованный диапазон Jc: 4-12
**Важно**: в коде **нет** перекрёстной валидации `Jmin <= Jmax` — параметры проверяются независимо. Если задать Jmin > Jmax, поведение непредсказуемо.
```
Пример: Jc=7, Jmin=50, Jmax=1000
→ 7 пакетов, каждый 50-1000 байт случайных данных
```
### 2. Padding — случайные байты перед пакетами
| Параметр | Версия | Применяется к | Размер сообщения | Как часто |
|----------|--------|---------------|------------------|-----------|
| **S1** | 1.0 | Handshake Initiation | 148 байт | Каждый handshake (~2 мин) |
| **S2** | 1.0 | Handshake Response | 92 байта | Каждый handshake |
| **S3** | **2.0** | Cookie Reply | 64 байта | Редко (только под нагрузкой) |
| **S4** | **2.0** | Transport Data | переменный | **Каждый data-пакет** |
Валидация: `int >= 0` (в отличие от Jc/Jmin/Jmax, допускается 0). Дефолт: 0 (отключено).
**Механизм S1/S2/S3** — выделяется новый буфер, random prefix:
```
buf = make([]byte, padding + len(packet))
rand.Read(buf[:padding]) // Заполняем prefix случайными байтами
copy(buf[padding:], packet) // Копируем пакет после prefix
```
**Механизм S4** — сдвиг данных в существующем буфере (отличается от S1-S3!):
```
// Сдвигаем зашифрованные данные ВПРАВО на padding байт
for i := len(elem.packet) - 1; i >= 0; i-- {
elem.buffer[i+padding] = elem.buffer[i]
}
rand.Read(elem.buffer[:padding]) // Заполняем начало случайными байтами
```
**Приём (все типы)**`DeterminePacketTypeAndPadding()` в `receive.go`:
```
data = packet[padding:] // Пропускаем padding, читаем заголовок
header.Validate(LittleEndian.Uint32(data)) // Проверяем magic header
```
**Особенности S4**:
- Применяется к **каждому** data-пакету (основной трафик)
- **НЕ** применяется к keepalive-пакетам (проверка: `len(elem.packet) != MessageKeepaliveSize`)
- `MessageKeepaliveSize = 32` байта (transport header 16B + Poly1305 tag 16B, нулевой payload)
- Добавляется **поверх** стандартного WireGuard-выравнивания до 16 байт (`PaddingMultiple = 16`)
- S1-S4 **не обязаны** быть кратны 16 — это любое целое >= 0; `PaddingMultiple` — отдельный внутренний механизм WireGuard
- При больших значениях может превысить MTU → фрагментация
### 3. Magic Headers — заголовки пакетов
| Параметр | Тип пакета | Формат | Byte order |
|----------|------------|--------|------------|
| **H1** | Handshake Init | `"N"` или `"N-M"` (uint32 range) | little-endian |
| **H2** | Handshake Response | `"N"` или `"N-M"` | little-endian |
| **H3** | Cookie Reply | `"N"` или `"N-M"` | little-endian |
| **H4** | Transport Data | `"N"` или `"N-M"` | little-endian |
Тип значения: `uint32` (0 — 4 294 967 295).
**Дефолтные значения** (устанавливаются в `NewDevice()`):
```go
device.headers.init = &magicHeader{start: 1, end: 1} // MessageInitiationType
device.headers.response = &magicHeader{start: 2, end: 2} // MessageResponseType
device.headers.cookie = &magicHeader{start: 3, end: 3} // MessageCookieReplyType
device.headers.transport = &magicHeader{start: 4, end: 4} // MessageTransportType
```
Т.е. по умолчанию заголовки = стандартный WireGuard (1, 2, 3, 4). AWG без конфигурации H1-H4 полностью совместим с обычным WireGuard.
**Реализация** (`magic-header.go`):
```go
type magicHeader struct {
start uint32
end uint32
}
func (h *magicHeader) Generate() uint32 {
high := int64(h.end - h.start + 1)
r, _ := rand.Int(rand.Reader, big.NewInt(high))
return h.start + uint32(r.Int64())
}
func (h *magicHeader) Validate(val uint32) bool {
return h.start <= val && val <= h.end
}
```
**Парсинг:**
- `"42"` → start=42, end=42 (фиксированный заголовок, как в AWG 1.0)
- `"471800590-471800690"` → start=471800590, end=471800690 (101 вариант)
- Валидация: `end >= start`, иначе ошибка `"wrong range specified"`
**Критичное ограничение:** диапазоны H1, H2, H3, H4 **НЕ должны пересекаться**.
Проверка происходит в `ipcSetDevice.mergeWithDevice()` — специальной функции, которая:
1. Заполняет не указанные в текущем IPC-вызове заголовки из существующего конфига устройства
2. Проверяет все 4 заголовка попарно на пересечение
3. Если ОК — применяет к устройству
Это значит: можно обновить один заголовок (напр. только H1) без повторного указания H2-H4 — они возьмутся из текущего конфига.
```go
// mergeWithDevice() — overlap check
headers := []*magicHeader{d.headers.init, d.headers.response, d.headers.cookie, d.headers.transport}
for i := 0; i < len(headers); i++ {
for j := i + 1; j < len(headers); j++ {
if left.start <= right.end && right.start <= left.end {
return errors.New("headers must not overlap")
}
}
}
```
**Где применяются:**
- H1 → `CreateMessageInitiation()` в `noise-protocol.go`: `msg.Type = device.headers.init.Generate()`
- H2 → `CreateMessageResponse()`: `msg.Type = device.headers.response.Generate()`
- H3 → `SendHandshakeCookie()`: `msgType := device.headers.cookie.Generate()`
- H4 → `RoutineEncryption()`: `msgType := device.headers.transport.Generate()`
```
ХОРОШО (не пересекаются):
H1 = 100-200
H2 = 300-400
H3 = 500-600
H4 = 700-800
ПЛОХО:
H1 = 100-200
H2 = 150-250 # Пересекается с H1 → ошибка "headers must not overlap"
```
### 4. Signature Packets (i1-i5) — мимикрия под протоколы (NEW в 2.0)
| Параметр | Тип | Индекс в массиве |
|----------|-----|-------------------|
| **i1** | string (CPS) | `device.ipackets[0]` |
| **i2** | string (CPS) | `device.ipackets[1]` |
| **i3** | string (CPS) | `device.ipackets[2]` |
| **i4** | string (CPS) | `device.ipackets[3]` |
| **i5** | string (CPS) | `device.ipackets[4]` |
- До 5 пакетов, отправляемых **перед** каждым WireGuard handshake
- Описываются на языке CPS (Custom Protocol Signature)
- Если пакет не настроен (`nil`) — пропускается
- Хранятся в `device.ipackets [5]*obfChain`
**Отправка** (из `SendHandshakeInitiation()`):
```go
for _, ipacket := range peer.device.ipackets {
if ipacket != nil {
buf := make([]byte, ipacket.ObfuscatedLen(0))
ipacket.Obfuscate(buf, nil) // src = nil, генерация из тегов
sendBuffer = append(sendBuffer, buf)
}
}
```
---
## CPS — язык описания сигнатур
### Все 8 тегов amneziawg-go (из исходного кода)
> [!warning] Набор тегов различается между реализациями
> Всё в этом разделе — про userspace-движок amneziawg-go. Модуль ядра Linux разбирает другой набор (сверено 20 августа 2026 года на тегах линий 3.0/3.1, функция `jp_parse_tags()` в `src/junk.c`): `<b>`, `<t>`, `<r>`, `<rc>`, `<rd>` плюс собственный недокументированный `<c>` — 4 байта, счётчик спецпакетов (u32 big-endian, при инициации рукопожатия стартует со случайного значения и растёт с каждым отправленным пакетом). Тегов `<d>`, `<ds>`, `<dz>` модуль ядра не знает: конфигурация с ними отвергается целиком с `-EINVAL`, причём пояснение `I1-packet invalid format` печатается через `net_dbg_ratelimited()` и без включённого dynamic debug в `dmesg` не появляется. Обратно, конфигурация с `<c>` не поднимется на go-движке — в журнале демона будет `unknown tag <c>`. Переносимые между реализациями рецепты `I1``I5` — только пересечение `{b, r, rc, rd, t}`. Важно: какая реализация окажется активной, определяет не тип узла, а наличие модуля на хосте — контейнер видит `/sys` хоста, и установка `amneziawg-dkms` рядом переводит его на модуль ядра при следующем запуске, меняя допустимый набор тегов на противоположный. Механика `awg-quick`, контекст и таблица расхождений — в [[amnezia-3-0/internals|разборе внутреннего устройства AWG 3.0]].
Зарегистрированы в `obfBuilders` map в `obf.go`:
```go
var obfBuilders = map[string]obfBuilder{
"b": newBytesObf, // obf_bytes.go
"t": newTimestampObf, // obf_timestamp.go
"r": newRandObf, // obf_rand.go
"rc": newRandCharObf, // obf_randchars.go (файл с 's'!)
"rd": newRandDigitsObf, // obf_randdigits.go
"d": newDataObf, // obf_data.go
"ds": newDataStringObf, // obf_datastring.go
"dz": newDataSizeObf, // obf_datasize.go
}
```
| Тег | Формат | Параметр | Размер вывода | Описание | Документирован? |
|-----|--------|----------|---------------|----------|-----------------|
| `<b>` | `<b 0xDEADBEEF>` | **обязателен** (hex) | len(hex)/2 байт | Фиксированные байты | Да |
| `<t>` | `<t>` | игнорируется | 4 байта | Unix timestamp, big-endian uint32 | Да |
| `<r>` | `<r 100>` | **обязателен** (int) | N байт | Криптографически случайные байты | Да |
| `<rc>` | `<rc 10>` | **обязателен** (int) | N байт | Случайные буквы a-zA-Z (52 символа) | Да |
| `<rd>` | `<rd 5>` | **обязателен** (int) | N байт | Случайные цифры 0-9 | Да |
| `<d>` | `<d>` | игнорируется | = input | Pass-through (копия входных данных) | **Нет** |
| `<ds>` | `<ds>` | игнорируется | ~133% input | Base64 RawStdEncoding (без '=' padding) | **Нет** |
| `<dz>` | `<dz 4>` | **обязателен** (int) | N байт (фикс.) | Длина входных данных в big-endian байтах | **Нет** |
### Синтаксис CPS
```
Формат: <тег параметр>
Цепочка: <тег1 параметр1><тег2 параметр2><тег3>...
```
**Парсер** (`newObfChain()` в `obf.go`):
- Ищет теги между `<` и `>`
- Имя тега — первый токен (до пробела), параметр — второй токен
- Теги обрабатываются последовательно слева направо
- Результаты конкатенируются в один пакет
- Ошибки **не** останавливают парсинг — собираются через `errors.Join()` и возвращаются все разом
**Ошибки парсера:**
- `"missing enclosing >"` — незакрытый тег
- `"empty tag"` — пустые скобки `<>`
- `"unknown tag <X>"` — тег не найден в `obfBuilders`
- `"failed to build <X>: ..."` — ошибка конструктора тега
### Интерфейс обфускатора
```go
type obf interface {
Obfuscate(dst, src []byte) // Записывает в dst
Deobfuscate(dst, src []byte) bool // Валидация + восстановление
ObfuscatedLen(srcLen int) int // Размер выхода
DeobfuscatedLen(srcLen int) int // Размер после деобфускации
}
```
### Детали реализации каждого тега
**`<b 0xHEX>` — фиксированные байты** (`obf_bytes.go`)
```
Вход: hex-строка, префикс "0x" опционален
Обработка префикса: strings.TrimPrefix(val, "0x") — только СТРОЧНЫЙ "0x"!
"0X" (заглавный) НЕ распознаётся и останется в строке → ошибка.
Примеры: "0xDEADBEEF" → "DEADBEEF", "DEADBEEF" → "DEADBEEF"
Выход: бинарные данные из hex.DecodeString()
Размер: len(hex_digits) / 2
Ошибки:
- "empty argument" — пустая строка (после trim)
- "odd amount of symbols" — НЕЧЁТНОЕ кол-во hex-цифр (каждый байт = 2 hex-цифры)
- hex.DecodeString error — невалидные hex-символы
Deobfuscate: проверяет ТОЧНОЕ побайтовое совпадение → false если не совпали
⚠ ВНИМАНИЕ: hex-строка ДОЛЖНА содержать ЧЁТНОЕ число hex-цифр!
"0xDEADBEEF" → 8 цифр → OK (4 байта)
"0xc7000000010" → 11 цифр → ОШИБКА "odd amount of symbols"!
"0xc70000000108" → 12 цифр → OK (6 байт)
```
**`<t>` — timestamp** (`obf_timestamp.go`)
```
Вход: параметр игнорируется (конструктор: newTimestampObf(_ string))
<t> и <t anything> — оба валидны
Выход: 4 байта = time.Now().Unix() в big-endian uint32
Deobfuscate: ВСЕГДА true (нет проверки значения!)
DeobfuscatedLen: 0
В коде комментарий: "replay attack check? requires time to be always synchronized"
→ защита от replay НЕ реализована
```
**`<r N>` — случайные байты** (`obf_rand.go`)
```
Вход: N — целое число (strconv.Atoi), обязательный параметр
Выход: N байт из crypto/rand.Read
Deobfuscate: ВСЕГДА true (невозможно проверить случайность)
DeobfuscatedLen: 0
В коде: "// there is no way to validate randomness :)"
```
**`<rc N>` — случайные буквы** (`obf_randchars.go`)
```
Вход: N — целое число
Алфавит: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" (52 символа)
Генерация: random_byte % 52 → индекс в алфавите
Deobfuscate: проверяет unicode.IsLetter() для каждого байта
DeobfuscatedLen: 0
```
**`<rd N>` — случайные цифры** (`obf_randdigits.go`)
```
Вход: N — целое число
Алфавит: "0123456789" (10 символов)
Генерация: random_byte % 10 → индекс
Deobfuscate: проверяет unicode.IsDigit() для каждого байта
DeobfuscatedLen: 0
```
**`<d>` — pass-through** (`obf_data.go`) — **НЕ ДОКУМЕНТИРОВАН**
```
Параметр: игнорируется (<d> и <d anything> — оба валидны)
Назначение: передаёт входные данные (src) без изменений в dst
ObfuscatedLen(n) = n (размер не меняется)
DeobfuscatedLen(n) = n
Deobfuscate: всегда true
В signature packets (i1-i5) src=nil → <d> выводит 0 байт (бесполезен)
```
**`<ds>` — Base64** (`obf_datastring.go`) — **НЕ ДОКУМЕНТИРОВАН**
```
Параметр: игнорируется
Назначение: кодирует src в Base64 (encoding/base64.RawStdEncoding, без '=' padding)
ObfuscatedLen(n) = base64.RawStdEncoding.EncodedLen(n) (≈133% от входа)
DeobfuscatedLen(n) = base64.RawStdEncoding.DecodedLen(n)
Deobfuscate: декодирует Base64, но ИГНОРИРУЕТ ошибки декодирования (потенциальный баг)
В signature packets (i1-i5) src=nil → <ds> выводит 0 байт (бесполезен)
```
**`<dz N>` — размер данных** (`obf_datasize.go`) — **НЕ ДОКУМЕНТИРОВАН**
```
Вход: N — количество байт для кодирования размера (strconv.Atoi), ОБЯЗАТЕЛЕН
Назначение: записывает len(src) как big-endian число в N байт
Алгоритм:
for i := N-1; i >= 0; i-- {
dst[i] = byte(srcLen & 0xFF)
srcLen >>= 8
}
ObfuscatedLen(n) = N (фиксированный размер)
DeobfuscatedLen(n) = 0
Deobfuscate: всегда true
В signature packets (i1-i5) src=nil → len(nil)=0 → выводит N нулевых байт (0x00...00)
Пример: <dz 2> в i1 → всегда выдаёт 0x0000
```
---
## Порядок отправки пакетов (верифицировано по send.go)
### SendHandshakeInitiation() — полная последовательность
```
HANDSHAKE (каждые ~2 минуты, проверка RekeyTimeout):
┌─────────────────────────────────────────────────────────────┐
│ 1. Signature packets: i1 → i2 → i3 → i4 → i5 │
│ (nil пропускаются, каждый — отдельный UDP-пакет) │
├─────────────────────────────────────────────────────────────┤
│ 2. Junk packets: Jc штук │
│ Размер каждого: rand(Jmin..Jmax) байт │
│ Содержимое: crypto/rand.Read │
├─────────────────────────────────────────────────────────────┤
│ 3. Handshake Init message: │
│ a) CreateMessageInitiation() → msg.Type = H1.Generate() │
│ b) binary.Write(LittleEndian, msg) → 148 байт │
│ c) cookieGenerator.AddMacs(packet) → MAC1 + MAC2 │
│ d) Если S1 > 0: [S1 random bytes][packet] │
│ Итого: S1 + 148 байт │
└─────────────────────────────────────────────────────────────┘
Всё отправляется ОДНИМ вызовом peer.SendBuffers(sendBuffer)
```
### SendHandshakeResponse() — отдельно
**Signature packets (i1-i5) и junk packets НЕ отправляются с Response!**
Они отправляются ТОЛЬКО с Init. Это подтверждено по коду: SendHandshakeResponse()
не содержит ссылок на `device.ipackets` или `device.junk`.
```
┌─────────────────────────────────────────────────────────────┐
│ 4. Handshake Response message: │
│ a) CreateMessageResponse() → msg.Type = H2.Generate() │
│ b) binary.Write(LittleEndian, msg) → 92 байта │
│ c) BeginSymmetricSession() → деривация ключей │
│ d) cookieGenerator.AddMacs(packet) │
│ e) Если S2 > 0: [S2 random bytes][packet] │
│ f) SendBuffers([][]byte{packet}) — один пакет │
│ Итого: S2 + 92 байта │
└─────────────────────────────────────────────────────────────┘
```
### SendHandshakeCookie() — под нагрузкой
```
ПОД НАГРУЗКОЙ (DoS protection, редко):
┌─────────────────────────────────────────────────────────────┐
│ 5. Cookie Reply: │
│ a) msgType = H3.Generate() │
│ b) cookieChecker.CreateReply(..., msgType) │
│ c) binary.Write → 64 байта │
│ d) Если S3 > 0: [S3 random bytes][packet] │
│ Итого: S3 + 64 байта │
│ │
│ Отправляется через device.net.bind.Send() НАПРЯМУЮ │
│ (не через peer queue, в отличие от Init/Response) │
└─────────────────────────────────────────────────────────────┘
```
### RoutineEncryption() + RoutineSequentialSender() — data трафик
```
DATA ТРАФИК (постоянно):
┌─────────────────────────────────────────────────────────────┐
│ 6. Transport packet: │
│ a) RoutineEncryption(): │
│ - H4.Generate() → первые 4 байта (little-endian) │
│ - calculatePaddingSize() → выравнивание до 16 байт │
│ - AEAD шифрование (ChaCha20-Poly1305) │
│ b) RoutineSequentialSender(): │
│ - Если НЕ keepalive И S4 > 0: │
│ сдвиг данных вправо на S4, random prefix │
│ Итого: S4 + 16B header + encrypted payload + 16B align │
│ │
│ Keepalive: S4 НЕ применяется (len == MessageKeepaliveSize)│
└─────────────────────────────────────────────────────────────┘
```
### Приём пакетов — DeterminePacketTypeAndPadding()
Функция в `receive.go` определяет тип пакета по размеру + magic header:
```go
// Для Init/Response/Cookie — ТОЧНОЕ совпадение размера:
if size == padding + MessageInitiationSize { ... } // S1 + 148
if size == padding + MessageResponseSize { ... } // S2 + 92
if size == padding + MessageCookieReplySize { ... } // S3 + 64
// Для Transport — больше или равно (переменный payload):
if size >= padding + MessageTransportHeaderSize { ... } // S4 + 16+
```
Затем:
1. Пропускает `padding` байт
2. Читает uint32 из первых 4 байт (little-endian)
3. Валидирует через `header.Validate(value)`
4. При совпадении — убирает padding: `copy(packet, packet[padding:])` + truncate
**Junk и Signature пакеты на приёме:**
Явной обработки нет. Junk-пакеты и signature-пакеты не проходят проверку `DeterminePacketTypeAndPadding()` (возвращается `MessageUnknownType`) и **молча отбрасываются** — ни ошибок, ни логов. Это by design: они нужны только для обмана DPI на сетевом уровне.
**Нет fallback к стандартному WireGuard:**
Функция проверяет пакеты **только** через настроенные AWG-заголовки (H1-H4). Если H1-H4 изменены, стандартные WireGuard-пакеты (type=1,2,3,4) будут отброшены как `MessageUnknownType`. Обратной совместимости с обычным WireGuard при изменённых заголовках нет.
---
## Примеры конфигураций
### Минимальная конфигурация AWG 2.0
```ini
[Interface]
PrivateKey = YOUR_KEY
Address = 10.8.1.2/24
DNS = 1.1.1.1
# Junk
Jc = 5
Jmin = 50
Jmax = 500
# Padding
S1 = 40
S2 = 40
# Headers (фиксированные, совместимость с 1.0)
H1 = 123456789
H2 = 987654321
H3 = 111111111
H4 = 222222222
[Peer]
PublicKey = SERVER_KEY
Endpoint = server:51820
AllowedIPs = 0.0.0.0/0
```
### Полная конфигурация AWG 2.0
```ini
[Interface]
PrivateKey = YOUR_KEY
Address = 10.8.1.2/24
DNS = 1.1.1.1, 1.0.0.1
# --- Junk packets ---
Jc = 7
Jmin = 50
Jmax = 1000
# --- Padding (все типы) ---
S1 = 68
S2 = 149
S3 = 32
S4 = 16
# --- Range-based headers ---
H1 = 471800590-471800690
H2 = 1246894907-1246895000
H3 = 923637689-923637690
H4 = 1769581055-1869581055
# --- Signature packets (мимикрия под QUIC) ---
i1 = <b 0xc70000000108><rc 8><t><r 100>
i2 = <b 0xf6ab3267fa><t><rc 20><r 80>
[Peer]
PublicKey = SERVER_KEY
Endpoint = server:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
```
### Примеры сигнатур для мимикрии
**DNS-запрос:**
```
i1 = <b 0x1a2d0100000100000000000109696e666572656e6365><t><r 15>
```
Побайтовый разбор hex (верифицировано по RFC 1035):
- `1a2d` — Transaction ID (произвольный)
- `0100` — Flags: Standard Query, Recursion Desired
- `0001` — QDCOUNT: 1 вопрос
- `0000 0000` — ANCOUNT=0, NSCOUNT=0
- `0001` — ARCOUNT: 1 additional record
- `09` — DNS label length = 9
- `696e666572656e6365` — ASCII "inference"
**QUIC Initial (верифицировано по RFC 9000):**
```
i1 = <b 0xc70000000108><rc 8><t><r 100>
```
Побайтовый разбор hex:
- `c7` = `11000111` — Form=1 (Long), Fixed=1, Type=00 (Initial), Reserved=0111
- `00000001` — Version = QUIC v1
- `08` — DCID Length = 8 байт
- `<rc 8>` — 8 случайных букв имитируют Destination Connection ID
- `<t>` — timestamp добавляет уникальность каждому handshake
- `<r 100>` — 100 случайных байт заполняют payload
**SIP INVITE (текстовый протокол):**
```
i1 = <b 0x494e56495445207369703a><rc 12><b 0x4053697056504e2e636f6d20534950><r 50>
```
Hex = "INVITE sip:" + random user + "@SipVPN.com SIP" + random payload.
**Многопакетная сигнатура:**
```
i1 = <b 0xc70000000108><rc 8><t><r 100> # мимикрия под QUIC Initial
i2 = <b 0xf6ab3267fa><t><rc 20><r 80> # произвольные магические байты (не протокол)
i3 = <r 200> # чистый шум
```
3 пакета перед каждым handshake: QUIC-подобный + кастомный + чистый шум.
**С недокументированным тегом `<dz>`:**
```
i1 = <dz 2><r 50>
```
2 нулевых байта (т.к. src=nil в signature packets, len=0) + 50 случайных байт.
Может имитировать length-prefixed протокол с пустым полем длины.
> **Примечание:** теги `<d>` и `<ds>` **бесполезны** в i1-i5, т.к. signature packets
> вызывают `Obfuscate(buf, nil)` — src всегда nil, и эти теги выводят 0 байт.
> Они предназначены для возможного использования obfChain в других контекстах.
---
## Подводные камни и ограничения
### 1. MTU overflow при S4
S4 добавляется поверх стандартного WireGuard-выравнивания и AEAD overhead.
**Расчёт размера пакета на проводе** (network MTU обычно = 1500):
```
IP header: 20 байт
UDP header: 8 байт
S4 random prefix: S4 байт
WG transport header: 16 байт (H4 type + receiver + nonce)
Encrypted payload: padded_plaintext байт (ChaCha20, размер = вход)
Poly1305 auth tag: 16 байт
─────────────────────────────
ИТОГО IP-пакет = 60 + S4 + padded_plaintext
Максимальный plaintext без фрагментации:
max_plaintext = network_MTU - 60 - S4
S4=0: max = 1500 - 60 = 1440 (стандартный WireGuard)
S4=16: max = 1500 - 76 = 1424
S4=50: max = 1500 - 110 = 1390
Рекомендуемый TUN MTU = network_MTU - 60 - S4
S4=0 → TUN MTU ≈ 1420 (стандартный WG дефолт)
S4=16 → TUN MTU ≈ 1400
S4=50 → TUN MTU ≈ 1370
```
**IPv6:** заголовок IPv6 = 40 байт (вместо 20 у IPv4). Формула для IPv6:
```
max_plaintext_ipv6 = network_MTU - 80 - S4 (40+8+16+16=80)
```
**Важно:** `calculatePaddingSize()` работает с TUN MTU (размер payload от TUN-устройства), а не с network MTU. Если TUN MTU не уменьшен для компенсации S4, пакеты будут фрагментироваться на уровне IP.
### 2. Дефолтные значения и совместимость
AWG-параметры по умолчанию:
- **H1-H4**: инициализированы стандартными WireGuard-типами (1, 2, 3, 4) в `NewDevice()`НЕ nil!
- **S1-S4, Jc/Jmin/Jmax**: 0 (обфускация выключена)
- **i1-i5**: nil (signature packets отключены)
Без явной конфигурации AWG-параметров протокол полностью совместим с обычным WireGuard.
Параметры клиента и сервера **должны совпадать** — иначе стороны не смогут декодировать пакеты.
### 3. Timestamp без replay-защиты
Тег `<t>` записывает `time.Now().Unix()`, но при deobfuscation **всегда возвращает true**. Код содержит комментарий: `"replay attack check? requires time to be always synchronized"` — защита не реализована.
### 4. Random нельзя валидировать
`<r N>` при deobfuscation возвращает true безусловно. `DeobfuscatedLen` = 0. Комментарий: `"there is no way to validate randomness :)"`.
### 5. Header overlap
Диапазоны H1-H4 проверяются на пересечение при **config merge** (после установки всех 4-х заголовков). Ошибка: `"headers must not overlap"`. Если пересекаются → невозможно определить тип пакета.
### 6. Keepalive без S4
S4 **не применяется** к keepalive (проверка `len(elem.packet) != MessageKeepaliveSize`). Это потенциальный fingerprint для DPI — keepalive-пакеты имеют предсказуемый размер.
### 7. Размер сигнатурных пакетов
- Минимум: 100+ байт (короткие подозрительны для DPI)
- Оптимум: 100-500 байт
- Максимум: ~1200 байт (UDP MTU)
- Нет жёсткого ограничения в коде, но > MTU → фрагментация
### 8. Jmin/Jmax не проверяются перекрёстно
В коде нет валидации `Jmin <= Jmax`. Каждый параметр проверяется только на `> 0` независимо.
### 9. `<ds>` игнорирует ошибки декодирования
`Deobfuscate()` в `obf_datastring.go` вызывает `base64.Decode()`, но **отбрасывает ошибку** — потенциальный баг в реализации.
### 10. `<b>` принимает только строчный "0x"
`strings.TrimPrefix(val, "0x")` — убирает только `0x`, но **НЕ** `0X`. Если написать `<b 0XDEAD>`, hex-парсинг сломается.
### 11. Signature/junk только при Initiation
Signature packets (i1-i5) и junk packets отправляются **только** при `SendHandshakeInitiation()`. `SendHandshakeResponse()` не содержит ссылок на `device.ipackets` и `device.junk` — Response отправляется без маскировки (только S2 padding и H2 header).
### 12. Transport пакеты батчатся
`RoutineSequentialSender()` собирает несколько зашифрованных transport-пакетов в `bufs` (до `maxBatchSize` штук) и отправляет одним вызовом `peer.SendBuffers(bufs)`. S4 padding применяется к каждому пакету в batch индивидуально.
### 13. Пробелы в hex-строках `<b>` молча обрезают данные
Парсер CPS использует `strings.Fields()` и берёт только `parts[1]`:
```
<b 0xDEADBEEF> → val="0xDEADBEEF" → OK, 4 байта
<b 0xDE AD BE EF> → val="0xDE" → ТОЛЬКО 1 байт! (AD BE EF потеряны)
```
Ошибки нет, данные молча теряются. Все hex-цифры должны быть слитно.
### 14. Текст между тегами CPS молча игнорируется
```
"hello<r 10>world<t>" → "hello" и "world" отброшены без ошибок
"<r 10> trailing text" → "trailing text" отброшен
```
Парсер ищет только содержимое внутри `< >`, всё остальное пропускается.
### 15. Только один динамический тег на цепочку
Теги `<d>` и `<ds>` имеют переменную длину выхода (зависит от входных данных). При `Deobfuscate()` длина вычисляется как:
```go
dynamicLen := len(src) - c.ObfuscatedLen(0) // все динамические байты
```
Эта формула корректна **только если в цепочке один динамический тег**. Два динамических тега (напр. `<d><ds>`) вызовут buffer overrun — первый заберёт все байты, второму ничего не останется. Ограничение не документировано и не валидируется парсером.
В контексте signature packets (i1-i5) это неактуально — src=nil, динамических данных нет.
---
## Рекомендации по выбору значений
### Для обхода базовых DPI (Россия, 2026)
```ini
# Достаточно для большинства случаев
Jc = 5
Jmin = 50
Jmax = 500
S1 = 40
S2 = 40
S3 = 0
S4 = 0
H1 = 100000000-200000000
H2 = 300000000-400000000
H3 = 500000000-600000000
H4 = 700000000-800000000
```
### Для продвинутых DPI (полная мимикрия)
```ini
Jc = 7
Jmin = 50
Jmax = 1000
S1 = 68
S2 = 149
S3 = 32
S4 = 16
H1 = 471800590-471800690
H2 = 1246894907-1246895000
H3 = 923637689-923637690
H4 = 1769581055-1869581055
i1 = <b 0xc70000000108><rc 8><t><r 100>
i2 = <b 0xf6ab3267fa><t><rc 20><r 80>
```
### Баланс безопасность vs скорость
| Параметр | Влияние на скорость | Влияние на обфускацию | Когда работает |
|----------|--------------------|-----------------------|----------------|
| Jc | Минимальное | Среднее | Только handshake |
| S1, S2 | Минимальное | Среднее | Только handshake |
| S3 | Минимальное | Низкое | Редко (DoS) |
| **S4** | **Значительное** | **Высокое** | **Каждый пакет** |
| H1-H4 ranges | Минимальное (1 rand/пакет) | Высокое | Каждый пакет |
| i1-i5 | Минимальное | Высокое | Только handshake |
**S4 — единственный параметр с заметным влиянием на throughput.** Начинайте с S4=0, увеличивайте при необходимости.
---
## Структура в исходном коде
```
amneziawg-go/device/
├── device.go # Device struct: junk, paddings, headers, ipackets[5]
├── uapi.go # IPC парсер: jc/jmin/jmax, s1-s4, h1-h4, i1-i5
├── send.go # SendHandshakeInitiation/Response/Cookie, RoutineEncryption/Sender
├── receive.go # DeterminePacketTypeAndPadding(), RoutineReceiveIncoming
├── noise-protocol.go # CreateMessageInitiation/Response → H1/H2 headers
├── magic-header.go # magicHeader: newMagicHeader(), Generate(), Validate()
├── constants.go # PaddingMultiple=16, RekeyAfterTime=120s, etc.
├── obf.go # obfChain, obfBuilders map, newObfChain() парсер
├── obf_bytes.go # <b> — фиксированные hex-байты
├── obf_timestamp.go # <t> — Unix timestamp (4B big-endian)
├── obf_rand.go # <r> — crypto/rand случайные байты
├── obf_randchars.go # <rc> — случайные буквы a-zA-Z (52 символа)
├── obf_randdigits.go # <rd> — случайные цифры 0-9
├── obf_data.go # <d> — pass-through (НЕ ДОКУМЕНТИРОВАН)
├── obf_datastring.go # <ds> — Base64 RawStdEncoding (НЕ ДОКУМЕНТИРОВАН)
└── obf_datasize.go # <dz> — длина данных в байтах (НЕ ДОКУМЕНТИРОВАН)
```
---
## Сравнение AWG 1.0 vs 2.0
| Возможность | AWG 1.0 | AWG 2.0 |
|-------------|---------|---------|
| Junk packets | Jc, Jmin, Jmax | Jc, Jmin, Jmax (без изменений) |
| Handshake padding | S1, S2 | S1, S2, **S3**, **S4** |
| Заголовки | H1-H4 (фиксированные uint32) | H1-H4 (**range-based**, N-M) |
| Мимикрия | Нет | **i1-i5 + CPS** |
| CPS теги | Нет | **8 тегов** (5 документированных + 3 скрытых) |
| DPI bypass | Signature-based only | **Signature + statistical + protocol mimicry** |
---
## Побайтовая структура сообщений WireGuard
Размеры верифицированы по структурам в `noise-protocol.go`:
### MessageInitiation — 148 байт
```
Offset Size Field
────── ──── ─────────────────────────────
0 4 Type (uint32, H1 magic header)
4 4 Sender (uint32, индекс отправителя)
8 32 Ephemeral (NoisePublicKey)
40 48 Static (NoisePublicKey 32 + Poly1305 Tag 16)
88 28 Timestamp (TAI64N 12 + Poly1305 Tag 16)
116 16 MAC1 (blake2s-128)
132 16 MAC2 (blake2s-128)
────── ────
148 ИТОГО
```
### MessageResponse — 92 байта
```
Offset Size Field
────── ──── ─────────────────────────────
0 4 Type (uint32, H2 magic header)
4 4 Sender (uint32)
8 4 Receiver (uint32)
12 32 Ephemeral (NoisePublicKey)
44 16 Empty (Poly1305 Tag, encrypted empty)
60 16 MAC1 (blake2s-128)
76 16 MAC2 (blake2s-128)
────── ────
92 ИТОГО
```
### MessageCookieReply — 64 байта
```
Offset Size Field
────── ──── ─────────────────────────────
0 4 Type (uint32, H3 magic header)
4 4 Receiver (uint32)
8 24 Nonce (XChaCha20-Poly1305 nonce)
32 32 Cookie (blake2s-128 16 + Poly1305 Tag 16)
────── ────
64 ИТОГО
```
### MessageTransport — 16+ байт (переменный)
```
Offset Size Field
────── ──────── ─────────────────────────────
0 4 Type (uint32, H4 magic header)
4 4 Receiver (uint32)
8 8 Counter (uint64, nonce)
16 variable Ciphertext (encrypted payload + 16B Poly1305 auth tag)
────── ────────
16+ ИТОГО (заголовок фиксирован, payload переменный)
```
### Полный wire-format transport пакета (с AWG обфускацией)
```
Порядок формирования (из кода):
1. RoutineEncryption():
- Генерирует H4 header → записывает в buffer[0:16]
- calculatePaddingSize() → добавляет нули к payload (выравнивание до 16 байт)
- AEAD Seal(header, nonce, padded_payload, nil):
ciphertext = Encrypt(padded_payload) + 16B Poly1305 tag
Результат: [16B header][ciphertext][16B tag]
2. RoutineSequentialSender() (если S4 > 0 и НЕ keepalive):
- Сдвигает весь зашифрованный пакет вправо на S4 байт
- Заполняет первые S4 байт случайными данными
Итоговый пакет на проводе:
┌──────────────┬──────────────────────────┬────────────────────────────┬───────────┐
│ S4 random │ WG Transport Header │ encrypted(payload + align) │ Poly1305 │
│ (S4 байт) │ H4(4B) + recv(4B) + nonce│ (переменный) │ (16B tag) │
│ │ (8B) = 16 байт │ │ │
└──────────────┴──────────────────────────┴────────────────────────────┴───────────┘
↑ header (16B, LE) ↑ ciphertext ↑
```
### MessageKeepalive — 32 байта
```
= MessageTransport с нулевым payload:
16B header + 16B Poly1305 tag (шифрование пустых данных) = 32 байта
MessageKeepaliveSize = MessageTransportSize = 32
```
---
## Стандартные константы WireGuard (constants.go)
```
RekeyAfterMessages = 2^60 сообщений
RejectAfterMessages = 2^64 - 2^13 - 1
RekeyAfterTime = 120 секунд ← интервал handshake (~2 мин)
RekeyAttemptTime = 90 секунд
RekeyTimeout = 5 секунд ← double-lock check в SendHandshakeInitiation
RekeyTimeoutJitterMaxMs = 334 мс
RejectAfterTime = 180 секунд
KeepaliveTimeout = 10 секунд
CookieRefreshTime = 120 секунд
HandshakeInitiationRate = 1/50 секунды ← rate limit
PaddingMultiple = 16 ← WireGuard внутреннее выравнивание payload
MaxTimerHandshakes = 90 / 5 = 18 ← макс. попыток handshake
```
**Стандартные type values** (заменяются H1-H4):
```
MessageInitiationType = 1
MessageResponseType = 2
MessageCookieReplyType = 3
MessageTransportType = 4
MessageUnknownType = 0 (пакет не опознан → отброс)
```
---
## Дополнительные технические детали
### Double-lock pattern в SendHandshakeInitiation()
```go
// Быстрая проверка без блокировки (RLock):
peer.handshake.mutex.RLock()
if time.Since(peer.handshake.lastSentHandshake) < RekeyTimeout {
peer.handshake.mutex.RUnlock()
return nil // Слишком рано для нового handshake
}
peer.handshake.mutex.RUnlock()
// Полная блокировка с повторной проверкой (Lock):
peer.handshake.mutex.Lock()
if time.Since(peer.handshake.lastSentHandshake) < RekeyTimeout {
peer.handshake.mutex.Unlock()
return nil
}
peer.handshake.lastSentHandshake = time.Now()
peer.handshake.mutex.Unlock()
```
Классический double-check locking для предотвращения дупликатов handshake.
### calculatePaddingSize() — выравнивание payload
```go
func calculatePaddingSize(packetSize, mtu int) int {
lastUnit := packetSize
if mtu == 0 {
// Нет MTU → выравнивание до ближайших 16 байт
return ((lastUnit + 16 - 1) & ^(16 - 1)) - lastUnit
}
if lastUnit > mtu {
lastUnit %= mtu // Берём остаток от деления на MTU
}
paddedSize := ((lastUnit + 16 - 1) & ^(16 - 1))
if paddedSize > mtu {
paddedSize = mtu // Не превышать MTU
}
return paddedSize - lastUnit
}
```
Применяется ПЕРЕД AEAD-шифрованием в `RoutineEncryption()`. S4 добавляется ПОСЛЕ.
**Примеры вычислений:**
```
calculatePaddingSize(1480, 1500) = 8 # 1480 → 1488 (ближайшее кратное 16)
calculatePaddingSize(1500, 1500) = 0 # ceil(1500/16)*16=1504 > MTU → cap to 1500, pad=0
calculatePaddingSize(1501, 1500) = 15 # 1501 % 1500 = 1, ceil(1/16)*16=16, pad=15
calculatePaddingSize(0, 1500) = 0 # пустой payload (keepalive), 0 уже кратно 16
calculatePaddingSize(100, 0) = 12 # без MTU: ceil(100/16)*16=112, pad=12
```
### Нет unit-тестов для CPS
Файл `obf_test.go` в репозитории **отсутствует**. Парсер CPS и все 8 обфускаторов не покрыты тестами. Это увеличивает риск скрытых багов (как `<ds>` Deobfuscate, игнорирующий ошибки).
### UAPI: порядок параметров при сериализации (GET)
```
jc → jmin → jmax → s1 → s2 → s3 → s4 → h1 → h2 → h3 → h4 → i1 → i2 → i3 → i4 → i5
```
Параметры со значением 0/nil **не включаются** в вывод.
---
## Ошибки в статье на Хабре (по результатам анализа кода)
| Что написано в статье | Что в коде | Комментарий |
|----------------------|------------|-------------|
| Init = 144 байта | `MessageInitiationSize` = 148 байт | Статья не учитывает 4 байта type field |
| Response = 88 байт | `MessageResponseSize` = 92 байта | Аналогично |
| Cookie Reply = 64 байта (в тексте), 60 байт (в схеме) | `MessageCookieReplySize` = 64 байта | Схема в статье неверна |
| CPS: 5 тегов | 8 тегов в obfBuilders | `<d>`, `<ds>`, `<dz>` не упомянуты |
| `<rc>` = "случайные буквы/цифры [A-Za-z]" | Только буквы a-zA-Z (52 символа) | В статье ошибочно написано "букв/цифр" |
| QUIC пример: `<b 0xc7000000010>` | 11 hex-цифр = **нечётное** → ОШИБКА | Реальные конфиги: один `<b>` blob ~1250 байт (чётное) |
| Сигнатуры из мелких тегов `<b><rc><t><r>` | Клиент генерирует один `<b 0x...>` blob | `<rc>`,`<t>`,`<r>` для ручных конфигов; клиент пакует всё в статический hex |
| Signature packets "перед каждым handshake" | Только перед **Init**, НЕ Response | SendHandshakeResponse() не содержит ipackets/junk |
---
## Реальные конфиги AmneziaVPN vs примеры из статьи
### Дефолтное значение I1 в клиенте AmneziaVPN
Найдено в исходниках клиента — `client/protocols/protocols_defs.h`:
```cpp
constexpr char defaultSpecialJunk1[] =
"<r 2><b 0x858000010001000000000669636c6f7564036366726d0000010001c00c00010001000105a0004445837373>";
// I2-I5 по умолчанию пустые (отключены)
```
Это **составной формат**:
- `<r 2>` — 2 случайных байта (уникальность каждого handshake)
- `<b 0x...>` — 43 байта статического DNS-подобного пакета (домен "icloud.cfrm")
I1-I5 в серверном скрипте `configure_container.sh` **закомментированы** (отключены по умолчанию).
### Два формата конфигов
**Формат 1 — составной (клиент AWG 2.0, `protocols_defs.h`):**
```
I1 = <r 2><b 0x8580...DNS-пакет...>
↑ random ↑ статические байты
```
Несколько тегов, есть рандомизация через `<r>`, `<t>`, `<rc>`. Каждый handshake — уникальный пакет.
**Формат 2 — монолитный blob (конфиги старых версий/WARP):**
```
I1 = <b 0xc70000000108df2b1b...2500 hex-цифр...896>
↑ ОДИН тег <b>, 1250 байт статических данных
```
Весь пакет сгенерирован заранее как один hex-blob. Каждый handshake — одинаковые байты.
**Статья Хабр** (образовательный пример, сломанный):
```
I1 = <b 0xc7000000010><rc 8><t><r 100>
↑ 11 hex-цифр = нечётное → ошибка парсера "odd amount of symbols"
```
### Какой формат правильный?
Оба формата валидны для парсера `newObfChain()` в amneziawg-go. Разница в DPI-устойчивости:
| | Составной (`<r><b><t>`) | Монолитный (`<b>` blob) |
|---|---|---|
| Каждый handshake | Уникальный пакет | Одинаковые байты |
| DPI fingerprint | Сложнее | Проще (статический паттерн) |
| Размер конфига | Компактный | Огромный (2500+ hex-цифр) |
| Генерация | Ручная или клиент AWG 2.0 | Клиент AWG 1.x / pre-generated |
---
## Что документирует README репозитория
README (`amneziawg-go/README.md`) документирует **только** эти CPS-теги:
```
<b 0x[seq]>, <r [size]>, <rd [size]>, <rc [size]>, <t>
```
Теги `<d>`, `<ds>`, `<dz>` **не упомянуты** ни в README, ни в статье на Хабре — обнаружены только анализом исходного кода (`obfBuilders` map в `obf.go`).
README рекомендует Jc = 4-12 и отмечает что все параметры дефолтятся в 0.
---
## Метаданные репозитория
- **Версия**: 0.0.20250522 (из `version.go`)
- **Go**: >= 1.24.4
- **AWG 2.0 merge**: сентябрь 2025 (PR #91 — ranged H1-H4, S3/S4)
- **Последний значимый коммит**: 2025-12-01 — рефакторинг junk packets (#103)
- **Unit-тесты CPS**: отсутствуют (`obf_test.go` не существует)
- **Примеры конфигов**: нет (конфигурация только через IPC/UAPI)