62265b424a
The client-facing UDP relay listener (cc) in handleUDP was bound to the wildcard address, so replies sent from it used the kernel-selected source IP (the interface's primary IP), which mismatches the BND.ADDR advertised to the client (the TCP control connection's local IP). Per RFC 1928 6, compliant clients drop such replies, breaking SOCKS5 UDP associate under multiple IP aliases where metadata.interface differs from the primary IP. Bind cc to the TCP control connection's local IP so its reply source IP always matches BND.ADDR. The outbound relay socket (pc) already binds the interface IP via Router.Dial -> dialer.ListenUDP(laddr).
173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
package v5
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/limiter"
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/core/observer/stats"
|
|
"github.com/go-gost/gosocks5"
|
|
ctxvalue "github.com/go-gost/x/ctx"
|
|
ictx "github.com/go-gost/x/internal/ctx"
|
|
xnet "github.com/go-gost/x/internal/net"
|
|
"github.com/go-gost/x/internal/net/udp"
|
|
"github.com/go-gost/x/internal/util/socks"
|
|
traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
|
|
metrics "github.com/go-gost/x/metrics/wrapper"
|
|
xstats "github.com/go-gost/x/observer/stats"
|
|
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
|
xrecorder "github.com/go-gost/x/recorder"
|
|
)
|
|
|
|
func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
|
log = log.WithFields(map[string]any{
|
|
"network": network,
|
|
"cmd": network,
|
|
})
|
|
|
|
if !h.md.enableUDP {
|
|
reply := gosocks5.NewReply(gosocks5.NotAllowed, nil)
|
|
log.Trace(reply)
|
|
log.Error("socks5: UDP relay is disabled")
|
|
return reply.Write(conn)
|
|
}
|
|
|
|
// Bind the client-facing relay socket to the TCP control connection's local
|
|
// IP — the address advertised to the client as BND.ADDR — so replies sent
|
|
// from this socket carry the matching source IP. A wildcard bind lets the
|
|
// kernel pick the interface's primary IP, which mismatches BND.ADDR and
|
|
// makes compliant clients drop the replies (RFC 1928 §6). This is what
|
|
// breaks UDP associate under multiple IP aliases (metadata.interface).
|
|
host, _, _ := net.SplitHostPort(conn.LocalAddr().String())
|
|
|
|
lc := xnet.ListenConfig{
|
|
Netns: h.options.Netns,
|
|
}
|
|
|
|
cc, err := lc.ListenPacket(ctx, network, net.JoinHostPort(host, "0"))
|
|
if err != nil {
|
|
log.Error(err)
|
|
reply := gosocks5.NewReply(gosocks5.Failure, nil)
|
|
log.Trace(reply)
|
|
reply.Write(conn)
|
|
return err
|
|
}
|
|
defer cc.Close()
|
|
|
|
log = log.WithFields(map[string]any{
|
|
"src": cc.LocalAddr().String(),
|
|
"bind": cc.LocalAddr().String(),
|
|
})
|
|
ro.SrcAddr = cc.LocalAddr().String()
|
|
|
|
saddr := gosocks5.Addr{}
|
|
saddr.ParseFrom(cc.LocalAddr().String())
|
|
|
|
saddr.Host = host
|
|
if v := net.ParseIP(h.md.publicAddr); v != nil {
|
|
saddr.Host = h.md.publicAddr
|
|
}
|
|
saddr.Type = 0
|
|
reply := gosocks5.NewReply(gosocks5.Succeeded, &saddr)
|
|
log.Trace(reply)
|
|
if err := reply.Write(conn); err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
|
|
log.Debugf("bind on %s OK", cc.LocalAddr())
|
|
|
|
// obtain a udp connection
|
|
var buf bytes.Buffer
|
|
c, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), network, "") // UDP association
|
|
ro.Route = buf.String()
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
defer c.Close()
|
|
|
|
pc, ok := c.(net.PacketConn)
|
|
if !ok {
|
|
err := errors.New("socks5: wrong connection type")
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
pc = metrics.WrapPacketConn(ro.Service, pc)
|
|
|
|
{
|
|
pStats := xstats.Stats{}
|
|
cc = stats_wrapper.WrapPacketConn(cc, &pStats)
|
|
|
|
defer func() {
|
|
ro.InputBytes += pStats.Get(stats.KindInputBytes)
|
|
ro.OutputBytes += pStats.Get(stats.KindOutputBytes)
|
|
}()
|
|
|
|
clientID := ctxvalue.ClientIDFromContext(ctx)
|
|
cc = traffic_wrapper.WrapPacketConn(
|
|
cc,
|
|
h.limiter,
|
|
string(clientID),
|
|
limiter.ServiceOption(h.options.Service),
|
|
limiter.ScopeOption(limiter.ScopeClient),
|
|
limiter.NetworkOption(network),
|
|
limiter.ClientOption(string(clientID)),
|
|
limiter.SrcOption(conn.RemoteAddr().String()),
|
|
)
|
|
if h.options.Observer != nil {
|
|
pstats := h.stats.Stats(string(clientID))
|
|
pstats.Add(stats.KindTotalConns, 1)
|
|
pstats.Add(stats.KindCurrentConns, 1)
|
|
defer pstats.Add(stats.KindCurrentConns, -1)
|
|
cc = stats_wrapper.WrapPacketConn(cc, pstats)
|
|
}
|
|
}
|
|
|
|
r := udp.NewRelay(socks.UDPConn(
|
|
&filteredPacketConn{
|
|
PacketConn: cc,
|
|
filterIP: conn.RemoteAddr().(*net.TCPAddr).IP,
|
|
},
|
|
h.md.udpBufferSize), pc).
|
|
WithService(h.options.Service).
|
|
WithBypass(h.options.Bypass).
|
|
WithBufferSize(h.md.udpBufferSize).
|
|
WithLogger(log)
|
|
|
|
go r.Run(ctx)
|
|
|
|
t := time.Now()
|
|
log.Debugf("%s <-> %s", conn.RemoteAddr(), cc.LocalAddr())
|
|
io.Copy(io.Discard, conn)
|
|
log.WithFields(map[string]any{"duration": time.Since(t)}).
|
|
Debugf("%s >-< %s", conn.RemoteAddr(), cc.LocalAddr())
|
|
|
|
return nil
|
|
}
|
|
|
|
// filteredPacketConn implements SOCKS5 RFC 1928 UDP relay security by filtering
|
|
// incoming packets to only accept those from the client IP that established the TCP control connection.
|
|
type filteredPacketConn struct {
|
|
net.PacketConn
|
|
filterIP net.IP // The expected client IP from the SOCKS5 TCP connection
|
|
}
|
|
|
|
func (f *filteredPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
for {
|
|
n, addr, err = f.PacketConn.ReadFrom(p)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if udpAddr := addr.(*net.UDPAddr); udpAddr.IP.Equal(f.filterIP) {
|
|
return
|
|
}
|
|
}
|
|
}
|