-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support mieru protocol (#1702)
- Loading branch information
Showing
7 changed files
with
391 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
package outbound | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net" | ||
"runtime" | ||
"strconv" | ||
"sync" | ||
|
||
mieruclient "github.com/enfein/mieru/v3/apis/client" | ||
mierumodel "github.com/enfein/mieru/v3/apis/model" | ||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb" | ||
"github.com/metacubex/mihomo/component/dialer" | ||
"github.com/metacubex/mihomo/component/proxydialer" | ||
C "github.com/metacubex/mihomo/constant" | ||
"google.golang.org/protobuf/proto" | ||
) | ||
|
||
type Mieru struct { | ||
*Base | ||
option *MieruOption | ||
client mieruclient.Client | ||
mu sync.Mutex | ||
} | ||
|
||
type MieruOption struct { | ||
BasicOption | ||
Name string `proxy:"name"` | ||
Server string `proxy:"server"` | ||
Port int `proxy:"port,omitempty"` | ||
PortRange string `proxy:"port-range,omitempty"` | ||
Transport string `proxy:"transport"` | ||
UserName string `proxy:"username"` | ||
Password string `proxy:"password"` | ||
} | ||
|
||
// DialContext implements C.ProxyAdapter | ||
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { | ||
if err := m.ensureClientIsRunning(opts...); err != nil { | ||
return nil, err | ||
} | ||
addr := metadataToMieruNetAddrSpec(metadata) | ||
c, err := m.client.DialContext(ctx, addr) | ||
if err != nil { | ||
return nil, fmt.Errorf("dial to %s failed: %w", addr, err) | ||
} | ||
return NewConn(c, m), nil | ||
} | ||
|
||
// ProxyInfo implements C.ProxyAdapter | ||
func (m *Mieru) ProxyInfo() C.ProxyInfo { | ||
info := m.Base.ProxyInfo() | ||
info.DialerProxy = m.option.DialerProxy | ||
return info | ||
} | ||
|
||
func (m *Mieru) ensureClientIsRunning(opts ...dialer.Option) error { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
|
||
if m.client.IsRunning() { | ||
return nil | ||
} | ||
|
||
// Create a dialer and add it to the client config, before starting the client. | ||
var dialer C.Dialer = dialer.NewDialer(m.Base.DialOptions(opts...)...) | ||
var err error | ||
if len(m.option.DialerProxy) > 0 { | ||
dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
config, err := m.client.Load() | ||
if err != nil { | ||
return err | ||
} | ||
config.Dialer = dialer | ||
if err := m.client.Store(config); err != nil { | ||
return err | ||
} | ||
|
||
if err := m.client.Start(); err != nil { | ||
return fmt.Errorf("failed to start mieru client: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func NewMieru(option MieruOption) (*Mieru, error) { | ||
config, err := buildMieruClientConfig(option) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to build mieru client config: %w", err) | ||
} | ||
c := mieruclient.NewClient() | ||
if err := c.Store(config); err != nil { | ||
return nil, fmt.Errorf("failed to store mieru client config: %w", err) | ||
} | ||
// Client is started lazily on the first use. | ||
|
||
var addr string | ||
if option.Port != 0 { | ||
addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) | ||
} else { | ||
beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange) | ||
addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort)) | ||
} | ||
outbound := &Mieru{ | ||
Base: &Base{ | ||
name: option.Name, | ||
addr: addr, | ||
iface: option.Interface, | ||
tp: C.Mieru, | ||
udp: false, | ||
xudp: false, | ||
rmark: option.RoutingMark, | ||
prefer: C.NewDNSPrefer(option.IPVersion), | ||
}, | ||
option: &option, | ||
client: c, | ||
} | ||
runtime.SetFinalizer(outbound, closeMieru) | ||
return outbound, nil | ||
} | ||
|
||
func closeMieru(m *Mieru) { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
if m.client != nil && m.client.IsRunning() { | ||
m.client.Stop() | ||
} | ||
} | ||
|
||
func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec { | ||
if metadata.Host != "" { | ||
return mierumodel.NetAddrSpec{ | ||
AddrSpec: mierumodel.AddrSpec{ | ||
FQDN: metadata.Host, | ||
Port: int(metadata.DstPort), | ||
}, | ||
Net: "tcp", | ||
} | ||
} else { | ||
return mierumodel.NetAddrSpec{ | ||
AddrSpec: mierumodel.AddrSpec{ | ||
IP: metadata.DstIP.AsSlice(), | ||
Port: int(metadata.DstPort), | ||
}, | ||
Net: "tcp", | ||
} | ||
} | ||
} | ||
|
||
func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) { | ||
if err := validateMieruOption(option); err != nil { | ||
return nil, fmt.Errorf("failed to validate mieru option: %w", err) | ||
} | ||
|
||
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), | ||
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, | ||
}, | ||
}, | ||
} | ||
} | ||
} | ||
return &mieruclient.ClientConfig{ | ||
Profile: &mierupb.ClientProfile{ | ||
ProfileName: proto.String(option.Name), | ||
User: &mierupb.User{ | ||
Name: proto.String(option.UserName), | ||
Password: proto.String(option.Password), | ||
}, | ||
Servers: []*mierupb.ServerEndpoint{server}, | ||
}, | ||
}, nil | ||
} | ||
|
||
func validateMieruOption(option MieruOption) error { | ||
if option.Name == "" { | ||
return fmt.Errorf("name is empty") | ||
} | ||
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 != 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") | ||
} | ||
if option.UserName == "" { | ||
return fmt.Errorf("username is empty") | ||
} | ||
if option.Password == "" { | ||
return fmt.Errorf("password is empty") | ||
} | ||
return nil | ||
} | ||
|
||
func beginAndEndPortFromPortRange(portRange string) (int, int, error) { | ||
var begin, end int | ||
_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end) | ||
return begin, end, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package outbound | ||
|
||
import "testing" | ||
|
||
func TestNewMieru(t *testing.T) { | ||
testCases := []struct { | ||
option MieruOption | ||
wantBaseAddr string | ||
}{ | ||
{ | ||
option: MieruOption{ | ||
Name: "test", | ||
Server: "1.2.3.4", | ||
Port: 10000, | ||
Transport: "TCP", | ||
UserName: "test", | ||
Password: "test", | ||
}, | ||
wantBaseAddr: "1.2.3.4:10000", | ||
}, | ||
{ | ||
option: MieruOption{ | ||
Name: "test", | ||
Server: "2001:db8::1", | ||
PortRange: "10001-10002", | ||
Transport: "TCP", | ||
UserName: "test", | ||
Password: "test", | ||
}, | ||
wantBaseAddr: "[2001:db8::1]:10001", | ||
}, | ||
{ | ||
option: MieruOption{ | ||
Name: "test", | ||
Server: "example.com", | ||
Port: 10003, | ||
Transport: "TCP", | ||
UserName: "test", | ||
Password: "test", | ||
}, | ||
wantBaseAddr: "example.com:10003", | ||
}, | ||
} | ||
|
||
for _, testCase := range testCases { | ||
mieru, err := NewMieru(testCase.option) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
if mieru.addr != testCase.wantBaseAddr { | ||
t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr) | ||
} | ||
} | ||
} | ||
|
||
func TestBeginAndEndPortFromPortRange(t *testing.T) { | ||
testCases := []struct { | ||
input string | ||
begin int | ||
end int | ||
hasErr bool | ||
}{ | ||
{"1-10", 1, 10, false}, | ||
{"1000-2000", 1000, 2000, false}, | ||
{"65535-65535", 65535, 65535, false}, | ||
{"1", 0, 0, true}, | ||
{"1-", 0, 0, true}, | ||
{"-10", 0, 0, true}, | ||
{"a-b", 0, 0, true}, | ||
{"1-b", 0, 0, true}, | ||
{"a-10", 0, 0, true}, | ||
} | ||
|
||
for _, testCase := range testCases { | ||
begin, end, err := beginAndEndPortFromPortRange(testCase.input) | ||
if testCase.hasErr { | ||
if err == nil { | ||
t.Errorf("beginAndEndPortFromPortRange(%s) should return an error", testCase.input) | ||
} | ||
} else { | ||
if err != nil { | ||
t.Errorf("beginAndEndPortFromPortRange(%s) should not return an error, but got %v", testCase.input, err) | ||
} | ||
if begin != testCase.begin { | ||
t.Errorf("beginAndEndPortFromPortRange(%s) begin port mismatch, got %d, want %d", testCase.input, begin, testCase.begin) | ||
} | ||
if end != testCase.end { | ||
t.Errorf("beginAndEndPortFromPortRange(%s) end port mismatch, got %d, want %d", testCase.input, end, testCase.end) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.