Некоторое время назад я обнаружил, что мой доселе чистый и светлый интернет стал подвержен проблеме, которую лично я охарактеризовал как "подзатупы". Суть "подзатупов" заключается в сериях из пауз передачи пакетов длиной в 1-4 секунды (в отдельных случаях - до 10 секунд), время от времени происходящих на протяжении дня. Поскольку работать с SSH в таких условиях не очень-то комфортно, я решил переключиться на мобильный интернет от другого провайдера. Однако, оказалось, что теперь данная проблема и там имеет место быть:
$ ping 8.8.8.8
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=135 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=70.3 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=558 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=115 time=2581 ms
64 bytes from 8.8.8.8: icmp_seq=5 ttl=115 time=1553 ms
64 bytes from 8.8.8.8: icmp_seq=6 ttl=115 time=529 ms
64 bytes from 8.8.8.8: icmp_seq=7 ttl=115 time=210 ms
Добавлял фрустрации тот факт, что после каждого переключения каналов мне приходилось заново переустанавливать все SSH и SFTP соединения. Хоть я и пользуюсь SSH-ключами там, где это возможно (и менеджером паролей там, где это малореально), тем не менее, мороки было немало. В принципе, на этом месте можно было бы начать затевать смену провайдера, либо прицениваться к местным "серым" продавцам Starlink. Однако, нет никакой гарантии, что у другого провайдера не будет этой же проблемы, а Starlink еще надо ухитриться затащить на крышу многоквартирного дома. Да и как раз на днях местный регулятор грозился что всех какбэ покарае, если только Маск вдруг не получит у него лицензию на оказание услуг связи.
Повыдирав волосы некоторое время, в мою голову пришла идея: а что, если дублировать IP-пакеты через оба сетевых интерфейса? Поскольку "подзатупы" разных провайдеров не коррелируют между собой, то даже если один из провайдеров в данный момент "тупит", то хотя бы одна из копий все равно наверняка должна добраться. А даже если в моменте все хорошо, мы хотя бы снижаем RTT до минимального по обоим провайдерам.
Очевидным тут является тот факт, что без внешнего сервера ничего не "выгорит": поскольку в TCP/IP используется так называемый 5-tuple для идентификации TCP-соединений, если мы будем менять source address в процессе, то ни один внешний хост нас не поймет. Поэтому я взял VDS в датацентре с пингом получше, и принялся за дело.
Смахиваем пыль с TUN/TAP
Я решил пойти путем, отработанным многочисленными VPN-клиентами. Мы создадим виртуальное сетевое устройство, который будет инкапсулировать приходящие IP-пакеты в UDP, и реплицировать их на VDS-ку через каждый из внешних сетевых интерфейсов. В свою очередь, серверная часть на VDS-ке будет отбрасывать "лишние" пакеты - но запоминать, с каких IP и портов они пришли, и форвардить пакеты туда. А в целях аутентификации (ну и дополнительной прослойки безопасности) мы будем шифровать исходящие пакеты по схеме AES256-GCM.
Как известно, TUN/TAP фактически разделен на 2 разных части - а именно, собственно, TUN и TAP. Обе из них реализуют концепцию виртуального сетевого устройства - однако, TUN оперирует IP-пакетами (таким образом, работая на 3-м уровне модели OSI), а TAP - Ethernet-фреймами (что соответствует 2-му уровню). Поскольку работать с Ethernet-фреймами было бы для нас избыточно, в дальнейшем речь будет идти именно о TUN, но не о TAP.
И так, добавляем крейт tun-rs, а так же pnet_packet для парсинга пакетов:
$ cargo new hello-tun && cd hello-tun/
$ cargo add tun-rs pnet_packet
В main.rs создаем виртуальное сетевое устройство и логируем поступающие из сетевого стека ОС пакеты. Для простоты сразу задаем IP-адрес, подсеть и MTU:
use tun_rs::DeviceBuilder;
use pnet_packet::ipv4::Ipv4Packet;
fn main() -> std::io::Result<()> {
let dev = DeviceBuilder::new()
.name("hellotun0")
.mtu(1500)
.ipv4("10.199.0.2", 24, None)
.build_sync()?;
println!("Created TUN interface: {}", dev.name()?);
let mut buf = vec![0u8; 65535];
loop {
let n = dev.recv(&mut buf)?;
println!("Packet bytes: {:?}", &buf[0..n]);
let Some(packet) = Ipv4Packet::new(&buf[0..n]) else { continue };
println!("Parsed packet: {:?}", packet);
}
}
Далее запускаем и пробуем пропинговать любой IP-адрес в подсети 10.199.0.0/24 - кроме нашего локального адреса 10.199.0.2. Если все хорошо, то в stdout должны появиться наши логи. В качестве небольшого proof of concept можно добавить ответы на пинги:
loop {
let n = dev.recv(&mut buf)?;
println!("Packet bytes: {:?}", &buf[0..n]);
let Some(mut packet) = MutableIpv4Packet::new(&mut buf[0..n]) else { continue };
println!("Parsed packet: {:?}", packet);
let src = packet.get_source();
let dst = packet.get_destination();
packet.set_source(dst);
packet.set_destination(src);
dev.send(&buf[..n])?;
}
Теперь мы можем пропинговать любой из адресов нашей виртуальной подсети - и нам будет возвращен корректный ответ.
Мучаем ChatGPT
На этом этапе я решил скормить собранные требования в качестве промпта ChatGPT. Помучав гптшку некоторое время, мне удалось получить компилирующийся и почти рабочий вариант. Вкратце разберем некоторые основные моменты выданного ChatGPT решения:
Протокол максимально простой: каждый UDP-пакет состоит из 12-байтного nonce и следующего за ним зашифрованного IP-пакета. Какие бы то ни было служебные команды и/или машины состояний полностью отсутствуют
Для отбрасывания "лишних" пакетов ChatGPT решил использовать DefaultHasher из стандартной библиотеки. Пусть остается так - хотя криптографически стойкие хэши в данном случае и не то, чтобы требуются, испытания показали, что наша производительность является более чем приемлимой. Поэтому я не стал дергаться и пытаться подключить более быстрые ahash или fxhash. В принципе, можно было быть еще эффективнее и вместо хэшей использовать sequence number из TCP-заголовка - но и ходить тогда будет только TCP.
Серверная и клиентская части кода фактически представляют из себя полный копипаст друг друга (WET) - хотя отличаются, по сути, только стратегией отправки UDP-пакетов: клиент реплицирует пакеты на IP-адрес внешнего сервера через каждый из прописанных в конфиге сетевых интерфейсов, а внешний сервер - на все IP-адреса, с которых ему приходили успешно аутентифицированные пакеты. Мне так и не удалось заставить ChatGPT зарефакторить этот момент, не сломав при этом все. Поэтому "осушаем" вручную
Для чтения из UDP-сокета использовалась весьма противная схема с переводом в неблокирующий режим с последующим опросом опросом раз в n миллисекунд - в клиентском копипасте кода, к тому же, оказавшаяся неработоспособной
Запускаем
Отполировав эти а так же еще некоторые моменты, я решил попробовать установить свое первое соединение. Прежде всего генерируем ключ для шифрования на сервере:
$ tuxburst gen-key
[2026-01-01 16:03:07][INFO] AES-256 key generated: 5543E0FF97FB85B2A5033043DF53FDA62C8B907AAF5FEB0F0C3C89F162C64F17
[2026-01-01 16:03:07][INFO] Wrote server.key
Заполняем client.toml и server.toml:
[common]
tun_name = "tuxburst0"
tun_addr = "10.99.0.2/30"
mtu = 1400
debug = false
[client]
host = "1.2.3.4:40000"
key = "5543E0FF97FB85B2A5033043DF53FDA62C8B907AAF5FEB0F0C3C89F162C64F17"
interfaces = ["wlo1", "enxf221672bb6de"]
[client.routes]
tun_gateway = "10.99.0.1"
metric = 5
[common]
tun_addr = "10.99.0.1/30"
tun_name = "tuxburst0"
debug = false
mtu = 1400
[server]
listen = "0.0.0.0:40000"
Настраиваем NAT на сервере и запускаем процессы на обоих концах:
$ sysctl -w net.ipv4.ip_forward=1
$ iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE
$ iptables -A FORWARD -i tuxburst0 -o ens3 -j ACCEPT
$ iptables -A FORWARD -i ens3 -o tuxburst0 -m state --state RELATED,ESTABLISHED -j ACCEPT
server$ sudo tuxburst serve -c server.toml
client$ sudo tuxburst connect -c client.toml
При старте мы автоматически добавляем правило route, перенаправляющиее весь трафик на гейтвей в нашей виртуальной сети по дефолту. Поскольку наше правило имеет наименьший metric, оно превалирует над аналогичными правилами, полученными при получении настроек по DHCP для остальных каналов. И наоборот - поскольку роут ассоциирован с нашим виртуальным сетевым устройством, при завершении процесса он будет "бесплатно" подчищен операционной системой - и, таким образом, прежний роутинг будет восстановлен:
$ route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Ifa
default _gateway 0.0.0.0 UG 5 0 0 tuxburst0
default _gateway 0.0.0.0 UG 100 0 0 enx62c0701601f2
default wifi-router 0.0.0.0 UG 600 0 0 wlo1
10.12.7.0 0.0.0.0 255.255.255.0 U 100 0 0 enx62c0701601f2
10.99.0.0 0.0.0.0 255.255.255.252 U 0 0 0 tuxburst0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.3.0 0.0.0.0 255.255.255.0 U 600 0 0 wlo1
Теперь самое время проверить соединение, запустив curl https://api.ipify.org. Как мы видим, возвращаемый IP действительно равен внешнему IP нашей внешней VDS-ки. Эксперименты с попарным отключением каждого из каналов показали устойчивость связи - а стало быть, схема работает.
Итоги
Опыт эксплуатации в течение недели показал, что мои волосы стали мягкими и шелковистыми проблема полностью решена. Я рад, что в итоге мне не потребовалось затевать смену провайдера, равно как и возиться с установкой Starlink на крышу многоквартирного дома. Отдельно бы хотелось отметить то, что TCP-соединения не прерываются даже если получилось так, что мне пришлось перезапустить процесс - поскольку мы не храним никаких состояний, которые могли бы негативно повлиять на существующие коннекты.
Если данный проект мог бы быть полезен еще кому-либо, или просто интересно глянуть код - вот ссылка на репозиторий: https://github.com/vdudouyt/tuxburst/
Комментарии (21)

