fix(matcher,plugin): case-insensitive matching, port accumulation, nil guards, doc comments

matcher: fix IPv6 normalization via netip.ParseAddr, port-range overwrite
by switching to []*PortRange map, case-insensitive domain/host matching,
glob.MustCompile→Compile to avoid panic on invalid patterns.

plugin: guard nil *Options in NewGRPCConn and NewHTTPClient; add
HTTPClientTransport helper for idle-connection cleanup.

net/*: add doc comments for all exported symbols (no code changes).

Add 30 tests (matcher_test.go 22, plugin_test.go 8) covering all
matcher types, nil safety, case insensitivity, IPv6 normalization,
port ranges, and plugin option composition.
This commit is contained in:
ginuerzh
2026-05-24 18:18:42 +08:00
parent 51455da96f
commit 0e96f602fa
17 changed files with 719 additions and 31 deletions
+11
View File
@@ -8,6 +8,12 @@ import (
"strings"
)
// ParseInterfaceAddr resolves ifceName to a network interface name and its addresses.
// If ifceName is an IP address, it finds the corresponding interface and returns
// a single address with port 0. If ifceName is an interface name, it returns all
// addresses assigned to that interface. The network parameter determines the address
// type (TCPAddr for "tcp"/"tcp4"/"tcp6", UDPAddr for "udp"/"udp4"/"udp6", IPAddr
// otherwise).
func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, err error) {
if ifceName == "" {
addr = append(addr, nil)
@@ -90,6 +96,7 @@ func findInterfaceByIP(ip net.IP) (string, error) {
// e.g. 192.168.1.1:0-65535
type AddrPortRange string
// Addrs expands the port range into individual "host:port" strings.
func (p AddrPortRange) Addrs() (addrs []string) {
// ignore url scheme, e.g. http://, tls://, tcp://.
if strings.Contains(string(p), "://") {
@@ -151,15 +158,18 @@ func (pr *PortRange) Parse(s string) error {
}
}
// Contains reports whether port falls within the range.
func (pr *PortRange) Contains(port int) bool {
return port >= pr.Min && port <= pr.Max
}
// IPRange represents a range of IP addresses.
type IPRange struct {
Min netip.Addr
Max netip.Addr
}
// Parse parses s as a single IP or IP range "min-max" into r.
func (r *IPRange) Parse(s string) error {
minmax := strings.Split(s, "-")
switch len(minmax) {
@@ -190,6 +200,7 @@ func (r *IPRange) Parse(s string) error {
}
}
// Contains reports whether addr falls within the IP range.
func (r *IPRange) Contains(addr netip.Addr) bool {
return !(addr.Less(r.Min) || r.Max.Less(addr))
}
+9
View File
@@ -16,13 +16,18 @@ import (
)
const (
// DefaultTimeout is the default dial timeout.
DefaultTimeout = 10 * time.Second
)
// DefaultNetDialer is the default Dialer used when a nil Dialer is provided.
var (
DefaultNetDialer = &Dialer{}
)
// Dialer is a network dialer with support for interface binding, network
// namespace switching, and socket marking. The zero value is ready to use
// via DefaultNetDialer.
type Dialer struct {
Interface string
Netns string
@@ -31,6 +36,10 @@ type Dialer struct {
Log logger.Logger
}
// Dial connects to addr on the named network. If d is nil, DefaultNetDialer
// is used. When Netns is set, the dial switches into that network namespace
// before creating the connection. When Interface is set, it iterates the
// specified interfaces, trying each address in turn.
func (d *Dialer) Dial(ctx context.Context, network, addr string) (conn net.Conn, err error) {
if d == nil {
d = DefaultNetDialer
+6
View File
@@ -8,6 +8,8 @@ import (
"strings"
)
// GetClientIP extracts the client IP from the request, checking CF-Connecting-IP,
// X-Forwarded-For, and X-Real-Ip headers in that order.
func GetClientIP(req *http.Request) net.IP {
if req == nil {
return nil
@@ -27,6 +29,7 @@ func GetClientIP(req *http.Request) net.IP {
return net.ParseIP(sip)
}
// Body wraps an io.ReadCloser with size-limited recording of the data read.
type Body struct {
r io.ReadCloser
buf bytes.Buffer
@@ -34,6 +37,7 @@ type Body struct {
recordSize int
}
// NewBody creates a Body that reads from r and records up to maxRecordSize bytes.
func NewBody(r io.ReadCloser, maxRecordSize int) *Body {
p := &Body{
r: r,
@@ -63,10 +67,12 @@ func (p *Body) Close() error {
return p.r.Close()
}
// Content returns the recorded content.
func (p *Body) Content() []byte {
return p.buf.Bytes()
}
// Length returns the total number of bytes read from the underlying reader.
func (p *Body) Length() int64 {
return p.length
}
+1
View File
@@ -18,6 +18,7 @@ var prots = map[waterutil.IPProtocol]string{
waterutil.IPv6_ICMP: "IPv6-ICMP",
}
// Protocol returns the string name for the given IP protocol number.
func Protocol(p waterutil.IPProtocol) string {
if v, ok := prots[p]; ok {
return v
+14 -1
View File
@@ -13,33 +13,41 @@ import (
"github.com/vishvananda/netns"
)
// SetBuffer is a connection that supports setting send and receive buffer sizes.
type SetBuffer interface {
SetReadBuffer(bytes int) error
SetWriteBuffer(bytes int) error
}
// SyscallConn is a connection that exposes its raw system file descriptor.
type SyscallConn interface {
SyscallConn() (syscall.RawConn, error)
}
// RemoteAddr is a connection that exposes its remote address.
type RemoteAddr interface {
RemoteAddr() net.Addr
}
// tcpraw.TCPConn
// SetDSCP is a connection that supports setting the DSCP field.
type SetDSCP interface {
SetDSCP(int) error
}
// IsIPv4 reports whether address is an IPv4 address by checking whether
// it starts with ':' or '[' (IPv6) vs a digit (IPv4).
func IsIPv4(address string) bool {
return address != "" && address[0] != ':' && address[0] != '['
}
// ListenConfig extends net.ListenConfig with network namespace support.
type ListenConfig struct {
Netns string
net.ListenConfig
}
// Listen announces on the local network address, switching into the configured
// network namespace first if one is set.
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error) {
if lc.Netns != "" {
runtime.LockOSThread()
@@ -70,6 +78,8 @@ func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (ne
return lc.ListenConfig.Listen(ctx, network, address)
}
// ListenPacket announces on the local network address for packet connections,
// switching into the configured network namespace first if one is set.
func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
if lc.Netns != "" {
runtime.LockOSThread()
@@ -105,6 +115,9 @@ type readWriteConn struct {
w io.Writer
}
// NewReadWriteConn returns a net.Conn that reads from r and writes to w,
// delegating all other operations to c. If c supports CloseRead or CloseWrite,
// those are forwarded; otherwise they return ErrUnsupported.
func NewReadWriteConn(r io.Reader, w io.Writer, c net.Conn) net.Conn {
return &readWriteConn{
Conn: c,
+5 -1
View File
@@ -31,7 +31,11 @@ func WithReadTimeout(d time.Duration) PipeOption {
}
}
// Pipe 在两个连接之间建立双向数据通道
// Pipe establishes a bidirectional data channel between two connections.
// Data read from rw1 is written to rw2, and data read from rw2 is written to
// rw1. Each direction runs in its own goroutine. Pipe returns the first
// non-EOF error encountered, or nil if both directions complete cleanly.
// When ctx is cancelled, both connections are forcefully closed.
func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser, opts ...PipeOption) error {
var options pipeOptions
for _, opt := range opts {
+3
View File
@@ -56,6 +56,9 @@ func (c *serverConn) CloseWrite() error {
return xio.ErrUnsupported
}
// WrapClientConn writes a PROXY protocol header to c using src and dst
// addresses, then returns c for continued use. If ppv is <= 0 or either
// address is nil, the connection is returned unchanged.
func WrapClientConn(ppv int, src, dst net.Addr, c net.Conn) net.Conn {
if ppv <= 0 || c == nil {
return c
+2
View File
@@ -32,6 +32,8 @@ func (ln *listener) Accept() (net.Conn, error) {
return &serverConn{Conn: conn, ctx: innerCtx}, nil
}
// WrapListener wraps ln with a PROXY protocol listener that parses the PROXY
// header on each accepted connection. If ppv <= 0, ln is returned unchanged.
func WrapListener(ppv int, ln net.Listener, readHeaderTimeout time.Duration) net.Listener {
if ppv <= 0 {
return ln
+3
View File
@@ -11,6 +11,9 @@ import (
ctxvalue "github.com/go-gost/x/ctx"
)
// Resolve resolves addr to a concrete IP address. If hosts is non-nil, it is
// consulted first. If r is non-nil and no host mapping matched, the resolver
// is used. If neither is available, addr is returned unchanged.
func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) {
if addr == "" {
return addr, nil
+4
View File
@@ -10,6 +10,8 @@ const (
bufferSize = 64 * 1024
)
// Transport copies data bidirectionally between rw1 and rw2 using two
// goroutines. It returns the first non-EOF error encountered.
func Transport(rw1, rw2 io.ReadWriter) error {
errc := make(chan error, 2)
go func() {
@@ -27,6 +29,8 @@ func Transport(rw1, rw2 io.ReadWriter) error {
return nil
}
// CopyBuffer copies from src to dst using a buffer of bufSize obtained from
// the buffer pool, reducing allocation overhead for repeated copies.
func CopyBuffer(dst io.Writer, src io.Reader, bufSize int) error {
buf := bufpool.Get(bufSize)
defer bufpool.Put(buf)
+4
View File
@@ -7,6 +7,8 @@ import (
xnet "github.com/go-gost/x/internal/net"
)
// Conn is a net.PacketConn that also supports io.Reader, io.Writer, buffer
// sizing, syscall access, and remote address exposure.
type Conn interface {
net.PacketConn
io.Reader
@@ -18,11 +20,13 @@ type Conn interface {
xnet.RemoteAddr
}
// ReadUDP supports reading UDP datagrams with metadata.
type ReadUDP interface {
ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error)
ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)
}
// WriteUDP supports writing UDP datagrams with metadata.
type WriteUDP interface {
WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)
WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error)
+4
View File
@@ -11,6 +11,7 @@ import (
"github.com/go-gost/core/logger"
)
// ListenConfig configures a UDP-to-TCP-stream listener.
type ListenConfig struct {
Addr net.Addr
Backlog int
@@ -29,6 +30,9 @@ type listener struct {
config *ListenConfig
}
// NewListener creates a net.Listener from a net.PacketConn by demultiplexing
// UDP datagrams into per-client net.Conn streams. Idle connections are cleaned
// up according to cfg.TTL.
func NewListener(conn net.PacketConn, cfg *ListenConfig) net.Listener {
if cfg == nil {
cfg = &ListenConfig{}
+7
View File
@@ -13,6 +13,7 @@ const (
defaultBufferSize = 4096
)
// Relay copies UDP datagrams between two net.PacketConns bidirectionally.
type Relay struct {
service string
pc1 net.PacketConn
@@ -22,6 +23,7 @@ type Relay struct {
logger logger.Logger
}
// NewRelay creates a Relay that copies datagrams between pc1 and pc2.
func NewRelay(pc1, pc2 net.PacketConn) *Relay {
return &Relay{
pc1: pc1,
@@ -29,26 +31,31 @@ func NewRelay(pc1, pc2 net.PacketConn) *Relay {
}
}
// WithService sets the service name for bypass matching.
func (r *Relay) WithService(service string) *Relay {
r.service = service
return r
}
// WithBypass sets the bypass matcher to skip certain addresses.
func (r *Relay) WithBypass(bp bypass.Bypass) *Relay {
r.bypass = bp
return r
}
// WithLogger sets the logger.
func (r *Relay) WithLogger(logger logger.Logger) *Relay {
r.logger = logger
return r
}
// WithBufferSize sets the buffer size for copy operations.
func (r *Relay) WithBufferSize(n int) *Relay {
r.bufferSize = n
return r
}
// Run starts the relay. It blocks until an error occurs or ctx is cancelled.
func (r *Relay) Run(ctx context.Context) (err error) {
errc := make(chan error, 2)