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

56 KiB
Raw Permalink Blame History

date tags aliases link description
2026-03-25
amneziawg
wireguard
обфускация
обход-блокировок
AmneziaWG 2.0
AWG 2.0
Амнезия 2.0
АмнезияВГ 2.0
Амнезия ВПН параметры конфига
Что означают Jc Jmin Jmax в конфиге
Что такое S1 S2 S3 S4 и H1 H2 H3 H4
Сигнатурные пакеты I1-I5 и язык CPS
https://github.com/amnezia-vpn/amneziawg-go Справочник всех 17 параметров AmneziaWG 2.0 по исходному коду: Jc/Jmin/Jmax, padding S1S4, заголовки H1H4 и сигнатурные пакеты I1I5.

[!mirror] Резервное зеркало Актуальная версия этой страницы — на основной вики: wiki.zapret.moe/amnezia-2-0/reference

AmneziaWG 2.0 — полный справочник параметров

Составлен на основе анализа исходного кода amneziawg-go Верифицировано по исходникам: 2026-03-25

[!info] У протокола есть третье поколение Справочник описывает AmneziaWG 2.0. 24 июля 2026 года вышло третье поколение — AmneziaWG 3.0 с шифрованием заголовков пакетов: обзор в amnezia-3-0/reference, побайтовый разбор по коду — в amnezia-3-0/internals, сопутствующий релиз приложения — в amnezia-3-0/client-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()):

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

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 — они возьмутся из текущего конфига.

// 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()):

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>. Переносимые между реализациями рецепты I1I5 — только пересечение {b, r, rc, rd, t}. Важно: какая реализация окажется активной, определяет не тип узла, а наличие модуля на хосте — контейнер видит /sys хоста, и установка amneziawg-dkms рядом переводит его на модуль ядра при следующем запуске, меняя допустимый набор тегов на противоположный. Механика awg-quick, контекст и таблица расхождений — в amnezia-3-0/internals.

Зарегистрированы в obfBuilders map в obf.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>: ..." — ошибка конструктора тега

Интерфейс обфускатора

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:

// Для 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

[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

[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() длина вычисляется как:

dynamicLen := len(src) - c.ObfuscatedLen(0)  // все динамические байты

Эта формула корректна только если в цепочке один динамический тег. Два динамических тега (напр. <d><ds>) вызовут buffer overrun — первый заберёт все байты, второму ничего не останется. Ограничение не документировано и не валидируется парсером.

В контексте signature packets (i1-i5) это неактуально — src=nil, динамических данных нет.


Рекомендации по выбору значений

Для обхода базовых DPI (Россия, 2026)

# Достаточно для большинства случаев
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 (полная мимикрия)

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

// Быстрая проверка без блокировки (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

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:

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)