Go und TUN/TAP: UDP-Relais mit YAML-Konfiguration erstellen
Go-Entwickler können Netzwerk-Relais erstellen, die mit TUN-Schnittstellen und UDP-Tunneln arbeiten. Die Architektur nutzt eine YAML-Konfiguration, um Eingangs- und Ausgangsketten zu definieren. Jeder Relay verarbeitet bidirektionale Datenströme – zwischen TUN und UDP und zurück – und ermöglicht komplexe Routing-Logik, ohne dass diese im Code fest verankert sein muss.
Im Kern verwendet die Implementierung io.ReadWriteCloser für alle Knoten. Die Logik ist einfach: Pakete vom Eingang lesen, an den Ausgang weiterleiten und den Rückfluss verarbeiten.
YAML-Konfigurationsstruktur
Die Konfiguration definiert Relays als Array von Eingangs-/Ausgangspaaren. Parameter umfassen Schnittstellentyp, IP-Adressierung, NAT-Regeln und Authentifizierung.
Beispiel: TUN-zu-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"
Für einen UDP-Relay fügen Sie Client- und Serverendpunkte hinzu:
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"
Dies erzeugt eine Kette: tun10 → UDP-Client → UDP-Server → tun11. Der Datenverkehr bleibt lokal.
UDP-Tunnel-Protokoll
Jedes IP-Paket erhält einen Header: 4 Bytes Zeitstempel (uint32 BigEndian) + 16 Bytes MD5-Hash. Der Hash wird aus Passwort + Zeitstempel + ersten 64 Bytes des Pakets berechnet.
Bei der Aufnahme prüft das System:
- Paketgröße ≥ HeaderSize (20 Bytes)
- Zeitstempel liegt innerhalb von 10 Sekunden der aktuellen Uhrzeit
- Hash stimmt überein
Entpackungscode:
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
}
Das Verpacken ist symmetrisch: calcHash + binary.BigEndian + Senden.
Leistungstest
Lokale Tests mit iperf3 über tun10-tun11:
Server:
iperf3 -s -B 10.0.1.2
Client:
iperf3 -c 10.0.1.2 -B 10.0.0.2
Ergebnisse (10 Sekunden):
| Intervall | Übertragung | Bitrate |
|----------|----------|---------|
| 0-1s | 113 MB | 941 Mbit/s |
| 1-2s | 109 MB | 921 Mbit/s |
| ... | ... | ... |
| Gesamt | 1,08 GB | 925 Mbit/s (Sender) |
~922 Mbit/s am Empfänger. CPU: Relay ~250% auf M1, iperf3 ~10–30%. Optimierungen sind möglich durch Batching, Zero-Copy oder SIMD-Hashing.
Wichtige Metriken für die Analyse:
- Durchsatz: Durchschnittlich 925 Mbit/s
- CPU-Auslastung: Hoher Einzelthread-Verbrauch
- Latenz: 10-Sekunden-Zeitfenster (ausreichend für lokale Tests)
- Zuverlässigkeit: Hash + Zeitstempel schützen vor Replay- und Injection-Angriffen
Wichtige Erkenntnisse
- YAML-Konfiguration ermöglicht das Mischen von TUN/UDP ohne Neukompilierung
- Protokoll ist minimal: 20-Byte-Overhead, MD5 + Zeitstempel
- Leistung ~900+ Mbit/s lokal, aber CPU-begrenzt
- Erweiterbar via
io.ReadWriteCloser: einfach WireGuard, QUIC hinzufügen - NAT-Regeln in der Konfiguration vereinfachen Peer-to-Peer-Setups
— Editorial Team
Noch keine Kommentare.