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.
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.
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:
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
Brak komentarzy.