From d151b6b9491f4b6ecb91ef57f57e3fb9d9fe2f46 Mon Sep 17 00:00:00 2001 From: Morax Date: Fri, 7 Aug 2026 09:17:34 +0200 Subject: [PATCH 1/2] fix: isolate tunnel UDP sessions by listener --- listener/tunnel/packet.go | 3 +- listener/tunnel/udp.go | 4 ++ listener/tunnel/udp_test.go | 82 +++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 listener/tunnel/udp_test.go diff --git a/listener/tunnel/packet.go b/listener/tunnel/packet.go index 165004d6e4..43de4c04f5 100644 --- a/listener/tunnel/packet.go +++ b/listener/tunnel/packet.go @@ -9,6 +9,7 @@ import ( type packet struct { pc net.PacketConn rAddr net.Addr + keyAddr net.Addr payload []byte } @@ -23,7 +24,7 @@ func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { - return c.rAddr + return c.keyAddr } func (c *packet) Drop() { diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go index 1f14a1e926..e193379480 100644 --- a/listener/tunnel/udp.go +++ b/listener/tunnel/udp.go @@ -6,6 +6,7 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" @@ -76,9 +77,12 @@ func NewUDP(addr, target, proxy string, lc C.InboundListenConfig, tunnel C.Tunne } func (l *PacketConn) handleUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, addr net.Addr, additions ...inbound.Addition) { + // Keep associations from different tunnel listeners separate for the same source address. + sessionKey := fmt.Sprintf("%s:%s", pc.LocalAddr(), addr) cPacket := &packet{ pc: pc, rAddr: addr, + keyAddr: N.NewCustomAddr(C.TUNNEL.String(), sessionKey, addr), payload: buf, } diff --git a/listener/tunnel/udp_test.go b/listener/tunnel/udp_test.go new file mode 100644 index 0000000000..d0496b5315 --- /dev/null +++ b/listener/tunnel/udp_test.go @@ -0,0 +1,82 @@ +package tunnel + +import ( + "net" + "testing" + "time" + + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" +) + +func TestHandleUDPUsesListenerAddressInSessionKey(t *testing.T) { + source := &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 51823} + packetConnA := &testPacketConn{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 51820}} + packetConnB := &testPacketConn{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 51821}} + capture := &testTunnel{} + + listenerA := &PacketConn{target: socks5.ParseAddr("198.51.100.20:51820")} + listenerB := &PacketConn{target: socks5.ParseAddr("198.51.100.21:51821")} + listenerA.handleUDP(packetConnA, capture, []byte("a"), source) + listenerB.handleUDP(packetConnB, capture, []byte("b"), source) + listenerA.handleUDP(packetConnA, capture, []byte("c"), source) + + if len(capture.packets) != 3 { + t.Fatalf("captured %d packets, want 3", len(capture.packets)) + } + if capture.packets[0].Key() == capture.packets[1].Key() { + t.Fatalf("session keys collide across listeners: %q", capture.packets[0].Key()) + } + if capture.packets[0].Key() != capture.packets[2].Key() { + t.Errorf("session key changed within one listener: %q != %q", capture.packets[0].Key(), capture.packets[2].Key()) + } + + for i, packet := range capture.packets { + if got := packet.Metadata().SourceAddress(); got != source.String() { + t.Errorf("packet %d source = %q, want %q", i, got, source) + } + } + if got := capture.packets[0].Metadata().InPort; got != 51820 { + t.Errorf("first packet inbound port = %d, want 51820", got) + } + if got := capture.packets[1].Metadata().InPort; got != 51821 { + t.Errorf("second packet inbound port = %d, want 51821", got) + } + + if _, err := capture.packets[0].WriteBack([]byte("response"), nil); err != nil { + t.Fatalf("write back: %v", err) + } + if got := packetConnA.writeAddr.String(); got != source.String() { + t.Errorf("write-back address = %q, want %q", got, source) + } +} + +type testTunnel struct { + packets []C.PacketAdapter +} + +func (*testTunnel) HandleTCPConn(net.Conn, *C.Metadata) {} + +func (t *testTunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) { + t.packets = append(t.packets, C.NewPacketAdapter(packet, metadata)) +} + +func (*testTunnel) NatTable() C.NatTable { return nil } + +type testPacketConn struct { + localAddr net.Addr + writeAddr net.Addr +} + +func (*testPacketConn) ReadFrom([]byte) (int, net.Addr, error) { return 0, nil, net.ErrClosed } + +func (c *testPacketConn) WriteTo(payload []byte, addr net.Addr) (int, error) { + c.writeAddr = addr + return len(payload), nil +} + +func (*testPacketConn) Close() error { return nil } +func (c *testPacketConn) LocalAddr() net.Addr { return c.localAddr } +func (*testPacketConn) SetDeadline(time.Time) error { return nil } +func (*testPacketConn) SetReadDeadline(time.Time) error { return nil } +func (*testPacketConn) SetWriteDeadline(time.Time) error { return nil } From 31c3ff8ace5c1ef7ec4dbb4b8d2d44ab7ad0f7e9 Mon Sep 17 00:00:00 2001 From: Morax Date: Sun, 9 Aug 2026 12:08:41 +0200 Subject: [PATCH 2/2] fix: clarify tunnel UDP session key Signed-off-by: Morax --- listener/tunnel/packet.go | 2 +- listener/tunnel/udp.go | 2 +- listener/tunnel/udp_test.go | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/listener/tunnel/packet.go b/listener/tunnel/packet.go index 43de4c04f5..b6ba43fbaf 100644 --- a/listener/tunnel/packet.go +++ b/listener/tunnel/packet.go @@ -22,7 +22,7 @@ func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { return c.pc.WriteTo(b, c.rAddr) } -// LocalAddr returns the source IP/Port of UDP Packet +// LocalAddr returns the listener-scoped address used as the packet's SNAT key. func (c *packet) LocalAddr() net.Addr { return c.keyAddr } diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go index e193379480..425adaa9e6 100644 --- a/listener/tunnel/udp.go +++ b/listener/tunnel/udp.go @@ -78,7 +78,7 @@ func NewUDP(addr, target, proxy string, lc C.InboundListenConfig, tunnel C.Tunne func (l *PacketConn) handleUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, addr net.Addr, additions ...inbound.Addition) { // Keep associations from different tunnel listeners separate for the same source address. - sessionKey := fmt.Sprintf("%s:%s", pc.LocalAddr(), addr) + sessionKey := fmt.Sprintf("%s|%s", pc.LocalAddr().String(), addr.String()) cPacket := &packet{ pc: pc, rAddr: addr, diff --git a/listener/tunnel/udp_test.go b/listener/tunnel/udp_test.go index d0496b5315..29c133f3df 100644 --- a/listener/tunnel/udp_test.go +++ b/listener/tunnel/udp_test.go @@ -30,6 +30,9 @@ func TestHandleUDPUsesListenerAddressInSessionKey(t *testing.T) { if capture.packets[0].Key() != capture.packets[2].Key() { t.Errorf("session key changed within one listener: %q != %q", capture.packets[0].Key(), capture.packets[2].Key()) } + if got, want := capture.packets[0].Key(), "127.0.0.1:51820|192.0.2.1:51823"; got != want { + t.Errorf("session key = %q, want %q", got, want) + } for i, packet := range capture.packets { if got := packet.Metadata().SourceAddress(); got != source.String() {