Некоторое время назад я обнаружил, что мой доселе чистый и светлый интернет стал подвержен проблеме, которую лично я охарактеризовал как "подзатупы". Суть "подзатупов" заключается в сериях из пауз передачи пакетов длиной в 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)


  1. HardWrMan
    02.01.2026 10:54

    Вспомнил, как я вкорячивал примитивный балансир D-Link Load Balance Router (DI-LB604) в конторе, в которой работал в 2008 году. Из забавного: торрент шуршал на оба канала и это хорошо. А сайты вот иногда агрились, мол у тебя IP меняется, на что мне начали жаловаться юзеры мылрушечки и вкашечки. Пришлось подпиливать кастом, чтобы после поднятия TCP сессии маршрут для клиента оставался постоянным до окончания сессии. А некоторых клиентов пришлось жёстко посадить на конкретное гнездо с переключением в случае падения того линка.

    Славные были времена, дорога полная граблей и подводных камней, что нас только закаляет.


    1. vdudouyt Автор
      02.01.2026 10:54

      Ну это немного другое, в моем случае IP-адрес регулярно меняется даже в пределах одного TCP-соединения (в зависимости от того, какой из дубликатов пакетов первый придет). Но сводится в единое целое NAT-ом, чтобы соединие сохранялось.

      Но да, тоже интересный кейс.


  1. ky0
    02.01.2026 10:54

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


    1. vdudouyt Автор
      02.01.2026 10:54

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


      1. ky0
        02.01.2026 10:54

        Код, идущий в комплекте с ядром - более громоздкий, чем отдельное приложение?


        1. poige
          02.01.2026 10:54

          А где тут MpTCP-то вообще?


      1. 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. …"


        1. vdudouyt Автор
          02.01.2026 10:54

          Нет, этот не ожидал, тут-то Вы меня и подловили )) Ну, значит, больше решений Богу решений ))


          1. poige
            02.01.2026 10:54

            Нет, этот не ожидал

            Там два, один прям почти один в один. :)

            Ну и не, ловить не тот метод, тот метод — искать. :)


  1. RTFM13
    02.01.2026 10:54

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


    1. vdudouyt Автор
      02.01.2026 10:54

      Ну тут дело происходит далеко за пределами досягаемости РКН )
      А в РФ с "подзатупами" как-то не сталкивался - хотя, с другой стороны, и давно там не был.


      1. RTFM13
        02.01.2026 10:54

        А это специфика скорее опсосов, чем рф. Они с самого начала саботировали конкурентный мультимедиа трафик.


        1. HardWrMan
          02.01.2026 10:54

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


          1. RTFM13
            02.01.2026 10:54

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


            1. 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М шейпинга.

              Мой вывод: не стоит искать теории заговора там, где можно объяснить человеческой глупостью. Иногда это просто криворукие админы, хотя, конечно, если у вас мания преследования то это ещё не значит, что за вами не следят.


              1. RTFM13
                02.01.2026 10:54

                Поясните, в каком месте ваш текст опровергает мои слова?


                1. HardWrMan
                  02.01.2026 10:54

                  Я собаку съел в своё время на диагностике каналов связи и перегрузку от саботажа отличаю лучше любого телепата. Вот эти "затупы" как в статье они очень специфичны.

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


                  1. RTFM13
                    02.01.2026 10:54

                    Я ваш посыл понял. Я спрашиваю где опровержение моих слов?

                    Вы там очень долго рассказываете что шейпер с малым буфером дропал пакеты. Охотно верю, но как это противоречит тому, что я писал выше?

                    Может, я еще какой-то смысл упустил?

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


                    1. HardWrMan
                      02.01.2026 10:54

                      Вы давно общались с каким-либо саппортом?

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

                      Так я решал разнообразные задачи, от блокировки нижних портов до нарушения маршрутизации до моего колокола или выезд специалиста для замеров шумов на линии специальныи прибором (это когда была медь у меня, оптику сам терминал измеряет). Как это сможет объяснить рядовой гражданин? Ответ - никак. Поэтому на вопрос "у меня подтупливает интернет" он резонно получит ответ "перегрузка на линии/перезагрузите устройство". И не пробъётся дальше Олега а тикет закроют как решённый.

                      PS Что касается генеральной линии то мои тезисы действительно лежат в одной плоскости с вашими. Я лишь обсуждаю их причины.


  1. mapnik
    02.01.2026 10:54

    mosh, атэц.
    Если пятисекундные задержки влияют только на ssh — используй mosh!


    1. vdudouyt Автор
      02.01.2026 10:54

      При серфинге веб-страничек тоже сильно заметно. А на всяких нетфликсах с ютубами периодически скидывает в минимальное качество и обратно, если я попал на период "подзатупов".