Go et TUN/TAP : Créer un relais UDP avec configuration YAML
Les développeurs Go peuvent créer des relais réseau fonctionnant avec les interfaces TUN et les tunnels UDP. L'architecture repose sur une configuration YAML pour définir des chaînes d'entrée et de sortie. Chaque relais gère le trafic bidirectionnel — entre TUN et UDP, puis inversement — permettant des routages complexes sans codage dur de la logique.
Au cœur de l'implémentation, on utilise io.ReadWriteCloser pour tous les nœuds. La logique est simple : lire les paquets à l'entrée, les acheminer vers la sortie, puis traiter le flux inverse.
Structure de configuration YAML
La configuration définit les relais sous forme de tableaux de paires entrée/sortie. Les paramètres incluent le type d'interface, l'adressage IP, les règles NAT et l'authentification.
Exemple : TUN vers 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"
Pour un relais UDP, ajoutez les points de terminaison client et serveur :
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"
Cela crée une chaîne : tun10 → client UDP → serveur UDP → tun11. Le trafic reste local.
Protocole de tunnel UDP
Un en-tête est ajouté à chaque paquet IP : 4 octets pour l'horodatage (uint32 BigEndian) + 16 octets pour le hachage MD5. Ce dernier est calculé à partir du mot de passe + horodatage + les 64 premiers octets du paquet.
À la réception, le système vérifie :
- La taille du paquet ≥ TailleEnTête (20 octets)
- L'horodatage dans une fenêtre de 10 secondes par rapport à l'heure actuelle
- Le hachage correspond
Code de dépaquetage :
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
}
Le paquetage est symétrique : calcHash + binary.BigEndian + envoi.
Tests de performance
Tests locaux avec iperf3 sur tun10-tun11 :
Serveur :
iperf3 -s -B 10.0.1.2
Client :
iperf3 -c 10.0.1.2 -B 10.0.0.2
Résultats (10 secondes) :
| Intervalle | Transfert | Débit |
|----------|----------|---------|
| 0-1s | 113 MB | 941 Mbit/s |
| 1-2s | 109 MB | 921 Mbit/s |
| ... | ... | ... |
| Total | 1.08 GB | 925 Mbit/s (expéditeur) |
~922 Mbit/s au récepteur. CPU : relais ~250 % sur M1, iperf3 ~10–30 %. Des optimisations sont possibles via regroupement, copie zéro ou hachage SIMD.
Métriques clés pour l’analyse :
- Débit : 925 Mbit/s en moyenne
- Charge CPU : usage élevé sur un seul thread
- Latence : fenêtre d’horodatage de 10 secondes (suffisante pour les tests locaux)
- Fiabilité : hachage + horodatage empêchent les attaques par relecture et injection
Points clés
- La configuration YAML permet de mélanger TUN/UDP sans recompilation
- Le protocole est minimal : surcharge de 20 octets, MD5 + horodatage
- Performance ~900+ Mbit/s localement, mais limitée par le CPU
- Extensible via
io.ReadWriteCloser: ajout facile de WireGuard, QUIC - Les règles NAT dans la config simplifient les configurations pair à pair
— Editorial Team
Aucun commentaire pour le moment.