From a8dde2909c5ad3d8facdbe93f805bd434642be24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8B=97=E5=B8=A6=E5=B8=A6=E5=AD=90?= <19979176+isdoge@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:42:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(speedtest):=20=E8=AE=A9=E6=B5=8B=E9=80=9F?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=8A=82=E7=82=B9=E7=9A=84=20DialerProxyName?= =?UTF-8?q?=20=E5=89=8D=E7=BD=AE=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:sublinkpro 内置测速调用 MihomoDelayTest/MihomoSpeedTest 时只 传节点原始 Link,不读 Node.DialerProxyName,导致配置了前置代理的 节点测速仍走直连,无法反映 dialer 实际效果。 修复: - services/mihomo/mihomo.go: 新增 mockTunnel 实现 constant.Tunnel 与 proxydialer.Tunnel,新增 GetMihomoAdapterWithDialer 与内部 getMihomoAdapterWithContext;当传入 dialer 时构建 dialer 代理映射 并用 adapter.WithTunnelForAPI 注入 tunnel,proxyMap 写入 dialer-proxy 字段,使 hy2/trojan/vmess 等节点经 dialer 拨号。 - services/scheduler/speedtest_task.go: 延迟与速度两阶段在调用前 通过 models.GetNodeByName(n.DialerProxyName) 查询 dialer 节点, 取其 EffectiveName() 与 Link 传入测速函数。 兼容性:DialerProxyName 为空时行为与原来完全一致;GetMihomoAdapter 旧签名保留,node/sub.go、node/usage.go、services/unlock/orchestrator.go 等既有调用点无需改动。 Refs: 内部验证发现 speedtest_task.go 只传 n.Link 不带 DialerProxyName --- services/mihomo/mihomo.go | 126 +++++++++++++++++++++++++-- services/scheduler/speedtest_task.go | 27 ++++++ 2 files changed, 146 insertions(+), 7 deletions(-) diff --git a/services/mihomo/mihomo.go b/services/mihomo/mihomo.go index 454354e39..e54fc66fc 100644 --- a/services/mihomo/mihomo.go +++ b/services/mihomo/mihomo.go @@ -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{ @@ -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 } @@ -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, @@ -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() { @@ -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 } @@ -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, @@ -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() { @@ -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 } diff --git a/services/scheduler/speedtest_task.go b/services/scheduler/speedtest_task.go index da9ee8909..d801d119e 100644 --- a/services/scheduler/speedtest_task.go +++ b/services/scheduler/speedtest_task.go @@ -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, @@ -271,6 +283,8 @@ func RunSpeedTestWithConfig(nodes []models.Node, trigger models.TaskTrigger, pro landingIPUrl, detectQualityInLatency, qualityCheckURL, + dialerName, + dialerLink, ) mu.Lock() @@ -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, @@ -531,6 +556,8 @@ func RunSpeedTestWithConfig(nodes []models.Node, trigger models.TaskTrigger, pro qualityCheckURL, speedRecordMode, peakSampleInterval, + dialerName, + dialerLink, ) mu.Lock()