Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 119 additions & 7 deletions services/mihomo/mihomo.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,52 @@ func SetIPv6(enable bool) {
resolver.DisableIPv6 = !enable
}

// mockTunnel implements constant.Tunnel and proxydialer.Tunnel for dialer support in speed tests
// It provides a minimal tunnel context for proxydialer.NewByName to look up dialer proxies
type mockTunnel struct {
proxies map[string]constant.Proxy
}

// HandleTCPConn implements constant.Tunnel
func (t *mockTunnel) HandleTCPConn(conn net.Conn, metadata *constant.Metadata) {
_ = conn.Close()
}

// HandleUDPPacket implements constant.Tunnel
func (t *mockTunnel) HandleUDPPacket(packet constant.UDPPacket, metadata *constant.Metadata) {
packet.Drop()
}

// NatTable implements constant.Tunnel
func (t *mockTunnel) NatTable() constant.NatTable {
return nil
}

// Proxies implements proxydialer.Tunnel - required for proxydialer.NewByName
func (t *mockTunnel) Proxies() map[string]constant.Proxy {
return t.proxies
}

// GetMihomoAdapter creates a Mihomo Proxy Adapter from a node link
func GetMihomoAdapter(nodeLink string) (constant.Proxy, error) {
return getMihomoAdapterWithContext(nodeLink, nil, "")
}

// GetMihomoAdapterWithDialer creates a Mihomo Proxy Adapter with dialer proxy support
// dialerName: name of the dialer proxy to use (matches node.EffectiveName())
// dialerLink: link of the dialer proxy node
func GetMihomoAdapterWithDialer(nodeLink, dialerName, dialerLink string) (constant.Proxy, error) {
return getMihomoAdapterWithContext(nodeLink, &dialerInfo{Name: dialerName, Link: dialerLink}, dialerName)
}

// dialerInfo holds dialer proxy information
type dialerInfo struct {
Name string
Link string
}

