tools: Network Quality

This commit is contained in:
世界
2026-04-08 14:44:14 +08:00
parent fcd2b90852
commit f4de7d622a
12 changed files with 2579 additions and 85 deletions

View File

@@ -6,4 +6,5 @@ const (
CommandGroup
CommandClashMode
CommandConnections
CommandOutbounds
)

View File

@@ -47,6 +47,7 @@ type CommandClientHandler interface {
WriteLogs(messageList LogIterator)
WriteStatus(message *StatusMessage)
WriteGroups(message OutboundGroupIterator)
WriteOutbounds(message OutboundGroupItemIterator)
InitializeClashMode(modeList StringIterator, currentMode string)
UpdateClashMode(newMode string)
WriteConnectionEvents(events *ConnectionEvents)
@@ -243,6 +244,8 @@ func (c *CommandClient) dispatchCommands() error {
go c.handleClashModeStream()
case CommandConnections:
go c.handleConnectionsStream()
case CommandOutbounds:
go c.handleOutboundsStream()
default:
return E.New("unknown command: ", command)
}
@@ -456,6 +459,25 @@ func (c *CommandClient) handleConnectionsStream() {
}
}
func (c *CommandClient) handleOutboundsStream() {
client, ctx := c.getStreamContext()
stream, err := client.SubscribeOutbounds(ctx, &emptypb.Empty{})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
for {
list, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteOutbounds(outboundGroupItemListFromGRPC(list))
}
}
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
@@ -603,3 +625,78 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
})
return err
}
func (c *CommandClient) ListOutbounds() (OutboundGroupItemIterator, error) {
return callWithResult(c, func(client daemon.StartedServiceClient) (OutboundGroupItemIterator, error) {
list, err := client.ListOutbounds(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
return outboundGroupItemListFromGRPC(list), nil
})
}
func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag string, handler NetworkQualityTestHandler) error {
return c.StartNetworkQualityTestWithSerialAndRuntime(
configURL,
outboundTag,
false,
NetworkQualityDefaultMaxRuntimeSeconds,
handler,
)
}
func (c *CommandClient) StartNetworkQualityTestWithSerial(configURL string, outboundTag string, serial bool, handler NetworkQualityTestHandler) error {
return c.StartNetworkQualityTestWithSerialAndRuntime(
configURL,
outboundTag,
serial,
NetworkQualityDefaultMaxRuntimeSeconds,
handler,
)
}
func (c *CommandClient) StartNetworkQualityTestWithSerialAndRuntime(configURL string, outboundTag string, serial bool, maxRuntimeSeconds int32, handler NetworkQualityTestHandler) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
if c.standalone {
defer c.closeConnection()
}
stream, err := client.StartNetworkQualityTest(context.Background(), &daemon.NetworkQualityTestRequest{
ConfigURL: configURL,
OutboundTag: outboundTag,
Serial: serial,
MaxRuntimeSeconds: maxRuntimeSeconds,
})
if err != nil {
return err
}
for {
event, recvErr := stream.Recv()
if recvErr != nil {
handler.OnError(recvErr.Error())
return recvErr
}
if event.IsFinal {
if event.Error != "" {
handler.OnError(event.Error)
} else {
handler.OnResult(&NetworkQualityResult{
DownloadCapacity: event.DownloadCapacity,
UploadCapacity: event.UploadCapacity,
DownloadRPM: event.DownloadRPM,
UploadRPM: event.UploadRPM,
IdleLatencyMs: event.IdleLatencyMs,
DownloadCapacityAccuracy: event.DownloadCapacityAccuracy,
UploadCapacityAccuracy: event.UploadCapacityAccuracy,
DownloadRPMAccuracy: event.DownloadRPMAccuracy,
UploadRPMAccuracy: event.UploadRPMAccuracy,
})
}
return nil
}
handler.OnProgress(networkQualityProgressFromGRPC(event))
}
}

View File

