mirror of
https://github.com/amnezia-vpn/amneziawg-go.git
synced 2026-10-14 05:41:54 +00:00
[GH-ISSUE #87] Fully encrypted protocol for AmneziaWG #44
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amnezia-vpn/amneziawg-go#44
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @darkk on GitHub (Jul 26, 2025).
Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-go/issues/87
Как справедливо замечено в комментарии https://github.com/amnezia-vpn/amneziawg-go/issues/75#issuecomment-2989336182, AmneziaWG не является FEP: как минимум счётчик по-прежнему остаётся на месте. Да и первые 8 байт в каждом пакете данных одинаковы на протяжении пары минут.
Исправить это обратно совместимым с WireGuard™ образом невозможно. Но и {H1,H2,H3,H4} не равные {1,2,3,4} тоже не будут совместимы, а если уж ломать совместимость — то надёжно и качественно.
Я предлагаю добавить к AmneziaWG подобие опции
tls-cryptиз OpenVPN. Ключ предлагается общий для клики peer'ов, а не свой для каждого peer'а (т.е. не TLS Crypt v2), т.к.tls-crypt-v2предполагает разделение "клиентов" и "серверов" судя по спецификации, а peer'ы в WireGuard равноправные - Initiator и Responder могут меняться местамиВ ближайшие дни пришлю pull request.
@polcodav commented on GitHub (Aug 28, 2025):
Привет, а что там с PR ?
@darkk commented on GitHub (Aug 28, 2025):
@polcodav как оно часто бывает, задача "сделать хорошо" оказалась чуть сложнее, чем я ожидал. Но, как говорится, "мы работаем над этим вопросом".
Если любопытны детали — был месяц назад небольшой драфт, простой как палка, который я дал почитать друзьям. Мы подумали, и оказалось, что есть некоторое количество вполне практически осуществимых атак, которые вполне можно закрыть. Поэтому драфт немного разросся до вот этого не до конца формального и формализованного черновика с Proof-of-Work и синхронизацией часов. Там вроде бы и сложно на первый взгляд, но вроде бы и всё по делу. После фразы
HELL HELL HELL TIME IS HELLначинается поток сознания :-)@darkk commented on GitHub (Apr 11, 2026):
7.5 месяцев спустя
Я в треде WireGuard obfuscation & active probing: staying virtuous under pressure описал сложность, которую встретил на пути построения AWG-FEP, и не самый плохой метод её обхода.
Там, конечно, чтива минут на 20 — понимаю, многабуков, но всё же буду рад услышать комментарии, если вдруг кто.
@darkk commented on GitHub (Jun 6, 2026):
ещё пару месяцев спустя
Некоторый poor-man mitigation для проблем, связанных с очевидностью текущего протокола и active probing, может выглядеть следующим образом.
latest-handshakesпри рестарте WireGuard.Device. Например:peer.persistentKeepaliveInterval— запчасть для наблюдателя.peer.persistentKeepaliveInterval, если он используется.Алгоритм для header encryption
Общая схема Encrypt(Mac(PAD(x))). Это полное нарушение The Cryptographic Doom Principle, но нам чуть-чуть поможет UDP в качестве транспорта вместо TCP — любая ошибка должна отбрасывать пакет.
Nonce для шифрования заголовков выбирается исходя их общего размера пакета: выбираются нешифруемые поля с полновесной энтропией.
Заголовков "мало", меньше 64 байт, т.е. используется один блок ChaCha20, поэтому 128-битный nonce меняет экспоненту с 2³² до 2⁴⁸ в замечании "Note that this is too short to be safely generated at random if the same key is reused more than 2³² times". Именно при таких экспонентах у birthday paradox вероятность коллизии 2⁻³², на которую опираются рекомендации NIST (IIRC).
2⁴⁸ при packet rate в 1M pps — это 9 лет, таким образом использовать фиксированный ключ
f(publicKey)для обфускации не идеально, но вполне практично.HASH(),MAC()и прочее следуют https://www.wireguard.com/protocol/, а дополнительные константы такие:Энкапсуляция Initiation и Response
Идея:
Энкапсуляция Transport
Идея:
message_typeв то, что сейчасreserved_zeroДекапсуляция
А дальше — в обратном порядке, в зависимости от одного из трёх message_type.
На первом шаге Clocked Peer ID можно проверять после DH с поправкой ±1 эпоха — это не защитит от computational DoS, но позволит обкатать код.
На втором шаге проверку Clocked Peer ID нужно будет вынести перед DH, т.е. перед вычислительно дорогим вызовом
device.staticIdentity.privateKey.sharedSecret(). Для каждого сконфигурированного Peer нужно будет добавить{u64 Epoch, int Seen, u16 HandshakeReplayFilter[24]}и поддерживать per-device lookup табличку Clocked Peer ID в Peer.Clocked Peer ID предоставляет pre-DH идентификацию, HandshakeReplayFilter ограничивает общее число Initiation за Epoch и делает невозможным replay flood. Класть туда можно, например, 16 бит от SipHash, HalfSipHash или
Blake2s(tempKey, input, 2). 16 бит вполне достаточно, вероятность дропа повторного Initiation пакета из-за коллизии получается 0.4%. 24 — этоEpochLength / RekeyTimeout.Are We Done Yet?
То, что я предлагаю, это ещё далеко от FEP:
P.S.:
Sponsored by: Scheiße und Pisse Stiftung@darkk commented on GitHub (Sep 15, 2026):
Предыстория:
v3.1.20260812исправили active probing через Cookies, но добавили тривиальный DoS при IsUnderLoad (как я и написал выше, "Dropping Cookies removes the distinguisher but breaks legitimate handshakes under load (clients cannot compute mac2)")v3.1.20260828исправили тривиальный DoS, открыв вектор Computational DoS черезcurve25519.ScalarMult(), от которого Cookies и защищали (умножение занимает ≈100us и ≈10K PPS полностью сатурируют одно CPU ядро).Среди особенностей awg v3.1.20260828 по сравнению с описаной выше схемой остаются:
Computational DoS mitigation
Задача борьбы с Computational DoS — отбросить Handshake пакеты, которых "слишком много". Cookies используют rate-limit по SrcIP, после валидации владения IP адресом. Вместо Cookie я предлагаю незначительно ослабить identity-hiding, добавив идентификатор наподобии TOTP для поиска Peer до вычисления DH.
Для TOTP нам понадобится конфигурируемый ClkIDPeriod, т.к. RekeyAfterTime стал конфигурируемым с v3.0.0, и HandshakesPerPeriod порядка 18…24, где 18 — это MaxTimerHandshakes, 24 — RekeyAfterTime / RekeyTimeout. В "норме" из этих слотов должен использоваться всего один, два-три — при потерях пакетов.
ClkIDPeriod можно сделать per-peer, а не per-device, но польза от этого ограничена возможностью смешивать peer'ов с разными ClkIDPeriod в пределах одного device, значение порядка 120s "подходит всем", а сложность кода в maintenance горутине с разными ClkIDPeriod станет заметной.
При ClkIDPeriod равном нулю MAC1 считается по-старому, MAC2 игнорируется в зависимости от
disableCookies.При ClkIDPeriod больше RekeyAfterTime.Lo() стоит написать notice в лог, т.к. у этой конфигурации есть недостаток — значние ClkID будет использоваться повторно даже в happy-path. ClkID зашифрован квази-публичным header encryption key, поэтому лучше его не повторять за пределами повторных попыток handshake, чтоб ограничить деградацию identity-hiding.
При ClkIDPeriod больше HandshakesPerPeriod * RekeyTimeout.Lo() стоит также написать notice в лог, т.к. в worst-case может не хватить слотов в
consumedHandshakesна все повторные попытки Handshake. "Все" попытки важны, т.к. peer может быть без сети, класть Handshake в очередь и отправить их "скопом", когда сеть появится.Отправка
При ClkIDPeriod > 0 отправка работает следующим образом вместо
cookieGenerator.AddMacs():Т.е. MAC2 вычисляется так же, как "раньше" вычислялся MAC1, только не по
[0:smac1], а по[0:smac2]и с peer-specific ключом, а не device-specific.Я не вижу разницы, иметь разные P2PMacRecvKey и P2PMacSendKey или же вычислять общий ключ для пары peer'ов, например, нижеуказаным методом. Ключ, необходимый для проверки входящего пакета, хранить в struct Peer хочется вслед за CookieChecker. А при отправке стоимость вычисления P2PMacSendKey на лету незначительна по сравнению с операциями с curve25519 и хранить его или нет — не так важно (CookieGenerator хранит).
Плюс использования device-specific ключа в том, чтоб для некорректных пакетов даже hashtable lookup не делать и не тратить CPU cache-miss на это.Недостаток — риск DoS'а, когда сетевой наблюдатель "возле" клиента, который вдобавок знает Header Encryption Key и Public Key "сервера". Такой наблюдатель сможет узнать ClkID для пользователя и DoS'ить его последующие handshake в пределах времени валидности ClkID. Если первый пакет дойдёт, то пользователь ничего не заметит, но если он потеряется, то retry будет заблокирован. Таким наблюдателем может быть ещё один пользователь "того же самого" AWG-сервера, который может по какой-то причине слушать трафик жертвы. В реалистичных сценариях такой наблюдатель трафик может и модифицировать и проблема лишь в том, что при миграции пользователя в другую сеть недоступность сохранится пока ClkID не протухнет.Если этот недостаток неприемлем, то за цену CPU cache-miss можно использовать peer-specific ключ для MAC вместо device-specific ключа (напримерHASH(LABEL_P2PMAC || peer.device.staticIdentity.publicKey || peer.handshake.remoteStatic || peer.handshake.presharedKey)).Maintenance
В Device при ClkIDPeriod > 0 нужно завести табличку
map [ClkID]ClkIDTableEntryctype ClkIDTableEntry struct { peer *Peer; epoch uint64 }.Сервисная горутина должна просыпаться раз в секунду, сверять epoch с часами, и, если наступила новая эпоха (по любым причинам, включая suspend/resume и перевод часов), выкидывать из ClkIDTable все записи с
epoch < (Epoch - 1) || (Epoch + 1) < epochи для каждого peer'а добавить записи для Epoch-1, Epoch и Epoch+1.Пространство для оптимизации по CPU — не считать
ClkID(Epoch, peer)несколько раз. Стоит побенчмаркать, т.к. Blake2s довольно быстрый и дополнительная сложность кода может съесть часть экономии CPU.Для оптимизации по памяти можно уменьшить ClkIDTable в полтора раза, храня всего две эпохи: посыпаясь в момент посредине эпохи, а не на её границе, и заменяя данные Epoch-1 на Epoch+1.
В каждом Peer нужно добавить
lastConsumedEpoch uint64; consumedHandshakes map[HandshakeMac]struct{}.Сбрасывать lastConsumedEpoch при переводе часов "назад" бессмысленно, т.к. WireGuard не поддерживает перевод часов назад и защита по
handshake.lastTimestampсработает в ConsumeMessageInitiation(). С этой точки зрения вопрос перевода часов "назад" — отдельный "проект".On-demand maintenance
На мобильных устройствах пересчитывать ClkIDTable каждую минуту-другую может оказаться слишком накладно с точки зрения application CPU wake-ups. Но можно реализовать ту же схему и по-другому, подробнее я написал в ClkID performance в вышеупомянутом письме.
Насколько эти попытки экономии осмысленны в условиях NAT44 и потенциальной необходимости поддерживать NAT-трансляцию keepalive'ом — надо измерять.
Приём
Получение пакета при ClkIDPeriod > 0:
По ClkID находим в ClkIDTable peer и clkidEpoch. Если не находим — пакет отбрасывается, он не из текущих эпох.
Если
clkidEpoch < peer.lastConsumedEpoch— пакет отбрасывается, древний replay, MAC не имеет значения.Вместо
CookieChecker.CheckMAC1()проверяем MAC по[0:smac2]с ключом P2PMacRecvKey; сверяем с[smac2:size], не сходится — пакет отбрасывается.Если
clkidEpoch > peer.lastConsumedEpoch— обновляемpeer.lastConsumedEpoch = clkidEpochи очищаемclear(consumedHandshakes).Если MAC пакета (из поля MAC2) есть в consumedHandshakes — пакет отбрасывается, это replay в пределах epoch.
Если
len(consumedHandshakes) >= HandshakesPerPeriod— пакет отбрасывается, flood от данного peer'а.Добавляем MAC в consumedHandshakes и обрабатываем пакет дальше, не забыв проверить, что peer по ClkID совпадает с peer по WireGuard.
Если consumedHandshakes потребляет слишком много памяти на Peer из-за накладных расходов на map[], его можно заменить маленьким массивом uint32_t, значения для которого вычислять как hsiphash_4u32(MAC, Device.runtimeSecret). Если и это много, то массив uint16_t выглядит допустимо, т.к. вероятность коллизии 16-битного хэша на ≈24 элементах всё ещё меньше вероятности потери одного пакета в интернете.
@darkk commented on GitHub (Sep 20, 2026):
Я немного ошибался, вполне существует сценарий, где наблюдатель может трафик слышать, но модификация затруднена: открытая Wi-Fi сеть с AP Client isolation. Впрочем, в таком случае злоумышленнику зачастую доступна Rogue AP, так что такая атака — это скорее party trick.
Способ выиграть race condition в такой ситуации иногда возможен следующий: трафик до удалённого VPN-сервера может проходить через ECMP линк с разной latency. Это не гипотетическая возможность — например, я вживую наблюдал стабильно разные маршруты и разницу latency в 10-15ms между СПб (Ростелекомом) и Франкфуртом (Linode, AKAMAI) для разных five-tuple.
Возможность у злоумышленника вслепую принудительно загрузить всю hash-таблицу в CPU cache серией пакетов есть и через MessageTransport: будет выполняться поиск по несуществующему Receiver. По этой причине использование ключа более широкого scope для экономии CPU cache-miss для невалидных MessageInitiation и MessageResponse не имеет смысла. Два MAC вместо одного тоже не выглядят осмысленно, т.к. CPU cache-miss это ~100ns, а дополнительный Blake2s-128 от MessageInitiation ~350ns.
Я обновил https://github.com/amnezia-vpn/amneziawg-go/issues/87#issuecomment-5683883588 с учётом вышесказанного.