chore: consolidate mieru port configuration (#2277)

Allow multiple port numbers and port ranges in "port" configuration.
The "port-range" configuration is deprecated but still functional.
This commit is contained in:
enfein 2025-09-23 00:19:12 +00:00 committed by GitHub
parent 8a9300d44e
commit 1b1f95aa9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 315 additions and 80 deletions

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"strings"
"sync" "sync"
CN "github.com/metacubex/mihomo/common/net" CN "github.com/metacubex/mihomo/common/net"
@ -30,8 +31,8 @@ type MieruOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port,omitempty"` Port string `proxy:"port,omitempty"`
PortRange string `proxy:"port-range,omitempty"` PortRange string `proxy:"port-range,omitempty"` // deprecated
Transport string `proxy:"transport"` Transport string `proxy:"transport"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
UserName string `proxy:"username"` UserName string `proxy:"username"`
@ -123,13 +124,19 @@ func NewMieru(option MieruOption) (*Mieru, error) {
} }
// Client is started lazily on the first use. // Client is started lazily on the first use.
// Use the first port to construct the address.
var addr string var addr string
if option.Port != 0 { var portStr string
addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) if option.Port != "" {
portStr = option.Port
} else { } else {
beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange) portStr = option.PortRange
addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort))
} }
firstPort, err := getFirstPort(portStr)
if err != nil {
return nil, fmt.Errorf("failed to get first port from port string %q: %w", portStr, err)
}
addr = net.JoinHostPort(option.Server, strconv.Itoa(firstPort))
outbound := &Mieru{ outbound := &Mieru{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
@ -183,54 +190,62 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro
} }
transportProtocol := mierupb.TransportProtocol_TCP.Enum() transportProtocol := mierupb.TransportProtocol_TCP.Enum()
var server *mierupb.ServerEndpoint
if net.ParseIP(option.Server) != nil { portBindings := make([]*mierupb.PortBinding, 0)
// server is an IP address if option.Port != "" {
if option.PortRange != "" { parts := strings.Split(option.Port, ",")
server = &mierupb.ServerEndpoint{ for _, part := range parts {
IpAddress: proto.String(option.Server), part = strings.TrimSpace(part)
PortBindings: []*mierupb.PortBinding{ if strings.Contains(part, "-") {
{ _, _, err := beginAndEndPortFromPortRange(part)
PortRange: proto.String(option.PortRange), if err == nil {
portBindings = append(portBindings, &mierupb.PortBinding{
PortRange: proto.String(part),
Protocol: transportProtocol, Protocol: transportProtocol,
}, })
}, } else {
} return nil, err
} else { }
server = &mierupb.ServerEndpoint{ } else {
IpAddress: proto.String(option.Server), p, err := strconv.Atoi(part)
PortBindings: []*mierupb.PortBinding{ if err != nil {
{ return nil, fmt.Errorf("invalid port value: %s", part)
Port: proto.Int32(int32(option.Port)), }
Protocol: transportProtocol, portBindings = append(portBindings, &mierupb.PortBinding{
}, Port: proto.Int32(int32(p)),
}, Protocol: transportProtocol,
} })
}
} else {
// server is a domain name
if option.PortRange != "" {
server = &mierupb.ServerEndpoint{
DomainName: proto.String(option.Server),
PortBindings: []*mierupb.PortBinding{
{
PortRange: proto.String(option.PortRange),
Protocol: transportProtocol,
},
},
}
} else {
server = &mierupb.ServerEndpoint{
DomainName: proto.String(option.Server),
PortBindings: []*mierupb.PortBinding{
{
Port: proto.Int32(int32(option.Port)),
Protocol: transportProtocol,
},
},
} }
} }
} }
if option.PortRange != "" {
parts := strings.Split(option.PortRange, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if _, _, err := beginAndEndPortFromPortRange(part); err == nil {
portBindings = append(portBindings, &mierupb.PortBinding{
PortRange: proto.String(part),
Protocol: transportProtocol,
})
}
}
}
var server *mierupb.ServerEndpoint
if net.ParseIP(option.Server) != nil {
// server is an IP address
server = &mierupb.ServerEndpoint{
IpAddress: proto.String(option.Server),
PortBindings: portBindings,
}
} else {
// server is a domain name
server = &mierupb.ServerEndpoint{
DomainName: proto.String(option.Server),
PortBindings: portBindings,
}
}
config := &mieruclient.ClientConfig{ config := &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{ Profile: &mierupb.ClientProfile{
ProfileName: proto.String(option.Name), ProfileName: proto.String(option.Name),
@ -259,31 +274,9 @@ func validateMieruOption(option MieruOption) error {
if option.Server == "" { if option.Server == "" {
return fmt.Errorf("server is empty") return fmt.Errorf("server is empty")
} }
if option.Port == 0 && option.PortRange == "" { if option.Port == "" && option.PortRange == "" {
return fmt.Errorf("either port or port-range must be set") return fmt.Errorf("port must be set")
} }
if option.Port != 0 && option.PortRange != "" {
return fmt.Errorf("port and port-range cannot be set at the same time")
}
if option.Port != 0 && (option.Port < 1 || option.Port > 65535) {
return fmt.Errorf("port must be between 1 and 65535")
}
if option.PortRange != "" {
begin, end, err := beginAndEndPortFromPortRange(option.PortRange)
if err != nil {
return fmt.Errorf("invalid port-range format")
}
if begin < 1 || begin > 65535 {
return fmt.Errorf("begin port must be between 1 and 65535")
}
if end < 1 || end > 65535 {
return fmt.Errorf("end port must be between 1 and 65535")
}
if begin > end {
return fmt.Errorf("begin port must be less than or equal to end port")
}
}
if option.Transport != "TCP" { if option.Transport != "TCP" {
return fmt.Errorf("transport must be TCP") return fmt.Errorf("transport must be TCP")
} }
@ -306,8 +299,36 @@ func validateMieruOption(option MieruOption) error {
return nil return nil
} }
func getFirstPort(portStr string) (int, error) {
if portStr == "" {
return 0, fmt.Errorf("port string is empty")
}
parts := strings.Split(portStr, ",")
firstPart := parts[0]
if strings.Contains(firstPart, "-") {
begin, _, err := beginAndEndPortFromPortRange(firstPart)
if err != nil {
return 0, err
}
return begin, nil
}
port, err := strconv.Atoi(firstPart)
if err != nil {
return 0, fmt.Errorf("invalid port format: %s", firstPart)
}
return port, nil
}
func beginAndEndPortFromPortRange(portRange string) (int, int, error) { func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
var begin, end int var begin, end int
_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end) _, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
if err != nil {
return 0, 0, fmt.Errorf("invalid port range format: %w", err)
}
if begin > end {
return 0, 0, fmt.Errorf("begin port is greater than end port: %s", portRange)
}
return begin, end, err return begin, end, err
} }

View File

@ -1,22 +1,51 @@
package outbound package outbound
import "testing" import (
"reflect"
"testing"
mieruclient "github.com/enfein/mieru/v3/apis/client"
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
"google.golang.org/protobuf/proto"
)
func TestNewMieru(t *testing.T) { func TestNewMieru(t *testing.T) {
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
testCases := []struct { testCases := []struct {
option MieruOption option MieruOption
wantBaseAddr string wantBaseAddr string
wantConfig *mieruclient.ClientConfig
}{ }{
{ {
option: MieruOption{ option: MieruOption{
Name: "test", Name: "test",
Server: "1.2.3.4", Server: "1.2.3.4",
Port: 10000, Port: "10000",
Transport: "TCP", Transport: "TCP",
UserName: "test", UserName: "test",
Password: "test", Password: "test",
}, },
wantBaseAddr: "1.2.3.4:10000", wantBaseAddr: "1.2.3.4:10000",
wantConfig: &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String("test"),
User: &mierupb.User{
Name: proto.String("test"),
Password: proto.String("test"),
},
Servers: []*mierupb.ServerEndpoint{
{
IpAddress: proto.String("1.2.3.4"),
PortBindings: []*mierupb.PortBinding{
{
Port: proto.Int32(10000),
Protocol: transportProtocol,
},
},
},
},
},
},
}, },
{ {
option: MieruOption{ option: MieruOption{
@ -28,28 +57,212 @@ func TestNewMieru(t *testing.T) {
Password: "test", Password: "test",
}, },
wantBaseAddr: "[2001:db8::1]:10001", wantBaseAddr: "[2001:db8::1]:10001",
wantConfig: &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String("test"),
User: &mierupb.User{
Name: proto.String("test"),
Password: proto.String("test"),
},
Servers: []*mierupb.ServerEndpoint{
{
IpAddress: proto.String("2001:db8::1"),
PortBindings: []*mierupb.PortBinding{
{
PortRange: proto.String("10001-10002"),
Protocol: transportProtocol,
},
},
},
},
},
},
}, },
{ {
option: MieruOption{ option: MieruOption{
Name: "test", Name: "test",
Server: "example.com", Server: "example.com",
Port: 10003, Port: "10003",
Transport: "TCP", Transport: "TCP",
UserName: "test", UserName: "test",
Password: "test", Password: "test",
}, },
wantBaseAddr: "example.com:10003", wantBaseAddr: "example.com:10003",
wantConfig: &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String("test"),
User: &mierupb.User{
Name: proto.String("test"),
Password: proto.String("test"),
},
Servers: []*mierupb.ServerEndpoint{
{
DomainName: proto.String("example.com"),
PortBindings: []*mierupb.PortBinding{
{
Port: proto.Int32(10003),
Protocol: transportProtocol,
},
},
},
},
},
},
},
{
option: MieruOption{
Name: "test",
Server: "example.com",
Port: "10004,10005",
Transport: "TCP",
UserName: "test",
Password: "test",
},
wantBaseAddr: "example.com:10004",
wantConfig: &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String("test"),
User: &mierupb.User{
Name: proto.String("test"),
Password: proto.String("test"),
},
Servers: []*mierupb.ServerEndpoint{
{
DomainName: proto.String("example.com"),
PortBindings: []*mierupb.PortBinding{
{
Port: proto.Int32(10004),
Protocol: transportProtocol,
},
{
Port: proto.Int32(10005),
Protocol: transportProtocol,
},
},
},
},
},
},
},
{
option: MieruOption{
Name: "test",
Server: "example.com",
Port: "10006-10007,11000",
Transport: "TCP",
UserName: "test",
Password: "test",
},
wantBaseAddr: "example.com:10006",
wantConfig: &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String("test"),
User: &mierupb.User{
Name: proto.String("test"),
Password: proto.String("test"),
},
Servers: []*mierupb.ServerEndpoint{
{
DomainName: proto.String("example.com"),
PortBindings: []*mierupb.PortBinding{
{
PortRange: proto.String("10006-10007"),
Protocol: transportProtocol,
},
{
Port: proto.Int32(11000),
Protocol: transportProtocol,
},
},
},
},
},
},
},
{
option: MieruOption{
Name: "test",
Server: "example.com",
Port: "10008",
PortRange: "10009-10010",
Transport: "TCP",
UserName: "test",
Password: "test",
},
wantBaseAddr: "example.com:10008",
wantConfig: &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String("test"),
User: &mierupb.User{
Name: proto.String("test"),
Password: proto.String("test"),
},
Servers: []*mierupb.ServerEndpoint{
{
DomainName: proto.String("example.com"),
PortBindings: []*mierupb.PortBinding{
{
Port: proto.Int32(10008),
Protocol: transportProtocol,
},
{
PortRange: proto.String("10009-10010"),
Protocol: transportProtocol,
},
},
},
},
},
},
}, },
} }
for _, testCase := range testCases { for _, testCase := range testCases {
mieru, err := NewMieru(testCase.option) mieru, err := NewMieru(testCase.option)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
} }
config, err := mieru.client.Load()
if err != nil {
t.Fatal(err)
}
config.Dialer = nil
if mieru.addr != testCase.wantBaseAddr { if mieru.addr != testCase.wantBaseAddr {
t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr) t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr)
} }
if !reflect.DeepEqual(config, testCase.wantConfig) {
t.Errorf("got config %+v, want %+v", config, testCase.wantConfig)
}
}
}
func TestNewMieruError(t *testing.T) {
testCases := []MieruOption{
{
Name: "test",
Server: "example.com",
Port: "invalid",
PortRange: "invalid",
Transport: "TCP",
UserName: "test",
Password: "test",
},
{
Name: "test",
Server: "example.com",
Port: "",
PortRange: "",
Transport: "TCP",
UserName: "test",
Password: "test",
},
}
for _, option := range testCases {
_, err := NewMieru(option)
if err == nil {
t.Errorf("expected error for option %+v, but got nil", option)
}
} }
} }
@ -63,6 +276,7 @@ func TestBeginAndEndPortFromPortRange(t *testing.T) {
{"1-10", 1, 10, false}, {"1-10", 1, 10, false},
{"1000-2000", 1000, 2000, false}, {"1000-2000", 1000, 2000, false},
{"65535-65535", 65535, 65535, false}, {"65535-65535", 65535, 65535, false},
{"2000-1000", 0, 0, true},
{"1", 0, 0, true}, {"1", 0, 0, true},
{"1-", 0, 0, true}, {"1-", 0, 0, true},
{"-10", 0, 0, true}, {"-10", 0, 0, true},

View File

@ -1024,8 +1024,8 @@ proxies: # socks5
- name: mieru - name: mieru
type: mieru type: mieru
server: 1.2.3.4 server: 1.2.3.4
port: 2999 port: 2999 # 支持使用 ports 格式,例如 2999,3999 或 2999-3010,3950,3995-3999
# port-range: 2090-2099 #(不可同时填写 port 和 port-range # port-range: 2090-2099 # 已废弃,请使用 port
transport: TCP # 只支持 TCP transport: TCP # 只支持 TCP
udp: true # 支持 UDP over TCP udp: true # 支持 UDP over TCP
username: user username: user