net/tstun,feature/conn25: make ErrInjectionQueueFull detectable as ENOBUFS

Export ErrInjectionQueueFull, returned by TryInjectOutbound when a full
injection queue forced a packet drop, and define it to wrap
syscall.ENOBUFS on platforms that define that errno. Callers can now
test for buffer exhaustion generically with errors.Is(err,
syscall.ENOBUFS) instead of knowing about tstun's error type. plan9 and
tamago have no ENOBUFS in their syscall packages, so there the error is
a plain sentinel; code that must build on those platforms (or on all
platforms, like feature/conn25) can still match the exported sentinel.

Also stop conn25 from logging queue-full TSMP rejections: those drops
are expected under saturation and are already counted as reason="queue_full"
in the outbound usermetric.

Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
James Tucker authored and James Tucker committed 2026-10-13 15:55:43 -07:00
1 parent 3c0508fa28
commit c606e83fb2
6 files changed
+70 -9

No files matched your search

+7 -1
View File
@@ -166,7 +166,13 @@ func (dh *datapathHandler) HandlePacketFromWireGuard(p *packet.Parsed, tun *tstu
Proto: p.IPProto,
Reason: packet.RejectedDueToUnknownAppConnectorTransitIP,
}
if err := tun.TryInjectOutbound(packet.Generate(rj, nil)); err != nil && !errors.Is(err, tstun.ErrClosed) {
// Queue-full drops are expected under saturation and are already
// counted in the outbound usermetric; only log the rest.
// (On platforms that define it, the error also matches
// syscall.ENOBUFS; tstun.ErrInjectionQueueFull is the portable
// form for code that builds where that errno is absent.)
if err := tun.TryInjectOutbound(packet.Generate(rj, nil)); err != nil &&
!errors.Is(err, tstun.ErrClosed) && !errors.Is(err, tstun.ErrInjectionQueueFull) {
dh.debugLogf("error sending TSMP flow rejection packet: %v", err)
}
return filter.Drop
+4 -6
View File
@@ -68,10 +68,6 @@ var (
ErrClosed = errors.New("device closed")
// ErrFiltered is returned when the acted-on packet is rejected by a filter.
ErrFiltered = errors.New("packet dropped by filter")
// errInjectionQueueFull is returned by TryInjectOutbound when the packet
// cannot be queued immediately.
errInjectionQueueFull = errors.New("injection queue full")
)
var (
@@ -548,7 +544,7 @@ func (t *Wrapper) tryInjectOutbound(data []byte) error {
default:
}
t.metrics.outboundDroppedPacketsTotal.Add(usermetric.DropLabels{Reason: usermetric.ReasonQueueFull}, 1)
return errInjectionQueueFull
return ErrInjectionQueueFull
}
// snat does SNAT on p if the destination address requires a different source address.
@@ -1529,7 +1525,9 @@ func (t *Wrapper) InjectOutbound(pkt []byte) error {
// TryInjectOutbound is InjectOutbound, but drops pkt rather than waiting for
// queue space. Use it for best-effort replies generated while processing
// packets, which must not stall behind the injection queue's reader.
// Dropped packets are counted as queue_full outbound drops.
// Dropped packets are counted as queue_full outbound drops, and the returned
// error matches syscall.ENOBUFS via errors.Is on platforms that define it,
// so callers can test for buffer exhaustion generically.
func (t *Wrapper) TryInjectOutbound(pkt []byte) error {
if len(pkt) > MaxPacketSize {
return errPacketTooBig
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9 && !tamago
package tstun
import (
"fmt"
"syscall"
)
// ErrInjectionQueueFull is returned when a full injection queue forced a
// best-effort packet to be dropped rather than queued. It wraps
// syscall.ENOBUFS, the platform's "no buffer space available" errno, so
// callers can test for buffer exhaustion without knowing this package's
// type:
//
// errors.Is(err, syscall.ENOBUFS)
//
// Dropped packets are already counted as reason="queue_full" in the
// tailscaled_outbound_dropped_packets_total usermetric, so callers need not
// log this error.
var ErrInjectionQueueFull = fmt.Errorf("injection queue full: %w", syscall.ENOBUFS)
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build plan9 || tamago
package tstun
import "errors"
// ErrInjectionQueueFull is returned when a full injection queue forced a
// best-effort packet to be dropped rather than queued. On platforms whose
// syscall package has no ENOBUFS, it matches nothing but itself.
var ErrInjectionQueueFull = errors.New("injection queue full")
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9 && !tamago
package tstun
import (
"errors"
"syscall"
"testing"
)
// ErrInjectionQueueFull must match the platform ENOBUFS errno, so callers
// can test for buffer exhaustion without knowing this package's type.
func TestErrInjectionQueueFullMatchesENOBUFS(t *testing.T) {
if !errors.Is(ErrInjectionQueueFull, syscall.ENOBUFS) {
t.Errorf("errors.Is(ErrInjectionQueueFull, syscall.ENOBUFS) = false, want true")
}
}
+2 -2
View File
@@ -256,8 +256,8 @@ func TestTryInjectOutbound(t *testing.T) {
t.Fatal(err)
}
// The queue is full and nothing reads it; this must drop, not block.
if err := w.TryInjectOutbound(data); err != errInjectionQueueFull {
t.Fatalf("TryInjectOutbound on full queue = %v, want errInjectionQueueFull", err)
if err := w.TryInjectOutbound(data); !errors.Is(err, ErrInjectionQueueFull) {
t.Fatalf("TryInjectOutbound on full queue = %v, want ErrInjectionQueueFull", err)
}
m, _ := w.metrics.outboundDroppedPacketsTotal.Get(usermetric.DropLabels{Reason: usermetric.ReasonQueueFull}).(*expvar.Int)
if m == nil || m.Value() != 1 {