Skip to content

Commit 5aa140c

Browse files
authored
feat: support mieru UDP outbound (#2384)
1 parent c107c6a commit 5aa140c

7 files changed

Lines changed: 157 additions & 58 deletions

File tree

adapter/outbound/mieru.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net"
7+
"net/netip"
78
"strconv"
89
"sync"
910

@@ -40,6 +41,20 @@ type MieruOption struct {
4041
HandshakeMode string `proxy:"handshake-mode,omitempty"`
4142
}
4243

44+
type mieruPacketDialer struct {
45+
C.Dialer
46+
}
47+
48+
var _ mierucommon.PacketDialer = (*mieruPacketDialer)(nil)
49+
50+
func (pd mieruPacketDialer) ListenPacket(ctx context.Context, network, laddr, raddr string) (net.PacketConn, error) {
51+
rAddrPort, err := netip.ParseAddrPort(raddr)
52+
if err != nil {
53+
return nil, fmt.Errorf("invalid address %s: %w", raddr, err)
54+
}
55+
return pd.Dialer.ListenPacket(ctx, network, laddr, rAddrPort)
56+
}
57+
4358
// DialContext implements C.ProxyAdapter
4459
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
4560
if err := m.ensureClientIsRunning(); err != nil {
@@ -102,6 +117,7 @@ func (m *Mieru) ensureClientIsRunning() error {
102117
return err
103118
}
104119
config.Dialer = dialer
120+
config.PacketDialer = mieruPacketDialer{Dialer: dialer}
105121
if err := m.client.Store(config); err != nil {
106122
return err
107123
}
@@ -158,31 +174,35 @@ func (m *Mieru) Close() error {
158174
}
159175

160176
func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec {
177+
spec := mierumodel.NetAddrSpec{
178+
Net: metadata.NetWork.String(),
179+
}
161180
if metadata.Host != "" {
162-
return mierumodel.NetAddrSpec{
163-
AddrSpec: mierumodel.AddrSpec{
164-
FQDN: metadata.Host,
165-
Port: int(metadata.DstPort),
166-
},
167-
Net: "tcp",
181+
spec.AddrSpec = mierumodel.AddrSpec{
182+
FQDN: metadata.Host,
183+
Port: int(metadata.DstPort),
168184
}
169185
} else {
170-
return mierumodel.NetAddrSpec{
171-
AddrSpec: mierumodel.AddrSpec{
172-
IP: metadata.DstIP.AsSlice(),
173-
Port: int(metadata.DstPort),
174-
},
175-
Net: "tcp",
186+
spec.AddrSpec = mierumodel.AddrSpec{
187+
IP: metadata.DstIP.AsSlice(),
188+
Port: int(metadata.DstPort),
176189
}
177190
}
191+
return spec
178192
}
179193

180194
func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) {
181195
if err := validateMieruOption(option); err != nil {
182196
return nil, fmt.Errorf("failed to validate mieru option: %w", err)
183197
}
184198

185-
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
199+
var transportProtocol = mierupb.TransportProtocol_UNKNOWN_TRANSPORT_PROTOCOL.Enum()
200+
switch option.Transport {
201+
case "TCP":
202+
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
203+
case "UDP":
204+
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
205+
}
186206
var server *mierupb.ServerEndpoint
187207
if net.ParseIP(option.Server) != nil {
188208
// server is an IP address
@@ -284,8 +304,8 @@ func validateMieruOption(option MieruOption) error {
284304
}
285305
}
286306

287-
if option.Transport != "TCP" {
288-
return fmt.Errorf("transport must be TCP")
307+
if option.Transport != "TCP" && option.Transport != "UDP" {
308+
return fmt.Errorf("transport must be TCP or UDP")
289309
}
290310
if option.UserName == "" {
291311
return fmt.Errorf("username is empty")

adapter/outbound/mieru_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestNewMieru(t *testing.T) {
3434
Name: "test",
3535
Server: "example.com",
3636
Port: 10003,
37-
Transport: "TCP",
37+
Transport: "UDP",
3838
UserName: "test",
3939
Password: "test",
4040
},

docs/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1027,7 +1027,7 @@ proxies: # socks5
10271027
server: 1.2.3.4
10281028
port: 2999
10291029
# port-range: 2090-2099 #(不可同时填写 port 和 port-range)
1030-
transport: TCP # 只支持 TCP
1030+
transport: TCP # 支持 TCP 或者 UDP
10311031
udp: true # 支持 UDP over TCP
10321032
username: user
10331033
password: password

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/bahlo/generic-list-go v0.2.0
77
github.com/coreos/go-iptables v0.8.0
88
github.com/dlclark/regexp2 v1.11.5
9-
github.com/enfein/mieru/v3 v3.22.1
9+
github.com/enfein/mieru/v3 v3.23.0
1010
github.com/go-chi/chi/v5 v5.2.3
1111
github.com/go-chi/render v1.0.3
1212
github.com/gobwas/ws v1.4.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
2323
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2424
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
2525
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
26-
github.com/enfein/mieru/v3 v3.22.1 h1:/XGYYXpEhEJlxosmtbpEJkhtRLHB8IToG7LB8kU2ZDY=
27-
github.com/enfein/mieru/v3 v3.22.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
26+
github.com/enfein/mieru/v3 v3.23.0 h1:f/dd3UAoi36FD9DZ9x49t6Ps0oHeSjrVSgWzvEstn0E=
27+
github.com/enfein/mieru/v3 v3.23.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
2828
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
2929
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
3030
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=

listener/inbound/common_test.go

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,13 @@ func init() {
5959
}
6060

6161
type TestTunnel struct {
62-
HandleTCPConnFn func(conn net.Conn, metadata *C.Metadata)
63-
HandleUDPPacketFn func(packet C.UDPPacket, metadata *C.Metadata)
64-
NatTableFn func() C.NatTable
65-
CloseFn func() error
66-
DoTestFn func(t *testing.T, proxy C.ProxyAdapter)
62+
HandleTCPConnFn func(conn net.Conn, metadata *C.Metadata)
63+
HandleUDPPacketFn func(packet C.UDPPacket, metadata *C.Metadata)
64+
NatTableFn func() C.NatTable
65+
CloseFn func() error
66+
DoTestFn func(t *testing.T, proxy C.ProxyAdapter)
67+
DoSequentialTestFn func(t *testing.T, proxy C.ProxyAdapter)
68+
DoConcurrentTestFn func(t *testing.T, proxy C.ProxyAdapter)
6769
}
6870

6971
func (tt *TestTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) {
@@ -86,6 +88,14 @@ func (tt *TestTunnel) DoTest(t *testing.T, proxy C.ProxyAdapter) {
8688
tt.DoTestFn(t, proxy)
8789
}
8890

91+
func (tt *TestTunnel) DoSequentialTest(t *testing.T, proxy C.ProxyAdapter) {
92+
tt.DoSequentialTestFn(t, proxy)
93+
}
94+
95+
func (tt *TestTunnel) DoConcurrentTest(t *testing.T, proxy C.ProxyAdapter) {
96+
tt.DoConcurrentTestFn(t, proxy)
97+
}
98+
8999
type TestTunnelListener struct {
90100
ch chan net.Conn
91101
ctx context.Context
@@ -213,6 +223,40 @@ func NewHttpTestTunnel() *TestTunnel {
213223
}
214224
assert.Equal(t, httpData[:size], data)
215225
}
226+
227+
sequentialTestFn := func(t *testing.T, proxy C.ProxyAdapter) {
228+
// Sequential testing for debugging
229+
t.Run("Sequential", func(t *testing.T) {
230+
testFn(t, proxy, "http", len(httpData))
231+
testFn(t, proxy, "https", len(httpData))
232+
})
233+
}
234+
235+
concurrentTestFn := func(t *testing.T, proxy C.ProxyAdapter) {
236+
// Concurrent testing to detect stress
237+
t.Run("Concurrent", func(t *testing.T) {
238+
wg := sync.WaitGroup{}
239+
num := len(httpData) / 1024
240+
for i := 1; i <= num; i++ {
241+
i := i
242+
wg.Add(1)
243+
go func() {
244+
testFn(t, proxy, "https", i*1024)
245+
defer wg.Done()
246+
}()
247+
}
248+
for i := 1; i <= num; i++ {
249+
i := i
250+
wg.Add(1)
251+
go func() {
252+
testFn(t, proxy, "http", i*1024)
253+
defer wg.Done()
254+
}()
255+
}
256+
wg.Wait()
257+
})
258+
}
259+
216260
tunnel := &TestTunnel{
217261
HandleTCPConnFn: func(conn net.Conn, metadata *C.Metadata) {
218262
defer conn.Close()
@@ -252,36 +296,11 @@ func NewHttpTestTunnel() *TestTunnel {
252296
},
253297
CloseFn: ln.Close,
254298
DoTestFn: func(t *testing.T, proxy C.ProxyAdapter) {
255-
256-
// Sequential testing for debugging
257-
t.Run("Sequential", func(t *testing.T) {
258-
testFn(t, proxy, "http", len(httpData))
259-
testFn(t, proxy, "https", len(httpData))
260-
})
261-
262-
// Concurrent testing to detect stress
263-
t.Run("Concurrent", func(t *testing.T) {
264-
wg := sync.WaitGroup{}
265-
num := len(httpData) / 1024
266-
for i := 1; i <= num; i++ {
267-
i := i
268-
wg.Add(1)
269-
go func() {
270-
testFn(t, proxy, "https", i*1024)
271-
defer wg.Done()
272-
}()
273-
}
274-
for i := 1; i <= num; i++ {
275-
i := i
276-
wg.Add(1)
277-
go func() {
278-
testFn(t, proxy, "http", i*1024)
279-
defer wg.Done()
280-
}()
281-
}
282-
wg.Wait()
283-
})
299+
sequentialTestFn(t, proxy)
300+
concurrentTestFn(t, proxy)
284301
},
302+
DoSequentialTestFn: sequentialTestFn,
303+
DoConcurrentTestFn: concurrentTestFn,
285304
}
286305
return tunnel
287306
}

listener/inbound/mieru_test.go

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,18 @@ func TestNewMieru(t *testing.T) {
149149
}
150150

151151
func TestInboundMieru(t *testing.T) {
152-
t.Run("HANDSHAKE_STANDARD", func(t *testing.T) {
152+
t.Run("TCP_HANDSHAKE_STANDARD", func(t *testing.T) {
153153
testInboundMieruTCP(t, "HANDSHAKE_STANDARD")
154154
})
155-
t.Run("HANDSHAKE_NO_WAIT", func(t *testing.T) {
155+
t.Run("TCP_HANDSHAKE_NO_WAIT", func(t *testing.T) {
156156
testInboundMieruTCP(t, "HANDSHAKE_NO_WAIT")
157157
})
158+
t.Run("UDP_HANDSHAKE_STANDARD", func(t *testing.T) {
159+
testInboundMieruUDP(t, "HANDSHAKE_STANDARD")
160+
})
161+
t.Run("UDP_HANDSHAKE_NO_WAIT", func(t *testing.T) {
162+
testInboundMieruUDP(t, "HANDSHAKE_NO_WAIT")
163+
})
158164
}
159165

160166
func testInboundMieruTCP(t *testing.T, handshakeMode string) {
@@ -168,7 +174,7 @@ func testInboundMieruTCP(t *testing.T, handshakeMode string) {
168174

169175
inboundOptions := inbound.MieruOption{
170176
BaseOption: inbound.BaseOption{
171-
NameStr: "mieru_inbound",
177+
NameStr: "mieru_inbound_tcp",
172178
Listen: "127.0.0.1",
173179
Port: strconv.Itoa(port),
174180
},
@@ -194,7 +200,7 @@ func testInboundMieruTCP(t *testing.T, handshakeMode string) {
194200
return
195201
}
196202
outboundOptions := outbound.MieruOption{
197-
Name: "mieru_outbound",
203+
Name: "mieru_outbound_tcp",
198204
Server: addrPort.Addr().String(),
199205
Port: int(addrPort.Port()),
200206
Transport: "TCP",
@@ -210,3 +216,57 @@ func testInboundMieruTCP(t *testing.T, handshakeMode string) {
210216

211217
tunnel.DoTest(t, out)
212218
}
219+
220+
func testInboundMieruUDP(t *testing.T, handshakeMode string) {
221+
t.Parallel()
222+
l, err := net.ListenPacket("udp", "127.0.0.1:0")
223+
if !assert.NoError(t, err) {
224+
return
225+
}
226+
port := l.LocalAddr().(*net.UDPAddr).Port
227+
l.Close()
228+
229+
inboundOptions := inbound.MieruOption{
230+
BaseOption: inbound.BaseOption{
231+
NameStr: "mieru_inbound_udp",
232+
Listen: "127.0.0.1",
233+
Port: strconv.Itoa(port),
234+
},
235+
Transport: "UDP",
236+
Users: map[string]string{"test": "password"},
237+
}
238+
in, err := inbound.NewMieru(&inboundOptions)
239+
if !assert.NoError(t, err) {
240+
return
241+
}
242+
243+
tunnel := NewHttpTestTunnel()
244+
defer tunnel.Close()
245+
246+
err = in.Listen(tunnel)
247+
if !assert.NoError(t, err) {
248+
return
249+
}
250+
defer in.Close()
251+
252+
addrPort, err := netip.ParseAddrPort(in.Address())
253+
if !assert.NoError(t, err) {
254+
return
255+
}
256+
outboundOptions := outbound.MieruOption{
257+
Name: "mieru_outbound_udp",
258+
Server: addrPort.Addr().String(),
259+
Port: int(addrPort.Port()),
260+
Transport: "UDP",
261+
UserName: "test",
262+
Password: "password",
263+
HandshakeMode: handshakeMode,
264+
}
265+
out, err := outbound.NewMieru(outboundOptions)
266+
if !assert.NoError(t, err) {
267+
return
268+
}
269+
defer out.Close()
270+
271+
tunnel.DoSequentialTest(t, out)
272+
}

0 commit comments

Comments
 (0)