Туннелирование трафика в macOS: адаптация примера на Go
- пятница, 20 марта 2026 г. в 00:00:13
В этой статье мы перенесем разработанный инструмент на macOS. Логика работы с трафиком в userspace сохраняется, но интеграция с системой требует иных решений. Разберем особенности создания utun-интерфейсов, настройки маршрутизации через ifscope и использования pf для NAT. По итогу запустим цепочку tun0 -> go app -> 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.