tools: Network Quality & STUN
This commit is contained in:
@@ -6,4 +6,5 @@ const (
|
||||
CommandGroup
|
||||
CommandClashMode
|
||||
CommandConnections
|
||||
CommandOutbounds
|
||||
)
|
||||
|
||||
@@ -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,98 @@ 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, serial bool, maxRuntimeSeconds int32, http3 bool, 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,
|
||||
Http3: http3,
|
||||
})
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler STUNTestHandler) error {
|
||||
client, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.standalone {
|
||||
defer c.closeConnection()
|
||||
}
|
||||
stream, err := client.StartSTUNTest(context.Background(), &daemon.STUNTestRequest{
|
||||
Server: server,
|
||||
OutboundTag: outboundTag,
|
||||
})
|
||||
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(&STUNTestResult{
|
||||
ExternalAddr: event.ExternalAddr,
|
||||
LatencyMs: event.LatencyMs,
|
||||
NATMapping: event.NatMapping,
|
||||
NATFiltering: event.NatFiltering,
|
||||
NATTypeSupported: event.NatTypeSupported,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
handler.OnProgress(stunTestProgressFromGRPC(event))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,6 +339,22 @@ func outboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator
|
||||
return newIterator(libboxGroups)
|
||||
}
|
||||
|
||||
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 connectionFromGRPC(conn *daemon.Connection) Connection {
|
||||
var processInfo *ProcessInfo
|
||||
if conn.ProcessInfo != nil {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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
|
||||
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 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,
|
||||
DownloadCapacityAccuracy: event.DownloadCapacityAccuracy,
|
||||
UploadCapacityAccuracy: event.UploadCapacityAccuracy,
|
||||
DownloadRPMAccuracy: event.DownloadRPMAccuracy,
|
||||
UploadRPMAccuracy: event.UploadRPMAccuracy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package libbox
|
||||
|
||||
import "github.com/sagernet/sing-box/daemon"
|
||||
|
||||
type STUNTestProgress struct {
|
||||
Phase int32
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping int32
|
||||
NATFiltering int32
|
||||
}
|
||||
|
||||
type STUNTestResult struct {
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping int32
|
||||
NATFiltering int32
|
||||
NATTypeSupported bool
|
||||
}
|
||||
|
||||
type STUNTestHandler interface {
|
||||
OnProgress(progress *STUNTestProgress)
|
||||
OnResult(result *STUNTestResult)
|
||||
OnError(message string)
|
||||
}
|
||||
|
||||
func stunTestProgressFromGRPC(event *daemon.STUNTestProgress) *STUNTestProgress {
|
||||
return &STUNTestProgress{
|
||||
Phase: event.Phase,
|
||||
ExternalAddr: event.ExternalAddr,
|
||||
LatencyMs: event.LatencyMs,
|
||||
NATMapping: event.NatMapping,
|
||||
NATFiltering: event.NatFiltering,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) {
|
||||
go func() {
|
||||
httpClient := networkquality.NewHTTPClient(nil)
|
||||
defer httpClient.CloseIdleConnections()
|
||||
|
||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(nil, http3)
|
||||
if err != nil {
|
||||
handler.OnError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := networkquality.Run(networkquality.Options{
|
||||
ConfigURL: configURL,
|
||||
HTTPClient: httpClient,
|
||||
NewMeasurementClient: measurementClientFactory,
|
||||
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()
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/locale"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -129,6 +131,56 @@ func FormatDuration(duration int64) string {
|
||||
return log.FormatDuration(time.Duration(duration) * time.Millisecond)
|
||||
}
|
||||
|
||||
func FormatBitrate(bps int64) string {
|
||||
return networkquality.FormatBitrate(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)
|
||||
)
|
||||
|
||||
const (
|
||||
NetworkQualityPhaseIdle = int32(networkquality.PhaseIdle)
|
||||
NetworkQualityPhaseDownload = int32(networkquality.PhaseDownload)
|
||||
NetworkQualityPhaseUpload = int32(networkquality.PhaseUpload)
|
||||
NetworkQualityPhaseDone = int32(networkquality.PhaseDone)
|
||||
)
|
||||
|
||||
const STUNDefaultServer = stun.DefaultServer
|
||||
|
||||
const (
|
||||
STUNPhaseBinding = int32(stun.PhaseBinding)
|
||||
STUNPhaseNATMapping = int32(stun.PhaseNATMapping)
|
||||
STUNPhaseNATFiltering = int32(stun.PhaseNATFiltering)
|
||||
STUNPhaseDone = int32(stun.PhaseDone)
|
||||
)
|
||||
|
||||
const (
|
||||
NATMappingEndpointIndependent = int32(stun.NATMappingEndpointIndependent)
|
||||
NATMappingAddressDependent = int32(stun.NATMappingAddressDependent)
|
||||
NATMappingAddressAndPortDependent = int32(stun.NATMappingAddressAndPortDependent)
|
||||
)
|
||||
|
||||
const (
|
||||
NATFilteringEndpointIndependent = int32(stun.NATFilteringEndpointIndependent)
|
||||
NATFilteringAddressDependent = int32(stun.NATFilteringAddressDependent)
|
||||
NATFilteringAddressAndPortDependent = int32(stun.NATFilteringAddressAndPortDependent)
|
||||
)
|
||||
|
||||
func FormatNATMapping(value int32) string {
|
||||
return stun.NATMapping(value).String()
|
||||
}
|
||||
|
||||
func FormatNATFiltering(value int32) string {
|
||||
return stun.NATFiltering(value).String()
|
||||
}
|
||||
|
||||
func ProxyDisplayType(proxyType string) string {
|
||||
return C.ProxyDisplayName(proxyType)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
)
|
||||
|
||||
type STUNTest struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewSTUNTest() *STUNTest {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &STUNTest{ctx: ctx, cancel: cancel}
|
||||
}
|
||||
|
||||
func (t *STUNTest) Start(server string, handler STUNTestHandler) {
|
||||
go func() {
|
||||
result, err := stun.Run(stun.Options{
|
||||
Server: server,
|
||||
Context: t.ctx,
|
||||
OnProgress: func(p stun.Progress) {
|
||||
handler.OnProgress(&STUNTestProgress{
|
||||
Phase: int32(p.Phase),
|
||||
ExternalAddr: p.ExternalAddr,
|
||||
LatencyMs: p.LatencyMs,
|
||||
NATMapping: int32(p.NATMapping),
|
||||
NATFiltering: int32(p.NATFiltering),
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
handler.OnError(err.Error())
|
||||
return
|
||||
}
|
||||
handler.OnResult(&STUNTestResult{
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: int32(result.NATMapping),
|
||||
NATFiltering: int32(result.NATFiltering),
|
||||
NATTypeSupported: result.NATTypeSupported,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *STUNTest) Cancel() {
|
||||
t.cancel()
|
||||
}
|
||||
Reference in New Issue
Block a user