golang

Go, TUN и UDP: пишем сетевой relay с гибкой конфигурацией

  • понедельник, 30 марта 2026 г. в 00:00:05
https://habr.com/ru/articles/1016296/

Продолжаю пилить на 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. Контакты в профиле.