- Network
- A
Go, TUN and UDP: Writing a Network Relay with Flexible Configuration
I continue to develop a Go utility for working with TUN interfaces. In the previous version, packets went through the path `system <-> tun10 <-> go app <-> tun11 <-> inet`. The main goal at that time was to understand TUN interfaces and network settings. In the current version, I added a simple `udp relay`, moved the complexity to the config, and overall redesigned the project.
The project now has a YAML config, and the high-level data flow logic is defined there:
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"There's a list of relay entries, each element is an entry (ingress) and exit (egress) point for packets (in reality, data moves in both directions). The example above describes the scheme tun10 -> tun11, but in this paradigm, adding new nodes is quite easy.
For example, for UDP, the config would look like this:
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"We get the scheme tun10 -> udp client -> udp server -> tun11, with UDP traffic staying within localhost.
The implementation is simple: to create your own ingress or egress, just implement the standard interface io.ReadWriteCloser:
type ReadWriteCloser interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
}The logic of the UDP relay is as simple as possible: a timestamp and MD5 hash are added to the original packet. The hash is computed from the password, timestamp, and the first 64 bytes of the IP packet. When receiving a packet, besides the hash, the time is checked: timestamp should not differ from the current time by more than 10 seconds.
The receiving and unpacking of the UDP packet is literally two functions, and the essence of the protocol is concentrated in the unpack function:
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
}
I haven’t done full-fledged testing, but I stressed the entire setup locally—tunnels and application—using iperf3.
iperf3 -s -B 10.0.1.2
iperf3 -c 10.0.1.2 -B 10.0.0.2
Connecting to host 10.0.1.2, port 5201
[ 5] local 10.0.0.2 port 60502 connected to 10.0.1.2 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 113 MBytes 941 Mbits/sec
[ 5] 1.00-2.00 sec 109 MBytes 921 Mbits/sec
[ 5] 2.00-3.00 sec 110 MBytes 922 Mbits/sec
[ 5] 3.00-4.00 sec 110 MBytes 926 Mbits/sec
[ 5] 4.00-5.00 sec 110 MBytes 921 Mbits/sec
[ 5] 5.00-6.00 sec 110 MBytes 929 Mbits/sec
[ 5] 6.00-7.00 sec 109 MBytes 914 Mbits/sec
[ 5] 7.00-8.00 sec 110 MBytes 925 Mbits/sec
[ 5] 8.00-9.00 sec 110 MBytes 922 Mbits/sec
[ 5] 9.00-10.00 sec 111 MBytes 929 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate
[ 5] 0.00-10.00 sec 1.08 GBytes 925 Mbits/sec sender
[ 5] 0.00-10.00 sec 1.07 GBytes 922 Mbits/sec receiver
The result looks good at first glance, but the program loads the CPU to ~250% according to top on m1, while iperf3 only uses ~10% and ~30%. That’s a lot; clearly, there’s room for optimization here.
P.S. Open to work proposals. Interested in Go, backend. Contacts are in my profile.
Write comment