Ни единого разрыва с TUN/TAP, или как я боролся с «подзатупами» интернета на острове

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

Запускаем

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

$ 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/

@vdudouyt
02.01.2026 15:44 UTC
Первоисточник

Комментарии

@
02.01.2026 10:54 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@vdudouyt
02.01.2026 10:59 UTC
0

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

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

@ky0
02.01.2026 11:02 UTC
+2

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

@vdudouyt
02.01.2026 11:04 UTC
0

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

02.01.2026 11:46 UTC
+1

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

02.01.2026 14:08 UTC
0

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

02.01.2026 14:11 UTC
+1

Ожидал этот комментарий

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

02.01.2026 17:41 UTC
0

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

03.01.2026 12:27 UTC
0

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

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

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

@RTFM13
02.01.2026 11:37 UTC
-1

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

@vdudouyt
02.01.2026 11:48 UTC
+2

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

02.01.2026 11:50 UTC
-1

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

02.01.2026 12:23 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
02.01.2026 15:21 UTC
0

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

03.01.2026 12:59 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
03.01.2026 17:14 UTC
-2

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

03.01.2026 18:01 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
03.01.2026 21:29 UTC
-1

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

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

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

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

04.01.2026 05:35 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@mapnik
02.01.2026 17:09 UTC
0

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

@vdudouyt
02.01.2026 17:46 UTC
+1

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