@@ -0,0 +1,71 @@
package libbox
import "github.com/sagernet/sing-box/daemon"
type NetworkQualityProgress struct {
Phase int32
DownloadCapacity int64
UploadCapacity int64
DownloadRPM int32
UploadRPM int32
IdleLatencyMs int32
ElapsedMs int64
IsFinal bool
Error string
DownloadCapacityAccuracy int32
UploadCapacityAccuracy int32
DownloadRPMAccuracy int32
UploadRPMAccuracy int32
}
type NetworkQualityResult struct {
DownloadCapacity int64
UploadCapacity int64
DownloadRPM int32
UploadRPM int32
IdleLatencyMs int32
DownloadCapacityAccuracy int32
UploadCapacityAccuracy int32
DownloadRPMAccuracy int32
UploadRPMAccuracy int32
}
type NetworkQualityTestHandler interface {
OnProgress(progress *NetworkQualityProgress)
OnResult(result *NetworkQualityResult)
OnError(message string)
}
func outboundGroupItemListFromGRPC(list *daemon.OutboundList) OutboundGroupItemIterator {
if list == nil || len(list.Outbounds) == 0 {
return newIterator([]*OutboundGroupItem{})
}
var items []*OutboundGroupItem
for _, ob := range list.Outbounds {
items = append(items, &OutboundGroupItem{
Tag: ob.Tag,
Type: ob.Type,
URLTestTime: ob.UrlTestTime,
URLTestDelay: ob.UrlTestDelay,
})
}
return newIterator(items)
}
func networkQualityProgressFromGRPC(event *daemon.NetworkQualityTestProgress) *NetworkQualityProgress {
return &NetworkQualityProgress{
Phase: event.Phase,
DownloadCapacity: event.DownloadCapacity,
UploadCapacity: event.UploadCapacity,
DownloadRPM: event.DownloadRPM,
UploadRPM: event.UploadRPM,
IdleLatencyMs: event.IdleLatencyMs,
ElapsedMs: event.ElapsedMs,
IsFinal: event.IsFinal,
Error: event.Error,
DownloadCapacityAccuracy: event.DownloadCapacityAccuracy,
UploadCapacityAccuracy: event.UploadCapacityAccuracy,
DownloadRPMAccuracy: event.DownloadRPMAccuracy,
UploadRPMAccuracy: event.UploadRPMAccuracy,
}
}

View File

@@ -0,0 +1,75 @@
package libbox
import (
"context"
"time"
"github.com/sagernet/sing-box/common/networkquality"
)
type NetworkQualityTest struct {
ctx context.Context
cancel context.CancelFunc
}
func NewNetworkQualityTest() *NetworkQualityTest {
ctx, cancel := context.WithCancel(context.Background())
return &NetworkQualityTest{ctx: ctx, cancel: cancel}
}
func (t *NetworkQualityTest) Start(configURL string, handler NetworkQualityTestHandler) {
t.StartWithSerialAndRuntime(configURL, false, NetworkQualityDefaultMaxRuntimeSeconds, handler)
}
func (t *NetworkQualityTest) StartWithSerial(configURL string, serial bool, handler NetworkQualityTestHandler) {
t.StartWithSerialAndRuntime(configURL, serial, NetworkQualityDefaultMaxRuntimeSeconds, handler)
}
func (t *NetworkQualityTest) StartWithSerialAndRuntime(configURL string, serial bool, maxRuntimeSeconds int32, handler NetworkQualityTestHandler) {
go func() {
httpClient := networkquality.NewHTTPClient(nil)
defer httpClient.CloseIdleConnections()
result, err := networkquality.Run(networkquality.Options{
ConfigURL: configURL,
HTTPClient: httpClient,
Serial: serial,
MaxRuntime: time.Duration(maxRuntimeSeconds) * time.Second,
Context: t.ctx,
OnProgress: func(p networkquality.Progress) {
handler.OnProgress(&NetworkQualityProgress{
Phase: int32(p.Phase),
DownloadCapacity: p.DownloadCapacity,
UploadCapacity: p.UploadCapacity,
DownloadRPM: p.DownloadRPM,
UploadRPM: p.UploadRPM,
IdleLatencyMs: p.IdleLatencyMs,
ElapsedMs: p.ElapsedMs,
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
})
},
})
if err != nil {
handler.OnError(err.Error())
return
}
handler.OnResult(&NetworkQualityResult{
DownloadCapacity: result.DownloadCapacity,
UploadCapacity: result.UploadCapacity,
DownloadRPM: result.DownloadRPM,
UploadRPM: result.UploadRPM,
IdleLatencyMs: result.IdleLatencyMs,
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
})
}()
}
func (t *NetworkQualityTest) Cancel() {
t.cancel()
}

View File

@@ -1,6 +1,7 @@
package libbox
import (
"fmt"
"math"
"os"
"path/filepath"
@@ -9,6 +10,7 @@ import (
"strings"
"time"
"github.com/sagernet/sing-box/common/networkquality"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing-box/log"
@@ -129,6 +131,29 @@ func FormatDuration(duration int64) string {
return log.FormatDuration(time.Duration(duration) * time.Millisecond)
}
func FormatBitrate(bps int64) string {
switch {
case bps >= 1_000_000_000:
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
case bps >= 1_000_000:
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
case bps >= 1_000:
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
default:
return fmt.Sprintf("%d bps", bps)
}
}
const NetworkQualityDefaultConfigURL = networkquality.DefaultConfigURL
const NetworkQualityDefaultMaxRuntimeSeconds = int32(networkquality.DefaultMaxRuntime / time.Second)
const (
NetworkQualityAccuracyLow = int32(networkquality.AccuracyLow)
NetworkQualityAccuracyMedium = int32(networkquality.AccuracyMedium)
NetworkQualityAccuracyHigh = int32(networkquality.AccuracyHigh)
)
func ProxyDisplayType(proxyType string) string {
return C.ProxyDisplayName(proxyType)
}