WireGuard не хранит AllowedIPs у пира

от автора

Сервер, несколько пиров, всё работало. Добавляем ещё одного — и wg show показывает вот это (публичные ключи заменены на метки, остальное как есть):

peer: <ПИР-B>  endpoint: 192.168.90.3:53333  allowed ips: 10.100.0.0/24  latest handshake: 16 seconds ago  transfer: 884 B received, 732 B sentpeer: <ПИР-A>  endpoint: 192.168.90.2:52160  allowed ips: (none)  latest handshake: 16 seconds ago  transfer: 884 B received, 348 B sent

У пира A было 10.100.0.0/24. В конфиге на диске оно и сейчас там. В ядре — нет.

Обратите внимание, что именно сломалось. Пир не отвалился: endpoint на месте, handshake шестнадцатисекундной давности, принято от него столько же, сколько от живого соседа. Асимметрия в другой колонке — отправлено ему 348 байт против 732. Его пакеты доезжают и проходят криптографию, а обратно не едет ничего: данные умирают после расшифровки, в счётчике кадровых ошибок на интерфейсе, у которого нет кадров.

Команда, которая это устроила, отработала успешно: код возврата 0, dmesg пуст, wg-quick промолчал.

А если бы у второго пира был 10.100.0.5/32 вместо 10.100.0.0/24, ничего бы не произошло: обе строки остались бы на месте. Разницу делает не пересечение префиксов, а кое-что другое, и в документации этого нет.

Кого это касается

Модуль в мейнлайне с ядра 5.6 — с марта 2020, — и за это время AllowedIPs превратился из строчки в конфиге в машинно-генерируемое поле. Его выдаёт веб-форма панели, шаблон в Ansible, контроллер mesh-сети, CNI в кластере. Человек, который увидит последствия, чаще всего не тот, кто эту строчку написал.

Road warrior, самый частый случай. На форуме OpenWrt в январе 2024 спрашивают дословно так: «whenever I add a mobile phone as a peer, the phones added previously are no longer able to connect». Причина — 0.0.0.0/0 в allowed_ips у каждого пира на сервере: значению этому место в конфиге клиента, а не сервера. Каждый новый телефон забирает 0.0.0.0/0 себе, и предыдущий замолкает.

Сетевые ОС. В трекере VyOS это заводили как баг ещё в июле 2020 — «allowed-ips is overwritten» — и закрыли как Invalid: не VyOS виноват, так делает ядро. В OPNsense тот же отчёт от июля 2025: три пира на одном fd00:1234:5678::/56, конфиг на диске верный, а wg show печатает allowed ips: (none) у двух из трёх. Автор закрыл issue сам, решив, что напутал в настройках, — механизм он так и не узнал. И, перебирая варианты, наткнулся на вторую половину этой истории: с масками /62, /63 и /64 применяются все три. Почему так, будет ниже.

Кластеры. Cilium и Calico шифруют трафик между узлами Kubernetes именно WireGuard, и AllowedIPs там — генерируемый список адресов подов, по записи на узел. На wg show в кластере не смотрит никто.

Симптом везде один: конфиг правильный, туннель поднят, трафика нет. И везде его читают как ошибку в настройке, потому что больше читать нечего.

Что зафиксировано

репозиторий

ref

SHA

torvalds/linux

тег v6.12

adc2186

wireguard-go

тег 0.0.20250522

f333402

wireguard-tools

тег v1.0.20260223

49ce333

cloudflare/boringtun

тег boringtun-cli-0.7.1

253f7af

freebsd/freebsd-src

ветка main

f343f03

openbsd/src

ветка master

557a527

WireGuard/wireguard-nt

ветка master

9ca1539

У BSD ref — движущаяся ветка, а не тег: номера строк в if_wg.c разъедутся при первом же коммите в файл, поэтому читать их надо из SHA.

Полное дерево ядра тянуть не нужно:

git clone --depth 1 --filter=blob:none --sparse --branch v6.12 \    https://github.com/torvalds/linux.gitcd linux && git sparse-checkout set drivers/net/wireguard include/linux

