From b2aa8591b3e11df3ee3c005dddcc8f7bd371fee6 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 18 Nov 2024 17:44:46 -0500 Subject: [PATCH 01/80] refactor: split UDP serving from handling of packets This will allow us to re-use the handling of packets in the Caddy server where serving is handled separately. --- cmd/outline-ss-server/main.go | 4 +- internal/integration_test/integration_test.go | 60 +++--- service/shadowsocks.go | 6 +- service/udp.go | 200 ++++++++++-------- service/udp_test.go | 8 +- 5 files changed, 160 insertions(+), 118 deletions(-) diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index 3a04af0b..23bd143c 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -238,7 +238,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go ssService.HandlePacket(pc) + go service.PacketServe(pc, ssService.HandlePacket) } for _, serviceConfig := range config.Services { @@ -271,7 +271,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go ssService.HandlePacket(pc) + go service.PacketServe(pc, ssService.HandlePacket) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 0994b90f..276eccfc 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -17,6 +17,7 @@ package integration_test import ( "bytes" "context" + "errors" "fmt" "io" "net" @@ -104,6 +105,9 @@ func startUDPEchoServer(t testing.TB) (*net.UDPConn, *sync.WaitGroup) { for { n, clientAddr, err := conn.ReadFromUDP(buf) if err != nil { + if errors.Is(err, net.ErrClosed) { + return + } t.Logf("Failed to read from UDP conn: %v", err) return } @@ -268,16 +272,23 @@ type fakeUDPConnMetrics struct { clientAddr net.Addr accessKey string up, down []udpRecord + mu sync.Mutex } var _ service.UDPConnMetrics = (*fakeUDPConnMetrics)(nil) func (m *fakeUDPConnMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { + m.mu.Lock() + defer m.mu.Unlock() m.up = append(m.up, udpRecord{m.clientAddr, m.accessKey, status, clientProxyBytes, proxyTargetBytes}) } + func (m *fakeUDPConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { + m.mu.Lock() + defer m.mu.Unlock() m.down = append(m.down, udpRecord{m.clientAddr, m.accessKey, status, targetProxyBytes, proxyClientBytes}) } + func (m *fakeUDPConnMetrics) RemoveNatEntry() { // Not tested because it requires waiting for a long timeout. } @@ -315,7 +326,7 @@ func TestUDPEcho(t *testing.T) { proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - proxy.Handle(proxyConn) + service.PacketServe(proxyConn, proxy.Handle) done <- struct{}{} }() @@ -361,29 +372,26 @@ func TestUDPEcho(t *testing.T) { snapshot := cipherList.SnapshotForClientIP(netip.Addr{}) keyID := snapshot[0].Value.(*service.CipherEntry).ID - if len(testMetrics.connMetrics) != 1 { - t.Errorf("Wrong NAT count: %d", len(testMetrics.connMetrics)) - } - if len(testMetrics.connMetrics[0].up) != 1 { - t.Errorf("Wrong number of packets sent: %v", testMetrics.connMetrics[0].up) - } else { - record := testMetrics.connMetrics[0].up[0] - require.Equal(t, conn.LocalAddr(), record.clientAddr, "Bad upstream metrics") - require.Equal(t, keyID, record.accessKey, "Bad upstream metrics") - require.Equal(t, "OK", record.status, "Bad upstream metrics") - require.Greater(t, record.in, record.out, "Bad upstream metrics") - require.Equal(t, int64(N), record.out, "Bad upstream metrics") - } - if len(testMetrics.connMetrics[0].down) != 1 { - t.Errorf("Wrong number of packets received: %v", testMetrics.connMetrics[0].down) - } else { - record := testMetrics.connMetrics[0].down[0] - require.Equal(t, conn.LocalAddr(), record.clientAddr, "Bad downstream metrics") - require.Equal(t, keyID, record.accessKey, "Bad downstream metrics") - require.Equal(t, "OK", record.status, "Bad downstream metrics") - require.Greater(t, record.out, record.in, "Bad downstream metrics") - require.Equal(t, int64(N), record.in, "Bad downstream metrics") - } + require.Lenf(t, testMetrics.connMetrics, 1, "Wrong NAT count") + + testMetrics.connMetrics[0].mu.Lock() + defer testMetrics.connMetrics[0].mu.Unlock() + + require.Lenf(t, testMetrics.connMetrics[0].up, 1, "Wrong number of packets sent") + record := testMetrics.connMetrics[0].up[0] + require.Equal(t, conn.LocalAddr(), record.clientAddr, "Bad upstream metrics") + require.Equal(t, keyID, record.accessKey, "Bad upstream metrics") + require.Equal(t, "OK", record.status, "Bad upstream metrics") + require.Greater(t, record.in, record.out, "Bad upstream metrics") + require.Equal(t, int64(N), record.out, "Bad upstream metrics") + + require.Lenf(t, testMetrics.connMetrics[0].down, 1, "Wrong number of packets received") + record = testMetrics.connMetrics[0].down[0] + require.Equal(t, conn.LocalAddr(), record.clientAddr, "Bad downstream metrics") + require.Equal(t, keyID, record.accessKey, "Bad downstream metrics") + require.Equal(t, "OK", record.status, "Bad downstream metrics") + require.Greater(t, record.out, record.in, "Bad downstream metrics") + require.Equal(t, int64(N), record.in, "Bad downstream metrics") } func BenchmarkTCPThroughput(b *testing.B) { @@ -548,7 +556,7 @@ func BenchmarkUDPEcho(b *testing.B) { proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - proxy.Handle(server) + service.PacketServe(server, proxy.Handle) done <- struct{}{} }() @@ -592,7 +600,7 @@ func BenchmarkUDPManyKeys(b *testing.B) { proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - proxy.Handle(proxyConn) + service.PacketServe(proxyConn, proxy.Handle) done <- struct{}{} }() diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 636fa94e..7b8221d3 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -44,7 +44,7 @@ type ServiceMetrics interface { type Service interface { HandleStream(ctx context.Context, conn transport.StreamConn) - HandlePacket(conn net.PacketConn) + HandlePacket(conn net.Conn, pkt []byte) } // Option is a Shadowsocks service constructor option. @@ -137,8 +137,8 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) } // HandlePacket handles a Shadowsocks packet connection. -func (s *ssService) HandlePacket(conn net.PacketConn) { - s.ph.Handle(conn) +func (s *ssService) HandlePacket(conn net.Conn, pkt []byte) { + s.ph.Handle(conn, pkt) } type ssConnMetrics struct { diff --git a/service/udp.go b/service/udp.go index 94078029..b19c784b 100644 --- a/service/udp.go +++ b/service/udp.go @@ -43,6 +43,12 @@ type UDPMetrics interface { // Max UDP buffer size for the server code. const serverUDPBufferSize = 64 * 1024 +var bufferPool = sync.Pool{ + New: func() interface{} { + return make([]byte, serverUDPBufferSize) + }, +} + // Wrapper for slog.Debug during UDP proxying. func debugUDP(l *slog.Logger, template string, cipherID string, attr slog.Attr) { // This is an optimization to reduce unnecessary allocations due to an interaction @@ -83,7 +89,7 @@ type packetHandler struct { logger *slog.Logger natTimeout time.Duration ciphers CipherList - m UDPMetrics + nm *natmap ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator } @@ -96,11 +102,11 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetr if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } + nm := newNATmap(natTimeout, m, noopLogger()) return &packetHandler{ logger: noopLogger(), - natTimeout: natTimeout, ciphers: cipherList, - m: m, + nm: nm, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, } @@ -108,12 +114,11 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetr // PacketHandler is a running UDP shadowsocks proxy that can be stopped. type PacketHandler interface { + Handle(conn net.Conn, pkt []byte) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) - // Handle returns after clientConn closes and all the sub goroutines return. - Handle(clientConn net.PacketConn) } func (h *packetHandler) SetLogger(l *slog.Logger) { @@ -127,97 +132,126 @@ func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPVali h.targetIPValidator = targetIPValidator } -// Listen on addr for encrypted packets and basically do UDP NAT. -// We take the ciphers as a pointer because it gets replaced on config updates. -func (h *packetHandler) Handle(clientConn net.PacketConn) { - nm := newNATmap(h.natTimeout, h.m, h.logger) - defer nm.Close() - cipherBuf := make([]byte, serverUDPBufferSize) - textBuf := make([]byte, serverUDPBufferSize) +type PacketHandleFunc func(conn net.Conn, pkt []byte) + +// PacketServe listens for packets and calls `handle` to handle them until the connection +// returns [ErrClosed]. +func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { + buffer := bufferPool.Get().([]byte) + defer bufferPool.Put(buffer) for { - clientProxyBytes, clientAddr, err := clientConn.ReadFrom(cipherBuf) - if errors.Is(err, net.ErrClosed) { - break + n, addr, err := clientConn.ReadFrom(buffer) + if err != nil { + if errors.Is(err, net.ErrClosed) { + break + } + slog.Warn("Failed to read from client. Continuing to listen.", "err", err) + continue } + pkt := buffer[:n] - var proxyTargetBytes int - var targetConn *natconn - - connError := func() (connError *onet.ConnectionError) { + func() { defer func() { if r := recover(); r != nil { - slog.Error("Panic in UDP loop: %v. Continuing to listen.", r) + slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) debug.PrintStack() } }() + handle(&wrappedPacketConn{PacketConn: clientConn, raddr: addr}, pkt) + }() + } +} + +type wrappedPacketConn struct { + net.PacketConn + raddr net.Addr +} + +var _ net.Conn = (*wrappedPacketConn)(nil) + +func (pc *wrappedPacketConn) Read(p []byte) (int, error) { + n, _, err := pc.PacketConn.ReadFrom(p) + return n, err +} + +func (pc *wrappedPacketConn) RemoteAddr() net.Addr { + return pc.raddr +} + +func (pc *wrappedPacketConn) Write(b []byte) (n int, err error) { + return pc.PacketConn.WriteTo(b, pc.raddr) +} + +func (h *packetHandler) Handle(clientConn net.Conn, pkt []byte) { + debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", len(pkt))) + + var err error + var proxyTargetBytes int + var targetConn *natconn + + connError := func() (connError *onet.ConnectionError) { + defer slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientConn.RemoteAddr().String())) + + var payload []byte + var tgtUDPAddr *net.UDPAddr + targetConn = h.nm.Get(clientConn.RemoteAddr().String()) + if targetConn == nil { + ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() + var textData []byte + var cryptoKey *shadowsocks.EncryptionKey + buffer := bufferPool.Get().([]byte) + defer bufferPool.Put(buffer) + unpackStart := time.Now() + textData, keyID, cryptoKey, err := findAccessKeyUDP(ip, buffer, pkt, h.ciphers, h.logger) + timeToCipher := time.Since(unpackStart) + h.ssm.AddCipherSearch(err == nil, timeToCipher) - // Error from ReadFrom if err != nil { - return onet.NewConnectionError("ERR_READ", "Failed to read from client", err) + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) } - defer slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientAddr.String())) - debugUDPAddr(h.logger, "Outbound packet.", clientAddr, slog.Int("bytes", clientProxyBytes)) - - cipherData := cipherBuf[:clientProxyBytes] - var payload []byte - var tgtUDPAddr *net.UDPAddr - targetConn = nm.Get(clientAddr.String()) - if targetConn == nil { - ip := clientAddr.(*net.UDPAddr).AddrPort().Addr() - var textData []byte - var cryptoKey *shadowsocks.EncryptionKey - unpackStart := time.Now() - textData, keyID, cryptoKey, err := findAccessKeyUDP(ip, textBuf, cipherData, h.ciphers, h.logger) - timeToCipher := time.Since(unpackStart) - h.ssm.AddCipherSearch(err == nil, timeToCipher) - - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) - } - var onetErr *onet.ConnectionError - if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { - return onetErr - } - - udpConn, err := net.ListenPacket("udp", "") - if err != nil { - return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create UDP socket", err) - } - targetConn = nm.Add(clientAddr, clientConn, cryptoKey, udpConn, keyID) - } else { - unpackStart := time.Now() - textData, err := shadowsocks.Unpack(nil, cipherData, targetConn.cryptoKey) - timeToCipher := time.Since(unpackStart) - h.ssm.AddCipherSearch(err == nil, timeToCipher) - - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) - } + var onetErr *onet.ConnectionError + if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { + return onetErr + } - var onetErr *onet.ConnectionError - if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { - return onetErr - } + udpConn, err := net.ListenPacket("udp", "") + if err != nil { + return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create UDP socket", err) } + targetConn = h.nm.Add(clientConn, udpConn, cryptoKey, keyID) + } else { + unpackStart := time.Now() + textData, err := shadowsocks.Unpack(nil, pkt, targetConn.cryptoKey) + timeToCipher := time.Since(unpackStart) + h.ssm.AddCipherSearch(err == nil, timeToCipher) - debugUDPAddr(h.logger, "Proxy exit.", clientAddr, slog.Any("target", targetConn.LocalAddr())) - proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) } - return nil - }() - status := "OK" - if connError != nil { - slog.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) - status = connError.Status + var onetErr *onet.ConnectionError + if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { + return onetErr + } } - if targetConn != nil { - targetConn.metrics.AddPacketFromClient(status, int64(clientProxyBytes), int64(proxyTargetBytes)) + + debugUDPAddr(h.logger, "Proxy exit.", clientConn.RemoteAddr(), slog.Any("target", targetConn.LocalAddr())) + proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature + if err != nil { + return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) } + return nil + }() + + status := "OK" + if connError != nil { + slog.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + status = connError.Status + } + if targetConn != nil { + targetConn.metrics.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } } @@ -355,14 +389,14 @@ func (m *natmap) del(key string) net.PacketConn { return nil } -func (m *natmap) Add(clientAddr net.Addr, clientConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, targetConn net.PacketConn, keyID string) *natconn { - connMetrics := m.metrics.AddUDPNatEntry(clientAddr, keyID) - entry := m.set(clientAddr.String(), targetConn, cryptoKey, connMetrics) +func (m *natmap) Add(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, keyID string) *natconn { + connMetrics := m.metrics.AddUDPNatEntry(clientConn.RemoteAddr(), keyID) + entry := m.set(clientConn.RemoteAddr().String(), targetConn, cryptoKey, connMetrics) go func() { - timedCopy(clientAddr, clientConn, entry, m.logger) + timedCopy(clientConn, entry, m.logger) connMetrics.RemoveNatEntry() - if pc := m.del(clientAddr.String()); pc != nil { + if pc := m.del(clientConn.RemoteAddr().String()); pc != nil { pc.Close() } }() @@ -388,7 +422,7 @@ func (m *natmap) Close() error { var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) // copy from target to client until read timeout -func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn *natconn, l *slog.Logger) { +func timedCopy(clientConn net.Conn, targetConn *natconn, l *slog.Logger) { // pkt is used for in-place encryption of downstream UDP packets, with the layout // [padding?][salt][address][body][tag][extra] // Padding is only used if the address is IPv4. @@ -421,7 +455,7 @@ func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn *natco return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) } - debugUDPAddr(l, "Got response.", clientAddr, slog.Any("target", raddr)) + debugUDPAddr(l, "Got response.", clientConn.RemoteAddr(), slog.Any("target", raddr)) srcAddr := socks.ParseAddr(raddr.String()) addrStart := bodyStart - len(srcAddr) // `plainTextBuf` concatenates the SOCKS address and body: @@ -442,7 +476,7 @@ func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn *natco if err != nil { return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) } - proxyClientBytes, err = clientConn.WriteTo(buf, clientAddr) + proxyClientBytes, err = clientConn.Write(buf) if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) } diff --git a/service/udp_test.go b/service/udp_test.go index 6f620316..9bf657ce 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -139,7 +139,7 @@ func sendToDiscard(payloads [][]byte, validator onet.TargetIPValidator) *natTest handler.SetTargetIPValidator(validator) done := make(chan struct{}) go func() { - handler.Handle(clientConn) + PacketServe(clientConn, handler.Handle) done <- struct{}{} }() @@ -216,7 +216,7 @@ func setupNAT() (*fakePacketConn, *fakePacketConn, *natconn) { nat := newNATmap(timeout, &natTestMetrics{}, noopLogger()) clientConn := makePacketConn() targetConn := makePacketConn() - nat.Add(&clientAddr, clientConn, natCryptoKey, targetConn, "key id") + nat.Add(&wrappedPacketConn{PacketConn: clientConn, raddr: &clientAddr}, targetConn, natCryptoKey, "key id") entry := nat.Get(clientAddr.String()) return clientConn, targetConn, entry } @@ -481,7 +481,7 @@ func TestUDPEarlyClose(t *testing.T) { } testMetrics := &natTestMetrics{} const testTimeout = 200 * time.Millisecond - s := NewPacketHandler(testTimeout, cipherList, testMetrics, &fakeShadowsocksMetrics{}) + ph := NewPacketHandler(testTimeout, cipherList, testMetrics, &fakeShadowsocksMetrics{}) clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) if err != nil { @@ -489,7 +489,7 @@ func TestUDPEarlyClose(t *testing.T) { } require.Nil(t, clientConn.Close()) // This should return quickly without timing out. - s.Handle(clientConn) + PacketServe(clientConn, ph.Handle) } // Makes sure the UDP listener returns [io.ErrClosed] on reads and writes after Close(). From c0c0e9fc2f09302cdd5d53042ee83d042ac0c254 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 20 Nov 2024 13:50:59 -0500 Subject: [PATCH 02/80] Use a read channel. --- service/udp.go | 25 ++++++++++++++++--------- service/udp_test.go | 4 ++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/service/udp.go b/service/udp.go index b19c784b..14613c39 100644 --- a/service/udp.go +++ b/service/udp.go @@ -150,39 +150,46 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { continue } pkt := buffer[:n] + conn := &wrappedPacketConn{ + PacketConn: clientConn, + readCh: make(chan []byte, 1), + raddr: addr, + } - func() { + func(conn *wrappedPacketConn) { defer func() { if r := recover(); r != nil { slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) debug.PrintStack() } }() - handle(&wrappedPacketConn{PacketConn: clientConn, raddr: addr}, pkt) - }() + handle(conn, pkt) + }(conn) + conn.readCh <- pkt } } type wrappedPacketConn struct { net.PacketConn + readCh chan []byte raddr net.Addr } var _ net.Conn = (*wrappedPacketConn)(nil) func (pc *wrappedPacketConn) Read(p []byte) (int, error) { - n, _, err := pc.PacketConn.ReadFrom(p) - return n, err -} - -func (pc *wrappedPacketConn) RemoteAddr() net.Addr { - return pc.raddr + data := <-pc.readCh + return copy(p, data), nil } func (pc *wrappedPacketConn) Write(b []byte) (n int, err error) { return pc.PacketConn.WriteTo(b, pc.raddr) } +func (pc *wrappedPacketConn) RemoteAddr() net.Addr { + return pc.raddr +} + func (h *packetHandler) Handle(clientConn net.Conn, pkt []byte) { debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", len(pkt))) diff --git a/service/udp_test.go b/service/udp_test.go index 9bf657ce..e933523a 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -169,7 +169,7 @@ func TestIPFilter(t *testing.T) { t.Run("Localhost allowed", func(t *testing.T) { metrics := sendToDiscard(payloads, allowAll) - assert.Equal(t, len(metrics.connMetrics), 1, "Expected 1 NAT entry, not %d", len(metrics.connMetrics)) + assert.Equal(t, 1, len(metrics.connMetrics), "Expected 1 NAT entry, not %d", len(metrics.connMetrics)) }) t.Run("Localhost not allowed", func(t *testing.T) { @@ -188,7 +188,7 @@ func TestUpstreamMetrics(t *testing.T) { metrics := sendToDiscard(payloads, allowAll) - assert.Equal(t, N, len(metrics.connMetrics[0].upstreamPackets), "Expected %d reports, not %v", N, metrics.connMetrics[0].upstreamPackets) + assert.Equal(t, N, len(metrics.connMetrics[0].upstreamPackets), "Expected %d reports, not %d", N, len(metrics.connMetrics[0].upstreamPackets)) for i, report := range metrics.connMetrics[0].upstreamPackets { assert.Equal(t, int64(i+1), report.proxyTargetBytes, "Expected %d payload bytes, not %d", i+1, report.proxyTargetBytes) assert.Greater(t, report.clientProxyBytes, report.proxyTargetBytes, "Expected nonzero input overhead (%d > %d)", report.clientProxyBytes, report.proxyTargetBytes) From 8d95309d59d0a2e3d18a46daa4a2832ba31965e8 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 20 Nov 2024 13:53:37 -0500 Subject: [PATCH 03/80] Rename `wrappedPacketConn` to just `packetConn`. --- service/udp.go | 14 +++++++------- service/udp_test.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/service/udp.go b/service/udp.go index 14613c39..df0dda0d 100644 --- a/service/udp.go +++ b/service/udp.go @@ -150,13 +150,13 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { continue } pkt := buffer[:n] - conn := &wrappedPacketConn{ + conn := &packetConn{ PacketConn: clientConn, readCh: make(chan []byte, 1), raddr: addr, } - func(conn *wrappedPacketConn) { + func(conn *packetConn) { defer func() { if r := recover(); r != nil { slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) @@ -169,24 +169,24 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { } } -type wrappedPacketConn struct { +type packetConn struct { net.PacketConn readCh chan []byte raddr net.Addr } -var _ net.Conn = (*wrappedPacketConn)(nil) +var _ net.Conn = (*packetConn)(nil) -func (pc *wrappedPacketConn) Read(p []byte) (int, error) { +func (pc *packetConn) Read(p []byte) (int, error) { data := <-pc.readCh return copy(p, data), nil } -func (pc *wrappedPacketConn) Write(b []byte) (n int, err error) { +func (pc *packetConn) Write(b []byte) (n int, err error) { return pc.PacketConn.WriteTo(b, pc.raddr) } -func (pc *wrappedPacketConn) RemoteAddr() net.Addr { +func (pc *packetConn) RemoteAddr() net.Addr { return pc.raddr } diff --git a/service/udp_test.go b/service/udp_test.go index e933523a..e081d272 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -216,7 +216,7 @@ func setupNAT() (*fakePacketConn, *fakePacketConn, *natconn) { nat := newNATmap(timeout, &natTestMetrics{}, noopLogger()) clientConn := makePacketConn() targetConn := makePacketConn() - nat.Add(&wrappedPacketConn{PacketConn: clientConn, raddr: &clientAddr}, targetConn, natCryptoKey, "key id") + nat.Add(&packetConn{PacketConn: clientConn, raddr: &clientAddr}, targetConn, natCryptoKey, "key id") entry := nat.Get(clientAddr.String()) return clientConn, targetConn, entry } From 51d5c889abfbc780c2eaf23c4017c35f56f30b3e Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 21 Nov 2024 12:23:12 -0500 Subject: [PATCH 04/80] Update `shadowsocks_handler`. --- caddy/shadowsocks_handler.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index a0c48747..a89a4570 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -29,6 +29,9 @@ import ( const ssModuleName = "layer4.handlers.shadowsocks" +// Max UDP buffer size for the server code. +const serverUDPBufferSize = 64 * 1024 + func init() { caddy.RegisterModule(ModuleRegistration{ ID: ssModuleName, @@ -46,6 +49,7 @@ type ShadowsocksHandler struct { Keys []KeyConfig `json:"keys,omitempty"` service outline.Service + buffer []byte logger *slog.Logger } @@ -107,6 +111,7 @@ func (h *ShadowsocksHandler) Provision(ctx caddy.Context) error { return err } h.service = service + h.buffer = make([]byte, serverUDPBufferSize) return nil } @@ -116,7 +121,12 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.service.HandleStream(cx.Context, conn) case net.PacketConn: - h.service.HandlePacket(conn) + n, err := cx.Read(h.buffer) + if err != nil { + return err + } + pkt := h.buffer[:n] + h.service.HandlePacket(cx, pkt) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } From 422542b7d835c07f0a349786cdc1fddee76cf217 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 22 Nov 2024 15:13:03 -0500 Subject: [PATCH 05/80] Change the `HandlePacket` API to not require the `pkt`. --- caddy/shadowsocks_handler.go | 14 ++------------ service/shadowsocks.go | 6 +++--- service/udp.go | 30 ++++++++++++++++++------------ 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index a89a4570..f9f098c2 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -29,9 +29,6 @@ import ( const ssModuleName = "layer4.handlers.shadowsocks" -// Max UDP buffer size for the server code. -const serverUDPBufferSize = 64 * 1024 - func init() { caddy.RegisterModule(ModuleRegistration{ ID: ssModuleName, @@ -49,7 +46,6 @@ type ShadowsocksHandler struct { Keys []KeyConfig `json:"keys,omitempty"` service outline.Service - buffer []byte logger *slog.Logger } @@ -111,7 +107,6 @@ func (h *ShadowsocksHandler) Provision(ctx caddy.Context) error { return err } h.service = service - h.buffer = make([]byte, serverUDPBufferSize) return nil } @@ -120,13 +115,8 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err switch conn := cx.Conn.(type) { case transport.StreamConn: h.service.HandleStream(cx.Context, conn) - case net.PacketConn: - n, err := cx.Read(h.buffer) - if err != nil { - return err - } - pkt := h.buffer[:n] - h.service.HandlePacket(cx, pkt) + case net.Conn: + h.service.HandlePacket(cx) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 7b8221d3..dffc91c0 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -44,7 +44,7 @@ type ServiceMetrics interface { type Service interface { HandleStream(ctx context.Context, conn transport.StreamConn) - HandlePacket(conn net.Conn, pkt []byte) + HandlePacket(conn net.Conn) } // Option is a Shadowsocks service constructor option. @@ -137,8 +137,8 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) } // HandlePacket handles a Shadowsocks packet connection. -func (s *ssService) HandlePacket(conn net.Conn, pkt []byte) { - s.ph.Handle(conn, pkt) +func (s *ssService) HandlePacket(conn net.Conn) { + s.ph.Handle(conn) } type ssConnMetrics struct { diff --git a/service/udp.go b/service/udp.go index df0dda0d..2fc0d131 100644 --- a/service/udp.go +++ b/service/udp.go @@ -114,7 +114,7 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetr // PacketHandler is a running UDP shadowsocks proxy that can be stopped. type PacketHandler interface { - Handle(conn net.Conn, pkt []byte) + Handle(conn net.Conn) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. @@ -132,7 +132,7 @@ func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPVali h.targetIPValidator = targetIPValidator } -type PacketHandleFunc func(conn net.Conn, pkt []byte) +type PacketHandleFunc func(conn net.Conn) // PacketServe listens for packets and calls `handle` to handle them until the connection // returns [ErrClosed]. @@ -153,17 +153,17 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { conn := &packetConn{ PacketConn: clientConn, readCh: make(chan []byte, 1), - raddr: addr, + raddr: addr, } - func(conn *packetConn) { + go func(conn *packetConn) { defer func() { if r := recover(); r != nil { slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) debug.PrintStack() } }() - handle(conn, pkt) + handle(conn) }(conn) conn.readCh <- pkt } @@ -172,7 +172,7 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { type packetConn struct { net.PacketConn readCh chan []byte - raddr net.Addr + raddr net.Addr } var _ net.Conn = (*packetConn)(nil) @@ -190,10 +190,16 @@ func (pc *packetConn) RemoteAddr() net.Addr { return pc.raddr } -func (h *packetHandler) Handle(clientConn net.Conn, pkt []byte) { - debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", len(pkt))) +func (h *packetHandler) Handle(clientConn net.Conn) { + cipherBuf := bufferPool.Get().([]byte) + defer bufferPool.Put(cipherBuf) + clientProxyBytes, err := clientConn.Read(cipherBuf) + if errors.Is(err, net.ErrClosed) { + return + } + debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) + pkt := cipherBuf[:clientProxyBytes] - var err error var proxyTargetBytes int var targetConn *natconn @@ -207,10 +213,10 @@ func (h *packetHandler) Handle(clientConn net.Conn, pkt []byte) { ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var textData []byte var cryptoKey *shadowsocks.EncryptionKey - buffer := bufferPool.Get().([]byte) - defer bufferPool.Put(buffer) + textBuf := bufferPool.Get().([]byte) + defer bufferPool.Put(textBuf) unpackStart := time.Now() - textData, keyID, cryptoKey, err := findAccessKeyUDP(ip, buffer, pkt, h.ciphers, h.logger) + textData, keyID, cryptoKey, err := findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) timeToCipher := time.Since(unpackStart) h.ssm.AddCipherSearch(err == nil, timeToCipher) From c10176fc3dc29475cd998adf7b7d0f80b5fdffa5 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 25 Nov 2024 14:00:07 -0500 Subject: [PATCH 06/80] Remove the NAT map from the `Handle()` function. Instead, make the connection an association by wrapping it with the client address and pass that to the `Handle()` function. Each connection will call `Handle()` only once. --- go.mod | 38 +++--- go.sum | 36 ++++++ service/udp.go | 334 ++++++++++++++++++++++++------------------------- 3 files changed, 217 insertions(+), 191 deletions(-) diff --git a/go.mod b/go.mod index 0018d9d0..49d3bf36 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/Jigsaw-Code/outline-ss-server require ( - github.com/Jigsaw-Code/outline-sdk v0.0.14 + github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241114201846-57720ede6d07 github.com/go-task/task/v3 v3.34.1 github.com/google/addlicense v1.1.1 github.com/google/go-licenses v1.6.0 @@ -12,8 +12,8 @@ require ( github.com/prometheus/client_golang v1.15.0 github.com/shadowsocks/go-shadowsocks2 v0.1.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.17.0 - golang.org/x/term v0.16.0 + golang.org/x/crypto v0.29.0 + golang.org/x/term v0.26.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -49,7 +49,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/alessio/shellescape v1.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -95,7 +95,7 @@ require ( github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb // indirect @@ -120,7 +120,7 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.11.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -143,7 +143,7 @@ require ( github.com/google/go-github/v50 v50.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/ko v0.13.0 // indirect - github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 // indirect + github.com/google/licenseclassifier v0.0.0-20221004142553-c1ed8fcf4bab // indirect github.com/google/s2a-go v0.1.2 // indirect github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect github.com/google/uuid v1.3.0 // indirect @@ -199,7 +199,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/oschwald/maxminddb-golang v1.10.0 // indirect - github.com/otiai10/copy v1.6.0 // indirect + github.com/otiai10/copy v1.14.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect @@ -214,16 +214,16 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sajari/fuzzy v1.0.0 // indirect github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect - github.com/sergi/go-diff v1.2.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/sigstore/cosign/v2 v2.0.0 // indirect github.com/sigstore/rekor v1.1.1 // indirect github.com/sigstore/sigstore v1.6.3 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/slack-go/slack v0.12.2 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.15.0 // indirect @@ -245,14 +245,14 @@ require ( go.uber.org/automaxprocs v1.5.2 // indirect gocloud.dev v0.29.0 // indirect golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.31.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.16.0 // indirect + golang.org/x/tools v0.27.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.119.0 // indirect google.golang.org/appengine v1.6.7 // indirect @@ -267,10 +267,10 @@ require ( gopkg.in/src-d/go-git.v4 v4.13.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.90.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect mvdan.cc/sh/v3 v3.7.0 // indirect sigs.k8s.io/kind v0.17.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) -go 1.21 +go 1.22.0 diff --git a/go.sum b/go.sum index 110613aa..6651db70 100644 --- a/go.sum +++ b/go.sum @@ -515,6 +515,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Jigsaw-Code/outline-sdk v0.0.14 h1:uJLvIne7YJNolbX7KDacd8gLidrUzRuweBO2APmQEmI= github.com/Jigsaw-Code/outline-sdk v0.0.14/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs= +github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241114201846-57720ede6d07 h1:fHYbTiI3oiGVbfvIVEuVXZ9CnswyEWbUEUzBXEoVrFU= +github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241114201846-57720ede6d07/go.mod h1:CFDKyGZA4zatKE4vMLe8TyQpZCyINOeRFbMAmYHxodw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -537,6 +539,8 @@ github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpz github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -928,6 +932,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1117,6 +1122,8 @@ github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= @@ -1337,6 +1344,9 @@ github.com/google/ko v0.13.0 h1:tWOBpAR2PO0nfGhRQjYI1YbnUnhz0RUvCXgiTjKTlGQ= github.com/google/ko v0.13.0/go.mod h1:0jnH1ruPe43u04aaYxTis3ZFMMuxK4zIsO/jD7S+PAA= github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 h1:TJsAqW6zLRMDTyGmc9TPosfn9OyVlHs8Hrn3pY6ONSY= github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148/go.mod h1:rq9F0RSpNKlrefnf6ZYMHKUnEJBCNzf6AcCXMYBeYvE= +github.com/google/licenseclassifier v0.0.0-20221004142553-c1ed8fcf4bab h1:okY7fFoWybMbxiHkaqStN4mxSrPfYmTZl5Zh32Z5FjY= +github.com/google/licenseclassifier v0.0.0-20221004142553-c1ed8fcf4bab/go.mod h1:jkYIPv59uiw+1MxTWlqQEKebsUDV1DCXQtBBn5lVzf4= +github.com/google/licenseclassifier/v2 v2.0.0-alpha.1/go.mod h1:YAgBGGTeNDMU+WfIgaFvjZe4rudym4f6nIn8ZH5X+VM= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE= github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -1920,6 +1930,8 @@ github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd918 github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= github.com/otiai10/copy v1.6.0 h1:IinKAryFFuPONZ7cm6T6E2QX/vcJwSnlaA5lfoaXIiQ= github.com/otiai10/copy v1.6.0/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= @@ -2070,8 +2082,11 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= github.com/shoenig/test v0.6.0/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0= @@ -2097,6 +2112,7 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= @@ -2132,6 +2148,8 @@ github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -2427,6 +2445,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2479,6 +2499,8 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2571,6 +2593,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2623,6 +2647,8 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2779,6 +2805,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -2792,6 +2820,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2809,6 +2838,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2916,6 +2947,8 @@ golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -3316,6 +3349,7 @@ k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= @@ -3326,6 +3360,8 @@ k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= diff --git a/service/udp.go b/service/udp.go index 2fc0d131..571eee28 100644 --- a/service/udp.go +++ b/service/udp.go @@ -17,6 +17,7 @@ package service import ( "errors" "fmt" + "io" "log/slog" "net" "net/netip" @@ -89,7 +90,7 @@ type packetHandler struct { logger *slog.Logger natTimeout time.Duration ciphers CipherList - nm *natmap + m UDPMetrics ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator } @@ -102,11 +103,11 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetr if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } - nm := newNATmap(natTimeout, m, noopLogger()) return &packetHandler{ logger: noopLogger(), + natTimeout: natTimeout, ciphers: cipherList, - nm: nm, + m: m, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, } @@ -137,10 +138,11 @@ type PacketHandleFunc func(conn net.Conn) // PacketServe listens for packets and calls `handle` to handle them until the connection // returns [ErrClosed]. func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { - buffer := bufferPool.Get().([]byte) - defer bufferPool.Put(buffer) - + nm := newNATmap() + defer nm.Close() for { + buffer := bufferPool.Get().([]byte) + defer bufferPool.Put(buffer) n, addr, err := clientConn.ReadFrom(buffer) if err != nil { if errors.Is(err, net.ErrClosed) { @@ -150,121 +152,130 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { continue } pkt := buffer[:n] - conn := &packetConn{ - PacketConn: clientConn, - readCh: make(chan []byte, 1), - raddr: addr, - } - go func(conn *packetConn) { - defer func() { - if r := recover(); r != nil { - slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) - debug.PrintStack() - } - }() - handle(conn) - }(conn) + conn := nm.Get(addr.String()) + if conn == nil { + conn = &natconn{ + Conn: &packetConnWrapper{PacketConn: clientConn, raddr: addr}, + readCh: make(chan []byte, 1), + } + deleteEntry := nm.Add(addr.String(), conn) + go func(conn *natconn) { + defer func() { + conn.Close() + deleteEntry() + }() + handle(conn) + }(conn) + } conn.readCh <- pkt } } -type packetConn struct { - net.PacketConn +type natconn struct { + net.Conn readCh chan []byte - raddr net.Addr } -var _ net.Conn = (*packetConn)(nil) +var _ net.Conn = (*natconn)(nil) -func (pc *packetConn) Read(p []byte) (int, error) { - data := <-pc.readCh - return copy(p, data), nil -} - -func (pc *packetConn) Write(b []byte) (n int, err error) { - return pc.PacketConn.WriteTo(b, pc.raddr) +func (c *natconn) Read(p []byte) (int, error) { + select { + case pkt := <-c.readCh: + if pkt == nil { + break + } + return copy(p, pkt), nil + case <-time.After(30 * time.Second): + break + } + return 0, io.EOF } -func (pc *packetConn) RemoteAddr() net.Addr { - return pc.raddr +func (c *natconn) Close() error { + close(c.readCh) + c.Conn.Close() + return nil } func (h *packetHandler) Handle(clientConn net.Conn) { - cipherBuf := bufferPool.Get().([]byte) - defer bufferPool.Put(cipherBuf) - clientProxyBytes, err := clientConn.Read(cipherBuf) - if errors.Is(err, net.ErrClosed) { + targetConn, err := newTargetConn(h.natTimeout) + if err != nil { + slog.Error("UDP: failed to create target connection", slog.Any("err", err)) return } - debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) - pkt := cipherBuf[:clientProxyBytes] + defer targetConn.Close() + + var connMetrics UDPConnMetrics + defer func() { + if connMetrics != nil { + connMetrics.RemoveNatEntry() + } + }() + var cryptoKey *shadowsocks.EncryptionKey var proxyTargetBytes int - var targetConn *natconn - - connError := func() (connError *onet.ConnectionError) { - defer slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientConn.RemoteAddr().String())) - - var payload []byte - var tgtUDPAddr *net.UDPAddr - targetConn = h.nm.Get(clientConn.RemoteAddr().String()) - if targetConn == nil { - ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() - var textData []byte - var cryptoKey *shadowsocks.EncryptionKey - textBuf := bufferPool.Get().([]byte) - defer bufferPool.Put(textBuf) + cipherBuf := make([]byte, serverUDPBufferSize) + textBuf := make([]byte, serverUDPBufferSize) + for { + clientProxyBytes, err := clientConn.Read(cipherBuf) + if errors.Is(err, net.ErrClosed) { + return + } + debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) + pkt := cipherBuf[:clientProxyBytes] + + connError := func() *onet.ConnectionError { + defer func() { + if r := recover(); r != nil { + slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) + debug.PrintStack() + } + slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientConn.RemoteAddr().String())) + }() + + var err error unpackStart := time.Now() - textData, keyID, cryptoKey, err := findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) + if cryptoKey == nil { + ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() + var keyID string + var cryptoKey *shadowsocks.EncryptionKey + pkt, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) + wrappedClientConn := shadowsocks.NewPacketConn(clientConn, cryptoKey) + clientConn = &packetConnWrapper{ + PacketConn: wrappedClientConn, + raddr: clientConn.RemoteAddr(), + } + connMetrics = h.m.AddUDPNatEntry(clientConn.RemoteAddr(), keyID) + go timedCopy(clientConn.RemoteAddr(), wrappedClientConn, targetConn, cryptoKey, connMetrics, h.logger) + } timeToCipher := time.Since(unpackStart) h.ssm.AddCipherSearch(err == nil, timeToCipher) - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack packet from client", err) } - var onetErr *onet.ConnectionError - if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { + payload, tgtUDPAddr, onetErr := h.validatePacket(pkt) + if onetErr != nil { return onetErr } - udpConn, err := net.ListenPacket("udp", "") + debugUDPAddr(h.logger, "Proxy exit.", clientConn.RemoteAddr(), slog.Any("target", targetConn.LocalAddr())) + proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature if err != nil { - return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create UDP socket", err) - } - targetConn = h.nm.Add(clientConn, udpConn, cryptoKey, keyID) - } else { - unpackStart := time.Now() - textData, err := shadowsocks.Unpack(nil, pkt, targetConn.cryptoKey) - timeToCipher := time.Since(unpackStart) - h.ssm.AddCipherSearch(err == nil, timeToCipher) - - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) + return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) } + return nil + }() - var onetErr *onet.ConnectionError - if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { - return onetErr - } + status := "OK" + if connError != nil { + h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + status = connError.Status } - - debugUDPAddr(h.logger, "Proxy exit.", clientConn.RemoteAddr(), slog.Any("target", targetConn.LocalAddr())) - proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature - if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + if connMetrics != nil { + connMetrics.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } - return nil - }() - - status := "OK" - if connError != nil { - slog.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) - status = connError.Status - } - if targetConn != nil { - targetConn.metrics.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } } @@ -294,11 +305,9 @@ func isDNS(addr net.Addr) bool { return port == "53" } -type natconn struct { +type targetconn struct { net.PacketConn - cryptoKey *shadowsocks.EncryptionKey - metrics UDPConnMetrics - // NAT timeout to apply for non-DNS packets. + // Connection timeout to apply for non-DNS packets. defaultTimeout time.Duration // Current read deadline of PacketConn. Used to avoid decreasing the // deadline. Initially zero. @@ -308,7 +317,18 @@ type natconn struct { fastClose sync.Once } -func (c *natconn) onWrite(addr net.Addr) { +func newTargetConn(defaultTimeout time.Duration) (net.PacketConn, error) { + pc, err := net.ListenPacket("udp", "") + if err != nil { + return nil, fmt.Errorf("failed to create UDP socket: %v", err) + } + return &targetconn{ + PacketConn: pc, + defaultTimeout: defaultTimeout, + }, nil +} + +func (c *targetconn) onWrite(addr net.Addr) { // Fast close is only allowed if there has been exactly one write, // and it was a DNS query. isDNS := isDNS(addr) @@ -331,7 +351,7 @@ func (c *natconn) onWrite(addr net.Addr) { } } -func (c *natconn) onRead(addr net.Addr) { +func (c *targetconn) onRead(addr net.Addr) { c.fastClose.Do(func() { if isDNS(addr) { // The next ReadFrom() should time out immediately. @@ -340,12 +360,12 @@ func (c *natconn) onRead(addr net.Addr) { }) } -func (c *natconn) WriteTo(buf []byte, dst net.Addr) (int, error) { +func (c *targetconn) WriteTo(buf []byte, dst net.Addr) (int, error) { c.onWrite(dst) return c.PacketConn.WriteTo(buf, dst) } -func (c *natconn) ReadFrom(buf []byte) (int, net.Addr, error) { +func (c *targetconn) ReadFrom(buf []byte) (int, net.Addr, error) { n, addr, err := c.PacketConn.ReadFrom(buf) if err == nil { c.onRead(addr) @@ -357,15 +377,11 @@ func (c *natconn) ReadFrom(buf []byte) (int, net.Addr, error) { type natmap struct { sync.RWMutex keyConn map[string]*natconn - logger *slog.Logger - timeout time.Duration - metrics UDPMetrics } -func newNATmap(timeout time.Duration, sm UDPMetrics, l *slog.Logger) *natmap { - m := &natmap{logger: l, metrics: sm} +func newNATmap() *natmap { + m := &natmap{} m.keyConn = make(map[string]*natconn) - m.timeout = timeout return m } @@ -375,22 +391,15 @@ func (m *natmap) Get(key string) *natconn { return m.keyConn[key] } -func (m *natmap) set(key string, pc net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, connMetrics UDPConnMetrics) *natconn { - entry := &natconn{ - PacketConn: pc, - cryptoKey: cryptoKey, - metrics: connMetrics, - defaultTimeout: m.timeout, - } - +func (m *natmap) set(key string, pc *natconn) { m.Lock() defer m.Unlock() - m.keyConn[key] = entry - return entry + m.keyConn[key] = pc + return } -func (m *natmap) del(key string) net.PacketConn { +func (m *natmap) del(key string) *natconn { m.Lock() defer m.Unlock() @@ -402,18 +411,13 @@ func (m *natmap) del(key string) net.PacketConn { return nil } -func (m *natmap) Add(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, keyID string) *natconn { - connMetrics := m.metrics.AddUDPNatEntry(clientConn.RemoteAddr(), keyID) - entry := m.set(clientConn.RemoteAddr().String(), targetConn, cryptoKey, connMetrics) - - go func() { - timedCopy(clientConn, entry, m.logger) - connMetrics.RemoveNatEntry() - if pc := m.del(clientConn.RemoteAddr().String()); pc != nil { - pc.Close() - } - }() - return entry +// Add adds a new UDP NAT entry to the natmap and returns a closure to delete +// the entry. +func (m *natmap) Add(key string, pc *natconn) func() { + m.set(key, pc) + return func() { + m.del(key) + } } func (m *natmap) Close() error { @@ -430,34 +434,40 @@ func (m *natmap) Close() error { return err } -// Get the maximum length of the shadowsocks address header by parsing -// and serializing an IPv6 address from the example range. -var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) +// packetConnWrapper wraps a [net.PacketConn] and provides a [net.Conn] interface +// with a given remote address. +type packetConnWrapper struct { + net.PacketConn + raddr net.Addr +} -// copy from target to client until read timeout -func timedCopy(clientConn net.Conn, targetConn *natconn, l *slog.Logger) { - // pkt is used for in-place encryption of downstream UDP packets, with the layout - // [padding?][salt][address][body][tag][extra] - // Padding is only used if the address is IPv4. - pkt := make([]byte, serverUDPBufferSize) +var _ net.Conn = (*packetConnWrapper)(nil) - saltSize := targetConn.cryptoKey.SaltSize() - // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). - bodyStart := saltSize + maxAddrLen +// ReadFrom reads data from the connection. +func (pcw *packetConnWrapper) Read(b []byte) (n int, err error) { + n, _, err = pcw.PacketConn.ReadFrom(b) + return +} + +// WriteTo writes data to the connection. +func (pcw *packetConnWrapper) Write(b []byte) (n int, err error) { + return pcw.PacketConn.WriteTo(b, pcw.raddr) +} + +// RemoteAddr returns the remote network address. +func (pcw *packetConnWrapper) RemoteAddr() net.Addr { + return pcw.raddr +} + +// copy from target to client until read timeout +func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPConnMetrics, l *slog.Logger) { + buffer := make([]byte, serverUDPBufferSize) expired := false for { var bodyLen, proxyClientBytes int connError := func() (connError *onet.ConnectionError) { - var ( - raddr net.Addr - err error - ) - // `readBuf` receives the plaintext body in `pkt`: - // [padding?][salt][address][body][tag][unused] - // |-- bodyStart --|[ readBuf ] - readBuf := pkt[bodyStart:] - bodyLen, raddr, err = targetConn.ReadFrom(readBuf) + n, raddr, err := targetConn.ReadFrom(buffer) if err != nil { if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { @@ -467,29 +477,9 @@ func timedCopy(clientConn net.Conn, targetConn *natconn, l *slog.Logger) { } return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) } - - debugUDPAddr(l, "Got response.", clientConn.RemoteAddr(), slog.Any("target", raddr)) - srcAddr := socks.ParseAddr(raddr.String()) - addrStart := bodyStart - len(srcAddr) - // `plainTextBuf` concatenates the SOCKS address and body: - // [padding?][salt][address][body][tag][unused] - // |-- addrStart -|[plaintextBuf ] - plaintextBuf := pkt[addrStart : bodyStart+bodyLen] - copy(plaintextBuf, srcAddr) - - // saltStart is 0 if raddr is IPv6. - saltStart := addrStart - saltSize - // `packBuf` adds space for the salt and tag. - // `buf` shows the space that was used. - // [padding?][salt][address][body][tag][unused] - // [ packBuf ] - // [ buf ] - packBuf := pkt[saltStart:] - buf, err := shadowsocks.Pack(packBuf, plaintextBuf, targetConn.cryptoKey) // Encrypt in-place - if err != nil { - return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) - } - proxyClientBytes, err = clientConn.Write(buf) + payload := buffer[:n] + debugUDPAddr(l, "Got response.", clientAddr, slog.Any("target", raddr)) + proxyClientBytes, err = clientConn.WriteTo(payload, raddr) if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) } @@ -497,13 +487,13 @@ func timedCopy(clientConn net.Conn, targetConn *natconn, l *slog.Logger) { }() status := "OK" if connError != nil { - slog.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + l.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } if expired { break } - targetConn.metrics.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) + m.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) } } From 004c6c33af9487065da4a696e2756ae2e8d38e24 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 26 Nov 2024 11:46:11 -0500 Subject: [PATCH 07/80] Rework the metrics now that the NAT is no longer done by the handler. --- prometheus/metrics.go | 59 +++++++++++++------------------------ service/shadowsocks.go | 10 +++++-- service/udp.go | 66 +++++++++++++++--------------------------- 3 files changed, 50 insertions(+), 85 deletions(-) diff --git a/prometheus/metrics.go b/prometheus/metrics.go index c87277eb..c0cce0ca 100644 --- a/prometheus/metrics.go +++ b/prometheus/metrics.go @@ -252,21 +252,23 @@ type udpConnMetrics struct { var _ service.UDPConnMetrics = (*udpConnMetrics)(nil) -func newUDPConnMetrics(udpServiceMetrics *udpServiceMetrics, tunnelTimeMetrics *tunnelTimeMetrics, accessKey string, clientAddr net.Addr, clientInfo ipinfo.IPInfo) *udpConnMetrics { - udpServiceMetrics.addNatEntry() - ipKey, err := toIPKey(clientAddr, accessKey) - if err == nil { - tunnelTimeMetrics.startConnection(*ipKey) - } +func newUDPConnMetrics(udpServiceMetrics *udpServiceMetrics, tunnelTimeMetrics *tunnelTimeMetrics, clientAddr net.Addr, clientInfo ipinfo.IPInfo) *udpConnMetrics { return &udpConnMetrics{ udpServiceMetrics: udpServiceMetrics, tunnelTimeMetrics: tunnelTimeMetrics, - accessKey: accessKey, clientAddr: clientAddr, clientInfo: clientInfo, } } +func (cm *udpConnMetrics) AddAuthenticated(accessKey string) { + cm.accessKey = accessKey + ipKey, err := toIPKey(cm.clientAddr, accessKey) + if err == nil { + cm.tunnelTimeMetrics.startConnection(*ipKey) + } +} + func (cm *udpConnMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { cm.udpServiceMetrics.addPacketFromClient(status, clientProxyBytes, proxyTargetBytes, cm.accessKey, cm.clientInfo) } @@ -275,12 +277,14 @@ func (cm *udpConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, p cm.udpServiceMetrics.addPacketFromTarget(status, targetProxyBytes, proxyClientBytes, cm.accessKey, cm.clientInfo) } -func (cm *udpConnMetrics) RemoveNatEntry() { - cm.udpServiceMetrics.removeNatEntry() - - ipKey, err := toIPKey(cm.clientAddr, cm.accessKey) - if err == nil { - cm.tunnelTimeMetrics.stopConnection(*ipKey) +func (cm *udpConnMetrics) AddClosed() { + // We only track authenticated connections, so ignore unauthenticated closed connections + // when calculating tunneltime. + if cm.accessKey != "" { + ipKey, err := toIPKey(cm.clientAddr, cm.accessKey) + if err == nil { + cm.tunnelTimeMetrics.stopConnection(*ipKey) + } } } @@ -315,18 +319,6 @@ func newUDPCollector() (*udpServiceMetrics, error) { Name: "packets_from_client_per_location", Help: "Packets received from the client, per location and status", }, []string{"location", "asn", "asorg", "status"}), - addedNatEntries: prometheus.NewCounter( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "nat_entries_added", - Help: "Entries added to the UDP NAT table", - }), - removedNatEntries: prometheus.NewCounter( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "nat_entries_removed", - Help: "Entries removed from the UDP NAT table", - }), }, nil } @@ -334,24 +326,12 @@ func (c *udpServiceMetrics) Describe(ch chan<- *prometheus.Desc) { c.proxyCollector.Describe(ch) c.timeToCipherMs.Describe(ch) c.packetsFromClientPerLocation.Describe(ch) - c.addedNatEntries.Describe(ch) - c.removedNatEntries.Describe(ch) } func (c *udpServiceMetrics) Collect(ch chan<- prometheus.Metric) { c.proxyCollector.Collect(ch) c.timeToCipherMs.Collect(ch) c.packetsFromClientPerLocation.Collect(ch) - c.addedNatEntries.Collect(ch) - c.removedNatEntries.Collect(ch) -} - -func (c *udpServiceMetrics) addNatEntry() { - c.addedNatEntries.Inc() -} - -func (c *udpServiceMetrics) removeNatEntry() { - c.removedNatEntries.Inc() } func (c *udpServiceMetrics) addPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64, accessKey string, clientInfo ipinfo.IPInfo) { @@ -537,9 +517,10 @@ func (m *serviceMetrics) AddOpenTCPConnection(clientConn net.Conn) service.TCPCo return newTCPConnMetrics(m.tcpServiceMetrics, m.tunnelTimeMetrics, clientConn, clientInfo) } -func (m *serviceMetrics) AddUDPNatEntry(clientAddr net.Addr, accessKey string) service.UDPConnMetrics { +func (m *serviceMetrics) AddOpenUDPAssociation(clientConn net.Conn) service.UDPConnMetrics { + clientAddr := clientConn.RemoteAddr() clientInfo := m.getIPInfoFromAddr(clientAddr) - return newUDPConnMetrics(m.udpServiceMetrics, m.tunnelTimeMetrics, accessKey, clientAddr, clientInfo) + return newUDPConnMetrics(m.udpServiceMetrics, m.tunnelTimeMetrics, clientAddr, clientInfo) } func (m *serviceMetrics) AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) { diff --git a/service/shadowsocks.go b/service/shadowsocks.go index dffc91c0..4d9726c7 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -37,7 +37,7 @@ type ShadowsocksConnMetrics interface { } type ServiceMetrics interface { - UDPMetrics + AddOpenUDPAssociation(conn net.Conn) UDPConnMetrics AddOpenTCPConnection(conn net.Conn) TCPConnMetrics AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) } @@ -85,7 +85,7 @@ func NewShadowsocksService(opts ...Option) (Service, error) { ) s.sh.SetLogger(s.logger) - s.ph = NewPacketHandler(s.natTimeout, s.ciphers, s.metrics, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"}) + s.ph = NewPacketHandler(s.natTimeout, s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"}) s.ph.SetLogger(s.logger) return s, nil @@ -138,7 +138,11 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) // HandlePacket handles a Shadowsocks packet connection. func (s *ssService) HandlePacket(conn net.Conn) { - s.ph.Handle(conn) + var connMetrics UDPConnMetrics + if s.metrics != nil { + connMetrics = s.metrics.AddOpenUDPAssociation(conn) + } + s.ph.Handle(conn, connMetrics) } type ssConnMetrics struct { diff --git a/service/udp.go b/service/udp.go index 571eee28..a0bca6ea 100644 --- a/service/udp.go +++ b/service/udp.go @@ -32,13 +32,10 @@ import ( // UDPConnMetrics is used to report metrics on UDP connections. type UDPConnMetrics interface { + AddAuthenticated(accessKey string) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) - RemoveNatEntry() -} - -type UDPMetrics interface { - AddUDPNatEntry(clientAddr net.Addr, accessKey string) UDPConnMetrics + AddClosed() } // Max UDP buffer size for the server code. @@ -90,16 +87,12 @@ type packetHandler struct { logger *slog.Logger natTimeout time.Duration ciphers CipherList - m UDPMetrics ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator } // NewPacketHandler creates a UDPService -func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetrics, ssMetrics ShadowsocksConnMetrics) PacketHandler { - if m == nil { - m = &NoOpUDPMetrics{} - } +func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics ShadowsocksConnMetrics) PacketHandler { if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } @@ -107,7 +100,6 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetr logger: noopLogger(), natTimeout: natTimeout, ciphers: cipherList, - m: m, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, } @@ -115,7 +107,7 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetr // PacketHandler is a running UDP shadowsocks proxy that can be stopped. type PacketHandler interface { - Handle(conn net.Conn) + Handle(conn net.Conn, connMetrics UDPConnMetrics) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. @@ -159,7 +151,7 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { Conn: &packetConnWrapper{PacketConn: clientConn, raddr: addr}, readCh: make(chan []byte, 1), } - deleteEntry := nm.Add(addr.String(), conn) + deleteEntry := nm.Add(addr, conn) go func(conn *natconn) { defer func() { conn.Close() @@ -198,7 +190,12 @@ func (c *natconn) Close() error { return nil } -func (h *packetHandler) Handle(clientConn net.Conn) { +func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) { + if connMetrics == nil { + connMetrics = &NoOpUDPConnMetrics{} + } + defer connMetrics.AddClosed() + targetConn, err := newTargetConn(h.natTimeout) if err != nil { slog.Error("UDP: failed to create target connection", slog.Any("err", err)) @@ -206,13 +203,6 @@ func (h *packetHandler) Handle(clientConn net.Conn) { } defer targetConn.Close() - var connMetrics UDPConnMetrics - defer func() { - if connMetrics != nil { - connMetrics.RemoveNatEntry() - } - }() - var cryptoKey *shadowsocks.EncryptionKey var proxyTargetBytes int cipherBuf := make([]byte, serverUDPBufferSize) @@ -235,22 +225,22 @@ func (h *packetHandler) Handle(clientConn net.Conn) { }() var err error - unpackStart := time.Now() if cryptoKey == nil { ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var keyID string var cryptoKey *shadowsocks.EncryptionKey + unpackStart := time.Now() pkt, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) + timeToCipher := time.Since(unpackStart) + h.ssm.AddCipherSearch(err == nil, timeToCipher) wrappedClientConn := shadowsocks.NewPacketConn(clientConn, cryptoKey) clientConn = &packetConnWrapper{ PacketConn: wrappedClientConn, raddr: clientConn.RemoteAddr(), } - connMetrics = h.m.AddUDPNatEntry(clientConn.RemoteAddr(), keyID) + connMetrics.AddAuthenticated(keyID) go timedCopy(clientConn.RemoteAddr(), wrappedClientConn, targetConn, cryptoKey, connMetrics, h.logger) } - timeToCipher := time.Since(unpackStart) - h.ssm.AddCipherSearch(err == nil, timeToCipher) if err != nil { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack packet from client", err) } @@ -273,9 +263,7 @@ func (h *packetHandler) Handle(clientConn net.Conn) { h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } - if connMetrics != nil { - connMetrics.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) - } + connMetrics.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } } @@ -380,9 +368,7 @@ type natmap struct { } func newNATmap() *natmap { - m := &natmap{} - m.keyConn = make(map[string]*natconn) - return m + return &natmap{keyConn: make(map[string]*natconn)} } func (m *natmap) Get(key string) *natconn { @@ -413,7 +399,8 @@ func (m *natmap) del(key string) *natconn { // Add adds a new UDP NAT entry to the natmap and returns a closure to delete // the entry. -func (m *natmap) Add(key string, pc *natconn) func() { +func (m *natmap) Add(addr net.Addr, pc *natconn) func() { + key := addr.String() m.set(key, pc) return func() { m.del(key) @@ -503,18 +490,11 @@ type NoOpUDPConnMetrics struct{} var _ UDPConnMetrics = (*NoOpUDPConnMetrics)(nil) +func (m *NoOpUDPConnMetrics) AddAuthenticated(accessKey string) {} + func (m *NoOpUDPConnMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { } func (m *NoOpUDPConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { } -func (m *NoOpUDPConnMetrics) RemoveNatEntry() {} - -// NoOpUDPMetrics is a [UDPMetrics] that doesn't do anything. Useful in tests -// or if you don't want to track metrics. -type NoOpUDPMetrics struct{} - -var _ UDPMetrics = (*NoOpUDPMetrics)(nil) - -func (m *NoOpUDPMetrics) AddUDPNatEntry(clientAddr net.Addr, accessKey string) UDPConnMetrics { - return &NoOpUDPConnMetrics{} -} +func (m *NoOpUDPConnMetrics) AddClosed() { +} \ No newline at end of file From a91d55b768b65dd89a607cb12c8fe281d50ca093 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 26 Nov 2024 11:54:56 -0500 Subject: [PATCH 08/80] Move the metrics `AddClosed()` call to after the `timedCopy` returns. --- service/udp.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/service/udp.go b/service/udp.go index a0bca6ea..b41be692 100644 --- a/service/udp.go +++ b/service/udp.go @@ -194,7 +194,6 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) if connMetrics == nil { connMetrics = &NoOpUDPConnMetrics{} } - defer connMetrics.AddClosed() targetConn, err := newTargetConn(h.natTimeout) if err != nil { @@ -239,7 +238,10 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) raddr: clientConn.RemoteAddr(), } connMetrics.AddAuthenticated(keyID) - go timedCopy(clientConn.RemoteAddr(), wrappedClientConn, targetConn, cryptoKey, connMetrics, h.logger) + go func() { + defer connMetrics.AddClosed() + timedCopy(clientConn.RemoteAddr(), wrappedClientConn, targetConn, connMetrics, h.logger) + }() } if err != nil { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack packet from client", err) @@ -447,7 +449,7 @@ func (pcw *packetConnWrapper) RemoteAddr() net.Addr { } // copy from target to client until read timeout -func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPConnMetrics, l *slog.Logger) { +func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn net.PacketConn, m UDPConnMetrics, l *slog.Logger) { buffer := make([]byte, serverUDPBufferSize) expired := false From 1d7200bbf460520f3728bc78bcb86b73be1cf373 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 26 Nov 2024 11:55:15 -0500 Subject: [PATCH 09/80] Remove the explicit `targetConn.Close()` and let it expire on its own. --- service/udp.go | 1 - 1 file changed, 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index b41be692..93d0996a 100644 --- a/service/udp.go +++ b/service/udp.go @@ -200,7 +200,6 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) slog.Error("UDP: failed to create target connection", slog.Any("err", err)) return } - defer targetConn.Close() var cryptoKey *shadowsocks.EncryptionKey var proxyTargetBytes int From 4b8eeae97aa90e3ffea312a890c8dc25e7f9b693 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 27 Nov 2024 12:11:03 -0500 Subject: [PATCH 10/80] Add the NAT metrics back in at the server level. --- cmd/outline-ss-server/main.go | 4 ++-- cmd/outline-ss-server/metrics.go | 32 +++++++++++++++++++++++++++++--- prometheus/metrics.go | 2 -- service/udp.go | 11 ++++++++++- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index 23bd143c..7a008d97 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -238,7 +238,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.HandlePacket) + go service.PacketServe(pc, ssService.HandlePacket, s.serverMetrics) } for _, serviceConfig := range config.Services { @@ -271,7 +271,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.HandlePacket) + go service.PacketServe(pc, ssService.HandlePacket, s.serverMetrics) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/cmd/outline-ss-server/metrics.go b/cmd/outline-ss-server/metrics.go index 32c9b0aa..35ead166 100644 --- a/cmd/outline-ss-server/metrics.go +++ b/cmd/outline-ss-server/metrics.go @@ -17,6 +17,7 @@ package main import ( "time" + "github.com/Jigsaw-Code/outline-ss-server/service" "github.com/prometheus/client_golang/prometheus" ) @@ -25,12 +26,15 @@ var now = time.Now type serverMetrics struct { // NOTE: New metrics need to be added to `newPrometheusServerMetrics()`, `Describe()` and `Collect()`. - buildInfo *prometheus.GaugeVec - accessKeys prometheus.Gauge - ports prometheus.Gauge + buildInfo *prometheus.GaugeVec + accessKeys prometheus.Gauge + ports prometheus.Gauge + addedNatEntries prometheus.Counter + removedNatEntries prometheus.Counter } var _ prometheus.Collector = (*serverMetrics)(nil) +var _ service.NATMetrics = (*serverMetrics)(nil) // newPrometheusServerMetrics constructs a Prometheus metrics collector for server // related metrics. @@ -48,6 +52,16 @@ func newPrometheusServerMetrics() *serverMetrics { Name: "ports", Help: "Count of open ports", }), + addedNatEntries: prometheus.NewCounter(prometheus.CounterOpts{ + Subsystem: "udp", + Name: "nat_entries_added", + Help: "Entries added to the UDP NAT table", + }), + removedNatEntries: prometheus.NewCounter(prometheus.CounterOpts{ + Subsystem: "udp", + Name: "nat_entries_removed", + Help: "Entries removed from the UDP NAT table", + }), } } @@ -55,12 +69,16 @@ func (m *serverMetrics) Describe(ch chan<- *prometheus.Desc) { m.buildInfo.Describe(ch) m.accessKeys.Describe(ch) m.ports.Describe(ch) + m.addedNatEntries.Describe(ch) + m.removedNatEntries.Describe(ch) } func (m *serverMetrics) Collect(ch chan<- prometheus.Metric) { m.buildInfo.Collect(ch) m.accessKeys.Collect(ch) m.ports.Collect(ch) + m.addedNatEntries.Collect(ch) + m.removedNatEntries.Collect(ch) } func (m *serverMetrics) SetVersion(version string) { @@ -71,3 +89,11 @@ func (m *serverMetrics) SetNumAccessKeys(numKeys int, ports int) { m.accessKeys.Set(float64(numKeys)) m.ports.Set(float64(ports)) } + +func (m *serverMetrics) AddNATEntry() { + m.addedNatEntries.Inc() +} + +func (m *serverMetrics) RemoveNATEntry() { + m.removedNatEntries.Inc() +} diff --git a/prometheus/metrics.go b/prometheus/metrics.go index c0cce0ca..4aed1331 100644 --- a/prometheus/metrics.go +++ b/prometheus/metrics.go @@ -292,8 +292,6 @@ type udpServiceMetrics struct { proxyCollector *proxyCollector // NOTE: New metrics need to be added to `newUDPCollector()`, `Describe()` and `Collect()`. packetsFromClientPerLocation *prometheus.CounterVec - addedNatEntries prometheus.Counter - removedNatEntries prometheus.Counter timeToCipherMs prometheus.ObserverVec } diff --git a/service/udp.go b/service/udp.go index 93d0996a..bf197632 100644 --- a/service/udp.go +++ b/service/udp.go @@ -30,6 +30,12 @@ import ( "github.com/shadowsocks/go-shadowsocks2/socks" ) +// NATMetrics is used to report NAT related metrics. +type NATMetrics interface { + AddNATEntry() + RemoveNATEntry() +} + // UDPConnMetrics is used to report metrics on UDP connections. type UDPConnMetrics interface { AddAuthenticated(accessKey string) @@ -129,7 +135,7 @@ type PacketHandleFunc func(conn net.Conn) // PacketServe listens for packets and calls `handle` to handle them until the connection // returns [ErrClosed]. -func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { +func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc, metrics NATMetrics) { nm := newNATmap() defer nm.Close() for { @@ -145,17 +151,20 @@ func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc) { } pkt := buffer[:n] + // TODO: Include server address in the NAT key as well. conn := nm.Get(addr.String()) if conn == nil { conn = &natconn{ Conn: &packetConnWrapper{PacketConn: clientConn, raddr: addr}, readCh: make(chan []byte, 1), } + metrics.AddNATEntry() deleteEntry := nm.Add(addr, conn) go func(conn *natconn) { defer func() { conn.Close() deleteEntry() + metrics.RemoveNATEntry() }() handle(conn) }(conn) From 1e89e8538791c21a090960dd38063bef9eded84c Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 27 Nov 2024 12:12:59 -0500 Subject: [PATCH 11/80] Use buffer pool on `packetHandler` instead of global. --- service/udp.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/service/udp.go b/service/udp.go index bf197632..22d329f3 100644 --- a/service/udp.go +++ b/service/udp.go @@ -47,12 +47,6 @@ type UDPConnMetrics interface { // Max UDP buffer size for the server code. const serverUDPBufferSize = 64 * 1024 -var bufferPool = sync.Pool{ - New: func() interface{} { - return make([]byte, serverUDPBufferSize) - }, -} - // Wrapper for slog.Debug during UDP proxying. func debugUDP(l *slog.Logger, template string, cipherID string, attr slog.Attr) { // This is an optimization to reduce unnecessary allocations due to an interaction @@ -91,6 +85,7 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis type packetHandler struct { logger *slog.Logger + bufferPool sync.Pool natTimeout time.Duration ciphers CipherList ssm ShadowsocksConnMetrics @@ -102,8 +97,14 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } + bufferPool := sync.Pool{ + New: func() interface{} { + return make([]byte, serverUDPBufferSize) + }, + } return &packetHandler{ logger: noopLogger(), + bufferPool: bufferPool, natTimeout: natTimeout, ciphers: cipherList, ssm: ssMetrics, @@ -138,9 +139,8 @@ type PacketHandleFunc func(conn net.Conn) func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc, metrics NATMetrics) { nm := newNATmap() defer nm.Close() + buffer := make([]byte, serverUDPBufferSize) for { - buffer := bufferPool.Get().([]byte) - defer bufferPool.Put(buffer) n, addr, err := clientConn.ReadFrom(buffer) if err != nil { if errors.Is(err, net.ErrClosed) { @@ -210,10 +210,15 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) return } + cipherBuf := h.bufferPool.Get().([]byte) + textBuf := h.bufferPool.Get().([]byte) + defer func() { + h.bufferPool.Put(cipherBuf) + h.bufferPool.Put(textBuf) + }() + var cryptoKey *shadowsocks.EncryptionKey var proxyTargetBytes int - cipherBuf := make([]byte, serverUDPBufferSize) - textBuf := make([]byte, serverUDPBufferSize) for { clientProxyBytes, err := clientConn.Read(cipherBuf) if errors.Is(err, net.ErrClosed) { From b4e7dbb728b1f2be11c0e650d1b8cba69842e745 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 27 Nov 2024 12:13:53 -0500 Subject: [PATCH 12/80] Revert wrapping the `clientConn` with shadowsocks encryption/decryption. --- go.mod | 38 +++++++++++----------- go.sum | 36 --------------------- service/udp.go | 86 +++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 86 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index 49d3bf36..0018d9d0 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/Jigsaw-Code/outline-ss-server require ( - github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241114201846-57720ede6d07 + github.com/Jigsaw-Code/outline-sdk v0.0.14 github.com/go-task/task/v3 v3.34.1 github.com/google/addlicense v1.1.1 github.com/google/go-licenses v1.6.0 @@ -12,8 +12,8 @@ require ( github.com/prometheus/client_golang v1.15.0 github.com/shadowsocks/go-shadowsocks2 v0.1.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.29.0 - golang.org/x/term v0.26.0 + golang.org/x/crypto v0.17.0 + golang.org/x/term v0.16.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -49,7 +49,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/alessio/shellescape v1.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -95,7 +95,7 @@ require ( github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb // indirect @@ -120,7 +120,7 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.11.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.2.3 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -143,7 +143,7 @@ require ( github.com/google/go-github/v50 v50.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/ko v0.13.0 // indirect - github.com/google/licenseclassifier v0.0.0-20221004142553-c1ed8fcf4bab // indirect + github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 // indirect github.com/google/s2a-go v0.1.2 // indirect github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect github.com/google/uuid v1.3.0 // indirect @@ -199,7 +199,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/oschwald/maxminddb-golang v1.10.0 // indirect - github.com/otiai10/copy v1.14.0 // indirect + github.com/otiai10/copy v1.6.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect @@ -214,16 +214,16 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sajari/fuzzy v1.0.0 // indirect github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect - github.com/sergi/go-diff v1.3.1 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/sigstore/cosign/v2 v2.0.0 // indirect github.com/sigstore/rekor v1.1.1 // indirect github.com/sigstore/sigstore v1.6.3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/slack-go/slack v0.12.2 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.15.0 // indirect @@ -245,14 +245,14 @@ require ( go.uber.org/automaxprocs v1.5.2 // indirect gocloud.dev v0.29.0 // indirect golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.31.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.27.0 // indirect + golang.org/x/tools v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.119.0 // indirect google.golang.org/appengine v1.6.7 // indirect @@ -267,10 +267,10 @@ require ( gopkg.in/src-d/go-git.v4 v4.13.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/klog/v2 v2.90.0 // indirect mvdan.cc/sh/v3 v3.7.0 // indirect sigs.k8s.io/kind v0.17.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) -go 1.22.0 +go 1.21 diff --git a/go.sum b/go.sum index 6651db70..110613aa 100644 --- a/go.sum +++ b/go.sum @@ -515,8 +515,6 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Jigsaw-Code/outline-sdk v0.0.14 h1:uJLvIne7YJNolbX7KDacd8gLidrUzRuweBO2APmQEmI= github.com/Jigsaw-Code/outline-sdk v0.0.14/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs= -github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241114201846-57720ede6d07 h1:fHYbTiI3oiGVbfvIVEuVXZ9CnswyEWbUEUzBXEoVrFU= -github.com/Jigsaw-Code/outline-sdk v0.0.18-0.20241114201846-57720ede6d07/go.mod h1:CFDKyGZA4zatKE4vMLe8TyQpZCyINOeRFbMAmYHxodw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -539,8 +537,6 @@ github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpz github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -932,7 +928,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1122,8 +1117,6 @@ github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= @@ -1344,9 +1337,6 @@ github.com/google/ko v0.13.0 h1:tWOBpAR2PO0nfGhRQjYI1YbnUnhz0RUvCXgiTjKTlGQ= github.com/google/ko v0.13.0/go.mod h1:0jnH1ruPe43u04aaYxTis3ZFMMuxK4zIsO/jD7S+PAA= github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 h1:TJsAqW6zLRMDTyGmc9TPosfn9OyVlHs8Hrn3pY6ONSY= github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148/go.mod h1:rq9F0RSpNKlrefnf6ZYMHKUnEJBCNzf6AcCXMYBeYvE= -github.com/google/licenseclassifier v0.0.0-20221004142553-c1ed8fcf4bab h1:okY7fFoWybMbxiHkaqStN4mxSrPfYmTZl5Zh32Z5FjY= -github.com/google/licenseclassifier v0.0.0-20221004142553-c1ed8fcf4bab/go.mod h1:jkYIPv59uiw+1MxTWlqQEKebsUDV1DCXQtBBn5lVzf4= -github.com/google/licenseclassifier/v2 v2.0.0-alpha.1/go.mod h1:YAgBGGTeNDMU+WfIgaFvjZe4rudym4f6nIn8ZH5X+VM= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE= github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -1930,8 +1920,6 @@ github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd918 github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= github.com/otiai10/copy v1.6.0 h1:IinKAryFFuPONZ7cm6T6E2QX/vcJwSnlaA5lfoaXIiQ= github.com/otiai10/copy v1.6.0/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E= -github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= -github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= @@ -2082,11 +2070,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= github.com/shoenig/test v0.6.0/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0= @@ -2112,7 +2097,6 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= @@ -2148,8 +2132,6 @@ github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -2445,8 +2427,6 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2499,8 +2479,6 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2593,8 +2571,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2647,8 +2623,6 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2805,8 +2779,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -2820,7 +2792,6 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2838,8 +2809,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2947,8 +2916,6 @@ golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -3349,7 +3316,6 @@ k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= @@ -3360,8 +3326,6 @@ k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= diff --git a/service/udp.go b/service/udp.go index 22d329f3..3b590d0e 100644 --- a/service/udp.go +++ b/service/udp.go @@ -225,7 +225,7 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) return } debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) - pkt := cipherBuf[:clientProxyBytes] + cipherData := cipherBuf[:clientProxyBytes] connError := func() *onet.ConnectionError { defer func() { @@ -236,31 +236,40 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientConn.RemoteAddr().String())) }() + var textData []byte var err error + if cryptoKey == nil { ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var keyID string var cryptoKey *shadowsocks.EncryptionKey unpackStart := time.Now() - pkt, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) + textData, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, cipherData, h.ciphers, h.logger) timeToCipher := time.Since(unpackStart) h.ssm.AddCipherSearch(err == nil, timeToCipher) - wrappedClientConn := shadowsocks.NewPacketConn(clientConn, cryptoKey) - clientConn = &packetConnWrapper{ - PacketConn: wrappedClientConn, - raddr: clientConn.RemoteAddr(), + + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) } + connMetrics.AddAuthenticated(keyID) go func() { defer connMetrics.AddClosed() - timedCopy(clientConn.RemoteAddr(), wrappedClientConn, targetConn, connMetrics, h.logger) + timedCopy(clientConn, targetConn, cryptoKey, connMetrics, h.logger) }() - } - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack packet from client", err) + + } else { + unpackStart := time.Now() + textData, err = shadowsocks.Unpack(nil, cipherData, cryptoKey) + timeToCipher := time.Since(unpackStart) + h.ssm.AddCipherSearch(err == nil, timeToCipher) + + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) + } } - payload, tgtUDPAddr, onetErr := h.validatePacket(pkt) + payload, tgtUDPAddr, onetErr := h.validatePacket(textData) if onetErr != nil { return onetErr } @@ -278,7 +287,7 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } - connMetrics.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) + connMetrics.AddPacketFromClient(status, int64(clientProxyBytes), int64(proxyTargetBytes)) } } @@ -461,15 +470,34 @@ func (pcw *packetConnWrapper) RemoteAddr() net.Addr { return pcw.raddr } +// Get the maximum length of the shadowsocks address header by parsing +// and serializing an IPv6 address from the example range. +var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) + // copy from target to client until read timeout -func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn net.PacketConn, m UDPConnMetrics, l *slog.Logger) { - buffer := make([]byte, serverUDPBufferSize) +func timedCopy(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPConnMetrics, l *slog.Logger) { + // pkt is used for in-place encryption of downstream UDP packets, with the layout + // [padding?][salt][address][body][tag][extra] + // Padding is only used if the address is IPv4. + pkt := make([]byte, serverUDPBufferSize) + + saltSize := cryptoKey.SaltSize() + // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). + bodyStart := saltSize + maxAddrLen expired := false for { var bodyLen, proxyClientBytes int connError := func() (connError *onet.ConnectionError) { - n, raddr, err := targetConn.ReadFrom(buffer) + var ( + raddr net.Addr + err error + ) + // `readBuf` receives the plaintext body in `pkt`: + // [padding?][salt][address][body][tag][unused] + // |-- bodyStart --|[ readBuf ] + readBuf := pkt[bodyStart:] + bodyLen, raddr, err = targetConn.ReadFrom(readBuf) if err != nil { if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { @@ -479,9 +507,29 @@ func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn net.Pa } return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) } - payload := buffer[:n] - debugUDPAddr(l, "Got response.", clientAddr, slog.Any("target", raddr)) - proxyClientBytes, err = clientConn.WriteTo(payload, raddr) + + debugUDPAddr(l, "Got response.", clientConn.RemoteAddr(), slog.Any("target", raddr)) + srcAddr := socks.ParseAddr(raddr.String()) + addrStart := bodyStart - len(srcAddr) + // `plainTextBuf` concatenates the SOCKS address and body: + // [padding?][salt][address][body][tag][unused] + // |-- addrStart -|[plaintextBuf ] + plaintextBuf := pkt[addrStart : bodyStart+bodyLen] + copy(plaintextBuf, srcAddr) + + // saltStart is 0 if raddr is IPv6. + saltStart := addrStart - saltSize + // `packBuf` adds space for the salt and tag. + // `buf` shows the space that was used. + // [padding?][salt][address][body][tag][unused] + // [ packBuf ] + // [ buf ] + packBuf := pkt[saltStart:] + buf, err := shadowsocks.Pack(packBuf, plaintextBuf, cryptoKey) // Encrypt in-place + if err != nil { + return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) + } + proxyClientBytes, err = clientConn.Write(buf) if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) } @@ -512,4 +560,4 @@ func (m *NoOpUDPConnMetrics) AddPacketFromClient(status string, clientProxyBytes func (m *NoOpUDPConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { } func (m *NoOpUDPConnMetrics) AddClosed() { -} \ No newline at end of file +} From 63c2cff43b1a3173292aba7971644c8ae7b2fe3e Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 27 Nov 2024 12:23:58 -0500 Subject: [PATCH 13/80] Rename `packet` to `association`. --- caddy/shadowsocks_handler.go | 2 +- cmd/outline-ss-server/main.go | 4 +- internal/integration_test/integration_test.go | 16 ++--- prometheus/metrics.go | 8 +-- service/shadowsocks.go | 26 ++++---- service/tcp.go | 2 + service/udp.go | 62 ++++++++++--------- 7 files changed, 62 insertions(+), 58 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index f9f098c2..8191e327 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -116,7 +116,7 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.service.HandleStream(cx.Context, conn) case net.Conn: - h.service.HandlePacket(cx) + h.service.HandleAssociation(cx) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index 7a008d97..35ab2a93 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -238,7 +238,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.HandlePacket, s.serverMetrics) + go service.PacketServe(pc, ssService.HandleAssociation, s.serverMetrics) } for _, serviceConfig := range config.Services { @@ -271,7 +271,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.HandlePacket, s.serverMetrics) + go service.PacketServe(pc, ssService.HandleAssociation, s.serverMetrics) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 276eccfc..eef18fcc 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -268,40 +268,40 @@ type udpRecord struct { in, out int64 } -type fakeUDPConnMetrics struct { +type fakeUDPAssocationMetrics struct { clientAddr net.Addr accessKey string up, down []udpRecord mu sync.Mutex } -var _ service.UDPConnMetrics = (*fakeUDPConnMetrics)(nil) +var _ service.UDPAssocationMetrics = (*fakeUDPAssocationMetrics)(nil) -func (m *fakeUDPConnMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { +func (m *fakeUDPAssocationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { m.mu.Lock() defer m.mu.Unlock() m.up = append(m.up, udpRecord{m.clientAddr, m.accessKey, status, clientProxyBytes, proxyTargetBytes}) } -func (m *fakeUDPConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { +func (m *fakeUDPAssocationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { m.mu.Lock() defer m.mu.Unlock() m.down = append(m.down, udpRecord{m.clientAddr, m.accessKey, status, targetProxyBytes, proxyClientBytes}) } -func (m *fakeUDPConnMetrics) RemoveNatEntry() { +func (m *fakeUDPAssocationMetrics) RemoveNatEntry() { // Not tested because it requires waiting for a long timeout. } // Fake metrics implementation for UDP type fakeUDPMetrics struct { - connMetrics []fakeUDPConnMetrics + connMetrics []fakeUDPAssocationMetrics } var _ service.UDPMetrics = (*fakeUDPMetrics)(nil) -func (m *fakeUDPMetrics) AddUDPNatEntry(clientAddr net.Addr, accessKey string) service.UDPConnMetrics { - cm := fakeUDPConnMetrics{ +func (m *fakeUDPMetrics) AddUDPNatEntry(clientAddr net.Addr, accessKey string) service.UDPAssocationMetrics { + cm := fakeUDPAssocationMetrics{ clientAddr: clientAddr, accessKey: accessKey, } diff --git a/prometheus/metrics.go b/prometheus/metrics.go index 4aed1331..036b74bc 100644 --- a/prometheus/metrics.go +++ b/prometheus/metrics.go @@ -250,9 +250,9 @@ type udpConnMetrics struct { accessKey string } -var _ service.UDPConnMetrics = (*udpConnMetrics)(nil) +var _ service.UDPAssocationMetrics = (*udpConnMetrics)(nil) -func newUDPConnMetrics(udpServiceMetrics *udpServiceMetrics, tunnelTimeMetrics *tunnelTimeMetrics, clientAddr net.Addr, clientInfo ipinfo.IPInfo) *udpConnMetrics { +func newUDPAssocationMetrics(udpServiceMetrics *udpServiceMetrics, tunnelTimeMetrics *tunnelTimeMetrics, clientAddr net.Addr, clientInfo ipinfo.IPInfo) *udpConnMetrics { return &udpConnMetrics{ udpServiceMetrics: udpServiceMetrics, tunnelTimeMetrics: tunnelTimeMetrics, @@ -515,10 +515,10 @@ func (m *serviceMetrics) AddOpenTCPConnection(clientConn net.Conn) service.TCPCo return newTCPConnMetrics(m.tcpServiceMetrics, m.tunnelTimeMetrics, clientConn, clientInfo) } -func (m *serviceMetrics) AddOpenUDPAssociation(clientConn net.Conn) service.UDPConnMetrics { +func (m *serviceMetrics) AddOpenUDPAssociation(clientConn net.Conn) service.UDPAssocationMetrics { clientAddr := clientConn.RemoteAddr() clientInfo := m.getIPInfoFromAddr(clientAddr) - return newUDPConnMetrics(m.udpServiceMetrics, m.tunnelTimeMetrics, clientAddr, clientInfo) + return newUDPAssocationMetrics(m.udpServiceMetrics, m.tunnelTimeMetrics, clientAddr, clientInfo) } func (m *serviceMetrics) AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) { diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 4d9726c7..32981226 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -37,14 +37,14 @@ type ShadowsocksConnMetrics interface { } type ServiceMetrics interface { - AddOpenUDPAssociation(conn net.Conn) UDPConnMetrics + AddOpenUDPAssociation(conn net.Conn) UDPAssocationMetrics AddOpenTCPConnection(conn net.Conn) TCPConnMetrics AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) } type Service interface { HandleStream(ctx context.Context, conn transport.StreamConn) - HandlePacket(conn net.Conn) + HandleAssociation(conn net.Conn) } // Option is a Shadowsocks service constructor option. @@ -58,7 +58,7 @@ type ssService struct { replayCache *ReplayCache sh StreamHandler - ph PacketHandler + ah AssociationHandler } // NewShadowsocksService creates a new Shadowsocks service. @@ -85,8 +85,8 @@ func NewShadowsocksService(opts ...Option) (Service, error) { ) s.sh.SetLogger(s.logger) - s.ph = NewPacketHandler(s.natTimeout, s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"}) - s.ph.SetLogger(s.logger) + s.ah = NewAssociationHandler(s.natTimeout, s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"}) + s.ah.SetLogger(s.logger) return s, nil } @@ -129,20 +129,20 @@ func WithNatTimeout(natTimeout time.Duration) Option { // HandleStream handles a Shadowsocks stream-based connection. func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) { - var connMetrics TCPConnMetrics + var metrics TCPConnMetrics if s.metrics != nil { - connMetrics = s.metrics.AddOpenTCPConnection(conn) + metrics = s.metrics.AddOpenTCPConnection(conn) } - s.sh.Handle(ctx, conn, connMetrics) + s.sh.Handle(ctx, conn, metrics) } -// HandlePacket handles a Shadowsocks packet connection. -func (s *ssService) HandlePacket(conn net.Conn) { - var connMetrics UDPConnMetrics +// HandleAssociation handles a Shadowsocks packet-based assocation. +func (s *ssService) HandleAssociation(conn net.Conn) { + var metrics UDPAssocationMetrics if s.metrics != nil { - connMetrics = s.metrics.AddOpenUDPAssociation(conn) + metrics = s.metrics.AddOpenUDPAssociation(conn) } - s.ph.Handle(conn, connMetrics) + s.ah.Handle(conn, metrics) } type ssConnMetrics struct { diff --git a/service/tcp.go b/service/tcp.go index 775509e9..7823e314 100644 --- a/service/tcp.go +++ b/service/tcp.go @@ -164,6 +164,8 @@ type streamHandler struct { dialer transport.StreamDialer } +var _ StreamHandler = (*streamHandler)(nil) + // NewStreamHandler creates a StreamHandler func NewStreamHandler(authenticate StreamAuthenticateFunc, timeout time.Duration) StreamHandler { return &streamHandler{ diff --git a/service/udp.go b/service/udp.go index 3b590d0e..3dd73157 100644 --- a/service/udp.go +++ b/service/udp.go @@ -36,8 +36,8 @@ type NATMetrics interface { RemoveNATEntry() } -// UDPConnMetrics is used to report metrics on UDP connections. -type UDPConnMetrics interface { +// UDPAssocationMetrics is used to report metrics on UDP connections. +type UDPAssocationMetrics interface { AddAuthenticated(accessKey string) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) @@ -83,7 +83,7 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis return nil, "", nil, errors.New("could not find valid UDP cipher") } -type packetHandler struct { +type associationHandler struct { logger *slog.Logger bufferPool sync.Pool natTimeout time.Duration @@ -92,8 +92,10 @@ type packetHandler struct { targetIPValidator onet.TargetIPValidator } -// NewPacketHandler creates a UDPService -func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics ShadowsocksConnMetrics) PacketHandler { +var _ AssociationHandler = (*associationHandler)(nil) + +// NewAssociationHandler creates a UDPService +func NewAssociationHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics ShadowsocksConnMetrics) AssociationHandler { if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } @@ -102,7 +104,7 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics return make([]byte, serverUDPBufferSize) }, } - return &packetHandler{ + return &associationHandler{ logger: noopLogger(), bufferPool: bufferPool, natTimeout: natTimeout, @@ -112,31 +114,31 @@ func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics } } -// PacketHandler is a running UDP shadowsocks proxy that can be stopped. -type PacketHandler interface { - Handle(conn net.Conn, connMetrics UDPConnMetrics) +// AssociationHandler is a handler that handles UDP assocations. +type AssociationHandler interface { + Handle(association net.Conn, metrics UDPAssocationMetrics) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) } -func (h *packetHandler) SetLogger(l *slog.Logger) { +func (h *associationHandler) SetLogger(l *slog.Logger) { if l == nil { l = noopLogger() } h.logger = l } -func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { +func (h *associationHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { h.targetIPValidator = targetIPValidator } -type PacketHandleFunc func(conn net.Conn) +type AssocationHandleFunc func(assocation net.Conn) // PacketServe listens for packets and calls `handle` to handle them until the connection // returns [ErrClosed]. -func PacketServe(clientConn net.PacketConn, handle PacketHandleFunc, metrics NATMetrics) { +func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics NATMetrics) { nm := newNATmap() defer nm.Close() buffer := make([]byte, serverUDPBufferSize) @@ -199,9 +201,9 @@ func (c *natconn) Close() error { return nil } -func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) { +func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPAssocationMetrics) { if connMetrics == nil { - connMetrics = &NoOpUDPConnMetrics{} + connMetrics = &NoOpUDPAssocationMetrics{} } targetConn, err := newTargetConn(h.natTimeout) @@ -220,11 +222,11 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) var cryptoKey *shadowsocks.EncryptionKey var proxyTargetBytes int for { - clientProxyBytes, err := clientConn.Read(cipherBuf) + clientProxyBytes, err := clientAssociation.Read(cipherBuf) if errors.Is(err, net.ErrClosed) { return } - debugUDPAddr(h.logger, "Outbound packet.", clientConn.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) + debugUDPAddr(h.logger, "Outbound packet.", clientAssociation.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) cipherData := cipherBuf[:clientProxyBytes] connError := func() *onet.ConnectionError { @@ -233,14 +235,14 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) debug.PrintStack() } - slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientConn.RemoteAddr().String())) + slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientAssociation.RemoteAddr().String())) }() var textData []byte var err error if cryptoKey == nil { - ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() + ip := clientAssociation.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var keyID string var cryptoKey *shadowsocks.EncryptionKey unpackStart := time.Now() @@ -255,7 +257,7 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) connMetrics.AddAuthenticated(keyID) go func() { defer connMetrics.AddClosed() - timedCopy(clientConn, targetConn, cryptoKey, connMetrics, h.logger) + timedCopy(clientAssociation, targetConn, cryptoKey, connMetrics, h.logger) }() } else { @@ -274,7 +276,7 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) return onetErr } - debugUDPAddr(h.logger, "Proxy exit.", clientConn.RemoteAddr(), slog.Any("target", targetConn.LocalAddr())) + debugUDPAddr(h.logger, "Proxy exit.", clientAssociation.RemoteAddr(), slog.Any("target", targetConn.LocalAddr())) proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) @@ -294,7 +296,7 @@ func (h *packetHandler) Handle(clientConn net.Conn, connMetrics UDPConnMetrics) // Given the decrypted contents of a UDP packet, return // the payload and the destination address, or an error if // this packet cannot or should not be forwarded. -func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { +func (h *associationHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { tgtAddr := socks.SplitAddr(textData) if tgtAddr == nil { return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) @@ -475,7 +477,7 @@ func (pcw *packetConnWrapper) RemoteAddr() net.Addr { var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) // copy from target to client until read timeout -func timedCopy(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPConnMetrics, l *slog.Logger) { +func timedCopy(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPAssocationMetrics, l *slog.Logger) { // pkt is used for in-place encryption of downstream UDP packets, with the layout // [padding?][salt][address][body][tag][extra] // Padding is only used if the address is IPv4. @@ -547,17 +549,17 @@ func timedCopy(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadow } } -// NoOpUDPConnMetrics is a [UDPConnMetrics] that doesn't do anything. Useful in tests +// NoOpUDPAssocationMetrics is a [UDPAssocationMetrics] that doesn't do anything. Useful in tests // or if you don't want to track metrics. -type NoOpUDPConnMetrics struct{} +type NoOpUDPAssocationMetrics struct{} -var _ UDPConnMetrics = (*NoOpUDPConnMetrics)(nil) +var _ UDPAssocationMetrics = (*NoOpUDPAssocationMetrics)(nil) -func (m *NoOpUDPConnMetrics) AddAuthenticated(accessKey string) {} +func (m *NoOpUDPAssocationMetrics) AddAuthenticated(accessKey string) {} -func (m *NoOpUDPConnMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { +func (m *NoOpUDPAssocationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { } -func (m *NoOpUDPConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { +func (m *NoOpUDPAssocationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { } -func (m *NoOpUDPConnMetrics) AddClosed() { +func (m *NoOpUDPAssocationMetrics) AddClosed() { } From 1eeb4d6c085c65b78807aa326d3e5da92817b38e Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 27 Nov 2024 21:46:07 -0500 Subject: [PATCH 14/80] Fix tests. --- internal/integration_test/integration_test.go | 81 ++- service/udp.go | 43 +- service/udp_test.go | 587 +++++++++++------- 3 files changed, 417 insertions(+), 294 deletions(-) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index eef18fcc..208b8f4c 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -261,53 +261,50 @@ func TestRestrictedAddresses(t *testing.T) { assert.ElementsMatch(t, testMetrics.statuses, expectedStatus) } +// Stub metrics implementation for testing NAT behaviors. + +type natTestMetrics struct { + natEntriesAdded int +} + +var _ service.NATMetrics = (*natTestMetrics)(nil) + +func (m *natTestMetrics) AddNATEntry() { + m.natEntriesAdded++ +} +func (m *natTestMetrics) RemoveNATEntry() {} + // Metrics about one UDP packet. type udpRecord struct { - clientAddr net.Addr accessKey, status string in, out int64 } type fakeUDPAssocationMetrics struct { - clientAddr net.Addr - accessKey string - up, down []udpRecord - mu sync.Mutex + accessKey string + up, down []udpRecord + mu sync.Mutex } var _ service.UDPAssocationMetrics = (*fakeUDPAssocationMetrics)(nil) +func (m *fakeUDPAssocationMetrics) AddAuthenticated(key string) { + m.accessKey = key +} + func (m *fakeUDPAssocationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { m.mu.Lock() defer m.mu.Unlock() - m.up = append(m.up, udpRecord{m.clientAddr, m.accessKey, status, clientProxyBytes, proxyTargetBytes}) + m.up = append(m.up, udpRecord{m.accessKey, status, clientProxyBytes, proxyTargetBytes}) } func (m *fakeUDPAssocationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { m.mu.Lock() defer m.mu.Unlock() - m.down = append(m.down, udpRecord{m.clientAddr, m.accessKey, status, targetProxyBytes, proxyClientBytes}) + m.down = append(m.down, udpRecord{m.accessKey, status, targetProxyBytes, proxyClientBytes}) } -func (m *fakeUDPAssocationMetrics) RemoveNatEntry() { - // Not tested because it requires waiting for a long timeout. -} - -// Fake metrics implementation for UDP -type fakeUDPMetrics struct { - connMetrics []fakeUDPAssocationMetrics -} - -var _ service.UDPMetrics = (*fakeUDPMetrics)(nil) - -func (m *fakeUDPMetrics) AddUDPNatEntry(clientAddr net.Addr, accessKey string) service.UDPAssocationMetrics { - cm := fakeUDPAssocationMetrics{ - clientAddr: clientAddr, - accessKey: accessKey, - } - m.connMetrics = append(m.connMetrics, cm) - return &m.connMetrics[len(m.connMetrics)-1] -} +func (m *fakeUDPAssocationMetrics) AddClosed() {} func TestUDPEcho(t *testing.T) { echoConn, echoRunning := startUDPEchoServer(t) @@ -321,12 +318,14 @@ func TestUDPEcho(t *testing.T) { if err != nil { t.Fatal(err) } - testMetrics := &fakeUDPMetrics{} - proxy := service.NewPacketHandler(time.Hour, cipherList, testMetrics, &fakeShadowsocksMetrics{}) + proxy := service.NewAssociationHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) + proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) + natMetrics := &natTestMetrics{} + associationMetrics := &fakeUDPAssocationMetrics{} go func() { - service.PacketServe(proxyConn, proxy.Handle) + service.PacketServe(proxyConn, func(conn net.Conn) { proxy.Handle(conn, associationMetrics) }, natMetrics) done <- struct{}{} }() @@ -372,22 +371,20 @@ func TestUDPEcho(t *testing.T) { snapshot := cipherList.SnapshotForClientIP(netip.Addr{}) keyID := snapshot[0].Value.(*service.CipherEntry).ID - require.Lenf(t, testMetrics.connMetrics, 1, "Wrong NAT count") + require.Equal(t, natMetrics.natEntriesAdded, 1, "Wrong NAT count") - testMetrics.connMetrics[0].mu.Lock() - defer testMetrics.connMetrics[0].mu.Unlock() + associationMetrics.mu.Lock() + defer associationMetrics.mu.Unlock() - require.Lenf(t, testMetrics.connMetrics[0].up, 1, "Wrong number of packets sent") - record := testMetrics.connMetrics[0].up[0] - require.Equal(t, conn.LocalAddr(), record.clientAddr, "Bad upstream metrics") + require.Lenf(t, associationMetrics.up, 1, "Wrong number of packets sent") + record := associationMetrics.up[0] require.Equal(t, keyID, record.accessKey, "Bad upstream metrics") require.Equal(t, "OK", record.status, "Bad upstream metrics") require.Greater(t, record.in, record.out, "Bad upstream metrics") require.Equal(t, int64(N), record.out, "Bad upstream metrics") - require.Lenf(t, testMetrics.connMetrics[0].down, 1, "Wrong number of packets received") - record = testMetrics.connMetrics[0].down[0] - require.Equal(t, conn.LocalAddr(), record.clientAddr, "Bad downstream metrics") + require.Lenf(t, associationMetrics.down, 1, "Wrong number of packets received") + record = associationMetrics.down[0] require.Equal(t, keyID, record.accessKey, "Bad downstream metrics") require.Equal(t, "OK", record.status, "Bad downstream metrics") require.Greater(t, record.out, record.in, "Bad downstream metrics") @@ -552,11 +549,11 @@ func BenchmarkUDPEcho(b *testing.B) { if err != nil { b.Fatal(err) } - proxy := service.NewPacketHandler(time.Hour, cipherList, &service.NoOpUDPMetrics{}, &fakeShadowsocksMetrics{}) + proxy := service.NewAssociationHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(server, proxy.Handle) + service.PacketServe(server, func(conn net.Conn) { proxy.Handle(conn, &service.NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) done <- struct{}{} }() @@ -596,11 +593,11 @@ func BenchmarkUDPManyKeys(b *testing.B) { if err != nil { b.Fatal(err) } - proxy := service.NewPacketHandler(time.Hour, cipherList, &service.NoOpUDPMetrics{}, &fakeShadowsocksMetrics{}) + proxy := service.NewAssociationHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(proxyConn, proxy.Handle) + service.PacketServe(proxyConn, func(conn net.Conn) { proxy.Handle(conn, &service.NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) done <- struct{}{} }() diff --git a/service/udp.go b/service/udp.go index 3dd73157..93b35fde 100644 --- a/service/udp.go +++ b/service/udp.go @@ -86,10 +86,10 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis type associationHandler struct { logger *slog.Logger bufferPool sync.Pool - natTimeout time.Duration ciphers CipherList ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator + targetConnFactory func() (net.PacketConn, error) } var _ AssociationHandler = (*associationHandler)(nil) @@ -107,10 +107,19 @@ func NewAssociationHandler(natTimeout time.Duration, cipherList CipherList, ssMe return &associationHandler{ logger: noopLogger(), bufferPool: bufferPool, - natTimeout: natTimeout, ciphers: cipherList, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, + targetConnFactory: func() (net.PacketConn, error) { + pc, err := net.ListenPacket("udp", "") + if err != nil { + return nil, fmt.Errorf("failed to create UDP socket: %v", err) + } + return &timedPacketConn{ + PacketConn: pc, + defaultTimeout: natTimeout, + }, nil + }, } } @@ -121,6 +130,8 @@ type AssociationHandler interface { SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) + // SetTargetConnFactory sets the function to be used to create new target connections. + SetTargetConnFactory(factory func() (net.PacketConn, error)) } func (h *associationHandler) SetLogger(l *slog.Logger) { @@ -134,6 +145,10 @@ func (h *associationHandler) SetTargetIPValidator(targetIPValidator onet.TargetI h.targetIPValidator = targetIPValidator } +func (h *associationHandler) SetTargetConnFactory(factory func() (net.PacketConn, error)) { + h.targetConnFactory = factory +} + type AssocationHandleFunc func(assocation net.Conn) // PacketServe listens for packets and calls `handle` to handle them until the connection @@ -206,7 +221,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA connMetrics = &NoOpUDPAssocationMetrics{} } - targetConn, err := newTargetConn(h.natTimeout) + targetConn, err := h.targetConnFactory() if err != nil { slog.Error("UDP: failed to create target connection", slog.Any("err", err)) return @@ -258,6 +273,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA go func() { defer connMetrics.AddClosed() timedCopy(clientAssociation, targetConn, cryptoKey, connMetrics, h.logger) + targetConn.Close() }() } else { @@ -319,7 +335,7 @@ func isDNS(addr net.Addr) bool { return port == "53" } -type targetconn struct { +type timedPacketConn struct { net.PacketConn // Connection timeout to apply for non-DNS packets. defaultTimeout time.Duration @@ -331,18 +347,7 @@ type targetconn struct { fastClose sync.Once } -func newTargetConn(defaultTimeout time.Duration) (net.PacketConn, error) { - pc, err := net.ListenPacket("udp", "") - if err != nil { - return nil, fmt.Errorf("failed to create UDP socket: %v", err) - } - return &targetconn{ - PacketConn: pc, - defaultTimeout: defaultTimeout, - }, nil -} - -func (c *targetconn) onWrite(addr net.Addr) { +func (c *timedPacketConn) onWrite(addr net.Addr) { // Fast close is only allowed if there has been exactly one write, // and it was a DNS query. isDNS := isDNS(addr) @@ -365,7 +370,7 @@ func (c *targetconn) onWrite(addr net.Addr) { } } -func (c *targetconn) onRead(addr net.Addr) { +func (c *timedPacketConn) onRead(addr net.Addr) { c.fastClose.Do(func() { if isDNS(addr) { // The next ReadFrom() should time out immediately. @@ -374,12 +379,12 @@ func (c *targetconn) onRead(addr net.Addr) { }) } -func (c *targetconn) WriteTo(buf []byte, dst net.Addr) (int, error) { +func (c *timedPacketConn) WriteTo(buf []byte, dst net.Addr) (int, error) { c.onWrite(dst) return c.PacketConn.WriteTo(buf, dst) } -func (c *targetconn) ReadFrom(buf []byte) (int, net.Addr, error) { +func (c *timedPacketConn) ReadFrom(buf []byte) (int, net.Addr, error) { n, addr, err := c.PacketConn.ReadFrom(buf) if err == nil { c.onRead(addr) diff --git a/service/udp_test.go b/service/udp_test.go index e081d272..160c9697 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -17,8 +17,10 @@ package service import ( "bytes" "errors" + "fmt" "net" "net/netip" + "sync" "testing" "time" @@ -34,6 +36,7 @@ const timeout = 5 * time.Minute var clientAddr = net.UDPAddr{IP: []byte{192, 0, 2, 1}, Port: 12345} var targetAddr = net.UDPAddr{IP: []byte{192, 0, 2, 2}, Port: 54321} +var localAddr = net.UDPAddr{IP: []byte{127, 0, 0, 1}, Port: 9} var dnsAddr = net.UDPAddr{IP: []byte{192, 0, 2, 3}, Port: 53} var natCryptoKey *shadowsocks.EncryptionKey @@ -53,6 +56,7 @@ type fakePacketConn struct { send chan packet recv chan packet deadline time.Time + mu sync.Mutex } func makePacketConn() *fakePacketConn { @@ -62,14 +66,32 @@ func makePacketConn() *fakePacketConn { } } +func (conn *fakePacketConn) getReadDeadline() time.Time { + conn.mu.Lock() + defer conn.mu.Unlock() + return conn.deadline +} + func (conn *fakePacketConn) SetReadDeadline(deadline time.Time) error { + conn.mu.Lock() + defer conn.mu.Unlock() conn.deadline = deadline return nil } func (conn *fakePacketConn) WriteTo(payload []byte, addr net.Addr) (int, error) { + conn.mu.Lock() + defer conn.mu.Unlock() + + var err error + defer func() { + if recover() != nil { + err = net.ErrClosed + } + }() + conn.send <- packet{addr, payload, nil} - return len(payload), nil + return len(payload), err } func (conn *fakePacketConn) ReadFrom(buffer []byte) (int, net.Addr, error) { @@ -85,111 +107,145 @@ func (conn *fakePacketConn) ReadFrom(buffer []byte) (int, net.Addr, error) { } func (conn *fakePacketConn) Close() error { + fmt.Println("closing fakePacketConn") + conn.mu.Lock() + defer conn.mu.Unlock() close(conn.send) close(conn.recv) return nil } +func (conn *fakePacketConn) LocalAddr() net.Addr { + return &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 9999} +} + +func (conn *fakePacketConn) RemoteAddr() net.Addr { + return &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 8888} +} + type udpReport struct { - clientAddr net.Addr accessKey, status string clientProxyBytes, proxyTargetBytes int64 } // Stub metrics implementation for testing NAT behaviors. -type fakeUDPConnMetrics struct { - clientAddr net.Addr + +type natTestMetrics struct { + natEntriesAdded int +} + +var _ NATMetrics = (*natTestMetrics)(nil) + +func (m *natTestMetrics) AddNATEntry() { + m.natEntriesAdded++ +} +func (m *natTestMetrics) RemoveNATEntry() {} + +type fakeUDPAssocationMetrics struct { accessKey string upstreamPackets []udpReport + mu sync.Mutex } -var _ UDPConnMetrics = (*fakeUDPConnMetrics)(nil) +var _ UDPAssocationMetrics = (*fakeUDPAssocationMetrics)(nil) -func (m *fakeUDPConnMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { - m.upstreamPackets = append(m.upstreamPackets, udpReport{m.clientAddr, m.accessKey, status, clientProxyBytes, proxyTargetBytes}) -} -func (m *fakeUDPConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { +func (m *fakeUDPAssocationMetrics) AddAuthenticated(key string) { + m.accessKey = key } -func (m *fakeUDPConnMetrics) RemoveNatEntry() { + +func (m *fakeUDPAssocationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { + m.mu.Lock() + defer m.mu.Unlock() + m.upstreamPackets = append(m.upstreamPackets, udpReport{m.accessKey, status, clientProxyBytes, proxyTargetBytes}) } -type natTestMetrics struct { - connMetrics []fakeUDPConnMetrics +func (m *fakeUDPAssocationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { } -var _ UDPMetrics = (*natTestMetrics)(nil) +func (m *fakeUDPAssocationMetrics) AddClosed() {} -func (m *natTestMetrics) AddUDPNatEntry(clientAddr net.Addr, accessKey string) UDPConnMetrics { - cm := fakeUDPConnMetrics{ - clientAddr: clientAddr, - accessKey: accessKey, +// sendSSPayload sends a single Shadowsocks packet to the provided connection. +// The packet is constructed with the given address, cipher, and payload. +func sendSSPayload(conn *fakePacketConn, addr net.Addr, cipher *shadowsocks.EncryptionKey, payload []byte) { + socksAddr := socks.ParseAddr(addr.String()) + plaintext := append(socksAddr, payload...) + ciphertext := make([]byte, cipher.SaltSize()+len(plaintext)+cipher.TagSize()) + shadowsocks.Pack(ciphertext, plaintext, cipher) + conn.recv <- packet{ + addr: &clientAddr, + payload: ciphertext, } - m.connMetrics = append(m.connMetrics, cm) - return &m.connMetrics[len(m.connMetrics)-1] } -// Takes a validation policy, and returns the metrics it -// generates when localhost access is attempted -func sendToDiscard(payloads [][]byte, validator onet.TargetIPValidator) *natTestMetrics { +// startTestHandler creates a new association handler with a fake +// client and target connection for testing purposes. It also starts a +// PacketServe goroutine to handle incoming packets on the client connection. +func startTestHandler() (AssociationHandler, func(target net.Addr, payload []byte), *fakePacketConn) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey + handler := NewAssociationHandler(10*time.Second, ciphers, nil) clientConn := makePacketConn() - metrics := &natTestMetrics{} - handler := NewPacketHandler(timeout, ciphers, metrics, &fakeShadowsocksMetrics{}) - handler.SetTargetIPValidator(validator) - done := make(chan struct{}) - go func() { - PacketServe(clientConn, handler.Handle) - done <- struct{}{} - }() - - // Send one packet to the "discard" port on localhost - targetAddr := socks.ParseAddr("127.0.0.1:9") - for _, payload := range payloads { - plaintext := append(targetAddr, payload...) - ciphertext := make([]byte, cipher.SaltSize()+len(plaintext)+cipher.TagSize()) - shadowsocks.Pack(ciphertext, plaintext, cipher) - clientConn.recv <- packet{ - addr: &net.UDPAddr{ - IP: net.ParseIP("192.0.2.1"), - Port: 54321, - }, - payload: ciphertext, - } - } - - clientConn.Close() - <-done - return metrics + targetConn := makePacketConn() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return targetConn, nil + }) + go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + return handler, func(target net.Addr, payload []byte) { + sendSSPayload(clientConn, target, cipher, payload) + }, targetConn } -func TestIPFilter(t *testing.T) { - // Test both the first-packet and subsequent-packet cases. - payloads := [][]byte{[]byte("payload1"), []byte("payload2")} +func TestAssociationHandler_Handle_IPFilter(t *testing.T) { + t.Run("RequirePublicIP blocks localhost", func(t *testing.T) { + handler, sendPayload, targetConn := startTestHandler() + handler.SetTargetIPValidator(onet.RequirePublicIP) + + sendPayload(&localAddr, []byte{1, 2, 3}) - t.Run("Localhost allowed", func(t *testing.T) { - metrics := sendToDiscard(payloads, allowAll) - assert.Equal(t, 1, len(metrics.connMetrics), "Expected 1 NAT entry, not %d", len(metrics.connMetrics)) + select { + case <-targetConn.send: + t.Errorf("Expected no packets to be sent") + case <-time.After(100 * time.Millisecond): + return + } }) - t.Run("Localhost not allowed", func(t *testing.T) { - metrics := sendToDiscard(payloads, onet.RequirePublicIP) - assert.Equal(t, 0, len(metrics.connMetrics), "Unexpected NAT entry on rejected packet") + t.Run("allowAll allows localhost", func(t *testing.T) { + handler, sendPayload, targetConn := startTestHandler() + handler.SetTargetIPValidator(allowAll) + + sendPayload(&localAddr, []byte{1, 2, 3}) + + sent := <-targetConn.send + if !bytes.Equal([]byte{1, 2, 3}, sent.payload) { + t.Errorf("Expected %v, but got %v", []byte{1, 2, 3}, sent.payload) + } }) } func TestUpstreamMetrics(t *testing.T) { + ciphers, _ := MakeTestCiphers([]string{"asdf"}) + cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey + handler := NewAssociationHandler(10*time.Second, ciphers, nil) + clientConn := makePacketConn() + targetConn := makePacketConn() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return targetConn, nil + }) + metrics := &fakeUDPAssocationMetrics{} + go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, metrics) }, &natTestMetrics{}) + // Test both the first-packet and subsequent-packet cases. const N = 10 - payloads := make([][]byte, 0) for i := 1; i <= N; i++ { - payloads = append(payloads, make([]byte, i)) + sendSSPayload(clientConn, &targetAddr, cipher, make([]byte, i)) + <-targetConn.send } - metrics := sendToDiscard(payloads, allowAll) - - assert.Equal(t, N, len(metrics.connMetrics[0].upstreamPackets), "Expected %d reports, not %d", N, len(metrics.connMetrics[0].upstreamPackets)) - for i, report := range metrics.connMetrics[0].upstreamPackets { + metrics.mu.Lock() + defer metrics.mu.Unlock() + assert.Equal(t, N, len(metrics.upstreamPackets), "Expected %d reports, not %d", N, len(metrics.upstreamPackets)) + for i, report := range metrics.upstreamPackets { assert.Equal(t, int64(i+1), report.proxyTargetBytes, "Expected %d payload bytes, not %d", i+1, report.proxyTargetBytes) assert.Greater(t, report.clientProxyBytes, report.proxyTargetBytes, "Expected nonzero input overhead (%d > %d)", report.clientProxyBytes, report.proxyTargetBytes) assert.Equal(t, "id-0", report.accessKey, "Unexpected access key name: %s", report.accessKey) @@ -205,196 +261,262 @@ func assertAlmostEqual(t *testing.T, a, b time.Time) { } } -func TestNATEmpty(t *testing.T) { - nat := newNATmap(timeout, &natTestMetrics{}, noopLogger()) - if nat.Get("foo") != nil { - t.Error("Expected nil value from empty NAT map") +func assertUDPAddrEqual(t *testing.T, a net.Addr, b *net.UDPAddr) { + addr, ok := a.(*net.UDPAddr) + if !ok || !addr.IP.Equal(b.IP) || addr.Port != b.Port || addr.Zone != b.Zone { + t.Errorf("Mismatched address: %v != %v", a, b) } } -func setupNAT() (*fakePacketConn, *fakePacketConn, *natconn) { - nat := newNATmap(timeout, &natTestMetrics{}, noopLogger()) - clientConn := makePacketConn() - targetConn := makePacketConn() - nat.Add(&packetConn{PacketConn: clientConn, raddr: &clientAddr}, targetConn, natCryptoKey, "key id") - entry := nat.Get(clientAddr.String()) - return clientConn, targetConn, entry +// Implements net.Error +type fakeTimeoutError struct { + error } -func TestNATGet(t *testing.T) { - _, targetConn, entry := setupNAT() - if entry == nil { - t.Fatal("Failed to find target conn") - } - if entry.PacketConn != targetConn { - t.Error("Mismatched connection returned") - } +func (e *fakeTimeoutError) Timeout() bool { + return true } -func TestNATWrite(t *testing.T) { - _, targetConn, entry := setupNAT() - - // Simulate one generic packet being sent - buf := []byte{1} - entry.WriteTo([]byte{1}, &targetAddr) - assertAlmostEqual(t, targetConn.deadline, time.Now().Add(timeout)) - sent := <-targetConn.send - if !bytes.Equal(sent.payload, buf) { - t.Errorf("Mismatched payload: %v != %v", sent.payload, buf) - } - if sent.addr != &targetAddr { - t.Errorf("Mismatched address: %v != %v", sent.addr, &targetAddr) - } +func (e *fakeTimeoutError) Temporary() bool { + return false } -func TestNATWriteDNS(t *testing.T) { - _, targetConn, entry := setupNAT() +func TestTimedPacketConn(t *testing.T) { + t.Run("Write", func(t *testing.T) { + handler, sendPayload, targetConn := startTestHandler() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) - // Simulate one DNS query being sent. - buf := []byte{1} - entry.WriteTo(buf, &dnsAddr) - // DNS-only connections have a fixed timeout of 17 seconds. - assertAlmostEqual(t, targetConn.deadline, time.Now().Add(17*time.Second)) - sent := <-targetConn.send - if !bytes.Equal(sent.payload, buf) { - t.Errorf("Mismatched payload: %v != %v", sent.payload, buf) - } - if sent.addr != &dnsAddr { - t.Errorf("Mismatched address: %v != %v", sent.addr, &targetAddr) - } -} + buf := []byte{1} + sendPayload(&targetAddr, buf) -func TestNATWriteDNSMultiple(t *testing.T) { - _, targetConn, entry := setupNAT() - - // Simulate three DNS queries being sent. - buf := []byte{1} - entry.WriteTo(buf, &dnsAddr) - <-targetConn.send - entry.WriteTo(buf, &dnsAddr) - <-targetConn.send - entry.WriteTo(buf, &dnsAddr) - <-targetConn.send - // DNS-only connections have a fixed timeout of 17 seconds. - assertAlmostEqual(t, targetConn.deadline, time.Now().Add(17*time.Second)) -} - -func TestNATWriteMixed(t *testing.T) { - _, targetConn, entry := setupNAT() - - // Simulate both non-DNS and DNS packets being sent. - buf := []byte{1} - entry.WriteTo(buf, &targetAddr) - <-targetConn.send - entry.WriteTo(buf, &dnsAddr) - <-targetConn.send - // Mixed DNS and non-DNS connections should have the user-specified timeout. - assertAlmostEqual(t, targetConn.deadline, time.Now().Add(timeout)) -} - -func TestNATFastClose(t *testing.T) { - clientConn, targetConn, entry := setupNAT() - - // Send one DNS query. - query := []byte{1} - entry.WriteTo(query, &dnsAddr) - sent := <-targetConn.send - require.Len(t, sent.payload, 1) - // Send the response. - response := []byte{1, 2, 3, 4, 5} - received := packet{addr: &dnsAddr, payload: response} - targetConn.recv <- received - sent, ok := <-clientConn.send - if !ok { - t.Error("clientConn was closed") - } - if len(sent.payload) <= len(response) { - t.Error("Packet is too short to be shadowsocks-AEAD") - } - if sent.addr != &clientAddr { - t.Errorf("Address mismatch: %v != %v", sent.addr, clientAddr) - } + assertAlmostEqual(t, targetConn.getReadDeadline(), time.Now().Add(timeout)) + sent := <-targetConn.send + if !bytes.Equal(sent.payload, buf) { + t.Errorf("Mismatched payload: %v != %v", sent.payload, buf) + } + assertUDPAddrEqual(t, sent.addr, &targetAddr) + }) - // targetConn should be scheduled to close immediately. - assertAlmostEqual(t, targetConn.deadline, time.Now()) -} + t.Run("WriteDNS", func(t *testing.T) { + handler, sendPayload, targetConn := startTestHandler() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) + + // Simulate one DNS query being sent. + buf := []byte{1} + sendPayload(&dnsAddr, buf) + + // DNS-only connections have a fixed timeout of 17 seconds. + assertAlmostEqual(t, targetConn.getReadDeadline(), time.Now().Add(17*time.Second)) + sent := <-targetConn.send + if !bytes.Equal(sent.payload, buf) { + t.Errorf("Mismatched payload: %v != %v", sent.payload, buf) + } + assertUDPAddrEqual(t, sent.addr, &dnsAddr) + }) -func TestNATNoFastClose_NotDNS(t *testing.T) { - clientConn, targetConn, entry := setupNAT() + t.Run("WriteDNSMultiple", func(t *testing.T) { + handler, sendPayload, targetConn := startTestHandler() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) + + // Simulate three DNS queries being sent. + buf := []byte{1} + sendPayload(&dnsAddr, buf) + <-targetConn.send + sendPayload(&dnsAddr, buf) + <-targetConn.send + sendPayload(&dnsAddr, buf) + <-targetConn.send + + // DNS-only connections have a fixed timeout of 17 seconds. + assertAlmostEqual(t, targetConn.getReadDeadline(), time.Now().Add(17*time.Second)) + }) - // Send one non-DNS packet. - query := []byte{1} - entry.WriteTo(query, &targetAddr) - sent := <-targetConn.send - require.Len(t, sent.payload, 1) - // Send the response. - response := []byte{1, 2, 3, 4, 5} - received := packet{addr: &targetAddr, payload: response} - targetConn.recv <- received - sent, ok := <-clientConn.send - if !ok { - t.Error("clientConn was closed") - } - if len(sent.payload) <= len(response) { - t.Error("Packet is too short to be shadowsocks-AEAD") - } - if sent.addr != &clientAddr { - t.Errorf("Address mismatch: %v != %v", sent.addr, clientAddr) - } - // targetConn should be scheduled to close after the full timeout. - assertAlmostEqual(t, targetConn.deadline, time.Now().Add(timeout)) -} + t.Run("WriteMixed", func(t *testing.T) { + handler, sendPayload, targetConn := startTestHandler() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) + + // Simulate both non-DNS and DNS packets being sent. + buf := []byte{1} + sendPayload(&targetAddr, buf) + <-targetConn.send + sendPayload(&dnsAddr, buf) + <-targetConn.send + + // Mixed DNS and non-DNS connections should have the user-specified timeout. + assertAlmostEqual(t, targetConn.getReadDeadline(), time.Now().Add(timeout)) + }) -func TestNATNoFastClose_MultipleDNS(t *testing.T) { - clientConn, targetConn, entry := setupNAT() + t.Run("FastClose", func(t *testing.T) { + ciphers, _ := MakeTestCiphers([]string{"asdf"}) + cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey + handler := NewAssociationHandler(10*time.Second, ciphers, nil) + clientConn := makePacketConn() + targetConn := makePacketConn() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) + go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + + // Send one DNS query. + sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) + sent := <-targetConn.send + require.Len(t, sent.payload, 1) + // Send the response. + response := []byte{1, 2, 3, 4, 5} + received := packet{addr: &dnsAddr, payload: response} + targetConn.recv <- received + sent, ok := <-clientConn.send + if !ok { + t.Error("clientConn was closed") + } - // Send two DNS packets. - query1 := []byte{1} - entry.WriteTo(query1, &dnsAddr) - <-targetConn.send - query2 := []byte{2} - entry.WriteTo(query2, &dnsAddr) - <-targetConn.send + // targetConn should be scheduled to close immediately. + assertAlmostEqual(t, targetConn.getReadDeadline(), time.Now()) + }) - // Send a response. - response := []byte{1, 2, 3, 4, 5} - received := packet{addr: &dnsAddr, payload: response} - targetConn.recv <- received - <-clientConn.send + t.Run("NoFastClose_NotDNS", func(t *testing.T) { + ciphers, _ := MakeTestCiphers([]string{"asdf"}) + cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey + handler := NewAssociationHandler(10*time.Second, ciphers, nil) + clientConn := makePacketConn() + targetConn := makePacketConn() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) + go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + + // Send one non-DNS packet. + sendSSPayload(clientConn, &targetAddr, cipher, []byte{1}) + sent := <-targetConn.send + require.Len(t, sent.payload, 1) + // Send the response. + response := []byte{1, 2, 3, 4, 5} + received := packet{addr: &targetAddr, payload: response} + targetConn.recv <- received + sent, ok := <-clientConn.send + if !ok { + t.Error("clientConn was closed") + } - // targetConn should be scheduled to close after the DNS timeout. - assertAlmostEqual(t, targetConn.deadline, time.Now().Add(17*time.Second)) -} + // targetConn should be scheduled to close after the full timeout. + assertAlmostEqual(t, targetConn.getReadDeadline(), time.Now().Add(timeout)) + }) -// Implements net.Error -type fakeTimeoutError struct { - error -} + t.Run("NoFastClose_MultipleDNS", func(t *testing.T) { + ciphers, _ := MakeTestCiphers([]string{"asdf"}) + cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey + handler := NewAssociationHandler(10*time.Second, ciphers, nil) + clientConn := makePacketConn() + targetConn := makePacketConn() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) + go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + + // Send two DNS packets. + sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) + <-targetConn.send + sendSSPayload(clientConn, &dnsAddr, cipher, []byte{2}) + <-targetConn.send + + // Send a response. + response := []byte{1, 2, 3, 4, 5} + received := packet{addr: &dnsAddr, payload: response} + targetConn.recv <- received + <-clientConn.send + + // targetConn should be scheduled to close after the DNS timeout. + assertAlmostEqual(t, targetConn.getReadDeadline(), time.Now().Add(17*time.Second)) + }) -func (e *fakeTimeoutError) Timeout() bool { - return true -} + t.Run("Timeout", func(t *testing.T) { + handler, sendPayload, targetConn := startTestHandler() + handler.SetTargetConnFactory(func() (net.PacketConn, error) { + return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil + }) + + // Simulate a non-DNS initial packet. + sendPayload(&targetAddr, []byte{1}) + <-targetConn.send + // Simulate a read timeout. + received := packet{err: &fakeTimeoutError{}} + before := time.Now() + targetConn.recv <- received + // Wait for targetConn to close. + if _, ok := <-targetConn.send; ok { + t.Error("targetConn should be closed due to read timeout") + } -func (e *fakeTimeoutError) Temporary() bool { - return false + // targetConn should be closed as soon as the timeout error is received. + assertAlmostEqual(t, before, time.Now()) + }) } -func TestNATTimeout(t *testing.T) { - _, targetConn, entry := setupNAT() +func TestNATMap(t *testing.T) { + t.Run("Empty", func(t *testing.T) { + nat := newNATmap() + if nat.Get("foo") != nil { + t.Error("Expected nil value from empty NAT map") + } + }) - // Simulate a non-DNS initial packet. - entry.WriteTo([]byte{1}, &targetAddr) - <-targetConn.send - // Simulate a read timeout. - received := packet{err: &fakeTimeoutError{}} - before := time.Now() - targetConn.recv <- received - // Wait for targetConn to close. - if _, ok := <-targetConn.send; ok { - t.Error("targetConn should be closed due to read timeout") - } - // targetConn should be closed as soon as the timeout error is received. - assertAlmostEqual(t, before, time.Now()) + t.Run("Add", func(t *testing.T) { + nat := newNATmap() + addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + conn1 := &natconn{} + + nat.Add(addr1, conn1) + assert.Equal(t, conn1, nat.Get(addr1.String()), "Get should return the correct connection") + + conn2 := &natconn{} + nat.Add(addr1, conn2) + assert.Equal(t, conn2, nat.Get(addr1.String()), "Adding with the same address should overwrite the entry") + }) + + t.Run("Get", func(t *testing.T) { + nat := newNATmap() + addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + conn1 := &natconn{} + nat.Add(addr1, conn1) + + assert.Equal(t, conn1, nat.Get(addr1.String()), "Get should return the correct connection for an existing address") + + addr2 := &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 5678} + assert.Nil(t, nat.Get(addr2.String()), "Get should return nil for a non-existent address") + }) + + t.Run("closure_deletes", func(t *testing.T) { + nat := newNATmap() + addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + conn1 := &natconn{} + deleteEntry := nat.Add(addr1, conn1) + + deleteEntry() + + assert.Nil(t, nat.Get(addr1.String()), "Get should return nil after deleting the entry") + }) + + t.Run("Close", func(t *testing.T) { + nat := newNATmap() + addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + pc := makePacketConn() + conn1 := &natconn{Conn: &packetConnWrapper{PacketConn: pc, raddr: addr1}} + nat.Add(addr1, conn1) + + err := nat.Close() + assert.NoError(t, err, "Close should not return an error") + + // The underlying connection should be scheduled to close immediately. + assertAlmostEqual(t, pc.deadline, time.Now()) + }) } // Simulates receiving invalid UDP packets on a server with 100 ciphers. @@ -479,9 +601,8 @@ func TestUDPEarlyClose(t *testing.T) { if err != nil { t.Fatal(err) } - testMetrics := &natTestMetrics{} const testTimeout = 200 * time.Millisecond - ph := NewPacketHandler(testTimeout, cipherList, testMetrics, &fakeShadowsocksMetrics{}) + ph := NewAssociationHandler(testTimeout, cipherList, &fakeShadowsocksMetrics{}) clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) if err != nil { @@ -489,7 +610,7 @@ func TestUDPEarlyClose(t *testing.T) { } require.Nil(t, clientConn.Close()) // This should return quickly without timing out. - PacketServe(clientConn, ph.Handle) + PacketServe(clientConn, func(conn net.Conn) { ph.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) } // Makes sure the UDP listener returns [io.ErrClosed] on reads and writes after Close(). From 08e8bbab55fafbadbedf97d3bfa257a3b2cadbc5 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 27 Nov 2024 22:10:40 -0500 Subject: [PATCH 15/80] Update the docstring for `PacketServe`. --- service/udp.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/service/udp.go b/service/udp.go index 93b35fde..388e5c59 100644 --- a/service/udp.go +++ b/service/udp.go @@ -151,8 +151,10 @@ func (h *associationHandler) SetTargetConnFactory(factory func() (net.PacketConn type AssocationHandleFunc func(assocation net.Conn) -// PacketServe listens for packets and calls `handle` to handle them until the connection -// returns [ErrClosed]. +// PacketServe listens for UDP packets on the provided [net.PacketConn], creates +// creates and manages NAT associations, and invokes the provided `handle` +// function for each association. It uses a NAT map to track active associations +// and handles their lifecycle. func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics NATMetrics) { nm := newNATmap() defer nm.Close() From 34a01d7b8413f3a1c6e40316a055e2d55f1d73d3 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 27 Nov 2024 22:13:01 -0500 Subject: [PATCH 16/80] Fix comment to refer to "associations". --- service/udp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index 388e5c59..220b3819 100644 --- a/service/udp.go +++ b/service/udp.go @@ -36,7 +36,7 @@ type NATMetrics interface { RemoveNATEntry() } -// UDPAssocationMetrics is used to report metrics on UDP connections. +// UDPAssocationMetrics is used to report metrics on UDP associations. type UDPAssocationMetrics interface { AddAuthenticated(accessKey string) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) From 033ba9d32a072df6d4bd12538765636b9c32aa8e Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 3 Dec 2024 15:32:02 -0500 Subject: [PATCH 17/80] Fix metrics test. --- prometheus/metrics_test.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/prometheus/metrics_test.go b/prometheus/metrics_test.go index 5dfcf05a..73f39fd2 100644 --- a/prometheus/metrics_test.go +++ b/prometheus/metrics_test.go @@ -70,17 +70,17 @@ func TestMethodsDontPanic(t *testing.T) { TargetProxy: 3, ProxyClient: 4, } - addr := fakeAddr("127.0.0.1:9") tcpMetrics := ssMetrics.AddOpenTCPConnection(&fakeConn{}) tcpMetrics.AddAuthenticated("0") tcpMetrics.AddClosed("OK", proxyMetrics, 10*time.Millisecond) tcpMetrics.AddProbe("ERR_CIPHER", "eof", proxyMetrics.ClientProxy) - udpMetrics := ssMetrics.AddUDPNatEntry(addr, "key-1") + udpMetrics := ssMetrics.AddOpenUDPAssociation(&fakeConn{}) + udpMetrics.AddAuthenticated("0") udpMetrics.AddPacketFromClient("OK", 10, 20) udpMetrics.AddPacketFromTarget("OK", 10, 20) - udpMetrics.RemoveNatEntry() + udpMetrics.AddClosed() ssMetrics.tcpServiceMetrics.AddCipherSearch(true, 10*time.Millisecond) ssMetrics.udpServiceMetrics.AddCipherSearch(true, 10*time.Millisecond) @@ -191,9 +191,7 @@ func BenchmarkProbe(b *testing.B) { func BenchmarkClientUDP(b *testing.B) { ssMetrics, _ := NewServiceMetrics(nil) - addr := fakeAddr("127.0.0.1:9") - accessKey := "key 1" - udpMetrics := ssMetrics.AddUDPNatEntry(addr, accessKey) + udpMetrics := ssMetrics.AddOpenUDPAssociation(&fakeConn{}) status := "OK" size := int64(1000) b.ResetTimer() @@ -204,9 +202,7 @@ func BenchmarkClientUDP(b *testing.B) { func BenchmarkTargetUDP(b *testing.B) { ssMetrics, _ := NewServiceMetrics(nil) - addr := fakeAddr("127.0.0.1:9") - accessKey := "key 1" - udpMetrics := ssMetrics.AddUDPNatEntry(addr, accessKey) + udpMetrics := ssMetrics.AddOpenUDPAssociation(&fakeConn{}) status := "OK" size := int64(1000) b.ResetTimer() @@ -215,12 +211,12 @@ func BenchmarkTargetUDP(b *testing.B) { } } -func BenchmarkNAT(b *testing.B) { +func BenchmarkClose(b *testing.B) { ssMetrics, _ := NewServiceMetrics(nil) addr := fakeAddr("127.0.0.1:9") b.ResetTimer() for i := 0; i < b.N; i++ { - udpMetrics := ssMetrics.AddUDPNatEntry(addr, "key-0") - udpMetrics.RemoveNatEntry() + udpMetrics := ssMetrics.AddOpenUDPAssociation(&fakeConn{}) + udpMetrics.AddClosed() } } From 87a9d23fc77c8788db3bcc5547705c3bcdb76ebe Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 3 Dec 2024 15:52:48 -0500 Subject: [PATCH 18/80] Use `slicepool`. --- service/udp.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/service/udp.go b/service/udp.go index 220b3819..433ac139 100644 --- a/service/udp.go +++ b/service/udp.go @@ -26,8 +26,10 @@ import ( "time" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" - onet "github.com/Jigsaw-Code/outline-ss-server/net" "github.com/shadowsocks/go-shadowsocks2/socks" + + "github.com/Jigsaw-Code/outline-ss-server/internal/slicepool" + onet "github.com/Jigsaw-Code/outline-ss-server/net" ) // NATMetrics is used to report NAT related metrics. @@ -84,8 +86,9 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis } type associationHandler struct { - logger *slog.Logger - bufferPool sync.Pool + logger *slog.Logger + // bufPool stores the byte slices used for reading and decrypting packets. + bufPool slicepool.Pool ciphers CipherList ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator @@ -94,19 +97,14 @@ type associationHandler struct { var _ AssociationHandler = (*associationHandler)(nil) -// NewAssociationHandler creates a UDPService +// NewAssociationHandler creates an AssociationHandler func NewAssociationHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics ShadowsocksConnMetrics) AssociationHandler { if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } - bufferPool := sync.Pool{ - New: func() interface{} { - return make([]byte, serverUDPBufferSize) - }, - } return &associationHandler{ logger: noopLogger(), - bufferPool: bufferPool, + bufPool: slicepool.MakePool(serverUDPBufferSize), ciphers: cipherList, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, @@ -229,18 +227,16 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA return } - cipherBuf := h.bufferPool.Get().([]byte) - textBuf := h.bufferPool.Get().([]byte) - defer func() { - h.bufferPool.Put(cipherBuf) - h.bufferPool.Put(textBuf) - }() + cipherLazySlice := h.bufPool.LazySlice() + textLazySlice := h.bufPool.LazySlice() var cryptoKey *shadowsocks.EncryptionKey var proxyTargetBytes int for { + cipherBuf := cipherLazySlice.Acquire() clientProxyBytes, err := clientAssociation.Read(cipherBuf) if errors.Is(err, net.ErrClosed) { + cipherLazySlice.Release() return } debugUDPAddr(h.logger, "Outbound packet.", clientAssociation.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) @@ -262,9 +258,11 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA ip := clientAssociation.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var keyID string var cryptoKey *shadowsocks.EncryptionKey + textBuf := textLazySlice.Acquire() unpackStart := time.Now() textData, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, cipherData, h.ciphers, h.logger) timeToCipher := time.Since(unpackStart) + textLazySlice.Release() h.ssm.AddCipherSearch(err == nil, timeToCipher) if err != nil { @@ -302,6 +300,8 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA return nil }() + cipherLazySlice.Release() + status := "OK" if connError != nil { h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) From 50feaa29ff3b9032a2fefbe8ec4327be6925a930 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 4 Dec 2024 12:24:20 -0500 Subject: [PATCH 19/80] Let the assocation handler provide the buffer. --- prometheus/metrics_test.go | 1 - service/udp.go | 50 +++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/prometheus/metrics_test.go b/prometheus/metrics_test.go index 73f39fd2..c435ee0c 100644 --- a/prometheus/metrics_test.go +++ b/prometheus/metrics_test.go @@ -213,7 +213,6 @@ func BenchmarkTargetUDP(b *testing.B) { func BenchmarkClose(b *testing.B) { ssMetrics, _ := NewServiceMetrics(nil) - addr := fakeAddr("127.0.0.1:9") b.ResetTimer() for i := 0; i < b.N; i++ { udpMetrics := ssMetrics.AddOpenUDPAssociation(&fakeConn{}) diff --git a/service/udp.go b/service/udp.go index 433ac139..87c5ba4b 100644 --- a/service/udp.go +++ b/service/udp.go @@ -17,7 +17,6 @@ package service import ( "errors" "fmt" - "io" "log/slog" "net" "net/netip" @@ -173,7 +172,8 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics if conn == nil { conn = &natconn{ Conn: &packetConnWrapper{PacketConn: clientConn, raddr: addr}, - readCh: make(chan []byte, 1), + readBufCh: make(chan []byte, 1), + bytesReadCh: make(chan int, 1), } metrics.AddNATEntry() deleteEntry := nm.Add(addr, conn) @@ -186,32 +186,44 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics handle(conn) }(conn) } - conn.readCh <- pkt + readBuf, ok := <-conn.readBufCh + if !ok { + continue + } + copy(readBuf, pkt) + conn.bytesReadCh <- n } } +// natconn adapts a [net.Conn] to provide a synchronized reading mechanism for NAT traversal. +// +// The application provides the buffer to `Read()` (BYOB: Bring Your Own Buffer!) +// which minimizes buffer allocations and copying. type natconn struct { net.Conn - readCh chan []byte + + // readBufCh provides a buffer to copy incoming packet data into. + readBufCh chan []byte + + // bytesReadCh is used to signal the availability of new data and carries + // the length of the received packet. + bytesReadCh chan int } var _ net.Conn = (*natconn)(nil) func (c *natconn) Read(p []byte) (int, error) { - select { - case pkt := <-c.readCh: - if pkt == nil { - break - } - return copy(p, pkt), nil - case <-time.After(30 * time.Second): - break + c.readBufCh <- p + n, ok := <-c.bytesReadCh + if !ok { + return 0, net.ErrClosed } - return 0, io.EOF + return n, nil } func (c *natconn) Close() error { - close(c.readCh) + close(c.readBufCh) + close(c.bytesReadCh) c.Conn.Close() return nil } @@ -228,20 +240,21 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA } cipherLazySlice := h.bufPool.LazySlice() + cipherBuf := cipherLazySlice.Acquire() + defer cipherLazySlice.Release() + textLazySlice := h.bufPool.LazySlice() var cryptoKey *shadowsocks.EncryptionKey var proxyTargetBytes int for { - cipherBuf := cipherLazySlice.Acquire() clientProxyBytes, err := clientAssociation.Read(cipherBuf) if errors.Is(err, net.ErrClosed) { cipherLazySlice.Release() return } debugUDPAddr(h.logger, "Outbound packet.", clientAssociation.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) - cipherData := cipherBuf[:clientProxyBytes] - + connError := func() *onet.ConnectionError { defer func() { if r := recover(); r != nil { @@ -251,6 +264,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientAssociation.RemoteAddr().String())) }() + cipherData := cipherBuf[:clientProxyBytes] var textData []byte var err error @@ -300,8 +314,6 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA return nil }() - cipherLazySlice.Release() - status := "OK" if connError != nil { h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) From b7a4a3cd3618af55e004624f789bdc59d932841d Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 10 Dec 2024 14:44:22 -0500 Subject: [PATCH 20/80] Remove the `packetConnWrapper` and move the logic into `natconn` instead. --- service/udp.go | 42 ++++++++++++++---------------------------- service/udp_test.go | 2 +- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/service/udp.go b/service/udp.go index 87c5ba4b..5d1583a2 100644 --- a/service/udp.go +++ b/service/udp.go @@ -171,7 +171,8 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics conn := nm.Get(addr.String()) if conn == nil { conn = &natconn{ - Conn: &packetConnWrapper{PacketConn: clientConn, raddr: addr}, + PacketConn: clientConn, + raddr: addr, readBufCh: make(chan []byte, 1), bytesReadCh: make(chan int, 1), } @@ -200,7 +201,8 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics // The application provides the buffer to `Read()` (BYOB: Bring Your Own Buffer!) // which minimizes buffer allocations and copying. type natconn struct { - net.Conn + net.PacketConn + raddr net.Addr // readBufCh provides a buffer to copy incoming packet data into. readBufCh chan []byte @@ -221,13 +223,22 @@ func (c *natconn) Read(p []byte) (int, error) { return n, nil } +func (c *natconn) Write(b []byte) (n int, err error) { + return c.PacketConn.WriteTo(b, c.raddr) +} + func (c *natconn) Close() error { close(c.readBufCh) close(c.bytesReadCh) - c.Conn.Close() + c.PacketConn.Close() return nil } +func (c *natconn) RemoteAddr() net.Addr { + return c.raddr +} + + func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPAssocationMetrics) { if connMetrics == nil { connMetrics = &NoOpUDPAssocationMetrics{} @@ -466,31 +477,6 @@ func (m *natmap) Close() error { return err } -// packetConnWrapper wraps a [net.PacketConn] and provides a [net.Conn] interface -// with a given remote address. -type packetConnWrapper struct { - net.PacketConn - raddr net.Addr -} - -var _ net.Conn = (*packetConnWrapper)(nil) - -// ReadFrom reads data from the connection. -func (pcw *packetConnWrapper) Read(b []byte) (n int, err error) { - n, _, err = pcw.PacketConn.ReadFrom(b) - return -} - -// WriteTo writes data to the connection. -func (pcw *packetConnWrapper) Write(b []byte) (n int, err error) { - return pcw.PacketConn.WriteTo(b, pcw.raddr) -} - -// RemoteAddr returns the remote network address. -func (pcw *packetConnWrapper) RemoteAddr() net.Addr { - return pcw.raddr -} - // Get the maximum length of the shadowsocks address header by parsing // and serializing an IPv6 address from the example range. var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) diff --git a/service/udp_test.go b/service/udp_test.go index 160c9697..1ca0ec96 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -508,7 +508,7 @@ func TestNATMap(t *testing.T) { nat := newNATmap() addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} pc := makePacketConn() - conn1 := &natconn{Conn: &packetConnWrapper{PacketConn: pc, raddr: addr1}} + conn1 := &natconn{PacketConn: pc, raddr: addr1} nat.Add(addr1, conn1) err := nat.Close() From f80294c39c894311b8b9138cb4bea105459a07f2 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 10 Dec 2024 16:35:19 -0500 Subject: [PATCH 21/80] Fix close while reading of `natconn`. --- service/udp.go | 32 ++++++++++++++++++-------------- service/udp_test.go | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/service/udp.go b/service/udp.go index 5d1583a2..4b822ce0 100644 --- a/service/udp.go +++ b/service/udp.go @@ -171,9 +171,10 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics conn := nm.Get(addr.String()) if conn == nil { conn = &natconn{ - PacketConn: clientConn, - raddr: addr, - readBufCh: make(chan []byte, 1), + PacketConn: clientConn, + raddr: addr, + doneCh: make(chan struct{}), + readBufCh: make(chan []byte, 1), bytesReadCh: make(chan int, 1), } metrics.AddNATEntry() @@ -202,10 +203,11 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics // which minimizes buffer allocations and copying. type natconn struct { net.PacketConn - raddr net.Addr + raddr net.Addr + doneCh chan struct{} // readBufCh provides a buffer to copy incoming packet data into. - readBufCh chan []byte + readBufCh chan []byte // bytesReadCh is used to signal the availability of new data and carries // the length of the received packet. @@ -215,12 +217,16 @@ type natconn struct { var _ net.Conn = (*natconn)(nil) func (c *natconn) Read(p []byte) (int, error) { - c.readBufCh <- p - n, ok := <-c.bytesReadCh - if !ok { + select { + case c.readBufCh <- p: + n, ok := <-c.bytesReadCh + if !ok { + return 0, net.ErrClosed + } + return n, nil + case <-c.doneCh: return 0, net.ErrClosed } - return n, nil } func (c *natconn) Write(b []byte) (n int, err error) { @@ -228,17 +234,15 @@ func (c *natconn) Write(b []byte) (n int, err error) { } func (c *natconn) Close() error { + close(c.doneCh) close(c.readBufCh) close(c.bytesReadCh) - c.PacketConn.Close() - return nil + return c.PacketConn.Close() } - func (c *natconn) RemoteAddr() net.Addr { return c.raddr } - func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPAssocationMetrics) { if connMetrics == nil { connMetrics = &NoOpUDPAssocationMetrics{} @@ -265,7 +269,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA return } debugUDPAddr(h.logger, "Outbound packet.", clientAssociation.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) - + connError := func() *onet.ConnectionError { defer func() { if r := recover(); r != nil { diff --git a/service/udp_test.go b/service/udp_test.go index 1ca0ec96..6bba5f0b 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -195,6 +195,24 @@ func startTestHandler() (AssociationHandler, func(target net.Addr, payload []byt }, targetConn } +func TestNatconnCloseWhileReading(t *testing.T) { + nc := &natconn{ + PacketConn: makePacketConn(), + raddr: &clientAddr, + doneCh: make(chan struct{}), + readBufCh: make(chan []byte, 1), + bytesReadCh: make(chan int, 1), + } + go func() { + buf := make([]byte, 1024) + nc.Read(buf) + }() + + err := nc.Close() + + assert.NoError(t, err, "Close should not panic or return an error") +} + func TestAssociationHandler_Handle_IPFilter(t *testing.T) { t.Run("RequirePublicIP blocks localhost", func(t *testing.T) { handler, sendPayload, targetConn := startTestHandler() From 36d4b275a0252862ba4593ecc436b626a93bcbb4 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 10 Dec 2024 16:51:46 -0500 Subject: [PATCH 22/80] Simplify the natmap a little. --- service/udp.go | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/service/udp.go b/service/udp.go index 4b822ce0..57818a78 100644 --- a/service/udp.go +++ b/service/udp.go @@ -179,7 +179,7 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics } metrics.AddNATEntry() deleteEntry := nm.Add(addr, conn) - go func(conn *natconn) { + go func(conn net.Conn) { defer func() { conn.Close() deleteEntry() @@ -239,6 +239,7 @@ func (c *natconn) Close() error { close(c.bytesReadCh) return c.PacketConn.Close() } + func (c *natconn) RemoteAddr() net.Addr { return c.raddr } @@ -437,33 +438,18 @@ func (m *natmap) Get(key string) *natconn { return m.keyConn[key] } -func (m *natmap) set(key string, pc *natconn) { - m.Lock() - defer m.Unlock() - - m.keyConn[key] = pc - return -} - -func (m *natmap) del(key string) *natconn { - m.Lock() - defer m.Unlock() - - entry, ok := m.keyConn[key] - if ok { - delete(m.keyConn, key) - return entry - } - return nil -} - // Add adds a new UDP NAT entry to the natmap and returns a closure to delete // the entry. func (m *natmap) Add(addr net.Addr, pc *natconn) func() { + m.Lock() + defer m.Unlock() + key := addr.String() - m.set(key, pc) + m.keyConn[key] = pc return func() { - m.del(key) + m.Lock() + defer m.Unlock() + delete(m.keyConn, key) } } From f5d9ac3a73cd97ea00a7c53a54ba68cd202db557 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 13 Dec 2024 12:56:59 -0500 Subject: [PATCH 23/80] Refactor `PacketServe` to use events (close and read). --- service/udp.go | 158 +++++++++++++++++++++++++++++--------------- service/udp_test.go | 52 +++++++-------- 2 files changed, 130 insertions(+), 80 deletions(-) diff --git a/service/udp.go b/service/udp.go index 57818a78..2431583f 100644 --- a/service/udp.go +++ b/service/udp.go @@ -48,6 +48,9 @@ type UDPAssocationMetrics interface { // Max UDP buffer size for the server code. const serverUDPBufferSize = 64 * 1024 +// Buffer pool used for reading UDP packets. +var readBufPool = slicepool.MakePool(serverUDPBufferSize) + // Wrapper for slog.Debug during UDP proxying. func debugUDP(l *slog.Logger, template string, cipherID string, attr slog.Attr) { // This is an optimization to reduce unnecessary allocations due to an interaction @@ -153,58 +156,97 @@ type AssocationHandleFunc func(assocation net.Conn) // function for each association. It uses a NAT map to track active associations // and handles their lifecycle. func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics NATMetrics) { + // This goroutine continuously reads from clientConn and sends the received data + // to readCh. It uses a buffer pool (readBufPool) to efficiently manage buffers + // and minimize allocations. The LazySlice is sent along with the read event + // to allow the receiver to release the buffer back to the pool after processing. + readCh := make(chan readEvent, 10) + go func() { + for { + lazySlice := readBufPool.LazySlice() + buffer := lazySlice.Acquire() + n, addr, err := clientConn.ReadFrom(buffer) + if err != nil { + lazySlice.Release() + if errors.Is(err, net.ErrClosed) { + readCh <- readEvent{err: err} + return + } + slog.Warn("Failed to read from client. Continuing to listen.", "err", err) + continue + } + readCh <- readEvent{ + poolSlice: lazySlice, + pkt: buffer[:n], + addr: addr, + } + } + }() + nm := newNATmap() defer nm.Close() - buffer := make([]byte, serverUDPBufferSize) + // This loop handles events from closeCh (connection closures) and readCh + // (incoming data). It removes NAT entries for closed connections and processes + // incoming data packets. The loop also ensures that buffers acquired from + // the readBufPool are released back to the pool after processing is complete. + closeCh := make(chan net.Addr, 10) for { - n, addr, err := clientConn.ReadFrom(buffer) - if err != nil { - if errors.Is(err, net.ErrClosed) { - break + select { + case addr := <-closeCh: + metrics.RemoveNATEntry() + nm.Del(addr) + case read := <-readCh: + if read.err != nil { + return } - slog.Warn("Failed to read from client. Continuing to listen.", "err", err) - continue - } - pkt := buffer[:n] - - // TODO: Include server address in the NAT key as well. - conn := nm.Get(addr.String()) - if conn == nil { - conn = &natconn{ - PacketConn: clientConn, - raddr: addr, - doneCh: make(chan struct{}), - readBufCh: make(chan []byte, 1), - bytesReadCh: make(chan int, 1), + + poolSlice := read.poolSlice + pkt := read.pkt + addr := read.addr + + // TODO: Include server address in the NAT key as well. + conn := nm.Get(addr.String()) + if conn == nil { + conn = &natconn{ + PacketConn: clientConn, + raddr: addr, + closeCh: closeCh, + doneCh: make(chan struct{}), + readBufCh: make(chan []byte, 1), + bytesReadCh: make(chan int, 1), + } + metrics.AddNATEntry() + nm.Add(addr, conn) + go handle(conn) } - metrics.AddNATEntry() - deleteEntry := nm.Add(addr, conn) - go func(conn net.Conn) { - defer func() { - conn.Close() - deleteEntry() - metrics.RemoveNATEntry() - }() - handle(conn) - }(conn) - } - readBuf, ok := <-conn.readBufCh - if !ok { - continue + readBuf, ok := <-conn.readBufCh + if !ok { + poolSlice.Release() + continue + } + copy(readBuf, pkt) + poolSlice.Release() + conn.bytesReadCh <- len(pkt) } - copy(readBuf, pkt) - conn.bytesReadCh <- n } } +type readEvent struct { + poolSlice slicepool.LazySlice + pkt []byte + addr net.Addr + err error +} + // natconn adapts a [net.Conn] to provide a synchronized reading mechanism for NAT traversal. // // The application provides the buffer to `Read()` (BYOB: Bring Your Own Buffer!) // which minimizes buffer allocations and copying. type natconn struct { net.PacketConn - raddr net.Addr - doneCh chan struct{} + raddr net.Addr + closeCh chan net.Addr + doneCh chan struct{} // readBufCh provides a buffer to copy incoming packet data into. readBufCh chan []byte @@ -218,14 +260,16 @@ var _ net.Conn = (*natconn)(nil) func (c *natconn) Read(p []byte) (int, error) { select { + case <-c.doneCh: + c.closeCh <- c.raddr + return 0, net.ErrClosed case c.readBufCh <- p: n, ok := <-c.bytesReadCh if !ok { + c.closeCh <- c.raddr return 0, net.ErrClosed } return n, nil - case <-c.doneCh: - return 0, net.ErrClosed } } @@ -235,9 +279,8 @@ func (c *natconn) Write(b []byte) (n int, err error) { func (c *natconn) Close() error { close(c.doneCh) - close(c.readBufCh) close(c.bytesReadCh) - return c.PacketConn.Close() + return nil } func (c *natconn) RemoteAddr() net.Addr { @@ -255,9 +298,9 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA return } - cipherLazySlice := h.bufPool.LazySlice() - cipherBuf := cipherLazySlice.Acquire() - defer cipherLazySlice.Release() + cipherSlice := h.bufPool.LazySlice() + cipherBuf := cipherSlice.Acquire() + defer cipherSlice.Release() textLazySlice := h.bufPool.LazySlice() @@ -266,7 +309,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA for { clientProxyBytes, err := clientAssociation.Read(cipherBuf) if errors.Is(err, net.ErrClosed) { - cipherLazySlice.Release() + cipherSlice.Release() return } debugUDPAddr(h.logger, "Outbound packet.", clientAssociation.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) @@ -301,9 +344,10 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA connMetrics.AddAuthenticated(keyID) go func() { - defer connMetrics.AddClosed() timedCopy(clientAssociation, targetConn, cryptoKey, connMetrics, h.logger) + connMetrics.AddClosed() targetConn.Close() + clientAssociation.Close() }() } else { @@ -438,19 +482,25 @@ func (m *natmap) Get(key string) *natconn { return m.keyConn[key] } +func (m *natmap) Del(addr net.Addr) net.PacketConn { + m.Lock() + defer m.Unlock() + + entry, ok := m.keyConn[addr.String()] + if ok { + delete(m.keyConn, addr.String()) + return entry + } + return nil +} + // Add adds a new UDP NAT entry to the natmap and returns a closure to delete // the entry. -func (m *natmap) Add(addr net.Addr, pc *natconn) func() { +func (m *natmap) Add(addr net.Addr, pc *natconn) { m.Lock() defer m.Unlock() - key := addr.String() - m.keyConn[key] = pc - return func() { - m.Lock() - defer m.Unlock() - delete(m.keyConn, key) - } + m.keyConn[addr.String()] = pc } func (m *natmap) Close() error { diff --git a/service/udp_test.go b/service/udp_test.go index 6bba5f0b..8a7eb043 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -480,56 +480,56 @@ func TestTimedPacketConn(t *testing.T) { func TestNATMap(t *testing.T) { t.Run("Empty", func(t *testing.T) { - nat := newNATmap() - if nat.Get("foo") != nil { + nm := newNATmap() + if nm.Get("foo") != nil { t.Error("Expected nil value from empty NAT map") } }) t.Run("Add", func(t *testing.T) { - nat := newNATmap() - addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + nm := newNATmap() + addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} conn1 := &natconn{} - nat.Add(addr1, conn1) - assert.Equal(t, conn1, nat.Get(addr1.String()), "Get should return the correct connection") + nm.Add(addr, conn1) + assert.Equal(t, conn1, nm.Get(addr.String()), "Get should return the correct connection") conn2 := &natconn{} - nat.Add(addr1, conn2) - assert.Equal(t, conn2, nat.Get(addr1.String()), "Adding with the same address should overwrite the entry") + nm.Add(addr, conn2) + assert.Equal(t, conn2, nm.Get(addr.String()), "Adding with the same address should overwrite the entry") }) t.Run("Get", func(t *testing.T) { - nat := newNATmap() - addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} - conn1 := &natconn{} - nat.Add(addr1, conn1) + nm := newNATmap() + addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + conn := &natconn{} + nm.Add(addr, conn) - assert.Equal(t, conn1, nat.Get(addr1.String()), "Get should return the correct connection for an existing address") + assert.Equal(t, conn, nm.Get(addr.String()), "Get should return the correct connection for an existing address") addr2 := &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 5678} - assert.Nil(t, nat.Get(addr2.String()), "Get should return nil for a non-existent address") + assert.Nil(t, nm.Get(addr2.String()), "Get should return nil for a non-existent address") }) - t.Run("closure_deletes", func(t *testing.T) { - nat := newNATmap() - addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} - conn1 := &natconn{} - deleteEntry := nat.Add(addr1, conn1) + t.Run("Del", func(t *testing.T) { + nm := newNATmap() + addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + conn := &natconn{} + nm.Add(addr, conn) - deleteEntry() + nm.Del(addr) - assert.Nil(t, nat.Get(addr1.String()), "Get should return nil after deleting the entry") + assert.Nil(t, nm.Get(addr.String()), "Get should return nil after deleting the entry") }) t.Run("Close", func(t *testing.T) { - nat := newNATmap() - addr1 := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} + nm := newNATmap() + addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} pc := makePacketConn() - conn1 := &natconn{PacketConn: pc, raddr: addr1} - nat.Add(addr1, conn1) + conn := &natconn{PacketConn: pc, raddr: addr} + nm.Add(addr, conn) - err := nat.Close() + err := nm.Close() assert.NoError(t, err, "Close should not return an error") // The underlying connection should be scheduled to close immediately. From e0547f28a46f1f1e6fac0cb6d1f5ddb89d1fcf5f Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 13 Dec 2024 13:37:33 -0500 Subject: [PATCH 24/80] Use correct logger. --- service/udp.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/udp.go b/service/udp.go index 2431583f..6ca1cd2c 100644 --- a/service/udp.go +++ b/service/udp.go @@ -294,7 +294,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA targetConn, err := h.targetConnFactory() if err != nil { - slog.Error("UDP: failed to create target connection", slog.Any("err", err)) + h.logger.Error("UDP: failed to create target connection", slog.Any("err", err)) return } @@ -317,10 +317,10 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA connError := func() *onet.ConnectionError { defer func() { if r := recover(); r != nil { - slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) + h.logger.Error("Panic in UDP loop. Continuing to listen.", "err", r) debug.PrintStack() } - slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientAssociation.RemoteAddr().String())) + h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientAssociation.RemoteAddr().String())) }() cipherData := cipherBuf[:clientProxyBytes] From 3dc12d10e83c936b248265951a13ba3cc554a39e Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 16 Dec 2024 15:47:13 -0500 Subject: [PATCH 25/80] Keep `readCh` and `closeCh` unbuffered. --- service/udp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/udp.go b/service/udp.go index 6ca1cd2c..60beed59 100644 --- a/service/udp.go +++ b/service/udp.go @@ -160,7 +160,7 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics // to readCh. It uses a buffer pool (readBufPool) to efficiently manage buffers // and minimize allocations. The LazySlice is sent along with the read event // to allow the receiver to release the buffer back to the pool after processing. - readCh := make(chan readEvent, 10) + readCh := make(chan readEvent) go func() { for { lazySlice := readBufPool.LazySlice() @@ -189,7 +189,7 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics // (incoming data). It removes NAT entries for closed connections and processes // incoming data packets. The loop also ensures that buffers acquired from // the readBufPool are released back to the pool after processing is complete. - closeCh := make(chan net.Addr, 10) + closeCh := make(chan net.Addr) for { select { case addr := <-closeCh: From afb9cd10b497071dfc5fcdd7a63b5b4f3de8f7c2 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 16 Dec 2024 15:54:24 -0500 Subject: [PATCH 26/80] Catch panics in the `ReadFrom` go routine. --- service/udp.go | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/service/udp.go b/service/udp.go index 60beed59..3e5c7854 100644 --- a/service/udp.go +++ b/service/udp.go @@ -165,21 +165,30 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics for { lazySlice := readBufPool.LazySlice() buffer := lazySlice.Acquire() - n, addr, err := clientConn.ReadFrom(buffer) - if err != nil { - lazySlice.Release() - if errors.Is(err, net.ErrClosed) { - readCh <- readEvent{err: err} + + func() { + defer func() { + if r := recover(); r != nil { + slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) + lazySlice.Release() + } + }() + n, addr, err := clientConn.ReadFrom(buffer) + if err != nil { + lazySlice.Release() + if errors.Is(err, net.ErrClosed) { + readCh <- readEvent{err: err} + return + } + slog.Warn("Failed to read from client. Continuing to listen.", "err", err) return } - slog.Warn("Failed to read from client. Continuing to listen.", "err", err) - continue - } - readCh <- readEvent{ - poolSlice: lazySlice, - pkt: buffer[:n], - addr: addr, - } + readCh <- readEvent{ + poolSlice: lazySlice, + pkt: buffer[:n], + addr: addr, + } + }() } }() From 5d2acc6c8164da72cfe917a1255ef4d652c1867f Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 16 Dec 2024 16:03:22 -0500 Subject: [PATCH 27/80] Close the `readCh` instead of sending the error on the `readCh`. --- service/udp.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/service/udp.go b/service/udp.go index 3e5c7854..2ac0a5eb 100644 --- a/service/udp.go +++ b/service/udp.go @@ -177,7 +177,7 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics if err != nil { lazySlice.Release() if errors.Is(err, net.ErrClosed) { - readCh <- readEvent{err: err} + close(readCh) return } slog.Warn("Failed to read from client. Continuing to listen.", "err", err) @@ -204,8 +204,8 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics case addr := <-closeCh: metrics.RemoveNATEntry() nm.Del(addr) - case read := <-readCh: - if read.err != nil { + case read, ok := <-readCh: + if !ok { return } @@ -244,7 +244,6 @@ type readEvent struct { poolSlice slicepool.LazySlice pkt []byte addr net.Addr - err error } // natconn adapts a [net.Conn] to provide a synchronized reading mechanism for NAT traversal. From 7cd0f1fc2a3835f7271d4b42b2e69f21bcce176d Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 16 Dec 2024 16:46:59 -0500 Subject: [PATCH 28/80] Wrap a logger with the association's client address so we can simplify the `timedCopy()` signature. --- service/udp.go | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/service/udp.go b/service/udp.go index 2ac0a5eb..ecfab4d5 100644 --- a/service/udp.go +++ b/service/udp.go @@ -17,6 +17,7 @@ package service import ( "errors" "fmt" + "io" "log/slog" "net" "net/netip" @@ -52,17 +53,11 @@ const serverUDPBufferSize = 64 * 1024 var readBufPool = slicepool.MakePool(serverUDPBufferSize) // Wrapper for slog.Debug during UDP proxying. -func debugUDP(l *slog.Logger, template string, cipherID string, attr slog.Attr) { +func debugUDP(l *slog.Logger, template string, attrs ...slog.Attr) { // This is an optimization to reduce unnecessary allocations due to an interaction // between Go's inlining/escape analysis and varargs functions like slog.Debug. if l.Enabled(nil, slog.LevelDebug) { - l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("UDP: %s", template), slog.String("ID", cipherID), attr) - } -} - -func debugUDPAddr(l *slog.Logger, template string, addr net.Addr, attr slog.Attr) { - if l.Enabled(nil, slog.LevelDebug) { - l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("UDP: %s", template), slog.String("address", addr.String()), attr) + l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("UDP: %s", template), attrs...) } } @@ -76,10 +71,10 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis id, cryptoKey := entry.Value.(*CipherEntry).ID, entry.Value.(*CipherEntry).CryptoKey buf, err := shadowsocks.Unpack(dst, src, cryptoKey) if err != nil { - debugUDP(l, "Failed to unpack.", id, slog.Any("err", err)) + debugUDP(l, "Failed to unpack.", slog.String("ID", id), slog.Any("err", err)) continue } - debugUDP(l, "Found cipher.", id, slog.Int("index", ci)) + debugUDP(l, "Found cipher.", slog.String("ID", id), slog.Int("index", ci)) // Move the active cipher to the front, so that the search is quicker next time. cipherList.MarkUsedByClientIP(entry, clientIP) return buf, id, cryptoKey, nil @@ -300,9 +295,11 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA connMetrics = &NoOpUDPAssocationMetrics{} } + clientLogger := h.logger.With(slog.String("client", clientAssociation.RemoteAddr().String())) + targetConn, err := h.targetConnFactory() if err != nil { - h.logger.Error("UDP: failed to create target connection", slog.Any("err", err)) + clientLogger.Error("UDP: failed to create target connection", slog.Any("err", err)) return } @@ -320,15 +317,15 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA cipherSlice.Release() return } - debugUDPAddr(h.logger, "Outbound packet.", clientAssociation.RemoteAddr(), slog.Int("bytes", clientProxyBytes)) + debugUDP(clientLogger, "Outbound packet.", slog.Int("bytes", clientProxyBytes)) connError := func() *onet.ConnectionError { defer func() { if r := recover(); r != nil { - h.logger.Error("Panic in UDP loop. Continuing to listen.", "err", r) + clientLogger.Error("Panic in UDP loop. Continuing to listen.", "err", r) debug.PrintStack() } - h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientAssociation.RemoteAddr().String())) + debugUDP(clientLogger, "Done") }() cipherData := cipherBuf[:clientProxyBytes] @@ -341,7 +338,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA var cryptoKey *shadowsocks.EncryptionKey textBuf := textLazySlice.Acquire() unpackStart := time.Now() - textData, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, cipherData, h.ciphers, h.logger) + textData, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, cipherData, h.ciphers, clientLogger) timeToCipher := time.Since(unpackStart) textLazySlice.Release() h.ssm.AddCipherSearch(err == nil, timeToCipher) @@ -352,7 +349,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA connMetrics.AddAuthenticated(keyID) go func() { - timedCopy(clientAssociation, targetConn, cryptoKey, connMetrics, h.logger) + timedCopy(clientAssociation, targetConn, cryptoKey, connMetrics, clientLogger) connMetrics.AddClosed() targetConn.Close() clientAssociation.Close() @@ -374,7 +371,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA return onetErr } - debugUDPAddr(h.logger, "Proxy exit.", clientAssociation.RemoteAddr(), slog.Any("target", targetConn.LocalAddr())) + debugUDP(clientLogger, "Proxy exit.", slog.Any("target", targetConn.LocalAddr())) proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) @@ -384,7 +381,7 @@ func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPA status := "OK" if connError != nil { - h.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + clientLogger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } connMetrics.AddPacketFromClient(status, int64(clientProxyBytes), int64(proxyTargetBytes)) @@ -530,7 +527,7 @@ func (m *natmap) Close() error { var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) // copy from target to client until read timeout -func timedCopy(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPAssocationMetrics, l *slog.Logger) { +func timedCopy(clientConn io.Writer, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPAssocationMetrics, l *slog.Logger) { // pkt is used for in-place encryption of downstream UDP packets, with the layout // [padding?][salt][address][body][tag][extra] // Padding is only used if the address is IPv4. @@ -563,7 +560,7 @@ func timedCopy(clientConn net.Conn, targetConn net.PacketConn, cryptoKey *shadow return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) } - debugUDPAddr(l, "Got response.", clientConn.RemoteAddr(), slog.Any("target", raddr)) + debugUDP(l, "Got response.", slog.Any("target", raddr)) srcAddr := socks.ParseAddr(raddr.String()) addrStart := bodyStart - len(srcAddr) // `plainTextBuf` concatenates the SOCKS address and body: From 078032cbb4a825bd4eb73df25ce996fdac9a1d08 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 16 Dec 2024 16:54:35 -0500 Subject: [PATCH 29/80] Reference GitHub issue for supporting multiple IPs. --- service/udp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index ecfab4d5..95ae313f 100644 --- a/service/udp.go +++ b/service/udp.go @@ -208,7 +208,7 @@ func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics pkt := read.pkt addr := read.addr - // TODO: Include server address in the NAT key as well. + // TODO(#19): Include server address in the NAT key as well. conn := nm.Get(addr.String()) if conn == nil { conn = &natconn{ From 2b3f74634899d4fd28fad7d236dbb6a44dec2972 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 18 Dec 2024 16:30:50 -0500 Subject: [PATCH 30/80] Simplify packet handling with a new `association` struct. --- caddy/shadowsocks_handler.go | 24 +- cmd/outline-ss-server/main.go | 4 +- internal/integration_test/integration_test.go | 23 +- service/shadowsocks.go | 12 +- service/udp.go | 486 ++++++++---------- service/udp_test.go | 69 +-- 6 files changed, 305 insertions(+), 313 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 8191e327..5d98d23e 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -16,17 +16,22 @@ package caddy import ( "container/list" + "errors" "fmt" "log/slog" "net" "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" - outline "github.com/Jigsaw-Code/outline-ss-server/service" "github.com/caddyserver/caddy/v2" "github.com/mholt/caddy-l4/layer4" + + "github.com/Jigsaw-Code/outline-ss-server/internal/slicepool" + outline "github.com/Jigsaw-Code/outline-ss-server/service" ) +const serverUDPBufferSize = 64 * 1024 + const ssModuleName = "layer4.handlers.shadowsocks" func init() { @@ -116,7 +121,22 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.service.HandleStream(cx.Context, conn) case net.Conn: - h.service.HandleAssociation(cx) + assoc, err := h.service.NewAssociation(conn) + if err != nil { + return fmt.Errorf("Failed to handle association: %v", err) + } + bufPool := slicepool.MakePool(serverUDPBufferSize) + for { + lazySlice := bufPool.LazySlice() + buf := lazySlice.Acquire() + n, err := conn.Read(buf) + if errors.Is(err, net.ErrClosed) { + lazySlice.Release() + return err + } + pkt := buf[:n] + go assoc.HandlePacket(pkt, lazySlice) + } default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index 35ab2a93..b497a0b0 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -238,7 +238,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.HandleAssociation, s.serverMetrics) + go service.PacketServe(pc, ssService.NewAssociation, s.serverMetrics) } for _, serviceConfig := range config.Services { @@ -271,7 +271,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.HandleAssociation, s.serverMetrics) + go service.PacketServe(pc, ssService.NewAssociation, s.serverMetrics) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 208b8f4c..5b2653b1 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -318,16 +318,14 @@ func TestUDPEcho(t *testing.T) { if err != nil { t.Fatal(err) } - proxy := service.NewAssociationHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) + proxy := service.NewPacketHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) - done := make(chan struct{}) natMetrics := &natTestMetrics{} associationMetrics := &fakeUDPAssocationMetrics{} - go func() { - service.PacketServe(proxyConn, func(conn net.Conn) { proxy.Handle(conn, associationMetrics) }, natMetrics) - done <- struct{}{} - }() + go service.PacketServe(proxyConn, func(conn net.Conn) (service.Association, error) { + return proxy.NewAssociation(conn, associationMetrics) + }, natMetrics) cryptoKey, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, secrets[0]) require.NoError(t, err) @@ -366,7 +364,6 @@ func TestUDPEcho(t *testing.T) { echoConn.Close() echoRunning.Wait() proxyConn.Close() - <-done // Verify that the expected metrics were reported. snapshot := cipherList.SnapshotForClientIP(netip.Addr{}) keyID := snapshot[0].Value.(*service.CipherEntry).ID @@ -549,11 +546,13 @@ func BenchmarkUDPEcho(b *testing.B) { if err != nil { b.Fatal(err) } - proxy := service.NewAssociationHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) + proxy := service.NewPacketHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(server, func(conn net.Conn) { proxy.Handle(conn, &service.NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + service.PacketServe(server, func(conn net.Conn) (service.Association, error) { + return proxy.NewAssociation(conn, nil) + }, &natTestMetrics{}) done <- struct{}{} }() @@ -593,11 +592,13 @@ func BenchmarkUDPManyKeys(b *testing.B) { if err != nil { b.Fatal(err) } - proxy := service.NewAssociationHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) + proxy := service.NewPacketHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(proxyConn, func(conn net.Conn) { proxy.Handle(conn, &service.NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + service.PacketServe(proxyConn, func(conn net.Conn) (service.Association, error) { + return proxy.NewAssociation(conn, nil) + }, &natTestMetrics{}) done <- struct{}{} }() diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 32981226..a923d172 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -44,7 +44,7 @@ type ServiceMetrics interface { type Service interface { HandleStream(ctx context.Context, conn transport.StreamConn) - HandleAssociation(conn net.Conn) + NewAssociation(conn net.Conn) (Association, error) } // Option is a Shadowsocks service constructor option. @@ -58,7 +58,7 @@ type ssService struct { replayCache *ReplayCache sh StreamHandler - ah AssociationHandler + ph PacketHandler } // NewShadowsocksService creates a new Shadowsocks service. @@ -85,8 +85,8 @@ func NewShadowsocksService(opts ...Option) (Service, error) { ) s.sh.SetLogger(s.logger) - s.ah = NewAssociationHandler(s.natTimeout, s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"}) - s.ah.SetLogger(s.logger) + s.ph = NewPacketHandler(s.natTimeout, s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"}) + s.ph.SetLogger(s.logger) return s, nil } @@ -137,12 +137,12 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) } // HandleAssociation handles a Shadowsocks packet-based assocation. -func (s *ssService) HandleAssociation(conn net.Conn) { +func (s *ssService) NewAssociation(conn net.Conn) (Association, error) { var metrics UDPAssocationMetrics if s.metrics != nil { metrics = s.metrics.AddOpenUDPAssociation(conn) } - s.ah.Handle(conn, metrics) + return s.ph.NewAssociation(conn, metrics) } type ssConnMetrics struct { diff --git a/service/udp.go b/service/udp.go index 95ae313f..c91e01a8 100644 --- a/service/udp.go +++ b/service/udp.go @@ -17,7 +17,6 @@ package service import ( "errors" "fmt" - "io" "log/slog" "net" "net/netip" @@ -82,7 +81,7 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis return nil, "", nil, errors.New("could not find valid UDP cipher") } -type associationHandler struct { +type packetHandler struct { logger *slog.Logger // bufPool stores the byte slices used for reading and decrypting packets. bufPool slicepool.Pool @@ -92,14 +91,14 @@ type associationHandler struct { targetConnFactory func() (net.PacketConn, error) } -var _ AssociationHandler = (*associationHandler)(nil) +var _ PacketHandler = (*packetHandler)(nil) -// NewAssociationHandler creates an AssociationHandler -func NewAssociationHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics ShadowsocksConnMetrics) AssociationHandler { +// NewPacketHandler creates an PacketHandler +func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, ssMetrics ShadowsocksConnMetrics) PacketHandler { if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } - return &associationHandler{ + return &packetHandler{ logger: noopLogger(), bufPool: slicepool.MakePool(serverUDPBufferSize), ciphers: cipherList, @@ -118,297 +117,137 @@ func NewAssociationHandler(natTimeout time.Duration, cipherList CipherList, ssMe } } -// AssociationHandler is a handler that handles UDP assocations. -type AssociationHandler interface { - Handle(association net.Conn, metrics UDPAssocationMetrics) +// PacketHandler is a handler that handles UDP assocations. +type PacketHandler interface { // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) // SetTargetConnFactory sets the function to be used to create new target connections. SetTargetConnFactory(factory func() (net.PacketConn, error)) + // NewAssociation creates a new Association instance. + NewAssociation(conn net.Conn, connMetrics UDPAssocationMetrics) (Association, error) } -func (h *associationHandler) SetLogger(l *slog.Logger) { +func (h *packetHandler) SetLogger(l *slog.Logger) { if l == nil { l = noopLogger() } h.logger = l } -func (h *associationHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { +func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { h.targetIPValidator = targetIPValidator } -func (h *associationHandler) SetTargetConnFactory(factory func() (net.PacketConn, error)) { +func (h *packetHandler) SetTargetConnFactory(factory func() (net.PacketConn, error)) { h.targetConnFactory = factory } -type AssocationHandleFunc func(assocation net.Conn) +func (h *packetHandler) NewAssociation(conn net.Conn, m UDPAssocationMetrics) (Association, error) { + if m == nil { + m = &NoOpUDPAssocationMetrics{} + } + + // Create the target connection + targetConn, err := h.targetConnFactory() + if err != nil { + return nil, fmt.Errorf("failed to create target connection: %w", err) + } + + return &association{ + Conn: conn, + m: m, + targetConn: targetConn, + logger: h.logger.With(slog.Any("client", conn.RemoteAddr()), slog.Any("ltarget", targetConn.LocalAddr())), + bufPool: &h.bufPool, + ciphers: h.ciphers, + ssm: h.ssm, + targetIPValidator: h.targetIPValidator, + doneCh: make(chan struct{}), + }, nil +} + + +type NewAssociationFunc func(conn net.Conn) (Association, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates // creates and manages NAT associations, and invokes the provided `handle` // function for each association. It uses a NAT map to track active associations // and handles their lifecycle. -func PacketServe(clientConn net.PacketConn, handle AssocationHandleFunc, metrics NATMetrics) { - // This goroutine continuously reads from clientConn and sends the received data - // to readCh. It uses a buffer pool (readBufPool) to efficiently manage buffers - // and minimize allocations. The LazySlice is sent along with the read event - // to allow the receiver to release the buffer back to the pool after processing. - readCh := make(chan readEvent) - go func() { - for { - lazySlice := readBufPool.LazySlice() - buffer := lazySlice.Acquire() - - func() { - defer func() { - if r := recover(); r != nil { - slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) - lazySlice.Release() - } - }() - n, addr, err := clientConn.ReadFrom(buffer) - if err != nil { - lazySlice.Release() - if errors.Is(err, net.ErrClosed) { - close(readCh) - return - } - slog.Warn("Failed to read from client. Continuing to listen.", "err", err) - return - } - readCh <- readEvent{ - poolSlice: lazySlice, - pkt: buffer[:n], - addr: addr, - } - }() - } - }() - +func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, metrics NATMetrics) { nm := newNATmap() defer nm.Close() - // This loop handles events from closeCh (connection closures) and readCh - // (incoming data). It removes NAT entries for closed connections and processes - // incoming data packets. The loop also ensures that buffers acquired from - // the readBufPool are released back to the pool after processing is complete. - closeCh := make(chan net.Addr) + for { - select { - case addr := <-closeCh: - metrics.RemoveNATEntry() - nm.Del(addr) - case read, ok := <-readCh: - if !ok { + lazySlice := readBufPool.LazySlice() + buffer := lazySlice.Acquire() + + func() { + defer func() { + if r := recover(); r != nil { + slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) + debug.PrintStack() + lazySlice.Release() + } + }() + n, addr, err := clientConn.ReadFrom(buffer) + if err != nil { + lazySlice.Release() + if errors.Is(err, net.ErrClosed) { + return + } + slog.Warn("Failed to read from client. Continuing to listen.", "err", err) return } - - poolSlice := read.poolSlice - pkt := read.pkt - addr := read.addr + pkt := buffer[:n] // TODO(#19): Include server address in the NAT key as well. - conn := nm.Get(addr.String()) - if conn == nil { - conn = &natconn{ - PacketConn: clientConn, - raddr: addr, - closeCh: closeCh, - doneCh: make(chan struct{}), - readBufCh: make(chan []byte, 1), - bytesReadCh: make(chan int, 1), + assoc := nm.Get(addr.String()) + if assoc == nil { + conn := &natconn{PacketConn: clientConn, raddr: addr} + assoc, err = newAssociation(conn) + if err != nil { + slog.Error("Failed to handle association", slog.Any("err", err)) + return } + metrics.AddNATEntry() - nm.Add(addr, conn) - go handle(conn) + nm.Add(addr, assoc) } - readBuf, ok := <-conn.readBufCh - if !ok { - poolSlice.Release() - continue + select { + case <-assoc.Done(): + lazySlice.Release() + metrics.RemoveNATEntry() + nm.Del(addr) + default: + go assoc.HandlePacket(pkt, lazySlice) } - copy(readBuf, pkt) - poolSlice.Release() - conn.bytesReadCh <- len(pkt) - } + }() } } -type readEvent struct { - poolSlice slicepool.LazySlice - pkt []byte - addr net.Addr -} - -// natconn adapts a [net.Conn] to provide a synchronized reading mechanism for NAT traversal. -// -// The application provides the buffer to `Read()` (BYOB: Bring Your Own Buffer!) -// which minimizes buffer allocations and copying. +// natconn wraps a [net.PacketConn] with an address into a [net.Conn]. type natconn struct { net.PacketConn - raddr net.Addr - closeCh chan net.Addr - doneCh chan struct{} - - // readBufCh provides a buffer to copy incoming packet data into. - readBufCh chan []byte - - // bytesReadCh is used to signal the availability of new data and carries - // the length of the received packet. - bytesReadCh chan int + raddr net.Addr } var _ net.Conn = (*natconn)(nil) func (c *natconn) Read(p []byte) (int, error) { - select { - case <-c.doneCh: - c.closeCh <- c.raddr - return 0, net.ErrClosed - case c.readBufCh <- p: - n, ok := <-c.bytesReadCh - if !ok { - c.closeCh <- c.raddr - return 0, net.ErrClosed - } - return n, nil - } + n, _, err := c.PacketConn.ReadFrom(p) + return n, err } func (c *natconn) Write(b []byte) (n int, err error) { return c.PacketConn.WriteTo(b, c.raddr) } -func (c *natconn) Close() error { - close(c.doneCh) - close(c.bytesReadCh) - return nil -} - func (c *natconn) RemoteAddr() net.Addr { return c.raddr } -func (h *associationHandler) Handle(clientAssociation net.Conn, connMetrics UDPAssocationMetrics) { - if connMetrics == nil { - connMetrics = &NoOpUDPAssocationMetrics{} - } - - clientLogger := h.logger.With(slog.String("client", clientAssociation.RemoteAddr().String())) - - targetConn, err := h.targetConnFactory() - if err != nil { - clientLogger.Error("UDP: failed to create target connection", slog.Any("err", err)) - return - } - - cipherSlice := h.bufPool.LazySlice() - cipherBuf := cipherSlice.Acquire() - defer cipherSlice.Release() - - textLazySlice := h.bufPool.LazySlice() - - var cryptoKey *shadowsocks.EncryptionKey - var proxyTargetBytes int - for { - clientProxyBytes, err := clientAssociation.Read(cipherBuf) - if errors.Is(err, net.ErrClosed) { - cipherSlice.Release() - return - } - debugUDP(clientLogger, "Outbound packet.", slog.Int("bytes", clientProxyBytes)) - - connError := func() *onet.ConnectionError { - defer func() { - if r := recover(); r != nil { - clientLogger.Error("Panic in UDP loop. Continuing to listen.", "err", r) - debug.PrintStack() - } - debugUDP(clientLogger, "Done") - }() - - cipherData := cipherBuf[:clientProxyBytes] - var textData []byte - var err error - - if cryptoKey == nil { - ip := clientAssociation.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() - var keyID string - var cryptoKey *shadowsocks.EncryptionKey - textBuf := textLazySlice.Acquire() - unpackStart := time.Now() - textData, keyID, cryptoKey, err = findAccessKeyUDP(ip, textBuf, cipherData, h.ciphers, clientLogger) - timeToCipher := time.Since(unpackStart) - textLazySlice.Release() - h.ssm.AddCipherSearch(err == nil, timeToCipher) - - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) - } - - connMetrics.AddAuthenticated(keyID) - go func() { - timedCopy(clientAssociation, targetConn, cryptoKey, connMetrics, clientLogger) - connMetrics.AddClosed() - targetConn.Close() - clientAssociation.Close() - }() - - } else { - unpackStart := time.Now() - textData, err = shadowsocks.Unpack(nil, cipherData, cryptoKey) - timeToCipher := time.Since(unpackStart) - h.ssm.AddCipherSearch(err == nil, timeToCipher) - - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) - } - } - - payload, tgtUDPAddr, onetErr := h.validatePacket(textData) - if onetErr != nil { - return onetErr - } - - debugUDP(clientLogger, "Proxy exit.", slog.Any("target", targetConn.LocalAddr())) - proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature - if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) - } - return nil - }() - - status := "OK" - if connError != nil { - clientLogger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) - status = connError.Status - } - connMetrics.AddPacketFromClient(status, int64(clientProxyBytes), int64(proxyTargetBytes)) - } -} - -// Given the decrypted contents of a UDP packet, return -// the payload and the destination address, or an error if -// this packet cannot or should not be forwarded. -func (h *associationHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { - tgtAddr := socks.SplitAddr(textData) - if tgtAddr == nil { - return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) - } - - tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String()) - if err != nil { - return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err) - } - if err := h.targetIPValidator(tgtUDPAddr.IP); err != nil { - return nil, nil, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address") - } - - payload := textData[len(tgtAddr):] - return payload, tgtUDPAddr, nil -} - func isDNS(addr net.Addr) bool { _, port, _ := net.SplitHostPort(addr.String()) return port == "53" @@ -474,38 +313,35 @@ func (c *timedPacketConn) ReadFrom(buf []byte) (int, net.Addr, error) { // Packet NAT table type natmap struct { sync.RWMutex - keyConn map[string]*natconn + keyConn map[string]Association } func newNATmap() *natmap { - return &natmap{keyConn: make(map[string]*natconn)} + return &natmap{keyConn: make(map[string]Association)} } -func (m *natmap) Get(key string) *natconn { +func (m *natmap) Get(key string) Association { m.RLock() defer m.RUnlock() return m.keyConn[key] } -func (m *natmap) Del(addr net.Addr) net.PacketConn { +func (m *natmap) Del(addr net.Addr) { m.Lock() defer m.Unlock() - entry, ok := m.keyConn[addr.String()] - if ok { + if _, ok := m.keyConn[addr.String()]; ok { delete(m.keyConn, addr.String()) - return entry } - return nil } // Add adds a new UDP NAT entry to the natmap and returns a closure to delete // the entry. -func (m *natmap) Add(addr net.Addr, pc *natconn) { +func (m *natmap) Add(addr net.Addr, assoc Association) { m.Lock() defer m.Unlock() - m.keyConn[addr.String()] = pc + m.keyConn[addr.String()] = assoc } func (m *natmap) Close() error { @@ -513,27 +349,153 @@ func (m *natmap) Close() error { defer m.Unlock() var err error - now := time.Now() - for _, pc := range m.keyConn { - if e := pc.SetReadDeadline(now); e != nil { + for _, assoc := range m.keyConn { + if e := assoc.Close(); e != nil { err = e } } return err } +type Association interface { + HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) + Done() <-chan struct{} + Close() error +} + +type association struct { + net.Conn + raddr net.UDPAddr + m UDPAssocationMetrics + logger *slog.Logger + targetConn net.PacketConn + cryptoKey *shadowsocks.EncryptionKey + bufPool *slicepool.Pool + ciphers CipherList + ssm ShadowsocksConnMetrics + targetIPValidator onet.TargetIPValidator + doneCh chan struct{} + findAccessKeyOnce sync.Once +} + +var _ Association = (*association)(nil) + +// Given the decrypted contents of a UDP packet, return +// the payload and the destination address, or an error if +// this packet cannot or should not be forwarded. +func (a *association) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { + tgtAddr := socks.SplitAddr(textData) + if tgtAddr == nil { + return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) + } + + tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String()) + if err != nil { + return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err) + } + if err := a.targetIPValidator(tgtUDPAddr.IP); err != nil { + return nil, nil, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address") + } + + payload := textData[len(tgtAddr):] + return payload, tgtUDPAddr, nil +} + +func (a *association) HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) { + defer lazySlice.Release() + defer debugUDP(a.logger, "Done") + + debugUDP(a.logger, "Outbound packet.", slog.Int("bytes", len(pkt))) + + var proxyTargetBytes int + connError := func() *onet.ConnectionError { + var textData []byte + var err error + + a.findAccessKeyOnce.Do(func() { + ip := a.raddr.AddrPort().Addr() + var keyID string + textLazySlice := a.bufPool.LazySlice() + textBuf := textLazySlice.Acquire() + unpackStart := time.Now() + textData, keyID, a.cryptoKey, err = findAccessKeyUDP(ip, textBuf, pkt, a.ciphers, a.logger) + timeToCipher := time.Since(unpackStart) + textLazySlice.Release() + a.ssm.AddCipherSearch(err == nil, timeToCipher) + + if err != nil { + return + } + + a.m.AddAuthenticated(keyID) + go a.timedCopy() + }) + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) + } + + if a.cryptoKey == nil { + // This should not happen since findAccessKeyUDP should have set `a.cryptoKey`. + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) + } + + unpackStart := time.Now() + textData, err = shadowsocks.Unpack(nil, pkt, a.cryptoKey) + timeToCipher := time.Since(unpackStart) + a.ssm.AddCipherSearch(err == nil, timeToCipher) + + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) + } + + payload, tgtUDPAddr, onetErr := a.validatePacket(textData) + if onetErr != nil { + return onetErr + } + + debugUDP(a.logger, "Proxy exit.") + proxyTargetBytes, err = a.targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature + if err != nil { + return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + } + return nil + }() + + status := "OK" + if connError != nil { + a.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + status = connError.Status + } + a.m.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) +} + +func (a *association) Done() <-chan struct{} { + return a.doneCh +} + +func (a *association) Close() error { + now := time.Now() + return a.SetReadDeadline(now) +} + // Get the maximum length of the shadowsocks address header by parsing // and serializing an IPv6 address from the example range. var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) // copy from target to client until read timeout -func timedCopy(clientConn io.Writer, targetConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, m UDPAssocationMetrics, l *slog.Logger) { +func (a *association) timedCopy() { + defer func() { + a.m.AddClosed() + a.targetConn.Close() + close(a.doneCh) + }() + // pkt is used for in-place encryption of downstream UDP packets, with the layout // [padding?][salt][address][body][tag][extra] // Padding is only used if the address is IPv4. pkt := make([]byte, serverUDPBufferSize) - saltSize := cryptoKey.SaltSize() + saltSize := a.cryptoKey.SaltSize() // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). bodyStart := saltSize + maxAddrLen @@ -549,7 +511,7 @@ func timedCopy(clientConn io.Writer, targetConn net.PacketConn, cryptoKey *shado // [padding?][salt][address][body][tag][unused] // |-- bodyStart --|[ readBuf ] readBuf := pkt[bodyStart:] - bodyLen, raddr, err = targetConn.ReadFrom(readBuf) + bodyLen, raddr, err = a.targetConn.ReadFrom(readBuf) if err != nil { if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { @@ -560,7 +522,7 @@ func timedCopy(clientConn io.Writer, targetConn net.PacketConn, cryptoKey *shado return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) } - debugUDP(l, "Got response.", slog.Any("target", raddr)) + debugUDP(a.logger, "Got response.", slog.Any("rtarget", raddr)) srcAddr := socks.ParseAddr(raddr.String()) addrStart := bodyStart - len(srcAddr) // `plainTextBuf` concatenates the SOCKS address and body: @@ -577,11 +539,11 @@ func timedCopy(clientConn io.Writer, targetConn net.PacketConn, cryptoKey *shado // [ packBuf ] // [ buf ] packBuf := pkt[saltStart:] - buf, err := shadowsocks.Pack(packBuf, plaintextBuf, cryptoKey) // Encrypt in-place + buf, err := shadowsocks.Pack(packBuf, plaintextBuf, a.cryptoKey) // Encrypt in-place if err != nil { return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) } - proxyClientBytes, err = clientConn.Write(buf) + proxyClientBytes, err = a.Write(buf) if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) } @@ -589,13 +551,13 @@ func timedCopy(clientConn io.Writer, targetConn net.PacketConn, cryptoKey *shado }() status := "OK" if connError != nil { - l.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + a.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } if expired { break } - m.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) + a.m.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) } } diff --git a/service/udp_test.go b/service/udp_test.go index 8a7eb043..384f4f67 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -180,16 +180,18 @@ func sendSSPayload(conn *fakePacketConn, addr net.Addr, cipher *shadowsocks.Encr // startTestHandler creates a new association handler with a fake // client and target connection for testing purposes. It also starts a // PacketServe goroutine to handle incoming packets on the client connection. -func startTestHandler() (AssociationHandler, func(target net.Addr, payload []byte), *fakePacketConn) { +func startTestHandler() (PacketHandler, func(target net.Addr, payload []byte), *fakePacketConn) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewAssociationHandler(10*time.Second, ciphers, nil) + handler := NewPacketHandler(10*time.Second, ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetConnFactory(func() (net.PacketConn, error) { return targetConn, nil }) - go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + go PacketServe(clientConn, func(conn net.Conn) (Association, error) { + return handler.NewAssociation(conn, nil) + }, &natTestMetrics{}) return handler, func(target net.Addr, payload []byte) { sendSSPayload(clientConn, target, cipher, payload) }, targetConn @@ -199,9 +201,6 @@ func TestNatconnCloseWhileReading(t *testing.T) { nc := &natconn{ PacketConn: makePacketConn(), raddr: &clientAddr, - doneCh: make(chan struct{}), - readBufCh: make(chan []byte, 1), - bytesReadCh: make(chan int, 1), } go func() { buf := make([]byte, 1024) @@ -213,7 +212,7 @@ func TestNatconnCloseWhileReading(t *testing.T) { assert.NoError(t, err, "Close should not panic or return an error") } -func TestAssociationHandler_Handle_IPFilter(t *testing.T) { +func TestPacketHandler_Handle_IPFilter(t *testing.T) { t.Run("RequirePublicIP blocks localhost", func(t *testing.T) { handler, sendPayload, targetConn := startTestHandler() handler.SetTargetIPValidator(onet.RequirePublicIP) @@ -244,14 +243,16 @@ func TestAssociationHandler_Handle_IPFilter(t *testing.T) { func TestUpstreamMetrics(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewAssociationHandler(10*time.Second, ciphers, nil) + handler := NewPacketHandler(10*time.Second, ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetConnFactory(func() (net.PacketConn, error) { return targetConn, nil }) metrics := &fakeUDPAssocationMetrics{} - go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, metrics) }, &natTestMetrics{}) + go PacketServe(clientConn, func(conn net.Conn) (Association, error) { + return handler.NewAssociation(conn, metrics) + }, &natTestMetrics{}) // Test both the first-packet and subsequent-packet cases. const N = 10 @@ -375,13 +376,15 @@ func TestTimedPacketConn(t *testing.T) { t.Run("FastClose", func(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewAssociationHandler(10*time.Second, ciphers, nil) + handler := NewPacketHandler(10*time.Second, ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetConnFactory(func() (net.PacketConn, error) { return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil }) - go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + go PacketServe(clientConn, func(conn net.Conn) (Association, error) { + return handler.NewAssociation(conn, nil) + }, &natTestMetrics{}) // Send one DNS query. sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) @@ -403,13 +406,15 @@ func TestTimedPacketConn(t *testing.T) { t.Run("NoFastClose_NotDNS", func(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewAssociationHandler(10*time.Second, ciphers, nil) + handler := NewPacketHandler(10*time.Second, ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetConnFactory(func() (net.PacketConn, error) { return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil }) - go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + go PacketServe(clientConn, func(conn net.Conn) (Association, error) { + return handler.NewAssociation(conn, nil) + }, &natTestMetrics{}) // Send one non-DNS packet. sendSSPayload(clientConn, &targetAddr, cipher, []byte{1}) @@ -431,13 +436,15 @@ func TestTimedPacketConn(t *testing.T) { t.Run("NoFastClose_MultipleDNS", func(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewAssociationHandler(10*time.Second, ciphers, nil) + handler := NewPacketHandler(10*time.Second, ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetConnFactory(func() (net.PacketConn, error) { return &timedPacketConn{PacketConn: targetConn, defaultTimeout: timeout}, nil }) - go PacketServe(clientConn, func(conn net.Conn) { handler.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + go PacketServe(clientConn, func(conn net.Conn) (Association, error) { + return handler.NewAssociation(conn, nil) + }, &natTestMetrics{}) // Send two DNS packets. sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) @@ -489,23 +496,23 @@ func TestNATMap(t *testing.T) { t.Run("Add", func(t *testing.T) { nm := newNATmap() addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} - conn1 := &natconn{} + assoc1 := &association{} - nm.Add(addr, conn1) - assert.Equal(t, conn1, nm.Get(addr.String()), "Get should return the correct connection") + nm.Add(addr, assoc1) + assert.Equal(t, assoc1, nm.Get(addr.String()), "Get should return the correct connection") - conn2 := &natconn{} - nm.Add(addr, conn2) - assert.Equal(t, conn2, nm.Get(addr.String()), "Adding with the same address should overwrite the entry") + assoc2 := &association{} + nm.Add(addr, assoc2) + assert.Equal(t, assoc2, nm.Get(addr.String()), "Adding with the same address should overwrite the entry") }) t.Run("Get", func(t *testing.T) { nm := newNATmap() addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} - conn := &natconn{} - nm.Add(addr, conn) + assoc := &association{} + nm.Add(addr, assoc) - assert.Equal(t, conn, nm.Get(addr.String()), "Get should return the correct connection for an existing address") + assert.Equal(t, assoc, nm.Get(addr.String()), "Get should return the correct connection for an existing address") addr2 := &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 5678} assert.Nil(t, nm.Get(addr2.String()), "Get should return nil for a non-existent address") @@ -514,8 +521,8 @@ func TestNATMap(t *testing.T) { t.Run("Del", func(t *testing.T) { nm := newNATmap() addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} - conn := &natconn{} - nm.Add(addr, conn) + assoc := &association{} + nm.Add(addr, assoc) nm.Del(addr) @@ -526,8 +533,8 @@ func TestNATMap(t *testing.T) { nm := newNATmap() addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} pc := makePacketConn() - conn := &natconn{PacketConn: pc, raddr: addr} - nm.Add(addr, conn) + assoc := &association{Conn: &natconn{PacketConn: pc, raddr: addr}} + nm.Add(addr, assoc) err := nm.Close() assert.NoError(t, err, "Close should not return an error") @@ -620,7 +627,7 @@ func TestUDPEarlyClose(t *testing.T) { t.Fatal(err) } const testTimeout = 200 * time.Millisecond - ph := NewAssociationHandler(testTimeout, cipherList, &fakeShadowsocksMetrics{}) + ph := NewPacketHandler(testTimeout, cipherList, &fakeShadowsocksMetrics{}) clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) if err != nil { @@ -628,7 +635,9 @@ func TestUDPEarlyClose(t *testing.T) { } require.Nil(t, clientConn.Close()) // This should return quickly without timing out. - PacketServe(clientConn, func(conn net.Conn) { ph.Handle(conn, &NoOpUDPAssocationMetrics{}) }, &natTestMetrics{}) + go PacketServe(clientConn, func(conn net.Conn) (Association, error) { + return ph.NewAssociation(conn, &NoOpUDPAssocationMetrics{}) + }, &natTestMetrics{}) } // Makes sure the UDP listener returns [io.ErrClosed] on reads and writes after Close(). From 2f2268b5703d700247bdccb2a86a0deb5a9854bf Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 18 Dec 2024 16:34:22 -0500 Subject: [PATCH 31/80] Add some comments to the `Association` interface. --- service/udp.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/service/udp.go b/service/udp.go index c91e01a8..64cca33d 100644 --- a/service/udp.go +++ b/service/udp.go @@ -357,9 +357,20 @@ func (m *natmap) Close() error { return err } +// Association represents a UDP association that handles incoming packets +// and forwards them to a target connection. type Association interface { + // HandlePacket processes a single incoming packet. + // + // pkt contains the raw packet data. + // lazySlice is the LazySlice that holds the pkt buffer, which should be + // released after the packet is processed. HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) + + // Done returns a channel that is closed when the association is closed. Done() <-chan struct{} + + // Close closes the association and releases any associated resources. Close() error } From bfe4b3540bb9b56aaf45e2935981e221ac3ae900 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 18 Dec 2024 16:38:32 -0500 Subject: [PATCH 32/80] Consolidate debug logging. --- service/udp.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/service/udp.go b/service/udp.go index 64cca33d..3fba661b 100644 --- a/service/udp.go +++ b/service/udp.go @@ -391,6 +391,10 @@ type association struct { var _ Association = (*association)(nil) +func (a *association) debugLog(template string, attrs ...slog.Attr) { + debugUDP(a.logger, template, attrs...) +} + // Given the decrypted contents of a UDP packet, return // the payload and the destination address, or an error if // this packet cannot or should not be forwarded. @@ -414,9 +418,9 @@ func (a *association) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *on func (a *association) HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) { defer lazySlice.Release() - defer debugUDP(a.logger, "Done") + defer a.debugLog("Done") - debugUDP(a.logger, "Outbound packet.", slog.Int("bytes", len(pkt))) + a.debugLog("Outbound packet.", slog.Int("bytes", len(pkt))) var proxyTargetBytes int connError := func() *onet.ConnectionError { @@ -464,7 +468,7 @@ func (a *association) HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) { return onetErr } - debugUDP(a.logger, "Proxy exit.") + a.debugLog("Proxy exit.") proxyTargetBytes, err = a.targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) @@ -533,7 +537,7 @@ func (a *association) timedCopy() { return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) } - debugUDP(a.logger, "Got response.", slog.Any("rtarget", raddr)) + a.debugLog("Got response.", slog.Any("rtarget", raddr)) srcAddr := socks.ParseAddr(raddr.String()) addrStart := bodyStart - len(srcAddr) // `plainTextBuf` concatenates the SOCKS address and body: @@ -562,7 +566,7 @@ func (a *association) timedCopy() { }() status := "OK" if connError != nil { - a.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + a.debugLog("Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } if expired { From f37f23ab0e2cf0214abd1328a081035f341a7cb5 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 18 Dec 2024 16:41:44 -0500 Subject: [PATCH 33/80] Rename some vars. --- service/udp.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/service/udp.go b/service/udp.go index 3fba661b..d45228dd 100644 --- a/service/udp.go +++ b/service/udp.go @@ -213,13 +213,13 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, m } metrics.AddNATEntry() - nm.Add(addr, assoc) + nm.Add(addr.String(), assoc) } select { case <-assoc.Done(): lazySlice.Release() metrics.RemoveNATEntry() - nm.Del(addr) + nm.Del(addr.String()) default: go assoc.HandlePacket(pkt, lazySlice) } @@ -313,35 +313,35 @@ func (c *timedPacketConn) ReadFrom(buf []byte) (int, net.Addr, error) { // Packet NAT table type natmap struct { sync.RWMutex - keyConn map[string]Association + associations map[string]Association } func newNATmap() *natmap { - return &natmap{keyConn: make(map[string]Association)} + return &natmap{associations: make(map[string]Association)} } -func (m *natmap) Get(key string) Association { +func (m *natmap) Get(clientAddr string) Association { m.RLock() defer m.RUnlock() - return m.keyConn[key] + return m.associations[clientAddr] } -func (m *natmap) Del(addr net.Addr) { +func (m *natmap) Del(clientAddr string) { m.Lock() defer m.Unlock() - if _, ok := m.keyConn[addr.String()]; ok { - delete(m.keyConn, addr.String()) + if _, ok := m.associations[clientAddr]; ok { + delete(m.associations, clientAddr) } } // Add adds a new UDP NAT entry to the natmap and returns a closure to delete // the entry. -func (m *natmap) Add(addr net.Addr, assoc Association) { +func (m *natmap) Add(clientAddr string, assoc Association) { m.Lock() defer m.Unlock() - m.keyConn[addr.String()] = assoc + m.associations[clientAddr] = assoc } func (m *natmap) Close() error { @@ -349,7 +349,7 @@ func (m *natmap) Close() error { defer m.Unlock() var err error - for _, assoc := range m.keyConn { + for _, assoc := range m.associations { if e := assoc.Close(); e != nil { err = e } From 08de4c39f576983f386a6f175a1014c9de7e49a3 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 18 Dec 2024 16:45:11 -0500 Subject: [PATCH 34/80] Update doc. --- service/shadowsocks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/shadowsocks.go b/service/shadowsocks.go index a923d172..112fb08b 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -136,7 +136,7 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) s.sh.Handle(ctx, conn, metrics) } -// HandleAssociation handles a Shadowsocks packet-based assocation. +// NewAssociation creates a new Shadowsocks packet-based association. func (s *ssService) NewAssociation(conn net.Conn) (Association, error) { var metrics UDPAssocationMetrics if s.metrics != nil { From 418c0e4369a4323c44aaa10f602ae43ad2d63352 Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 19 Dec 2024 11:40:18 -0500 Subject: [PATCH 35/80] Format. --- internal/integration_test/integration_test.go | 1 - service/udp.go | 1 - service/udp_test.go | 5 ++--- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 5b2653b1..cfba8b0a 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -262,7 +262,6 @@ func TestRestrictedAddresses(t *testing.T) { } // Stub metrics implementation for testing NAT behaviors. - type natTestMetrics struct { natEntriesAdded int } diff --git a/service/udp.go b/service/udp.go index d45228dd..8bfeedf8 100644 --- a/service/udp.go +++ b/service/udp.go @@ -168,7 +168,6 @@ func (h *packetHandler) NewAssociation(conn net.Conn, m UDPAssocationMetrics) (A }, nil } - type NewAssociationFunc func(conn net.Conn) (Association, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates diff --git a/service/udp_test.go b/service/udp_test.go index 384f4f67..a2410074 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -129,7 +129,6 @@ type udpReport struct { } // Stub metrics implementation for testing NAT behaviors. - type natTestMetrics struct { natEntriesAdded int } @@ -199,8 +198,8 @@ func startTestHandler() (PacketHandler, func(target net.Addr, payload []byte), * func TestNatconnCloseWhileReading(t *testing.T) { nc := &natconn{ - PacketConn: makePacketConn(), - raddr: &clientAddr, + PacketConn: makePacketConn(), + raddr: &clientAddr, } go func() { buf := make([]byte, 1024) From 98988b0af3dc2e84371cb8905c9def9a073fa69a Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 19 Dec 2024 17:07:58 -0500 Subject: [PATCH 36/80] Do not unpack first packets twice. --- service/udp.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/service/udp.go b/service/udp.go index 8bfeedf8..4695b58f 100644 --- a/service/udp.go +++ b/service/udp.go @@ -453,10 +453,14 @@ func (a *association) HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) } - unpackStart := time.Now() - textData, err = shadowsocks.Unpack(nil, pkt, a.cryptoKey) - timeToCipher := time.Since(unpackStart) - a.ssm.AddCipherSearch(err == nil, timeToCipher) + if textData == nil { + // This is a subsequent packet. First packets are already decrypted as part of the + // initial access key search. + unpackStart := time.Now() + textData, err = shadowsocks.Unpack(nil, pkt, a.cryptoKey) + timeToCipher := time.Since(unpackStart) + a.ssm.AddCipherSearch(err == nil, timeToCipher) + } if err != nil { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) From b9c028667a115ba0b7e9b41647e672eebd7a82ff Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 20 Dec 2024 14:43:36 -0500 Subject: [PATCH 37/80] Move handling into the association. --- caddy/shadowsocks_handler.go | 15 +-------------- service/udp.go | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 5d98d23e..9df9d555 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -16,7 +16,6 @@ package caddy import ( "container/list" - "errors" "fmt" "log/slog" "net" @@ -26,7 +25,6 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/mholt/caddy-l4/layer4" - "github.com/Jigsaw-Code/outline-ss-server/internal/slicepool" outline "github.com/Jigsaw-Code/outline-ss-server/service" ) @@ -125,18 +123,7 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err if err != nil { return fmt.Errorf("Failed to handle association: %v", err) } - bufPool := slicepool.MakePool(serverUDPBufferSize) - for { - lazySlice := bufPool.LazySlice() - buf := lazySlice.Acquire() - n, err := conn.Read(buf) - if errors.Is(err, net.ErrClosed) { - lazySlice.Release() - return err - } - pkt := buf[:n] - go assoc.HandlePacket(pkt, lazySlice) - } + assoc.Handle(conn) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/service/udp.go b/service/udp.go index 4695b58f..2caf9205 100644 --- a/service/udp.go +++ b/service/udp.go @@ -359,6 +359,9 @@ func (m *natmap) Close() error { // Association represents a UDP association that handles incoming packets // and forwards them to a target connection. type Association interface { + // Handle reads data from the given connection and handles incoming packets. + Handle(conn net.Conn) + // HandlePacket processes a single incoming packet. // // pkt contains the raw packet data. @@ -394,6 +397,26 @@ func (a *association) debugLog(template string, attrs ...slog.Attr) { debugUDP(a.logger, template, attrs...) } +func (a *association) Handle(conn net.Conn) { + for { + lazySlice := a.bufPool.LazySlice() + buf := lazySlice.Acquire() + n, err := conn.Read(buf) + if errors.Is(err, net.ErrClosed) { + lazySlice.Release() + return + } + pkt := buf[:n] + select { + case <-a.Done(): + lazySlice.Release() + return + default: + go a.HandlePacket(pkt, lazySlice) + } + } +} + // Given the decrypted contents of a UDP packet, return // the payload and the destination address, or an error if // this packet cannot or should not be forwarded. From eef630a8d46091a71bb3b7dba5021c7edc419352 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 20 Dec 2024 15:44:34 -0500 Subject: [PATCH 38/80] Add some comments to the timeout value. --- service/udp_linux.go | 10 ++++++---- service/udp_other.go | 9 ++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/service/udp_linux.go b/service/udp_linux.go index f0b8bad4..10b3282f 100644 --- a/service/udp_linux.go +++ b/service/udp_linux.go @@ -26,7 +26,9 @@ import ( ) type udpListener struct { - natTimeout time.Duration + // NAT mapping timeout is the default time a mapping will stay active + // without packets traversing the NAT, applied to non-DNS packets. + timeout time.Duration // fwmark can be used in conjunction with other Linux networking features like cgroups, network // namespaces, and TC (Traffic Control) for sophisticated network management. // Value of 0 disables fwmark (SO_MARK) (Linux only) @@ -35,8 +37,8 @@ type udpListener struct { // NewPacketListener creates a new PacketListener that listens on UDP // and optionally sets a firewall mark on the socket (Linux only). -func MakeTargetUDPListener(natTimeout time.Duration, fwmark uint) transport.PacketListener { - return &udpListener{natTimeout: natTimeout, fwmark: fwmark} +func MakeTargetUDPListener(timeout time.Duration, fwmark uint) transport.PacketListener { + return &udpListener{timeout: timeout, fwmark: fwmark} } func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) { @@ -59,5 +61,5 @@ func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) } } - return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.natTimeout}, nil + return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, nil } diff --git a/service/udp_other.go b/service/udp_other.go index c67e59f3..0cfde76a 100644 --- a/service/udp_other.go +++ b/service/udp_other.go @@ -26,12 +26,15 @@ import ( type udpListener struct { *transport.UDPListener - natTimeout time.Duration + + // NAT mapping timeout is the default time a mapping will stay active + // without packets traversing the NAT, applied to non-DNS packets. + timeout time.Duration } // fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management. // Value of 0 disables fwmark (SO_MARK) -func MakeTargetUDPListener(natTimeout time.Duration, fwmark uint) transport.PacketListener { +func MakeTargetUDPListener(timeout time.Duration, fwmark uint) transport.PacketListener { if fwmark != 0 { panic("fwmark is linux-specific feature and should be 0") } @@ -43,5 +46,5 @@ func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) if err != nil { return nil, err } - return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.natTimeout}, nil + return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, nil } From 1c462b120f541291428f58a187d114532b5c298e Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 20 Dec 2024 16:00:00 -0500 Subject: [PATCH 39/80] Don't set the stream dialer in the old config flow. --- cmd/outline-ss-server/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index ae20648b..37c71753 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -227,7 +227,6 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), - service.WithStreamDialer(service.MakeValidatingTCPStreamDialer(onet.RequirePublicIP, 0)), service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, 0)), service.WithLogger(slog.Default()), ) From 09180509ca1a1cec66738077566bf7f4efd988c2 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 6 Jan 2025 13:23:59 -0500 Subject: [PATCH 40/80] Rename `AddAuthentication` and `AddClose`. --- internal/integration_test/integration_test.go | 6 +++--- prometheus/metrics.go | 8 ++++---- prometheus/metrics_test.go | 20 +++++++++---------- service/tcp.go | 12 +++++------ service/tcp_test.go | 4 ++-- service/udp.go | 12 +++++------ service/udp_test.go | 4 ++-- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 680c7ad7..52fb025b 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -201,7 +201,7 @@ type statusMetrics struct { statuses []string } -func (m *statusMetrics) AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) { +func (m *statusMetrics) AddClose(status string, data metrics.ProxyMetrics, duration time.Duration) { m.Lock() m.statuses = append(m.statuses, status) m.Unlock() @@ -287,7 +287,7 @@ type fakeUDPAssocationMetrics struct { var _ service.UDPAssocationMetrics = (*fakeUDPAssocationMetrics)(nil) -func (m *fakeUDPAssocationMetrics) AddAuthenticated(key string) { +func (m *fakeUDPAssocationMetrics) AddAuthentication(key string) { m.accessKey = key } @@ -303,7 +303,7 @@ func (m *fakeUDPAssocationMetrics) AddPacketFromTarget(status string, targetProx m.down = append(m.down, udpRecord{m.accessKey, status, targetProxyBytes, proxyClientBytes}) } -func (m *fakeUDPAssocationMetrics) AddClosed() {} +func (m *fakeUDPAssocationMetrics) AddClose() {} func TestUDPEcho(t *testing.T) { echoConn, echoRunning := startUDPEchoServer(t) diff --git a/prometheus/metrics.go b/prometheus/metrics.go index 036b74bc..37c8ac1c 100644 --- a/prometheus/metrics.go +++ b/prometheus/metrics.go @@ -117,7 +117,7 @@ func newTCPConnMetrics(tcpServiceMetrics *tcpServiceMetrics, tunnelTimeMetrics * } } -func (cm *tcpConnMetrics) AddAuthenticated(accessKey string) { +func (cm *tcpConnMetrics) AddAuthentication(accessKey string) { cm.accessKey = accessKey ipKey, err := toIPKey(cm.clientAddr, accessKey) if err == nil { @@ -125,7 +125,7 @@ func (cm *tcpConnMetrics) AddAuthenticated(accessKey string) { } } -func (cm *tcpConnMetrics) AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) { +func (cm *tcpConnMetrics) AddClose(status string, data metrics.ProxyMetrics, duration time.Duration) { cm.tcpServiceMetrics.proxyCollector.addClientTarget(data.ClientProxy, data.ProxyTarget, cm.accessKey, cm.clientInfo) cm.tcpServiceMetrics.proxyCollector.addTargetClient(data.TargetProxy, data.ProxyClient, cm.accessKey, cm.clientInfo) cm.tcpServiceMetrics.closeConnection(status, duration, cm.accessKey, cm.clientInfo) @@ -261,7 +261,7 @@ func newUDPAssocationMetrics(udpServiceMetrics *udpServiceMetrics, tunnelTimeMet } } -func (cm *udpConnMetrics) AddAuthenticated(accessKey string) { +func (cm *udpConnMetrics) AddAuthentication(accessKey string) { cm.accessKey = accessKey ipKey, err := toIPKey(cm.clientAddr, accessKey) if err == nil { @@ -277,7 +277,7 @@ func (cm *udpConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, p cm.udpServiceMetrics.addPacketFromTarget(status, targetProxyBytes, proxyClientBytes, cm.accessKey, cm.clientInfo) } -func (cm *udpConnMetrics) AddClosed() { +func (cm *udpConnMetrics) AddClose() { // We only track authenticated connections, so ignore unauthenticated closed connections // when calculating tunneltime. if cm.accessKey != "" { diff --git a/prometheus/metrics_test.go b/prometheus/metrics_test.go index c435ee0c..735b3dd4 100644 --- a/prometheus/metrics_test.go +++ b/prometheus/metrics_test.go @@ -72,15 +72,15 @@ func TestMethodsDontPanic(t *testing.T) { } tcpMetrics := ssMetrics.AddOpenTCPConnection(&fakeConn{}) - tcpMetrics.AddAuthenticated("0") - tcpMetrics.AddClosed("OK", proxyMetrics, 10*time.Millisecond) + tcpMetrics.AddAuthentication("0") + tcpMetrics.AddClose("OK", proxyMetrics, 10*time.Millisecond) tcpMetrics.AddProbe("ERR_CIPHER", "eof", proxyMetrics.ClientProxy) udpMetrics := ssMetrics.AddOpenUDPAssociation(&fakeConn{}) - udpMetrics.AddAuthenticated("0") + udpMetrics.AddAuthentication("0") udpMetrics.AddPacketFromClient("OK", 10, 20) udpMetrics.AddPacketFromTarget("OK", 10, 20) - udpMetrics.AddClosed() + udpMetrics.AddClose() ssMetrics.tcpServiceMetrics.AddCipherSearch(true, 10*time.Millisecond) ssMetrics.udpServiceMetrics.AddCipherSearch(true, 10*time.Millisecond) @@ -99,7 +99,7 @@ func TestTunnelTime(t *testing.T) { reg.MustRegister(ssMetrics) connMetrics := ssMetrics.AddOpenTCPConnection(&fakeConn{}) - connMetrics.AddAuthenticated("key-1") + connMetrics.AddAuthentication("key-1") setNow(time.Date(2010, 1, 2, 3, 4, 20, .0, time.Local)) expected := strings.NewReader(` @@ -122,7 +122,7 @@ func TestTunnelTime(t *testing.T) { reg.MustRegister(ssMetrics) connMetrics := ssMetrics.AddOpenTCPConnection(&fakeConn{}) - connMetrics.AddAuthenticated("key-1") + connMetrics.AddAuthentication("key-1") setNow(time.Date(2010, 1, 2, 3, 4, 10, .0, time.Local)) expected := strings.NewReader(` @@ -144,7 +144,7 @@ func TestTunnelTimePerKeyDoesNotPanicOnUnknownClosedConnection(t *testing.T) { ssMetrics, _ := NewServiceMetrics(nil) connMetrics := ssMetrics.AddOpenTCPConnection(&fakeConn{}) - connMetrics.AddClosed("OK", metrics.ProxyMetrics{}, time.Minute) + connMetrics.AddClose("OK", metrics.ProxyMetrics{}, time.Minute) err := promtest.GatherAndCompare( reg, @@ -172,8 +172,8 @@ func BenchmarkCloseTCP(b *testing.B) { duration := time.Minute b.ResetTimer() for i := 0; i < b.N; i++ { - connMetrics.AddAuthenticated(accessKey) - connMetrics.AddClosed(status, data, duration) + connMetrics.AddAuthentication(accessKey) + connMetrics.AddClose(status, data, duration) } } @@ -216,6 +216,6 @@ func BenchmarkClose(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { udpMetrics := ssMetrics.AddOpenUDPAssociation(&fakeConn{}) - udpMetrics.AddClosed() + udpMetrics.AddClose() } } diff --git a/service/tcp.go b/service/tcp.go index 5cb1b864..286c3753 100644 --- a/service/tcp.go +++ b/service/tcp.go @@ -37,8 +37,8 @@ import ( // TCPConnMetrics is used to report metrics on TCP connections. type TCPConnMetrics interface { - AddAuthenticated(accessKey string) - AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) + AddAuthentication(accessKey string) + AddClose(status string, data metrics.ProxyMetrics, duration time.Duration) AddProbe(status, drainResult string, clientProxyBytes int64) } @@ -266,7 +266,7 @@ func (h *streamHandler) Handle(ctx context.Context, clientConn transport.StreamC status = connError.Status h.logger.LogAttrs(nil, slog.LevelDebug, "TCP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) } - connMetrics.AddClosed(status, proxyMetrics, connDuration) + connMetrics.AddClose(status, proxyMetrics, connDuration) measuredClientConn.Close() // Closing after the metrics are added aids integration testing. h.logger.LogAttrs(nil, slog.LevelDebug, "TCP: Done.", slog.String("status", status), slog.Duration("duration", connDuration)) } @@ -338,7 +338,7 @@ func (h *streamHandler) handleConnection(ctx context.Context, outerConn transpor h.absorbProbe(outerConn, connMetrics, authErr.Status, proxyMetrics) return authErr } - connMetrics.AddAuthenticated(id) + connMetrics.AddAuthentication(id) // Read target address and dial it. tgtAddr, err := getProxyRequest(innerConn) @@ -389,9 +389,9 @@ type NoOpTCPConnMetrics struct{} var _ TCPConnMetrics = (*NoOpTCPConnMetrics)(nil) -func (m *NoOpTCPConnMetrics) AddAuthenticated(accessKey string) {} +func (m *NoOpTCPConnMetrics) AddAuthentication(accessKey string) {} -func (m *NoOpTCPConnMetrics) AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) { +func (m *NoOpTCPConnMetrics) AddClose(status string, data metrics.ProxyMetrics, duration time.Duration) { } func (m *NoOpTCPConnMetrics) AddProbe(status, drainResult string, clientProxyBytes int64) {} diff --git a/service/tcp_test.go b/service/tcp_test.go index ab497f91..555a3a3f 100644 --- a/service/tcp_test.go +++ b/service/tcp_test.go @@ -235,13 +235,13 @@ var _ TCPConnMetrics = (*probeTestMetrics)(nil) var _ ShadowsocksConnMetrics = (*fakeShadowsocksMetrics)(nil) -func (m *probeTestMetrics) AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) { +func (m *probeTestMetrics) AddClose(status string, data metrics.ProxyMetrics, duration time.Duration) { m.mu.Lock() m.closeStatus = append(m.closeStatus, status) m.mu.Unlock() } -func (m *probeTestMetrics) AddAuthenticated(accessKey string) { +func (m *probeTestMetrics) AddAuthentication(accessKey string) { } func (m *probeTestMetrics) AddProbe(status, drainResult string, clientProxyBytes int64) { diff --git a/service/udp.go b/service/udp.go index 3c2f89c6..fc299c2b 100644 --- a/service/udp.go +++ b/service/udp.go @@ -41,10 +41,10 @@ type NATMetrics interface { // UDPAssocationMetrics is used to report metrics on UDP associations. type UDPAssocationMetrics interface { - AddAuthenticated(accessKey string) + AddAuthentication(accessKey string) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) - AddClosed() + AddClose() } const ( @@ -462,7 +462,7 @@ func (a *association) HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) { return } - a.m.AddAuthenticated(keyID) + a.m.AddAuthentication(keyID) go a.timedCopy() }) if err != nil { @@ -524,7 +524,7 @@ var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) // copy from target to client until read timeout func (a *association) timedCopy() { defer func() { - a.m.AddClosed() + a.m.AddClose() a.targetConn.Close() close(a.doneCh) }() @@ -606,11 +606,11 @@ type NoOpUDPAssociationMetrics struct{} var _ UDPAssocationMetrics = (*NoOpUDPAssociationMetrics)(nil) -func (m *NoOpUDPAssociationMetrics) AddAuthenticated(accessKey string) {} +func (m *NoOpUDPAssociationMetrics) AddAuthentication(accessKey string) {} func (m *NoOpUDPAssociationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { } func (m *NoOpUDPAssociationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { } -func (m *NoOpUDPAssociationMetrics) AddClosed() { +func (m *NoOpUDPAssociationMetrics) AddClose() { } diff --git a/service/udp_test.go b/service/udp_test.go index 358dd32e..d9c64bad 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -160,7 +160,7 @@ type fakeUDPAssocationMetrics struct { var _ UDPAssocationMetrics = (*fakeUDPAssocationMetrics)(nil) -func (m *fakeUDPAssocationMetrics) AddAuthenticated(key string) { +func (m *fakeUDPAssocationMetrics) AddAuthentication(key string) { m.accessKey = key } @@ -173,7 +173,7 @@ func (m *fakeUDPAssocationMetrics) AddPacketFromClient(status string, clientProx func (m *fakeUDPAssocationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { } -func (m *fakeUDPAssocationMetrics) AddClosed() {} +func (m *fakeUDPAssocationMetrics) AddClose() {} // sendSSPayload sends a single Shadowsocks packet to the provided connection. // The packet is constructed with the given address, cipher, and payload. From 78af4be983c8510445021c5dad4863f2897ddb83 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 6 Jan 2025 13:26:37 -0500 Subject: [PATCH 41/80] Update comment to reflect it handles packets from both directions. --- service/udp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index fc299c2b..85e2b19a 100644 --- a/service/udp.go +++ b/service/udp.go @@ -357,7 +357,8 @@ func (m *natmap) Close() error { // Association represents a UDP association that handles incoming packets // and forwards them to a target connection. type Association interface { - // Handle reads data from the given connection and handles incoming packets. + // Handle reads data from the given connection and handles incoming and + // outgoing packets. Handle(conn net.Conn) // HandlePacket processes a single incoming packet. From 2cf398ceda9b994f2625039332d2c4fdab0d9c2b Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 6 Jan 2025 13:51:50 -0500 Subject: [PATCH 42/80] Separate the interfaces for `Handle()` and `HandlePacket()`. --- caddy/shadowsocks_handler.go | 2 +- cmd/outline-ss-server/main.go | 4 +- internal/integration_test/integration_test.go | 12 ++--- service/shadowsocks.go | 20 ++++++-- service/udp.go | 46 +++++++++++++------ service/udp_test.go | 24 +++++----- 6 files changed, 68 insertions(+), 40 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 9df9d555..2bcc3c41 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -119,7 +119,7 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.service.HandleStream(cx.Context, conn) case net.Conn: - assoc, err := h.service.NewAssociation(conn) + assoc, err := h.service.NewConnAssociation(conn) if err != nil { return fmt.Errorf("Failed to handle association: %v", err) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index 37c71753..d41bf685 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -242,7 +242,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.NewAssociation, s.serverMetrics) + go service.PacketServe(pc, ssService.NewPacketAssociation, s.serverMetrics) } for _, serviceConfig := range config.Services { @@ -286,7 +286,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { } return serviceConfig.Dialer.Fwmark }()) - go service.PacketServe(pc, ssService.NewAssociation, s.serverMetrics) + go service.PacketServe(pc, ssService.NewPacketAssociation, s.serverMetrics) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 52fb025b..f8738797 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -322,8 +322,8 @@ func TestUDPEcho(t *testing.T) { proxy.SetTargetIPValidator(allowAll) natMetrics := &natTestMetrics{} associationMetrics := &fakeUDPAssocationMetrics{} - go service.PacketServe(proxyConn, func(conn net.Conn) (service.Association, error) { - return proxy.NewAssociation(conn, associationMetrics) + go service.PacketServe(proxyConn, func(conn net.Conn) (service.PacketAssociation, error) { + return proxy.NewPacketAssociation(conn, associationMetrics) }, natMetrics) cryptoKey, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, secrets[0]) @@ -549,8 +549,8 @@ func BenchmarkUDPEcho(b *testing.B) { proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(server, func(conn net.Conn) (service.Association, error) { - return proxy.NewAssociation(conn, nil) + service.PacketServe(server, func(conn net.Conn) (service.PacketAssociation, error) { + return proxy.NewPacketAssociation(conn, nil) }, &natTestMetrics{}) done <- struct{}{} }() @@ -595,8 +595,8 @@ func BenchmarkUDPManyKeys(b *testing.B) { proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(proxyConn, func(conn net.Conn) (service.Association, error) { - return proxy.NewAssociation(conn, nil) + service.PacketServe(proxyConn, func(conn net.Conn) (service.PacketAssociation, error) { + return proxy.NewPacketAssociation(conn, nil) }, &natTestMetrics{}) done <- struct{}{} }() diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 2a80ea83..f81f98bc 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -43,7 +43,8 @@ type ServiceMetrics interface { type Service interface { HandleStream(ctx context.Context, conn transport.StreamConn) - NewAssociation(conn net.Conn) (Association, error) + NewConnAssociation(conn net.Conn) (ConnAssociation, error) + NewPacketAssociation(conn net.Conn) (PacketAssociation, error) } // Option is a Shadowsocks service constructor option. @@ -146,13 +147,24 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) s.sh.Handle(ctx, conn, metrics) } -// NewAssociation creates a new Shadowsocks packet-based association. -func (s *ssService) NewAssociation(conn net.Conn) (Association, error) { +// NewConnAssociation creates a new Shadowsocks packet-based association that +// handles incoming packets. Used by Caddy. +func (s *ssService) NewConnAssociation(conn net.Conn) (ConnAssociation, error) { var metrics UDPAssocationMetrics if s.metrics != nil { metrics = s.metrics.AddOpenUDPAssociation(conn) } - return s.ph.NewAssociation(conn, metrics) + return s.ph.NewConnAssociation(conn, metrics) +} + +// NewPacketAssociation creates a new Shadowsocks packet-based association. +// Used by outline-ss-server. +func (s *ssService) NewPacketAssociation(conn net.Conn) (PacketAssociation, error) { + var metrics UDPAssocationMetrics + if s.metrics != nil { + metrics = s.metrics.AddOpenUDPAssociation(conn) + } + return s.ph.NewPacketAssociation(conn, metrics) } type ssConnMetrics struct { diff --git a/service/udp.go b/service/udp.go index 85e2b19a..39a766bb 100644 --- a/service/udp.go +++ b/service/udp.go @@ -123,8 +123,10 @@ type PacketHandler interface { SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) // SetTargetPacketListener sets the packet listener to use for target connections. SetTargetPacketListener(targetListener transport.PacketListener) - // NewAssociation creates a new Association instance. - NewAssociation(conn net.Conn, connMetrics UDPAssocationMetrics) (Association, error) + // NewConnAssociation creates a new ConnAssociation. + NewConnAssociation(conn net.Conn, connMetrics UDPAssocationMetrics) (ConnAssociation, error) + // NewPacketAssociation creates a new PacketAssociation. + NewPacketAssociation(conn net.Conn, connMetrics UDPAssocationMetrics) (PacketAssociation, error) } func (h *packetHandler) SetLogger(l *slog.Logger) { @@ -142,7 +144,7 @@ func (h *packetHandler) SetTargetPacketListener(targetListener transport.PacketL h.targetListener = targetListener } -func (h *packetHandler) NewAssociation(conn net.Conn, m UDPAssocationMetrics) (Association, error) { +func (h *packetHandler) newAssociation(conn net.Conn, m UDPAssocationMetrics) (*association, error) { if m == nil { m = &NoOpUDPAssociationMetrics{} } @@ -166,12 +168,20 @@ func (h *packetHandler) NewAssociation(conn net.Conn, m UDPAssocationMetrics) (A }, nil } -type NewAssociationFunc func(conn net.Conn) (Association, error) +func (h *packetHandler) NewConnAssociation(conn net.Conn, m UDPAssocationMetrics) (ConnAssociation, error) { + return h.newAssociation(conn, m) +} + +func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssocationMetrics) (PacketAssociation, error) { + return h.newAssociation(conn, m) +} + +type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates -// creates and manages NAT associations, and invokes the provided `handle` -// function for each association. It uses a NAT map to track active associations -// and handles their lifecycle. +// and manages NAT associations, and invokes the provided `handle` function for +// each association. It uses a NAT map to track active associations and handles +// their lifecycle. func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, metrics NATMetrics) { nm := newNATmap() defer nm.Close() @@ -310,14 +320,14 @@ func (c *timedPacketConn) ReadFrom(buf []byte) (int, net.Addr, error) { // Packet NAT table type natmap struct { sync.RWMutex - associations map[string]Association + associations map[string]PacketAssociation } func newNATmap() *natmap { - return &natmap{associations: make(map[string]Association)} + return &natmap{associations: make(map[string]PacketAssociation)} } -func (m *natmap) Get(clientAddr string) Association { +func (m *natmap) Get(clientAddr string) PacketAssociation { m.RLock() defer m.RUnlock() return m.associations[clientAddr] @@ -334,7 +344,7 @@ func (m *natmap) Del(clientAddr string) { // Add adds a new UDP NAT entry to the natmap and returns a closure to delete // the entry. -func (m *natmap) Add(clientAddr string, assoc Association) { +func (m *natmap) Add(clientAddr string, assoc PacketAssociation) { m.Lock() defer m.Unlock() @@ -354,13 +364,18 @@ func (m *natmap) Close() error { return err } -// Association represents a UDP association that handles incoming packets -// and forwards them to a target connection. -type Association interface { +// ConnAssociation represents a UDP association that handles incoming packets +// from a [net.Conn] and forwards them to a target connection, and vice versa. +// Used by Caddy. +type ConnAssociation interface { // Handle reads data from the given connection and handles incoming and // outgoing packets. Handle(conn net.Conn) +} +// PacketAssociation represents a UDP association that handles individual +// incoming packets. Used by outline-ss-server. +type PacketAssociation interface { // HandlePacket processes a single incoming packet. // // pkt contains the raw packet data. @@ -390,7 +405,8 @@ type association struct { findAccessKeyOnce sync.Once } -var _ Association = (*association)(nil) +var _ ConnAssociation = (*association)(nil) +var _ PacketAssociation = (*association)(nil) func (a *association) debugLog(template string, attrs ...slog.Attr) { debugUDP(a.logger, template, attrs...) diff --git a/service/udp_test.go b/service/udp_test.go index d9c64bad..b54da2ac 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -198,8 +198,8 @@ func startTestHandler() (PacketHandler, func(target net.Addr, payload []byte), * clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetPacketListener(&packetListener{targetConn}) - go PacketServe(clientConn, func(conn net.Conn) (Association, error) { - return handler.NewAssociation(conn, nil) + go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { + return handler.NewPacketAssociation(conn, nil) }, &natTestMetrics{}) return handler, func(target net.Addr, payload []byte) { sendSSPayload(clientConn, target, cipher, payload) @@ -257,8 +257,8 @@ func TestUpstreamMetrics(t *testing.T) { targetConn := makePacketConn() handler.SetTargetPacketListener(&packetListener{targetConn}) metrics := &fakeUDPAssocationMetrics{} - go PacketServe(clientConn, func(conn net.Conn) (Association, error) { - return handler.NewAssociation(conn, metrics) + go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { + return handler.NewPacketAssociation(conn, metrics) }, &natTestMetrics{}) // Test both the first-packet and subsequent-packet cases. @@ -379,8 +379,8 @@ func TestTimedPacketConn(t *testing.T) { clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetPacketListener(&packetListener{targetConn}) - go PacketServe(clientConn, func(conn net.Conn) (Association, error) { - return handler.NewAssociation(conn, nil) + go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { + return handler.NewPacketAssociation(conn, nil) }, &natTestMetrics{}) // Send one DNS query. @@ -407,8 +407,8 @@ func TestTimedPacketConn(t *testing.T) { clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetPacketListener(&packetListener{targetConn}) - go PacketServe(clientConn, func(conn net.Conn) (Association, error) { - return handler.NewAssociation(conn, nil) + go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { + return handler.NewPacketAssociation(conn, nil) }, &natTestMetrics{}) // Send one non-DNS packet. @@ -435,8 +435,8 @@ func TestTimedPacketConn(t *testing.T) { clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetPacketListener(&packetListener{targetConn}) - go PacketServe(clientConn, func(conn net.Conn) (Association, error) { - return handler.NewAssociation(conn, nil) + go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { + return handler.NewPacketAssociation(conn, nil) }, &natTestMetrics{}) // Send two DNS packets. @@ -626,8 +626,8 @@ func TestUDPEarlyClose(t *testing.T) { } require.Nil(t, clientConn.Close()) // This should return quickly without timing out. - go PacketServe(clientConn, func(conn net.Conn) (Association, error) { - return ph.NewAssociation(conn, &NoOpUDPAssociationMetrics{}) + go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { + return ph.NewPacketAssociation(conn, &NoOpUDPAssociationMetrics{}) }, &natTestMetrics{}) } From 1055cb187e5f0c9b0c60f701b23dad20778ffb8f Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 6 Jan 2025 13:52:18 -0500 Subject: [PATCH 43/80] Fix typo in `UDPAssocationMetrics`. --- internal/integration_test/integration_test.go | 14 ++++++------- prometheus/metrics.go | 8 ++++---- service/shadowsocks.go | 6 +++--- service/udp.go | 20 +++++++++---------- service/udp_test.go | 14 ++++++------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index f8738797..739c4683 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -279,31 +279,31 @@ type udpRecord struct { in, out int64 } -type fakeUDPAssocationMetrics struct { +type fakeUDPAssociationMetrics struct { accessKey string up, down []udpRecord mu sync.Mutex } -var _ service.UDPAssocationMetrics = (*fakeUDPAssocationMetrics)(nil) +var _ service.UDPAssociationMetrics = (*fakeUDPAssociationMetrics)(nil) -func (m *fakeUDPAssocationMetrics) AddAuthentication(key string) { +func (m *fakeUDPAssociationMetrics) AddAuthentication(key string) { m.accessKey = key } -func (m *fakeUDPAssocationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { +func (m *fakeUDPAssociationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { m.mu.Lock() defer m.mu.Unlock() m.up = append(m.up, udpRecord{m.accessKey, status, clientProxyBytes, proxyTargetBytes}) } -func (m *fakeUDPAssocationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { +func (m *fakeUDPAssociationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { m.mu.Lock() defer m.mu.Unlock() m.down = append(m.down, udpRecord{m.accessKey, status, targetProxyBytes, proxyClientBytes}) } -func (m *fakeUDPAssocationMetrics) AddClose() {} +func (m *fakeUDPAssociationMetrics) AddClose() {} func TestUDPEcho(t *testing.T) { echoConn, echoRunning := startUDPEchoServer(t) @@ -321,7 +321,7 @@ func TestUDPEcho(t *testing.T) { proxy.SetTargetIPValidator(allowAll) natMetrics := &natTestMetrics{} - associationMetrics := &fakeUDPAssocationMetrics{} + associationMetrics := &fakeUDPAssociationMetrics{} go service.PacketServe(proxyConn, func(conn net.Conn) (service.PacketAssociation, error) { return proxy.NewPacketAssociation(conn, associationMetrics) }, natMetrics) diff --git a/prometheus/metrics.go b/prometheus/metrics.go index 37c8ac1c..698f1083 100644 --- a/prometheus/metrics.go +++ b/prometheus/metrics.go @@ -250,9 +250,9 @@ type udpConnMetrics struct { accessKey string } -var _ service.UDPAssocationMetrics = (*udpConnMetrics)(nil) +var _ service.UDPAssociationMetrics = (*udpConnMetrics)(nil) -func newUDPAssocationMetrics(udpServiceMetrics *udpServiceMetrics, tunnelTimeMetrics *tunnelTimeMetrics, clientAddr net.Addr, clientInfo ipinfo.IPInfo) *udpConnMetrics { +func newUDPAssociationMetrics(udpServiceMetrics *udpServiceMetrics, tunnelTimeMetrics *tunnelTimeMetrics, clientAddr net.Addr, clientInfo ipinfo.IPInfo) *udpConnMetrics { return &udpConnMetrics{ udpServiceMetrics: udpServiceMetrics, tunnelTimeMetrics: tunnelTimeMetrics, @@ -515,10 +515,10 @@ func (m *serviceMetrics) AddOpenTCPConnection(clientConn net.Conn) service.TCPCo return newTCPConnMetrics(m.tcpServiceMetrics, m.tunnelTimeMetrics, clientConn, clientInfo) } -func (m *serviceMetrics) AddOpenUDPAssociation(clientConn net.Conn) service.UDPAssocationMetrics { +func (m *serviceMetrics) AddOpenUDPAssociation(clientConn net.Conn) service.UDPAssociationMetrics { clientAddr := clientConn.RemoteAddr() clientInfo := m.getIPInfoFromAddr(clientAddr) - return newUDPAssocationMetrics(m.udpServiceMetrics, m.tunnelTimeMetrics, clientAddr, clientInfo) + return newUDPAssociationMetrics(m.udpServiceMetrics, m.tunnelTimeMetrics, clientAddr, clientInfo) } func (m *serviceMetrics) AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) { diff --git a/service/shadowsocks.go b/service/shadowsocks.go index f81f98bc..17a02417 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -36,7 +36,7 @@ type ShadowsocksConnMetrics interface { } type ServiceMetrics interface { - AddOpenUDPAssociation(conn net.Conn) UDPAssocationMetrics + AddOpenUDPAssociation(conn net.Conn) UDPAssociationMetrics AddOpenTCPConnection(conn net.Conn) TCPConnMetrics AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) } @@ -150,7 +150,7 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) // NewConnAssociation creates a new Shadowsocks packet-based association that // handles incoming packets. Used by Caddy. func (s *ssService) NewConnAssociation(conn net.Conn) (ConnAssociation, error) { - var metrics UDPAssocationMetrics + var metrics UDPAssociationMetrics if s.metrics != nil { metrics = s.metrics.AddOpenUDPAssociation(conn) } @@ -160,7 +160,7 @@ func (s *ssService) NewConnAssociation(conn net.Conn) (ConnAssociation, error) { // NewPacketAssociation creates a new Shadowsocks packet-based association. // Used by outline-ss-server. func (s *ssService) NewPacketAssociation(conn net.Conn) (PacketAssociation, error) { - var metrics UDPAssocationMetrics + var metrics UDPAssociationMetrics if s.metrics != nil { metrics = s.metrics.AddOpenUDPAssociation(conn) } diff --git a/service/udp.go b/service/udp.go index 39a766bb..a9b2cbe9 100644 --- a/service/udp.go +++ b/service/udp.go @@ -39,8 +39,8 @@ type NATMetrics interface { RemoveNATEntry() } -// UDPAssocationMetrics is used to report metrics on UDP associations. -type UDPAssocationMetrics interface { +// UDPAssociationMetrics is used to report metrics on UDP associations. +type UDPAssociationMetrics interface { AddAuthentication(accessKey string) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) @@ -124,9 +124,9 @@ type PacketHandler interface { // SetTargetPacketListener sets the packet listener to use for target connections. SetTargetPacketListener(targetListener transport.PacketListener) // NewConnAssociation creates a new ConnAssociation. - NewConnAssociation(conn net.Conn, connMetrics UDPAssocationMetrics) (ConnAssociation, error) + NewConnAssociation(conn net.Conn, connMetrics UDPAssociationMetrics) (ConnAssociation, error) // NewPacketAssociation creates a new PacketAssociation. - NewPacketAssociation(conn net.Conn, connMetrics UDPAssocationMetrics) (PacketAssociation, error) + NewPacketAssociation(conn net.Conn, connMetrics UDPAssociationMetrics) (PacketAssociation, error) } func (h *packetHandler) SetLogger(l *slog.Logger) { @@ -144,7 +144,7 @@ func (h *packetHandler) SetTargetPacketListener(targetListener transport.PacketL h.targetListener = targetListener } -func (h *packetHandler) newAssociation(conn net.Conn, m UDPAssocationMetrics) (*association, error) { +func (h *packetHandler) newAssociation(conn net.Conn, m UDPAssociationMetrics) (*association, error) { if m == nil { m = &NoOpUDPAssociationMetrics{} } @@ -168,11 +168,11 @@ func (h *packetHandler) newAssociation(conn net.Conn, m UDPAssocationMetrics) (* }, nil } -func (h *packetHandler) NewConnAssociation(conn net.Conn, m UDPAssocationMetrics) (ConnAssociation, error) { +func (h *packetHandler) NewConnAssociation(conn net.Conn, m UDPAssociationMetrics) (ConnAssociation, error) { return h.newAssociation(conn, m) } -func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssocationMetrics) (PacketAssociation, error) { +func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssociationMetrics) (PacketAssociation, error) { return h.newAssociation(conn, m) } @@ -393,7 +393,7 @@ type PacketAssociation interface { type association struct { net.Conn raddr net.UDPAddr - m UDPAssocationMetrics + m UDPAssociationMetrics logger *slog.Logger targetConn net.PacketConn cryptoKey *shadowsocks.EncryptionKey @@ -617,11 +617,11 @@ func (a *association) timedCopy() { } } -// NoOpUDPAssociationMetrics is a [UDPAssocationMetrics] that doesn't do anything. Useful in tests +// NoOpUDPAssociationMetrics is a [UDPAssociationMetrics] that doesn't do anything. Useful in tests // or if you don't want to track metrics. type NoOpUDPAssociationMetrics struct{} -var _ UDPAssocationMetrics = (*NoOpUDPAssociationMetrics)(nil) +var _ UDPAssociationMetrics = (*NoOpUDPAssociationMetrics)(nil) func (m *NoOpUDPAssociationMetrics) AddAuthentication(accessKey string) {} diff --git a/service/udp_test.go b/service/udp_test.go index b54da2ac..89fdca87 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -152,28 +152,28 @@ func (m *natTestMetrics) AddNATEntry() { } func (m *natTestMetrics) RemoveNATEntry() {} -type fakeUDPAssocationMetrics struct { +type fakeUDPAssociationMetrics struct { accessKey string upstreamPackets []udpReport mu sync.Mutex } -var _ UDPAssocationMetrics = (*fakeUDPAssocationMetrics)(nil) +var _ UDPAssociationMetrics = (*fakeUDPAssociationMetrics)(nil) -func (m *fakeUDPAssocationMetrics) AddAuthentication(key string) { +func (m *fakeUDPAssociationMetrics) AddAuthentication(key string) { m.accessKey = key } -func (m *fakeUDPAssocationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { +func (m *fakeUDPAssociationMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) { m.mu.Lock() defer m.mu.Unlock() m.upstreamPackets = append(m.upstreamPackets, udpReport{m.accessKey, status, clientProxyBytes, proxyTargetBytes}) } -func (m *fakeUDPAssocationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { +func (m *fakeUDPAssociationMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) { } -func (m *fakeUDPAssocationMetrics) AddClose() {} +func (m *fakeUDPAssociationMetrics) AddClose() {} // sendSSPayload sends a single Shadowsocks packet to the provided connection. // The packet is constructed with the given address, cipher, and payload. @@ -256,7 +256,7 @@ func TestUpstreamMetrics(t *testing.T) { clientConn := makePacketConn() targetConn := makePacketConn() handler.SetTargetPacketListener(&packetListener{targetConn}) - metrics := &fakeUDPAssocationMetrics{} + metrics := &fakeUDPAssociationMetrics{} go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { return handler.NewPacketAssociation(conn, metrics) }, &natTestMetrics{}) From f8ab81a4a83871171118e7603ca76cdce4c0b63f Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 6 Jan 2025 14:06:04 -0500 Subject: [PATCH 44/80] Don't pass `conn` to `Handle()`. --- caddy/shadowsocks_handler.go | 2 +- service/udp.go | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 2bcc3c41..06f0c1c2 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -123,7 +123,7 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err if err != nil { return fmt.Errorf("Failed to handle association: %v", err) } - assoc.Handle(conn) + assoc.Handle() default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/service/udp.go b/service/udp.go index a9b2cbe9..04fc0e96 100644 --- a/service/udp.go +++ b/service/udp.go @@ -364,13 +364,13 @@ func (m *natmap) Close() error { return err } -// ConnAssociation represents a UDP association that handles incoming packets -// from a [net.Conn] and forwards them to a target connection, and vice versa. -// Used by Caddy. +// ConnAssociation represents a UDP association that handles incoming and +// outgoing packets from a [net.Conn] and forwards them to a target connection, +// and vice versa. Used by Caddy. type ConnAssociation interface { - // Handle reads data from the given connection and handles incoming and - // outgoing packets. - Handle(conn net.Conn) + // Handle reads data from the association and handles incoming and outgoing + // packets. + Handle() } // PacketAssociation represents a UDP association that handles individual @@ -412,11 +412,11 @@ func (a *association) debugLog(template string, attrs ...slog.Attr) { debugUDP(a.logger, template, attrs...) } -func (a *association) Handle(conn net.Conn) { +func (a *association) Handle() { for { lazySlice := a.bufPool.LazySlice() buf := lazySlice.Acquire() - n, err := conn.Read(buf) + n, err := a.Conn.Read(buf) if errors.Is(err, net.ErrClosed) { lazySlice.Release() return From e6ef2f3fad1f35411e7299aa9a90948f30de9424 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 6 Jan 2025 14:13:48 -0500 Subject: [PATCH 45/80] Update comment. --- service/udp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/udp.go b/service/udp.go index 04fc0e96..e551d2ca 100644 --- a/service/udp.go +++ b/service/udp.go @@ -179,8 +179,8 @@ func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssociationMetr type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates -// and manages NAT associations, and invokes the provided `handle` function for -// each association. It uses a NAT map to track active associations and handles +// and manages NAT associations, and invokes the `HandlePacket` function for +// each packet. It uses a NAT map to track active associations and handles // their lifecycle. func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, metrics NATMetrics) { nm := newNATmap() From 2939f8af40a584d9e2ce5e20cc79b93ae9407d86 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 6 Jan 2025 14:19:00 -0500 Subject: [PATCH 46/80] Add comments. --- service/udp.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/service/udp.go b/service/udp.go index e551d2ca..dc0708c9 100644 --- a/service/udp.go +++ b/service/udp.go @@ -327,12 +327,14 @@ func newNATmap() *natmap { return &natmap{associations: make(map[string]PacketAssociation)} } +// Get returns a UDP NAT entry from the natmap. func (m *natmap) Get(clientAddr string) PacketAssociation { m.RLock() defer m.RUnlock() return m.associations[clientAddr] } +// Del deletes a UDP NAT entry from the natmap. func (m *natmap) Del(clientAddr string) { m.Lock() defer m.Unlock() @@ -342,8 +344,7 @@ func (m *natmap) Del(clientAddr string) { } } -// Add adds a new UDP NAT entry to the natmap and returns a closure to delete -// the entry. +// Add adds a new UDP NAT entry to the natmap. func (m *natmap) Add(clientAddr string, assoc PacketAssociation) { m.Lock() defer m.Unlock() From 23a03e915ae244ebeef3183a976b774a37adf3c2 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 8 Jan 2025 12:55:59 -0500 Subject: [PATCH 47/80] Remove ConnAssociation in favor of a `HandleAssociation(Conn, PacketAssociation)`. --- caddy/shadowsocks_handler.go | 6 ++-- service/shadowsocks.go | 12 ------- service/udp.go | 61 ++++++++++++------------------------ 3 files changed, 23 insertions(+), 56 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 06f0c1c2..b5edb283 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -119,11 +119,11 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.service.HandleStream(cx.Context, conn) case net.Conn: - assoc, err := h.service.NewConnAssociation(conn) + assoc, err := h.service.NewPacketAssociation(conn) if err != nil { - return fmt.Errorf("Failed to handle association: %v", err) + return fmt.Errorf("failed to handle association: %v", err) } - assoc.Handle() + outline.HandleAssociation(conn, assoc) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 17a02417..062a7087 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -43,7 +43,6 @@ type ServiceMetrics interface { type Service interface { HandleStream(ctx context.Context, conn transport.StreamConn) - NewConnAssociation(conn net.Conn) (ConnAssociation, error) NewPacketAssociation(conn net.Conn) (PacketAssociation, error) } @@ -147,18 +146,7 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) s.sh.Handle(ctx, conn, metrics) } -// NewConnAssociation creates a new Shadowsocks packet-based association that -// handles incoming packets. Used by Caddy. -func (s *ssService) NewConnAssociation(conn net.Conn) (ConnAssociation, error) { - var metrics UDPAssociationMetrics - if s.metrics != nil { - metrics = s.metrics.AddOpenUDPAssociation(conn) - } - return s.ph.NewConnAssociation(conn, metrics) -} - // NewPacketAssociation creates a new Shadowsocks packet-based association. -// Used by outline-ss-server. func (s *ssService) NewPacketAssociation(conn net.Conn) (PacketAssociation, error) { var metrics UDPAssociationMetrics if s.metrics != nil { diff --git a/service/udp.go b/service/udp.go index dc0708c9..8cbfea6a 100644 --- a/service/udp.go +++ b/service/udp.go @@ -123,8 +123,6 @@ type PacketHandler interface { SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) // SetTargetPacketListener sets the packet listener to use for target connections. SetTargetPacketListener(targetListener transport.PacketListener) - // NewConnAssociation creates a new ConnAssociation. - NewConnAssociation(conn net.Conn, connMetrics UDPAssociationMetrics) (ConnAssociation, error) // NewPacketAssociation creates a new PacketAssociation. NewPacketAssociation(conn net.Conn, connMetrics UDPAssociationMetrics) (PacketAssociation, error) } @@ -144,7 +142,7 @@ func (h *packetHandler) SetTargetPacketListener(targetListener transport.PacketL h.targetListener = targetListener } -func (h *packetHandler) newAssociation(conn net.Conn, m UDPAssociationMetrics) (*association, error) { +func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssociationMetrics) (PacketAssociation, error) { if m == nil { m = &NoOpUDPAssociationMetrics{} } @@ -168,14 +166,6 @@ func (h *packetHandler) newAssociation(conn net.Conn, m UDPAssociationMetrics) ( }, nil } -func (h *packetHandler) NewConnAssociation(conn net.Conn, m UDPAssociationMetrics) (ConnAssociation, error) { - return h.newAssociation(conn, m) -} - -func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssociationMetrics) (PacketAssociation, error) { - return h.newAssociation(conn, m) -} - type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates @@ -365,17 +355,27 @@ func (m *natmap) Close() error { return err } -// ConnAssociation represents a UDP association that handles incoming and -// outgoing packets from a [net.Conn] and forwards them to a target connection, -// and vice versa. Used by Caddy. -type ConnAssociation interface { - // Handle reads data from the association and handles incoming and outgoing - // packets. - Handle() +func HandleAssociation(conn net.Conn, assoc PacketAssociation) { + for { + lazySlice := readBufPool.LazySlice() + buf := lazySlice.Acquire() + n, err := conn.Read(buf) + if errors.Is(err, net.ErrClosed) { + lazySlice.Release() + return + } + pkt := buf[:n] + select { + case <-assoc.Done(): + lazySlice.Release() + return + default: + go assoc.HandlePacket(pkt, lazySlice) + } + } } -// PacketAssociation represents a UDP association that handles individual -// incoming packets. Used by outline-ss-server. +// PacketAssociation represents a UDP association that handles individual packets. type PacketAssociation interface { // HandlePacket processes a single incoming packet. // @@ -406,33 +406,12 @@ type association struct { findAccessKeyOnce sync.Once } -var _ ConnAssociation = (*association)(nil) var _ PacketAssociation = (*association)(nil) func (a *association) debugLog(template string, attrs ...slog.Attr) { debugUDP(a.logger, template, attrs...) } -func (a *association) Handle() { - for { - lazySlice := a.bufPool.LazySlice() - buf := lazySlice.Acquire() - n, err := a.Conn.Read(buf) - if errors.Is(err, net.ErrClosed) { - lazySlice.Release() - return - } - pkt := buf[:n] - select { - case <-a.Done(): - lazySlice.Release() - return - default: - go a.HandlePacket(pkt, lazySlice) - } - } -} - // Given the decrypted contents of a UDP packet, return // the payload and the destination address, or an error if // this packet cannot or should not be forwarded. From aa130f05d44611bb352b81c76b680e72a9573224 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 8 Jan 2025 13:04:38 -0500 Subject: [PATCH 48/80] Split `Service` interface into outline-ss-server and Caddy interfaces. --- caddy/shadowsocks_handler.go | 14 +++++++++----- cmd/outline-ss-server/main.go | 11 ++++++++++- service/shadowsocks.go | 19 +++++++++++++------ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index b5edb283..c7247ad9 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -16,6 +16,7 @@ package caddy import ( "container/list" + "context" "fmt" "log/slog" "net" @@ -32,6 +33,11 @@ const serverUDPBufferSize = 64 * 1024 const ssModuleName = "layer4.handlers.shadowsocks" +type OutlineService interface { + HandleStream(ctx context.Context, conn transport.StreamConn) + HandleAssociation(conn net.Conn) error +} + func init() { caddy.RegisterModule(ModuleRegistration{ ID: ssModuleName, @@ -48,7 +54,7 @@ type KeyConfig struct { type ShadowsocksHandler struct { Keys []KeyConfig `json:"keys,omitempty"` - service outline.Service + service OutlineService logger *slog.Logger } @@ -119,11 +125,9 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.service.HandleStream(cx.Context, conn) case net.Conn: - assoc, err := h.service.NewPacketAssociation(conn) - if err != nil { - return fmt.Errorf("failed to handle association: %v", err) + if err := h.service.HandleAssociation(conn); err != nil { + return err } - outline.HandleAssociation(conn, assoc) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index d41bf685..602a5b3f 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -16,6 +16,7 @@ package main import ( "container/list" + "context" "flag" "fmt" "log/slog" @@ -27,6 +28,7 @@ import ( "syscall" "time" + "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" "github.com/lmittmann/tint" "github.com/prometheus/client_golang/prometheus" @@ -60,6 +62,11 @@ func init() { ) } +type OutlineService interface { + HandleStream(ctx context.Context, conn transport.StreamConn) + NewPacketAssociation(conn net.Conn) (service.PacketAssociation, error) +} + type OutlineServer struct { stopConfig func() error lnManager service.ListenerManager @@ -223,6 +230,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { ciphers := service.NewCipherList() ciphers.Update(cipherList) + var ssService OutlineService ssService, err := service.NewShadowsocksService( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), @@ -250,7 +258,8 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { if err != nil { return fmt.Errorf("failed to create cipher list from config: %v", err) } - ssService, err := service.NewShadowsocksService( + var ssService OutlineService + ssService, err = service.NewShadowsocksService( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 062a7087..9137136d 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -16,6 +16,7 @@ package service import ( "context" + "fmt" "log/slog" "net" "time" @@ -41,11 +42,6 @@ type ServiceMetrics interface { AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) } -type Service interface { - HandleStream(ctx context.Context, conn transport.StreamConn) - NewPacketAssociation(conn net.Conn) (PacketAssociation, error) -} - // Option is a Shadowsocks service constructor option. type Option func(s *ssService) @@ -63,7 +59,7 @@ type ssService struct { } // NewShadowsocksService creates a new Shadowsocks service. -func NewShadowsocksService(opts ...Option) (Service, error) { +func NewShadowsocksService(opts ...Option) (*ssService, error) { s := &ssService{} for _, opt := range opts { @@ -146,6 +142,17 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) s.sh.Handle(ctx, conn, metrics) } +// HandleAssociation handles a Shadowsocks packet-based connection. +func (s *ssService) HandleAssociation(conn net.Conn) error { + assoc, err := s.NewPacketAssociation(conn) + if err != nil { + return fmt.Errorf("failed to handle association: %v", err) + } + HandleAssociation(conn, assoc) + return nil +} + + // NewPacketAssociation creates a new Shadowsocks packet-based association. func (s *ssService) NewPacketAssociation(conn net.Conn) (PacketAssociation, error) { var metrics UDPAssociationMetrics From 19fb5f76b9281cead1293fdc2ee455289e8ab581 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 8 Jan 2025 13:22:36 -0500 Subject: [PATCH 49/80] Remove unused const. --- caddy/shadowsocks_handler.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index c7247ad9..5a496a98 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -29,8 +29,6 @@ import ( outline "github.com/Jigsaw-Code/outline-ss-server/service" ) -const serverUDPBufferSize = 64 * 1024 - const ssModuleName = "layer4.handlers.shadowsocks" type OutlineService interface { From c656b36e88e419c876c9d6dcfe4291dc6177877e Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 8 Jan 2025 13:30:58 -0500 Subject: [PATCH 50/80] Don't require `conn` in `HandleAssociation()`. --- service/shadowsocks.go | 2 +- service/udp.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 9137136d..23c17623 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -148,7 +148,7 @@ func (s *ssService) HandleAssociation(conn net.Conn) error { if err != nil { return fmt.Errorf("failed to handle association: %v", err) } - HandleAssociation(conn, assoc) + HandleAssociation(assoc) return nil } diff --git a/service/udp.go b/service/udp.go index 8cbfea6a..94f8430b 100644 --- a/service/udp.go +++ b/service/udp.go @@ -355,11 +355,11 @@ func (m *natmap) Close() error { return err } -func HandleAssociation(conn net.Conn, assoc PacketAssociation) { +func HandleAssociation(assoc PacketAssociation) { for { lazySlice := readBufPool.LazySlice() buf := lazySlice.Acquire() - n, err := conn.Read(buf) + n, err := assoc.Read(buf) if errors.Is(err, net.ErrClosed) { lazySlice.Release() return @@ -384,9 +384,13 @@ type PacketAssociation interface { // released after the packet is processed. HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) + // Read reads data from the association. + Read(b []byte) (n int, err error) + // Done returns a channel that is closed when the association is closed. Done() <-chan struct{} + // Close closes the association and releases any associated resources. Close() error } From 31cec7c37a7c777ea4940510b1891027c0cae9fb Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 9 Jan 2025 15:31:58 -0500 Subject: [PATCH 51/80] Exit the loop if the connection is closed. --- service/udp.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/service/udp.go b/service/udp.go index 94f8430b..9a061ffc 100644 --- a/service/udp.go +++ b/service/udp.go @@ -180,7 +180,7 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, m lazySlice := readBufPool.LazySlice() buffer := lazySlice.Acquire() - func() { + isClosed := func() bool { defer func() { if r := recover(); r != nil { slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) @@ -192,10 +192,10 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, m if err != nil { lazySlice.Release() if errors.Is(err, net.ErrClosed) { - return + return true } slog.Warn("Failed to read from client. Continuing to listen.", "err", err) - return + return false } pkt := buffer[:n] @@ -206,7 +206,7 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, m assoc, err = newAssociation(conn) if err != nil { slog.Error("Failed to handle association", slog.Any("err", err)) - return + return false } metrics.AddNATEntry() @@ -220,7 +220,11 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, m default: go assoc.HandlePacket(pkt, lazySlice) } + return false }() + if isClosed { + return + } } } @@ -390,7 +394,6 @@ type PacketAssociation interface { // Done returns a channel that is closed when the association is closed. Done() <-chan struct{} - // Close closes the association and releases any associated resources. Close() error } From 06426987550b4f51a83479499cddcdff13f4254b Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 10 Jan 2025 15:24:33 -0500 Subject: [PATCH 52/80] Re-use global buffer pool. --- service/udp.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/service/udp.go b/service/udp.go index 9a061ffc..616cce9e 100644 --- a/service/udp.go +++ b/service/udp.go @@ -89,9 +89,7 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis } type packetHandler struct { - logger *slog.Logger - // bufPool stores the byte slices used for reading and decrypting packets. - bufPool slicepool.Pool + logger *slog.Logger ciphers CipherList ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator @@ -107,7 +105,6 @@ func NewPacketHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) P } return &packetHandler{ logger: noopLogger(), - bufPool: slicepool.MakePool(serverUDPBufferSize), ciphers: cipherList, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, @@ -158,7 +155,6 @@ func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssociationMetr m: m, targetConn: targetConn, logger: h.logger.With(slog.Any("client", conn.RemoteAddr()), slog.Any("ltarget", targetConn.LocalAddr())), - bufPool: &h.bufPool, ciphers: h.ciphers, ssm: h.ssm, targetIPValidator: h.targetIPValidator, @@ -359,7 +355,7 @@ func (m *natmap) Close() error { return err } -func HandleAssociation(assoc PacketAssociation) { +func HandleAssociation(assoc PacketAssociation) { for { lazySlice := readBufPool.LazySlice() buf := lazySlice.Acquire() @@ -405,7 +401,6 @@ type association struct { logger *slog.Logger targetConn net.PacketConn cryptoKey *shadowsocks.EncryptionKey - bufPool *slicepool.Pool ciphers CipherList ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator @@ -454,7 +449,7 @@ func (a *association) HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) { a.findAccessKeyOnce.Do(func() { ip := a.raddr.AddrPort().Addr() var keyID string - textLazySlice := a.bufPool.LazySlice() + textLazySlice := readBufPool.LazySlice() textBuf := textLazySlice.Acquire() unpackStart := time.Now() textData, keyID, a.cryptoKey, err = findAccessKeyUDP(ip, textBuf, pkt, a.ciphers, a.logger) From a16c1b399f82e5bb126fa41862d81b6cd2d56c70 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 10 Jan 2025 15:30:02 -0500 Subject: [PATCH 53/80] Remove app-specific interfaces. Instead move the `HandleAssociation` logic into the Caddy Shadowsocks handler. --- caddy/shadowsocks_handler.go | 15 ++++++--------- cmd/outline-ss-server/main.go | 11 +---------- service/shadowsocks.go | 17 +++++------------ 3 files changed, 12 insertions(+), 31 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 5a496a98..53c923d0 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -16,7 +16,6 @@ package caddy import ( "container/list" - "context" "fmt" "log/slog" "net" @@ -31,11 +30,6 @@ import ( const ssModuleName = "layer4.handlers.shadowsocks" -type OutlineService interface { - HandleStream(ctx context.Context, conn transport.StreamConn) - HandleAssociation(conn net.Conn) error -} - func init() { caddy.RegisterModule(ModuleRegistration{ ID: ssModuleName, @@ -52,7 +46,7 @@ type KeyConfig struct { type ShadowsocksHandler struct { Keys []KeyConfig `json:"keys,omitempty"` - service OutlineService + service outline.Service logger *slog.Logger } @@ -123,9 +117,12 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.service.HandleStream(cx.Context, conn) case net.Conn: - if err := h.service.HandleAssociation(conn); err != nil { - return err + assoc, err := h.service.NewPacketAssociation(conn) + if err != nil { + return fmt.Errorf("failed to handle association: %v", err) } + outline.HandleAssociation(assoc) + return nil default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index 602a5b3f..d41bf685 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -16,7 +16,6 @@ package main import ( "container/list" - "context" "flag" "fmt" "log/slog" @@ -28,7 +27,6 @@ import ( "syscall" "time" - "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" "github.com/lmittmann/tint" "github.com/prometheus/client_golang/prometheus" @@ -62,11 +60,6 @@ func init() { ) } -type OutlineService interface { - HandleStream(ctx context.Context, conn transport.StreamConn) - NewPacketAssociation(conn net.Conn) (service.PacketAssociation, error) -} - type OutlineServer struct { stopConfig func() error lnManager service.ListenerManager @@ -230,7 +223,6 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { ciphers := service.NewCipherList() ciphers.Update(cipherList) - var ssService OutlineService ssService, err := service.NewShadowsocksService( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), @@ -258,8 +250,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { if err != nil { return fmt.Errorf("failed to create cipher list from config: %v", err) } - var ssService OutlineService - ssService, err = service.NewShadowsocksService( + ssService, err := service.NewShadowsocksService( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 23c17623..95374bfb 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -16,7 +16,6 @@ package service import ( "context" - "fmt" "log/slog" "net" "time" @@ -42,6 +41,11 @@ type ServiceMetrics interface { AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) } +type Service interface { + HandleStream(ctx context.Context, conn transport.StreamConn) + NewPacketAssociation(conn net.Conn) (PacketAssociation, error) +} + // Option is a Shadowsocks service constructor option. type Option func(s *ssService) @@ -142,17 +146,6 @@ func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) s.sh.Handle(ctx, conn, metrics) } -// HandleAssociation handles a Shadowsocks packet-based connection. -func (s *ssService) HandleAssociation(conn net.Conn) error { - assoc, err := s.NewPacketAssociation(conn) - if err != nil { - return fmt.Errorf("failed to handle association: %v", err) - } - HandleAssociation(assoc) - return nil -} - - // NewPacketAssociation creates a new Shadowsocks packet-based association. func (s *ssService) NewPacketAssociation(conn net.Conn) (PacketAssociation, error) { var metrics UDPAssociationMetrics From 77983fceb461180f4f89dc577ed492e48f5ddbf9 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 10 Jan 2025 23:46:27 -0500 Subject: [PATCH 54/80] Decouple the association and the packet handling. --- caddy/shadowsocks_handler.go | 28 ++-- cmd/outline-ss-server/main.go | 36 +++- prometheus/metrics.go | 14 +- service/shadowsocks.go | 79 +++------ service/udp.go | 302 ++++++++++++++++++---------------- 5 files changed, 229 insertions(+), 230 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 53c923d0..8358414f 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -19,6 +19,7 @@ import ( "fmt" "log/slog" "net" + "time" "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" @@ -30,6 +31,9 @@ import ( const ssModuleName = "layer4.handlers.shadowsocks" +// A UDP NAT timeout of at least 5 minutes is recommended in RFC 4787 Section 4.3. +const defaultNatTimeout time.Duration = 5 * time.Minute + func init() { caddy.RegisterModule(ModuleRegistration{ ID: ssModuleName, @@ -46,8 +50,11 @@ type KeyConfig struct { type ShadowsocksHandler struct { Keys []KeyConfig `json:"keys,omitempty"` - service outline.Service - logger *slog.Logger + streamHandler outline.StreamHandler + packetHandler outline.PacketHandler + metrics outline.ServiceMetrics + tgtListener transport.PacketListener + logger *slog.Logger } var ( @@ -71,6 +78,7 @@ func (h *ShadowsocksHandler) Provision(ctx caddy.Context) error { if !ok { return fmt.Errorf("module `%s` is of type `%T`, expected `OutlineApp`", outlineModuleName, app) } + h.metrics = app.Metrics if len(h.Keys) == 0 { h.logger.Warn("no keys configured") @@ -98,16 +106,13 @@ func (h *ShadowsocksHandler) Provision(ctx caddy.Context) error { ciphers := outline.NewCipherList() ciphers.Update(cipherList) - service, err := outline.NewShadowsocksService( + h.streamHandler, h.packetHandler = outline.NewShadowsocksHandlers( outline.WithLogger(h.logger), outline.WithCiphers(ciphers), - outline.WithMetrics(app.Metrics), + outline.WithMetrics(h.metrics), outline.WithReplayCache(&app.ReplayCache), ) - if err != nil { - return err - } - h.service = service + h.tgtListener = outline.MakeTargetUDPListener(defaultNatTimeout, 0) return nil } @@ -115,14 +120,13 @@ func (h *ShadowsocksHandler) Provision(ctx caddy.Context) error { func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) error { switch conn := cx.Conn.(type) { case transport.StreamConn: - h.service.HandleStream(cx.Context, conn) + h.streamHandler.Handle(cx.Context, conn, h.metrics.AddOpenTCPConnection(conn)) case net.Conn: - assoc, err := h.service.NewPacketAssociation(conn) + assoc, err := outline.NewPacketAssociation(conn, h.tgtListener, h.metrics.AddOpenUDPAssociation(conn)) if err != nil { return fmt.Errorf("failed to handle association: %v", err) } - outline.HandleAssociation(assoc) - return nil + outline.HandleAssociation(assoc, h.packetHandler.Handle) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index d41bf685..58851906 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -16,6 +16,7 @@ package main import ( "container/list" + "context" "flag" "fmt" "log/slog" @@ -27,6 +28,7 @@ import ( "syscall" "time" + "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" "github.com/lmittmann/tint" "github.com/prometheus/client_golang/prometheus" @@ -223,11 +225,10 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { ciphers := service.NewCipherList() ciphers.Update(cipherList) - ssService, err := service.NewShadowsocksService( + streamHandler, packetHandler := service.NewShadowsocksHandlers( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), - service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, 0)), service.WithLogger(slog.Default()), ) ln, err := lnSet.ListenStream(addr) @@ -235,14 +236,24 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("TCP service started.", "address", ln.Addr().String()) - go service.StreamServe(ln.AcceptStream, ssService.HandleStream) + go service.StreamServe(ln.AcceptStream, func(ctx context.Context, conn transport.StreamConn) { + streamHandler.Handle(ctx, conn, s.serviceMetrics.AddOpenTCPConnection(conn)) + }) pc, err := lnSet.ListenPacket(addr) if err != nil { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - go service.PacketServe(pc, ssService.NewPacketAssociation, s.serverMetrics) + tgtListener := service.MakeTargetUDPListener(s.natTimeout, 0) + go service.PacketServe(pc, func(conn net.Conn) (service.PacketAssociation, error) { + m := s.serviceMetrics.AddOpenUDPAssociation(conn) + assoc, err := service.NewPacketAssociation(conn, tgtListener, m) + if err != nil { + return nil, fmt.Errorf("failed to handle association: %v", err) + } + return assoc, nil + }, packetHandler.Handle, s.serverMetrics) } for _, serviceConfig := range config.Services { @@ -250,12 +261,11 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { if err != nil { return fmt.Errorf("failed to create cipher list from config: %v", err) } - ssService, err := service.NewShadowsocksService( + streamHandler, packetHandler := service.NewShadowsocksHandlers( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), service.WithStreamDialer(service.MakeValidatingTCPStreamDialer(onet.RequirePublicIP, serviceConfig.Dialer.Fwmark)), - service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, serviceConfig.Dialer.Fwmark)), service.WithLogger(slog.Default()), ) if err != nil { @@ -274,7 +284,9 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { } return serviceConfig.Dialer.Fwmark }()) - go service.StreamServe(ln.AcceptStream, ssService.HandleStream) + go service.StreamServe(ln.AcceptStream, func(ctx context.Context, conn transport.StreamConn) { + streamHandler.Handle(ctx, conn, s.serviceMetrics.AddOpenTCPConnection(conn)) + }) case listenerTypeUDP: pc, err := lnSet.ListenPacket(lnConfig.Address) if err != nil { @@ -286,7 +298,15 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { } return serviceConfig.Dialer.Fwmark }()) - go service.PacketServe(pc, ssService.NewPacketAssociation, s.serverMetrics) + tgtListener := service.MakeTargetUDPListener(s.natTimeout, serviceConfig.Dialer.Fwmark) + go service.PacketServe(pc, func(conn net.Conn) (service.PacketAssociation, error) { + m := s.serviceMetrics.AddOpenUDPAssociation(conn) + assoc, err := service.NewPacketAssociation(conn, tgtListener, m) + if err != nil { + return nil, fmt.Errorf("failed to handle association: %v", err) + } + return assoc, nil + }, packetHandler.Handle, s.serverMetrics) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/prometheus/metrics.go b/prometheus/metrics.go index 698f1083..ffc2a7de 100644 --- a/prometheus/metrics.go +++ b/prometheus/metrics.go @@ -509,6 +509,8 @@ func (m *serviceMetrics) getIPInfoFromAddr(addr net.Addr) ipinfo.IPInfo { return ipInfo } +// TODO: Split TCP and UDP metrics. + func (m *serviceMetrics) AddOpenTCPConnection(clientConn net.Conn) service.TCPConnMetrics { clientAddr := clientConn.RemoteAddr() clientInfo := m.getIPInfoFromAddr(clientAddr) @@ -521,12 +523,12 @@ func (m *serviceMetrics) AddOpenUDPAssociation(clientConn net.Conn) service.UDPA return newUDPAssociationMetrics(m.udpServiceMetrics, m.tunnelTimeMetrics, clientAddr, clientInfo) } -func (m *serviceMetrics) AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) { - if proto == "tcp" { - m.tcpServiceMetrics.AddCipherSearch(accessKeyFound, timeToCipher) - } else if proto == "udp" { - m.udpServiceMetrics.AddCipherSearch(accessKeyFound, timeToCipher) - } +func (m *serviceMetrics) AddTCPCipherSearch(accessKeyFound bool, timeToCipher time.Duration) { + m.tcpServiceMetrics.AddCipherSearch(accessKeyFound, timeToCipher) +} + +func (m *serviceMetrics) AddUDPCipherSearch(accessKeyFound bool, timeToCipher time.Duration) { + m.udpServiceMetrics.AddCipherSearch(accessKeyFound, timeToCipher) } // addIfNonZero helps avoid the creation of series that are always zero. diff --git a/service/shadowsocks.go b/service/shadowsocks.go index 95374bfb..dd4ac517 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -15,7 +15,6 @@ package service import ( - "context" "log/slog" "net" "time" @@ -38,12 +37,8 @@ type ShadowsocksConnMetrics interface { type ServiceMetrics interface { AddOpenUDPAssociation(conn net.Conn) UDPAssociationMetrics AddOpenTCPConnection(conn net.Conn) TCPConnMetrics - AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration) -} - -type Service interface { - HandleStream(ctx context.Context, conn transport.StreamConn) - NewPacketAssociation(conn net.Conn) (PacketAssociation, error) + AddTCPCipherSearch(accessKeyFound bool, timeToCipher time.Duration) + AddUDPCipherSearch(accessKeyFound bool, timeToCipher time.Duration) } // Option is a Shadowsocks service constructor option. @@ -51,47 +46,38 @@ type Option func(s *ssService) type ssService struct { logger *slog.Logger - metrics ServiceMetrics ciphers CipherList + metrics ServiceMetrics targetIPValidator onet.TargetIPValidator replayCache *ReplayCache - streamDialer transport.StreamDialer - sh StreamHandler - packetListener transport.PacketListener - ph PacketHandler + streamDialer transport.StreamDialer } -// NewShadowsocksService creates a new Shadowsocks service. -func NewShadowsocksService(opts ...Option) (*ssService, error) { - s := &ssService{} +// NewShadowsocksHandlers creates new Shadowsocks stream and packet handlers. +func NewShadowsocksHandlers(opts ...Option) (StreamHandler, PacketHandler) { + s := &ssService{ + logger: noopLogger(), + } for _, opt := range opts { opt(s) } - // If no logger is provided via options, use a noop logger. - if s.logger == nil { - s.logger = noopLogger() - } - // TODO: Register initial data metrics at zero. - s.sh = NewStreamHandler( - NewShadowsocksStreamAuthenticator(s.ciphers, s.replayCache, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "tcp"}, s.logger), + sh := NewStreamHandler( + NewShadowsocksStreamAuthenticator(s.ciphers, s.replayCache, &ssConnMetrics{s.metrics.AddTCPCipherSearch}, s.logger), tcpReadTimeout, ) if s.streamDialer != nil { - s.sh.SetTargetDialer(s.streamDialer) + sh.SetTargetDialer(s.streamDialer) } - s.sh.SetLogger(s.logger) + sh.SetLogger(s.logger) - s.ph = NewPacketHandler(s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"}) - if s.packetListener != nil { - s.ph.SetTargetPacketListener(s.packetListener) - } - s.ph.SetLogger(s.logger) + ph := NewPacketHandler(s.ciphers, &ssConnMetrics{s.metrics.AddUDPCipherSearch}) + ph.SetLogger(s.logger) - return s, nil + return sh, ph } // WithLogger can be used to provide a custom log target. If not provided, @@ -109,7 +95,6 @@ func WithCiphers(ciphers CipherList) Option { } } -// WithMetrics option function. func WithMetrics(metrics ServiceMetrics) Option { return func(s *ssService) { s.metrics = metrics @@ -130,41 +115,15 @@ func WithStreamDialer(dialer transport.StreamDialer) Option { } } -// WithPacketListener option function. -func WithPacketListener(listener transport.PacketListener) Option { - return func(s *ssService) { - s.packetListener = listener - } -} - -// HandleStream handles a Shadowsocks stream-based connection. -func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) { - var metrics TCPConnMetrics - if s.metrics != nil { - metrics = s.metrics.AddOpenTCPConnection(conn) - } - s.sh.Handle(ctx, conn, metrics) -} - -// NewPacketAssociation creates a new Shadowsocks packet-based association. -func (s *ssService) NewPacketAssociation(conn net.Conn) (PacketAssociation, error) { - var metrics UDPAssociationMetrics - if s.metrics != nil { - metrics = s.metrics.AddOpenUDPAssociation(conn) - } - return s.ph.NewPacketAssociation(conn, metrics) -} - type ssConnMetrics struct { - ServiceMetrics - proto string + metricFunc func(accessKeyFound bool, timeToCipher time.Duration) } var _ ShadowsocksConnMetrics = (*ssConnMetrics)(nil) func (cm *ssConnMetrics) AddCipherSearch(accessKeyFound bool, timeToCipher time.Duration) { - if cm.ServiceMetrics != nil { - cm.ServiceMetrics.AddCipherSearch(cm.proto, accessKeyFound, timeToCipher) + if cm.metricFunc != nil { + cm.metricFunc(accessKeyFound, timeToCipher) } } diff --git a/service/udp.go b/service/udp.go index 616cce9e..18d96a8b 100644 --- a/service/udp.go +++ b/service/udp.go @@ -47,23 +47,18 @@ type UDPAssociationMetrics interface { AddClose() } -const ( - // A UDP NAT timeout of at least 5 minutes is recommended in RFC 4787 Section 4.3. - defaultNatTimeout time.Duration = 5 * time.Minute - - // Max UDP buffer size for the server code. - serverUDPBufferSize = 64 * 1024 -) +// Max UDP buffer size for the server code. +const serverUDPBufferSize = 64 * 1024 // Buffer pool used for reading UDP packets. var readBufPool = slicepool.MakePool(serverUDPBufferSize) // Wrapper for slog.Debug during UDP proxying. -func debugUDP(l *slog.Logger, template string, attrs ...slog.Attr) { +func debugUDP(l *slog.Logger, msg string, attrs ...slog.Attr) { // This is an optimization to reduce unnecessary allocations due to an interaction // between Go's inlining/escape analysis and varargs functions like slog.Debug. if l.Enabled(nil, slog.LevelDebug) { - l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("UDP: %s", template), attrs...) + l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("UDP: %s", msg), attrs...) } } @@ -93,7 +88,7 @@ type packetHandler struct { ciphers CipherList ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator - targetListener transport.PacketListener + onces map[string]*sync.Once } var _ PacketHandler = (*packetHandler)(nil) @@ -108,20 +103,16 @@ func NewPacketHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) P ciphers: cipherList, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, - targetListener: MakeTargetUDPListener(defaultNatTimeout, 0), } } // PacketHandler is a handler that handles UDP assocations. type PacketHandler interface { + Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc PacketAssociation) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) - // SetTargetPacketListener sets the packet listener to use for target connections. - SetTargetPacketListener(targetListener transport.PacketListener) - // NewPacketAssociation creates a new PacketAssociation. - NewPacketAssociation(conn net.Conn, connMetrics UDPAssociationMetrics) (PacketAssociation, error) } func (h *packetHandler) SetLogger(l *slog.Logger) { @@ -135,40 +126,95 @@ func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPVali h.targetIPValidator = targetIPValidator } -func (h *packetHandler) SetTargetPacketListener(targetListener transport.PacketListener) { - h.targetListener = targetListener +func (h *packetHandler) Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc PacketAssociation) { + l := h.logger.With(slog.Any("association", assoc)) + defer lazySlice.Release() + defer debugUDP(l, "Done") + + debugUDP(l, "Outbound packet.", slog.Int("bytes", len(pkt))) + + var proxyTargetBytes int + connError := func() *onet.ConnectionError { + var textData []byte + + cryptoKey, err := assoc.Authenticate(func() (keyID string, cryptoKey *shadowsocks.EncryptionKey, keyErr error) { + ip := assoc.RemoteAddr().AddrPort().Addr() + textLazySlice := readBufPool.LazySlice() + textBuf := textLazySlice.Acquire() + unpackStart := time.Now() + textData, keyID, cryptoKey, keyErr = findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) + timeToCipher := time.Since(unpackStart) + textLazySlice.Release() + h.ssm.AddCipherSearch(keyErr == nil, timeToCipher) + return keyID, cryptoKey, keyErr + }) + + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) + } + + if textData == nil { + // This is a subsequent packet. First packets are already decrypted as part of the + // initial access key search. + unpackStart := time.Now() + textData, err = shadowsocks.Unpack(nil, pkt, cryptoKey) + timeToCipher := time.Since(unpackStart) + h.ssm.AddCipherSearch(err == nil, timeToCipher) + } + + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) + } + + payload, tgtUDPAddr, onetErr := h.validatePacket(textData) + if onetErr != nil { + return onetErr + } + + debugUDP(l, "Proxy exit.") + proxyTargetBytes, err = assoc.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature + if err != nil { + return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + } + return nil + }() + + status := "OK" + if connError != nil { + debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + status = connError.Status + } + assoc.Metrics().AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } -func (h *packetHandler) NewPacketAssociation(conn net.Conn, m UDPAssociationMetrics) (PacketAssociation, error) { - if m == nil { - m = &NoOpUDPAssociationMetrics{} +// Given the decrypted contents of a UDP packet, return +// the payload and the destination address, or an error if +// this packet cannot or should not be forwarded. +func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { + tgtAddr := socks.SplitAddr(textData) + if tgtAddr == nil { + return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) } - // Create the target connection - targetConn, err := h.targetListener.ListenPacket(context.Background()) + tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String()) if err != nil { - return nil, fmt.Errorf("failed to create target connection: %w", err) + return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err) + } + if err := h.targetIPValidator(tgtUDPAddr.IP); err != nil { + return nil, nil, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address") } - return &association{ - Conn: conn, - m: m, - targetConn: targetConn, - logger: h.logger.With(slog.Any("client", conn.RemoteAddr()), slog.Any("ltarget", targetConn.LocalAddr())), - ciphers: h.ciphers, - ssm: h.ssm, - targetIPValidator: h.targetIPValidator, - doneCh: make(chan struct{}), - }, nil + payload := textData[len(tgtAddr):] + return payload, tgtUDPAddr, nil } type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates -// and manages NAT associations, and invokes the `HandlePacket` function for -// each packet. It uses a NAT map to track active associations and handles -// their lifecycle. -func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, metrics NATMetrics) { +// and manages NAT associations, and invokes the `handle` function for each +// packet. It uses a NAT map to track active associations and handles their +// lifecycle. +func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, handle PacketHandleFunc, metrics NATMetrics) { nm := newNATmap() defer nm.Close() @@ -214,7 +260,7 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, m metrics.RemoveNATEntry() nm.Del(addr.String()) default: - go assoc.HandlePacket(pkt, lazySlice) + go handle(pkt, lazySlice, assoc) } return false }() @@ -355,7 +401,14 @@ func (m *natmap) Close() error { return err } -func HandleAssociation(assoc PacketAssociation) { +// PacketHandleFunc processes a single incoming packet. +// +// pkt contains the raw packet data. +// lazySlice is the LazySlice that holds the pkt buffer, which should be +// released after the packet is processed. +type PacketHandleFunc func(pkt []byte, lazySlice slicepool.LazySlice, assoc PacketAssociation) + +func HandleAssociation(assoc PacketAssociation, handle PacketHandleFunc) { for { lazySlice := readBufPool.LazySlice() buf := lazySlice.Acquire() @@ -370,141 +423,90 @@ func HandleAssociation(assoc PacketAssociation) { lazySlice.Release() return default: - go assoc.HandlePacket(pkt, lazySlice) + go handle(pkt, lazySlice, assoc) } } } -// PacketAssociation represents a UDP association that handles individual packets. +// PacketAssociation represents a UDP association. type PacketAssociation interface { - // HandlePacket processes a single incoming packet. - // - // pkt contains the raw packet data. - // lazySlice is the LazySlice that holds the pkt buffer, which should be - // released after the packet is processed. - HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) - // Read reads data from the association. Read(b []byte) (n int, err error) + // WriteTo writes data to the association. + WriteTo(b []byte, addr net.Addr) (int, error) + + // Authenticate authenticates the association. + Authenticate(authenticateFunc PacketAuthenticateFunc) (*shadowsocks.EncryptionKey, error) + + // RemoteAddr returns the remote network address of the association, if known. + RemoteAddr() *net.UDPAddr + // Done returns a channel that is closed when the association is closed. Done() <-chan struct{} // Close closes the association and releases any associated resources. Close() error + + // Returns association metrics. + // TODO(sbruens): Refactor so this isn't needed. + Metrics() UDPAssociationMetrics } +type PacketAuthenticateFunc func() (string, *shadowsocks.EncryptionKey, error) + type association struct { net.Conn - raddr net.UDPAddr - m UDPAssociationMetrics - logger *slog.Logger - targetConn net.PacketConn - cryptoKey *shadowsocks.EncryptionKey - ciphers CipherList - ssm ShadowsocksConnMetrics - targetIPValidator onet.TargetIPValidator - doneCh chan struct{} - findAccessKeyOnce sync.Once + targetConn net.PacketConn + authenticateOnce sync.Once + cryptoKey *shadowsocks.EncryptionKey + m UDPAssociationMetrics + doneCh chan struct{} } var _ PacketAssociation = (*association)(nil) +var _ slog.LogValuer = (*association)(nil) -func (a *association) debugLog(template string, attrs ...slog.Attr) { - debugUDP(a.logger, template, attrs...) -} - -// Given the decrypted contents of a UDP packet, return -// the payload and the destination address, or an error if -// this packet cannot or should not be forwarded. -func (a *association) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { - tgtAddr := socks.SplitAddr(textData) - if tgtAddr == nil { - return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) - } - - tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String()) +// NewPacketAssociation creates a new packet-based association. +func NewPacketAssociation(conn net.Conn, listener transport.PacketListener, m UDPAssociationMetrics) (PacketAssociation, error) { + // Create the target connection + targetConn, err := listener.ListenPacket(context.Background()) if err != nil { - return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err) - } - if err := a.targetIPValidator(tgtUDPAddr.IP); err != nil { - return nil, nil, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address") + return nil, fmt.Errorf("failed to create target connection: %w", err) } - payload := textData[len(tgtAddr):] - return payload, tgtUDPAddr, nil + return &association{ + Conn: conn, + targetConn: targetConn, + m: m, + doneCh: make(chan struct{}), + }, nil } -func (a *association) HandlePacket(pkt []byte, lazySlice slicepool.LazySlice) { - defer lazySlice.Release() - defer a.debugLog("Done") - - a.debugLog("Outbound packet.", slog.Int("bytes", len(pkt))) - - var proxyTargetBytes int - connError := func() *onet.ConnectionError { - var textData []byte - var err error - - a.findAccessKeyOnce.Do(func() { - ip := a.raddr.AddrPort().Addr() - var keyID string - textLazySlice := readBufPool.LazySlice() - textBuf := textLazySlice.Acquire() - unpackStart := time.Now() - textData, keyID, a.cryptoKey, err = findAccessKeyUDP(ip, textBuf, pkt, a.ciphers, a.logger) - timeToCipher := time.Since(unpackStart) - textLazySlice.Release() - a.ssm.AddCipherSearch(err == nil, timeToCipher) - - if err != nil { - return - } - - a.m.AddAuthentication(keyID) - go a.timedCopy() - }) - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) - } - - if a.cryptoKey == nil { - // This should not happen since findAccessKeyUDP should have set `a.cryptoKey`. - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) - } - - if textData == nil { - // This is a subsequent packet. First packets are already decrypted as part of the - // initial access key search. - unpackStart := time.Now() - textData, err = shadowsocks.Unpack(nil, pkt, a.cryptoKey) - timeToCipher := time.Since(unpackStart) - a.ssm.AddCipherSearch(err == nil, timeToCipher) - } +func (a *association) WriteTo(b []byte, addr net.Addr) (int, error) { + return a.targetConn.WriteTo(b, addr) +} - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) - } +func (a *association) RemoteAddr() *net.UDPAddr { + return a.Conn.RemoteAddr().(*net.UDPAddr) +} - payload, tgtUDPAddr, onetErr := a.validatePacket(textData) - if onetErr != nil { - return onetErr - } +func (a *association) Authenticate(authenticateFunc PacketAuthenticateFunc) (*shadowsocks.EncryptionKey, error) { + var err error + a.authenticateOnce.Do(func() { + var keyID string + keyID, a.cryptoKey, err = authenticateFunc() + a.m.AddAuthentication(keyID) - a.debugLog("Proxy exit.") - proxyTargetBytes, err = a.targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + return } - return nil - }() - status := "OK" - if connError != nil { - a.logger.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) - status = connError.Status - } - a.m.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) + // TODO(sbruens): Pass in a `handle` function to handle shadowsocks and move it to + // the packet handler. + go a.timedCopy() + }) + return a.cryptoKey, err } func (a *association) Done() <-chan struct{} { @@ -516,6 +518,17 @@ func (a *association) Close() error { return a.SetReadDeadline(now) } +func (a *association) Metrics() UDPAssociationMetrics { + return a.m +} + +func (a *association) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("client", a.Conn.RemoteAddr()), + slog.Any("ltarget", a.targetConn.LocalAddr()), + ) +} + // Get the maximum length of the shadowsocks address header by parsing // and serializing an IPv6 address from the example range. var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) @@ -560,7 +573,8 @@ func (a *association) timedCopy() { return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) } - a.debugLog("Got response.", slog.Any("rtarget", raddr)) + // TODO(sbruens): Figure out the logger here and below. + // debugUDP(l, "Got response.", slog.Any("rtarget", raddr)) srcAddr := socks.ParseAddr(raddr.String()) addrStart := bodyStart - len(srcAddr) // `plainTextBuf` concatenates the SOCKS address and body: @@ -589,7 +603,7 @@ func (a *association) timedCopy() { }() status := "OK" if connError != nil { - a.debugLog("Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + //debugUDP(a.Logger(), "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } if expired { From f3d63aeea0216ac0c26889283d2c8903e1503492 Mon Sep 17 00:00:00 2001 From: sbruens Date: Sat, 11 Jan 2025 01:35:12 -0500 Subject: [PATCH 55/80] Move timedCopy handling to the packet handler. --- service/udp.go | 294 +++++++++++++++++++++++++++++-------------------- 1 file changed, 176 insertions(+), 118 deletions(-) diff --git a/service/udp.go b/service/udp.go index 18d96a8b..9ee33c61 100644 --- a/service/udp.go +++ b/service/udp.go @@ -108,7 +108,7 @@ func NewPacketHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) P // PacketHandler is a handler that handles UDP assocations. type PacketHandler interface { - Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc PacketAssociation) + Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. @@ -126,7 +126,7 @@ func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPVali h.targetIPValidator = targetIPValidator } -func (h *packetHandler) Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc PacketAssociation) { +func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics { l := h.logger.With(slog.Any("association", assoc)) defer lazySlice.Release() defer debugUDP(l, "Done") @@ -138,7 +138,7 @@ func (h *packetHandler) Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc var textData []byte cryptoKey, err := assoc.Authenticate(func() (keyID string, cryptoKey *shadowsocks.EncryptionKey, keyErr error) { - ip := assoc.RemoteAddr().AddrPort().Addr() + ip := assoc.ClientAddr().AddrPort().Addr() textLazySlice := readBufPool.LazySlice() textBuf := textLazySlice.Acquire() unpackStart := time.Now() @@ -147,7 +147,7 @@ func (h *packetHandler) Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc textLazySlice.Release() h.ssm.AddCipherSearch(keyErr == nil, timeToCipher) return keyID, cryptoKey, keyErr - }) + }, h.handleTargetPacket) if err != nil { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) @@ -172,7 +172,7 @@ func (h *packetHandler) Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc } debugUDP(l, "Proxy exit.") - proxyTargetBytes, err = assoc.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature + proxyTargetBytes, err = assoc.WriteToTarget(payload, tgtUDPAddr) // accept only UDPAddr despite the signature if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) } @@ -184,7 +184,11 @@ func (h *packetHandler) Handle(pkt []byte, lazySlice slicepool.LazySlice, assoc debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } - assoc.Metrics().AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) + return &packetMetrics{ + status: status, + bytesIn: int64(len(pkt)), + bytesOut: int64(proxyTargetBytes), + } } // Given the decrypted contents of a UDP packet, return @@ -208,13 +212,93 @@ func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, * return payload, tgtUDPAddr, nil } +// Get the maximum length of the shadowsocks address header by parsing +// and serializing an IPv6 address from the example range. +var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) + +func (h *packetHandler) handleTargetPacket(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics { + l := h.logger.With(slog.Any("association", assoc)) + + expired := false + + saltSize := cryptoKey.SaltSize() + // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). + bodyStart := saltSize + maxAddrLen + + var bodyLen, proxyClientBytes int + connError := func() *onet.ConnectionError { + var ( + raddr net.Addr + err error + ) + // `readBuf` receives the plaintext body in `pkt`: + // [padding?][salt][address][body][tag][unused] + // |-- bodyStart --|[ readBuf ] + readBuf := pkt[bodyStart:] + bodyLen, raddr, err := assoc.ReadFromTarget(readBuf) + if err != nil { + if netErr, ok := err.(net.Error); ok { + if netErr.Timeout() { + expired = true + return nil + } + } + return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) + } + + debugUDP(l, "Got response.", slog.Any("rtarget", raddr)) + srcAddr := socks.ParseAddr(raddr.String()) + addrStart := bodyStart - len(srcAddr) + // `plainTextBuf` concatenates the SOCKS address and body: + // [padding?][salt][address][body][tag][unused] + // |-- addrStart -|[plaintextBuf ] + plaintextBuf := pkt[addrStart : bodyStart+bodyLen] + copy(plaintextBuf, srcAddr) + + // saltStart is 0 if raddr is IPv6. + saltStart := addrStart - saltSize + // `packBuf` adds space for the salt and tag. + // `buf` shows the space that was used. + // [padding?][salt][address][body][tag][unused] + // [ packBuf ] + // [ buf ] + packBuf := pkt[saltStart:] + buf, err := shadowsocks.Pack(packBuf, plaintextBuf, cryptoKey) // Encrypt in-place + if err != nil { + return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) + } + proxyClientBytes, err = assoc.WriteToClient(buf) + if err != nil { + return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) + } + return nil + }() + status := "OK" + if connError != nil { + debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + status = connError.Status + } + return &packetMetrics{ + status: status, + expired: expired, + bytesIn: int64(bodyLen), + bytesOut: int64(proxyClientBytes), + } +} + +type packetMetrics struct { + status string + expired bool + bytesIn, bytesOut int64 +} + type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates // and manages NAT associations, and invokes the `handle` function for each // packet. It uses a NAT map to track active associations and handles their // lifecycle. -func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, handle PacketHandleFunc, metrics NATMetrics) { +func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, handle PacketHandleFuncWithLazySlice, metrics NATMetrics) { nm := newNATmap() defer nm.Close() @@ -260,7 +344,10 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, h metrics.RemoveNATEntry() nm.Del(addr.String()) default: - go handle(pkt, lazySlice, assoc) + go func() { + metrics := handle(pkt, assoc, lazySlice) + assoc.Metrics().AddPacketFromClient(metrics.status, metrics.bytesIn, metrics.bytesOut) + }() } return false }() @@ -402,17 +489,20 @@ func (m *natmap) Close() error { } // PacketHandleFunc processes a single incoming packet. +type PacketHandleFunc func(pkt []byte, assoc PacketAssociation) *packetMetrics + +// PacketHandleFuncWithLazySlice processes a single incoming packet. // -// pkt contains the raw packet data. + // lazySlice is the LazySlice that holds the pkt buffer, which should be // released after the packet is processed. -type PacketHandleFunc func(pkt []byte, lazySlice slicepool.LazySlice, assoc PacketAssociation) +type PacketHandleFuncWithLazySlice func(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics -func HandleAssociation(assoc PacketAssociation, handle PacketHandleFunc) { +func HandleAssociation(assoc PacketAssociation, handle PacketHandleFuncWithLazySlice) { for { lazySlice := readBufPool.LazySlice() buf := lazySlice.Acquire() - n, err := assoc.Read(buf) + n, err := assoc.ReadFromClient(buf) if errors.Is(err, net.ErrClosed) { lazySlice.Release() return @@ -423,24 +513,51 @@ func HandleAssociation(assoc PacketAssociation, handle PacketHandleFunc) { lazySlice.Release() return default: - go handle(pkt, lazySlice, assoc) + go func() { + metrics := handle(pkt, assoc, lazySlice) + assoc.Metrics().AddPacketFromClient(metrics.status, metrics.bytesIn, metrics.bytesOut) + }() + } + } +} + +// HandleAssociationTimedCopy handles the target-side of the association by +// copying from target to client until read timeout. +func HandleAssociationTimedCopy(assoc PacketAssociation, handle PacketHandleFunc) { + defer assoc.CloseTarget() + + // pkt is used for in-place encryption of downstream UDP packets. + // Padding is only used if the address is IPv4. + pkt := make([]byte, serverUDPBufferSize) + + for { + metrics := handle(pkt, assoc) + if metrics.expired { + break } + assoc.Metrics().AddPacketFromTarget(metrics.status, metrics.bytesIn, metrics.bytesOut) } } // PacketAssociation represents a UDP association. type PacketAssociation interface { - // Read reads data from the association. - Read(b []byte) (n int, err error) + // ReadFromClient reads data from the client side of the association. + ReadFromClient(b []byte) (n int, err error) - // WriteTo writes data to the association. - WriteTo(b []byte, addr net.Addr) (int, error) + // WriteToClient writes data to the client side of the association. + WriteToClient(b []byte) (n int, err error) + + // ReadFromTarget reads data from the target side of the association. + ReadFromTarget(p []byte) (n int, addr net.Addr, err error) + + // WriteToTarget writes data to the target side of the association. + WriteToTarget(b []byte, addr net.Addr) (int, error) // Authenticate authenticates the association. - Authenticate(authenticateFunc PacketAuthenticateFunc) (*shadowsocks.EncryptionKey, error) + Authenticate(authenticate PacketAuthenticateFunc, handleTarget func(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics) (*shadowsocks.EncryptionKey, error) - // RemoteAddr returns the remote network address of the association, if known. - RemoteAddr() *net.UDPAddr + // ClientAddr returns the remote network address of the client connection, if known. + ClientAddr() *net.UDPAddr // Done returns a channel that is closed when the association is closed. Done() <-chan struct{} @@ -448,15 +565,16 @@ type PacketAssociation interface { // Close closes the association and releases any associated resources. Close() error + // Closes the target side of the association. + CloseTarget() error + // Returns association metrics. // TODO(sbruens): Refactor so this isn't needed. Metrics() UDPAssociationMetrics } -type PacketAuthenticateFunc func() (string, *shadowsocks.EncryptionKey, error) - type association struct { - net.Conn + clientConn net.Conn targetConn net.PacketConn authenticateOnce sync.Once cryptoKey *shadowsocks.EncryptionKey @@ -476,35 +594,49 @@ func NewPacketAssociation(conn net.Conn, listener transport.PacketListener, m UD } return &association{ - Conn: conn, + clientConn: conn, targetConn: targetConn, m: m, doneCh: make(chan struct{}), }, nil } -func (a *association) WriteTo(b []byte, addr net.Addr) (int, error) { +func (a *association) ReadFromClient(b []byte) (n int, err error) { + return a.clientConn.Read(b) +} + +func (a *association) WriteToClient(b []byte) (n int, err error) { + return a.clientConn.Write(b) +} + +func (a *association) ReadFromTarget(p []byte) (n int, addr net.Addr, err error) { + return a.targetConn.ReadFrom(p) +} + +func (a *association) WriteToTarget(b []byte, addr net.Addr) (int, error) { return a.targetConn.WriteTo(b, addr) } -func (a *association) RemoteAddr() *net.UDPAddr { - return a.Conn.RemoteAddr().(*net.UDPAddr) +func (a *association) ClientAddr() *net.UDPAddr { + return a.clientConn.RemoteAddr().(*net.UDPAddr) } -func (a *association) Authenticate(authenticateFunc PacketAuthenticateFunc) (*shadowsocks.EncryptionKey, error) { +type PacketAuthenticateFunc func() (string, *shadowsocks.EncryptionKey, error) + +func (a *association) Authenticate(authenticate PacketAuthenticateFunc, handleTarget func(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics) (*shadowsocks.EncryptionKey, error) { var err error a.authenticateOnce.Do(func() { var keyID string - keyID, a.cryptoKey, err = authenticateFunc() + keyID, a.cryptoKey, err = authenticate() a.m.AddAuthentication(keyID) if err != nil { return } - // TODO(sbruens): Pass in a `handle` function to handle shadowsocks and move it to - // the packet handler. - go a.timedCopy() + go HandleAssociationTimedCopy(a, func(pkt []byte, assoc PacketAssociation) *packetMetrics { + return handleTarget(pkt, a.cryptoKey, assoc) + }) }) return a.cryptoKey, err } @@ -515,7 +647,17 @@ func (a *association) Done() <-chan struct{} { func (a *association) Close() error { now := time.Now() - return a.SetReadDeadline(now) + return a.clientConn.SetReadDeadline(now) +} + +func (a *association) CloseTarget() error { + a.m.AddClose() + err := a.targetConn.Close() + if err != nil { + return err + } + close(a.doneCh) + return nil } func (a *association) Metrics() UDPAssociationMetrics { @@ -524,95 +666,11 @@ func (a *association) Metrics() UDPAssociationMetrics { func (a *association) LogValue() slog.Value { return slog.GroupValue( - slog.Any("client", a.Conn.RemoteAddr()), + slog.Any("client", a.clientConn.RemoteAddr()), slog.Any("ltarget", a.targetConn.LocalAddr()), ) } -// Get the maximum length of the shadowsocks address header by parsing -// and serializing an IPv6 address from the example range. -var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) - -// copy from target to client until read timeout -func (a *association) timedCopy() { - defer func() { - a.m.AddClose() - a.targetConn.Close() - close(a.doneCh) - }() - - // pkt is used for in-place encryption of downstream UDP packets, with the layout - // [padding?][salt][address][body][tag][extra] - // Padding is only used if the address is IPv4. - pkt := make([]byte, serverUDPBufferSize) - - saltSize := a.cryptoKey.SaltSize() - // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). - bodyStart := saltSize + maxAddrLen - - expired := false - for { - var bodyLen, proxyClientBytes int - connError := func() (connError *onet.ConnectionError) { - var ( - raddr net.Addr - err error - ) - // `readBuf` receives the plaintext body in `pkt`: - // [padding?][salt][address][body][tag][unused] - // |-- bodyStart --|[ readBuf ] - readBuf := pkt[bodyStart:] - bodyLen, raddr, err = a.targetConn.ReadFrom(readBuf) - if err != nil { - if netErr, ok := err.(net.Error); ok { - if netErr.Timeout() { - expired = true - return nil - } - } - return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) - } - - // TODO(sbruens): Figure out the logger here and below. - // debugUDP(l, "Got response.", slog.Any("rtarget", raddr)) - srcAddr := socks.ParseAddr(raddr.String()) - addrStart := bodyStart - len(srcAddr) - // `plainTextBuf` concatenates the SOCKS address and body: - // [padding?][salt][address][body][tag][unused] - // |-- addrStart -|[plaintextBuf ] - plaintextBuf := pkt[addrStart : bodyStart+bodyLen] - copy(plaintextBuf, srcAddr) - - // saltStart is 0 if raddr is IPv6. - saltStart := addrStart - saltSize - // `packBuf` adds space for the salt and tag. - // `buf` shows the space that was used. - // [padding?][salt][address][body][tag][unused] - // [ packBuf ] - // [ buf ] - packBuf := pkt[saltStart:] - buf, err := shadowsocks.Pack(packBuf, plaintextBuf, a.cryptoKey) // Encrypt in-place - if err != nil { - return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) - } - proxyClientBytes, err = a.Write(buf) - if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) - } - return nil - }() - status := "OK" - if connError != nil { - //debugUDP(a.Logger(), "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) - status = connError.Status - } - if expired { - break - } - a.m.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) - } -} - // NoOpUDPAssociationMetrics is a [UDPAssociationMetrics] that doesn't do anything. Useful in tests // or if you don't want to track metrics. type NoOpUDPAssociationMetrics struct{} From 4c4072ccd1d0f5e1fe0bfef427055b31cce960f9 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 13 Jan 2025 12:00:19 -0500 Subject: [PATCH 56/80] Remove unused property. --- service/udp.go | 1 - 1 file changed, 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index 9ee33c61..b2005a31 100644 --- a/service/udp.go +++ b/service/udp.go @@ -88,7 +88,6 @@ type packetHandler struct { ciphers CipherList ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator - onces map[string]*sync.Once } var _ PacketHandler = (*packetHandler)(nil) From 413a1aa7e9eca53f000dc5672f5f675d401da4f0 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 13 Jan 2025 13:41:48 -0500 Subject: [PATCH 57/80] Decouple shadowsocks from association. --- service/udp.go | 64 +++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/service/udp.go b/service/udp.go index b2005a31..a17e140c 100644 --- a/service/udp.go +++ b/service/udp.go @@ -136,7 +136,7 @@ func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice sl connError := func() *onet.ConnectionError { var textData []byte - cryptoKey, err := assoc.Authenticate(func() (keyID string, cryptoKey *shadowsocks.EncryptionKey, keyErr error) { + authenticate := func() (keyID string, cryptoKey *shadowsocks.EncryptionKey, keyErr error) { ip := assoc.ClientAddr().AddrPort().Addr() textLazySlice := readBufPool.LazySlice() textBuf := textLazySlice.Acquire() @@ -145,12 +145,26 @@ func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice sl timeToCipher := time.Since(unpackStart) textLazySlice.Release() h.ssm.AddCipherSearch(keyErr == nil, timeToCipher) - return keyID, cryptoKey, keyErr - }, h.handleTargetPacket) + return + } + key, err := assoc.DoOnce(func() (any, error) { + keyID, key, authErr := authenticate() + assoc.Metrics().AddAuthentication(keyID) + if authErr == nil { + go HandleAssociationTimedCopy(assoc, func(pkt []byte, assoc PacketAssociation) *packetMetrics { + return h.handleTarget(pkt, key, assoc) + }) + } + return key, authErr + }) if err != nil { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) } + cryptoKey, ok := key.(*shadowsocks.EncryptionKey) + if !ok { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) + } if textData == nil { // This is a subsequent packet. First packets are already decrypted as part of the @@ -215,7 +229,7 @@ func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, * // and serializing an IPv6 address from the example range. var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) -func (h *packetHandler) handleTargetPacket(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics { +func (h *packetHandler) handleTarget(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics { l := h.logger.With(slog.Any("association", assoc)) expired := false @@ -492,7 +506,6 @@ type PacketHandleFunc func(pkt []byte, assoc PacketAssociation) *packetMetrics // PacketHandleFuncWithLazySlice processes a single incoming packet. // - // lazySlice is the LazySlice that holds the pkt buffer, which should be // released after the packet is processed. type PacketHandleFuncWithLazySlice func(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics @@ -552,12 +565,12 @@ type PacketAssociation interface { // WriteToTarget writes data to the target side of the association. WriteToTarget(b []byte, addr net.Addr) (int, error) - // Authenticate authenticates the association. - Authenticate(authenticate PacketAuthenticateFunc, handleTarget func(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics) (*shadowsocks.EncryptionKey, error) - // ClientAddr returns the remote network address of the client connection, if known. ClientAddr() *net.UDPAddr + // DoOnce executes the provided function only once and caches the result. + DoOnce(f func() (any, error)) (any, error) + // Done returns a channel that is closed when the association is closed. Done() <-chan struct{} @@ -573,12 +586,14 @@ type PacketAssociation interface { } type association struct { - clientConn net.Conn - targetConn net.PacketConn - authenticateOnce sync.Once - cryptoKey *shadowsocks.EncryptionKey - m UDPAssociationMetrics - doneCh chan struct{} + clientConn net.Conn + targetConn net.PacketConn + + once sync.Once + cachedResult any + + m UDPAssociationMetrics + doneCh chan struct{} } var _ PacketAssociation = (*association)(nil) @@ -620,24 +635,15 @@ func (a *association) ClientAddr() *net.UDPAddr { return a.clientConn.RemoteAddr().(*net.UDPAddr) } -type PacketAuthenticateFunc func() (string, *shadowsocks.EncryptionKey, error) - -func (a *association) Authenticate(authenticate PacketAuthenticateFunc, handleTarget func(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics) (*shadowsocks.EncryptionKey, error) { +func (a *association) DoOnce(f func() (any, error)) (any, error) { var err error - a.authenticateOnce.Do(func() { - var keyID string - keyID, a.cryptoKey, err = authenticate() - a.m.AddAuthentication(keyID) - - if err != nil { - return + a.once.Do(func() { + result, err := f() + if err == nil { + a.cachedResult = result } - - go HandleAssociationTimedCopy(a, func(pkt []byte, assoc PacketAssociation) *packetMetrics { - return handleTarget(pkt, a.cryptoKey, assoc) - }) }) - return a.cryptoKey, err + return a.cachedResult, err } func (a *association) Done() <-chan struct{} { From db3d0a33eaf20b98a784f7742bb70f372ea9b9cd Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 13 Jan 2025 14:16:46 -0500 Subject: [PATCH 58/80] Move authentication into its own function. --- service/udp.go | 89 +++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/service/udp.go b/service/udp.go index a17e140c..27d18f0c 100644 --- a/service/udp.go +++ b/service/udp.go @@ -125,6 +125,54 @@ func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPVali h.targetIPValidator = targetIPValidator } +func (h *packetHandler) authenticate(pkt []byte, assoc PacketAssociation) ([]byte, error) { + var textData []byte + keyResult, err := assoc.DoOnce(func() (any, error) { + var ( + keyID string + key *shadowsocks.EncryptionKey + keyErr error + ) + ip := assoc.ClientAddr().AddrPort().Addr() + textLazySlice := readBufPool.LazySlice() + textBuf := textLazySlice.Acquire() + unpackStart := time.Now() + textData, keyID, key, keyErr = findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) + timeToCipher := time.Since(unpackStart) + textLazySlice.Release() + h.ssm.AddCipherSearch(keyErr == nil, timeToCipher) + + assoc.Metrics().AddAuthentication(keyID) + if keyErr != nil { + return nil, keyErr + } + go HandleAssociationTimedCopy(assoc, func(pkt []byte, assoc PacketAssociation) *packetMetrics { + return h.handleTarget(pkt, key, assoc) + }) + return key, nil + }) + if err != nil { + return nil, err + } + cryptoKey, ok := keyResult.(*shadowsocks.EncryptionKey) + if !ok { + // This should never happen in practice. We return a `shadowsocks.EncrypTionKey` + // in the `authenticate` anonymous function above. + return nil, errors.New("authentication result is not an encryption key") + } + + if textData == nil { + // This is a subsequent packet. First packets are already decrypted as part of the + // initial access key search. + unpackStart := time.Now() + textData, err = shadowsocks.Unpack(nil, pkt, cryptoKey) + timeToCipher := time.Since(unpackStart) + h.ssm.AddCipherSearch(err == nil, timeToCipher) + } + + return textData, nil +} + func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics { l := h.logger.With(slog.Any("association", assoc)) defer lazySlice.Release() @@ -134,46 +182,7 @@ func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice sl var proxyTargetBytes int connError := func() *onet.ConnectionError { - var textData []byte - - authenticate := func() (keyID string, cryptoKey *shadowsocks.EncryptionKey, keyErr error) { - ip := assoc.ClientAddr().AddrPort().Addr() - textLazySlice := readBufPool.LazySlice() - textBuf := textLazySlice.Acquire() - unpackStart := time.Now() - textData, keyID, cryptoKey, keyErr = findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) - timeToCipher := time.Since(unpackStart) - textLazySlice.Release() - h.ssm.AddCipherSearch(keyErr == nil, timeToCipher) - return - } - - key, err := assoc.DoOnce(func() (any, error) { - keyID, key, authErr := authenticate() - assoc.Metrics().AddAuthentication(keyID) - if authErr == nil { - go HandleAssociationTimedCopy(assoc, func(pkt []byte, assoc PacketAssociation) *packetMetrics { - return h.handleTarget(pkt, key, assoc) - }) - } - return key, authErr - }) - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) - } - cryptoKey, ok := key.(*shadowsocks.EncryptionKey) - if !ok { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) - } - - if textData == nil { - // This is a subsequent packet. First packets are already decrypted as part of the - // initial access key search. - unpackStart := time.Now() - textData, err = shadowsocks.Unpack(nil, pkt, cryptoKey) - timeToCipher := time.Since(unpackStart) - h.ssm.AddCipherSearch(err == nil, timeToCipher) - } + textData, err := h.authenticate(pkt, assoc) if err != nil { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) From 5f685bb2fee1a7f6e8feda5f232268b0492140e2 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 13 Jan 2025 14:46:08 -0500 Subject: [PATCH 59/80] Remove the `packetMetrics` struct. We'll need to decouple metrics from handling in some future change. --- service/udp.go | 54 ++++++++++++++++---------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/service/udp.go b/service/udp.go index 27d18f0c..12d2df91 100644 --- a/service/udp.go +++ b/service/udp.go @@ -107,7 +107,7 @@ func NewPacketHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) P // PacketHandler is a handler that handles UDP assocations. type PacketHandler interface { - Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics + Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. @@ -146,8 +146,8 @@ func (h *packetHandler) authenticate(pkt []byte, assoc PacketAssociation) ([]byt if keyErr != nil { return nil, keyErr } - go HandleAssociationTimedCopy(assoc, func(pkt []byte, assoc PacketAssociation) *packetMetrics { - return h.handleTarget(pkt, key, assoc) + go HandleAssociationTimedCopy(assoc, func(pkt []byte, assoc PacketAssociation) error { + return h.handleTarget(pkt, assoc, key) }) return key, nil }) @@ -173,9 +173,8 @@ func (h *packetHandler) authenticate(pkt []byte, assoc PacketAssociation) ([]byt return textData, nil } -func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics { +func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) { l := h.logger.With(slog.Any("association", assoc)) - defer lazySlice.Release() defer debugUDP(l, "Done") debugUDP(l, "Outbound packet.", slog.Int("bytes", len(pkt))) @@ -183,7 +182,7 @@ func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice sl var proxyTargetBytes int connError := func() *onet.ConnectionError { textData, err := h.authenticate(pkt, assoc) - + lazySlice.Release() if err != nil { return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) } @@ -206,11 +205,7 @@ func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice sl debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } - return &packetMetrics{ - status: status, - bytesIn: int64(len(pkt)), - bytesOut: int64(proxyTargetBytes), - } + assoc.Metrics().AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } // Given the decrypted contents of a UDP packet, return @@ -238,7 +233,7 @@ func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, * // and serializing an IPv6 address from the example range. var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) -func (h *packetHandler) handleTarget(pkt []byte, cryptoKey *shadowsocks.EncryptionKey, assoc PacketAssociation) *packetMetrics { +func (h *packetHandler) handleTarget(pkt []byte, assoc PacketAssociation, cryptoKey *shadowsocks.EncryptionKey) error { l := h.logger.With(slog.Any("association", assoc)) expired := false @@ -300,18 +295,11 @@ func (h *packetHandler) handleTarget(pkt []byte, cryptoKey *shadowsocks.Encrypti debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } - return &packetMetrics{ - status: status, - expired: expired, - bytesIn: int64(bodyLen), - bytesOut: int64(proxyClientBytes), + assoc.Metrics().AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) + if expired { + return errors.New("target connection has expired") } -} - -type packetMetrics struct { - status string - expired bool - bytesIn, bytesOut int64 + return nil } type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) @@ -366,10 +354,7 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, h metrics.RemoveNATEntry() nm.Del(addr.String()) default: - go func() { - metrics := handle(pkt, assoc, lazySlice) - assoc.Metrics().AddPacketFromClient(metrics.status, metrics.bytesIn, metrics.bytesOut) - }() + go handle(pkt, assoc, lazySlice) } return false }() @@ -511,13 +496,13 @@ func (m *natmap) Close() error { } // PacketHandleFunc processes a single incoming packet. -type PacketHandleFunc func(pkt []byte, assoc PacketAssociation) *packetMetrics +type PacketHandleFunc func(pkt []byte, assoc PacketAssociation) error // PacketHandleFuncWithLazySlice processes a single incoming packet. // // lazySlice is the LazySlice that holds the pkt buffer, which should be -// released after the packet is processed. -type PacketHandleFuncWithLazySlice func(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) *packetMetrics +// released as soon as the packet is processed. +type PacketHandleFuncWithLazySlice func(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) func HandleAssociation(assoc PacketAssociation, handle PacketHandleFuncWithLazySlice) { for { @@ -534,10 +519,7 @@ func HandleAssociation(assoc PacketAssociation, handle PacketHandleFuncWithLazyS lazySlice.Release() return default: - go func() { - metrics := handle(pkt, assoc, lazySlice) - assoc.Metrics().AddPacketFromClient(metrics.status, metrics.bytesIn, metrics.bytesOut) - }() + go handle(pkt, assoc, lazySlice) } } } @@ -552,11 +534,9 @@ func HandleAssociationTimedCopy(assoc PacketAssociation, handle PacketHandleFunc pkt := make([]byte, serverUDPBufferSize) for { - metrics := handle(pkt, assoc) - if metrics.expired { + if err := handle(pkt, assoc); err != nil { break } - assoc.Metrics().AddPacketFromTarget(metrics.status, metrics.bytesIn, metrics.bytesOut) } } From 1dbfa9928146081fdde76b68e47b7163868f5300 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 13 Jan 2025 15:17:06 -0500 Subject: [PATCH 60/80] Update tests. --- internal/integration_test/integration_test.go | 15 +++--- service/udp.go | 7 ++- service/udp_test.go | 52 +++++++++---------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 739c4683..d37c4f86 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -323,8 +323,9 @@ func TestUDPEcho(t *testing.T) { natMetrics := &natTestMetrics{} associationMetrics := &fakeUDPAssociationMetrics{} go service.PacketServe(proxyConn, func(conn net.Conn) (service.PacketAssociation, error) { - return proxy.NewPacketAssociation(conn, associationMetrics) - }, natMetrics) + assoc, _ := service.NewPacketAssociation(conn, &transport.UDPListener{Address: ""}, associationMetrics) + return assoc, nil + }, proxy.Handle, natMetrics) cryptoKey, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, secrets[0]) require.NoError(t, err) @@ -550,8 +551,9 @@ func BenchmarkUDPEcho(b *testing.B) { done := make(chan struct{}) go func() { service.PacketServe(server, func(conn net.Conn) (service.PacketAssociation, error) { - return proxy.NewPacketAssociation(conn, nil) - }, &natTestMetrics{}) + assoc, _ := service.NewPacketAssociation(conn, &transport.UDPListener{Address: ""}, nil) + return assoc, nil + }, proxy.Handle, &natTestMetrics{}) done <- struct{}{} }() @@ -596,8 +598,9 @@ func BenchmarkUDPManyKeys(b *testing.B) { done := make(chan struct{}) go func() { service.PacketServe(proxyConn, func(conn net.Conn) (service.PacketAssociation, error) { - return proxy.NewPacketAssociation(conn, nil) - }, &natTestMetrics{}) + assoc, _ := service.NewPacketAssociation(conn, &transport.UDPListener{Address: ""}, nil) + return assoc, nil + }, proxy.Handle, &natTestMetrics{}) done <- struct{}{} }() diff --git a/service/udp.go b/service/udp.go index 12d2df91..47b73550 100644 --- a/service/udp.go +++ b/service/udp.go @@ -252,7 +252,7 @@ func (h *packetHandler) handleTarget(pkt []byte, assoc PacketAssociation, crypto // [padding?][salt][address][body][tag][unused] // |-- bodyStart --|[ readBuf ] readBuf := pkt[bodyStart:] - bodyLen, raddr, err := assoc.ReadFromTarget(readBuf) + bodyLen, raddr, err = assoc.ReadFromTarget(readBuf) if err != nil { if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { @@ -295,10 +295,10 @@ func (h *packetHandler) handleTarget(pkt []byte, assoc PacketAssociation, crypto debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } - assoc.Metrics().AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) if expired { return errors.New("target connection has expired") } + assoc.Metrics().AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) return nil } @@ -590,6 +590,9 @@ var _ slog.LogValuer = (*association)(nil) // NewPacketAssociation creates a new packet-based association. func NewPacketAssociation(conn net.Conn, listener transport.PacketListener, m UDPAssociationMetrics) (PacketAssociation, error) { + if m == nil { + m = &NoOpUDPAssociationMetrics{} + } // Create the target connection targetConn, err := listener.ListenPacket(context.Background()) if err != nil { diff --git a/service/udp_test.go b/service/udp_test.go index 89fdca87..51363c35 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -197,10 +197,10 @@ func startTestHandler() (PacketHandler, func(target net.Addr, payload []byte), * handler := NewPacketHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - handler.SetTargetPacketListener(&packetListener{targetConn}) go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - return handler.NewPacketAssociation(conn, nil) - }, &natTestMetrics{}) + assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) + return assoc, nil + }, handler.Handle, &natTestMetrics{}) return handler, func(target net.Addr, payload []byte) { sendSSPayload(clientConn, target, cipher, payload) }, targetConn @@ -255,11 +255,11 @@ func TestUpstreamMetrics(t *testing.T) { handler := NewPacketHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - handler.SetTargetPacketListener(&packetListener{targetConn}) metrics := &fakeUDPAssociationMetrics{} go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - return handler.NewPacketAssociation(conn, metrics) - }, &natTestMetrics{}) + assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, metrics) + return assoc, nil + }, handler.Handle, &natTestMetrics{}) // Test both the first-packet and subsequent-packet cases. const N = 10 @@ -309,8 +309,7 @@ func (e *fakeTimeoutError) Temporary() bool { func TestTimedPacketConn(t *testing.T) { t.Run("Write", func(t *testing.T) { - handler, sendPayload, targetConn := startTestHandler() - handler.SetTargetPacketListener(&packetListener{targetConn}) + _, sendPayload, targetConn := startTestHandler() buf := []byte{1} sendPayload(&targetAddr, buf) @@ -324,8 +323,7 @@ func TestTimedPacketConn(t *testing.T) { }) t.Run("WriteDNS", func(t *testing.T) { - handler, sendPayload, targetConn := startTestHandler() - handler.SetTargetPacketListener(&packetListener{targetConn}) + _, sendPayload, targetConn := startTestHandler() // Simulate one DNS query being sent. buf := []byte{1} @@ -341,8 +339,7 @@ func TestTimedPacketConn(t *testing.T) { }) t.Run("WriteDNSMultiple", func(t *testing.T) { - handler, sendPayload, targetConn := startTestHandler() - handler.SetTargetPacketListener(&packetListener{targetConn}) + _, sendPayload, targetConn := startTestHandler() // Simulate three DNS queries being sent. buf := []byte{1} @@ -358,8 +355,7 @@ func TestTimedPacketConn(t *testing.T) { }) t.Run("WriteMixed", func(t *testing.T) { - handler, sendPayload, targetConn := startTestHandler() - handler.SetTargetPacketListener(&packetListener{targetConn}) + _, sendPayload, targetConn := startTestHandler() // Simulate both non-DNS and DNS packets being sent. buf := []byte{1} @@ -378,10 +374,10 @@ func TestTimedPacketConn(t *testing.T) { handler := NewPacketHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - handler.SetTargetPacketListener(&packetListener{targetConn}) go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - return handler.NewPacketAssociation(conn, nil) - }, &natTestMetrics{}) + assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) + return assoc, nil + }, handler.Handle, &natTestMetrics{}) // Send one DNS query. sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) @@ -406,10 +402,10 @@ func TestTimedPacketConn(t *testing.T) { handler := NewPacketHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - handler.SetTargetPacketListener(&packetListener{targetConn}) go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - return handler.NewPacketAssociation(conn, nil) - }, &natTestMetrics{}) + assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) + return assoc, nil + }, handler.Handle, &natTestMetrics{}) // Send one non-DNS packet. sendSSPayload(clientConn, &targetAddr, cipher, []byte{1}) @@ -434,10 +430,10 @@ func TestTimedPacketConn(t *testing.T) { handler := NewPacketHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - handler.SetTargetPacketListener(&packetListener{targetConn}) go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - return handler.NewPacketAssociation(conn, nil) - }, &natTestMetrics{}) + assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) + return assoc, nil + }, handler.Handle, &natTestMetrics{}) // Send two DNS packets. sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) @@ -456,8 +452,7 @@ func TestTimedPacketConn(t *testing.T) { }) t.Run("Timeout", func(t *testing.T) { - handler, sendPayload, targetConn := startTestHandler() - handler.SetTargetPacketListener(&packetListener{targetConn}) + _, sendPayload, targetConn := startTestHandler() // Simulate a non-DNS initial packet. sendPayload(&targetAddr, []byte{1}) @@ -524,7 +519,7 @@ func TestNATMap(t *testing.T) { nm := newNATmap() addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} pc := makePacketConn() - assoc := &association{Conn: &natconn{PacketConn: pc, raddr: addr}} + assoc := &association{clientConn: &natconn{PacketConn: pc, raddr: addr}} nm.Add(addr.String(), assoc) err := nm.Close() @@ -627,8 +622,9 @@ func TestUDPEarlyClose(t *testing.T) { require.Nil(t, clientConn.Close()) // This should return quickly without timing out. go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - return ph.NewPacketAssociation(conn, &NoOpUDPAssociationMetrics{}) - }, &natTestMetrics{}) + assoc, _ := NewPacketAssociation(conn, &packetListener{makePacketConn()}, nil) + return assoc, nil + }, ph.Handle, &natTestMetrics{}) } // Makes sure the UDP listener returns [io.ErrClosed] on reads and writes after Close(). From 23050efe0dd63e00639075f1d35cb47731988640 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 13 Jan 2025 15:22:14 -0500 Subject: [PATCH 61/80] Remove the `Metrics()` method. --- service/udp.go | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/service/udp.go b/service/udp.go index 47b73550..8d9fc3d7 100644 --- a/service/udp.go +++ b/service/udp.go @@ -142,7 +142,7 @@ func (h *packetHandler) authenticate(pkt []byte, assoc PacketAssociation) ([]byt textLazySlice.Release() h.ssm.AddCipherSearch(keyErr == nil, timeToCipher) - assoc.Metrics().AddAuthentication(keyID) + assoc.AddAuthentication(keyID) if keyErr != nil { return nil, keyErr } @@ -205,7 +205,7 @@ func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice sl debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) status = connError.Status } - assoc.Metrics().AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) + assoc.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } // Given the decrypted contents of a UDP packet, return @@ -298,7 +298,7 @@ func (h *packetHandler) handleTarget(pkt []byte, assoc PacketAssociation, crypto if expired { return errors.New("target connection has expired") } - assoc.Metrics().AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) + assoc.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) return nil } @@ -542,6 +542,9 @@ func HandleAssociationTimedCopy(assoc PacketAssociation, handle PacketHandleFunc // PacketAssociation represents a UDP association. type PacketAssociation interface { + // TODO(sbruens): Decouple the metrics from the association. + UDPAssociationMetrics + // ReadFromClient reads data from the client side of the association. ReadFromClient(b []byte) (n int, err error) @@ -568,10 +571,6 @@ type PacketAssociation interface { // Closes the target side of the association. CloseTarget() error - - // Returns association metrics. - // TODO(sbruens): Refactor so this isn't needed. - Metrics() UDPAssociationMetrics } type association struct { @@ -581,11 +580,12 @@ type association struct { once sync.Once cachedResult any - m UDPAssociationMetrics + UDPAssociationMetrics doneCh chan struct{} } var _ PacketAssociation = (*association)(nil) +var _ UDPAssociationMetrics = (*association)(nil) var _ slog.LogValuer = (*association)(nil) // NewPacketAssociation creates a new packet-based association. @@ -600,10 +600,10 @@ func NewPacketAssociation(conn net.Conn, listener transport.PacketListener, m UD } return &association{ - clientConn: conn, - targetConn: targetConn, - m: m, - doneCh: make(chan struct{}), + clientConn: conn, + targetConn: targetConn, + UDPAssociationMetrics: m, + doneCh: make(chan struct{}), }, nil } @@ -648,7 +648,7 @@ func (a *association) Close() error { } func (a *association) CloseTarget() error { - a.m.AddClose() + a.UDPAssociationMetrics.AddClose() err := a.targetConn.Close() if err != nil { return err @@ -657,10 +657,6 @@ func (a *association) CloseTarget() error { return nil } -func (a *association) Metrics() UDPAssociationMetrics { - return a.m -} - func (a *association) LogValue() slog.Value { return slog.GroupValue( slog.Any("client", a.clientConn.RemoteAddr()), From 47222f6a1153efcb7e18f5708f306004370c70ee Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 16 Jan 2025 13:06:46 -0500 Subject: [PATCH 62/80] Move variables into the anonymous functions. --- service/udp.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/service/udp.go b/service/udp.go index 8d9fc3d7..e88c486d 100644 --- a/service/udp.go +++ b/service/udp.go @@ -237,13 +237,12 @@ func (h *packetHandler) handleTarget(pkt []byte, assoc PacketAssociation, crypto l := h.logger.With(slog.Any("association", assoc)) expired := false - - saltSize := cryptoKey.SaltSize() - // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). - bodyStart := saltSize + maxAddrLen - var bodyLen, proxyClientBytes int connError := func() *onet.ConnectionError { + saltSize := cryptoKey.SaltSize() + // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). + bodyStart := saltSize + maxAddrLen + var ( raddr net.Addr err error From 8e9af05974ab9e10f7ffb5a5168d85172e4388f0 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 17 Jan 2025 16:14:40 -0500 Subject: [PATCH 63/80] Rename stream and packet handlers `Handle()` methods. --- caddy/shadowsocks_handler.go | 4 ++-- cmd/outline-ss-server/main.go | 8 +++---- internal/integration_test/integration_test.go | 8 +++---- service/tcp.go | 4 ++-- service/tcp_test.go | 24 +++++++++---------- service/udp.go | 4 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 8358414f..0167895c 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -120,13 +120,13 @@ func (h *ShadowsocksHandler) Provision(ctx caddy.Context) error { func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) error { switch conn := cx.Conn.(type) { case transport.StreamConn: - h.streamHandler.Handle(cx.Context, conn, h.metrics.AddOpenTCPConnection(conn)) + h.streamHandler.HandleStream(cx.Context, conn, h.metrics.AddOpenTCPConnection(conn)) case net.Conn: assoc, err := outline.NewPacketAssociation(conn, h.tgtListener, h.metrics.AddOpenUDPAssociation(conn)) if err != nil { return fmt.Errorf("failed to handle association: %v", err) } - outline.HandleAssociation(assoc, h.packetHandler.Handle) + outline.HandleAssociation(assoc, h.packetHandler.HandlePacket) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index 58851906..eed8f876 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -237,7 +237,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { } slog.Info("TCP service started.", "address", ln.Addr().String()) go service.StreamServe(ln.AcceptStream, func(ctx context.Context, conn transport.StreamConn) { - streamHandler.Handle(ctx, conn, s.serviceMetrics.AddOpenTCPConnection(conn)) + streamHandler.HandleStream(ctx, conn, s.serviceMetrics.AddOpenTCPConnection(conn)) }) pc, err := lnSet.ListenPacket(addr) @@ -253,7 +253,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return nil, fmt.Errorf("failed to handle association: %v", err) } return assoc, nil - }, packetHandler.Handle, s.serverMetrics) + }, packetHandler.HandlePacket, s.serverMetrics) } for _, serviceConfig := range config.Services { @@ -285,7 +285,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return serviceConfig.Dialer.Fwmark }()) go service.StreamServe(ln.AcceptStream, func(ctx context.Context, conn transport.StreamConn) { - streamHandler.Handle(ctx, conn, s.serviceMetrics.AddOpenTCPConnection(conn)) + streamHandler.HandleStream(ctx, conn, s.serviceMetrics.AddOpenTCPConnection(conn)) }) case listenerTypeUDP: pc, err := lnSet.ListenPacket(lnConfig.Address) @@ -306,7 +306,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return nil, fmt.Errorf("failed to handle association: %v", err) } return assoc, nil - }, packetHandler.Handle, s.serverMetrics) + }, packetHandler.HandlePacket, s.serverMetrics) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index d37c4f86..82fb370f 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -142,7 +142,7 @@ func TestTCPEcho(t *testing.T) { go func() { service.StreamServe( func() (transport.StreamConn, error) { return proxyListener.AcceptTCP() }, - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -221,7 +221,7 @@ func TestRestrictedAddresses(t *testing.T) { go func() { service.StreamServe( service.WrapStreamAcceptFunc(proxyListener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -409,7 +409,7 @@ func BenchmarkTCPThroughput(b *testing.B) { go func() { service.StreamServe( service.WrapStreamAcceptFunc(proxyListener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -476,7 +476,7 @@ func BenchmarkTCPMultiplexing(b *testing.B) { go func() { service.StreamServe( service.WrapStreamAcceptFunc(proxyListener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() diff --git a/service/tcp.go b/service/tcp.go index 286c3753..2dae9504 100644 --- a/service/tcp.go +++ b/service/tcp.go @@ -178,7 +178,7 @@ func NewStreamHandler(authenticate StreamAuthenticateFunc, timeout time.Duration // StreamHandler is a handler that handles stream connections. type StreamHandler interface { - Handle(ctx context.Context, conn transport.StreamConn, connMetrics TCPConnMetrics) + HandleStream(ctx context.Context, conn transport.StreamConn, connMetrics TCPConnMetrics) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetDialer sets the [transport.StreamDialer] to be used to connect to target addresses. @@ -250,7 +250,7 @@ func StreamServe(accept StreamAcceptFunc, handle StreamHandleFunc) { } } -func (h *streamHandler) Handle(ctx context.Context, clientConn transport.StreamConn, connMetrics TCPConnMetrics) { +func (h *streamHandler) HandleStream(ctx context.Context, clientConn transport.StreamConn, connMetrics TCPConnMetrics) { if connMetrics == nil { connMetrics = &NoOpTCPConnMetrics{} } diff --git a/service/tcp_test.go b/service/tcp_test.go index 555a3a3f..946ed1d4 100644 --- a/service/tcp_test.go +++ b/service/tcp_test.go @@ -292,7 +292,7 @@ func TestProbeRandom(t *testing.T) { go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -368,12 +368,12 @@ func TestProbeClientBytesBasicTruncated(t *testing.T) { testMetrics := &probeTestMetrics{} authFunc := NewShadowsocksStreamAuthenticator(cipherList, nil, &fakeShadowsocksMetrics{}, nil) handler := NewStreamHandler(authFunc, 200*time.Millisecond) - handler.SetTargetDialer(MakeValidatingTCPStreamDialer(allowAll, 0)) + handler.SetTargetDialerStream(MakeValidatingTCPStreamDialer(allowAll, 0)) done := make(chan struct{}) go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -406,12 +406,12 @@ func TestProbeClientBytesBasicModified(t *testing.T) { testMetrics := &probeTestMetrics{} authFunc := NewShadowsocksStreamAuthenticator(cipherList, nil, &fakeShadowsocksMetrics{}, nil) handler := NewStreamHandler(authFunc, 200*time.Millisecond) - handler.SetTargetDialer(MakeValidatingTCPStreamDialer(allowAll, 0)) + handler.SetTargetDialerStream(MakeValidatingTCPStreamDialer(allowAll, 0)) done := make(chan struct{}) go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -445,12 +445,12 @@ func TestProbeClientBytesCoalescedModified(t *testing.T) { testMetrics := &probeTestMetrics{} authFunc := NewShadowsocksStreamAuthenticator(cipherList, nil, &fakeShadowsocksMetrics{}, nil) handler := NewStreamHandler(authFunc, 200*time.Millisecond) - handler.SetTargetDialer(MakeValidatingTCPStreamDialer(allowAll, 0)) + handler.SetTargetDialerStream(MakeValidatingTCPStreamDialer(allowAll, 0)) done := make(chan struct{}) go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -495,7 +495,7 @@ func TestProbeServerBytesModified(t *testing.T) { go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -551,7 +551,7 @@ func TestReplayDefense(t *testing.T) { go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -624,7 +624,7 @@ func TestReverseReplayDefense(t *testing.T) { go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -686,7 +686,7 @@ func probeExpectTimeout(t *testing.T, payloadSize int) { go func() { StreamServe( WrapStreamAcceptFunc(listener.AcceptTCP), - func(ctx context.Context, conn transport.StreamConn) { handler.Handle(ctx, conn, testMetrics) }, + func(ctx context.Context, conn transport.StreamConn) { handler.HandleStream(ctx, conn, testMetrics) }, ) done <- struct{}{} }() @@ -747,7 +747,7 @@ func TestStreamServeEarlyClose(t *testing.T) { err = tcpListener.Close() require.NoError(t, err) // This should return quickly, without timing out or calling the handler. - StreamServe(WrapStreamAcceptFunc(tcpListener.AcceptTCP), nil) + StreamServeStream(WrapStreamAcceptFunc(tcpListener.AcceptTCP), nil) } // Makes sure the TCP listener returns [io.ErrClosed] on Close(). diff --git a/service/udp.go b/service/udp.go index e88c486d..62ba13d9 100644 --- a/service/udp.go +++ b/service/udp.go @@ -107,7 +107,7 @@ func NewPacketHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) P // PacketHandler is a handler that handles UDP assocations. type PacketHandler interface { - Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) + HandlePacket(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. @@ -173,7 +173,7 @@ func (h *packetHandler) authenticate(pkt []byte, assoc PacketAssociation) ([]byt return textData, nil } -func (h *packetHandler) Handle(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) { +func (h *packetHandler) HandlePacket(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) { l := h.logger.With(slog.Any("association", assoc)) defer debugUDP(l, "Done") From 57047183db7509365541738057591e4f6efa0930 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 17 Jan 2025 16:20:27 -0500 Subject: [PATCH 64/80] More `handle` naming clarification. --- service/tcp.go | 4 ++-- service/udp.go | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/service/tcp.go b/service/tcp.go index 2dae9504..50b6b0d7 100644 --- a/service/tcp.go +++ b/service/tcp.go @@ -221,7 +221,7 @@ type StreamHandleFunc func(ctx context.Context, conn transport.StreamConn) // StreamServe repeatedly calls `accept` to obtain connections and `handle` to handle them until // accept() returns [ErrClosed]. When that happens, all connection handlers will be notified // via their [context.Context]. StreamServe will return after all pending handlers return. -func StreamServe(accept StreamAcceptFunc, handle StreamHandleFunc) { +func StreamServe(accept StreamAcceptFunc, streamHandle StreamHandleFunc) { var running sync.WaitGroup defer running.Wait() ctx, contextCancel := context.WithCancel(context.Background()) @@ -245,7 +245,7 @@ func StreamServe(accept StreamAcceptFunc, handle StreamHandleFunc) { slog.Warn("Panic in TCP handler. Continuing to listen.", "err", r) } }() - handle(ctx, clientConn) + streamHandle(ctx, clientConn) }() } } diff --git a/service/udp.go b/service/udp.go index 62ba13d9..70363d29 100644 --- a/service/udp.go +++ b/service/udp.go @@ -146,8 +146,8 @@ func (h *packetHandler) authenticate(pkt []byte, assoc PacketAssociation) ([]byt if keyErr != nil { return nil, keyErr } - go HandleAssociationTimedCopy(assoc, func(pkt []byte, assoc PacketAssociation) error { - return h.handleTarget(pkt, assoc, key) + go relayTargetToClient(assoc, func(pkt []byte, assoc PacketAssociation) error { + return h.handlePacketFromTarget(pkt, assoc, key) }) return key, nil }) @@ -233,7 +233,7 @@ func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, * // and serializing an IPv6 address from the example range. var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) -func (h *packetHandler) handleTarget(pkt []byte, assoc PacketAssociation, cryptoKey *shadowsocks.EncryptionKey) error { +func (h *packetHandler) handlePacketFromTarget(pkt []byte, assoc PacketAssociation, cryptoKey *shadowsocks.EncryptionKey) error { l := h.logger.With(slog.Any("association", assoc)) expired := false @@ -304,10 +304,10 @@ func (h *packetHandler) handleTarget(pkt []byte, assoc PacketAssociation, crypto type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) // PacketServe listens for UDP packets on the provided [net.PacketConn], creates -// and manages NAT associations, and invokes the `handle` function for each -// packet. It uses a NAT map to track active associations and handles their -// lifecycle. -func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, handle PacketHandleFuncWithLazySlice, metrics NATMetrics) { +// and manages NAT associations, and invokes the provided `handlePacket` +// function for each packet. It uses a NAT map to track active associations and +// handles their lifecycle. +func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, handlePacket PacketHandleFuncWithLazySlice, metrics NATMetrics) { nm := newNATmap() defer nm.Close() @@ -353,7 +353,7 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, h metrics.RemoveNATEntry() nm.Del(addr.String()) default: - go handle(pkt, assoc, lazySlice) + go handlePacket(pkt, assoc, lazySlice) } return false }() @@ -503,7 +503,7 @@ type PacketHandleFunc func(pkt []byte, assoc PacketAssociation) error // released as soon as the packet is processed. type PacketHandleFuncWithLazySlice func(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) -func HandleAssociation(assoc PacketAssociation, handle PacketHandleFuncWithLazySlice) { +func HandleAssociation(assoc PacketAssociation, handlePacket PacketHandleFuncWithLazySlice) { for { lazySlice := readBufPool.LazySlice() buf := lazySlice.Acquire() @@ -518,14 +518,14 @@ func HandleAssociation(assoc PacketAssociation, handle PacketHandleFuncWithLazyS lazySlice.Release() return default: - go handle(pkt, assoc, lazySlice) + go handlePacket(pkt, assoc, lazySlice) } } } -// HandleAssociationTimedCopy handles the target-side of the association by +// relayTargetToClient handles the target-side of the association by // copying from target to client until read timeout. -func HandleAssociationTimedCopy(assoc PacketAssociation, handle PacketHandleFunc) { +func relayTargetToClient(assoc PacketAssociation, handlePacket PacketHandleFunc) { defer assoc.CloseTarget() // pkt is used for in-place encryption of downstream UDP packets. @@ -533,7 +533,7 @@ func HandleAssociationTimedCopy(assoc PacketAssociation, handle PacketHandleFunc pkt := make([]byte, serverUDPBufferSize) for { - if err := handle(pkt, assoc); err != nil { + if err := handlePacket(pkt, assoc); err != nil { break } } From 0cbcd615b484a88b2bf5ef1ac3a7976a70ea8fe9 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 21 Jan 2025 14:48:34 -0500 Subject: [PATCH 65/80] Refactor to make packet handler an association handler. --- caddy/shadowsocks_handler.go | 12 +- cmd/outline-ss-server/main.go | 30 +- internal/integration_test/integration_test.go | 27 +- service/shadowsocks.go | 27 +- service/tcp_test.go | 8 +- service/udp.go | 621 ++++++++---------- service/udp_test.go | 117 ++-- 7 files changed, 358 insertions(+), 484 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index 0167895c..adc85d2a 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -51,9 +51,8 @@ type ShadowsocksHandler struct { Keys []KeyConfig `json:"keys,omitempty"` streamHandler outline.StreamHandler - packetHandler outline.PacketHandler + associationHandler outline.AssociationHandler metrics outline.ServiceMetrics - tgtListener transport.PacketListener logger *slog.Logger } @@ -106,13 +105,12 @@ func (h *ShadowsocksHandler) Provision(ctx caddy.Context) error { ciphers := outline.NewCipherList() ciphers.Update(cipherList) - h.streamHandler, h.packetHandler = outline.NewShadowsocksHandlers( + h.streamHandler, h.associationHandler = outline.NewShadowsocksHandlers( outline.WithLogger(h.logger), outline.WithCiphers(ciphers), outline.WithMetrics(h.metrics), outline.WithReplayCache(&app.ReplayCache), ) - h.tgtListener = outline.MakeTargetUDPListener(defaultNatTimeout, 0) return nil } @@ -122,11 +120,7 @@ func (h *ShadowsocksHandler) Handle(cx *layer4.Connection, _ layer4.Handler) err case transport.StreamConn: h.streamHandler.HandleStream(cx.Context, conn, h.metrics.AddOpenTCPConnection(conn)) case net.Conn: - assoc, err := outline.NewPacketAssociation(conn, h.tgtListener, h.metrics.AddOpenUDPAssociation(conn)) - if err != nil { - return fmt.Errorf("failed to handle association: %v", err) - } - outline.HandleAssociation(assoc, h.packetHandler.HandlePacket) + h.associationHandler.HandleAssociation(cx.Context, conn, h.metrics.AddOpenUDPAssociation(conn)) default: return fmt.Errorf("failed to handle unknown connection type: %t", conn) } diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index eed8f876..e8203d42 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -225,10 +225,11 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { ciphers := service.NewCipherList() ciphers.Update(cipherList) - streamHandler, packetHandler := service.NewShadowsocksHandlers( + streamHandler, associationHandler := service.NewShadowsocksHandlers( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), + service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, 0)), service.WithLogger(slog.Default()), ) ln, err := lnSet.ListenStream(addr) @@ -245,15 +246,9 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return err } slog.Info("UDP service started.", "address", pc.LocalAddr().String()) - tgtListener := service.MakeTargetUDPListener(s.natTimeout, 0) - go service.PacketServe(pc, func(conn net.Conn) (service.PacketAssociation, error) { - m := s.serviceMetrics.AddOpenUDPAssociation(conn) - assoc, err := service.NewPacketAssociation(conn, tgtListener, m) - if err != nil { - return nil, fmt.Errorf("failed to handle association: %v", err) - } - return assoc, nil - }, packetHandler.HandlePacket, s.serverMetrics) + go service.PacketServe(pc, func(ctx context.Context, conn net.Conn) { + associationHandler.HandleAssociation(ctx, conn, s.serviceMetrics.AddOpenUDPAssociation(conn)) + }, s.serverMetrics) } for _, serviceConfig := range config.Services { @@ -261,11 +256,12 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { if err != nil { return fmt.Errorf("failed to create cipher list from config: %v", err) } - streamHandler, packetHandler := service.NewShadowsocksHandlers( + streamHandler, associationHandler := service.NewShadowsocksHandlers( service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), service.WithStreamDialer(service.MakeValidatingTCPStreamDialer(onet.RequirePublicIP, serviceConfig.Dialer.Fwmark)), + service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, serviceConfig.Dialer.Fwmark)), service.WithLogger(slog.Default()), ) if err != nil { @@ -298,15 +294,9 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { } return serviceConfig.Dialer.Fwmark }()) - tgtListener := service.MakeTargetUDPListener(s.natTimeout, serviceConfig.Dialer.Fwmark) - go service.PacketServe(pc, func(conn net.Conn) (service.PacketAssociation, error) { - m := s.serviceMetrics.AddOpenUDPAssociation(conn) - assoc, err := service.NewPacketAssociation(conn, tgtListener, m) - if err != nil { - return nil, fmt.Errorf("failed to handle association: %v", err) - } - return assoc, nil - }, packetHandler.HandlePacket, s.serverMetrics) + go service.PacketServe(pc, func(ctx context.Context, conn net.Conn) { + associationHandler.HandleAssociation(ctx, conn, s.serviceMetrics.AddOpenUDPAssociation(conn)) + }, s.serverMetrics) } } totalCipherCount += len(serviceConfig.Keys) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 82fb370f..10ac5018 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -317,15 +317,14 @@ func TestUDPEcho(t *testing.T) { if err != nil { t.Fatal(err) } - proxy := service.NewPacketHandler(cipherList, &fakeShadowsocksMetrics{}) + proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) natMetrics := &natTestMetrics{} associationMetrics := &fakeUDPAssociationMetrics{} - go service.PacketServe(proxyConn, func(conn net.Conn) (service.PacketAssociation, error) { - assoc, _ := service.NewPacketAssociation(conn, &transport.UDPListener{Address: ""}, associationMetrics) - return assoc, nil - }, proxy.Handle, natMetrics) + go service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) { + proxy.HandleAssociation(ctx, conn, associationMetrics) + }, natMetrics) cryptoKey, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, secrets[0]) require.NoError(t, err) @@ -546,14 +545,13 @@ func BenchmarkUDPEcho(b *testing.B) { if err != nil { b.Fatal(err) } - proxy := service.NewPacketHandler(cipherList, &fakeShadowsocksMetrics{}) + proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(server, func(conn net.Conn) (service.PacketAssociation, error) { - assoc, _ := service.NewPacketAssociation(conn, &transport.UDPListener{Address: ""}, nil) - return assoc, nil - }, proxy.Handle, &natTestMetrics{}) + service.PacketServe(server, func(ctx context.Context, conn net.Conn) { + proxy.HandleAssociation(ctx, conn, &fakeUDPAssociationMetrics{}) + }, &natTestMetrics{}) done <- struct{}{} }() @@ -593,14 +591,13 @@ func BenchmarkUDPManyKeys(b *testing.B) { if err != nil { b.Fatal(err) } - proxy := service.NewPacketHandler(cipherList, &fakeShadowsocksMetrics{}) + proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) proxy.SetTargetIPValidator(allowAll) done := make(chan struct{}) go func() { - service.PacketServe(proxyConn, func(conn net.Conn) (service.PacketAssociation, error) { - assoc, _ := service.NewPacketAssociation(conn, &transport.UDPListener{Address: ""}, nil) - return assoc, nil - }, proxy.Handle, &natTestMetrics{}) + service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) { + proxy.HandleAssociation(ctx, conn, &fakeUDPAssociationMetrics{}) + }, &natTestMetrics{}) done <- struct{}{} }() diff --git a/service/shadowsocks.go b/service/shadowsocks.go index dd4ac517..e63ba835 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -24,10 +24,8 @@ import ( onet "github.com/Jigsaw-Code/outline-ss-server/net" ) -const ( - // 59 seconds is most common timeout for servers that do not respond to invalid requests - tcpReadTimeout time.Duration = 59 * time.Second -) +// 59 seconds is most common timeout for servers that do not respond to invalid requests +const tcpReadTimeout time.Duration = 59 * time.Second // ShadowsocksConnMetrics is used to report Shadowsocks related metrics on connections. type ShadowsocksConnMetrics interface { @@ -51,11 +49,12 @@ type ssService struct { targetIPValidator onet.TargetIPValidator replayCache *ReplayCache - streamDialer transport.StreamDialer + streamDialer transport.StreamDialer + packetListener transport.PacketListener } // NewShadowsocksHandlers creates new Shadowsocks stream and packet handlers. -func NewShadowsocksHandlers(opts ...Option) (StreamHandler, PacketHandler) { +func NewShadowsocksHandlers(opts ...Option) (StreamHandler, AssociationHandler) { s := &ssService{ logger: noopLogger(), } @@ -74,10 +73,13 @@ func NewShadowsocksHandlers(opts ...Option) (StreamHandler, PacketHandler) { } sh.SetLogger(s.logger) - ph := NewPacketHandler(s.ciphers, &ssConnMetrics{s.metrics.AddUDPCipherSearch}) - ph.SetLogger(s.logger) + ah := NewAssociationHandler(s.ciphers, &ssConnMetrics{s.metrics.AddUDPCipherSearch}) + if s.packetListener != nil { + ah.SetTargetPacketListener(s.packetListener) + } + ah.SetLogger(s.logger) - return sh, ph + return sh, ah } // WithLogger can be used to provide a custom log target. If not provided, @@ -115,6 +117,13 @@ func WithStreamDialer(dialer transport.StreamDialer) Option { } } +// WithPacketListener option function. +func WithPacketListener(listener transport.PacketListener) Option { + return func(s *ssService) { + s.packetListener = listener + } +} + type ssConnMetrics struct { metricFunc func(accessKeyFound bool, timeToCipher time.Duration) } diff --git a/service/tcp_test.go b/service/tcp_test.go index 946ed1d4..9f5ecb60 100644 --- a/service/tcp_test.go +++ b/service/tcp_test.go @@ -368,7 +368,7 @@ func TestProbeClientBytesBasicTruncated(t *testing.T) { testMetrics := &probeTestMetrics{} authFunc := NewShadowsocksStreamAuthenticator(cipherList, nil, &fakeShadowsocksMetrics{}, nil) handler := NewStreamHandler(authFunc, 200*time.Millisecond) - handler.SetTargetDialerStream(MakeValidatingTCPStreamDialer(allowAll, 0)) + handler.SetTargetDialer(MakeValidatingTCPStreamDialer(allowAll, 0)) done := make(chan struct{}) go func() { StreamServe( @@ -406,7 +406,7 @@ func TestProbeClientBytesBasicModified(t *testing.T) { testMetrics := &probeTestMetrics{} authFunc := NewShadowsocksStreamAuthenticator(cipherList, nil, &fakeShadowsocksMetrics{}, nil) handler := NewStreamHandler(authFunc, 200*time.Millisecond) - handler.SetTargetDialerStream(MakeValidatingTCPStreamDialer(allowAll, 0)) + handler.SetTargetDialer(MakeValidatingTCPStreamDialer(allowAll, 0)) done := make(chan struct{}) go func() { StreamServe( @@ -445,7 +445,7 @@ func TestProbeClientBytesCoalescedModified(t *testing.T) { testMetrics := &probeTestMetrics{} authFunc := NewShadowsocksStreamAuthenticator(cipherList, nil, &fakeShadowsocksMetrics{}, nil) handler := NewStreamHandler(authFunc, 200*time.Millisecond) - handler.SetTargetDialerStream(MakeValidatingTCPStreamDialer(allowAll, 0)) + handler.SetTargetDialer(MakeValidatingTCPStreamDialer(allowAll, 0)) done := make(chan struct{}) go func() { StreamServe( @@ -747,7 +747,7 @@ func TestStreamServeEarlyClose(t *testing.T) { err = tcpListener.Close() require.NoError(t, err) // This should return quickly, without timing out or calling the handler. - StreamServeStream(WrapStreamAcceptFunc(tcpListener.AcceptTCP), nil) + StreamServe(WrapStreamAcceptFunc(tcpListener.AcceptTCP), nil) } // Makes sure the TCP listener returns [io.ErrClosed] on Close(). diff --git a/service/udp.go b/service/udp.go index 70363d29..e21a52c5 100644 --- a/service/udp.go +++ b/service/udp.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "io" "log/slog" "net" "net/netip" @@ -47,8 +48,13 @@ type UDPAssociationMetrics interface { AddClose() } -// Max UDP buffer size for the server code. -const serverUDPBufferSize = 64 * 1024 +const ( + // Max UDP buffer size for the server code. + serverUDPBufferSize = 64 * 1024 + + // A UDP NAT timeout of at least 5 minutes is recommended in RFC 4787 Section 4.3. + defaultNatTimeout time.Duration = 5 * time.Minute +) // Buffer pool used for reading UDP packets. var readBufPool = slicepool.MakePool(serverUDPBufferSize) @@ -83,135 +89,148 @@ func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherLis return nil, "", nil, errors.New("could not find valid UDP cipher") } -type packetHandler struct { +type associationHandler struct { logger *slog.Logger ciphers CipherList ssm ShadowsocksConnMetrics targetIPValidator onet.TargetIPValidator + targetListener transport.PacketListener } -var _ PacketHandler = (*packetHandler)(nil) +var _ AssociationHandler = (*associationHandler)(nil) -// NewPacketHandler creates a PacketHandler -func NewPacketHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) PacketHandler { +// NewAssociationHandler creates a AssociationHandler +func NewAssociationHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) AssociationHandler { if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} } - return &packetHandler{ + return &associationHandler{ logger: noopLogger(), ciphers: cipherList, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, + targetListener: MakeTargetUDPListener(defaultNatTimeout, 0), } } -// PacketHandler is a handler that handles UDP assocations. -type PacketHandler interface { - HandlePacket(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) +// AssociationHandler is a handler that handles UDP assocations. +type AssociationHandler interface { + HandleAssociation(ctx context.Context, conn net.Conn, assocMetrics UDPAssociationMetrics) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) // SetTargetIPValidator sets the function to be used to validate the target IP addresses. SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) + // SetTargetPacketListener sets the packet listener to use for target connections. + SetTargetPacketListener(targetListener transport.PacketListener) } -func (h *packetHandler) SetLogger(l *slog.Logger) { +func (h *associationHandler) SetLogger(l *slog.Logger) { if l == nil { l = noopLogger() } h.logger = l } -func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { +func (h *associationHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { h.targetIPValidator = targetIPValidator } -func (h *packetHandler) authenticate(pkt []byte, assoc PacketAssociation) ([]byte, error) { - var textData []byte - keyResult, err := assoc.DoOnce(func() (any, error) { - var ( - keyID string - key *shadowsocks.EncryptionKey - keyErr error - ) - ip := assoc.ClientAddr().AddrPort().Addr() - textLazySlice := readBufPool.LazySlice() - textBuf := textLazySlice.Acquire() - unpackStart := time.Now() - textData, keyID, key, keyErr = findAccessKeyUDP(ip, textBuf, pkt, h.ciphers, h.logger) - timeToCipher := time.Since(unpackStart) - textLazySlice.Release() - h.ssm.AddCipherSearch(keyErr == nil, timeToCipher) - - assoc.AddAuthentication(keyID) - if keyErr != nil { - return nil, keyErr +func (h *associationHandler) SetTargetPacketListener(targetListener transport.PacketListener) { + h.targetListener = targetListener +} + +func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn net.Conn, assocMetrics UDPAssociationMetrics) { + l := h.logger.With(slog.Any("client", clientConn.RemoteAddr())) + + defer func() { + debugUDP(l, "Done") + assocMetrics.AddClose() + }() + + var cryptoKey *shadowsocks.EncryptionKey + + readBufLazySlice := readBufPool.LazySlice() + readBuf := readBufLazySlice.Acquire() + defer readBufLazySlice.Release() + for { + clientProxyBytes, err := clientConn.Read(readBuf) + if errors.Is(err, net.ErrClosed) { + break } - go relayTargetToClient(assoc, func(pkt []byte, assoc PacketAssociation) error { - return h.handlePacketFromTarget(pkt, assoc, key) - }) - return key, nil - }) - if err != nil { - return nil, err - } - cryptoKey, ok := keyResult.(*shadowsocks.EncryptionKey) - if !ok { - // This should never happen in practice. We return a `shadowsocks.EncrypTionKey` - // in the `authenticate` anonymous function above. - return nil, errors.New("authentication result is not an encryption key") - } + pkt := readBuf[:clientProxyBytes] + debugUDP(l, "Outbound packet.", slog.Int("bytes", clientProxyBytes)) + + var proxyTargetBytes int + var targetConn net.PacketConn + + connError := func() *onet.ConnectionError { + var payload []byte + var tgtUDPAddr *net.UDPAddr + if targetConn == nil { + ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() + var textData []byte + var keyID string + textLazySlice := readBufPool.LazySlice() + unpackStart := time.Now() + textData, keyID, cryptoKey, err = findAccessKeyUDP(ip, textLazySlice.Acquire(), pkt, h.ciphers, h.logger) + timeToCipher := time.Since(unpackStart) + textLazySlice.Release() + h.ssm.AddCipherSearch(err == nil, timeToCipher) - if textData == nil { - // This is a subsequent packet. First packets are already decrypted as part of the - // initial access key search. - unpackStart := time.Now() - textData, err = shadowsocks.Unpack(nil, pkt, cryptoKey) - timeToCipher := time.Since(unpackStart) - h.ssm.AddCipherSearch(err == nil, timeToCipher) - } + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err) + } + assocMetrics.AddAuthentication(keyID) - return textData, nil -} + var onetErr *onet.ConnectionError + if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { + return onetErr + } -func (h *packetHandler) HandlePacket(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) { - l := h.logger.With(slog.Any("association", assoc)) - defer debugUDP(l, "Done") + // Create the target connection. + targetConn, err = h.targetListener.ListenPacket(ctx) + if err != nil { + return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create a `PacketConn`", err) + } + l = l.With(slog.Any("ltarget", targetConn.LocalAddr())) + go relayTargetToClient(targetConn, clientConn, cryptoKey, assocMetrics, l) + } else { + unpackStart := time.Now() + textData, err := shadowsocks.Unpack(nil, pkt, cryptoKey) + timeToCipher := time.Since(unpackStart) + h.ssm.AddCipherSearch(err == nil, timeToCipher) - debugUDP(l, "Outbound packet.", slog.Int("bytes", len(pkt))) + if err != nil { + return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) + } - var proxyTargetBytes int - connError := func() *onet.ConnectionError { - textData, err := h.authenticate(pkt, assoc) - lazySlice.Release() - if err != nil { - return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err) - } + var onetErr *onet.ConnectionError + if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { + return onetErr + } + } - payload, tgtUDPAddr, onetErr := h.validatePacket(textData) - if onetErr != nil { - return onetErr - } + debugUDP(l, "Proxy exit.") + proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature + if err != nil { + return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + } + return nil + }() - debugUDP(l, "Proxy exit.") - proxyTargetBytes, err = assoc.WriteToTarget(payload, tgtUDPAddr) // accept only UDPAddr despite the signature - if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + status := "OK" + if connError != nil { + debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + status = connError.Status } - return nil - }() - - status := "OK" - if connError != nil { - debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) - status = connError.Status + assocMetrics.AddPacketFromClient(status, int64(clientProxyBytes), int64(proxyTargetBytes)) } - assoc.AddPacketFromClient(status, int64(len(pkt)), int64(proxyTargetBytes)) } // Given the decrypted contents of a UDP packet, return // the payload and the destination address, or an error if // this packet cannot or should not be forwarded. -func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { +func (h *associationHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { tgtAddr := socks.SplitAddr(textData) if tgtAddr == nil { return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) @@ -229,93 +248,22 @@ func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, * return payload, tgtUDPAddr, nil } -// Get the maximum length of the shadowsocks address header by parsing -// and serializing an IPv6 address from the example range. -var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) - -func (h *packetHandler) handlePacketFromTarget(pkt []byte, assoc PacketAssociation, cryptoKey *shadowsocks.EncryptionKey) error { - l := h.logger.With(slog.Any("association", assoc)) +type AssociationHandleFunc func(ctx context.Context, conn net.Conn) - expired := false - var bodyLen, proxyClientBytes int - connError := func() *onet.ConnectionError { - saltSize := cryptoKey.SaltSize() - // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). - bodyStart := saltSize + maxAddrLen - - var ( - raddr net.Addr - err error - ) - // `readBuf` receives the plaintext body in `pkt`: - // [padding?][salt][address][body][tag][unused] - // |-- bodyStart --|[ readBuf ] - readBuf := pkt[bodyStart:] - bodyLen, raddr, err = assoc.ReadFromTarget(readBuf) - if err != nil { - if netErr, ok := err.(net.Error); ok { - if netErr.Timeout() { - expired = true - return nil - } - } - return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) - } - - debugUDP(l, "Got response.", slog.Any("rtarget", raddr)) - srcAddr := socks.ParseAddr(raddr.String()) - addrStart := bodyStart - len(srcAddr) - // `plainTextBuf` concatenates the SOCKS address and body: - // [padding?][salt][address][body][tag][unused] - // |-- addrStart -|[plaintextBuf ] - plaintextBuf := pkt[addrStart : bodyStart+bodyLen] - copy(plaintextBuf, srcAddr) - - // saltStart is 0 if raddr is IPv6. - saltStart := addrStart - saltSize - // `packBuf` adds space for the salt and tag. - // `buf` shows the space that was used. - // [padding?][salt][address][body][tag][unused] - // [ packBuf ] - // [ buf ] - packBuf := pkt[saltStart:] - buf, err := shadowsocks.Pack(packBuf, plaintextBuf, cryptoKey) // Encrypt in-place - if err != nil { - return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) - } - proxyClientBytes, err = assoc.WriteToClient(buf) - if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) - } - return nil - }() - status := "OK" - if connError != nil { - debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) - status = connError.Status - } - if expired { - return errors.New("target connection has expired") - } - assoc.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes)) - return nil -} - -type NewAssociationFunc func(conn net.Conn) (PacketAssociation, error) - -// PacketServe listens for UDP packets on the provided [net.PacketConn], creates -// and manages NAT associations, and invokes the provided `handlePacket` -// function for each packet. It uses a NAT map to track active associations and +// PacketServe listens for UDP packets on the provided [net.PacketConn] and creates +// and manages NAT associations. It uses a NAT map to track active associations and // handles their lifecycle. -func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, handlePacket PacketHandleFuncWithLazySlice, metrics NATMetrics) { +func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, metrics NATMetrics) { nm := newNATmap() - defer nm.Close() + ctx, contextCancel := context.WithCancel(context.Background()) + defer contextCancel() for { lazySlice := readBufPool.LazySlice() buffer := lazySlice.Acquire() - isClosed := func() bool { + expired := false + func() { defer func() { if r := recover(); r != nil { slog.Error("Panic in UDP loop. Continuing to listen.", "err", r) @@ -327,63 +275,108 @@ func PacketServe(clientConn net.PacketConn, newAssociation NewAssociationFunc, h if err != nil { lazySlice.Release() if errors.Is(err, net.ErrClosed) { - return true + expired = true + return } slog.Warn("Failed to read from client. Continuing to listen.", "err", err) - return false + return } - pkt := buffer[:n] + pkt := &packet{payload: buffer[:n], done: lazySlice.Release} // TODO(#19): Include server address in the NAT key as well. assoc := nm.Get(addr.String()) if assoc == nil { - conn := &natconn{PacketConn: clientConn, raddr: addr} - assoc, err = newAssociation(conn) + assoc = &association{ + pc: clientConn, + raddr: addr, + readCh: make(chan *packet, 5), + } if err != nil { slog.Error("Failed to handle association", slog.Any("err", err)) - return false + return } metrics.AddNATEntry() nm.Add(addr.String(), assoc) + go func() { + assocHandle(ctx, assoc) + metrics.RemoveNATEntry() + nm.Del(addr.String()) + }() } select { - case <-assoc.Done(): - lazySlice.Release() - metrics.RemoveNATEntry() - nm.Del(addr.String()) + case assoc.readCh <- pkt: default: - go handlePacket(pkt, assoc, lazySlice) + slog.Debug("Dropping packet due to full read queue") + // TODO: Add a metric to track number of dropped packets. } - return false }() - if isClosed { - return + if expired { + break } } } -// natconn wraps a [net.PacketConn] with an address into a [net.Conn]. -type natconn struct { - net.PacketConn - raddr net.Addr +type packet struct { + // The contents of the packet. + payload []byte + + // A function to call as soon as the payload has been consumed. This can be + // used to release resources. + done func() +} + +// association wraps a [net.PacketConn] with an address into a [net.Conn]. +type association struct { + pc net.PacketConn + raddr net.Addr + readCh chan *packet } -var _ net.Conn = (*natconn)(nil) +var _ net.Conn = (*association)(nil) -func (c *natconn) Read(p []byte) (int, error) { - n, _, err := c.PacketConn.ReadFrom(p) - return n, err +func (c *association) Read(p []byte) (int, error) { + pkt, ok := <-c.readCh + if !ok { + return 0, net.ErrClosed + } + n := copy(p, pkt.payload) + pkt.done() + if n < len(pkt.payload) { + return n, io.ErrShortBuffer + } + return n, nil } -func (c *natconn) Write(b []byte) (n int, err error) { - return c.PacketConn.WriteTo(b, c.raddr) +func (c *association) Write(b []byte) (n int, err error) { + return c.pc.WriteTo(b, c.raddr) } -func (c *natconn) RemoteAddr() net.Addr { +func (c *association) Close() error { + close(c.readCh) + return c.pc.Close() +} + +func (c *association) LocalAddr() net.Addr { + return c.pc.LocalAddr() +} + +func (c *association) RemoteAddr() net.Addr { return c.raddr } +func (c *association) SetDeadline(t time.Time) error { + return c.pc.SetDeadline(t) +} + +func (c *association) SetReadDeadline(t time.Time) error { + return c.pc.SetReadDeadline(t) +} + +func (c *association) SetWriteDeadline(t time.Time) error { + return c.pc.SetWriteDeadline(t) +} + func isDNS(addr net.Addr) bool { _, port, _ := net.SplitHostPort(addr.String()) return port == "53" @@ -449,15 +442,15 @@ func (c *timedPacketConn) ReadFrom(buf []byte) (int, net.Addr, error) { // Packet NAT table type natmap struct { sync.RWMutex - associations map[string]PacketAssociation + associations map[string]*association } func newNATmap() *natmap { - return &natmap{associations: make(map[string]PacketAssociation)} + return &natmap{associations: make(map[string]*association)} } // Get returns a UDP NAT entry from the natmap. -func (m *natmap) Get(clientAddr string) PacketAssociation { +func (m *natmap) Get(clientAddr string) *association { m.RLock() defer m.RUnlock() return m.associations[clientAddr] @@ -474,193 +467,97 @@ func (m *natmap) Del(clientAddr string) { } // Add adds a new UDP NAT entry to the natmap. -func (m *natmap) Add(clientAddr string, assoc PacketAssociation) { +func (m *natmap) Add(clientAddr string, assoc *association) { m.Lock() defer m.Unlock() m.associations[clientAddr] = assoc } -func (m *natmap) Close() error { - m.Lock() - defer m.Unlock() - - var err error - for _, assoc := range m.associations { - if e := assoc.Close(); e != nil { - err = e - } - } - return err -} - -// PacketHandleFunc processes a single incoming packet. -type PacketHandleFunc func(pkt []byte, assoc PacketAssociation) error - -// PacketHandleFuncWithLazySlice processes a single incoming packet. -// -// lazySlice is the LazySlice that holds the pkt buffer, which should be -// released as soon as the packet is processed. -type PacketHandleFuncWithLazySlice func(pkt []byte, assoc PacketAssociation, lazySlice slicepool.LazySlice) - -func HandleAssociation(assoc PacketAssociation, handlePacket PacketHandleFuncWithLazySlice) { - for { - lazySlice := readBufPool.LazySlice() - buf := lazySlice.Acquire() - n, err := assoc.ReadFromClient(buf) - if errors.Is(err, net.ErrClosed) { - lazySlice.Release() - return - } - pkt := buf[:n] - select { - case <-assoc.Done(): - lazySlice.Release() - return - default: - go handlePacket(pkt, assoc, lazySlice) - } - } -} +// Get the maximum length of the shadowsocks address header by parsing +// and serializing an IPv6 address from the example range. +var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) -// relayTargetToClient handles the target-side of the association by -// copying from target to client until read timeout. -func relayTargetToClient(assoc PacketAssociation, handlePacket PacketHandleFunc) { - defer assoc.CloseTarget() +// relayTargetToClient copies from target to client until read timeout. +func relayTargetToClient(targetConn net.PacketConn, clientConn net.Conn, cryptoKey *shadowsocks.EncryptionKey, m UDPAssociationMetrics, l *slog.Logger) { + defer targetConn.Close() - // pkt is used for in-place encryption of downstream UDP packets. + // pkt is used for in-place encryption of downstream UDP packets, with the layout + // [padding?][salt][address][body][tag][extra] // Padding is only used if the address is IPv4. pkt := make([]byte, serverUDPBufferSize) - for { - if err := handlePacket(pkt, assoc); err != nil { - break - } - } -} - -// PacketAssociation represents a UDP association. -type PacketAssociation interface { - // TODO(sbruens): Decouple the metrics from the association. - UDPAssociationMetrics - - // ReadFromClient reads data from the client side of the association. - ReadFromClient(b []byte) (n int, err error) - - // WriteToClient writes data to the client side of the association. - WriteToClient(b []byte) (n int, err error) - - // ReadFromTarget reads data from the target side of the association. - ReadFromTarget(p []byte) (n int, addr net.Addr, err error) - - // WriteToTarget writes data to the target side of the association. - WriteToTarget(b []byte, addr net.Addr) (int, error) + saltSize := cryptoKey.SaltSize() + // Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6). + bodyStart := saltSize + maxAddrLen - // ClientAddr returns the remote network address of the client connection, if known. - ClientAddr() *net.UDPAddr - - // DoOnce executes the provided function only once and caches the result. - DoOnce(f func() (any, error)) (any, error) - - // Done returns a channel that is closed when the association is closed. - Done() <-chan struct{} - - // Close closes the association and releases any associated resources. - Close() error - - // Closes the target side of the association. - CloseTarget() error -} - -type association struct { - clientConn net.Conn - targetConn net.PacketConn - - once sync.Once - cachedResult any - - UDPAssociationMetrics - doneCh chan struct{} -} - -var _ PacketAssociation = (*association)(nil) -var _ UDPAssociationMetrics = (*association)(nil) -var _ slog.LogValuer = (*association)(nil) - -// NewPacketAssociation creates a new packet-based association. -func NewPacketAssociation(conn net.Conn, listener transport.PacketListener, m UDPAssociationMetrics) (PacketAssociation, error) { - if m == nil { - m = &NoOpUDPAssociationMetrics{} - } - // Create the target connection - targetConn, err := listener.ListenPacket(context.Background()) - if err != nil { - return nil, fmt.Errorf("failed to create target connection: %w", err) - } - - return &association{ - clientConn: conn, - targetConn: targetConn, - UDPAssociationMetrics: m, - doneCh: make(chan struct{}), - }, nil -} - -func (a *association) ReadFromClient(b []byte) (n int, err error) { - return a.clientConn.Read(b) -} - -func (a *association) WriteToClient(b []byte) (n int, err error) { - return a.clientConn.Write(b) -} - -func (a *association) ReadFromTarget(p []byte) (n int, addr net.Addr, err error) { - return a.targetConn.ReadFrom(p) -} - -func (a *association) WriteToTarget(b []byte, addr net.Addr) (int, error) { - return a.targetConn.WriteTo(b, addr) -} - -func (a *association) ClientAddr() *net.UDPAddr { - return a.clientConn.RemoteAddr().(*net.UDPAddr) -} + expired := false + for { + var targetProxyBytes, proxyClientBytes int + connError := func() *onet.ConnectionError { + var ( + raddr net.Addr + err error + ) + // `readBuf` receives the plaintext body in `pkt`: + // [padding?][salt][address][body][tag][unused] + // |-- bodyStart --|[ readBuf ] + readBuf := pkt[bodyStart:] + targetProxyBytes, raddr, err = targetConn.ReadFrom(readBuf) + if err != nil { + if netErr, ok := err.(net.Error); ok { + if netErr.Timeout() { + expired = true + return nil + } + } + return onet.NewConnectionError("ERR_READ", "Failed to read from target", err) + } -func (a *association) DoOnce(f func() (any, error)) (any, error) { - var err error - a.once.Do(func() { - result, err := f() - if err == nil { - a.cachedResult = result + debugUDP(l, "Got response.", slog.Any("rtarget", raddr)) + srcAddr := socks.ParseAddr(raddr.String()) + addrStart := bodyStart - len(srcAddr) + // `plainTextBuf` concatenates the SOCKS address and body: + // [padding?][salt][address][body][tag][unused] + // |-- addrStart -|[plaintextBuf ] + plaintextBuf := pkt[addrStart : bodyStart+targetProxyBytes] + copy(plaintextBuf, srcAddr) + + // saltStart is 0 if raddr is IPv6. + saltStart := addrStart - saltSize + // `packBuf` adds space for the salt and tag. + // `buf` shows the space that was used. + // [padding?][salt][address][body][tag][unused] + // [ packBuf ] + // [ buf ] + packBuf := pkt[saltStart:] + buf, err := shadowsocks.Pack(packBuf, plaintextBuf, cryptoKey) // Encrypt in-place + if err != nil { + return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) + } + proxyClientBytes, err = clientConn.Write(buf) + if err != nil { + if netErr, ok := err.(net.Error); ok { + if netErr.Timeout() { + expired = true + return nil + } + } + return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) + } + return nil + }() + status := "OK" + if connError != nil { + debugUDP(l, "Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause)) + status = connError.Status } - }) - return a.cachedResult, err -} -func (a *association) Done() <-chan struct{} { - return a.doneCh -} - -func (a *association) Close() error { - now := time.Now() - return a.clientConn.SetReadDeadline(now) -} - -func (a *association) CloseTarget() error { - a.UDPAssociationMetrics.AddClose() - err := a.targetConn.Close() - if err != nil { - return err + if expired { + break + } + m.AddPacketFromTarget(status, int64(targetProxyBytes), int64(proxyClientBytes)) } - close(a.doneCh) - return nil -} - -func (a *association) LogValue() slog.Value { - return slog.GroupValue( - slog.Any("client", a.clientConn.RemoteAddr()), - slog.Any("ltarget", a.targetConn.LocalAddr()), - ) } // NoOpUDPAssociationMetrics is a [UDPAssociationMetrics] that doesn't do anything. Useful in tests diff --git a/service/udp_test.go b/service/udp_test.go index 51363c35..bf1090f6 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -17,8 +17,8 @@ package service import ( "bytes" "context" - "errors" "fmt" + "io" "net" "net/netip" "sync" @@ -49,7 +49,7 @@ func init() { natCryptoKey, _ = shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, "test password") } -type packet struct { +type fakePacket struct { addr net.Addr payload []byte err error @@ -65,16 +65,16 @@ func (ln *packetListener) ListenPacket(ctx context.Context) (net.PacketConn, err type fakePacketConn struct { net.PacketConn - send chan packet - recv chan packet + send chan fakePacket + recv chan fakePacket deadline time.Time mu sync.Mutex } func makePacketConn() *fakePacketConn { return &fakePacketConn{ - send: make(chan packet, 1), - recv: make(chan packet), + send: make(chan fakePacket, 1), + recv: make(chan fakePacket), } } @@ -102,7 +102,7 @@ func (conn *fakePacketConn) WriteTo(payload []byte, addr net.Addr) (int, error) } }() - conn.send <- packet{addr, payload, nil} + conn.send <- fakePacket{addr, payload, nil} return len(payload), err } @@ -113,7 +113,7 @@ func (conn *fakePacketConn) ReadFrom(buffer []byte) (int, net.Addr, error) { } n := copy(buffer, pkt.payload) if n < len(pkt.payload) { - return n, pkt.addr, errors.New("buffer was too short") + return n, pkt.addr, io.ErrShortBuffer } return n, pkt.addr, pkt.err } @@ -182,7 +182,7 @@ func sendSSPayload(conn *fakePacketConn, addr net.Addr, cipher *shadowsocks.Encr plaintext := append(socksAddr, payload...) ciphertext := make([]byte, cipher.SaltSize()+len(plaintext)+cipher.TagSize()) shadowsocks.Pack(ciphertext, plaintext, cipher) - conn.recv <- packet{ + conn.recv <- fakePacket{ addr: &clientAddr, payload: ciphertext, } @@ -191,37 +191,38 @@ func sendSSPayload(conn *fakePacketConn, addr net.Addr, cipher *shadowsocks.Encr // startTestHandler creates a new association handler with a fake // client and target connection for testing purposes. It also starts a // PacketServe goroutine to handle incoming packets on the client connection. -func startTestHandler() (PacketHandler, func(target net.Addr, payload []byte), *fakePacketConn) { +func startTestHandler() (AssociationHandler, func(target net.Addr, payload []byte), *fakePacketConn) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewPacketHandler(ciphers, nil) + handler := NewAssociationHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) - return assoc, nil - }, handler.Handle, &natTestMetrics{}) + handler.SetTargetPacketListener(&packetListener{targetConn}) + go PacketServe(clientConn, func(ctx context.Context, conn net.Conn) { + handler.HandleAssociation(ctx, conn, &fakeUDPAssociationMetrics{}) + }, &natTestMetrics{}) return handler, func(target net.Addr, payload []byte) { sendSSPayload(clientConn, target, cipher, payload) }, targetConn } -func TestNatconnCloseWhileReading(t *testing.T) { - nc := &natconn{ - PacketConn: makePacketConn(), - raddr: &clientAddr, +func TestAssociationCloseWhileReading(t *testing.T) { + assoc := &association{ + pc: makePacketConn(), + raddr: &clientAddr, + readCh: make(chan *packet), } go func() { buf := make([]byte, 1024) - nc.Read(buf) + assoc.Read(buf) }() - err := nc.Close() + err := assoc.Close() assert.NoError(t, err, "Close should not panic or return an error") } -func TestPacketHandler_Handle_IPFilter(t *testing.T) { +func TestAssociationHandler_Handle_IPFilter(t *testing.T) { t.Run("RequirePublicIP blocks localhost", func(t *testing.T) { handler, sendPayload, targetConn := startTestHandler() handler.SetTargetIPValidator(onet.RequirePublicIP) @@ -252,14 +253,14 @@ func TestPacketHandler_Handle_IPFilter(t *testing.T) { func TestUpstreamMetrics(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewPacketHandler(ciphers, nil) + handler := NewAssociationHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() + handler.SetTargetPacketListener(&packetListener{targetConn}) metrics := &fakeUDPAssociationMetrics{} - go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, metrics) - return assoc, nil - }, handler.Handle, &natTestMetrics{}) + go PacketServe(clientConn, func(ctx context.Context, conn net.Conn) { + handler.HandleAssociation(ctx, conn, metrics) + }, &natTestMetrics{}) // Test both the first-packet and subsequent-packet cases. const N = 10 @@ -371,13 +372,13 @@ func TestTimedPacketConn(t *testing.T) { t.Run("FastClose", func(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewPacketHandler(ciphers, nil) + handler := NewAssociationHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) - return assoc, nil - }, handler.Handle, &natTestMetrics{}) + handler.SetTargetPacketListener(&packetListener{targetConn}) + go PacketServe(clientConn, func(ctx context.Context, conn net.Conn) { + handler.HandleAssociation(ctx, conn, &fakeUDPAssociationMetrics{}) + }, &natTestMetrics{}) // Send one DNS query. sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) @@ -385,7 +386,7 @@ func TestTimedPacketConn(t *testing.T) { require.Len(t, sent.payload, 1) // Send the response. response := []byte{1, 2, 3, 4, 5} - received := packet{addr: &dnsAddr, payload: response} + received := fakePacket{addr: &dnsAddr, payload: response} targetConn.recv <- received sent, ok := <-clientConn.send if !ok { @@ -399,13 +400,13 @@ func TestTimedPacketConn(t *testing.T) { t.Run("NoFastClose_NotDNS", func(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewPacketHandler(ciphers, nil) + handler := NewAssociationHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) - return assoc, nil - }, handler.Handle, &natTestMetrics{}) + handler.SetTargetPacketListener(&packetListener{targetConn}) + go PacketServe(clientConn, func(ctx context.Context, conn net.Conn) { + handler.HandleAssociation(ctx, conn, &fakeUDPAssociationMetrics{}) + }, &natTestMetrics{}) // Send one non-DNS packet. sendSSPayload(clientConn, &targetAddr, cipher, []byte{1}) @@ -413,7 +414,7 @@ func TestTimedPacketConn(t *testing.T) { require.Len(t, sent.payload, 1) // Send the response. response := []byte{1, 2, 3, 4, 5} - received := packet{addr: &targetAddr, payload: response} + received := fakePacket{addr: &targetAddr, payload: response} targetConn.recv <- received sent, ok := <-clientConn.send if !ok { @@ -427,13 +428,13 @@ func TestTimedPacketConn(t *testing.T) { t.Run("NoFastClose_MultipleDNS", func(t *testing.T) { ciphers, _ := MakeTestCiphers([]string{"asdf"}) cipher := ciphers.SnapshotForClientIP(netip.Addr{})[0].Value.(*CipherEntry).CryptoKey - handler := NewPacketHandler(ciphers, nil) + handler := NewAssociationHandler(ciphers, nil) clientConn := makePacketConn() targetConn := makePacketConn() - go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - assoc, _ := NewPacketAssociation(conn, &packetListener{targetConn}, nil) - return assoc, nil - }, handler.Handle, &natTestMetrics{}) + handler.SetTargetPacketListener(&packetListener{targetConn}) + go PacketServe(clientConn, func(ctx context.Context, conn net.Conn) { + handler.HandleAssociation(ctx, conn, &fakeUDPAssociationMetrics{}) + }, &natTestMetrics{}) // Send two DNS packets. sendSSPayload(clientConn, &dnsAddr, cipher, []byte{1}) @@ -443,7 +444,7 @@ func TestTimedPacketConn(t *testing.T) { // Send a response. response := []byte{1, 2, 3, 4, 5} - received := packet{addr: &dnsAddr, payload: response} + received := fakePacket{addr: &dnsAddr, payload: response} targetConn.recv <- received <-clientConn.send @@ -458,7 +459,7 @@ func TestTimedPacketConn(t *testing.T) { sendPayload(&targetAddr, []byte{1}) <-targetConn.send // Simulate a read timeout. - received := packet{err: &fakeTimeoutError{}} + received := fakePacket{err: &fakeTimeoutError{}} before := time.Now() targetConn.recv <- received // Wait for targetConn to close. @@ -514,20 +515,6 @@ func TestNATMap(t *testing.T) { assert.Nil(t, nm.Get(addr.String()), "Get should return nil after deleting the entry") }) - - t.Run("Close", func(t *testing.T) { - nm := newNATmap() - addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.1"), Port: 1234} - pc := makePacketConn() - assoc := &association{clientConn: &natconn{PacketConn: pc, raddr: addr}} - nm.Add(addr.String(), assoc) - - err := nm.Close() - assert.NoError(t, err, "Close should not return an error") - - // The underlying connection should be scheduled to close immediately. - assertAlmostEqual(t, pc.deadline, time.Now()) - }) } // Simulates receiving invalid UDP packets on a server with 100 ciphers. @@ -613,7 +600,8 @@ func TestUDPEarlyClose(t *testing.T) { t.Fatal(err) } const testTimeout = 200 * time.Millisecond - ph := NewPacketHandler(cipherList, &fakeShadowsocksMetrics{}) + handler := NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) + handler.SetTargetPacketListener(&packetListener{makePacketConn()}) clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) if err != nil { @@ -621,10 +609,9 @@ func TestUDPEarlyClose(t *testing.T) { } require.Nil(t, clientConn.Close()) // This should return quickly without timing out. - go PacketServe(clientConn, func(conn net.Conn) (PacketAssociation, error) { - assoc, _ := NewPacketAssociation(conn, &packetListener{makePacketConn()}, nil) - return assoc, nil - }, ph.Handle, &natTestMetrics{}) + go PacketServe(clientConn, func(ctx context.Context, conn net.Conn) { + handler.HandleAssociation(ctx, conn, &fakeUDPAssociationMetrics{}) + }, &natTestMetrics{}) } // Makes sure the UDP listener returns [io.ErrClosed] on reads and writes after Close(). From bcac22b0087e8a52c39f0ea6a6e41521be183127 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 21 Jan 2025 17:05:28 -0500 Subject: [PATCH 66/80] Only handle the association if it was new. --- service/udp.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/service/udp.go b/service/udp.go index e21a52c5..1201d1e5 100644 --- a/service/udp.go +++ b/service/udp.go @@ -297,12 +297,15 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m } metrics.AddNATEntry() - nm.Add(addr.String(), assoc) - go func() { - assocHandle(ctx, assoc) - metrics.RemoveNATEntry() - nm.Del(addr.String()) - }() + var existing bool + assoc, existing = nm.Add(addr.String(), assoc) + if !existing { + go func() { + assocHandle(ctx, assoc) + metrics.RemoveNATEntry() + nm.Del(addr.String()) + }() + } } select { case assoc.readCh <- pkt: @@ -466,12 +469,18 @@ func (m *natmap) Del(clientAddr string) { } } -// Add adds a new UDP NAT entry to the natmap. -func (m *natmap) Add(clientAddr string, assoc *association) { +// Add adds a UDP NAT entry to the natmap and returns it. If it already existed, +// in the natmap, the existing entry is returned instead. +func (m *natmap) Add(clientAddr string, assoc *association) (*association, bool) { m.Lock() defer m.Unlock() + if existing, ok := m.associations[clientAddr]; ok { + return existing, true + } + m.associations[clientAddr] = assoc + return assoc, false } // Get the maximum length of the shadowsocks address header by parsing From 220d1d7f3d70a3192b6bf5ce5595e71f7dfc774b Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 21 Jan 2025 17:12:00 -0500 Subject: [PATCH 67/80] Fix the metric race condition in tests. --- internal/integration_test/integration_test.go | 2 ++ service/udp_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 10ac5018..7fb08718 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -288,6 +288,8 @@ type fakeUDPAssociationMetrics struct { var _ service.UDPAssociationMetrics = (*fakeUDPAssociationMetrics)(nil) func (m *fakeUDPAssociationMetrics) AddAuthentication(key string) { + m.mu.Lock() + defer m.mu.Unlock() m.accessKey = key } diff --git a/service/udp_test.go b/service/udp_test.go index bf1090f6..87ec31c0 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -161,6 +161,8 @@ type fakeUDPAssociationMetrics struct { var _ UDPAssociationMetrics = (*fakeUDPAssociationMetrics)(nil) func (m *fakeUDPAssociationMetrics) AddAuthentication(key string) { + m.mu.Lock() + defer m.mu.Unlock() m.accessKey = key } From d81f12828d25d9663ecf6e591f025b7bda82515a Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 21 Jan 2025 17:14:23 -0500 Subject: [PATCH 68/80] Move `AddNATEntry()` call to new entry only. --- service/udp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index 1201d1e5..7c4ca66e 100644 --- a/service/udp.go +++ b/service/udp.go @@ -296,10 +296,10 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m return } - metrics.AddNATEntry() var existing bool assoc, existing = nm.Add(addr.String(), assoc) if !existing { + metrics.AddNATEntry() go func() { assocHandle(ctx, assoc) metrics.RemoveNATEntry() From 96de2a6f06bf2cc4181954214c35a27316c3c765 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 22 Jan 2025 15:45:01 -0500 Subject: [PATCH 69/80] Format. --- caddy/shadowsocks_handler.go | 6 +++--- service/udp_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/caddy/shadowsocks_handler.go b/caddy/shadowsocks_handler.go index adc85d2a..51f71057 100644 --- a/caddy/shadowsocks_handler.go +++ b/caddy/shadowsocks_handler.go @@ -50,10 +50,10 @@ type KeyConfig struct { type ShadowsocksHandler struct { Keys []KeyConfig `json:"keys,omitempty"` - streamHandler outline.StreamHandler + streamHandler outline.StreamHandler associationHandler outline.AssociationHandler - metrics outline.ServiceMetrics - logger *slog.Logger + metrics outline.ServiceMetrics + logger *slog.Logger } var ( diff --git a/service/udp_test.go b/service/udp_test.go index 87ec31c0..718f4601 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -210,8 +210,8 @@ func startTestHandler() (AssociationHandler, func(target net.Addr, payload []byt func TestAssociationCloseWhileReading(t *testing.T) { assoc := &association{ - pc: makePacketConn(), - raddr: &clientAddr, + pc: makePacketConn(), + raddr: &clientAddr, readCh: make(chan *packet), } go func() { From 09e471ffc37b205746e950abf1faf3f130b1249d Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 22 Jan 2025 15:54:33 -0500 Subject: [PATCH 70/80] Address review comments. --- service/shadowsocks.go | 6 +++--- service/udp.go | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/service/shadowsocks.go b/service/shadowsocks.go index e63ba835..75bfe245 100644 --- a/service/shadowsocks.go +++ b/service/shadowsocks.go @@ -125,14 +125,14 @@ func WithPacketListener(listener transport.PacketListener) Option { } type ssConnMetrics struct { - metricFunc func(accessKeyFound bool, timeToCipher time.Duration) + addCipherSearch func(accessKeyFound bool, timeToCipher time.Duration) } var _ ShadowsocksConnMetrics = (*ssConnMetrics)(nil) func (cm *ssConnMetrics) AddCipherSearch(accessKeyFound bool, timeToCipher time.Duration) { - if cm.metricFunc != nil { - cm.metricFunc(accessKeyFound, timeToCipher) + if cm.addCipherSearch != nil { + cm.addCipherSearch(accessKeyFound, timeToCipher) } } diff --git a/service/udp.go b/service/udp.go index 7c4ca66e..48aae40a 100644 --- a/service/udp.go +++ b/service/udp.go @@ -99,7 +99,7 @@ type associationHandler struct { var _ AssociationHandler = (*associationHandler)(nil) -// NewAssociationHandler creates a AssociationHandler +// NewAssociationHandler creates a Shadowsocks proxy AssociationHandler. func NewAssociationHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetrics) AssociationHandler { if ssMetrics == nil { ssMetrics = &NoOpShadowsocksConnMetrics{} @@ -147,12 +147,18 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n assocMetrics.AddClose() }() + var targetConn net.PacketConn var cryptoKey *shadowsocks.EncryptionKey readBufLazySlice := readBufPool.LazySlice() readBuf := readBufLazySlice.Acquire() defer readBufLazySlice.Release() for { + select { + case <-ctx.Done(): + break + default: + } clientProxyBytes, err := clientConn.Read(readBuf) if errors.Is(err, net.ErrClosed) { break @@ -161,7 +167,6 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n debugUDP(l, "Outbound packet.", slog.Int("bytes", clientProxyBytes)) var proxyTargetBytes int - var targetConn net.PacketConn connError := func() *onet.ConnectionError { var payload []byte @@ -290,6 +295,7 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m pc: clientConn, raddr: addr, readCh: make(chan *packet, 5), + doneCh: make(chan struct{}), } if err != nil { slog.Error("Failed to handle association", slog.Any("err", err)) @@ -303,11 +309,13 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m go func() { assocHandle(ctx, assoc) metrics.RemoveNATEntry() - nm.Del(addr.String()) + close(assoc.doneCh) }() } } select { + case <-assoc.doneCh: + nm.Del(addr.String()) case assoc.readCh <- pkt: default: slog.Debug("Dropping packet due to full read queue") @@ -334,6 +342,7 @@ type association struct { pc net.PacketConn raddr net.Addr readCh chan *packet + doneCh chan struct{} } var _ net.Conn = (*association)(nil) @@ -546,12 +555,6 @@ func relayTargetToClient(targetConn net.PacketConn, clientConn net.Conn, cryptoK } proxyClientBytes, err = clientConn.Write(buf) if err != nil { - if netErr, ok := err.(net.Error); ok { - if netErr.Timeout() { - expired = true - return nil - } - } return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) } return nil From 64c48ce384e386d96d2e9507319b429fe82ef61b Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 22 Jan 2025 16:17:49 -0500 Subject: [PATCH 71/80] Make `clientConn` an `io.Writer`. --- service/udp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index 48aae40a..df878f10 100644 --- a/service/udp.go +++ b/service/udp.go @@ -497,7 +497,7 @@ func (m *natmap) Add(clientAddr string, assoc *association) (*association, bool) var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) // relayTargetToClient copies from target to client until read timeout. -func relayTargetToClient(targetConn net.PacketConn, clientConn net.Conn, cryptoKey *shadowsocks.EncryptionKey, m UDPAssociationMetrics, l *slog.Logger) { +func relayTargetToClient(targetConn net.PacketConn, clientConn io.Writer, cryptoKey *shadowsocks.EncryptionKey, m UDPAssociationMetrics, l *slog.Logger) { defer targetConn.Close() // pkt is used for in-place encryption of downstream UDP packets, with the layout From 9f2cdb1d60a13df56cc6863660a02c8bd0add5cf Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 22 Jan 2025 17:09:01 -0500 Subject: [PATCH 72/80] refactor: move IP validation to the PacketListener --- cmd/outline-ss-server/main.go | 4 +-- internal/integration_test/integration_test.go | 11 ++++--- service/udp.go | 32 +++++++++++-------- service/udp_linux.go | 19 ++++++++--- service/udp_other.go | 18 +++++++++-- service/udp_test.go | 14 ++++++-- 6 files changed, 69 insertions(+), 29 deletions(-) diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index e8203d42..c606c242 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -229,7 +229,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { service.WithCiphers(ciphers), service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), - service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, 0)), + service.WithPacketListener(service.MakeTargetUDPListener(onet.RequirePublicIP, s.natTimeout, 0)), service.WithLogger(slog.Default()), ) ln, err := lnSet.ListenStream(addr) @@ -261,7 +261,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { service.WithMetrics(s.serviceMetrics), service.WithReplayCache(&s.replayCache), service.WithStreamDialer(service.MakeValidatingTCPStreamDialer(onet.RequirePublicIP, serviceConfig.Dialer.Fwmark)), - service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, serviceConfig.Dialer.Fwmark)), + service.WithPacketListener(service.MakeTargetUDPListener(onet.RequirePublicIP, s.natTimeout, serviceConfig.Dialer.Fwmark)), service.WithLogger(slog.Default()), ) if err != nil { diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index 7fb08718..e300e1b5 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -35,7 +35,10 @@ import ( "github.com/stretchr/testify/require" ) -const maxUDPPacketSize = 64 * 1024 +const ( + maxUDPPacketSize = 64 * 1024 + timeout = 5 * time.Minute +) func init() { logging.SetLevel(logging.INFO, "") @@ -321,7 +324,7 @@ func TestUDPEcho(t *testing.T) { } proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) - proxy.SetTargetIPValidator(allowAll) + proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, timeout, 0)) natMetrics := &natTestMetrics{} associationMetrics := &fakeUDPAssociationMetrics{} go service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) { @@ -548,7 +551,7 @@ func BenchmarkUDPEcho(b *testing.B) { b.Fatal(err) } proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) - proxy.SetTargetIPValidator(allowAll) + proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, timeout, 0)) done := make(chan struct{}) go func() { service.PacketServe(server, func(ctx context.Context, conn net.Conn) { @@ -594,7 +597,7 @@ func BenchmarkUDPManyKeys(b *testing.B) { b.Fatal(err) } proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) - proxy.SetTargetIPValidator(allowAll) + proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, timeout, 0)) done := make(chan struct{}) go func() { service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) { diff --git a/service/udp.go b/service/udp.go index df878f10..1eb65413 100644 --- a/service/udp.go +++ b/service/udp.go @@ -109,7 +109,7 @@ func NewAssociationHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetri ciphers: cipherList, ssm: ssMetrics, targetIPValidator: onet.RequirePublicIP, - targetListener: MakeTargetUDPListener(defaultNatTimeout, 0), + targetListener: MakeTargetUDPListener(onet.RequirePublicIP, defaultNatTimeout, 0), } } @@ -118,8 +118,6 @@ type AssociationHandler interface { HandleAssociation(ctx context.Context, conn net.Conn, assocMetrics UDPAssociationMetrics) // SetLogger sets the logger used to log messages. Uses a no-op logger if nil. SetLogger(l *slog.Logger) - // SetTargetIPValidator sets the function to be used to validate the target IP addresses. - SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) // SetTargetPacketListener sets the packet listener to use for target connections. SetTargetPacketListener(targetListener transport.PacketListener) } @@ -131,10 +129,6 @@ func (h *associationHandler) SetLogger(l *slog.Logger) { h.logger = l } -func (h *associationHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { - h.targetIPValidator = targetIPValidator -} - func (h *associationHandler) SetTargetPacketListener(targetListener transport.PacketListener) { h.targetListener = targetListener } @@ -232,9 +226,8 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n } } -// Given the decrypted contents of a UDP packet, return -// the payload and the destination address, or an error if -// this packet cannot or should not be forwarded. +// Given the decrypted contents of a UDP packet, return the payload and the +// destination address, or an error if this packet cannot be forwarded. func (h *associationHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { tgtAddr := socks.SplitAddr(textData) if tgtAddr == nil { @@ -245,9 +238,6 @@ func (h *associationHandler) validatePacket(textData []byte) ([]byte, *net.UDPAd if err != nil { return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err) } - if err := h.targetIPValidator(tgtUDPAddr.IP); err != nil { - return nil, nil, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address") - } payload := textData[len(tgtAddr):] return payload, tgtUDPAddr, nil @@ -394,6 +384,22 @@ func isDNS(addr net.Addr) bool { return port == "53" } +type validatingPacketConn struct { + net.PacketConn + targetIPValidator onet.TargetIPValidator +} + +func (vpc *validatingPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { + udpAddr, err := net.ResolveUDPAddr("udp", addr.String()) + if err != nil { + return 0, fmt.Errorf("failed to resolve target address %v", udpAddr) + } + if err := vpc.targetIPValidator(udpAddr.IP); err != nil { + return 0, err + } + return vpc.PacketConn.WriteTo(p, addr) +} + type timedPacketConn struct { net.PacketConn // Connection timeout to apply for non-DNS packets. diff --git a/service/udp_linux.go b/service/udp_linux.go index 10b3282f..9292b970 100644 --- a/service/udp_linux.go +++ b/service/udp_linux.go @@ -23,12 +23,18 @@ import ( "time" "github.com/Jigsaw-Code/outline-sdk/transport" + + onet "github.com/Jigsaw-Code/outline-ss-server/net" ) type udpListener struct { // NAT mapping timeout is the default time a mapping will stay active // without packets traversing the NAT, applied to non-DNS packets. timeout time.Duration + + // The validator to be used to validate target IP addresses. + targetIPValidator onet.TargetIPValidator + // fwmark can be used in conjunction with other Linux networking features like cgroups, network // namespaces, and TC (Traffic Control) for sophisticated network management. // Value of 0 disables fwmark (SO_MARK) (Linux only) @@ -37,14 +43,14 @@ type udpListener struct { // NewPacketListener creates a new PacketListener that listens on UDP // and optionally sets a firewall mark on the socket (Linux only). -func MakeTargetUDPListener(timeout time.Duration, fwmark uint) transport.PacketListener { - return &udpListener{timeout: timeout, fwmark: fwmark} +func MakeTargetUDPListener(targetIPValidator onet.TargetIPValidator, timeout time.Duration, fwmark uint) transport.PacketListener { + return &udpListener{timeout: timeout, targetIPValidator: targetIPValidator, fwmark: fwmark} } func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) { conn, err := net.ListenUDP("udp", nil) if err != nil { - return nil, fmt.Errorf("Failed to create UDP socket: %w", err) + return nil, fmt.Errorf("failed to create UDP socket: %w", err) } if ln.fwmark > 0 { @@ -57,9 +63,12 @@ func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) err = SetFwmark(rawConn, ln.fwmark) if err != nil { conn.Close() - return nil, fmt.Errorf("Failed to set `fwmark`: %w", err) + return nil, fmt.Errorf("failed to set `fwmark`: %w", err) } } - return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, nil + return &validatingPacketConn{ + PacketConn: &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, + targetIPValidator: ln.targetIPValidator, + }, nil } diff --git a/service/udp_other.go b/service/udp_other.go index 0cfde76a..a33d758f 100644 --- a/service/udp_other.go +++ b/service/udp_other.go @@ -22,11 +22,16 @@ import ( "time" "github.com/Jigsaw-Code/outline-sdk/transport" + + onet "github.com/Jigsaw-Code/outline-ss-server/net" ) type udpListener struct { *transport.UDPListener + // The validator to be used to validate target IP addresses. + targetIPValidator onet.TargetIPValidator + // NAT mapping timeout is the default time a mapping will stay active // without packets traversing the NAT, applied to non-DNS packets. timeout time.Duration @@ -34,11 +39,15 @@ type udpListener struct { // fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management. // Value of 0 disables fwmark (SO_MARK) -func MakeTargetUDPListener(timeout time.Duration, fwmark uint) transport.PacketListener { +func MakeTargetUDPListener(targetIPValidator onet.TargetIPValidator, timeout time.Duration, fwmark uint) transport.PacketListener { if fwmark != 0 { panic("fwmark is linux-specific feature and should be 0") } - return &udpListener{UDPListener: &transport.UDPListener{Address: ""}} + return &udpListener{ + targetIPValidator: targetIPValidator, + timeout: timeout, + UDPListener: &transport.UDPListener{Address: ""}, + } } func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) { @@ -46,5 +55,8 @@ func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) if err != nil { return nil, err } - return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, nil + return &validatingPacketConn{ + PacketConn: &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, + targetIPValidator: ln.targetIPValidator, + }, nil } diff --git a/service/udp_test.go b/service/udp_test.go index 718f4601..eab28c87 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -25,6 +25,7 @@ import ( "testing" "time" + "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" logging "github.com/op/go-logging" "github.com/shadowsocks/go-shadowsocks2/socks" @@ -63,6 +64,15 @@ func (ln *packetListener) ListenPacket(ctx context.Context) (net.PacketConn, err return ln.conn, nil } +func WrapWithValidatingPacketListener(conn net.PacketConn, targetIPValidator onet.TargetIPValidator) transport.PacketListener { + return &packetListener{ + &validatingPacketConn{ + PacketConn: conn, + targetIPValidator: targetIPValidator, + }, + } +} + type fakePacketConn struct { net.PacketConn send chan fakePacket @@ -227,7 +237,7 @@ func TestAssociationCloseWhileReading(t *testing.T) { func TestAssociationHandler_Handle_IPFilter(t *testing.T) { t.Run("RequirePublicIP blocks localhost", func(t *testing.T) { handler, sendPayload, targetConn := startTestHandler() - handler.SetTargetIPValidator(onet.RequirePublicIP) + handler.SetTargetPacketListener(WrapWithValidatingPacketListener(targetConn, onet.RequirePublicIP)) sendPayload(&localAddr, []byte{1, 2, 3}) @@ -241,7 +251,7 @@ func TestAssociationHandler_Handle_IPFilter(t *testing.T) { t.Run("allowAll allows localhost", func(t *testing.T) { handler, sendPayload, targetConn := startTestHandler() - handler.SetTargetIPValidator(allowAll) + handler.SetTargetPacketListener(WrapWithValidatingPacketListener(targetConn, allowAll)) sendPayload(&localAddr, []byte{1, 2, 3}) From 76789e7284e4459d70011a474eb59f2012225636 Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 22 Jan 2025 17:14:40 -0500 Subject: [PATCH 73/80] Rename `validatePacket()` to reflect what it currently does. --- service/udp.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/service/udp.go b/service/udp.go index 1eb65413..7deaa68e 100644 --- a/service/udp.go +++ b/service/udp.go @@ -164,7 +164,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n connError := func() *onet.ConnectionError { var payload []byte - var tgtUDPAddr *net.UDPAddr + var tgtAddr net.Addr if targetConn == nil { ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var textData []byte @@ -182,7 +182,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n assocMetrics.AddAuthentication(keyID) var onetErr *onet.ConnectionError - if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { + if payload, tgtAddr, onetErr = h.extractPayloadAndDestination(textData); onetErr != nil { return onetErr } @@ -204,13 +204,13 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n } var onetErr *onet.ConnectionError - if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil { + if payload, tgtAddr, onetErr = h.extractPayloadAndDestination(textData); onetErr != nil { return onetErr } } debugUDP(l, "Proxy exit.") - proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature + proxyTargetBytes, err = targetConn.WriteTo(payload, tgtAddr) if err != nil { return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) } @@ -226,9 +226,9 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n } } -// Given the decrypted contents of a UDP packet, return the payload and the -// destination address, or an error if this packet cannot be forwarded. -func (h *associationHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { +// extractPayloadAndDestination processes a decrypted Shadowsocks UDP packet and +// extracts the payload data and destination address. +func (h *associationHandler) extractPayloadAndDestination(textData []byte) ([]byte, net.Addr, *onet.ConnectionError) { tgtAddr := socks.SplitAddr(textData) if tgtAddr == nil { return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) From de27a3280c4fc8dd946f26e3c33e474bb8a3cf9f Mon Sep 17 00:00:00 2001 From: sbruens Date: Wed, 22 Jan 2025 17:19:17 -0500 Subject: [PATCH 74/80] Reorder for consistency. --- service/udp_linux.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/udp_linux.go b/service/udp_linux.go index 9292b970..788f18f9 100644 --- a/service/udp_linux.go +++ b/service/udp_linux.go @@ -28,13 +28,13 @@ import ( ) type udpListener struct { + // The validator to be used to validate target IP addresses. + targetIPValidator onet.TargetIPValidator + // NAT mapping timeout is the default time a mapping will stay active // without packets traversing the NAT, applied to non-DNS packets. timeout time.Duration - // The validator to be used to validate target IP addresses. - targetIPValidator onet.TargetIPValidator - // fwmark can be used in conjunction with other Linux networking features like cgroups, network // namespaces, and TC (Traffic Control) for sophisticated network management. // Value of 0 disables fwmark (SO_MARK) (Linux only) From a62e0b65544b8bb863209ff4b5dac8cfc5f77e19 Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 23 Jan 2025 13:39:05 -0500 Subject: [PATCH 75/80] Handle the `EOF` case and stop reading from the connection. --- service/udp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/udp.go b/service/udp.go index df878f10..e3d78309 100644 --- a/service/udp.go +++ b/service/udp.go @@ -160,7 +160,8 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n default: } clientProxyBytes, err := clientConn.Read(readBuf) - if errors.Is(err, net.ErrClosed) { + if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { + debugUDP(l, "Client closed connection") break } pkt := readBuf[:clientProxyBytes] From f67f21792b836076dad60ad0630e78361b0f1bba Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 23 Jan 2025 15:19:34 -0500 Subject: [PATCH 76/80] Pass through the connection errors from the IP validator. --- service/udp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/udp.go b/service/udp.go index 9c2e567c..ed11da24 100644 --- a/service/udp.go +++ b/service/udp.go @@ -213,7 +213,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n debugUDP(l, "Proxy exit.") proxyTargetBytes, err = targetConn.WriteTo(payload, tgtAddr) if err != nil { - return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err) + return ensureConnectionError(err, "ERR_WRITE", "Failed to write to target") } return nil }() @@ -396,7 +396,7 @@ func (vpc *validatingPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { return 0, fmt.Errorf("failed to resolve target address %v", udpAddr) } if err := vpc.targetIPValidator(udpAddr.IP); err != nil { - return 0, err + return 0, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address") } return vpc.PacketConn.WriteTo(p, addr) } From 242767026b596060c5854a6abf31a377c128c971 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 24 Jan 2025 14:21:39 -0500 Subject: [PATCH 77/80] Address review comments. --- service/udp.go | 36 ++++++++++++++++++------------------ service/udp_test.go | 6 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/service/udp.go b/service/udp.go index e3d78309..775d7b27 100644 --- a/service/udp.go +++ b/service/udp.go @@ -198,7 +198,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n if err != nil { return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create a `PacketConn`", err) } - l = l.With(slog.Any("ltarget", targetConn.LocalAddr())) + l = l.With(slog.Any("tgtListener", targetConn.LocalAddr())) go relayTargetToClient(targetConn, clientConn, cryptoKey, assocMetrics, l) } else { unpackStart := time.Now() @@ -277,7 +277,7 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m lazySlice.Release() } }() - n, addr, err := clientConn.ReadFrom(buffer) + n, clientAddr, err := clientConn.ReadFrom(buffer) if err != nil { lazySlice.Release() if errors.Is(err, net.ErrClosed) { @@ -290,13 +290,13 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m pkt := &packet{payload: buffer[:n], done: lazySlice.Release} // TODO(#19): Include server address in the NAT key as well. - assoc := nm.Get(addr.String()) + assoc := nm.Get(clientAddr.String()) if assoc == nil { assoc = &association{ - pc: clientConn, - raddr: addr, - readCh: make(chan *packet, 5), - doneCh: make(chan struct{}), + pc: clientConn, + clientAddr: clientAddr, + readCh: make(chan *packet, 5), + doneCh: make(chan struct{}), } if err != nil { slog.Error("Failed to handle association", slog.Any("err", err)) @@ -304,7 +304,7 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m } var existing bool - assoc, existing = nm.Add(addr.String(), assoc) + assoc, existing = nm.Add(clientAddr.String(), assoc) if !existing { metrics.AddNATEntry() go func() { @@ -316,7 +316,7 @@ func PacketServe(clientConn net.PacketConn, assocHandle AssociationHandleFunc, m } select { case <-assoc.doneCh: - nm.Del(addr.String()) + nm.Del(clientAddr.String()) case assoc.readCh <- pkt: default: slog.Debug("Dropping packet due to full read queue") @@ -340,10 +340,10 @@ type packet struct { // association wraps a [net.PacketConn] with an address into a [net.Conn]. type association struct { - pc net.PacketConn - raddr net.Addr - readCh chan *packet - doneCh chan struct{} + pc net.PacketConn + clientAddr net.Addr + readCh chan *packet + doneCh chan struct{} } var _ net.Conn = (*association)(nil) @@ -362,7 +362,7 @@ func (c *association) Read(p []byte) (int, error) { } func (c *association) Write(b []byte) (n int, err error) { - return c.pc.WriteTo(b, c.raddr) + return c.pc.WriteTo(b, c.clientAddr) } func (c *association) Close() error { @@ -375,19 +375,19 @@ func (c *association) LocalAddr() net.Addr { } func (c *association) RemoteAddr() net.Addr { - return c.raddr + return c.clientAddr } func (c *association) SetDeadline(t time.Time) error { - return c.pc.SetDeadline(t) + return errors.ErrUnsupported } func (c *association) SetReadDeadline(t time.Time) error { - return c.pc.SetReadDeadline(t) + return errors.ErrUnsupported } func (c *association) SetWriteDeadline(t time.Time) error { - return c.pc.SetWriteDeadline(t) + return errors.ErrUnsupported } func isDNS(addr net.Addr) bool { diff --git a/service/udp_test.go b/service/udp_test.go index 718f4601..e8ec7ff3 100644 --- a/service/udp_test.go +++ b/service/udp_test.go @@ -210,9 +210,9 @@ func startTestHandler() (AssociationHandler, func(target net.Addr, payload []byt func TestAssociationCloseWhileReading(t *testing.T) { assoc := &association{ - pc: makePacketConn(), - raddr: &clientAddr, - readCh: make(chan *packet), + pc: makePacketConn(), + clientAddr: &clientAddr, + readCh: make(chan *packet), } go func() { buf := make([]byte, 1024) From 07c1c41d219f104079fb890f069756f10c07dd95 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 24 Jan 2025 14:39:52 -0500 Subject: [PATCH 78/80] Rename `timeout` to `natTimeout` in test. --- internal/integration_test/integration_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/integration_test/integration_test.go b/internal/integration_test/integration_test.go index e300e1b5..3f95b00b 100644 --- a/internal/integration_test/integration_test.go +++ b/internal/integration_test/integration_test.go @@ -37,7 +37,7 @@ import ( const ( maxUDPPacketSize = 64 * 1024 - timeout = 5 * time.Minute + natTimeout = 5 * time.Minute ) func init() { @@ -324,7 +324,7 @@ func TestUDPEcho(t *testing.T) { } proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) - proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, timeout, 0)) + proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, natTimeout, 0)) natMetrics := &natTestMetrics{} associationMetrics := &fakeUDPAssociationMetrics{} go service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) { @@ -551,7 +551,7 @@ func BenchmarkUDPEcho(b *testing.B) { b.Fatal(err) } proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) - proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, timeout, 0)) + proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, natTimeout, 0)) done := make(chan struct{}) go func() { service.PacketServe(server, func(ctx context.Context, conn net.Conn) { @@ -597,7 +597,7 @@ func BenchmarkUDPManyKeys(b *testing.B) { b.Fatal(err) } proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{}) - proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, timeout, 0)) + proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, natTimeout, 0)) done := make(chan struct{}) go func() { service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) { From b077e558ba0f0e035a674d0633ead0d1ad4a3096 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 24 Jan 2025 14:40:23 -0500 Subject: [PATCH 79/80] Only resolve the `net.Addr` if it's not already a `UDPAddr`. --- service/udp.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/service/udp.go b/service/udp.go index 16426334..b494d30c 100644 --- a/service/udp.go +++ b/service/udp.go @@ -165,7 +165,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n connError := func() *onet.ConnectionError { var payload []byte - var tgtAddr net.Addr + var tgtAddr *net.UDPAddr if targetConn == nil { ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var textData []byte @@ -211,7 +211,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n } debugUDP(l, "Proxy exit.") - proxyTargetBytes, err = targetConn.WriteTo(payload, tgtAddr) + proxyTargetBytes, err = targetConn.WriteTo(payload, tgtAddr) // accept only `net.UDPAddr` despite the signature if err != nil { return ensureConnectionError(err, "ERR_WRITE", "Failed to write to target") } @@ -229,7 +229,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n // extractPayloadAndDestination processes a decrypted Shadowsocks UDP packet and // extracts the payload data and destination address. -func (h *associationHandler) extractPayloadAndDestination(textData []byte) ([]byte, net.Addr, *onet.ConnectionError) { +func (h *associationHandler) extractPayloadAndDestination(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { tgtAddr := socks.SplitAddr(textData) if tgtAddr == nil { return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) @@ -391,14 +391,22 @@ type validatingPacketConn struct { } func (vpc *validatingPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { - udpAddr, err := net.ResolveUDPAddr("udp", addr.String()) - if err != nil { - return 0, fmt.Errorf("failed to resolve target address %v", udpAddr) + var ( + udpAddr *net.UDPAddr + ok bool + ) + if udpAddr, ok = addr.(*net.UDPAddr); !ok { + var err error + udpAddr, err = net.ResolveUDPAddr("udp", addr.String()) + if err != nil { + return 0, fmt.Errorf("failed to resolve target address %v", addr) + } } if err := vpc.targetIPValidator(udpAddr.IP); err != nil { return 0, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address") } - return vpc.PacketConn.WriteTo(p, addr) + + return vpc.PacketConn.WriteTo(p, udpAddr) // accept only `net.UDPAddr` despite the signature } type timedPacketConn struct { From b866030fd831030e881fd34e8b42856b21551434 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 24 Jan 2025 16:33:18 -0500 Subject: [PATCH 80/80] Use `MakeNetAddr()` instead of resolving when extracting the addr. --- service/udp.go | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/service/udp.go b/service/udp.go index b494d30c..8f8b96f2 100644 --- a/service/udp.go +++ b/service/udp.go @@ -165,7 +165,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n connError := func() *onet.ConnectionError { var payload []byte - var tgtAddr *net.UDPAddr + var tgtAddr net.Addr if targetConn == nil { ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr() var textData []byte @@ -211,7 +211,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n } debugUDP(l, "Proxy exit.") - proxyTargetBytes, err = targetConn.WriteTo(payload, tgtAddr) // accept only `net.UDPAddr` despite the signature + proxyTargetBytes, err = targetConn.WriteTo(payload, tgtAddr) if err != nil { return ensureConnectionError(err, "ERR_WRITE", "Failed to write to target") } @@ -229,15 +229,15 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n // extractPayloadAndDestination processes a decrypted Shadowsocks UDP packet and // extracts the payload data and destination address. -func (h *associationHandler) extractPayloadAndDestination(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) { +func (h *associationHandler) extractPayloadAndDestination(textData []byte) ([]byte, net.Addr, *onet.ConnectionError) { tgtAddr := socks.SplitAddr(textData) if tgtAddr == nil { return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil) } - tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String()) + tgtUDPAddr, err := transport.MakeNetAddr("udp", tgtAddr.String()) if err != nil { - return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err) + return nil, nil, onet.NewConnectionError("ERR_CONVERT_ADDRESS", fmt.Sprintf("Failed to convert target address %v", tgtAddr), err) } payload := textData[len(tgtAddr):] @@ -391,16 +391,9 @@ type validatingPacketConn struct { } func (vpc *validatingPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { - var ( - udpAddr *net.UDPAddr - ok bool - ) - if udpAddr, ok = addr.(*net.UDPAddr); !ok { - var err error - udpAddr, err = net.ResolveUDPAddr("udp", addr.String()) - if err != nil { - return 0, fmt.Errorf("failed to resolve target address %v", addr) - } + udpAddr, err := net.ResolveUDPAddr("udp", addr.String()) + if err != nil { + return 0, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", udpAddr), err) } if err := vpc.targetIPValidator(udpAddr.IP); err != nil { return 0, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address")