Go, TUN и UDP: пишем сетевой relay с гибкой конфигурацией
- понедельник, 30 марта 2026 г. в 00:00:05
Продолжаю пилить на Go утилиту для работы с TUN-интерфейсами. В предыдущей версии пакеты проходили путь system <-> tun10 <-> go app <-> tun11 <-> inet. Основная цель тогда была одна — разобраться с TUN-интерфейсами и сетевыми настройками. В текущей версии я добавил простейший udp relay, вынес сложность в конфиг и в целом переработал проект.

В проекте появился YAML-конфиг, и теперь верхнеуровневая логика движения данных находится там:
relays: - ingress: type: tun name: tun10 cidr: "10.0.0.2/24" peer: "10.0.0.1" egress: type: tun name: tun11 cidr: "10.0.1.2/24" peer: "10.0.1.1" nat: forward: src: "10.0.1.1" backward: dst: "10.0.0.2"
Есть список relay, каждый элемент — это точка входа (ingress) и выхода (egress) пакетов (в реальности данные ходят в обоих направлениях). В примере выше описана схема tun10 -> tun11, но в такой парадигме достаточно легко добавлять новые узлы.
Например, для UDP конфиг будет выглядеть так:
relays: - ingress: type: tun name: tun10 cidr: "10.0.0.2/24" peer: "10.0.0.1" egress: type: udp dial: "localhost:4000" password: "pass" - ingress: type: udp listen: "localhost:4000" password: "pass" egress: type: tun name: tun11 cidr: "10.0.1.2/24" peer: "10.0.1.1" nat: forward: src: "10.0.1.1" backward: dst: "10.0.0.2"
Получаем схему tun10 -> udp client -> udp server -> tun11, UDP-трафик не покидает localhost.
С реализацией всё просто: чтобы создать свой ingress или egress, достаточно имплементировать стандартный интерфейс io.ReadWriteCloser:
type ReadWriteCloser interface { Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) Close() error }
Логика работы UDP-relay максимально простая: к исходному пакету добавляются timestamp и MD5-хеш. Хеш вычисляется от пароля, timestamp и первых 64 байт IP-пакета. При получении пакета помимо хеша проверяется время: timestamp не должен отличаться от текущего более чем на 10 секунд.
Получение и распаковка UDP-пакета буквально две функции, вся суть протокола сосредоточена в функции unpack:
func (i *Ingress) Read(b []byte) (int, error) { n, raddr, err := i.conn.ReadFrom(b) if err != nil { return 0, err } data, err := unpack(b[:n:n], i.pass) if err != nil { return 0, err } i.raddr = raddr copy(b, data) return len(data), nil } func unpack(packet []byte, pass string) ([]byte, error) { if len(packet) < HeaderSize { return nil, ErrSmallPacket } rtimestamp := binary.BigEndian.Uint32(packet[0:4]) rhash := [HashSize]byte(packet[4 : 4+HashSize]) payload := packet[HeaderSize:] timestamp := uint32(time.Now().Unix()) if timestamp-rtimestamp > MaxTimeDiff && rtimestamp-timestamp > MaxTimeDiff { return nil, ErrStalePacket } hash, err := calcHash(pass, payload, rtimestamp) if err != nil { return nil, fmt.Errorf("calc hash: %w", err) } if rhash != hash { return nil, ErrWrongPass } return payload, nil }
Полноценными тестами я не занимался, но нагрузил локально всю связку туннели и приложение с помощью iperf3.
iperf3 -s -B 10.0.1.2
iperf3 -c 10.0.1.2 -B 10.0.0.2
Connecting to host 10.0.1.2, port 5201 [ 5] local 10.0.0.2 port 60502 connected to 10.0.1.2 port 5201 [ ID] Interval Transfer Bitrate [ 5] 0.00-1.00 sec 113 MBytes 941 Mbits/sec [ 5] 1.00-2.00 sec 109 MBytes 921 Mbits/sec [ 5] 2.00-3.00 sec 110 MBytes 922 Mbits/sec [ 5] 3.00-4.00 sec 110 MBytes 926 Mbits/sec [ 5] 4.00-5.00 sec 110 MBytes 921 Mbits/sec [ 5] 5.00-6.00 sec 110 MBytes 929 Mbits/sec [ 5] 6.00-7.00 sec 109 MBytes 914 Mbits/sec [ 5] 7.00-8.00 sec 110 MBytes 925 Mbits/sec [ 5] 8.00-9.00 sec 110 MBytes 922 Mbits/sec [ 5] 9.00-10.00 sec 111 MBytes 929 Mbits/sec - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate [ 5] 0.00-10.00 sec 1.08 GBytes 925 Mbits/sec sender [ 5] 0.00-10.00 sec 1.07 GBytes 922 Mbits/sec receiver
Результат на первый взгляд хороший, но при этом программа нагружает проц на ~250% по top на m1, при этом iperf3 тратит всего ~10% и ~30%. Это много, здесь очевидно есть пространство для оптимизации.
P.S. Открыт для предложений по работе. Интересует Go, backend. Контакты в профиле.