ky0
02.01.2026 10:54Ура, вы придумали Multipath TCP :)

vdudouyt Автор
02.01.2026 10:54Ожидал этот комментарий. В принципе, тоже вариант, хотя и более громоздкий.

poige
02.01.2026 10:54Ожидал этот комментарий
а https://en.wikipedia.org/wiki/Mosh_(software) этот?
UPD.: немного поискав: https://github.com/porech/engarde — A go network utility to create a reliable IP tunnel over multiple connections: "… engarde constantly sends every single packet through all the available connections: if one of the links has problems, the packet will still fastly reach its destination through the other ones, and the user won't even notice it. …"

RTFM13
02.01.2026 10:54Обидно, что подзатупы это в большинстве случаев искуственно внедренная вещь и появилась (преимущественно у опсосов) задолго до того как этим стал пользоваться ркп. Вангую что у них прям галочка в биллинге есть включенная по умолчанию. Или даже hidden услуга.

vdudouyt Автор
02.01.2026 10:54Ну тут дело происходит далеко за пределами досягаемости РКН )
А в РФ с "подзатупами" как-то не сталкивался - хотя, с другой стороны, и давно там не был.
RTFM13
02.01.2026 10:54А это специфика скорее опсосов, чем рф. Они с самого начала саботировали конкурентный мультимедиа трафик.

