Back to Home

Go TUN UDP relay: YAML config and code

The article describes the implementation of UDP-relay on Go with support for TUN interfaces via YAML configuration. The protocol with MD5 hash and timestamp is detailed, iperf3 benchmarks at 925 Mbit/s are provided. Suitable for middle/senior network software developers.

Creating UDP-relay on Go with TUN interfaces
Advertisement 728x90

Go and TUN/TAP: Building a UDP Relay with YAML Configuration

Go developers can build network relays that work with TUN interfaces and UDP tunnels. The architecture uses a YAML configuration to define ingress-egress chains. Each relay handles bidirectional traffic—between TUN and UDP, and back again—enabling complex routing without hardcoding logic.

At its core, the implementation uses io.ReadWriteCloser for all nodes. The logic is straightforward: read packets from ingress, forward them to egress, and process the reverse flow.

YAML Configuration Structure

The config defines relays as an array of ingress/egress pairs. Parameters include interface type, IP addressing, NAT rules, and authentication.

Google AdInline article slot

Example: 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"

For a UDP relay, add client and server endpoints:

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"

This creates a chain: tun10 → UDP client → UDP server → tun11. Traffic stays local.

Google AdInline article slot

UDP Tunnel Protocol

A header is added to each IP packet: 4 bytes timestamp (uint32 BigEndian) + 16 bytes MD5 hash. The hash is computed from password + timestamp + first 64 bytes of the packet.

On reception, the system checks:

  • Packet size ≥ HeaderSize (20 bytes)
  • Timestamp within 10 seconds of current time
  • Hash matches

Unpacking code:

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
}

Packing is symmetric: calcHash + binary.BigEndian + send.

Performance Testing

Local tests using iperf3 over tun10-tun11:

Server:

iperf3 -s -B 10.0.1.2

Client:

iperf3 -c 10.0.1.2 -B 10.0.0.2

Results (10 seconds):

| Interval | Transfer | Bitrate |

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

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

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

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

| Total | 1.08 GB | 925 Mbit/s (sender) |

~922 Mbit/s on receiver. CPU: relay ~250% on M1, iperf3 ~10–30%. Optimization possible via batching, zero-copy, or SIMD hashing.

Key metrics for analysis:

  • Throughput: 925 Mbit/s average
  • CPU load: High single-thread usage
  • Latency: 10-second timestamp window (sufficient for local testing)
  • Reliability: Hash + timestamp prevent replay and injection attacks

Key Takeaways

  • YAML config enables mixing TUN/UDP without recompilation
  • Protocol is minimal: 20-byte overhead, MD5 + timestamp
  • Performance ~900+ Mbit/s locally, but CPU-bound
  • Extensible via io.ReadWriteCloser: easy to add WireGuard, QUIC
  • NAT rules in config simplify peer-to-peer setups

— Editorial Team

Advertisement 728x90

Read Next