Skip to content

Commit

Permalink
feat: support mieru protocol (issue MetaCubeX#1563)
Browse files Browse the repository at this point in the history
  • Loading branch information
enfein committed Dec 3, 2024
1 parent 5a24efd commit 943a0ff
Show file tree
Hide file tree
Showing 7 changed files with 436 additions and 6 deletions.
320 changes: 320 additions & 0 deletions adapter/outbound/mieru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
package outbound

import (
"context"
"fmt"
"math/rand"
"net"
"net/netip"
"runtime"
"strconv"

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"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"google.golang.org/protobuf/proto"
)

const (
// Default MTU used in mieru UDP transport.
mieruDefaultMTU = 1400
)

type Mieru struct {
*Base
option *MieruOption
client mieruclient.Client
}

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"`
MTU int `proxy:"mtu,omitempty"`
}

type MieruResolver struct {
resolver.Resolver
}

// DialContext implements C.ProxyAdapter
func (m *Mieru) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
addr := metadataToMieruNetAddrSpec(metadata)
return m.client.DialContextWithConn(ctx, c, addr)
}

// DialContext implements C.ProxyAdapter
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return m.DialContextWithDialer(ctx, dialer.NewDialer(m.Base.DialOptions(opts...)...), metadata)
}

// DialContext implements C.ProxyAdapter
func (m *Mieru) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
var err error
if len(m.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
network, address, err := m.pickOneServerEndpoint()
if err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, network, address)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", address, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = m.StreamConnContext(ctx, c, metadata)
return NewConn(c, m), err
}

func (m *Mieru) pickOneServerEndpoint() (network, address string, err error) {
if m.option.Transport == "TCP" {
network = "tcp"
} else if m.option.Transport == "UDP" {
network = "udp"
} else {
err = fmt.Errorf("transport %s is invalid", m.option.Transport)
return
}
if m.option.Port != 0 {
address = net.JoinHostPort(m.option.Server, strconv.Itoa(m.option.Port))
} else {
var beginPort, endPort int
beginPort, endPort, err = beginAndEndPortFromPortRange(m.option.PortRange)
if err != nil {
return
}
randomPort := beginPort + rand.Intn(endPort-beginPort+1)
address = net.JoinHostPort(m.option.Server, strconv.Itoa(randomPort))
}
return
}

func (mr MieruResolver) LookupIP(ctx context.Context, network, host string) ([]net.IP, error) {
var netIPs []netip.Addr
var err error
if network == "ip4" {
netIPs, err = mr.Resolver.LookupIPv4(ctx, host)
} else if network == "ip6" {
netIPs, err = mr.Resolver.LookupIPv6(ctx, host)
} else {
netIPs, err = mr.Resolver.LookupIP(ctx, host)
}
if err != nil {
return nil, err
}

ips := make([]net.IP, len(netIPs))
for i := 0; i < len(netIPs); i++ {
ips[i] = netIPs[i].AsSlice()
}
return ips, 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)
}
if err := c.Start(); err != nil {
return nil, fmt.Errorf("failed to start mieru client: %w", err)
}

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) {
if m.client != nil {
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)
}

var transportProtocol *mierupb.TransportProtocol
if option.Transport == "TCP" {
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
} else if option.Transport == "UDP" {
transportProtocol = mierupb.TransportProtocol_UDP.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,
},
},
}
}
}
if option.MTU == 0 {
option.MTU = mieruDefaultMTU
}
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},
Mtu: proto.Int32(int32(option.MTU)),
Multiplexing: &mierupb.MultiplexingConfig{
// Multiplexing doesn't work well with connection tracking.
Level: mierupb.MultiplexingLevel_MULTIPLEXING_OFF.Enum(),
},
},
Resolver: MieruResolver{Resolver: resolver.DefaultResolver},
}, 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" && option.Transport != "UDP" {
return fmt.Errorf("transport must be TCP or UDP")
}
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
}
Loading

0 comments on commit 943a0ff

Please sign in to comment.