HardWrMan
02.01.2026 10:54Дык, этот самый конкурентный мультимедиа трафик в какой-то момент забил все воздушные каналы с лёгкой руки менеджеров, продававших дешёвые безлимиты направо и налево без контроля. Да так их забил, что фундаментальная передача голоса уже была под угрозой. Деградация была такая, что немедленно вспомнились конец 90х - начало 00х.

RTFM13
02.01.2026 10:54Не, голосовой трафик имеет приоритет изначально. Хотя бываетт и он встаёт, но это другая история. Бывает что инет летает, но на пару секунд в минуту втаёт колом. Я собаку съел в своё время на диагностике каналов связи и перегрузку от саботажа отличаю лучше любого телепата. Вот эти "затупы" как в статье они очень специфичны.

HardWrMan
02.01.2026 10:54Вот эти "затупы" как в статье они очень специфичны.
Я когда сидел на сильно ограниченном ADSL 15 лет назад, то там по "безлимитному" тарифу при ширине физического канала 512К/128К давали 128К/128К на 7 гигабайт а при превышении этого объёма оно падало до 32К/32К. Так вот, эти зашейпенные 32К совсем не так работали, как те же модемные 36,6К или даже 46,6К, которые у меня были до ADSL.
Учитывая особенность моего провайдера я уже тогда стал ставить у себя отдельный шлюз с двумя сетевыми интерфейсами, который работал на FreeBSD. С одной стороны, это позволяло мне лучше управлять расходом трафика канала и организовать локальный кеш, что ускоряло работу тех же форумов, а с другой дополнительно играться с сетевыми технологиями получая знания, которые потом применял на рабочем места.
Так вот, шейпинг 32К провайдер делал грубо, уменьшая корзину (basket) в шейпере. При наличии быстрого канала до шейпера это приводило к быстрому заполнению всей корзины и отбрасыванию всех не влезающих пакетов. Никакого управления трафиком типа back-pressure не производилось. Как итог: модемные 36,6К ощущались просто как медленный линк, перегрузить который можно лишь запустив приличное количество потоков/TCP сессий, в то время как провайдерский шейпер отваливался буквально на 5-6 сессиях. Да, его хватало на эксклюзивный сёрфинг (но иногда приходилось нажимать F5 для прогрузки части относительно тяжёлого контента на странице) или для мессенджеров + закачка в 1 поток. Но стоило чуток забыться или иметь дома более 2х пользователей как сразу всё отваливалось.
Я потом, когда провайдер при линке 8М/1М давал уже безшейперный безлимитный тариф 4М/512К (который довольно-таки быстро сменился на 8М/1М), игрался с шейпером в IPFW на шлюзе и получал эффекты провайдерских 32К при определённых настройках. Причём, этот эффект масштабируется: при 8М линке его можно получить даже на 1М шейпинга.
Мой вывод: не стоит искать теории заговора там, где можно объяснить человеческой глупостью. Иногда это просто криворукие админы, хотя, конечно, если у вас мания преследования то это ещё не значит, что за вами не следят.

