Powrót do strony głównej

Go TUN UDP relay: konfiguracja YAML i kod

Artykuł opisuje implementację UDP-relay w Go z obsługą interfejsów TUN poprzez konfigurację YAML. Szczegółowo opisano protokół z hashem MD5 i timestamp, podano benchmarki iperf3 na 925 Mbit/s. Nadaje się dla middle/senior programistów oprogramowania sieciowego.

Tworzymy UDP-relay w Go z interfejsami TUN
Advertisement 728x90

Go i TUN/TAP: UDP-relay z konfiguracją YAML

Programiści w języku Go mogą tworzyć relaje sieciowe obsługujące interfejsy TUN oraz tunelowanie UDP. Architektura wykorzystuje plik konfiguracyjny w formacie YAML do definiowania łańcuchów wejścia/wyjścia. Każdy relay przetwarza strumień dwukierunkowy — od TUN do UDP i z powrotem. Dzięki temu możliwe jest budowanie skomplikowanych tras bez potrzeby zmian w kodzie.

W podstawie leży implementacja interfejsu io.ReadWriteCloser dla wszystkich węzłów. Logika jest prosta: odczyt pakietów z wejścia, przekazanie na wyjście, a następnie przetwarzanie w kierunku przeciwnym.

Struktura konfiguracji YAML

Konfiguracja definiuje relaje jako tablicę par wejście/wyjście. Parametry obejmują typ interfejsu, adresację IP, reguły NAT oraz uwierzytelnianie.

Google AdInline article slot

Przykład TUN-to-TUN:

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"

Do relay'a UDP dodaje się klienta i serwer:

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"

Powstaje łańcuch tun10 → klient-UDP → serwer-UDP → tun11. Ruch pozostaje lokalnie.

Google AdInline article slot

Protokół tunelowania UDP

Do pakietu IP dodawany jest nagłówek: 4 bajty timestamp (uint32 BigEndian) + 16 bajtów skrótu MD5. Skrót obliczany jest z hasła + timestamp + pierwsze 64 bajtów pakietu.

Podczas odbioru sprawdzane są:

  • Rozmiar pakietu ≥ HeaderSize (20 bajtów)
  • Timestamp w granicach 10 sekund od aktualnego czasu
  • Poprawność skrótu

Kod rozpakowywania:

Google AdInline article slot
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
}

Pakowanie jest symetryczne: calcHash + binary.BigEndian + wysyłka.

Testowanie wydajności

Testy lokalne za pomocą iperf3 przez tun10-tun11:

Serwer:

iperf3 -s -B 10.0.1.2

Klient:

iperf3 -c 10.0.1.2 -B 10.0.0.2

Wyniki (10 sekund):

| Interwał | Transfer | Prędkość |

|----------|----------|---------|

| 0-1s | 113 MB | 941 Mbit/s |

| 1-2s | 109 MB | 921 Mbit/s |

| ... | ... | ... |

| Razem | 1,08 GB | 925 Mbit/s (nadawca) |

~922 Mbit/s na odbiorcy. CPU: relay ~250% na M1, iperf3 ~10–30%. Możliwa optymalizacja poprzez batching, zero-copy lub SIMD-hashowanie.

Kluczowe metryki analizy:

  • Przepustowość: średnio 925 Mbit/s
  • Obciążenie CPU: wysokie obciążenie jednowątkowe
  • Opóźnienie: okno timestamp 10 s (dostateczne dla testów lokalnych)
  • Niezawodność: skrót + czas chronią przed replay/injection

Co warto wiedzieć

  • Konfiguracja YAML pozwala łączyć TUN/UDP bez ponownego kompilowania
  • Protokół minimalny: 20 bajtów narzutu, MD5 + timestamp
  • Wydajność ~900+ Mbit/s lokalnie, ale ograniczona przez CPU
  • Rozszerzalność dzięki io.ReadWriteCloser: łatwo dodać WireGuard, QUIC
  • Reguły NAT w konfiguracji upraszczają połączenia peer-to-peer

— Editorial Team

Advertisement 728x90

Czytaj dalej