|
|
@@ -0,0 +1,497 @@
|
|
|
+// Copyright 2018 Jigsaw Operations LLC
|
|
|
+//
|
|
|
+// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+// you may not use this file except in compliance with the License.
|
|
|
+// You may obtain a copy of the License at
|
|
|
+//
|
|
|
+// https://www.apache.org/licenses/LICENSE-2.0
|
|
|
+//
|
|
|
+// Unless required by applicable law or agreed to in writing, software
|
|
|
+// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+// See the License for the specific language governing permissions and
|
|
|
+// limitations under the License.
|
|
|
+
|
|
|
+package service
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "log/slog"
|
|
|
+ "net"
|
|
|
+ "net/netip"
|
|
|
+ "runtime/debug"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/Jigsaw-Code/outline-sdk/transport"
|
|
|
+ "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
|
|
|
+ "github.com/shadowsocks/go-shadowsocks2/socks"
|
|
|
+
|
|
|
+ onet "github.com/Jigsaw-Code/outline-ss-server/net"
|
|
|
+)
|
|
|
+
|
|
|
+// UDPConnMetrics is used to report metrics on UDP connections.
|
|
|
+type UDPConnMetrics interface {
|
|
|
+ AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64)
|
|
|
+ AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64)
|
|
|
+ RemoveNatEntry()
|
|
|
+}
|
|
|
+
|
|
|
+type UDPMetrics interface {
|
|
|
+ AddUDPNatEntry(clientAddr net.Addr, accessKey string) UDPConnMetrics
|
|
|
+}
|
|
|
+
|
|
|
+// Max UDP buffer size for the server code.
|
|
|
+const serverUDPBufferSize = 64 * 1024
|
|
|
+
|
|
|
+// 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
|
|
|
+ // 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)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Decrypts src into dst. It tries each cipher until it finds one that authenticates
|
|
|
+// correctly. dst and src must not overlap.
|
|
|
+func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherList, l *slog.Logger) ([]byte, string, *shadowsocks.EncryptionKey, error) {
|
|
|
+ // Try each cipher until we find one that authenticates successfully. This assumes that all ciphers are AEAD.
|
|
|
+ // We snapshot the list because it may be modified while we use it.
|
|
|
+ snapshot := cipherList.SnapshotForClientIP(clientIP)
|
|
|
+ for ci, entry := range snapshot {
|
|
|
+ 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))
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ debugUDP(l, "Found cipher.", 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
|
|
|
+ }
|
|
|
+ return nil, "", nil, errors.New("could not find valid UDP cipher")
|
|
|
+}
|
|
|
+
|
|
|
+type packetHandler struct {
|
|
|
+ logger *slog.Logger
|
|
|
+ natTimeout time.Duration
|
|
|
+ ciphers CipherList
|
|
|
+ m UDPMetrics
|
|
|
+ ssm ShadowsocksConnMetrics
|
|
|
+ targetIPValidator onet.TargetIPValidator
|
|
|
+ targetListener transport.PacketListener
|
|
|
+}
|
|
|
+
|
|
|
+// NewPacketHandler creates a PacketHandler
|
|
|
+func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetrics, ssMetrics ShadowsocksConnMetrics) PacketHandler {
|
|
|
+ if m == nil {
|
|
|
+ m = &NoOpUDPMetrics{}
|
|
|
+ }
|
|
|
+ if ssMetrics == nil {
|
|
|
+ ssMetrics = &NoOpShadowsocksConnMetrics{}
|
|
|
+ }
|
|
|
+ return &packetHandler{
|
|
|
+ logger: noopLogger(),
|
|
|
+ natTimeout: natTimeout,
|
|
|
+ ciphers: cipherList,
|
|
|
+ m: m,
|
|
|
+ ssm: ssMetrics,
|
|
|
+ targetIPValidator: onet.RequirePublicIP,
|
|
|
+ targetListener: MakeTargetUDPListener(0),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// PacketHandler is a running UDP shadowsocks proxy that can be stopped.
|
|
|
+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)
|
|
|
+ // SetTargetPacketListener sets the packet listener to use for target connections.
|
|
|
+ SetTargetPacketListener(targetListener transport.PacketListener)
|
|
|
+ // Handle returns after clientConn closes and all the sub goroutines return.
|
|
|
+ Handle(clientConn net.PacketConn)
|
|
|
+}
|
|
|
+
|
|
|
+func (h *packetHandler) SetLogger(l *slog.Logger) {
|
|
|
+ if l == nil {
|
|
|
+ l = noopLogger()
|
|
|
+ }
|
|
|
+ h.logger = l
|
|
|
+}
|
|
|
+
|
|
|
+func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) {
|
|
|
+ h.targetIPValidator = targetIPValidator
|
|
|
+}
|
|
|
+
|
|
|
+func (h *packetHandler) SetTargetPacketListener(targetListener transport.PacketListener) {
|
|
|
+ h.targetListener = targetListener
|
|
|
+}
|
|
|
+
|
|
|
+// 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)
|
|
|
+
|
|
|
+ for {
|
|
|
+ clientProxyBytes, clientAddr, err := clientConn.ReadFrom(cipherBuf)
|
|
|
+ if errors.Is(err, net.ErrClosed) {
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ var proxyTargetBytes int
|
|
|
+ var targetConn *natconn
|
|
|
+
|
|
|
+ connError := func() (connError *onet.ConnectionError) {
|
|
|
+ defer func() {
|
|
|
+ if r := recover(); r != nil {
|
|
|
+ slog.Error("Panic in UDP loop: %v. Continuing to listen.", r)
|
|
|
+ debug.PrintStack()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ // Error from ReadFrom
|
|
|
+ if err != nil {
|
|
|
+ return onet.NewConnectionError("ERR_READ", "Failed to read from client", 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 := h.targetListener.ListenPacket(context.Background())
|
|
|
+ if err != nil {
|
|
|
+ return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create a `PacketConn`", 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
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 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(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 *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)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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"
|
|
|
+}
|
|
|
+
|
|
|
+type natconn struct {
|
|
|
+ net.PacketConn
|
|
|
+ cryptoKey *shadowsocks.EncryptionKey
|
|
|
+ metrics UDPConnMetrics
|
|
|
+ // NAT timeout to apply for non-DNS packets.
|
|
|
+ defaultTimeout time.Duration
|
|
|
+ // Current read deadline of PacketConn. Used to avoid decreasing the
|
|
|
+ // deadline. Initially zero.
|
|
|
+ readDeadline time.Time
|
|
|
+ // If the connection has only sent one DNS query, it will close
|
|
|
+ // if it receives a DNS response.
|
|
|
+ fastClose sync.Once
|
|
|
+}
|
|
|
+
|
|
|
+func (c *natconn) 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)
|
|
|
+ isFirstWrite := c.readDeadline.IsZero()
|
|
|
+ if !isDNS || !isFirstWrite {
|
|
|
+ // Disable fast close. (Idempotent.)
|
|
|
+ c.fastClose.Do(func() {})
|
|
|
+ }
|
|
|
+
|
|
|
+ timeout := c.defaultTimeout
|
|
|
+ if isDNS {
|
|
|
+ // Shorten timeout as required by RFC 5452 Section 10.
|
|
|
+ timeout = 17 * time.Second
|
|
|
+ }
|
|
|
+
|
|
|
+ newDeadline := time.Now().Add(timeout)
|
|
|
+ if newDeadline.After(c.readDeadline) {
|
|
|
+ c.readDeadline = newDeadline
|
|
|
+ c.SetReadDeadline(newDeadline)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (c *natconn) onRead(addr net.Addr) {
|
|
|
+ c.fastClose.Do(func() {
|
|
|
+ if isDNS(addr) {
|
|
|
+ // The next ReadFrom() should time out immediately.
|
|
|
+ c.SetReadDeadline(time.Now())
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func (c *natconn) 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) {
|
|
|
+ n, addr, err := c.PacketConn.ReadFrom(buf)
|
|
|
+ if err == nil {
|
|
|
+ c.onRead(addr)
|
|
|
+ }
|
|
|
+ return n, addr, err
|
|
|
+}
|
|
|
+
|
|
|
+// Packet NAT table
|
|
|
+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}
|
|
|
+ m.keyConn = make(map[string]*natconn)
|
|
|
+ m.timeout = timeout
|
|
|
+ return m
|
|
|
+}
|
|
|
+
|
|
|
+func (m *natmap) Get(key string) *natconn {
|
|
|
+ m.RLock()
|
|
|
+ defer m.RUnlock()
|
|
|
+ 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,
|
|
|
+ }
|
|
|
+
|
|
|
+ m.Lock()
|
|
|
+ defer m.Unlock()
|
|
|
+
|
|
|
+ m.keyConn[key] = entry
|
|
|
+ return entry
|
|
|
+}
|
|
|
+
|
|
|
+func (m *natmap) del(key string) net.PacketConn {
|
|
|
+ m.Lock()
|
|
|
+ defer m.Unlock()
|
|
|
+
|
|
|
+ entry, ok := m.keyConn[key]
|
|
|
+ if ok {
|
|
|
+ delete(m.keyConn, key)
|
|
|
+ return entry
|
|
|
+ }
|
|
|
+ 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)
|
|
|
+
|
|
|
+ go func() {
|
|
|
+ timedCopy(clientAddr, clientConn, entry, m.logger)
|
|
|
+ connMetrics.RemoveNatEntry()
|
|
|
+ if pc := m.del(clientAddr.String()); pc != nil {
|
|
|
+ pc.Close()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ return entry
|
|
|
+}
|
|
|
+
|
|
|
+func (m *natmap) Close() error {
|
|
|
+ m.Lock()
|
|
|
+ defer m.Unlock()
|
|
|
+
|
|
|
+ var err error
|
|
|
+ now := time.Now()
|
|
|
+ for _, pc := range m.keyConn {
|
|
|
+ if e := pc.SetReadDeadline(now); e != nil {
|
|
|
+ err = e
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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"))
|
|
|
+
|
|
|
+// copy from target to client until read timeout
|
|
|
+func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, 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)
|
|
|
+
|
|
|
+ saltSize := targetConn.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 = 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)
|
|
|
+ }
|
|
|
+
|
|
|
+ debugUDPAddr(l, "Got response.", clientAddr, 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.WriteTo(buf, clientAddr)
|
|
|
+ if err != nil {
|
|
|
+ return onet.NewConnectionError("ERR_WRITE", "Failed to write to 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
|
|
|
+ }
|
|
|
+ if expired {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ targetConn.metrics.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// NoOpUDPConnMetrics is a [UDPConnMetrics] that doesn't do anything. Useful in tests
|
|
|
+// or if you don't want to track metrics.
|
|
|
+type NoOpUDPConnMetrics struct{}
|
|
|
+
|
|
|
+var _ UDPConnMetrics = (*NoOpUDPConnMetrics)(nil)
|
|
|
+
|
|
|
+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{}
|
|
|
+}
|