RTFM13
02.01.2026 10:54Поясните, в каком месте ваш текст опровергает мои слова?

HardWrMan
02.01.2026 10:54Я собаку съел в своё время на диагностике каналов связи и перегрузку от саботажа отличаю лучше любого телепата. Вот эти "затупы" как в статье они очень специфичны.
Саботаж - умышленное неисполнение или небрежное исполнение определённых обязанностей, скрытое противодействие осуществлению чего-либо. Я же говорю, что это скорее банальное разгильдяйство, обычно от низкой квалификации.

RTFM13
02.01.2026 10:54Я ваш посыл понял. Я спрашиваю где опровержение моих слов?
Вы там очень долго рассказываете что шейпер с малым буфером дропал пакеты. Охотно верю, но как это противоречит тому, что я писал выше?
Может, я еще какой-то смысл упустил?
Опять же, проблеме не первый десяток летт и не у одного опсоса (при этом она может появляться и пропадать на отдельных тарифах/опсосах). За это время можно было найти специалистов любой квалификации, это в оборотах опсосов - пыль. Думаю заявок в саппорте достаточно. А если менеджмент не сподобился озаботиться этой проблемой, то это (цитирую ваше сообщение) умышленное неисполнение или небрежное исполнение определённых обязанностей, скрытое противодействие осуществлению чего-либо со стороны этгого менеджмента.

