Files
x/admission/wrapper/conn.go
T
ginuerzh b7d807b464 fix(wrapper): prevent nil-pointer dereference in WrapUDPConn chain when stats is nil
stats.WrapUDPConn returned nil when pStats was nil (introduced in 7c95d40),
but callers in the kcp and rudp listeners passed the result directly to
admission.WrapUDPConn and limiter.WrapUDPConn, which wrapped a nil PacketConn.
When service.Serve() called conn.LocalAddr(), the auto-generated method
delegated to the nil PacketConn, causing a panic.

Root cause: WrapUDPConn behavior was inconsistent with WrapConn and
WrapPacketConn in the same package, which both return the original connection
instead of nil when the optional config is absent.

Fixes:

1. listener/kcp, listener/rudp — guard stats.WrapUDPConn with Stats != nil
   (consistent with quic and wt listeners which already had this guard).

2. observer/stats/wrapper — when pStats is nil, pass through the original
   connection instead of returning nil (matches WrapConn/WrapPacketConn).

3. admission/wrapper, limiter/traffic/wrapper — add defensive nil guards:
   nil pc → return nil; nil config → pass through original connection.
   All four WrapUDPConn implementations now share the same nil semantics.

4. listener/udp_wrapper_safety_test.go — regression test that verifies the
   full metrics→stats→admission→limiter chain with all optional configs nil
   does not produce a wrapper with a nil internal PacketConn.

Fixes: go-gost/gost#873
2026-06-20 13:52:09 +08:00

342 lines
9.4 KiB
Go