// getMihomoAdapterWithContext creates a Mihomo Proxy Adapter with optional dialer context
func getMihomoAdapterWithContext(nodeLink string, dialer *dialerInfo, dialerProxyName string) (constant.Proxy, error) {
// 1. Parse node link to Proxy struct
// We use a default OutputConfig as we only need the proxy connection info
outputConfig := protocol.OutputConfig{
Expand Down Expand Up @@ -70,10 +114,59 @@ func GetMihomoAdapter(nodeLink string) (constant.Proxy, error) {
return nil, fmt.Errorf("unmarshal proxy map error: %v", err)
}

// 3. Create Mihomo Proxy Adapter
proxyAdapter, err := adapter.ParseProxy(proxyMap)
if err != nil {
return nil, fmt.Errorf("create mihomo adapter error: %v", err)
// 3. Build proxy map with dialer support if provided
var proxyAdapter constant.Proxy
if dialer != nil && dialer.Name != "" && dialer.Link != "" {
// Parse dialer node to build proxy map
dialerProxyStruct, err := protocol.LinkToProxy(protocol.Urls{Url: dialer.Link}, outputConfig)
if err != nil {
return nil, fmt.Errorf("parse dialer link error: %v", err)
}

dialerYamlBytes, err := yaml.Marshal(dialerProxyStruct)
if err != nil {
return nil, fmt.Errorf("marshal dialer proxy error: %v", err)
}

var dialerProxyMap map[string]any
err = yaml.Unmarshal(dialerYamlBytes, &dialerProxyMap)
if err != nil {
return nil, fmt.Errorf("unmarshal dialer proxy map error: %v", err)
}

// Ensure dialer has a name for proxydialer.NewByName lookup
if name, ok := dialerProxyMap["name"].(string); !ok || name == "" {
dialerProxyMap["name"] = dialer.Name
}

// Build proxy map: dialer + test node
proxies := make(map[string]constant.Proxy)
// Create dialer proxy adapter first
dialerAdapter, err := adapter.ParseProxy(dialerProxyMap)
if err != nil {
return nil, fmt.Errorf("create dialer adapter error: %v", err)
}
proxies[dialer.Name] = adapter.NewProxy(dialerAdapter)

// Create mock tunnel with dialer in proxy map
mockTunnel := &mockTunnel{proxies: proxies}

// Add dialer-proxy field to test node if dialerProxyName specified
if dialerProxyName != "" {
proxyMap["dialer-proxy"] = dialerProxyName
}

// Parse test node with tunnel context
proxyAdapter, err = adapter.ParseProxy(proxyMap, adapter.WithTunnelForAPI(mockTunnel))
if err != nil {
return nil, fmt.Errorf("create proxy adapter with dialer error: %v", err)
}
} else {
// 4. Create Mihomo Proxy Adapter (without dialer)
proxyAdapter, err = adapter.ParseProxy(proxyMap)
if err != nil {
return nil, fmt.Errorf("create mihomo adapter error: %v", err)
}
}
return proxyAdapter, nil
}
Expand Down Expand Up @@ -118,6 +211,8 @@ func MihomoDelayWithAdapter(proxyAdapter constant.Proxy, testUrl string, timeout
// landingIPUrl: IP查询服务URL,空则使用默认值 https://api.ipify.org
// detectQuality: 是否检测 IP 质量
// qualityURL: 质量查询服务 URL,空则使用默认值 https://my.123169.xyz/v1/info
// dialerName: 前置代理节点名称(对应 node.EffectiveName())
// dialerLink: 前置代理节点链接
// 返回: latency(ms), landingIP(若未检测或失败则为空), quality(若未检测或失败则为nil), error
func MihomoDelayTest(
nodeLink string,
Expand All @@ -128,6 +223,8 @@ func MihomoDelayTest(
landingIPUrl string,
detectQuality bool,
qualityURL string,
dialerName string,
dialerLink string,
) (latency int, landingIP string, quality *QualityCheckResult, err error) {
// Recover from any panics
defer func() {
Expand All @@ -143,8 +240,13 @@ func MihomoDelayTest(
testUrl = "http://cp.cloudflare.com/generate_204"
}

// 创建 adapter
proxyAdapter, err := GetMihomoAdapter(nodeLink)
// 创建 adapter(支持 dialer)
var proxyAdapter constant.Proxy
if dialerName != "" && dialerLink != "" {
proxyAdapter, err = GetMihomoAdapterWithDialer(nodeLink, dialerName, dialerLink)
} else {
proxyAdapter, err = GetMihomoAdapter(nodeLink)
}
if err != nil {
return 0, "", nil, err
}
Expand Down Expand Up @@ -175,6 +277,8 @@ func MihomoDelayTest(
// qualityURL: 质量查询服务 URL,空则使用默认值 https://my.123169.xyz/v1/info
// speedRecordMode: 速度记录模式 "average"=平均速度, "peak"=峰值速度
// peakSampleInterval: 峰值采样间隔(毫秒),仅在peak模式下生效,范围50-200
// dialerName: 前置代理节点名称(对应 node.EffectiveName())
// dialerLink: 前置代理节点链接
// 返回: speed(MB/s), latency(ms), bytesDownloaded, landingIP(若未检测或失败则为空), quality(若未检测或失败则为nil), error
func MihomoSpeedTest(
nodeLink string,
Expand All @@ -186,6 +290,8 @@ func MihomoSpeedTest(
qualityURL string,
speedRecordMode string,
peakSampleInterval int,
dialerName string,
dialerLink string,
) (speed float64, latency int, bytesDownloaded int64, landingIP string, quality *QualityCheckResult, err error) {
// Recover from any panics and return error with zero values
defer func() {
Expand All @@ -209,7 +315,13 @@ func MihomoSpeedTest(
peakSampleInterval = 200
}

proxyAdapter, err := GetMihomoAdapter(nodeLink)
// 创建 adapter(支持 dialer)
var proxyAdapter constant.Proxy
if dialerName != "" && dialerLink != "" {
proxyAdapter, err = GetMihomoAdapterWithDialer(nodeLink, dialerName, dialerLink)
} else {
proxyAdapter, err = GetMihomoAdapter(nodeLink)
}
if err != nil {
return 0, 0, 0, "", nil, err
}
Expand Down
27 changes: 27 additions & 0 deletions services/scheduler/speedtest_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,18 @@ func RunSpeedTestWithConfig(nodes []models.Node, trigger models.TaskTrigger, pro
// TCP模式下需要检测IP(因为没有速度测试阶段),mihomo模式在速度阶段检测
detectIPInLatency := detectCountry && speedTestMode == "tcp"
detectQualityInLatency := detectQuality && speedTestMode == "tcp"

// 准备 dialer 参数(如果节点配置了前置代理)
dialerName := ""
dialerLink := ""
if n.DialerProxyName != "" {
// 查找 dialer 节点(按名称或 LinkName)
if dialerNode, found := models.GetNodeByName(n.DialerProxyName); found && dialerNode != nil {
dialerName = dialerNode.EffectiveName()
dialerLink = dialerNode.Link
}
}

latency, landingIP, qualityInfo, err := mihomo.MihomoDelayTest(
n.Link,
latencyTestUrl,
Expand All @@ -271,6 +283,8 @@ func RunSpeedTestWithConfig(nodes []models.Node, trigger models.TaskTrigger, pro
landingIPUrl,
detectQualityInLatency,
qualityCheckURL,
dialerName,
dialerLink,
)

mu.Lock()
Expand Down Expand Up @@ -521,6 +535,17 @@ func RunSpeedTestWithConfig(nodes []models.Node, trigger models.TaskTrigger, pro
}

// 速度测试(延迟已在阶段一获取,同时可选检测落地IP)
// 准备 dialer 参数(如果节点配置了前置代理)
dialerName := ""
dialerLink := ""
if result.node.DialerProxyName != "" {
// 查找 dialer 节点(按名称或 LinkName)
if dialerNode, found := models.GetNodeByName(result.node.DialerProxyName); found && dialerNode != nil {
dialerName = dialerNode.EffectiveName()
dialerLink = dialerNode.Link
}
}

speed, _, bytesDownloaded, landingIP, qualityInfo, err := mihomo.MihomoSpeedTest(
result.node.Link,
speedTestUrl,
Expand All @@ -531,6 +556,8 @@ func RunSpeedTestWithConfig(nodes []models.Node, trigger models.TaskTrigger, pro
qualityCheckURL,
speedRecordMode,
peakSampleInterval,
dialerName,
dialerLink,
)

mu.Lock()
Expand Down