HardWrMan
02.01.2026 10:54Вы давно общались с каким-либо саппортом?
Я скажу так: если у меня проблемы с провайдером, то я сначала собираю детальную диагностическую информацию о проблеме. И обращаюсь в саппорт только если понимаю, что я её не могу решить на своей стороне. Затем я звоню в т.н. "бюро ремонта" и моя задача не просто пробиться к технарю а показать первой линии что я понимаю о проблеме сильно больше, чем рядовой пользователь. Раньше социальная инженерия требовала от меня довольно-таки много времени, сейчас же несколько фраз с техническими терминами из лексикона самого телекома и всё скатывается к быстрой формальоой анкете тикета и перевод на специалиста, с которым уже я общаюсь на равных. Специалист же либо даёт на время решения проблемы (если исправление займёт некоторое время) внутренний номер телефона, либо иногда сам перезванивает со служебной мобилы, чтобы я видел прямой номер. В последнее время этот процесс ускорился, возможно они поставили какой-то маркер на моей учётке, не знаю.
Так я решал разнообразные задачи, от блокировки нижних портов до нарушения маршрутизации до моего колокола или выезд специалиста для замеров шумов на линии специальныи прибором (это когда была медь у меня, оптику сам терминал измеряет). Как это сможет объяснить рядовой гражданин? Ответ - никак. Поэтому на вопрос "у меня подтупливает интернет" он резонно получит ответ "перегрузка на линии/перезагрузите устройство". И не пробъётся дальше Олега а тикет закроют как решённый.
PS Что касается генеральной линии то мои тезисы действительно лежат в одной плоскости с вашими. Я лишь обсуждаю их причины.

mapnik
02.01.2026 10:54mosh, атэц.
Если пятисекундные задержки влияют только на ssh — используй mosh!
vdudouyt Автор
02.01.2026 10:54При серфинге веб-страничек тоже сильно заметно. А на всяких нетфликсах с ютубами периодически скидывает в минимальное качество и обратно, если я попал на период "подзатупов".
HardWrMan
Вспомнил, как я вкорячивал примитивный балансир D-Link Load Balance Router (DI-LB604) в конторе, в которой работал в 2008 году. Из забавного: торрент шуршал на оба канала и это хорошо. А сайты вот иногда агрились, мол у тебя IP меняется, на что мне начали жаловаться юзеры мылрушечки и вкашечки. Пришлось подпиливать кастом, чтобы после поднятия TCP сессии маршрут для клиента оставался постоянным до окончания сессии. А некоторых клиентов пришлось жёстко посадить на конкретное гнездо с переключением в случае падения того линка.
Славные были времена, дорога полная граблей и подводных камней, что нас только закаляет.
vdudouyt Автор
Ну это немного другое, в моем случае IP-адрес регулярно меняется даже в пределах одного TCP-соединения (в зависимости от того, какой из дубликатов пакетов первый придет). Но сводится в единое целое NAT-ом, чтобы соединие сохранялось.
Но да, тоже интересный кейс.