mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
fix(tun): survive oversized packets without panic or stream teardown
A 20546-byte spoke datagram hit wbuf[:writeOffset+len(p)] (cap 16400) and panicked the whole hub process. Three parts: - tunDevice.Write rejects packets bigger than its buffers with the new exported ErrPacketTooLarge instead of reslicing past the end. The kernel would have refused the write anyway (tun MTU 1420), so nothing deliverable is lost. - Buffers follow the device MTU (max 16KB): newTunDevice(ifce, mtu), so raising tun.mtu for legitimate jumbo UDP automatically makes such packets writable. The relay/KCP/stream path already passes 64KB. - fromSpoke drops an oversized datagram with a windowed count and keeps the peer stream up; only a real device failure still tears the stream down. Tests: tun write guard, normal passthrough, large-MTU accept, and stream survival with rate-limited warnings; all written first and watched fail.
This commit is contained in:
@@ -13,6 +13,8 @@ import (
|
||||
"github.com/songgao/water/waterutil"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
|
||||
listenertun "github.com/go-gost/x/listener/tun"
|
||||
)
|
||||
|
||||
// ErrNoRoute is what deliver reports when a destination names no peer, or a
|
||||
@@ -175,6 +177,13 @@ type p2pHub struct {
|
||||
dropDst string
|
||||
dropSince time.Time
|
||||
|
||||
// overMu guards the oversized-packet tally that countOversize keeps. Same
|
||||
// shape as the unrouted tally above: the first miss is logged at once, the
|
||||
// rest is summarized once per window, so a persistently oversized sender
|
||||
// stays visible instead of becoming a silent blackhole.
|
||||
overMu sync.Mutex
|
||||
overCount int
|
||||
overSince time.Time
|
||||
}
|
||||
|
||||
func newP2PHub(device io.ReadWriter, router *peerRouter, ownNets []net.IPNet, warn func(string)) *p2pHub {
|
||||
@@ -276,6 +285,32 @@ func (h *p2pHub) countUnrouted(dst net.IP) {
|
||||
time.Since(since).Round(time.Millisecond).String())
|
||||
}
|
||||
|
||||
// countOversize records one dropped oversized packet and reports the running
|
||||
// total at most once per window.
|
||||
func (h *p2pHub) countOversize(size int) {
|
||||
h.overMu.Lock()
|
||||
now := time.Now()
|
||||
if h.overSince.IsZero() {
|
||||
h.overSince = now
|
||||
h.overCount = 1
|
||||
h.overMu.Unlock()
|
||||
h.warnf("oversized packet (%d bytes), discarded", size)
|
||||
return
|
||||
}
|
||||
h.overCount++
|
||||
// The window only restarts on a report, so a continuous flood reports once
|
||||
// per window instead of once per packet.
|
||||
if now.Sub(h.overSince) < unroutedWindow {
|
||||
h.overMu.Unlock()
|
||||
return
|
||||
}
|
||||
n, since := h.overCount, h.overSince
|
||||
h.overSince, h.overCount = now, 0
|
||||
h.overMu.Unlock()
|
||||
h.warnf("oversized packets discarded: %d in %s", n,
|
||||
time.Since(since).Round(time.Millisecond).String())
|
||||
}
|
||||
|
||||
// fromSpoke handles one datagram from one peer: a keepalive is answered on the
|
||||
// peer it arrived from, anything else is the device's.
|
||||
func (h *p2pHub) fromSpoke(s *peerStream, pkt []byte) error {
|
||||
@@ -287,6 +322,13 @@ func (h *p2pHub) fromSpoke(s *peerStream, pkt []byte) error {
|
||||
defer h.wmu.Unlock()
|
||||
|
||||
_, err := h.device.Write(pkt)
|
||||
if errors.Is(err, listenertun.ErrPacketTooLarge) {
|
||||
// One undeliverable datagram, not a dead device: drop it, count it,
|
||||
// and leave the stream up. Returning the error would tear the peer
|
||||
// down over a sender-side anomaly and turn it into a reconnect storm.
|
||||
h.countOversize(len(pkt))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
listenertun "github.com/go-gost/x/listener/tun"
|
||||
)
|
||||
|
||||
// oversizeDevice rejects every write the way a real tun device rejects a
|
||||
// packet bigger than its buffers.
|
||||
type oversizeDevice struct{ nopDevice }
|
||||
|
||||
func (oversizeDevice) Write([]byte) (int, error) {
|
||||
return 0, listenertun.ErrPacketTooLarge
|
||||
}
|
||||
|
||||
// An oversized datagram is dropped and counted, but the peer's stream
|
||||
// survives: killing the stream over one undeliverable packet turns a
|
||||
// sender-side anomaly into a reconnect storm.
|
||||
func TestP2POversizedDatagramKeepsStream(t *testing.T) {
|
||||
var warns []string
|
||||
hub := newP2PHub(oversizeDevice{}, newTestRouter(), nil,
|
||||
func(s string) { warns = append(warns, s) })
|
||||
_, peerEnd := newDatagramPipe(4)
|
||||
s := newPeerStream(context.Background(), "peer-a", peerEnd)
|
||||
|
||||
pkt := make([]byte, 20546)
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := hub.fromSpoke(s, pkt); err != nil {
|
||||
t.Fatalf("fromSpoke #%d: %v, want nil (drop, don't kill the stream)", i, err)
|
||||
}
|
||||
}
|
||||
if len(warns) != 1 {
|
||||
t.Fatalf("warnings = %d, want 1 (first miss now, flood summarized per window)", len(warns))
|
||||
}
|
||||
}
|
||||
+34
-10
@@ -1,6 +1,7 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
@@ -10,6 +11,15 @@ const (
|
||||
maxBufSize = 16 * 1024
|
||||
)
|
||||
|
||||
// ErrPacketTooLarge is returned when a packet does not fit the device write
|
||||
// buffer. Dropping it is the only sane option: the kernel would reject it
|
||||
// anyway, and reslicing wbuf to fit would panic and take down the process.
|
||||
//
|
||||
// It is exported so the p2p hub can tell "one undeliverable datagram" apart
|
||||
// from a dead device: the former is dropped and counted with the stream left
|
||||
// up, the latter still tears the stream down.
|
||||
var ErrPacketTooLarge = errors.New("tun: packet larger than device write buffer")
|
||||
|
||||
type tunDevice struct {
|
||||
dev tun.Device
|
||||
packets int
|
||||
@@ -18,6 +28,10 @@ type tunDevice struct {
|
||||
wbufs [][]byte
|
||||
rbuf []byte
|
||||
wbuf []byte
|
||||
// maxPacket is the largest packet body the buffers hold. It follows the
|
||||
// device MTU (floor maxBufSize): anything bigger is rejected in Write
|
||||
// instead of panicking the reslice below.
|
||||
maxPacket int
|
||||
}
|
||||
|
||||
func (d *tunDevice) Read(p []byte) (n int, err error) {
|
||||
@@ -56,6 +70,9 @@ func (d *tunDevice) Read(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (d *tunDevice) Write(p []byte) (n int, err error) {
|
||||
if len(p) > d.maxPacket {
|
||||
return 0, ErrPacketTooLarge
|
||||
}
|
||||
if writeOffset > 0 {
|
||||
copy(d.wbuf[writeOffset:], p)
|
||||
d.wbufs[0] = d.wbuf[:writeOffset+len(p)]
|
||||
@@ -78,27 +95,34 @@ func (l *tunListener) createTunDevice() (dev io.ReadWriteCloser, name string, er
|
||||
return
|
||||
}
|
||||
|
||||
return newTunDevice(ifce)
|
||||
return newTunDevice(ifce, l.md.config.MTU)
|
||||
}
|
||||
|
||||
// newTunDevice adapts a tun device to the byte-stream conn this package hands
|
||||
// out: the device reads and writes packets in batches, the listener speaks
|
||||
// io.ReadWriteCloser.
|
||||
func newTunDevice(ifce tun.Device) (dev io.ReadWriteCloser, name string, err error) {
|
||||
// io.ReadWriteCloser. mtu sizes the packet buffers so a raised tun MTU (jumbo
|
||||
// UDP) is writable; non-positive mtu keeps the default 16KB buffers.
|
||||
func newTunDevice(ifce tun.Device, mtu int) (dev io.ReadWriteCloser, name string, err error) {
|
||||
batchSize := ifce.BatchSize()
|
||||
|
||||
bufSize := maxBufSize
|
||||
if mtu > bufSize {
|
||||
bufSize = mtu
|
||||
}
|
||||
|
||||
rbufs := make([][]byte, batchSize)
|
||||
for i := 1; i < len(rbufs); i++ {
|
||||
rbufs[i] = make([]byte, maxBufSize)
|
||||
rbufs[i] = make([]byte, bufSize)
|
||||
}
|
||||
|
||||
dev = &tunDevice{
|
||||
dev: ifce,
|
||||
sizes: make([]int, batchSize),
|
||||
rbufs: rbufs,
|
||||
wbufs: make([][]byte, 1),
|
||||
rbuf: make([]byte, maxBufSize+readOffset),
|
||||
wbuf: make([]byte, maxBufSize+writeOffset),
|
||||
dev: ifce,
|
||||
sizes: make([]int, batchSize),
|
||||
rbufs: rbufs,
|
||||
wbufs: make([][]byte, 1),
|
||||
rbuf: make([]byte, bufSize+readOffset),
|
||||
wbuf: make([]byte, bufSize+writeOffset),
|
||||
maxPacket: bufSize,
|
||||
}
|
||||
name, err = ifce.Name()
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ func (l *tunListener) createTun() (dev io.ReadWriteCloser, name string, ip net.I
|
||||
return
|
||||
}
|
||||
|
||||
dev, _, err = newTunDevice(ifce)
|
||||
dev, _, err = newTunDevice(ifce, l.md.config.MTU)
|
||||
if err != nil {
|
||||
ifce.Close()
|
||||
return
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
type fakeTunDevice struct {
|
||||
wrote [][]byte
|
||||
}
|
||||
|
||||
func (f *fakeTunDevice) File() *os.File { return nil }
|
||||
|
||||
func (f *fakeTunDevice) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (f *fakeTunDevice) Write(bufs [][]byte, offset int) (int, error) {
|
||||
f.wrote = append(f.wrote, bufs[0][offset:])
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (f *fakeTunDevice) MTU() (int, error) { return 1500, nil }
|
||||
func (f *fakeTunDevice) Name() (string, error) { return "fake0", nil }
|
||||
func (f *fakeTunDevice) Events() <-chan tun.Event { return nil }
|
||||
func (f *fakeTunDevice) Close() error { return nil }
|
||||
func (f *fakeTunDevice) BatchSize() int { return 1 }
|
||||
|
||||
// A packet that does not fit the device write buffer must be rejected with an
|
||||
// error, never panic the process. Field case: a 20546-byte spoke packet hit
|
||||
// wbuf[:writeOffset+len(p)] with cap 16400 and took down the whole hub.
|
||||
func TestTunDeviceWriteOversizedPacket(t *testing.T) {
|
||||
fake := &fakeTunDevice{}
|
||||
dev, _, err := newTunDevice(fake, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dev.Close()
|
||||
|
||||
big := make([]byte, maxBufSize+1)
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("Write panicked on %d-byte packet: %v", len(big), r)
|
||||
}
|
||||
}()
|
||||
n, err := dev.Write(big)
|
||||
if !errors.Is(err, ErrPacketTooLarge) {
|
||||
t.Fatalf("Write err = %v, want %v", err, ErrPacketTooLarge)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatalf("Write n = %d, want 0", n)
|
||||
}
|
||||
}()
|
||||
if len(fake.wrote) != 0 {
|
||||
t.Fatalf("oversized packet reached the device, want it dropped")
|
||||
}
|
||||
}
|
||||
|
||||
// A device created with a large MTU must accept packets that the default
|
||||
// 16KB buffer would reject. Field case: tun.mtu raised for jumbo UDP.
|
||||
func TestTunDeviceWriteLargeMTUAcceptsJumbo(t *testing.T) {
|
||||
fake := &fakeTunDevice{}
|
||||
dev, _, err := newTunDevice(fake, 32768)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dev.Close()
|
||||
|
||||
pkt := make([]byte, 20546)
|
||||
n, err := dev.Write(pkt)
|
||||
if err != nil {
|
||||
t.Fatalf("Write err = %v, want nil", err)
|
||||
}
|
||||
if n != len(pkt) {
|
||||
t.Fatalf("Write n = %d, want %d", n, len(pkt))
|
||||
}
|
||||
if len(fake.wrote) != 1 {
|
||||
t.Fatalf("device writes = %d, want 1", len(fake.wrote))
|
||||
}
|
||||
}
|
||||
|
||||
// A normal-size packet must still pass through untouched.
|
||||
func TestTunDeviceWriteNormalPacket(t *testing.T) {
|
||||
fake := &fakeTunDevice{}
|
||||
dev, _, err := newTunDevice(fake, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dev.Close()
|
||||
|
||||
pkt := make([]byte, 1500)
|
||||
n, err := dev.Write(pkt)
|
||||
if err != nil {
|
||||
t.Fatalf("Write err = %v, want nil", err)
|
||||
}
|
||||
if n != len(pkt) {
|
||||
t.Fatalf("Write n = %d, want %d", n, len(pkt))
|
||||
}
|
||||
if len(fake.wrote) != 1 {
|
||||
t.Fatalf("device writes = %d, want 1", len(fake.wrote))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user