Весь модуль — 14 файлов .c и 5157 строк, не считая selftest/, который собирается только при CONFIG_WIREGUARD_DEBUG. Интересующий нас allowedips.c — 389 строк.

Где на самом деле лежит AllowedIPs

Где живёт AllowedIPs

AllowedIPs хранится в trie устройства

AllowedIPs выглядит свойством пира: он написан внутри секции [Peer], рядом с PublicKey и Endpoint. Man-страница подтверждает ощущение:

AllowedIPs — a comma-separated list of IP (v4 or v6) addresses with CIDR masks from which incoming traffic for this peer is allowed and to which outgoing traffic for this peer is directed.

wireguard-tools/src/man/wg.8:158. Про то, что пространство префиксов общее, — ни слова.

В коде это устроено ровно наоборот. Префиксная trie одна на всё устройство:

struct pubkey_hashtable *peer_hashtable;struct index_hashtable *index_hashtable;struct allowedips peer_allowedips;

drivers/net/wireguard/device.h:48-50 — поле struct wg_device

А у пира лежит не список префиксов, а список обратных ссылок на узлы этой самой trie:

struct list_head peer_list;struct list_head allowedips_list;

drivers/net/wireguard/peer.h:63-64 — поле struct wg_peer

Разница не косметическая. Владелец адреса — узел trie, у него ровно один указатель node->peer. Пир не «имеет» префикс; префикс указывает на пира. Список у пира нужен лишь чтобы wg show мог распечатать, какие узлы сейчас смотрят на него.

Что делает вторая запись

Точное совпадение против длинного префикса

Exact match переназначает узел

Вставка в trie:

if (node_placement(*trie, key, cidr, bits, &node, lock)) {rcu_assign_pointer(node->peer, peer);list_move_tail(&node->peer_list, &peer->allowedips_list);return 0;}

drivers/net/wireguard/allowedips.c:199-203

Тело if — три строки, каждая делает ровно то, что написано.

rcu_assign_pointer перенаправляет узел на нового пира. list_move_tail — это list_del плюс list_add_tail: узел вынимается из списка старого пира. С этого момента wg show для первого пира про префикс не знает, потому что netlink обходит именно peer->allowedips_list:

