golang

Туннелирование трафика: простое решение на Go 2

  • четверг, 19 марта 2026 г. в 00:00:09
https://habr.com/ru/articles/1011676/

Ранее я начал писать небольшую программу на Go, чтобы прокидывать трафик между компьютером и своим сервером. Назначение программы исследовательское - посмотреть "как это работает" и разобраться, как именно работают механизмы, мешающие нормальному интернету.

В продолжение эксперимента: имеется подключение к интернету через менее дружелюбного провайдера и ноут на macOS (первая версия была под Linux). Прежде чем усложнять код, решил запустить программу на маке без изменения логики. Напомню: весь функционал - пакеты прокидываются по цепочке tun0 -> tun1 -> inet. Это минимальная заготовка, на которой удобно разбираться с туннелями.

Принципиальных изменений в коде нет, но туннели создаются и настраиваются иначе. В Linux-варианте я открывал туннель без сторонних библиотек, здесь же разбираться не стал и использовал библиотеку water (пока не важно, потом нужно сделать единообразно). Программа завелась без проблем, а вот с маршрутизацией пришлось повозиться.

Для начала нужно включить форвардинг пакетов (для этапа tun1 -> inet):

sysctl -w net.inet.ip.forwarding=1

Когда туннели поднимаются, в таблице маршрутизации появляются две записи:

netstat -nr -f inet
Routing tables

Internet:
Destination        Gateway            Flags               Netif Expire
...
10.0.0.1           10.0.0.2           UHr                utun10
10.0.1.1           10.0.1.2           UHr                utun11
...

Привязать пинг к конкретному интерфейсу нельзя. Можно указать источник через -S, это не совсем то, но допустим:

ping -S 10.0.0.2 8.8.8.8

Система отказывается отправлять пакеты в нужный интерфейс, если по таблице маршрутизации они попадают в другой (en0). До туннеля пакеты не доходят, программа их не видит.

PING 8.8.8.8 (8.8.8.8) from 10.0.0.2: 56 data bytes
ping: sendto: No route to host
ping: sendto: No route to host

Проблема решается указанием маршрута по умолчанию для конкретного интерфейса:

route add -ifscope utun10 default 10.0.0.1
Routing tables

Internet:
Destination        Gateway            Flags               Netif Expire
default            192.168.1.1        UGScg                 en0
default            10.0.0.1           UGScIg             utun10
default            10.0.1.1           UGScIg             utun11
10.0.0.1           10.0.0.2           UHr                utun10
10.0.1.1           10.0.1.2           UHr                utun11

Осталось включить системный NAT для форвардинга utun11 -> en0.

echo "nat on en0 from 10.0.1.1/32 to any -> (en0)" | sudo pfctl -Ef -

После этого всё заработало:

PING 8.8.8.8 (8.8.8.8) from 10.0.0.2: 56 data bytes
64 bytes from 8.8.8.8: icmp_seq=0 ttl=105 time=49.326 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=105 time=51.521 ms
tcpdump -k -i any -n icmp
19:00:37.914822 IP 10.0.0.2 > 8.8.8.8: ICMP echo request
19:00:37.914995 IP 10.0.1.1 > 8.8.8.8: ICMP echo request
19:00:37.915062 IP 192.168.1.137 > 8.8.8.8: ICMP echo request
19:00:37.968373 IP 8.8.8.8 > 192.168.1.137: ICMP echo reply
19:00:37.968483 IP 8.8.8.8 > 10.0.1.1: ICMP echo reply
19:00:37.968774 IP 8.8.8.8 > 10.0.0.2: ICMP echo reply

Может показаться, что изменений немного - просто запустил код на другой ОС. Однако отладка на стыке программы и системных настроек неудобна: ошибок нет, но ничего не работает. Зато теперь у меня есть всё необходимое, чтобы получать и отправлять пакеты из системы. Теперь можно пробовать прокидывать реальный трафик и переходить к самим экспериментам.

Ссылка на github.