overlay: replace per-fd tun readers with a batched Queue interface

Device loses io.ReadWriteCloser + NewMultiQueueReader in favor of
Queues(n), which returns up to n tio.Queue objects; platforms without
multiqueue hand back their single queue and the interface sizes its
reader routines to what it actually got. Queue.Read returns a batch of
borrowed packets (single-element for every current backend) so a future
backend can deliver more than one packet per syscall without another
interface change.

The Linux poll/eventfd machinery moves out of tun_linux.go into the new
overlay/tio package: nonblocking fds, a shared shutdown eventfd owned by
the queue set, and pollfd arrays built on the stack so concurrent
writers parked in blockOnWrite no longer share Revents storage. Other
platforms wrap their existing one-datagram Read/Write in a singleQueue
adapter that owns a private scratch buffer, so multiqueue-by-sharing
devices (user, disabled) no longer race concurrent readers on one
buffer.

This is the tun-interface subset of better-tun-interface-ordering,
extracted at 18dc13b with none of the GSO/GRO offload mechanics and no
udp/sendmmsg changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
JackDoan
2026-07-16 12:18:38 -05:00
parent 6c3972f464
commit 913a37cfee
25 changed files with 930 additions and 501 deletions
+13 -9
View File
@@ -11,6 +11,7 @@ import (
"github.com/gaissmai/bart"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/test"
"github.com/slackhq/nebula/udp"
@@ -30,9 +31,9 @@ func newFakeDevice() *fakeDevice {
// Read blocks until Close like a real tun with no traffic, then reports EOF
// the same way a closed device does
func (d *fakeDevice) Read(p []byte) (int, error) {
func (d *fakeDevice) Read() ([]tio.Packet, error) {
<-d.closedCh
return 0, io.EOF
return nil, io.EOF
}
func (d *fakeDevice) Write(p []byte) (int, error) { return len(p), nil }
@@ -49,10 +50,8 @@ func (d *fakeDevice) Activate() error { return nil }
func (d *fakeDevice) Networks() []netip.Prefix { return nil }
func (d *fakeDevice) Name() string { return "fake" }
func (d *fakeDevice) RoutesFor(netip.Addr) routing.Gateways { return nil }
func (d *fakeDevice) SupportsMultiqueue() bool { return false }
func (d *fakeDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, errors.New("unsupported")
}
func (d *fakeDevice) Queues(int) ([]tio.Queue, error) { return []tio.Queue{d}, nil }
// newReadyControl hand-builds the minimum Control that Main would have
// produced right before Start, including the construction token NewInterface
@@ -78,7 +77,6 @@ func newReadyControl(t *testing.T) (*Control, *fakeDevice, *fakeConn) {
inside: dev,
outside: conn,
writers: []udp.Conn{conn},
readers: make([]io.ReadWriteCloser, 1),
routines: 1,
hostMap: newHostMap(l),
lightHouse: lh,
@@ -155,7 +153,14 @@ type multiqueueDevice struct {
*fakeDevice
}
func (d *multiqueueDevice) SupportsMultiqueue() bool { return true }
// Queues claims multiqueue support but fails to open the second queue,
// exercising the activation error path.
func (d *multiqueueDevice) Queues(n int) ([]tio.Queue, error) {
if n > 1 {
return nil, errors.New("second queue failed to open")
}
return d.fakeDevice.Queues(n)
}
func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
dev := &multiqueueDevice{fakeDevice: newFakeDevice()}
@@ -166,7 +171,6 @@ func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
inside: dev,
outside: conn,
writers: []udp.Conn{conn},
readers: make([]io.ReadWriteCloser, 2),
routines: 2,
l: test.NewLogger(),
}