list_for_each_entry_from(allowedips_node, &peer->allowedips_list,

drivers/net/wireguard/netlink.c:175

И return 0. Не -EEXIST, не предупреждение — успешное завершение, неотличимое от обычной вставки. Наверх подниматься нечему: в wg set единственная реакция на неудачу — perror("Unable to modify interface") (src/set.c:32), а при успехе утилита не печатает ничего.

Проверок нет и в userspace: ни wg, ни wg-quick не смотрят на пересечения.

git grep -in 'overlap\|duplicat\|conflict\|collid' v1.0.20260223 \    -- 'src/*.c' 'src/*.h' 'src/wg-quick/'
v1.0.20260223:src/curve25519-fiat32.h:258: * Can overlap h with f or g.v1.0.20260223:src/curve25519-fiat32.h:301: * Can overlap h with f or g.

Оба попадания — комментарии про перекрытие буферов в Curve25519. В коде, который разбирает конфиг и говорит с ядром, совпадений нет.

Почему «пропало у одних пиров, но не у всех»

На Server Fault висит вопрос с почти такой формулировкой, и ответ там сводится к «уберите подсети» — работает, но почему у одних пропадает, а у других нет, не объясняет. Объяснение той части, которая про исчезнувшую общую подсеть, — в условии выхода из цикла поиска места:

while (node && node->cidr <= cidr && prefix_matches(node, key, bits)) {parent = node;if (parent->cidr == cidr) {exact = true;break;}

drivers/net/wireguard/allowedips.c:157-161

exact становится true только когда совпала длина префикса — не когда префиксы пересекаются, а когда они одинаковой длины и на одном пути в trie. Иначе создаётся новый узел, и оба живут дальше. Выбирает между ними уже поиск:

while (node && prefix_matches(node, key, bits)) {if (rcu_access_pointer(node->peer))found = node;if (node->cidr == bits)break;

drivers/net/wireguard/allowedips.c:116-120

Спуск идёт вниз, found каждый раз перезаписывается — побеждает длиннейший совпавший префикс. Обычный longest-prefix match, как в таблице маршрутов.

Отсюда два разных симптома из одной причины:

конфигурация

что в wg show

кому уйдёт трафик

A: 10.0.0.0/24, B: 10.0.0.0/24

у A строка исчезает

B

A: 10.0.0.0/24, B: 10.0.0.5/32

обе на месте

B на .5, A на всё остальное

Это же объясняет и находку из отчёта OPNsense: три пира на одинаковом /56 дают (none) у двух, а /62, /63 и /64 спокойно живут втроём. Длины разные — узлы разные.

Про пересечения предупреждают и вендоры — но описывают их неверно. defguard в чеклисте ротации ключей называет это «unpredictable routing», а в разборе AllowedIPs — «undefined behavior», трафик «could go to either peer». Ничего неопределённого здесь нет: длиннее — выигрывает, при равной длине — последний записавший. Предупреждение верное, механизм — нет, и из-за этого непонятно, что делать с конкретным конфигом.

Что происходит с пакетами

Судьба пакета

Приём и передача обрабатывают отсутствие пира по-разному

Обокраденный пир не отваливается. Он продолжает слать данные, они честно проходят криптографию — и отбрасываются уже после расшифровки:

routed_peer = wg_allowedips_lookup_src(&peer->device->peer_allowedips,       skb);wg_peer_put(routed_peer); /* We don't need the extra reference. */if (unlikely(routed_peer != peer))goto dishonest_packet_peer;

drivers/net/wireguard/receive.c:404-409

Криптоключевая маршрутизация: адрес отправителя внутри туннеля обязан принадлежать пиру, чьим ключом пакет подписан. Иначе:

dishonest_packet_peer:net_dbg_skb_ratelimited("%s: Packet has unallowed src IP (%pISc) from peer %llu (%pISpfsc)\n",dev->name, skb, peer->internal_id,&peer->endpoint.addr);DEV_STATS_INC(dev, rx_errors);DEV_STATS_INC(dev, rx_frame_errors);

drivers/net/wireguard/receive.c:415-420

rx_frame_errors. Кадровая ошибка — на интерфейсе, у которого нет ни кадров, ни физического уровня. Вместе с ней растёт общий rx_errors, так что в обычном ip -s link видны просто «ошибки приёма», без намёка на причину. Разложение по видам показывает только ip -s -s link show wg0, колонка frame.

Три пинга от обокраденного пира на ядре 6.18.33.2-microsoft-standard-WSL2:

    RX:  bytes packets errors dropped  missed   mcast          1320      13      3       0       0       0    RX errors:  length    crc   frame    fifo overrun                     0      0       3       0       0

Три пакета — три frame. Остальные по нулям: физического уровня, где они бывают, здесь нет.

А вот что модуль пишет в dmesg, если включить dynamic_debug:

[ 2506.903624] wireguard: wg0: Packet has unallowed src IP (10.100.0.2) from peer 13 (192.168.91.2:44011)

peer 13 — это internal_id, глобальный счётчик пиров на машине (peer.c:42). В wg show его нет, сопоставить с ключом нечем: из лога опознаётся только endpoint.

С исходящей стороны отказ выглядит иначе — и это единственное место во всей истории, где WireGuard говорит прямым текстом. Если под адрес назначения не нашлось пира:

peer = wg_allowedips_lookup_dst(&wg->peer_allowedips, skb);if (unlikely(!peer)) {ret = -ENOKEY;

drivers/net/wireguard/device.c:153-155

ENOKEY — это errno 126, «Required key not available». wg_xmit возвращает его наверх из метки err:, по дороге инкрементируя счётчик:

err:DEV_STATS_INC(dev, tx_errors);kfree_skb(skb);return ret;

drivers/net/wireguard/device.c:230-233

Приложение получает эту ошибку синхронно, из sendmsg. На живом ядре (6.18.33.2-microsoft-standard-WSL2), два пинга на адрес без пира:

ping: sendmsg: Required key not available--- 10.200.0.7 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1027ms

И счётчик интерфейса ровно после этого:

    TX:  bytes packets errors dropped carrier collsns          1080       9      2       0       0       0

Два пакета — две ошибки передачи, при живом туннеле, через который прошло девять пакетов. Отсюда и картина «до одних хостов достучаться можно, до других нет» — самый неприятный вид отказа для диагностики. Так что если вы когда-нибудь видели Required key not available и решили, что это что-то про ключи шифрования, — нет. Это AllowedIPs, под которые не подошёл адрес назначения.

Про «молча» — уточнение

Утверждать, что диагностики нет, было бы неправдой. CONFIG_WIREGUARD_DEBUG выключен по умолчанию (drivers/net/Kconfig:107-116), но он управляет селфтестами, а не этими строками. Строку про чужой src-адрес печатает net_dbg_skb_ratelimited, и гейт у неё свой — #if defined(CONFIG_DYNAMIC_DEBUG) || defined(DEBUG) в drivers/net/wireguard/socket.h:33. CONFIG_DYNAMIC_DEBUG в дистрибутивных ядрах включён, так что сообщение зажигается на живой системе без пересборки:

modprobe wireguard && echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control

Способ описан в wg(8), в секции DEBUGGING INFORMATION. То есть правильная формулировка не «WireGuard молчит», а: сообщение есть, оно выключено, и о нём нужно знать заранее. Молчит именно перезапись префикса — про неё не сообщает никто и ничем.

Проверить самому, без ядра и без root

allowedips.c почти не зависит от ядра: списки, RCU, slab, fls — и всё. Значит его можно собрать в userspace как есть, не меняя ни строчки. Заглушек пять: mutex.h, ip.h, ipv6.h, peer.h — сто с небольшим строк на все четыре — и пустой selftest/allowedips.c, потому что последняя, 389-я строка файла подтягивает селфтест, живущий под #ifdef DEBUG. main.c дёргает wg_allowedips_insert_v4() и печатает списки пиров тем же обходом, что и netlink.

Заглушки и main.c — в репозитории со стендом, каталог stand/harness; из ядра берутся ровно два файла и не правятся:

git -C ~/linux show v6.12:drivers/net/wireguard/allowedips.c > build/allowedips.cgit -C ~/linux show v6.12:drivers/net/wireguard/allowedips.h > build/allowedips.hmkdir -p build/selftest && : > build/selftest/allowedips.cgcc -O1 -I include -I build -o trie build/allowedips.c build/main.c./trie
== 1. A получает 10.100.0.0/24 ==insert вернул 0ПИР-A10.100.0.0/24ПИР-B(none)== 2. B получает ТОТ ЖЕ 10.100.0.0/24 ==insert вернул 0ПИР-A(none)ПИР-B10.100.0.0/2410.100.0.2 -> ПИР-B== 3. Сброс. A: 10.100.0.0/24, B: 10.100.0.3/32 ==ПИР-A10.100.0.0/24ПИР-B10.100.0.3/3210.100.0.2 -> ПИР-A10.100.0.3 -> ПИР-B

Это исполняется настоящий код ядра, а не его пересказ.

Второй стенд — три network namespace с живыми туннелями. Оба клиента подняли handshake, у обоих AllowedIPs = 10.100.0.0/24:

<ПИР-A>(none)<ПИР-B>10.100.0.0/24--- A (10.100.0.2) -> 10.100.0.1 ---3 packets transmitted, 0 received, 100% packet loss, time 2040ms--- B (10.100.0.3) -> 10.100.0.1 ---3 packets transmitted, 3 received, 0% packet loss, time 2035msrtt min/avg/max/mdev = 0.590/0.662/0.714/0.052 ms

Что сервер об этом думает — три пинга, три записи с шагом в секунду:

2026/08/17 00:05:50 IPv4 packet with disallowed source address from peer(lbW4…2VQY)2026/08/17 00:05:51 IPv4 packet with disallowed source address from peer(lbW4…2VQY)2026/08/17 00:05:52 IPv4 packet with disallowed source address from peer(lbW4…2VQY)

Третий прогон — против ядерного модуля, на другом ядре (6.18, не тот v6.12, по которому шёл разбор):

===== ШАГ 2. Пиру B выдан ТОТ ЖЕ префикс =====код возврата wg set: 0<ПИР-A>(none)<ПИР-B>10.100.0.0/24===== ШАГ 3. Тот же случай, но у B префикс длиннее: 10.100.0.3/32 =====<ПИР-A>10.100.0.0/24<ПИР-B>10.100.0.3/32

Одно и то же в трёх местах: код ядра v6.12 в userspace, wireguard-go и живой модуль 6.18. Механизм не версионный.

Шесть кодовых баз, одно поведение

Шесть кодовых баз и вендорский слой

Шесть кодовых баз, один контракт

Общего автора здесь больше, чем кажется: копирайт Jason Donenfeld стоит и в ядре, и в wireguard-go, и в wireguard-nt, и в шапке обоих if_wg.c — BSD-шные драйверы выросли из его кода (git show f343f03:sys/dev/wg/if_wg.c | head -8). Так что «независимо сошлись» — не про эту историю. Интересно другое: AllowedIPs лежит в принципиально разных структурах данных, и ведут они себя одинаково.

реализация

что под капотом

ядро Linux

своя trie, написанная под задачу

wireguard-go

своя trie, отдельно написанная на Go

BoringTun (Cloudflare, Rust)

чужой крейт ip_network_table

FreeBSD if_wg

штатная BSD-шная radix_node_head — та же, что у таблицы маршрутов

OpenBSD if_wg

ART, собственная таблица маршрутизации OpenBSD

wireguard-nt

своя trie — построчный порт ядерной, драйвер ядра Windows

Шесть разных кодовых баз, пять структур данных, и единственная посторонняя по родословной реализация — BoringTun от Cloudflare. Вот FreeBSD, когда префикс уже занят:

} else if (node != aip->a_nodes) {free(aip, M_WG);aip = (struct wg_aip *)node;if (aip->a_peer != peer) {LIST_REMOVE(aip, a_entry);aip->a_peer->p_aips_num--;aip->a_peer = peer;LIST_INSERT_HEAD(&peer->p_aips, aip, a_entry);

sys/dev/wg/if_wg.c:600-607, FreeBSD main, f343f03

LIST_REMOVE плюс LIST_INSERT_HEAD — это list_move_tail из Linux, записанный макросами BSD: узел вынимают у прежнего пира, отдают новому, функция возвращает 0. OpenBSD в sys/net/if_wg.c:651-653 (557a527) делает то же поверх совсем другого дерева. А вот тот же фрагмент в драйвере ядра Windows:

    if (NodePlacement(*Trie, Key, Cidr, Bits, &Node, Lock))    {        RcuAssignPointer(Node->Peer, Peer);        RemoveEntryList(&Node->PeerList);        InsertTailList(&Peer->AllowedIpsList, &Node->PeerList);        return STATUS_SUCCESS;    }

driver/allowedips.c:242-248, wireguard-nt, 9ca1539

Три операции, три языка описания одного и того же: list_move_tail, LIST_REMOVE+LIST_INSERT_HEAD, RemoveEntryList+InsertTailList. Дальше по файлу — NodePlacement() с тем же if (Parent->Cidr == Cidr) { Exact = TRUE; } на :191, и FindNode() с тем же Found = Node в цикле на :137-138. Это не совпадение поведения, это построчный порт: 499 строк под GPL-2.0.

BoringTun остаётся единственной кодовой базой, где решение принимали заново.

BoringTun отличается интереснее всех. Его trie умеет сказать, что префикс занят:

    pub fn insert(&mut self, key: IpAddr, cidr: u32, data: D) -> Option<D> {

boringtun/src/device/allowed_ips.rs:42, тег boringtun-cli-0.7.1

Option<D> — прежний владелец префикса, и mod.rs:347 выбрасывает его не глядя. А главное, у пира лежит своя копия списка (peer.rs:25), и её же отдаёт UAPI (peer.rs:152api.rs:188). Харнесс на настоящем типе из крейта:

device.insert вернул: Some("ПИР-A")   <- прежний владелец, и он отбрасываетсяПИР-A10.100.0.0/24ПИР-B10.100.0.0/24трафик на 10.100.0.2 -> Some("ПИР-B")

Оба пира показывают префикс, трафик идёт одному. В ядре хотя бы (none) намекает, что что-то произошло; здесь не намекает ничто.

Расходятся реализации только в том, как сообщают о проблеме — и то в мелочах:

Linux

go

BoringTun

FreeBSD

OpenBSD

NT

точное совпадение

перезапись

перезапись

перезапись

перезапись

перезапись

перезапись

код возврата

0

нет

0

0

0

0

разная длина

длиннейший

длиннейший

длиннейший

длиннейший

длиннейший

длиннейший

wg show после

(none)

(none)

у обоих

(none)

(none)

(none)

передача без пира

ENOKEY

молча

молча

ENETUNREACH

ENETUNREACH

таймаут

Не все клетки одного качества, и это стоит сказать прямо. Linux и wireguard-go замерены целиком. У BSD прочитан только код: ни одной команды на них я не выполнял, вся их колонка — предсказание по if_wg.c. У wireguard-nt замерены перезапись и сосуществование узлов на живой Windows, а «длиннейший» взят из FindNode() (driver/allowedips.c:135-142): трафика на Windows я не пускал. Клетка BoringTun «у обоих» получена чтением UAPI (peer.rs:25api.rs:188) и воспроизведена харнессом на настоящем типе из крейта — но не сквозным прогоном, и почему, будет в конце.

И отдельно — MikroTik

RouterOS выносит запрет прямо в раздел Peers своего справочника: «Allowed-address range cannot overlap on one interface, so you need to set own range for each peer». Формулировка живёт на замороженной ветке документации — MikroTik переехал на manual.mikrotik.com, и там этой фразы я не нашёл. У wg(8) про это нет ни слова нигде.

Проверил на CHR 7.20.2 под qemu: два клиента на wireguard-go, оба пира на одном allowed-address.

PEERA allowed=10.100.0.0/24 rx=308 tx=92 hs=00:00:20PEERB allowed=10.100.0.0/24 rx=308 tx=92 hs=00:00:20пинг A (10.100.0.2): 4 packets transmitted, 0 received, 100% packet loss, time 3050msпинг B (10.100.0.3): 4 packets transmitted, 4 received, 0% packet loss, time 3005msPEERA allowed=10.100.0.0/24 rx=340 tx=92 hs=00:00:28PEERB allowed=10.100.0.0/24 rx=852 tx=604 hs=00:00:28

Ошибки нет, hs= свежий у обоих, префикс показан у обоих — и трафик идёт только второму. Дальше интереснее: удаление PEERB префикс первому не возвращает, хотя в конфиге у PEERA он никуда не девался. Оживает пир только после set с тем же значением, которое там и так написано:

пинг A (10.100.0.2): 4 packets transmitted, 4 received, 0% packet loss, time 3004ms

Вендор, который вынес запрет в документацию, сам его не проверяет — и держит в print конфигурацию, которой в датапате нет.

Отдельная кодовая база под ядро Windows ведёт себя ровно так же:

windows:   Майкрософт Windows 11 Pro 10.0.26200.0===== ШАГ 2. Пиру B выдан ТОТ ЖЕ префикс =====код возврата wg set: 0<ПИР-A>(none)<ПИР-B>10.100.0.0/24

Единственное осязаемое расхождение — что видит приложение при передаче на адрес без пира. Linux отдаёт ENOKEY синхронно из sendmsg («Required key not available»), BSD — ENETUNREACH, а на Windows ping истекает по таймауту, без всякой ошибки. Один и тот же дроп, три разных симптома.

Что из этого следует

Один префикс — один пир, на всём интерфейсе. Не рекомендация, а свойство структуры данных: узел 0.0.0.0/0 в trie один, и два клиента с таким AllowedIPs на одном интерфейсе одновременно работать не могут.

wg showconf — источник истины, файл на диске — нет. Сравнивайте после правки: расхождение и есть след перезаписи, готовая проверка для CI. На BoringTun и RouterOS не поможет: там show печатает свою копию конфига, а на RouterOS префикс ещё и возвращается в датапат только перезаписью того же значения.

Автоматизация должна проверять пересечения сама — ни одна из шести реализаций этого не делает. Ровно на этом в июле 2026 чинили netbird: счётчик ссылок был ключован только по префиксу, и WireGuard оставался нацелен на удалённого пира.

При ротации ключа порядок обязателен. Новый пир с тем же AllowedIPs забирает префикс мгновенно, ещё до handshake. Сначала новый ключ на клиенте, потом свежий latest handshake на сервере, и только потом удаление старого пира.

rx_frame_errors на wg0 — вообще не про кадры. Счётчик растёт из двух мест receive.c, и оба про содержимое уже расшифрованного пакета: чужой src-адрес (:420) или не-IP внутри туннеля (:426). Второе на исправном туннеле не случается, так что практически это первое — разъехавшиеся AllowedIPs либо попытка подменить адрес. Мониторить стоит.

Чего я не знаю

Считать честно: исходники прочитаны у всех шести реализаций — ядро, wireguard-go, BoringTun, FreeBSD, OpenBSD, wireguard-nt. Исходников нет только у RouterOS: там измерено поведение снаружи и не прочитано ничего. Поведение замерено у четырёх — ядро, wireguard-go, wireguard-nt, RouterOS — шестью прогонами: allowedips.c из v6.12 в userspace, живой модуль 6.18, три netns на wireguard-go, Windows 11, RouterOS на конфиге и RouterOS на трафике. FreeBSD и OpenBSD не запускались вовсе. У BoringTun замерена trie харнессом, но сквозного прогона нет.

Копирайт на пяти реализациях из шести — один и тот же. Ядро, wireguard-go, wireguard-nt и оба if_wg.c несут имя Jason Donenfeld. Так что «шесть команд независимо пришли к одному решению» сказать нельзя, и я не говорю. Постороннее подтверждение здесь ровно одно — BoringTun.

ICMP я так и не увидел. device.c:227 вызывает icmp_ndo_send, но ENOKEY приходит из sendmsg раньше, и поймать ICMP на проводе не вышло.

Счётчик на Windows не атрибутирован. OutboundPacketErrors оказался равен 56 при двух пингах: у интерфейса был адрес и фоновый трафик. Что растёт — видно, от чего — не доказано.

Отсюда два вопроса, дорогой и бесплатный.

Дорогой. BoringTun на boringtun-cli-0.7.1 заведённого пира менять не умеет в принципе:

        // Update an existing peer        if self.peers.get(&pub_key).is_some() {            // We already have a peer, we need to merge the existing config into the newly created one            panic!("Modifying existing peers is not yet supported. Remove and add again instead.");

boringtun/src/device/mod.rs:318-321

api_set_peer доходит до update_peer без единой проверки на существование пира (api.rs:296 и api.rs:343), так что любой wg set по заведённому ключу — кроме remove — роняет демон. remove обрабатывается на пять строк выше паники (mod.rs:313-315), и только поэтому обходной путь вообще существует. Мой стенд на этом и падает — wg при этом печатает EPROTO, и самой паники в логе нет: boringtun уходит в фон, а его stderr стенд не ловит. Кто соберёт сценарий через remove + add и покажет wg show — закроет последнее белое пятно. По коду (peer.rs:25api.rs:188) префикс должен быть виден у обоих пиров.

Бесплатный. Один вопрос из памяти, без консоли: у вас в конфигах есть два пира с одинаковым AllowedIPs — и вы про это знали или узнали только что?)

ссылка на оригинал статьи https://habr.com/ru/articles/1071026/