Tunnel is a golang tun package that supports generic segment/receive offloads (gso and gro) in linux.
- supports gso and gro for both udp* and tcp
- reduce number of read/write syscalls when gso/gro is enabled
Note
Tunnel udp offloading was added in linux v6.2, for prior versions udp offloading is disabled.
This package does not implement tap device.
The linux tun/tap driver is designed to process one packet at a time for each read and write syscall. This means that with each write or read syscall, a single packet, sized according to the tunnel's MTU (maximum transmission unit), is written or read. Considering that system calls are relatively expensive operations, this approach negatively impacts the performance and throughput of the tunnel.
By using generic segmentation and receive offloads and setting the IFF_VNET_HDR flag (virtual network header), more than one packet can be written to or read from the tunnel file descriptor in each read and write operation.
This reduces the number of system calls required and consequently improves performance and throughput by allowing larger data chunks to be processed in a single syscall operation.
For sake of simplicity, in the source code below, setting the IP address on the tunnel interface is done using the ip command. However, for a production environment, it's recommended to use the kernel's netlink APIs directly.
func someVPNTunnelService() {
// request new tun device from kernel
device, err := tunnel.New(tunnel.Config{})
if err != nil {
panic(err)
}
// setting ip by using ip command
ipa := "192.168.87.1/24"
cmd := exec.Command("/usr/bin/ip", "addr", "add", ipa, "dev", device.Name())
if err := cmd.Run(); err!= nil {
panic(err)
}
// setting interface up
cmd := exec.Command("/usr/bin/ip", "link", "set", device.Name(), "up")
if err := cmd.Run(); err!= nil {
panic(err)
}
// conn can be any type of ReadWriteCloser, typically a network connection
conn := someIoReadWriteCloser()
var wg sync.WaitGroup
wg.Add(2)
// read from conn and write to tun device
go func() {
defer device.Close()
writeToTun(device, conn)
wg.Done()
}()
// read from tun device and write to conn
go func() {
defer conn.Close()
io.Copy(conn, device)
wg.Done()
}()
wg.Wait()
}
func writeToTun(dst io.Writer, src io.Reader) error {
// alloc buffer for ip packets:
// note: using a small buffer diminishes the advantages of
// GSO and GRO because the number of system calls increases
buff := make([]byte, 32*1024)
p_left := 0 // unwritten data position
for {
// read from connection
nr, err := src.Read(buff[p_left:])
if err != nil {
return err
}
// if there is any data left from the previous write, take it into account
if p_left > 0 {
nr += p_left
p_left = 0
}
// write to tun device
// if the err is short or fragmented packet, we should continue
// reading from the connection until the packet is fully received
nw, err := dst.Write(buff[:nr])
if err != nil && !shortDataErr(err) {
return err
}
// we couldn't write all the data we read
// the unwritten data will be buffered for the next time
if nr > nw {
// move unwritten data to the head
p_left = copy(buff, buff[nw:nr])
}
}
}
func shortDataErr(err error) bool {
// check if err is fragmented or short packets
return errors.Is(err, tunnel.ErrFragmentedPacket) ||
errors.Is(err, tunnel.ErrShortPacket)
}
Checkout the example for a simple vpn over tcp daemon
The tunnel configuration structure includes the following fields:
type Config struct {
Name string // Name of the tunnel interface (e.g., "tun0")
Persist bool // Whether the tunnel interface should persist (remain after being closed)
Permissions *DevicePermissions // Permissions for the tunnel device
MultiQueue bool // Whether to enable multi-queue support
DisableGsoGro bool // Whether to disable gso/gro and VnetHDR
}
Persist: Indicates whether the tunnel interface should persist. If set to true, the interface will remain active even after being closed.
Permissions: A pointer to a DevicePermissions structure that defines the permissions for the tunnel device. This can include read/write permissions and any other access control settings.
type DevicePermissions struct {
Owner uint // UID of the owner
Group uint // GID of the group
}
MultiQueue: Enables or disables multiqueue support, which can improve performance by allowing multiple queues for packet processing.
Following is the Linux MultiQueue documentation:
From version 3.8, Linux supports multiqueue tuntap which can uses multiple file descriptors (queues) to parallelize packets sending or receiving. The device allocation is the same as before, and if user wants to create multiple queues, TUNSETIFF with the same device name must be called many times with IFF_MULTI_QUEUE flag.
char *dev should be the name of the device, queues is the number of queues to be created, fds is used to store and return the file descriptors (queues) created to the caller. Each file descriptor were served as the interface of a queue which could be accessed by userspace.
DisableGsoGro: Indicates whether to disable gso/gro and VnetHDR
feel free to email me [email protected] if you want to contribute to this project
Copyright 2024 SNIX LLC [email protected] This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.