Skip to content

Commit 14e2f0a

Browse files
vanytsvetkovaboch
authored andcommitted
tuntap: add support for dynamically managing multi-queue FDs
Introduce AddQueues and RemoveQueues methods for attaching and detaching queue file descriptors to an existing TUN/TAP interface in multi-queue mode. This enables controlled testing of disabled queues and fine-grained queue management without relying on interface recreation. Signed-off-by: Ivan Tsvetkov <[email protected]>
1 parent 298a362 commit 14e2f0a

File tree

2 files changed

+223
-1
lines changed

2 files changed

+223
-1
lines changed

link_test.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,9 +650,29 @@ func compareTuntap(t *testing.T, expected, actual *Tuntap) {
650650
t.Fatal("Tuntap.Group doesn't match")
651651
}
652652

653+
if expected.Flags&TUNTAP_NO_PI != actual.Flags&TUNTAP_NO_PI {
654+
t.Fatal("Tuntap.NoPI doesn't match")
655+
}
656+
657+
if expected.Flags&TUNTAP_VNET_HDR != actual.Flags&TUNTAP_VNET_HDR {
658+
t.Fatal("Tuntap.VNetHdr doesn't match")
659+
}
660+
653661
if expected.NonPersist != actual.NonPersist {
654662
t.Fatal("Tuntap.Group doesn't match")
655663
}
664+
665+
if expected.Flags&TUNTAP_MULTI_QUEUE != actual.Flags&TUNTAP_MULTI_QUEUE {
666+
t.Fatal("Tuntap.MultiQueue doesn't match")
667+
}
668+
669+
if expected.Queues != actual.Queues {
670+
t.Fatal("Tuntap.Queues doesn't match")
671+
}
672+
673+
if expected.DisabledQueues != actual.DisabledQueues {
674+
t.Fatal("Tuntap.DisableQueues doesn't match")
675+
}
656676
}
657677

658678
func compareBareUDP(t *testing.T, expected, actual *BareUDP) {
@@ -3051,13 +3071,78 @@ func TestLinkAddDelTuntapMq(t *testing.T) {
30513071
testLinkAddDel(t, &Tuntap{
30523072
LinkAttrs: LinkAttrs{Name: "foo"},
30533073
Mode: TUNTAP_MODE_TAP,
3054-
Queues: 4})
3074+
Queues: 4,
3075+
Flags: TUNTAP_MULTI_QUEUE_DEFAULTS})
30553076

30563077
testLinkAddDel(t, &Tuntap{
30573078
LinkAttrs: LinkAttrs{Name: "foo"},
30583079
Mode: TUNTAP_MODE_TAP,
30593080
Queues: 4,
30603081
Flags: TUNTAP_MULTI_QUEUE_DEFAULTS | TUNTAP_VNET_HDR})
3082+
3083+
testLinkAddDel(t, &Tuntap{
3084+
LinkAttrs: LinkAttrs{Name: "foo"},
3085+
Mode: TUNTAP_MODE_TAP,
3086+
Queues: 0,
3087+
Flags: TUNTAP_MULTI_QUEUE_DEFAULTS | TUNTAP_VNET_HDR})
3088+
}
3089+
3090+
func TestTuntapPartialQueues(t *testing.T) {
3091+
tearDown := setUpNetlinkTest(t)
3092+
defer tearDown()
3093+
3094+
if err := syscall.Mount("sysfs", "/sys", "sysfs", syscall.MS_RDONLY, ""); err != nil {
3095+
t.Fatal("Cannot mount sysfs")
3096+
}
3097+
3098+
defer func() {
3099+
if err := syscall.Unmount("/sys", 0); err != nil {
3100+
t.Fatal("Cannot umount /sys")
3101+
}
3102+
}()
3103+
3104+
compare := func(expected *Tuntap) {
3105+
result, err := LinkByName(expected.Name)
3106+
if err != nil {
3107+
t.Fatal(err)
3108+
}
3109+
3110+
other, ok := result.(*Tuntap)
3111+
if !ok {
3112+
t.Fatal("Result of create is not a tuntap")
3113+
}
3114+
compareTuntap(t, expected, other)
3115+
}
3116+
3117+
tap := &Tuntap{
3118+
LinkAttrs: LinkAttrs{Name: "foo"},
3119+
Mode: TUNTAP_MODE_TAP,
3120+
Queues: 2,
3121+
Flags: TUNTAP_MULTI_QUEUE_DEFAULTS | TUNTAP_VNET_HDR,
3122+
}
3123+
3124+
if err := LinkAdd(tap); err != nil {
3125+
t.Fatalf("Failed to add tap: %v", err)
3126+
}
3127+
defer cleanupFds(tap.Fds)
3128+
3129+
fds, err := tap.AddQueues(2)
3130+
if err != nil {
3131+
t.Fatalf("Failed to enable queues: %v", err)
3132+
}
3133+
3134+
compare(tap)
3135+
3136+
err = tap.RemoveQueues(fds...)
3137+
if err != nil {
3138+
t.Fatalf("Failed to close queues: %v", err)
3139+
}
3140+
3141+
compare(tap)
3142+
3143+
if err = LinkDel(tap); err != nil {
3144+
t.Fatal(err)
3145+
}
30613146
}
30623147

30633148
func TestLinkAddDelTuntapOwnerGroup(t *testing.T) {

link_tuntap_linux.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,151 @@
11
package netlink
22

3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
"syscall"
8+
9+
"golang.org/x/sys/unix"
10+
)
11+
312
// ideally golang.org/x/sys/unix would define IfReq but it only has
413
// IFNAMSIZ, hence this minimalistic implementation
514
const (
615
SizeOfIfReq = 40
716
IFNAMSIZ = 16
817
)
918

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

0 commit comments

Comments
 (0)