diff --git a/handler/tun/p2p.go b/handler/tun/p2p.go index b0ef705..7373090 100644 --- a/handler/tun/p2p.go +++ b/handler/tun/p2p.go @@ -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 } diff --git a/handler/tun/p2p_oversize_test.go b/handler/tun/p2p_oversize_test.go new file mode 100644 index 0000000..89768e3 --- /dev/null +++ b/handler/tun/p2p_oversize_test.go @@ -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)) + } +} diff --git a/listener/tun/tun.go b/listener/tun/tun.go index 24afd42..77e1721 100644 --- a/listener/tun/tun.go +++ b/listener/tun/tun.go @@ -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() diff --git a/listener/tun/tun_android.go b/listener/tun/tun_android.go index e74a38d..c76f1e5 100644 --- a/listener/tun/tun_android.go +++ b/listener/tun/tun_android.go @@ -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 diff --git a/listener/tun/tun_test.go b/listener/tun/tun_test.go new file mode 100644 index 0000000..5e9ba1a --- /dev/null +++ b/listener/tun/tun_test.go @@ -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)) + } +}