package wrapper
import (
"context"
"errors"
"io"
"net"
"syscall"
"github.com/go-gost/core/admission"
"github.com/go-gost/x/ctx"
xio "github.com/go-gost/x/internal/io"
xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/internal/net/udp"
)
var (
errUnsupport = errors.New("unsupported operation")
)
// serverConn wraps a TCP connection (net.Conn) with per-read admission
// checking. Unlike the listener wrapper — which rejects at accept time —
// this wrapper re-validates the remote address on each Read call.
// This is useful when the admission rules may change during the lifetime
// of a connection, or when the connection was established before
// admission wrappers were applied.
type serverConn struct {
net.Conn
admission admission.Admission
}
// WrapConn wraps a net.Conn with per-read admission control.
// If admission is nil, the original connection is returned unchanged.
//
// When admission is denied on a Read, io.EOF is returned so the caller
// sees a clean end-of-stream rather than a hard error.
func WrapConn(admission admission.Admission, c net.Conn) net.Conn {
if admission == nil {
return c
}
return &serverConn{
Conn: c,
admission: admission,
}
}
// UnwrapConn returns the underlying connection, allowing type assertions
// through wrapper layers.
func (c *serverConn) UnwrapConn() net.Conn {
return c.Conn
}
// Read checks the remote address against the admission controller
// before delegating to the underlying connection. If denied, it returns
// io.EOF to signal end-of-stream.
func (c *serverConn) Read(b []byte) (n int, err error) {
if c.admission != nil &&
!c.admission.Admit(context.Background(), c.RemoteAddr().Network(), c.RemoteAddr().String()) {
err = io.EOF
return
}
return c.Conn.Read(b)
}
// SyscallConn exposes the underlying raw connection for socket-level
// operations (e.g. setting SO_MARK, SO_REUSEPORT). Returns
// errUnsupport if the inner connection does not support this.
func (c *serverConn) SyscallConn() (rc syscall.RawConn, err error) {
if sc, ok := c.Conn.(syscall.Conn); ok {
rc, err = sc.SyscallConn()
return
}
err = errUnsupport
return
}
// CloseRead shuts down the read side of the connection if supported.
func (c *serverConn) CloseRead() error {
if sc, ok := c.Conn.(xio.CloseRead); ok {
return sc.CloseRead()
}
return xio.ErrUnsupported
}
// CloseWrite shuts down the write side of the connection if supported.
func (c *serverConn) CloseWrite() error {
if sc, ok := c.Conn.(xio.CloseWrite); ok {
return sc.CloseWrite()
}
return xio.ErrUnsupported
}
// Context returns the context carried by the underlying connection,
// or nil if the connection does not carry context.
func (c *serverConn) Context() context.Context {
if innerCtx, ok := c.Conn.(ctx.Context); ok {
return innerCtx.Context()
}
return nil
}
// packetConn wraps a net.PacketConn with per-packet admission checking.
// Packets from denied addresses are silently dropped and the read
// loop continues to the next packet.
type packetConn struct {
net.PacketConn
admission admission.Admission
}
// WrapPacketConn wraps a net.PacketConn with per-packet admission control.
// If admission is nil, the original connection is returned unchanged.
//
// Denied packets are silently discarded; ReadFrom loops until it receives
// a packet from an allowed address or encounters an error.
func WrapPacketConn(admission admission.Admission, pc net.PacketConn) net.PacketConn {
if admission == nil {
return pc
}
return &packetConn{
PacketConn: pc,
admission: admission,
}
}
// ReadFrom reads the next packet from an admitted address. Packets from
// denied addresses are discarded and the method retries automatically.
func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
for {
n, addr, err = c.PacketConn.ReadFrom(p)
if err != nil {
return
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
continue
}
return
}
}
// Context returns the context carried by the underlying packet connection.
func (c *packetConn) Context() context.Context {
if innerCtx, ok := c.PacketConn.(ctx.Context); ok {
return innerCtx.Context()
}
return nil
}
// udpConn wraps a packet connection as a UDP-specific connection.
// It provides the same admission filtering as packetConn, plus UDP-
// specific operations (ReadFromUDP, ReadMsgUDP, WriteToUDP, WriteMsgUDP,
// SetDSCP, etc.).
//
// The wrapper delegates to optional interfaces on the underlying
// connection, returning errUnsupport if the capability is not available.
type udpConn struct {
net.PacketConn
admission admission.Admission
}
// WrapUDPConn wraps a net.PacketConn as a udp.Conn with admission control.
// This is used in UDP forwarding paths where the connection is treated
// as a connected UDP socket.
// If pc is nil, nil is returned. If admission is nil, the original connection
// is returned unchanged (no-op).
func WrapUDPConn(adm admission.Admission, pc net.PacketConn) udp.Conn {
if pc == nil {
return nil
}
if adm == nil {
if uc, ok := pc.(udp.Conn); ok {
return uc
}
}
return &udpConn{
PacketConn: pc,
admission: adm,
}
}
// RemoteAddr returns the remote address from the underlying connection
// if it implements the RemoteAddr interface. This is used for "connected"
// UDP sockets that have a fixed peer.
func (c *udpConn) RemoteAddr() net.Addr {
if nc, ok := c.PacketConn.(xnet.RemoteAddr); ok {
return nc.RemoteAddr()
}
return nil
}
// SetReadBuffer sets the socket receive buffer size.
func (c *udpConn) SetReadBuffer(n int) error {
if nc, ok := c.PacketConn.(xnet.SetBuffer); ok {
return nc.SetReadBuffer(n)
}
return errUnsupport
}
// SetWriteBuffer sets the socket send buffer size.
func (c *udpConn) SetWriteBuffer(n int) error {
if nc, ok := c.PacketConn.(xnet.SetBuffer); ok {
return nc.SetWriteBuffer(n)
}
return errUnsupport
}
// Read delegates to the underlying connection's Read method if available
// (for connected UDP sockets that support stream-like reads).
func (c *udpConn) Read(b []byte) (n int, err error) {
if nc, ok := c.PacketConn.(io.Reader); ok {
n, err = nc.Read(b)
return
}
err = errUnsupport
return
}
// ReadFrom reads the next packet from an admitted address, discarding
// packets from denied addresses.
func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
for {
n, addr, err = c.PacketConn.ReadFrom(p)
if err != nil {
return
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
continue
}
return
}
}
// ReadFromUDP reads the next UDP packet from an admitted address.
// Packets from denied addresses are discarded transparently.
func (c *udpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
if nc, ok := c.PacketConn.(udp.ReadUDP); ok {
for {
n, addr, err = nc.ReadFromUDP(b)
if err != nil {
return
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
continue
}
return
}
}
err = errUnsupport
return
}
// ReadMsgUDP reads a UDP packet with out-of-band data from an admitted
// address. Packets from denied addresses are silently skipped.
func (c *udpConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
if nc, ok := c.PacketConn.(udp.ReadUDP); ok {
for {
n, oobn, flags, addr, err = nc.ReadMsgUDP(b, oob)
if err != nil {
return
}
if c.admission != nil &&
!c.admission.Admit(context.Background(), addr.Network(), addr.String()) {
continue
}
return
}
}
err = errUnsupport
return
}
// Write delegates to the underlying connection's Write method if available
// (for connected UDP sockets).
func (c *udpConn) Write(b []byte) (n int, err error) {
if nc, ok := c.PacketConn.(io.Writer); ok {
n, err = nc.Write(b)
return
}
err = errUnsupport
return
}
// WriteTo sends a packet to the specified address via the underlying
// packet connection. No admission check is performed on writes.
func (c *udpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
n, err = c.PacketConn.WriteTo(p, addr)
return
}
// WriteToUDP sends a UDP packet to the specified address if the
// underlying connection supports UDP-specific writes.
func (c *udpConn) WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) {
if nc, ok := c.PacketConn.(udp.WriteUDP); ok {
n, err = nc.WriteToUDP(b, addr)
return
}
err = errUnsupport
return
}
// WriteMsgUDP sends a UDP packet with out-of-band data if supported.
func (c *udpConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
if nc, ok := c.PacketConn.(udp.WriteUDP); ok {
n, oobn, err = nc.WriteMsgUDP(b, oob, addr)
return
}
err = errUnsupport
return
}
// SyscallConn exposes the underlying raw connection for socket-level
// operations if supported.
func (c *udpConn) SyscallConn() (rc syscall.RawConn, err error) {
if nc, ok := c.PacketConn.(xnet.SyscallConn); ok {
return nc.SyscallConn()
}
err = errUnsupport
return
}
// SetDSCP sets the Differentiated Services Code Point on the socket
// for traffic classification/QoS. Silently succeeds if not supported
// (best-effort).
func (c *udpConn) SetDSCP(n int) error {
if nc, ok := c.PacketConn.(xnet.SetDSCP); ok {
return nc.SetDSCP(n)
}
return nil
}
// Context returns the context carried by the underlying packet connection.
func (c *udpConn) Context() context.Context {
if innerCtx, ok := c.PacketConn.(ctx.Context); ok {
return innerCtx.Context()
}
return nil
}