Go a TUN/TAP: UDP-relay s konfigurací ve formátu YAML
Vývojáři v Go mohou vytvořit síťový relay pro práci s TUN rozhraními a UDP tunely. Architektura využívá konfiguraci ve formátu YAML k definici řetězce ingress-egress. Každý relay zpracovává dvousměrný tok: od TUN k UDP a zpět. To umožňuje vytvářet složité trasy bez pevného přidružení k kódu.
Základem je implementace io.ReadWriteCloser pro všechny uzly. Logika je jednoduchá: čtení paketů z ingress, předání na egress, zpracování v opačném směru.
Struktura YAML-konfigurace
Konfigurace definuje relays jako pole párů ingress/egress. Parametry zahrnují typ rozhraní, IP adresaci, pravidla NAT a ověření přístupu.
Příklad 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"
Pro UDP-relay se přidají klient a server:
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"
Vznikne tak řetězec tun10 → UDP-klient → UDP-server → tun11. Doprava zůstává na localhostu.
Protokol UDP-tunelu
K IP-paketu se přidá hlavička: 4 bajty timestamp (uint32 BigEndian) + 16 bajtů MD5-haš. Haš se počítá z hesla + timestamp + prvních 64 bajtů paketu.
Při přijetí se ověří:
- Velikost paketu ≥ HeaderSize (20 bajtů)
- Timestamp je v rámci 10 sekund od aktuálního času
- Správnost haše
Kód rozbalení:
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
}
Balení je symetrické: calcHash + binary.BigEndian + odeslání.
Testování výkonu
Místní testy s iperf3 přes tun10-tun11:
Server:
iperf3 -s -B 10.0.1.2
Klient:
iperf3 -c 10.0.1.2 -B 10.0.0.2
Výsledky (10 sekund):
| Interval | Transfer | Bitrate |
|----------|----------|---------|
| 0-1s | 113 MB | 941 Mbit/s |
| 1-2s | 109 MB | 921 Mbit/s |
| ... | ... | ... |
| Celkem | 1,08 GB | 925 Mbit/s (odesílatel) |
~922 Mbit/s na přijímači. CPU: relay ~250% na M1, iperf3 ~10–30%. Optimalizace je možná prostřednictvím batching, zero-copy nebo SIMD-hashování.
Klíčové metriky pro analýzu:
- Propustnost: průměrně 925 Mbit/s
- Nároky na CPU: vysoká zátěž jednoho vlákna
- Zpoždění: okno timestampu 10 s (dostatečné pro místní testy)
- Spolehlivost: haš + čas chrání proti replay/injection
Co je důležité
- Konfigurace ve formátu YAML umožňuje kombinovat TUN/UDP bez překompilace
- Protokol je minimalistický: 20 bajtů nadbytečných dat, MD5 + timestamp
- Výkon ~900+ Mbit/s lokálně, ale omezený výkonem CPU
- Rozšiřitelnost prostřednictvím
io.ReadWriteCloser: snadné přidání WireGuard, QUIC - Pravidla NAT v konfiguraci zjednodušují komunikaci mezi uzly
— Editorial Team
Zatím žádné komentáře.