diff --git a/adapter/outbound/mieru.go b/adapter/outbound/mieru.go index 8ef9cfd7..bfdf0e51 100644 --- a/adapter/outbound/mieru.go +++ b/adapter/outbound/mieru.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "strconv" + "strings" "sync" CN "github.com/metacubex/mihomo/common/net" @@ -30,8 +31,8 @@ type MieruOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - PortRange string `proxy:"port-range,omitempty"` + Port string `proxy:"port,omitempty"` + PortRange string `proxy:"port-range,omitempty"` // deprecated Transport string `proxy:"transport"` UDP bool `proxy:"udp,omitempty"` UserName string `proxy:"username"` @@ -123,13 +124,19 @@ func NewMieru(option MieruOption) (*Mieru, error) { } // Client is started lazily on the first use. + // Use the first port to construct the address. var addr string - if option.Port != 0 { - addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + var portStr string + if option.Port != "" { + portStr = option.Port } else { - beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange) - addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort)) + portStr = option.PortRange } + 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{ Base: &Base{ name: option.Name, @@ -183,54 +190,62 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro } transportProtocol := mierupb.TransportProtocol_TCP.Enum() - var server *mierupb.ServerEndpoint - if net.ParseIP(option.Server) != nil { - // server is an IP address - if option.PortRange != "" { - server = &mierupb.ServerEndpoint{ - IpAddress: proto.String(option.Server), - PortBindings: []*mierupb.PortBinding{ - { - PortRange: proto.String(option.PortRange), + + portBindings := make([]*mierupb.PortBinding, 0) + if option.Port != "" { + parts := strings.Split(option.Port, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.Contains(part, "-") { + _, _, err := beginAndEndPortFromPortRange(part) + if err == nil { + portBindings = append(portBindings, &mierupb.PortBinding{ + PortRange: proto.String(part), Protocol: transportProtocol, - }, - }, - } - } else { - server = &mierupb.ServerEndpoint{ - IpAddress: proto.String(option.Server), - PortBindings: []*mierupb.PortBinding{ - { - Port: proto.Int32(int32(option.Port)), - 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, - }, - }, + }) + } else { + return nil, err + } + } else { + p, err := strconv.Atoi(part) + if err != nil { + return nil, fmt.Errorf("invalid port value: %s", part) + } + portBindings = append(portBindings, &mierupb.PortBinding{ + Port: proto.Int32(int32(p)), + 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{ Profile: &mierupb.ClientProfile{ ProfileName: proto.String(option.Name), @@ -259,31 +274,9 @@ func validateMieruOption(option MieruOption) error { if option.Server == "" { return fmt.Errorf("server is empty") } - if option.Port == 0 && option.PortRange == "" { - return fmt.Errorf("either port or port-range must be set") + if option.Port == "" && option.PortRange == "" { + 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" { return fmt.Errorf("transport must be TCP") } @@ -306,8 +299,36 @@ func validateMieruOption(option MieruOption) error { 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) { var begin, end int _, 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 } diff --git a/adapter/outbound/mieru_test.go b/adapter/outbound/mieru_test.go index 086b7910..2b7976e4 100644 --- a/adapter/outbound/mieru_test.go +++ b/adapter/outbound/mieru_test.go @@ -1,22 +1,51 @@ 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) { + transportProtocol := mierupb.TransportProtocol_TCP.Enum() testCases := []struct { option MieruOption wantBaseAddr string + wantConfig *mieruclient.ClientConfig }{ { option: MieruOption{ Name: "test", Server: "1.2.3.4", - Port: 10000, + Port: "10000", Transport: "TCP", UserName: "test", Password: "test", }, 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{ @@ -28,28 +57,212 @@ func TestNewMieru(t *testing.T) { Password: "test", }, 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{ Name: "test", Server: "example.com", - Port: 10003, + Port: "10003", Transport: "TCP", UserName: "test", Password: "test", }, 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 { mieru, err := NewMieru(testCase.option) 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 { 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}, {"1000-2000", 1000, 2000, false}, {"65535-65535", 65535, 65535, false}, + {"2000-1000", 0, 0, true}, {"1", 0, 0, true}, {"1-", 0, 0, true}, {"-10", 0, 0, true}, diff --git a/docs/config.yaml b/docs/config.yaml index b2d86c23..57418818 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1024,8 +1024,8 @@ proxies: # socks5 - name: mieru type: mieru server: 1.2.3.4 - port: 2999 - # port-range: 2090-2099 #(不可同时填写 port 和 port-range) + port: 2999 # 支持使用 ports 格式,例如 2999,3999 或 2999-3010,3950,3995-3999 + # port-range: 2090-2099 # 已废弃,请使用 port transport: TCP # 只支持 TCP udp: true # 支持 UDP over TCP username: user