Skip to content

Commit

Permalink
feat: support mieru
Browse files Browse the repository at this point in the history
  • Loading branch information
snakem982 committed Dec 5, 2024
1 parent 85871ea commit a02e5e0
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 59 deletions.
173 changes: 123 additions & 50 deletions adapter/outbound/mieru.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ package outbound
import (
"context"
"fmt"
mierumodel "github.com/enfein/mieru/v3/apis/model"
"github.com/metacubex/mihomo/component/proxydialer"
"math/rand"
"net"
"net/netip"
"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"
"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
mu sync.Mutex
}

type MieruOption struct {
Expand All @@ -36,49 +36,47 @@ type MieruOption struct {
Transport string `proxy:"transport"`
UserName string `proxy:"username"`
Password string `proxy:"password"`
MTU int `proxy:"mtu,omitempty"`
}

type MieruResolver struct {
resolver.Resolver
}

// StreamConnContext implements C.ProxyAdapter
func (m *Mieru) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
netAddrSpec := mierumodel.NetAddrSpec{
AddrSpec: mierumodel.AddrSpec{
Port: int(metadata.DstPort),
},
Net: metadata.NetWork.String(),
}

if metadata.Host != "" {
netAddrSpec.AddrSpec.FQDN = metadata.Host
} else {
netAddrSpec.AddrSpec.IP = metadata.DstIP.AsSlice()
if err := m.ensureClientIsRunning(); err != nil {
return nil, err
}

return m.client.DialContextWithConn(ctx, c, netAddrSpec)
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, err error) {
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)
}

// DialContextWithDialer implements C.ProxyAdapter
func (m *Mieru) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
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
}
}
c, err := dialer.DialContext(ctx, "tcp", m.addr)
network, address, err := m.pickOneServerEndpoint()
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", m.addr, err)
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)
if err != nil {
return nil, err
Expand All @@ -92,18 +90,84 @@ func (m *Mieru) SupportWithDialer() C.NetWork {
return C.TCP
}

// SupportUOT implements C.ProxyAdapter
func (m *Mieru) SupportUOT() bool {
return false
}

// 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() error {
m.mu.Lock()
defer m.mu.Unlock()

if m.client.IsRunning() {
return nil
}

config, err := m.client.Load()
if err != nil {
return err
}
serverDomainName := config.Profile.GetServers()[0].GetDomainName()
if serverDomainName != "" && config.Resolver == nil {
if resolver.ProxyServerHostResolver == nil {
return fmt.Errorf("mieru server is a domain name %q but DNS is not enabled", serverDomainName)
}
config.Resolver = MieruResolver{Resolver: resolver.ProxyServerHostResolver}
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 (m *Mieru) pickOneServerEndpoint() (network, address string, err error) {
if m.option.Transport == "TCP" {
network = "tcp"
} 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 {
Expand All @@ -113,9 +177,7 @@ func NewMieru(option MieruOption) (*Mieru, error) {
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)
}
// Client is started lazily on the first use.

var addr string
if option.Port != 0 {
Expand Down Expand Up @@ -143,22 +205,39 @@ func NewMieru(option MieruOption) (*Mieru, error) {
}

func closeMieru(m *Mieru) {
if m.client != nil {
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)
}

var transportProtocol *mierupb.TransportProtocol
if option.Transport == "TCP" {
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
} else if option.Transport == "UDP" {
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
}
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
var server *mierupb.ServerEndpoint
if net.ParseIP(option.Server) != nil {
// server is an IP address
Expand Down Expand Up @@ -207,9 +286,6 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro
}
}
}
if option.MTU == 0 {
option.MTU = mieruDefaultMTU
}
return &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String(option.Name),
Expand All @@ -218,18 +294,15 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro
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(),
},
},
}, nil
}

func validateMieruOption(option MieruOption) error {
if option.DialerProxy != "" {
return fmt.Errorf("dialer proxy is not supported")
}
if option.Name == "" {
return fmt.Errorf("name is empty")
}
Expand Down Expand Up @@ -261,8 +334,8 @@ func validateMieruOption(option MieruOption) error {
}
}

if option.Transport != "TCP" && option.Transport != "UDP" {
return fmt.Errorf("transport must be TCP or UDP")
if option.Transport != "TCP" {
return fmt.Errorf("transport must be TCP")
}
if option.UserName == "" {
return fmt.Errorf("username is empty")
Expand Down
29 changes: 20 additions & 9 deletions common/convert/singbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"gopkg.in/yaml.v3"
"net/url"
"reflect"
"strconv"
"strings"
)
Expand Down Expand Up @@ -132,7 +133,11 @@ func ConvertsSingBox(buf []byte) ([]map[string]any, error) {

vmess["uuid"] = outbound.UUID
vmess["alterId"] = outbound.AlterID
vmess["cipher"] = outbound.Security
if outbound.Security != "" {
vmess["cipher"] = outbound.Security
} else {
vmess["cipher"] = "auto"
}

vmess["udp"] = true
vmess["xudp"] = true
Expand Down Expand Up @@ -275,14 +280,14 @@ type SingReality struct {
}

type SingTransport struct {
Headers map[string]any `json:"headers,omitempty" yaml:"headers,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
EarlyDataHeaderName string `json:"early_data_header_name,omitempty" yaml:"early-data-header-name,omitempty"`
MaxEarlyData int `json:"max_early_data,omitempty" yaml:"max-early-data,omitempty"`
Host any `json:"host,omitempty" yaml:"host,omitempty"`
Method string `json:"method,omitempty" yaml:"method,omitempty"`
ServiceName string `json:"service_name,omitempty" yaml:"grpc-service-name,omitempty"`
Headers any `json:"headers,omitempty" yaml:"headers,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
EarlyDataHeaderName string `json:"early_data_header_name,omitempty" yaml:"early-data-header-name,omitempty"`
MaxEarlyData int `json:"max_early_data,omitempty" yaml:"max-early-data,omitempty"`
Host any `json:"host,omitempty" yaml:"host,omitempty"`
Method string `json:"method,omitempty" yaml:"method,omitempty"`
ServiceName string `json:"service_name,omitempty" yaml:"grpc-service-name,omitempty"`
}

type SingMultiplex struct {
Expand Down Expand Up @@ -403,5 +408,11 @@ func SingTransportToMap(obj *SingTransport) map[string]interface{} {
return nil
}

if _, exists := result["headers"]; exists {
if reflect.TypeOf(result["headers"]).Kind() != reflect.Map {
delete(result, "headers")
}
}

return result
}

0 comments on commit a02e5e0

Please sign in to comment.