Skip to content

Commit 7fae9e7

Browse files
committed
feat: support mieru inbound
1 parent ff76576 commit 7fae9e7

File tree

8 files changed

+541
-3
lines changed

8 files changed

+541
-3
lines changed

constant/metadata.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
TUIC
3939
HYSTERIA2
4040
ANYTLS
41+
MIERU
4142
INNER
4243
)
4344

@@ -109,6 +110,8 @@ func (t Type) String() string {
109110
return "Hysteria2"
110111
case ANYTLS:
111112
return "AnyTLS"
113+
case MIERU:
114+
return "Mieru"
112115
case INNER:
113116
return "Inner"
114117
default:
@@ -149,6 +152,8 @@ func ParseType(t string) (*Type, error) {
149152
res = HYSTERIA2
150153
case "ANYTLS":
151154
res = ANYTLS
155+
case "MIERU":
156+
res = MIERU
152157
case "INNER":
153158
res = INNER
154159
default:

docs/config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,15 @@ listeners:
15561556
# -----END ECH KEYS-----
15571557
# padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme
15581558

1559+
- name: mieru-in-1
1560+
type: mieru
1561+
port: 10818 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
1562+
listen: 0.0.0.0
1563+
transport: TCP # 支持 TCP 或者 UDP
1564+
users:
1565+
username1: password1
1566+
username2: password2
1567+
15591568
- name: trojan-in-1
15601569
type: trojan
15611570
port: 10819 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/coreos/go-iptables v0.8.0
88
github.com/dlclark/regexp2 v1.11.5
99
github.com/ebitengine/purego v0.9.0
10-
github.com/enfein/mieru/v3 v3.20.0
10+
github.com/enfein/mieru/v3 v3.22.0
1111
github.com/go-chi/chi/v5 v5.2.3
1212
github.com/go-chi/render v1.0.3
1313
github.com/gobwas/ws v1.4.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
2525
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
2626
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
2727
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
28-
github.com/enfein/mieru/v3 v3.20.0 h1:1ob7pCIVSH5FYFAfYvim8isLW1vBOS4cFOUF9exJS38=
29-
github.com/enfein/mieru/v3 v3.20.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
28+
github.com/enfein/mieru/v3 v3.22.0 h1:xXG5YzU5sk4PpmPiQSDOGKLfNwbUyogAQykTr6HIqKY=
29+
github.com/enfein/mieru/v3 v3.22.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
3030
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
3131
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
3232
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=

listener/inbound/mieru.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package inbound
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"sync"
8+
9+
"github.com/metacubex/mihomo/adapter/inbound"
10+
"github.com/metacubex/mihomo/common/utils"
11+
C "github.com/metacubex/mihomo/constant"
12+
"github.com/metacubex/mihomo/listener/mieru"
13+
"github.com/metacubex/mihomo/log"
14+
"google.golang.org/protobuf/proto"
15+
16+
mieruserver "github.com/enfein/mieru/v3/apis/server"
17+
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
18+
)
19+
20+
type Mieru struct {
21+
*Base
22+
option *MieruOption
23+
server mieruserver.Server
24+
mu sync.Mutex
25+
}
26+
27+
type MieruOption struct {
28+
BaseOption
29+
Transport string `inbound:"transport"`
30+
Users map[string]string `inbound:"users"`
31+
}
32+
33+
type mieruListenerFactory struct{}
34+
35+
func (mieruListenerFactory) Listen(ctx context.Context, network, address string) (net.Listener, error) {
36+
return inbound.ListenContext(ctx, network, address)
37+
}
38+
39+
func (mieruListenerFactory) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
40+
return inbound.ListenPacketContext(ctx, network, address)
41+
}
42+
43+
func NewMieru(option *MieruOption) (*Mieru, error) {
44+
base, err := NewBase(&option.BaseOption)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
config, err := buildMieruServerConfig(option, base.ports)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to build mieru server config: %w", err)
52+
}
53+
s := mieruserver.NewServer()
54+
if err := s.Store(config); err != nil {
55+
return nil, fmt.Errorf("failed to store mieru server config: %w", err)
56+
}
57+
// Server is started lazily when Listen() is called for the first time.
58+
return &Mieru{
59+
Base: base,
60+
option: option,
61+
server: s,
62+
}, nil
63+
}
64+
65+
func (m *Mieru) Config() C.InboundConfig {
66+
return m.option
67+
}
68+
69+
func (m *Mieru) Listen(tunnel C.Tunnel) error {
70+
m.mu.Lock()
71+
defer m.mu.Unlock()
72+
73+
if !m.server.IsRunning() {
74+
if err := m.server.Start(); err != nil {
75+
return fmt.Errorf("failed to start mieru server: %w", err)
76+
}
77+
}
78+
79+
additions := m.config.Additions()
80+
if len(additions) == 0 {
81+
additions = []inbound.Addition{
82+
inbound.WithInName("DEFAULT-MIERU"),
83+
inbound.WithSpecialRules(""),
84+
}
85+
}
86+
87+
go func() {
88+
for {
89+
c, req, err := m.server.Accept()
90+
if err != nil {
91+
if !m.server.IsRunning() {
92+
break
93+
}
94+
}
95+
go mieru.Handle(c, tunnel, req, additions...)
96+
}
97+
}()
98+
log.Infoln("Mieru[%s] proxy listening at: %s", m.Name(), m.Address())
99+
return nil
100+
}
101+
102+
func (m *Mieru) Close() error {
103+
m.mu.Lock()
104+
defer m.mu.Unlock()
105+
106+
if m.server.IsRunning() {
107+
return m.server.Stop()
108+
}
109+
110+
return nil
111+
}
112+
113+
var _ C.InboundListener = (*Mieru)(nil)
114+
115+
func (o MieruOption) Equal(config C.InboundConfig) bool {
116+
return optionToString(o) == optionToString(config)
117+
}
118+
119+
func buildMieruServerConfig(option *MieruOption, ports utils.IntRanges[uint16]) (*mieruserver.ServerConfig, error) {
120+
if err := validateMieruOption(option); err != nil {
121+
return nil, fmt.Errorf("failed to validate mieru option: %w", err)
122+
}
123+
if len(ports) == 0 {
124+
return nil, fmt.Errorf("port is not set")
125+
}
126+
127+
var transportProtocol *mierupb.TransportProtocol
128+
switch option.Transport {
129+
case "TCP":
130+
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
131+
case "UDP":
132+
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
133+
}
134+
var portBindings []*mierupb.PortBinding
135+
for _, portRange := range ports {
136+
if portRange.Start() == portRange.End() {
137+
portBindings = append(portBindings, &mierupb.PortBinding{
138+
Port: proto.Int32(int32(portRange.Start())),
139+
Protocol: transportProtocol,
140+
})
141+
} else {
142+
portBindings = append(portBindings, &mierupb.PortBinding{
143+
PortRange: proto.String(fmt.Sprintf("%d-%d", portRange.Start(), portRange.End())),
144+
Protocol: transportProtocol,
145+
})
146+
}
147+
}
148+
var users []*mierupb.User
149+
for username, password := range option.Users {
150+
users = append(users, &mierupb.User{
151+
Name: proto.String(username),
152+
Password: proto.String(password),
153+
})
154+
}
155+
return &mieruserver.ServerConfig{
156+
Config: &mierupb.ServerConfig{
157+
PortBindings: portBindings,
158+
Users: users,
159+
},
160+
StreamListenerFactory: mieruListenerFactory{},
161+
PacketListenerFactory: mieruListenerFactory{},
162+
}, nil
163+
}
164+
165+
func validateMieruOption(option *MieruOption) error {
166+
if option.Transport != "TCP" && option.Transport != "UDP" {
167+
return fmt.Errorf("transport must be TCP or UDP")
168+
}
169+
if len(option.Users) == 0 {
170+
return fmt.Errorf("users is empty")
171+
}
172+
for username, password := range option.Users {
173+
if username == "" {
174+
return fmt.Errorf("username is empty")
175+
}
176+
if password == "" {
177+
return fmt.Errorf("password is empty")
178+
}
179+
}
180+
return nil
181+
}

0 commit comments

Comments
 (0)