1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
|
package netlink
import (
"fmt"
"os"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
// ideally golang.org/x/sys/unix would define IfReq but it only has
// IFNAMSIZ, hence this minimalistic implementation
const (
SizeOfIfReq = 40
IFNAMSIZ = 16
)
const TUN = "/dev/net/tun"
type ifReq struct {
Name [IFNAMSIZ]byte
Flags uint16
pad [SizeOfIfReq - IFNAMSIZ - 2]byte
}
// AddQueues opens and attaches multiple queue file descriptors to an existing
// TUN/TAP interface in multi-queue mode.
//
// It performs TUNSETIFF ioctl on each opened file descriptor with the current
// tuntap configuration. Each resulting fd is set to non-blocking mode and
// returned as *os.File.
//
// If the interface was created with a name pattern (e.g. "tap%d"),
// the first successful TUNSETIFF call will return the resolved name,
// which is saved back into tuntap.Name.
//
// This method assumes that the interface already exists and is in multi-queue mode.
// The returned FDs are also appended to tuntap.Fds and tuntap.Queues is updated.
//
// It is the caller's responsibility to close the FDs when they are no longer needed.
func (tuntap *Tuntap) AddQueues(count int) ([]*os.File, error) {
if tuntap.Mode < unix.IFF_TUN || tuntap.Mode > unix.IFF_TAP {
return nil, fmt.Errorf("Tuntap.Mode %v unknown", tuntap.Mode)
}
if tuntap.Flags&TUNTAP_MULTI_QUEUE == 0 {
return nil, fmt.Errorf("TUNTAP_MULTI_QUEUE not set")
}
if count < 1 {
return nil, fmt.Errorf("count must be >= 1")
}
req, err := unix.NewIfreq(tuntap.Name)
if err != nil {
return nil, err
}
req.SetUint16(uint16(tuntap.Mode) | uint16(tuntap.Flags))
var fds []*os.File
for i := 0; i < count; i++ {
localReq := req
fd, err := unix.Open(TUN, os.O_RDWR|syscall.O_CLOEXEC, 0)
if err != nil {
cleanupFds(fds)
return nil, err
}
err = unix.IoctlIfreq(fd, unix.TUNSETIFF, req)
if err != nil {
// close the new fd
unix.Close(fd)
// and the already opened ones
cleanupFds(fds)
return nil, fmt.Errorf("tuntap IOCTL TUNSETIFF failed [%d]: %w", i, err)
}
// Set the tun device to non-blocking before use. The below comment
// taken from:
//
// https://github.com/mistsys/tuntap/commit/161418c25003bbee77d085a34af64d189df62bea
//
// Note there is a complication because in go, if a device node is
// opened, go sets it to use nonblocking I/O. However a /dev/net/tun
// doesn't work with epoll until after the TUNSETIFF ioctl has been
// done. So we open the unix fd directly, do the ioctl, then put the
// fd in nonblocking mode, an then finally wrap it in a os.File,
// which will see the nonblocking mode and add the fd to the
// pollable set, so later on when we Read() from it blocked the
// calling thread in the kernel.
//
// See
// https://github.com/golang/go/issues/30426
// which got exposed in go 1.13 by the fix to
// https://github.com/golang/go/issues/30624
err = unix.SetNonblock(fd, true)
if err != nil {
cleanupFds(fds)
return nil, fmt.Errorf("tuntap set to non-blocking failed [%d]: %w", i, err)
}
// create the file from the file descriptor and store it
file := os.NewFile(uintptr(fd), TUN)
fds = append(fds, file)
// 1) we only care for the name of the first tap in the multi queue set
// 2) if the original name was empty, the localReq has now the actual name
//
// In addition:
// This ensures that the link name is always identical to what the kernel returns.
// Not only in case of an empty name, but also when using name templates.
// e.g. when the provided name is "tap%d", the kernel replaces %d with the next available number.
if i == 0 {
tuntap.Name = strings.Trim(localReq.Name(), "\x00")
}
}
tuntap.Fds = append(tuntap.Fds, fds...)
tuntap.Queues = len(tuntap.Fds)
return fds, nil
}
// RemoveQueues closes the given TAP queue file descriptors and removes them
// from the tuntap.Fds list.
//
// This is a logical counterpart to AddQueues and allows releasing specific queues
// (e.g., to simulate queue failure or perform partial detach).
//
// The method updates tuntap.Queues to reflect the number of remaining active queues.
//
// It is safe to call with a subset of tuntap.Fds, but the caller must ensure
// that the passed *os.File descriptors belong to this interface.
func (tuntap *Tuntap) RemoveQueues(fds ...*os.File) error {
toClose := make(map[uintptr]struct{}, len(fds))
for _, fd := range fds {
toClose[fd.Fd()] = struct{}{}
}
var newFds []*os.File
for _, fd := range tuntap.Fds {
if _, shouldClose := toClose[fd.Fd()]; shouldClose {
if err := fd.Close(); err != nil {
return fmt.Errorf("failed to close queue fd %d: %w", fd.Fd(), err)
}
tuntap.Queues--
} else {
newFds = append(newFds, fd)
}
}
tuntap.Fds = newFds
return nil
}
|