Compare commits
10 commits
f7779ccea2
...
a3404e463f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3404e463f | ||
|
|
17852ccaaa | ||
|
|
03572bc845 | ||
|
|
a9fe3f386e | ||
|
|
1933c19064 | ||
|
|
5de7c678e1 | ||
|
|
82476702ea | ||
|
|
326615d1a3 | ||
|
|
fc25cedc25 | ||
|
|
c688b61c3b |
78 changed files with 6103 additions and 419 deletions
|
|
@ -46,7 +46,6 @@ type ConnectionRouterEx interface {
|
|||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
IncRef()
|
||||
|
|
|
|||
15
adapter/usbip.go
Normal file
15
adapter/usbip.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-usbip"
|
||||
)
|
||||
|
||||
type USBIPDynamicServer interface {
|
||||
AddDevice(info usbip.ProvidedDeviceInfo, transport usbip.DeviceTransport) (string, error)
|
||||
RemoveDevice(busID string)
|
||||
SubscribeDevices(ctx context.Context, listener func([]usbip.ControlDeviceInfo))
|
||||
}
|
||||
7
adapter/usbip_stub.go
Normal file
7
adapter/usbip_stub.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
|
||||
|
||||
package adapter
|
||||
|
||||
type USBIPDynamicServer interface {
|
||||
usbipNotIncluded()
|
||||
}
|
||||
23
adapter/user_manager.go
Normal file
23
adapter/user_manager.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package adapter
|
||||
|
||||
import "context"
|
||||
|
||||
type UserTraffic struct {
|
||||
Tx int64 `json:"tx"`
|
||||
Rx int64 `json:"rx"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
Username string
|
||||
Credential string
|
||||
}
|
||||
|
||||
type UserManager interface {
|
||||
Service
|
||||
Authenticate(ctx context.Context, protocol string, credential string, addr string) (username string, err error)
|
||||
GetCredentials(protocol string) ([]UserInfo, error)
|
||||
ReportTraffic(username string, tx int64, rx int64)
|
||||
GetTraffic(username string) (tx int64, rx int64)
|
||||
ListTraffic() map[string]UserTraffic
|
||||
KickUser(username string)
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func init() {
|
|||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_usbip", "badlinkname", "tfogo_checklinkname0")
|
||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
||||
// memcTags = append(memcTags, "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
||||
|
|
|
|||
|
|
@ -33,9 +33,12 @@ const (
|
|||
TypeCCM = "ccm"
|
||||
TypeOCM = "ocm"
|
||||
TypeOOMKiller = "oom-killer"
|
||||
TypeUSBIPServer = "usbip-server"
|
||||
TypeUSBIPClient = "usbip-client"
|
||||
TypeHysteriaRealm = "hysteria-realm"
|
||||
TypeACME = "acme"
|
||||
TypeCloudflareOriginCA = "cloudflare-origin-ca"
|
||||
TypeUserManager = "user_manager"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import (
|
|||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
|
@ -13,19 +16,22 @@ import (
|
|||
var _ ManagedServiceServer = (*ManagedService)(nil)
|
||||
|
||||
type ManagedService struct {
|
||||
handler ManagedHandler
|
||||
debug bool
|
||||
handler ManagedHandler
|
||||
debug bool
|
||||
oomReporter oomkiller.OOMReporter
|
||||
}
|
||||
|
||||
type ManagedServiceOptions struct {
|
||||
Handler ManagedHandler
|
||||
Debug bool
|
||||
Handler ManagedHandler
|
||||
Debug bool
|
||||
OOMReporter oomkiller.OOMReporter
|
||||
}
|
||||
|
||||
func NewManagedService(options ManagedServiceOptions) *ManagedService {
|
||||
return &ManagedService{
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
oomReporter: options.OOMReporter,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,5 +86,12 @@ func (s *ManagedService) TriggerDebugCrash(ctx context.Context, request *DebugCr
|
|||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *ManagedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
if s.oomReporter == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
||||
}
|
||||
return &emptypb.Empty{}, s.oomReporter.WriteReport(memory.Total())
|
||||
}
|
||||
|
||||
func (s *ManagedService) mustEmbedUnimplementedManagedServiceServer() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,13 +218,14 @@ const file_daemon_managed_service_proto_rawDesc = "" +
|
|||
"\x04Type\x12\x06\n" +
|
||||
"\x02GO\x10\x00\x12\n" +
|
||||
"\n" +
|
||||
"\x06NATIVE\x10\x012\x80\x03\n" +
|
||||
"\x06NATIVE\x10\x012\xc6\x03\n" +
|
||||
"\x0eManagedService\x12=\n" +
|
||||
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
|
||||
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
|
||||
"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
|
||||
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n" +
|
||||
"\x11TriggerDebugCrash\x12\x19.daemon.DebugCrashRequest\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
|
||||
"\x11TriggerDebugCrash\x12\x19.daemon.DebugCrashRequest\x1a\x16.google.protobuf.Empty\"\x00\x12D\n" +
|
||||
"\x10TriggerOOMReport\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
|
||||
|
||||
var (
|
||||
file_daemon_managed_service_proto_rawDescOnce sync.Once
|
||||
|
|
@ -257,13 +258,15 @@ var file_daemon_managed_service_proto_depIdxs = []int32{
|
|||
4, // 3: daemon.ManagedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
|
||||
2, // 4: daemon.ManagedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
|
||||
3, // 5: daemon.ManagedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest
|
||||
4, // 6: daemon.ManagedService.StopService:output_type -> google.protobuf.Empty
|
||||
4, // 7: daemon.ManagedService.ReloadService:output_type -> google.protobuf.Empty
|
||||
1, // 8: daemon.ManagedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
|
||||
4, // 9: daemon.ManagedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
|
||||
4, // 10: daemon.ManagedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
|
||||
6, // [6:11] is the sub-list for method output_type
|
||||
1, // [1:6] is the sub-list for method input_type
|
||||
4, // 6: daemon.ManagedService.TriggerOOMReport:input_type -> google.protobuf.Empty
|
||||
4, // 7: daemon.ManagedService.StopService:output_type -> google.protobuf.Empty
|
||||
4, // 8: daemon.ManagedService.ReloadService:output_type -> google.protobuf.Empty
|
||||
1, // 9: daemon.ManagedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
|
||||
4, // 10: daemon.ManagedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
|
||||
4, // 11: daemon.ManagedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
|
||||
4, // 12: daemon.ManagedService.TriggerOOMReport:output_type -> google.protobuf.Empty
|
||||
7, // [7:13] is the sub-list for method output_type
|
||||
1, // [1:7] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ service ManagedService {
|
|||
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
|
||||
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
|
||||
rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {}
|
||||
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||
}
|
||||
|
||||
message SystemProxyStatus {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const (
|
|||
ManagedService_GetSystemProxyStatus_FullMethodName = "/daemon.ManagedService/GetSystemProxyStatus"
|
||||
ManagedService_SetSystemProxyEnabled_FullMethodName = "/daemon.ManagedService/SetSystemProxyEnabled"
|
||||
ManagedService_TriggerDebugCrash_FullMethodName = "/daemon.ManagedService/TriggerDebugCrash"
|
||||
ManagedService_TriggerOOMReport_FullMethodName = "/daemon.ManagedService/TriggerOOMReport"
|
||||
)
|
||||
|
||||
// ManagedServiceClient is the client API for ManagedService service.
|
||||
|
|
@ -31,6 +32,7 @@ type ManagedServiceClient interface {
|
|||
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
}
|
||||
|
||||
type managedServiceClient struct {
|
||||
|
|
@ -91,6 +93,16 @@ func (c *managedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugC
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *managedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, ManagedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ManagedServiceServer is the server API for ManagedService service.
|
||||
// All implementations must embed UnimplementedManagedServiceServer
|
||||
// for forward compatibility.
|
||||
|
|
@ -100,6 +112,7 @@ type ManagedServiceServer interface {
|
|||
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
|
||||
TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error)
|
||||
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
mustEmbedUnimplementedManagedServiceServer()
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +142,10 @@ func (UnimplementedManagedServiceServer) SetSystemProxyEnabled(context.Context,
|
|||
func (UnimplementedManagedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedManagedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
|
||||
}
|
||||
func (UnimplementedManagedServiceServer) mustEmbedUnimplementedManagedServiceServer() {}
|
||||
func (UnimplementedManagedServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
|
|
@ -240,6 +257,24 @@ func _ManagedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Cont
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ManagedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ManagedServiceServer).TriggerOOMReport(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ManagedService_TriggerOOMReport_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ManagedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ManagedService_ServiceDesc is the grpc.ServiceDesc for ManagedService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
|
|
@ -267,6 +302,10 @@ var ManagedService_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "TriggerDebugCrash",
|
||||
Handler: _ManagedService_TriggerDebugCrash_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "TriggerOOMReport",
|
||||
Handler: _ManagedService_TriggerOOMReport_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "daemon/managed_service.proto",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import (
|
|||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/protocol/group"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/batch"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
|
|
@ -32,7 +31,7 @@ import (
|
|||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const APIVersion = 1
|
||||
const APIVersion = 2
|
||||
|
||||
var _ StartedServiceServer = (*StartedService)(nil)
|
||||
|
||||
|
|
@ -510,7 +509,7 @@ func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.
|
|||
clashServer := s.instance.clashServer
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
return nil, status.Error(codes.Unimplemented, "clash mode not available")
|
||||
return nil, status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
return &ClashModeStatus{
|
||||
ModeList: clashServer.ModeList(),
|
||||
|
|
@ -537,7 +536,7 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
|
|||
clashServer := s.instance.clashServer
|
||||
if clashServer == nil {
|
||||
s.serviceAccess.RUnlock()
|
||||
return status.Error(codes.Unimplemented, "clash mode not available")
|
||||
return status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
message := &ClashMode{Mode: clashServer.Mode()}
|
||||
s.serviceAccess.RUnlock()
|
||||
|
|
@ -566,7 +565,7 @@ func (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (
|
|||
clashServer := s.instance.clashServer
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
return nil, status.Error(codes.Unimplemented, "clash mode not available")
|
||||
return nil, status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
clashServer.SetMode(request.Mode)
|
||||
return &emptypb.Empty{}, nil
|
||||
|
|
@ -669,18 +668,6 @@ func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupEx
|
|||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
instance := s.Instance()
|
||||
if instance == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "service not started")
|
||||
}
|
||||
reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx)
|
||||
if reporter == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
||||
}
|
||||
return &emptypb.Empty{}, reporter.WriteReport(memory.Total())
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -22,8 +22,6 @@ service StartedService {
|
|||
rpc SelectOutbound(SelectOutboundRequest) returns (google.protobuf.Empty) {}
|
||||
rpc SetGroupExpand(SetGroupExpandRequest) returns (google.protobuf.Empty) {}
|
||||
|
||||
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||
|
||||
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
|
||||
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
|
||||
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||
|
|
@ -38,6 +36,8 @@ service StartedService {
|
|||
rpc SetTailscaleExitNode(SetTailscaleExitNodeRequest) returns (google.protobuf.Empty) {}
|
||||
rpc TailscaleLogout(TailscaleLogoutRequest) returns (google.protobuf.Empty) {}
|
||||
rpc StartTailscaleSSHSession(stream TailscaleSSHClientMessage) returns (stream TailscaleSSHServerMessage) {}
|
||||
rpc ProvideUSBDevices(stream USBProviderMessage) returns (stream USBServerMessage) {}
|
||||
rpc SubscribeUSBIPServerStatus(google.protobuf.Empty) returns (stream USBIPServerStatusUpdate) {}
|
||||
}
|
||||
|
||||
message Version {
|
||||
|
|
@ -389,3 +389,130 @@ message TailscaleSSHExit {
|
|||
message TailscaleSSHError {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message USBProviderMessage {
|
||||
oneof message {
|
||||
USBDeviceAttach attach = 1;
|
||||
USBDeviceDetach detach = 2;
|
||||
USBURBResponse urbResponse = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message USBServerMessage {
|
||||
oneof message {
|
||||
USBDeviceReady ready = 1;
|
||||
USBURBRequest urbRequest = 2;
|
||||
USBEndpointAbort abort = 3;
|
||||
USBError error = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message USBDeviceDescriptor {
|
||||
string deviceId = 1;
|
||||
uint32 busNum = 2;
|
||||
uint32 devNum = 3;
|
||||
uint32 speed = 4;
|
||||
uint32 vendorId = 5;
|
||||
uint32 productId = 6;
|
||||
uint32 bcdDevice = 7;
|
||||
uint32 deviceClass = 8;
|
||||
uint32 deviceSubClass = 9;
|
||||
uint32 deviceProtocol = 10;
|
||||
uint32 configurationValue = 11;
|
||||
uint32 numConfigurations = 12;
|
||||
repeated USBInterface interfaces = 13;
|
||||
string serial = 14;
|
||||
string product = 15;
|
||||
}
|
||||
|
||||
message USBDeviceAttach {
|
||||
string serverTag = 1;
|
||||
USBDeviceDescriptor descriptor = 2;
|
||||
}
|
||||
|
||||
message USBInterface {
|
||||
uint32 interfaceClass = 1;
|
||||
uint32 interfaceSubClass = 2;
|
||||
uint32 interfaceProtocol = 3;
|
||||
}
|
||||
|
||||
message USBDeviceDetach {
|
||||
string deviceId = 1;
|
||||
}
|
||||
|
||||
message USBDeviceReady {
|
||||
string deviceId = 1;
|
||||
string busId = 2;
|
||||
}
|
||||
|
||||
message USBURBRequest {
|
||||
string deviceId = 1;
|
||||
uint64 seq = 2;
|
||||
uint32 endpoint = 3;
|
||||
bool directionIn = 4;
|
||||
uint32 transferFlags = 5;
|
||||
bytes setup = 6;
|
||||
uint32 transferBufferLength = 7;
|
||||
bytes outData = 8;
|
||||
int32 numberOfPackets = 9;
|
||||
int32 startFrame = 10;
|
||||
int32 interval = 11;
|
||||
repeated USBIsoPacket isoPackets = 12;
|
||||
}
|
||||
|
||||
message USBURBResponse {
|
||||
string deviceId = 1;
|
||||
uint64 seq = 2;
|
||||
int32 status = 3;
|
||||
int32 actualLength = 4;
|
||||
bytes inData = 5;
|
||||
repeated USBIsoPacket isoPackets = 6;
|
||||
}
|
||||
|
||||
message USBIsoPacket {
|
||||
int32 offset = 1;
|
||||
int32 length = 2;
|
||||
int32 actualLength = 3;
|
||||
int32 status = 4;
|
||||
}
|
||||
|
||||
message USBEndpointAbort {
|
||||
string deviceId = 1;
|
||||
uint32 endpoint = 2;
|
||||
}
|
||||
|
||||
message USBError {
|
||||
string deviceId = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message USBIPServerStatusUpdate {
|
||||
repeated USBIPServerStatus servers = 1;
|
||||
}
|
||||
|
||||
message USBIPServerStatus {
|
||||
string serverTag = 1;
|
||||
repeated USBSharedDevice devices = 2;
|
||||
}
|
||||
|
||||
message USBSharedDevice {
|
||||
USBDeviceDescriptor descriptor = 1;
|
||||
string busId = 2;
|
||||
string stableId = 3;
|
||||
USBBackend backend = 4;
|
||||
USBDeviceState state = 5;
|
||||
}
|
||||
|
||||
enum USBDeviceState {
|
||||
USB_DEVICE_STATE_IDLE = 0;
|
||||
USB_DEVICE_STATE_ATTACHED = 1;
|
||||
USB_DEVICE_STATE_UNAVAILABLE = 2;
|
||||
}
|
||||
|
||||
enum USBBackend {
|
||||
USB_BACKEND_UNSPECIFIED = 0;
|
||||
USB_BACKEND_LINUX_SYSFS = 1;
|
||||
USB_BACKEND_DYNAMIC = 2;
|
||||
USB_BACKEND_DARWIN_IOKIT = 3;
|
||||
USB_BACKEND_WINDOWS_VBOXUSB = 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,33 +15,34 @@ import (
|
|||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
StartedService_GetVersion_FullMethodName = "/daemon.StartedService/GetVersion"
|
||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
|
||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
||||
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
|
||||
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
|
||||
StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest"
|
||||
StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus"
|
||||
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
|
||||
StartedService_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode"
|
||||
StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout"
|
||||
StartedService_StartTailscaleSSHSession_FullMethodName = "/daemon.StartedService/StartTailscaleSSHSession"
|
||||
StartedService_GetVersion_FullMethodName = "/daemon.StartedService/GetVersion"
|
||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
||||
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
|
||||
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
|
||||
StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest"
|
||||
StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus"
|
||||
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
|
||||
StartedService_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode"
|
||||
StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout"
|
||||
StartedService_StartTailscaleSSHSession_FullMethodName = "/daemon.StartedService/StartTailscaleSSHSession"
|
||||
StartedService_ProvideUSBDevices_FullMethodName = "/daemon.StartedService/ProvideUSBDevices"
|
||||
StartedService_SubscribeUSBIPServerStatus_FullMethodName = "/daemon.StartedService/SubscribeUSBIPServerStatus"
|
||||
)
|
||||
|
||||
// StartedServiceClient is the client API for StartedService service.
|
||||
|
|
@ -61,7 +62,6 @@ type StartedServiceClient interface {
|
|||
URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
|
||||
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
|
|
@ -75,6 +75,8 @@ type StartedServiceClient interface {
|
|||
SetTailscaleExitNode(ctx context.Context, in *SetTailscaleExitNodeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
TailscaleLogout(ctx context.Context, in *TailscaleLogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
StartTailscaleSSHSession(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TailscaleSSHClientMessage, TailscaleSSHServerMessage], error)
|
||||
ProvideUSBDevices(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[USBProviderMessage, USBServerMessage], error)
|
||||
SubscribeUSBIPServerStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[USBIPServerStatusUpdate], error)
|
||||
}
|
||||
|
||||
type startedServiceClient struct {
|
||||
|
|
@ -260,16 +262,6 @@ func (c *startedServiceClient) SetGroupExpand(ctx context.Context, in *SetGroupE
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
|
||||
|
|
@ -457,6 +449,38 @@ func (c *startedServiceClient) StartTailscaleSSHSession(ctx context.Context, opt
|
|||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartTailscaleSSHSessionClient = grpc.BidiStreamingClient[TailscaleSSHClientMessage, TailscaleSSHServerMessage]
|
||||
|
||||
func (c *startedServiceClient) ProvideUSBDevices(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[USBProviderMessage, USBServerMessage], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[12], StartedService_ProvideUSBDevices_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[USBProviderMessage, USBServerMessage]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_ProvideUSBDevicesClient = grpc.BidiStreamingClient[USBProviderMessage, USBServerMessage]
|
||||
|
||||
func (c *startedServiceClient) SubscribeUSBIPServerStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[USBIPServerStatusUpdate], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[13], StartedService_SubscribeUSBIPServerStatus_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[emptypb.Empty, USBIPServerStatusUpdate]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeUSBIPServerStatusClient = grpc.ServerStreamingClient[USBIPServerStatusUpdate]
|
||||
|
||||
// StartedServiceServer is the server API for StartedService service.
|
||||
// All implementations must embed UnimplementedStartedServiceServer
|
||||
// for forward compatibility.
|
||||
|
|
@ -474,7 +498,6 @@ type StartedServiceServer interface {
|
|||
URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error)
|
||||
SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error)
|
||||
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
|
||||
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
|
||||
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
|
||||
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
|
|
@ -488,6 +511,8 @@ type StartedServiceServer interface {
|
|||
SetTailscaleExitNode(context.Context, *SetTailscaleExitNodeRequest) (*emptypb.Empty, error)
|
||||
TailscaleLogout(context.Context, *TailscaleLogoutRequest) (*emptypb.Empty, error)
|
||||
StartTailscaleSSHSession(grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]) error
|
||||
ProvideUSBDevices(grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error
|
||||
SubscribeUSBIPServerStatus(*emptypb.Empty, grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error
|
||||
mustEmbedUnimplementedStartedServiceServer()
|
||||
}
|
||||
|
||||
|
|
@ -550,10 +575,6 @@ func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGro
|
|||
return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
|
||||
}
|
||||
|
|
@ -605,6 +626,14 @@ func (UnimplementedStartedServiceServer) TailscaleLogout(context.Context, *Tails
|
|||
func (UnimplementedStartedServiceServer) StartTailscaleSSHSession(grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartTailscaleSSHSession not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) ProvideUSBDevices(grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error {
|
||||
return status.Error(codes.Unimplemented, "method ProvideUSBDevices not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeUSBIPServerStatus(*emptypb.Empty, grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeUSBIPServerStatus not implemented")
|
||||
}
|
||||
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
|
||||
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
|
|
@ -825,24 +854,6 @@ func _StartedService_SetGroupExpand_Handler(srv interface{}, ctx context.Context
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StartedServiceServer).TriggerOOMReport(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: StartedService_TriggerOOMReport_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(SubscribeConnectionsRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
|
|
@ -1024,6 +1035,24 @@ func _StartedService_StartTailscaleSSHSession_Handler(srv interface{}, stream gr
|
|||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartTailscaleSSHSessionServer = grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]
|
||||
|
||||
func _StartedService_ProvideUSBDevices_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(StartedServiceServer).ProvideUSBDevices(&grpc.GenericServerStream[USBProviderMessage, USBServerMessage]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_ProvideUSBDevicesServer = grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]
|
||||
|
||||
func _StartedService_SubscribeUSBIPServerStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(emptypb.Empty)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).SubscribeUSBIPServerStatus(m, &grpc.GenericServerStream[emptypb.Empty, USBIPServerStatusUpdate]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeUSBIPServerStatusServer = grpc.ServerStreamingServer[USBIPServerStatusUpdate]
|
||||
|
||||
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
|
|
@ -1063,10 +1092,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "SetGroupExpand",
|
||||
Handler: _StartedService_SetGroupExpand_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "TriggerOOMReport",
|
||||
Handler: _StartedService_TriggerOOMReport_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CloseConnection",
|
||||
Handler: _StartedService_CloseConnection_Handler,
|
||||
|
|
@ -1154,6 +1179,17 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
|||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "ProvideUSBDevices",
|
||||
Handler: _StartedService_ProvideUSBDevices_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "SubscribeUSBIPServerStatus",
|
||||
Handler: _StartedService_SubscribeUSBIPServerStatus_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "daemon/started_service.proto",
|
||||
}
|
||||
|
|
|
|||
470
daemon/started_service_usbip.go
Normal file
470
daemon/started_service_usbip.go
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-usbip"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func (s *StartedService) ProvideUSBDevices(server grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error {
|
||||
ctx := server.Context()
|
||||
err := s.waitForStarted(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
instance := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
if instance == nil {
|
||||
return E.New("service not started")
|
||||
}
|
||||
serviceManager := service.FromContext[adapter.ServiceManager](instance.ctx)
|
||||
if serviceManager == nil {
|
||||
return E.New("missing service manager")
|
||||
}
|
||||
|
||||
sessionCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var sendAccess sync.Mutex
|
||||
send := func(message *USBServerMessage) error {
|
||||
sendAccess.Lock()
|
||||
defer sendAccess.Unlock()
|
||||
return server.Send(message)
|
||||
}
|
||||
|
||||
var devicesAccess sync.Mutex
|
||||
devices := make(map[string]*usbProvidedDevice)
|
||||
defer func() {
|
||||
devicesAccess.Lock()
|
||||
for _, device := range devices {
|
||||
device.close()
|
||||
}
|
||||
devicesAccess.Unlock()
|
||||
}()
|
||||
|
||||
for {
|
||||
message, recvErr := server.Recv()
|
||||
if recvErr != nil {
|
||||
if recvErr == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return recvErr
|
||||
}
|
||||
switch body := message.GetMessage().(type) {
|
||||
case *USBProviderMessage_Attach:
|
||||
attach := body.Attach
|
||||
deviceID := attach.GetDescriptor_().GetDeviceId()
|
||||
device, addErr := addUSBDevice(sessionCtx, serviceManager, send, attach)
|
||||
if addErr != nil {
|
||||
_ = send(&USBServerMessage{Message: &USBServerMessage_Error{Error: &USBError{
|
||||
DeviceId: deviceID,
|
||||
Message: addErr.Error(),
|
||||
}}})
|
||||
continue
|
||||
}
|
||||
devicesAccess.Lock()
|
||||
previous, replaced := devices[deviceID]
|
||||
devices[deviceID] = device
|
||||
devicesAccess.Unlock()
|
||||
if replaced {
|
||||
previous.close()
|
||||
}
|
||||
_ = send(&USBServerMessage{Message: &USBServerMessage_Ready{Ready: &USBDeviceReady{
|
||||
DeviceId: deviceID,
|
||||
BusId: device.busID,
|
||||
}}})
|
||||
case *USBProviderMessage_Detach:
|
||||
deviceID := body.Detach.GetDeviceId()
|
||||
devicesAccess.Lock()
|
||||
device, found := devices[deviceID]
|
||||
if found {
|
||||
delete(devices, deviceID)
|
||||
}
|
||||
devicesAccess.Unlock()
|
||||
if found {
|
||||
device.close()
|
||||
}
|
||||
case *USBProviderMessage_UrbResponse:
|
||||
response := body.UrbResponse
|
||||
devicesAccess.Lock()
|
||||
device, found := devices[response.GetDeviceId()]
|
||||
devicesAccess.Unlock()
|
||||
if found {
|
||||
device.deliver(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeUSBIPServerStatus(
|
||||
_ *emptypb.Empty,
|
||||
server grpc.ServerStreamingServer[USBIPServerStatusUpdate],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
instance := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
if instance == nil {
|
||||
return E.New("service not started")
|
||||
}
|
||||
serviceManager := service.FromContext[adapter.ServiceManager](instance.ctx)
|
||||
if serviceManager == nil {
|
||||
return status.Error(codes.FailedPrecondition, "service manager not available")
|
||||
}
|
||||
|
||||
type usbipServer struct {
|
||||
tag string
|
||||
provider adapter.USBIPDynamicServer
|
||||
}
|
||||
var servers []usbipServer
|
||||
for _, serverService := range serviceManager.Services() {
|
||||
provider, isDynamic := serverService.(adapter.USBIPDynamicServer)
|
||||
if !isDynamic {
|
||||
continue
|
||||
}
|
||||
servers = append(servers, usbipServer{tag: serverService.Tag(), provider: provider})
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
return status.Error(codes.NotFound, "no usbip-server found")
|
||||
}
|
||||
|
||||
type taggedStatus struct {
|
||||
tag string
|
||||
devices []usbip.ControlDeviceInfo
|
||||
}
|
||||
updates := make(chan taggedStatus, len(servers))
|
||||
ctx, cancel := context.WithCancel(server.Context())
|
||||
defer cancel()
|
||||
|
||||
var waitGroup sync.WaitGroup
|
||||
for _, srv := range servers {
|
||||
// sing-usbip invokes the SubscribeDevices listener while holding the
|
||||
// ledger's broadcast lock, so it must never block.
|
||||
latest := make(chan []usbip.ControlDeviceInfo, 1)
|
||||
waitGroup.Add(1)
|
||||
go func(provider adapter.USBIPDynamicServer) {
|
||||
defer waitGroup.Done()
|
||||
provider.SubscribeDevices(ctx, func(devices []usbip.ControlDeviceInfo) {
|
||||
sendLatestUSBSnapshot(latest, devices)
|
||||
})
|
||||
}(srv.provider)
|
||||
waitGroup.Add(1)
|
||||
go func(tag string) {
|
||||
defer waitGroup.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case devices := <-latest:
|
||||
select {
|
||||
case updates <- taggedStatus{tag: tag, devices: devices}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}(srv.tag)
|
||||
}
|
||||
|
||||
go func() {
|
||||
waitGroup.Wait()
|
||||
close(updates)
|
||||
}()
|
||||
|
||||
var tags []string
|
||||
deviceStates := make(map[string][]usbip.ControlDeviceInfo, len(servers))
|
||||
for update := range updates {
|
||||
if _, exists := deviceStates[update.tag]; !exists {
|
||||
tags = append(tags, update.tag)
|
||||
}
|
||||
deviceStates[update.tag] = update.devices
|
||||
protoServers := make([]*USBIPServerStatus, 0, len(deviceStates))
|
||||
for _, tag := range tags {
|
||||
protoServers = append(protoServers, &USBIPServerStatus{
|
||||
ServerTag: tag,
|
||||
Devices: usbSharedDevicesToProto(deviceStates[tag]),
|
||||
})
|
||||
}
|
||||
sendErr := server.Send(&USBIPServerStatusUpdate{Servers: protoServers})
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendLatestUSBSnapshot(slot chan []usbip.ControlDeviceInfo, devices []usbip.ControlDeviceInfo) {
|
||||
select {
|
||||
case slot <- devices:
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-slot:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case slot <- devices:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func usbSharedDevicesToProto(devices []usbip.ControlDeviceInfo) []*USBSharedDevice {
|
||||
if len(devices) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*USBSharedDevice, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
interfaces := make([]*USBInterface, 0, len(device.Interfaces))
|
||||
for _, deviceInterface := range device.Interfaces {
|
||||
interfaces = append(interfaces, &USBInterface{
|
||||
InterfaceClass: uint32(deviceInterface.Class),
|
||||
InterfaceSubClass: uint32(deviceInterface.SubClass),
|
||||
InterfaceProtocol: uint32(deviceInterface.Protocol),
|
||||
})
|
||||
}
|
||||
out = append(out, &USBSharedDevice{
|
||||
Descriptor_: &USBDeviceDescriptor{
|
||||
DeviceId: device.BusID,
|
||||
BusNum: device.BusNum,
|
||||
DevNum: device.DevNum,
|
||||
Speed: device.Speed,
|
||||
VendorId: uint32(device.VendorID),
|
||||
ProductId: uint32(device.ProductID),
|
||||
BcdDevice: uint32(device.BCDDevice),
|
||||
DeviceClass: uint32(device.DeviceClass),
|
||||
DeviceSubClass: uint32(device.DeviceSubClass),
|
||||
DeviceProtocol: uint32(device.DeviceProtocol),
|
||||
ConfigurationValue: uint32(device.ConfigurationValue),
|
||||
NumConfigurations: uint32(device.NumConfigurations),
|
||||
Interfaces: interfaces,
|
||||
Serial: device.Serial,
|
||||
Product: device.Product,
|
||||
},
|
||||
BusId: device.BusID,
|
||||
StableId: device.StableID,
|
||||
Backend: USBBackend(device.Backend),
|
||||
State: USBDeviceState(device.State),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func addUSBDevice(ctx context.Context, serviceManager adapter.ServiceManager, send func(*USBServerMessage) error, attach *USBDeviceAttach) (*usbProvidedDevice, error) {
|
||||
serverService, found := serviceManager.Get(attach.GetServerTag())
|
||||
if !found {
|
||||
return nil, E.New("usbip-server not found: ", attach.GetServerTag())
|
||||
}
|
||||
provider, isDynamic := serverService.(adapter.USBIPDynamicServer)
|
||||
if !isDynamic {
|
||||
return nil, E.New("service ", attach.GetServerTag(), " is not a dynamic usbip-server")
|
||||
}
|
||||
descriptor := attach.GetDescriptor_()
|
||||
if descriptor == nil {
|
||||
return nil, E.New("missing device descriptor")
|
||||
}
|
||||
device := &usbProvidedDevice{
|
||||
deviceID: descriptor.GetDeviceId(),
|
||||
provider: provider,
|
||||
send: send,
|
||||
ctx: ctx,
|
||||
pending: make(map[uint64]chan *USBURBResponse),
|
||||
}
|
||||
entry := usbDeviceEntryFromDescriptor(descriptor)
|
||||
busID, err := provider.AddDevice(usbip.ProvidedDeviceInfo{Entry: entry}, device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
device.busID = busID
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func usbDeviceEntryFromDescriptor(descriptor *USBDeviceDescriptor) usbip.DeviceEntry {
|
||||
deviceID := descriptor.GetDeviceId()
|
||||
interfaces := usbInterfacesFromProto(descriptor.GetInterfaces())
|
||||
info := usbip.DeviceInfoTruncated{
|
||||
BusNum: descriptor.GetBusNum(),
|
||||
DevNum: descriptor.GetDevNum(),
|
||||
Speed: descriptor.GetSpeed(),
|
||||
IDVendor: uint16(descriptor.GetVendorId()),
|
||||
IDProduct: uint16(descriptor.GetProductId()),
|
||||
BCDDevice: uint16(descriptor.GetBcdDevice()),
|
||||
BDeviceClass: uint8(descriptor.GetDeviceClass()),
|
||||
BDeviceSubClass: uint8(descriptor.GetDeviceSubClass()),
|
||||
BDeviceProtocol: uint8(descriptor.GetDeviceProtocol()),
|
||||
BConfigurationValue: uint8(descriptor.GetConfigurationValue()),
|
||||
BNumConfigurations: uint8(descriptor.GetNumConfigurations()),
|
||||
BNumInterfaces: uint8(len(interfaces)),
|
||||
}
|
||||
copy(info.BusID[:], deviceID)
|
||||
return usbip.DeviceEntry{
|
||||
Info: info,
|
||||
Interfaces: interfaces,
|
||||
Serial: descriptor.GetSerial(),
|
||||
Product: descriptor.GetProduct(),
|
||||
}
|
||||
}
|
||||
|
||||
// sing-usbip calls Submit concurrently across endpoints for a single device.
|
||||
type usbProvidedDevice struct {
|
||||
deviceID string
|
||||
busID string
|
||||
provider adapter.USBIPDynamicServer
|
||||
send func(*USBServerMessage) error
|
||||
ctx context.Context
|
||||
|
||||
seq atomic.Uint64
|
||||
access sync.Mutex
|
||||
pending map[uint64]chan *USBURBResponse
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (d *usbProvidedDevice) Submit(request usbip.URBRequest) usbip.URBResponse {
|
||||
seq := d.seq.Add(1)
|
||||
responseChan := make(chan *USBURBResponse, 1)
|
||||
d.access.Lock()
|
||||
if d.closed {
|
||||
d.access.Unlock()
|
||||
return usbip.URBResponse{Error: E.New("device detached")}
|
||||
}
|
||||
d.pending[seq] = responseChan
|
||||
d.access.Unlock()
|
||||
defer func() {
|
||||
d.access.Lock()
|
||||
delete(d.pending, seq)
|
||||
d.access.Unlock()
|
||||
}()
|
||||
|
||||
directionIn := request.Endpoint&0x80 != 0
|
||||
message := &USBURBRequest{
|
||||
DeviceId: d.deviceID,
|
||||
Seq: seq,
|
||||
Endpoint: uint32(request.Endpoint),
|
||||
DirectionIn: directionIn,
|
||||
TransferFlags: uint32(request.Command.TransferFlags),
|
||||
Setup: append([]byte(nil), request.Command.Setup[:]...),
|
||||
TransferBufferLength: uint32(request.Command.TransferBufferLength),
|
||||
NumberOfPackets: request.Command.NumberOfPackets,
|
||||
StartFrame: request.Command.StartFrame,
|
||||
Interval: request.Command.Interval,
|
||||
IsoPackets: isoPacketsToProto(request.IsoPackets),
|
||||
}
|
||||
if !directionIn {
|
||||
message.OutData = request.Buffer
|
||||
}
|
||||
sendErr := d.send(&USBServerMessage{Message: &USBServerMessage_UrbRequest{UrbRequest: message}})
|
||||
if sendErr != nil {
|
||||
return usbip.URBResponse{Error: sendErr}
|
||||
}
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return usbip.URBResponse{Error: d.ctx.Err()}
|
||||
case response := <-responseChan:
|
||||
result := usbip.URBResponse{
|
||||
Status: response.GetStatus(),
|
||||
ActualLength: response.GetActualLength(),
|
||||
IsoPackets: isoPacketsFromProto(response.GetIsoPackets()),
|
||||
}
|
||||
if directionIn {
|
||||
result.Buffer = response.GetInData()
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func (d *usbProvidedDevice) AbortEndpoint(endpoint uint8) error {
|
||||
return d.send(&USBServerMessage{Message: &USBServerMessage_Abort{Abort: &USBEndpointAbort{
|
||||
DeviceId: d.deviceID,
|
||||
Endpoint: uint32(endpoint),
|
||||
}}})
|
||||
}
|
||||
|
||||
func (d *usbProvidedDevice) deliver(response *USBURBResponse) {
|
||||
d.access.Lock()
|
||||
responseChan, found := d.pending[response.GetSeq()]
|
||||
d.access.Unlock()
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case responseChan <- response:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (d *usbProvidedDevice) close() {
|
||||
d.access.Lock()
|
||||
if d.closed {
|
||||
d.access.Unlock()
|
||||
return
|
||||
}
|
||||
d.closed = true
|
||||
d.access.Unlock()
|
||||
if d.busID != "" {
|
||||
d.provider.RemoveDevice(d.busID)
|
||||
}
|
||||
}
|
||||
|
||||
func usbInterfacesFromProto(interfaces []*USBInterface) []usbip.DeviceInterface {
|
||||
if len(interfaces) == 0 {
|
||||
return nil
|
||||
}
|
||||
deviceInterfaces := make([]usbip.DeviceInterface, 0, len(interfaces))
|
||||
for _, deviceInterface := range interfaces {
|
||||
deviceInterfaces = append(deviceInterfaces, usbip.DeviceInterface{
|
||||
BInterfaceClass: uint8(deviceInterface.GetInterfaceClass()),
|
||||
BInterfaceSubClass: uint8(deviceInterface.GetInterfaceSubClass()),
|
||||
BInterfaceProtocol: uint8(deviceInterface.GetInterfaceProtocol()),
|
||||
})
|
||||
}
|
||||
return deviceInterfaces
|
||||
}
|
||||
|
||||
func isoPacketsToProto(packets []usbip.IsoPacketDescriptor) []*USBIsoPacket {
|
||||
if len(packets) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*USBIsoPacket, 0, len(packets))
|
||||
for _, packet := range packets {
|
||||
out = append(out, &USBIsoPacket{
|
||||
Offset: packet.Offset,
|
||||
Length: packet.Length,
|
||||
ActualLength: packet.ActualLength,
|
||||
Status: packet.Status,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isoPacketsFromProto(packets []*USBIsoPacket) []usbip.IsoPacketDescriptor {
|
||||
if len(packets) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]usbip.IsoPacketDescriptor, 0, len(packets))
|
||||
for _, packet := range packets {
|
||||
out = append(out, usbip.IsoPacketDescriptor{
|
||||
Offset: packet.GetOffset(),
|
||||
Length: packet.GetLength(),
|
||||
ActualLength: packet.GetActualLength(),
|
||||
Status: packet.GetStatus(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
18
daemon/started_service_usbip_stub.go
Normal file
18
daemon/started_service_usbip_stub.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func (s *StartedService) ProvideUSBDevices(server grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error {
|
||||
return status.Error(codes.Unimplemented, "USB/IP is not included in this build, rebuild with -tags with_usbip")
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeUSBIPServerStatus(_ *emptypb.Empty, server grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error {
|
||||
return status.Error(codes.NotFound, "USB/IP is not included in this build, rebuild with -tags with_usbip")
|
||||
}
|
||||
|
|
@ -2,6 +2,36 @@
|
|||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.32
|
||||
|
||||
* Add dashboard support for the API service **1**
|
||||
* Add USB/IP service **2**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
The [sing-box API service](/configuration/service/api/) can now download, update
|
||||
and serve [sing-box-dashboard](https://github.com/SagerNet/sing-box-dashboard)
|
||||
directly over its listener, configured via the new
|
||||
[`dashboard`](/configuration/service/api/#dashboard) option.
|
||||
|
||||
**2**:
|
||||
|
||||
New [USB/IP Server](/configuration/service/usbip-server/) and
|
||||
[USB/IP Client](/configuration/service/usbip-client/) services export and import
|
||||
USB devices over the [USB/IP](https://usbip.sourceforge.net/) protocol, built on
|
||||
[sing-usbip](https://github.com/SagerNet/sing-usbip), which adds hotplug while
|
||||
staying interoperable with standard USB/IP. Exporting config-selected local
|
||||
devices (`provider: default`) runs via the CLI on Linux, Windows, and macOS and
|
||||
requires elevated privileges (macOS additionally needs a CGO build and disabled
|
||||
System Integrity Protection). With `provider: dynamic`, devices are instead
|
||||
supplied at runtime through the API service by the graphical clients on macOS and
|
||||
Android, or the [sing-box Dashboard](https://github.com/SagerNet/sing-box-dashboard).
|
||||
|
||||
#### 1.14.0-alpha.31
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.30
|
||||
|
||||
* Introducing sing-box API service **1**
|
||||
|
|
@ -16,16 +46,7 @@ server for observing and controlling the running sing-box instance,
|
|||
exposing the same interface the graphical clients use locally: service
|
||||
status, logs, outbound groups (selection and URL tests), Clash mode,
|
||||
connection tracking, and tools such as network quality tests, STUN
|
||||
tests, and Tailscale operations. The server also accepts
|
||||
[gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md)
|
||||
requests, including the WebSocket transport of
|
||||
[@improbable-eng/grpc-web](https://github.com/improbable-eng/grpc-web)
|
||||
for bidirectional streaming methods, so browsers can connect directly.
|
||||
Clients authenticate via the
|
||||
[`secret`](/configuration/service/api/#secret) field; TLS and CORS
|
||||
options are available. Connection tracking and Clash mode methods
|
||||
require the [Clash API](/configuration/experimental/clash-api/) to be
|
||||
configured.
|
||||
tests, and Tailscale operations.
|
||||
|
||||
**2**:
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,9 @@ Fragment TLS handshake into multiple TLS records to bypass firewalls.
|
|||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
==Linux/macOS/Windows only, requires elevated privileges==
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, and Windows, and requires elevated privileges.
|
||||
|
||||
Inject a forged TLS ClientHello carrying this SNI before the real one,
|
||||
to fool SNI-filtering middleboxes that permit specific hostnames.
|
||||
|
|
|
|||
|
|
@ -248,7 +248,9 @@ UDP 连接超时时间。
|
|||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
==仅 Linux/macOS/Windows,需要管理员权限==
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS 和 Windows,需要提升的权限。
|
||||
|
||||
在真实 ClientHello 之前注入携带本字段所指定 SNI 的伪造 TLS ClientHello,
|
||||
用于欺骗仅放行特定主机名的 SNI 过滤中间盒。
|
||||
|
|
|
|||
|
|
@ -119,7 +119,16 @@ HTTP Client for downloading rule-set.
|
|||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
Default transport will be used if empty.
|
||||
When empty, the default HTTP client is used: the one named by
|
||||
[`default_http_client`](/configuration/route/#default_http_client), or the first top-level
|
||||
`http_clients` entry when `default_http_client` is empty.
|
||||
|
||||
!!! failure "Implicit default deprecated in sing-box 1.14.0"
|
||||
|
||||
When neither `http_clients` nor `default_http_client` is configured, an implicit HTTP
|
||||
client connecting through the default outbound is used. This implicit default is
|
||||
deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0; define
|
||||
`http_clients` instead.
|
||||
|
||||
#### update_interval
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,13 @@
|
|||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
如果为空,将使用默认传输。
|
||||
留空时使用默认 HTTP 客户端:即由 [`default_http_client`](/zh/configuration/route/#default_http_client)
|
||||
指定的客户端,或当 `default_http_client` 为空时使用顶级 `http_clients` 的第一项。
|
||||
|
||||
!!! failure "隐式默认已在 sing-box 1.14.0 废弃"
|
||||
|
||||
当 `http_clients` 与 `default_http_client` 均未配置时,将使用通过默认出站连接的隐式 HTTP 客户端。
|
||||
该隐式默认已在 sing-box 1.14.0 废弃,并将在 sing-box 1.16.0 移除;请改为定义 `http_clients`。
|
||||
|
||||
#### update_interval
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ icon: material/new-box
|
|||
|
||||
The sing-box API service is a gRPC server for observing and controlling the running sing-box instance.
|
||||
|
||||
It can be accessed by the [sing-box graphical clients](/clients/) for iOS, macOS, and
|
||||
Android (via the Remote Control feature), or the
|
||||
[sing-box dashboard](https://github.com/SagerNet/sing-box-dashboard).
|
||||
|
||||
The server also accepts [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) requests,
|
||||
including the WebSocket transport of [@improbable-eng/grpc-web](https://github.com/improbable-eng/grpc-web)
|
||||
for bidirectional streaming methods.
|
||||
|
|
@ -23,6 +27,13 @@ for bidirectional streaming methods.
|
|||
"secret": "",
|
||||
"access_control_allow_origin": [],
|
||||
"access_control_allow_private_network": false,
|
||||
"dashboard": {
|
||||
"enabled": true,
|
||||
"path": "",
|
||||
"download_url": "",
|
||||
"http_client": "", // or {}
|
||||
"update_interval": ""
|
||||
},
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
|
@ -49,9 +60,59 @@ CORS allowed origins, `*` will be used if empty.
|
|||
|
||||
Allow access from private network.
|
||||
|
||||
#### dashboard
|
||||
|
||||
Web dashboard downloaded and served over the API listener at `/dashboard/`; other browser
|
||||
requests are redirected to it.
|
||||
|
||||
!!! info ""
|
||||
|
||||
The object can be replaced with a boolean value (equivalent to `{ "enabled": <bool> }`),
|
||||
or with a string path (equivalent to `{ "enabled": true, "path": "<string>" }`).
|
||||
|
||||
##### enabled
|
||||
|
||||
Enable the dashboard.
|
||||
|
||||
##### path
|
||||
|
||||
Directory the dashboard files are stored in.
|
||||
|
||||
`dashboard` in the working directory will be used by default.
|
||||
|
||||
If the directory is empty, the dashboard is downloaded and an `.etag` file is stored inside
|
||||
it to skip unchanged updates. A non-empty directory without an `.etag` file is served as-is
|
||||
and never updated automatically.
|
||||
|
||||
##### download_url
|
||||
|
||||
Download URL of the dashboard archive (zip).
|
||||
|
||||
`https://github.com/SagerNet/sing-box-dashboard/archive/refs/heads/gh-pages.zip` will be used by default.
|
||||
|
||||
##### http_client
|
||||
|
||||
HTTP client used to download the dashboard, with the same behavior as remote rule-sets.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
When empty, the default HTTP client is used: the one named by
|
||||
[`default_http_client`](/configuration/route/#default_http_client), or the first top-level
|
||||
`http_clients` entry when `default_http_client` is empty.
|
||||
|
||||
!!! failure "Implicit default deprecated in sing-box 1.14.0"
|
||||
|
||||
When neither `http_clients` nor `default_http_client` is configured, an implicit HTTP
|
||||
client connecting through the default outbound is used. This implicit default is
|
||||
deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0; define
|
||||
`http_clients` instead.
|
||||
|
||||
##### update_interval
|
||||
|
||||
Update interval of the dashboard.
|
||||
|
||||
`1d` will be used by default.
|
||||
|
||||
#### tls
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
Connection tracking and Clash mode methods require [Clash API](/configuration/experimental/clash-api/)
|
||||
to be configured, otherwise they fail with `UNIMPLEMENTED`.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ icon: material/new-box
|
|||
|
||||
sing-box API 服务是用于观察与控制正在运行的 sing-box 实例的 gRPC 服务器。
|
||||
|
||||
它可以由 iOS、macOS 和 Android 上的 [sing-box 图形客户端](/zh/clients/)(通过 Remote Control 功能)或 [sing-box dashboard](https://github.com/SagerNet/sing-box-dashboard) 访问。
|
||||
|
||||
服务器同时接受 [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) 请求,
|
||||
包括用于双向流方法的 [@improbable-eng/grpc-web](https://github.com/improbable-eng/grpc-web) WebSocket 传输。
|
||||
|
||||
|
|
@ -22,6 +24,13 @@ sing-box API 服务是用于观察与控制正在运行的 sing-box 实例的 gR
|
|||
"secret": "",
|
||||
"access_control_allow_origin": [],
|
||||
"access_control_allow_private_network": false,
|
||||
"dashboard": {
|
||||
"enabled": true,
|
||||
"path": "",
|
||||
"download_url": "",
|
||||
"http_client": "", // 或 {}
|
||||
"update_interval": ""
|
||||
},
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
|
@ -38,7 +47,7 @@ API 密钥。
|
|||
|
||||
客户端通过标准的 `authorization: Bearer <secret>` gRPC metadata 头认证。
|
||||
|
||||
留空则禁用认证。
|
||||
默认无需认证。
|
||||
|
||||
#### access_control_allow_origin
|
||||
|
||||
|
|
@ -48,9 +57,54 @@ API 密钥。
|
|||
|
||||
允许从私有网络访问。
|
||||
|
||||
#### dashboard
|
||||
|
||||
下载并通过 API 监听器在 `/dashboard/` 提供的 Web 仪表板;其他浏览器请求将被重定向到该路径。
|
||||
|
||||
!!! info ""
|
||||
|
||||
该对象可以替换为布尔值(等同于 `{ "enabled": <bool> }`),
|
||||
或字符串路径(等同于 `{ "enabled": true, "path": "<string>" }`)。
|
||||
|
||||
##### enabled
|
||||
|
||||
启用仪表板。
|
||||
|
||||
##### path
|
||||
|
||||
存放仪表板文件的目录。
|
||||
|
||||
默认使用工作目录下的 `dashboard`。
|
||||
|
||||
如果目录为空,将下载仪表板,并在其中存放 `.etag` 文件以跳过未变更的更新。
|
||||
非空且不含 `.etag` 文件的目录将按原样提供,且不会自动更新。
|
||||
|
||||
##### download_url
|
||||
|
||||
仪表板压缩包(zip)的下载 URL。
|
||||
|
||||
默认使用 `https://github.com/SagerNet/sing-box-dashboard/archive/refs/heads/gh-pages.zip`。
|
||||
|
||||
##### http_client
|
||||
|
||||
用于下载仪表板的 HTTP 客户端,行为与远程规则集相同。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/)。
|
||||
|
||||
留空时使用默认 HTTP 客户端:即由 [`default_http_client`](/zh/configuration/route/#default_http_client)
|
||||
指定的客户端,或当 `default_http_client` 为空时使用顶级 `http_clients` 的第一项。
|
||||
|
||||
!!! failure "隐式默认已在 sing-box 1.14.0 废弃"
|
||||
|
||||
当 `http_clients` 与 `default_http_client` 均未配置时,将使用通过默认出站连接的隐式 HTTP 客户端。
|
||||
该隐式默认已在 sing-box 1.14.0 废弃,并将在 sing-box 1.16.0 移除;请改为定义 `http_clients`。
|
||||
|
||||
##### update_interval
|
||||
|
||||
仪表板的更新间隔。
|
||||
|
||||
默认使用 `1d`。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
连接跟踪与 Clash 模式方法需要配置 [Clash API](/zh/configuration/experimental/clash-api/),
|
||||
否则将以 `UNIMPLEMENTED` 失败。
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ icon: material/new-box
|
|||
| `ocm` | [OCM](./ocm) |
|
||||
| `resolved` | [Resolved](./resolved) |
|
||||
| `ssm-api` | [SSM API](./ssm-api) |
|
||||
| `usbip-server` | [USB/IP Server](./usbip-server) |
|
||||
| `usbip-client` | [USB/IP Client](./usbip-client) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ icon: material/new-box
|
|||
| `ocm` | [OCM](./ocm) |
|
||||
| `resolved` | [Resolved](./resolved) |
|
||||
| `ssm-api` | [SSM API](./ssm-api) |
|
||||
| `usbip-server` | [USB/IP Server](./usbip-server) |
|
||||
| `usbip-client` | [USB/IP Client](./usbip-client) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
|
|
|||
78
docs/configuration/service/usbip-client.md
Normal file
78
docs/configuration/service/usbip-client.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
# USB/IP Client
|
||||
|
||||
USB/IP Client service imports remote USB devices over [USB/IP](https://usbip.sourceforge.net/),
|
||||
exported by the [USB/IP Server](/configuration/service/usbip-server/).
|
||||
|
||||
Available on Linux, Windows, and macOS (macOS requires a build with CGO). Not available on iOS.
|
||||
|
||||
The server must be a sing-box (or sing-usbip) server.
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "usbip-client",
|
||||
|
||||
... // Dial Fields
|
||||
|
||||
"server": "",
|
||||
"server_port": 0,
|
||||
"devices": []
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from the official USB/IP protocol"
|
||||
|
||||
sing-box uses [sing-usbip](https://github.com/SagerNet/sing-usbip), which uses an additional
|
||||
set of protocols to support enhancements such as hotplug, while remaining interoperable with
|
||||
the standard USB/IP protocol.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
Only `detour` takes effect.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
|
||||
==Required==
|
||||
|
||||
The remote `usbip-server` address.
|
||||
|
||||
#### server_port
|
||||
|
||||
The remote `usbip-server` port. Defaults to `3240`.
|
||||
|
||||
#### devices
|
||||
|
||||
List of device matches selecting which remote devices to import. If empty, all exported devices
|
||||
are imported.
|
||||
|
||||
Object format:
|
||||
|
||||
```json
|
||||
{
|
||||
"bus_id": "",
|
||||
"vendor_id": 0,
|
||||
"product_id": 0,
|
||||
"serial": ""
|
||||
}
|
||||
```
|
||||
|
||||
Object fields:
|
||||
|
||||
- `bus_id`: USB bus ID, e.g. `1-2`.
|
||||
- `vendor_id`: USB vendor ID, as a number.
|
||||
- `product_id`: USB product ID, as a number.
|
||||
- `serial`: Device serial number.
|
||||
|
||||
Within one object, all specified fields must match; multiple objects are combined as a union. At
|
||||
least one field is required.
|
||||
73
docs/configuration/service/usbip-client.zh.md
Normal file
73
docs/configuration/service/usbip-client.zh.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
# USB/IP Client
|
||||
|
||||
USB/IP Client 服务通过 [USB/IP](https://usbip.sourceforge.net/) 导入由 [USB/IP Server](/zh/configuration/service/usbip-server/) 导出的远程 USB 设备。
|
||||
|
||||
可用于 Linux、Windows 和 macOS(macOS 需要使用 CGO 构建)。不支持 iOS。
|
||||
|
||||
服务端必须是 sing-box(或 sing-usbip)服务端。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "usbip-client",
|
||||
|
||||
... // 拨号字段
|
||||
|
||||
"server": "",
|
||||
"server_port": 0,
|
||||
"devices": []
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与官方 USB/IP 协议的区别"
|
||||
|
||||
sing-box 使用 [sing-usbip](https://github.com/SagerNet/sing-usbip),它使用一套附加协议来支持热插拔等增强功能,但仍然可以与标准 USB/IP 互操作。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
|
||||
仅 `detour` 生效。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
|
||||
==必填==
|
||||
|
||||
远程 `usbip-server` 地址。
|
||||
|
||||
#### server_port
|
||||
|
||||
远程 `usbip-server` 端口。默认为 `3240`。
|
||||
|
||||
#### devices
|
||||
|
||||
设备匹配列表,用于选择要导入的远程设备。如果为空,则导入所有已导出的设备。
|
||||
|
||||
对象格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"bus_id": "",
|
||||
"vendor_id": 0,
|
||||
"product_id": 0,
|
||||
"serial": ""
|
||||
}
|
||||
```
|
||||
|
||||
对象字段:
|
||||
|
||||
- `bus_id`:USB 总线 ID,例如 `1-2`。
|
||||
- `vendor_id`:USB 供应商 ID,为数字。
|
||||
- `product_id`:USB 产品 ID,为数字。
|
||||
- `serial`:设备序列号。
|
||||
|
||||
在一个对象内,所有指定的字段都必须匹配;多个对象之间取并集。至少需要一个字段。
|
||||
83
docs/configuration/service/usbip-server.md
Normal file
83
docs/configuration/service/usbip-server.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
# USB/IP Server
|
||||
|
||||
USB/IP Server service exports local USB devices over [USB/IP](https://usbip.sourceforge.net/),
|
||||
to be imported by the [USB/IP Client](/configuration/service/usbip-client/) or a standard USB/IP
|
||||
client.
|
||||
|
||||
Available on Linux, Windows, and macOS (macOS requires a build with CGO, and exporting devices
|
||||
requires disabling System Integrity Protection). Not available on iOS.
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "usbip-server",
|
||||
|
||||
... // Listen Fields
|
||||
|
||||
"provider": "",
|
||||
"devices": []
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from the official USB/IP protocol"
|
||||
|
||||
sing-box uses [sing-usbip](https://github.com/SagerNet/sing-usbip), which uses an additional
|
||||
set of protocols to support enhancements such as hotplug, while remaining interoperable with
|
||||
the standard USB/IP protocol.
|
||||
|
||||
### Listen Fields
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
`listen_port` defaults to `3240`.
|
||||
|
||||
### Fields
|
||||
|
||||
#### provider
|
||||
|
||||
The device source provider.
|
||||
|
||||
- `default`: Exports the local devices matched by `devices`. The default value.
|
||||
- `dynamic`: Devices are provided at runtime through a [sing-box API](/configuration/service/api/)
|
||||
client instead of from configuration, on supported platforms: the sing-box graphical clients on
|
||||
[macOS](/clients/apple/) and [Android](/clients/android/), and Chromium-based browsers with
|
||||
[sing-box Dashboard](https://github.com/SagerNet/sing-box-dashboard).
|
||||
|
||||
!!! quote ""
|
||||
|
||||
The `default` provider is only supported when running directly via the CLI on Linux, Windows,
|
||||
and macOS, and requires elevated privileges.
|
||||
|
||||
#### devices
|
||||
|
||||
==Required== with the `default` provider.
|
||||
|
||||
List of device matches selecting which local USB devices to export.
|
||||
|
||||
Object format:
|
||||
|
||||
```json
|
||||
{
|
||||
"bus_id": "",
|
||||
"vendor_id": 0,
|
||||
"product_id": 0,
|
||||
"serial": ""
|
||||
}
|
||||
```
|
||||
|
||||
Object fields:
|
||||
|
||||
- `bus_id`: USB bus ID, e.g. `1-2`.
|
||||
- `vendor_id`: USB vendor ID, as a number.
|
||||
- `product_id`: USB product ID, as a number.
|
||||
- `serial`: Device serial number.
|
||||
|
||||
Within one object, all specified fields must match; multiple objects are combined as a union. At
|
||||
least one field is required.
|
||||
73
docs/configuration/service/usbip-server.zh.md
Normal file
73
docs/configuration/service/usbip-server.zh.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
# USB/IP Server
|
||||
|
||||
USB/IP Server 服务通过 [USB/IP](https://usbip.sourceforge.net/) 导出本地 USB 设备,供 [USB/IP Client](/zh/configuration/service/usbip-client/) 或标准 USB/IP 客户端导入。
|
||||
|
||||
可用于 Linux、Windows 和 macOS(macOS 需要使用 CGO 构建,且导出设备需要禁用系统完整性保护)。不支持 iOS。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "usbip-server",
|
||||
|
||||
... // 监听字段
|
||||
|
||||
"provider": "",
|
||||
"devices": []
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与官方 USB/IP 协议的区别"
|
||||
|
||||
sing-box 使用 [sing-usbip](https://github.com/SagerNet/sing-usbip),它使用一套附加协议来支持热插拔等增强功能,但仍然可以与标准 USB/IP 互操作。
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。
|
||||
|
||||
`listen_port` 默认为 `3240`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### provider
|
||||
|
||||
设备来源提供者。
|
||||
|
||||
- `default`:导出由 `devices` 匹配的本地设备。默认值。
|
||||
- `dynamic`:设备在运行时通过 [sing-box API](/zh/configuration/service/api/) 客户端提供,而非来自配置文件,支持的平台包括 [macOS](/zh/clients/apple/) 和 [Android](/zh/clients/android/) 上的 sing-box 图形客户端,以及配合 [sing-box Dashboard](https://github.com/SagerNet/sing-box-dashboard) 的基于 Chromium 的浏览器。
|
||||
|
||||
!!! quote ""
|
||||
|
||||
`default` 提供者仅支持通过 CLI 直接运行在 Linux、Windows 和 macOS 上,并且需要提升的权限。
|
||||
|
||||
#### devices
|
||||
|
||||
使用 `default` 提供者时 ==必填==。
|
||||
|
||||
设备匹配列表,用于选择要导出的本地 USB 设备。
|
||||
|
||||
对象格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"bus_id": "",
|
||||
"vendor_id": 0,
|
||||
"product_id": 0,
|
||||
"serial": ""
|
||||
}
|
||||
```
|
||||
|
||||
对象字段:
|
||||
|
||||
- `bus_id`:USB 总线 ID,例如 `1-2`。
|
||||
- `vendor_id`:USB 供应商 ID,为数字。
|
||||
- `product_id`:USB 产品 ID,为数字。
|
||||
- `serial`:设备序列号。
|
||||
|
||||
在一个对象内,所有指定的字段都必须匹配;多个对象之间取并集。至少需要一个字段。
|
||||
|
|
@ -678,7 +678,11 @@ Fragment TLS handshake into multiple TLS records to bypass firewalls.
|
|||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
==Client only, Linux/macOS/Windows only, requires elevated privileges==
|
||||
==Client only==
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, and Windows, and requires elevated privileges.
|
||||
|
||||
Inject a forged TLS ClientHello carrying a whitelisted SNI before the real one,
|
||||
to fool SNI-filtering middleboxes that permit specific hostnames.
|
||||
|
|
|
|||
|
|
@ -673,7 +673,11 @@ ECH 配置路径,PEM 格式。
|
|||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
==仅客户端,仅 Linux/macOS/Windows,需要提权==
|
||||
==仅客户端==
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS 和 Windows,需要提升的权限。
|
||||
|
||||
在真实 ClientHello 之前注入一个伪造的、携带白名单 SNI 的 TLS ClientHello,
|
||||
以欺骗基于 SNI 过滤的中间盒放行连接。
|
||||
|
|
|
|||
|
|
@ -456,23 +456,17 @@ func (c *CommandClient) handleClashModeStream() {
|
|||
|
||||
modeStatus, err := client.GetClashModeStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "get clash mode status").Error())
|
||||
return
|
||||
if status.Code(err) != codes.NotFound {
|
||||
c.handler.Disconnected(E.Cause(err, "get clash mode status").Error())
|
||||
return
|
||||
}
|
||||
modeStatus = &daemon.ClashModeStatus{}
|
||||
}
|
||||
|
||||
if sFixAndroidStack {
|
||||
go func() {
|
||||
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error())
|
||||
}
|
||||
}()
|
||||
go c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
|
||||
} else {
|
||||
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
|
|
@ -481,6 +475,9 @@ func (c *CommandClient) handleClashModeStream() {
|
|||
|
||||
stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.NotFound {
|
||||
return
|
||||
}
|
||||
c.handler.Disconnected(E.Cause(err, "subscribe clash mode").Error())
|
||||
return
|
||||
}
|
||||
|
|
@ -488,6 +485,9 @@ func (c *CommandClient) handleClashModeStream() {
|
|||
for {
|
||||
mode, err := stream.Recv()
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.NotFound {
|
||||
return
|
||||
}
|
||||
c.handler.Disconnected(E.Cause(err, "clash mode stream recv").Error())
|
||||
return
|
||||
}
|
||||
|
|
@ -673,7 +673,7 @@ func (c *CommandClient) TriggerNativeCrash() error {
|
|||
}
|
||||
|
||||
func (c *CommandClient) TriggerOOMReport() error {
|
||||
_, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
_, err := callManagedWithResult(c, func(ctx context.Context, client daemon.ManagedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.TriggerOOMReport(ctx, &emptypb.Empty{})
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -924,6 +924,61 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler)
|
|||
return session, nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) SubscribeUSBIPServerStatus(handler USBIPServerStatusHandler) (*USBIPServerStatusSubscription, error) {
|
||||
client, parentCtx, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "subscribe usbip server status")
|
||||
}
|
||||
|
||||
streamCtx, cancel := context.WithCancel(parentCtx)
|
||||
session := &USBIPServerStatusSubscription{
|
||||
streamSession: streamSession{
|
||||
ctx: streamCtx,
|
||||
cancel: cancel,
|
||||
closeDone: make(chan struct{}),
|
||||
},
|
||||
}
|
||||
|
||||
failStart := func(cause error, message string) (*USBIPServerStatusSubscription, error) {
|
||||
cancel()
|
||||
if c.standalone {
|
||||
c.closeConnection()
|
||||
}
|
||||
return nil, E.Cause(cause, message)
|
||||
}
|
||||
|
||||
stream, err := client.SubscribeUSBIPServerStatus(streamCtx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return failStart(err, "subscribe usbip server status")
|
||||
}
|
||||
|
||||
standalone := c.standalone
|
||||
go func() {
|
||||
defer func() {
|
||||
close(session.closeDone)
|
||||
if standalone {
|
||||
c.closeConnection()
|
||||
}
|
||||
}()
|
||||
for {
|
||||
event, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if session.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable {
|
||||
return
|
||||
}
|
||||
handler.OnError(E.Cause(recvErr, "usbip server status recv").Error())
|
||||
return
|
||||
}
|
||||
handler.OnStatusUpdate(usbipServerStatusUpdateFromGRPC(event))
|
||||
}
|
||||
}()
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) SetTailscaleExitNode(endpointTag string, stableID string) error {
|
||||
_, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.SetTailscaleExitNode(ctx, &daemon.SetTailscaleExitNodeRequest{
|
||||
|
|
@ -1125,3 +1180,59 @@ func (c *CommandClient) StartTailscaleSSHSession(opts *TailscaleSSHOptions, hand
|
|||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) ProvideUSBDevices(handler USBProviderHandler) (*USBProviderSession, error) {
|
||||
client, parentCtx, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "provide usb devices")
|
||||
}
|
||||
|
||||
streamCtx, cancel := context.WithCancel(parentCtx)
|
||||
stream, err := client.ProvideUSBDevices(streamCtx)
|
||||
if err != nil {
|
||||
cancel()
|
||||
if c.standalone {
|
||||
c.closeConnection()
|
||||
}
|
||||
return nil, E.Cause(err, "provide usb devices")
|
||||
}
|
||||
|
||||
session := &USBProviderSession{
|
||||
stream: stream,
|
||||
ctx: streamCtx,
|
||||
cancel: cancel,
|
||||
closeDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
standalone := c.standalone
|
||||
go func() {
|
||||
defer close(session.closeDone)
|
||||
for {
|
||||
message, recvErr := stream.Recv()
|
||||
if recvErr == io.EOF {
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
if recvErr != nil {
|
||||
handler.OnError("", E.Cause(recvErr, "usb provider recv").Error())
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
switch payload := message.GetMessage().(type) {
|
||||
case *daemon.USBServerMessage_Ready:
|
||||
handler.OnReady(payload.Ready.GetDeviceId(), payload.Ready.GetBusId())
|
||||
case *daemon.USBServerMessage_UrbRequest:
|
||||
handler.OnURBRequest(usbURBRequestFromGRPC(payload.UrbRequest))
|
||||
case *daemon.USBServerMessage_Abort:
|
||||
handler.OnAbort(payload.Abort.GetDeviceId(), int32(payload.Abort.GetEndpoint()))
|
||||
case *daemon.USBServerMessage_Error:
|
||||
handler.OnError(payload.Error.GetDeviceId(), payload.Error.GetMessage())
|
||||
}
|
||||
}
|
||||
if standalone {
|
||||
c.closeConnection()
|
||||
}
|
||||
}()
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,8 +75,9 @@ func NewCommandServer(handler CommandServerHandler, platformInterface PlatformIn
|
|||
// SystemProxyEnabled: false,
|
||||
})
|
||||
server.managedService = daemon.NewManagedService(daemon.ManagedServiceOptions{
|
||||
Handler: (*platformHandler)(server),
|
||||
Debug: sDebug,
|
||||
Handler: (*platformHandler)(server),
|
||||
Debug: sDebug,
|
||||
OOMReporter: sOOMReporter,
|
||||
})
|
||||
return server, nil
|
||||
}
|
||||
|
|
|
|||
203
experimental/libbox/command_types_usbip.go
Normal file
203
experimental/libbox/command_types_usbip.go
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
)
|
||||
|
||||
type USBProviderHandler interface {
|
||||
OnReady(deviceID string, busID string)
|
||||
OnURBRequest(request *USBURBRequest)
|
||||
OnAbort(deviceID string, endpoint int32)
|
||||
OnError(deviceID string, message string)
|
||||
}
|
||||
|
||||
type USBIsoPacket struct {
|
||||
Offset int32
|
||||
Length int32
|
||||
ActualLength int32
|
||||
Status int32
|
||||
}
|
||||
|
||||
type USBDeviceDescriptor struct {
|
||||
ServerTag string
|
||||
DeviceID string
|
||||
BusNum int32
|
||||
DevNum int32
|
||||
Speed int32
|
||||
VendorID int32
|
||||
ProductID int32
|
||||
BCDDevice int32
|
||||
DeviceClass int32
|
||||
DeviceSubClass int32
|
||||
DeviceProtocol int32
|
||||
ConfigurationValue int32
|
||||
NumConfigurations int32
|
||||
Serial string
|
||||
Product string
|
||||
|
||||
interfaces []*daemon.USBInterface
|
||||
}
|
||||
|
||||
func NewUSBDeviceDescriptor(serverTag string, deviceID string) *USBDeviceDescriptor {
|
||||
return &USBDeviceDescriptor{ServerTag: serverTag, DeviceID: deviceID}
|
||||
}
|
||||
|
||||
func (d *USBDeviceDescriptor) AddInterface(interfaceClass int32, interfaceSubClass int32, interfaceProtocol int32) {
|
||||
d.interfaces = append(d.interfaces, &daemon.USBInterface{
|
||||
InterfaceClass: uint32(interfaceClass),
|
||||
InterfaceSubClass: uint32(interfaceSubClass),
|
||||
InterfaceProtocol: uint32(interfaceProtocol),
|
||||
})
|
||||
}
|
||||
|
||||
func (d *USBDeviceDescriptor) toProto() *daemon.USBDeviceAttach {
|
||||
return &daemon.USBDeviceAttach{
|
||||
ServerTag: d.ServerTag,
|
||||
Descriptor_: &daemon.USBDeviceDescriptor{
|
||||
DeviceId: d.DeviceID,
|
||||
BusNum: uint32(d.BusNum),
|
||||
DevNum: uint32(d.DevNum),
|
||||
Speed: uint32(d.Speed),
|
||||
VendorId: uint32(d.VendorID),
|
||||
ProductId: uint32(d.ProductID),
|
||||
BcdDevice: uint32(d.BCDDevice),
|
||||
DeviceClass: uint32(d.DeviceClass),
|
||||
DeviceSubClass: uint32(d.DeviceSubClass),
|
||||
DeviceProtocol: uint32(d.DeviceProtocol),
|
||||
ConfigurationValue: uint32(d.ConfigurationValue),
|
||||
NumConfigurations: uint32(d.NumConfigurations),
|
||||
Interfaces: d.interfaces,
|
||||
Serial: d.Serial,
|
||||
Product: d.Product,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type USBURBRequest struct {
|
||||
DeviceID string
|
||||
Seq int64
|
||||
Endpoint int32
|
||||
DirectionIn bool
|
||||
TransferFlags int32
|
||||
Setup []byte
|
||||
TransferBufferLength int32
|
||||
OutData []byte
|
||||
NumberOfPackets int32
|
||||
StartFrame int32
|
||||
Interval int32
|
||||
|
||||
isoPackets []*daemon.USBIsoPacket
|
||||
}
|
||||
|
||||
func (r *USBURBRequest) IsoPacketCount() int32 {
|
||||
return int32(len(r.isoPackets))
|
||||
}
|
||||
|
||||
func (r *USBURBRequest) GetIsoPacket(index int32) *USBIsoPacket {
|
||||
if index < 0 || int(index) >= len(r.isoPackets) {
|
||||
return nil
|
||||
}
|
||||
packet := r.isoPackets[index]
|
||||
return &USBIsoPacket{
|
||||
Offset: packet.GetOffset(),
|
||||
Length: packet.GetLength(),
|
||||
ActualLength: packet.GetActualLength(),
|
||||
Status: packet.GetStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
func usbURBRequestFromGRPC(request *daemon.USBURBRequest) *USBURBRequest {
|
||||
return &USBURBRequest{
|
||||
DeviceID: request.GetDeviceId(),
|
||||
Seq: int64(request.GetSeq()),
|
||||
Endpoint: int32(request.GetEndpoint()),
|
||||
DirectionIn: request.GetDirectionIn(),
|
||||
TransferFlags: int32(request.GetTransferFlags()),
|
||||
Setup: request.GetSetup(),
|
||||
TransferBufferLength: int32(request.GetTransferBufferLength()),
|
||||
OutData: request.GetOutData(),
|
||||
NumberOfPackets: request.GetNumberOfPackets(),
|
||||
StartFrame: request.GetStartFrame(),
|
||||
Interval: request.GetInterval(),
|
||||
isoPackets: request.GetIsoPackets(),
|
||||
}
|
||||
}
|
||||
|
||||
type USBURBResponse struct {
|
||||
DeviceID string
|
||||
Seq int64
|
||||
Status int32
|
||||
ActualLength int32
|
||||
InData []byte
|
||||
|
||||
isoPackets []*daemon.USBIsoPacket
|
||||
}
|
||||
|
||||
func NewUSBURBResponse(deviceID string, seq int64) *USBURBResponse {
|
||||
return &USBURBResponse{DeviceID: deviceID, Seq: seq}
|
||||
}
|
||||
|
||||
func (r *USBURBResponse) AddIsoPacket(offset int32, length int32, actualLength int32, status int32) {
|
||||
r.isoPackets = append(r.isoPackets, &daemon.USBIsoPacket{
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
ActualLength: actualLength,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *USBURBResponse) toProto() *daemon.USBURBResponse {
|
||||
return &daemon.USBURBResponse{
|
||||
DeviceId: r.DeviceID,
|
||||
Seq: uint64(r.Seq),
|
||||
Status: r.Status,
|
||||
ActualLength: r.ActualLength,
|
||||
InData: r.InData,
|
||||
IsoPackets: r.isoPackets,
|
||||
}
|
||||
}
|
||||
|
||||
type USBProviderSession struct {
|
||||
stream daemon.StartedService_ProvideUSBDevicesClient
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
sendAccess sync.Mutex
|
||||
closeOnce sync.Once
|
||||
closeDone chan struct{}
|
||||
}
|
||||
|
||||
func (s *USBProviderSession) send(message *daemon.USBProviderMessage) error {
|
||||
s.sendAccess.Lock()
|
||||
defer s.sendAccess.Unlock()
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return os.ErrClosed
|
||||
default:
|
||||
}
|
||||
return s.stream.Send(message)
|
||||
}
|
||||
|
||||
func (s *USBProviderSession) AttachDevice(descriptor *USBDeviceDescriptor) error {
|
||||
return s.send(&daemon.USBProviderMessage{Message: &daemon.USBProviderMessage_Attach{Attach: descriptor.toProto()}})
|
||||
}
|
||||
|
||||
func (s *USBProviderSession) DetachDevice(deviceID string) error {
|
||||
return s.send(&daemon.USBProviderMessage{Message: &daemon.USBProviderMessage_Detach{Detach: &daemon.USBDeviceDetach{DeviceId: deviceID}}})
|
||||
}
|
||||
|
||||
func (s *USBProviderSession) SendURBResponse(response *USBURBResponse) error {
|
||||
return s.send(&daemon.USBProviderMessage{Message: &daemon.USBProviderMessage_UrbResponse{UrbResponse: response.toProto()}})
|
||||
}
|
||||
|
||||
func (s *USBProviderSession) Close() error {
|
||||
s.closeOnce.Do(func() {
|
||||
s.cancel()
|
||||
_ = s.stream.CloseSend()
|
||||
})
|
||||
<-s.closeDone
|
||||
return nil
|
||||
}
|
||||
46
experimental/libbox/command_types_usbip_local.go
Normal file
46
experimental/libbox/command_types_usbip_local.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package libbox
|
||||
|
||||
type USBLocalDeviceInfo struct {
|
||||
StableID string
|
||||
BusID string
|
||||
Backend int32
|
||||
BusNum int32
|
||||
DevNum int32
|
||||
Speed int32
|
||||
VendorID int32
|
||||
ProductID int32
|
||||
BCDDevice int32
|
||||
DeviceClass int32
|
||||
DeviceSubClass int32
|
||||
DeviceProtocol int32
|
||||
ConfigurationValue int32
|
||||
NumConfigurations int32
|
||||
Serial string
|
||||
Product string
|
||||
|
||||
interfaces []*USBSharedDeviceInterface
|
||||
}
|
||||
|
||||
func (d *USBLocalDeviceInfo) Interfaces() USBSharedDeviceInterfaceIterator {
|
||||
return newIterator(d.interfaces)
|
||||
}
|
||||
|
||||
type USBLocalDeviceInfoIterator interface {
|
||||
Next() *USBLocalDeviceInfo
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type USBLocalProvidedDevice struct {
|
||||
ServerTag string
|
||||
DeviceID string
|
||||
LocalDeviceID string
|
||||
Label string
|
||||
VendorID int32
|
||||
ProductID int32
|
||||
}
|
||||
|
||||
type USBLocalProviderHandler interface {
|
||||
OnDeviceError(serverTag string, deviceID string, message string)
|
||||
OnSessionError(serverTag string, message string)
|
||||
OnLocalDevicesChanged()
|
||||
}
|
||||
538
experimental/libbox/command_types_usbip_local_darwin.go
Normal file
538
experimental/libbox/command_types_usbip_local_darwin.go
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
//go:build with_usbip && darwin && !ios && cgo
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing-usbip"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func (c *CommandClient) NewUSBLocalProvider(handler USBLocalProviderHandler) (*USBLocalProviderManager, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
manager := &USBLocalProviderManager{
|
||||
client: c,
|
||||
handler: handler,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
sessions: make(map[string]*usbDarwinLocalProviderSession),
|
||||
devices: make(map[string]*usbDarwinProvidedDevice),
|
||||
}
|
||||
err := usbip.WatchLocalDevices(ctx, func() {
|
||||
manager.detachMissing()
|
||||
manager.notifyLocalDevicesChanged()
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, E.Cause(err, "watch local usb devices")
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
type USBLocalProviderManager struct {
|
||||
client *CommandClient
|
||||
handler USBLocalProviderHandler
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
counter atomic.Uint64
|
||||
access sync.Mutex
|
||||
closed bool
|
||||
sessions map[string]*usbDarwinLocalProviderSession
|
||||
devices map[string]*usbDarwinProvidedDevice
|
||||
}
|
||||
|
||||
type usbDarwinLocalProviderSession struct {
|
||||
tag string
|
||||
session *USBProviderSession
|
||||
}
|
||||
|
||||
type usbDarwinProvidedDevice struct {
|
||||
manager *USBLocalProviderManager
|
||||
session *usbDarwinLocalProviderSession
|
||||
local usbip.LocalDevice
|
||||
info *USBLocalProvidedDevice
|
||||
queueAccess sync.Mutex
|
||||
queues map[uint8]chan *USBURBRequest
|
||||
closed bool
|
||||
closeOnce sync.Once
|
||||
closeFinished chan struct{}
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) ListDevices() (USBLocalDeviceInfoIterator, error) {
|
||||
devices, err := usbip.ListLocalDevices()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*USBLocalDeviceInfo, 0, len(devices))
|
||||
for i := range devices {
|
||||
out = append(out, usbLocalDeviceInfoFromUSBIP(devices[i]))
|
||||
}
|
||||
return newIterator(out), nil
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) Attach(serverTag string, localDeviceID string) (*USBLocalProvidedDevice, error) {
|
||||
if serverTag == "" {
|
||||
return nil, E.New("missing usbip-server tag")
|
||||
}
|
||||
if localDeviceID == "" {
|
||||
return nil, E.New("missing local USB device id")
|
||||
}
|
||||
session, err := m.ensureSession(serverTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localDevice, err := usbip.OpenLocalDevice(localDeviceID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceID := fmt.Sprintf("local-%d", m.counter.Add(1))
|
||||
localInfo := usbLocalDeviceInfoFromUSBIP(usbip.LocalDeviceInfo{
|
||||
StableID: localDevice.StableID(),
|
||||
Entry: localDevice.Entry(),
|
||||
})
|
||||
descriptor := usbDeviceDescriptorFromLocalInfo(serverTag, deviceID, localInfo)
|
||||
provided := &USBLocalProvidedDevice{
|
||||
ServerTag: serverTag,
|
||||
DeviceID: deviceID,
|
||||
LocalDeviceID: localDevice.StableID(),
|
||||
Label: localInfo.Product,
|
||||
VendorID: localInfo.VendorID,
|
||||
ProductID: localInfo.ProductID,
|
||||
}
|
||||
device := &usbDarwinProvidedDevice{
|
||||
manager: m,
|
||||
session: session,
|
||||
local: localDevice,
|
||||
info: provided,
|
||||
queues: make(map[uint8]chan *USBURBRequest),
|
||||
closeFinished: make(chan struct{}),
|
||||
}
|
||||
m.access.Lock()
|
||||
if m.closed {
|
||||
m.access.Unlock()
|
||||
_ = localDevice.Close()
|
||||
return nil, os.ErrClosed
|
||||
}
|
||||
m.devices[deviceID] = device
|
||||
m.access.Unlock()
|
||||
err = session.session.AttachDevice(descriptor)
|
||||
if err != nil {
|
||||
m.removeDevice(deviceID)
|
||||
device.close(false)
|
||||
return nil, err
|
||||
}
|
||||
return provided, nil
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) Detach(deviceID string) error {
|
||||
device := m.removeDevice(deviceID)
|
||||
if device == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
err := device.detach()
|
||||
device.close(false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) Close() error {
|
||||
m.access.Lock()
|
||||
if m.closed {
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.closed = true
|
||||
devices := make([]*usbDarwinProvidedDevice, 0, len(m.devices))
|
||||
for _, device := range m.devices {
|
||||
devices = append(devices, device)
|
||||
}
|
||||
m.devices = make(map[string]*usbDarwinProvidedDevice)
|
||||
sessions := make([]*usbDarwinLocalProviderSession, 0, len(m.sessions))
|
||||
for _, session := range m.sessions {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
m.sessions = make(map[string]*usbDarwinLocalProviderSession)
|
||||
m.access.Unlock()
|
||||
|
||||
m.cancel()
|
||||
for _, device := range devices {
|
||||
device.close(false)
|
||||
}
|
||||
var err error
|
||||
for _, session := range sessions {
|
||||
err = E.Append(err, session.session.Close(), func(err error) error {
|
||||
return E.Cause(err, "close usb provider session ", session.tag)
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) ensureSession(serverTag string) (*usbDarwinLocalProviderSession, error) {
|
||||
m.access.Lock()
|
||||
if m.closed {
|
||||
m.access.Unlock()
|
||||
return nil, os.ErrClosed
|
||||
}
|
||||
existing := m.sessions[serverTag]
|
||||
m.access.Unlock()
|
||||
if existing != nil {
|
||||
return existing, nil
|
||||
}
|
||||
session, err := m.client.ProvideUSBDevices(&usbDarwinLocalProviderStreamHandler{
|
||||
manager: m,
|
||||
serverTag: serverTag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrapped := &usbDarwinLocalProviderSession{tag: serverTag, session: session}
|
||||
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.closed {
|
||||
_ = session.Close()
|
||||
return nil, os.ErrClosed
|
||||
}
|
||||
if existing = m.sessions[serverTag]; existing != nil {
|
||||
_ = session.Close()
|
||||
return existing, nil
|
||||
}
|
||||
m.sessions[serverTag] = wrapped
|
||||
return wrapped, nil
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) removeDevice(deviceID string) *usbDarwinProvidedDevice {
|
||||
m.access.Lock()
|
||||
device := m.devices[deviceID]
|
||||
if device != nil {
|
||||
delete(m.devices, deviceID)
|
||||
}
|
||||
m.access.Unlock()
|
||||
return device
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) device(deviceID string) *usbDarwinProvidedDevice {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.devices[deviceID]
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) detachMissing() {
|
||||
devices, err := usbip.ListLocalDevices()
|
||||
if err != nil {
|
||||
m.notifySessionError("", E.Cause(err, "list local USB devices").Error())
|
||||
return
|
||||
}
|
||||
present := make(map[string]struct{}, len(devices))
|
||||
for i := range devices {
|
||||
present[devices[i].StableID] = struct{}{}
|
||||
}
|
||||
|
||||
var stale []*usbDarwinProvidedDevice
|
||||
m.access.Lock()
|
||||
for deviceID, device := range m.devices {
|
||||
if _, ok := present[device.info.LocalDeviceID]; ok {
|
||||
continue
|
||||
}
|
||||
delete(m.devices, deviceID)
|
||||
stale = append(stale, device)
|
||||
}
|
||||
m.access.Unlock()
|
||||
|
||||
for _, device := range stale {
|
||||
_ = device.detach()
|
||||
device.close(false)
|
||||
m.notifyDeviceError(device.info.ServerTag, device.info.DeviceID, "local USB device disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) onURBRequest(request *USBURBRequest) {
|
||||
device := m.device(request.DeviceID)
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
device.submit(request)
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) onAbort(deviceID string, endpoint int32) {
|
||||
device := m.device(deviceID)
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
err := device.local.AbortEndpoint(uint8(endpoint))
|
||||
if err != nil {
|
||||
m.notifyDeviceError(device.info.ServerTag, deviceID, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) onDeviceError(serverTag string, deviceID string, message string) {
|
||||
device := m.removeDevice(deviceID)
|
||||
if device != nil {
|
||||
device.close(false)
|
||||
}
|
||||
m.notifyDeviceError(serverTag, deviceID, message)
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) onSessionError(serverTag string, message string) {
|
||||
var affected []*usbDarwinProvidedDevice
|
||||
m.access.Lock()
|
||||
delete(m.sessions, serverTag)
|
||||
for deviceID, device := range m.devices {
|
||||
if device.info.ServerTag != serverTag {
|
||||
continue
|
||||
}
|
||||
delete(m.devices, deviceID)
|
||||
affected = append(affected, device)
|
||||
}
|
||||
m.access.Unlock()
|
||||
|
||||
for _, device := range affected {
|
||||
device.close(false)
|
||||
}
|
||||
m.notifySessionError(serverTag, message)
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) notifyLocalDevicesChanged() {
|
||||
if m.handler != nil {
|
||||
m.handler.OnLocalDevicesChanged()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) notifyDeviceError(serverTag string, deviceID string, message string) {
|
||||
if m.handler != nil {
|
||||
m.handler.OnDeviceError(serverTag, deviceID, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) notifySessionError(serverTag string, message string) {
|
||||
if m.handler != nil {
|
||||
m.handler.OnSessionError(serverTag, message)
|
||||
}
|
||||
}
|
||||
|
||||
type usbDarwinLocalProviderStreamHandler struct {
|
||||
manager *USBLocalProviderManager
|
||||
serverTag string
|
||||
}
|
||||
|
||||
func (h *usbDarwinLocalProviderStreamHandler) OnReady(deviceID string, busID string) {
|
||||
}
|
||||
|
||||
func (h *usbDarwinLocalProviderStreamHandler) OnURBRequest(request *USBURBRequest) {
|
||||
h.manager.onURBRequest(request)
|
||||
}
|
||||
|
||||
func (h *usbDarwinLocalProviderStreamHandler) OnAbort(deviceID string, endpoint int32) {
|
||||
h.manager.onAbort(deviceID, endpoint)
|
||||
}
|
||||
|
||||
func (h *usbDarwinLocalProviderStreamHandler) OnError(deviceID string, message string) {
|
||||
if deviceID == "" {
|
||||
h.manager.onSessionError(h.serverTag, message)
|
||||
return
|
||||
}
|
||||
h.manager.onDeviceError(h.serverTag, deviceID, message)
|
||||
}
|
||||
|
||||
func (d *usbDarwinProvidedDevice) submit(request *USBURBRequest) {
|
||||
endpoint := uint8(request.Endpoint)
|
||||
d.queueAccess.Lock()
|
||||
if d.closed {
|
||||
d.queueAccess.Unlock()
|
||||
d.sendResponse(usbURBErrorResponse(request))
|
||||
return
|
||||
}
|
||||
queue := d.queues[endpoint]
|
||||
if queue == nil {
|
||||
queue = make(chan *USBURBRequest, 64)
|
||||
d.queues[endpoint] = queue
|
||||
go d.runQueue(queue)
|
||||
}
|
||||
select {
|
||||
case queue <- request:
|
||||
d.queueAccess.Unlock()
|
||||
default:
|
||||
d.queueAccess.Unlock()
|
||||
d.sendResponse(usbURBErrorResponse(request))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *usbDarwinProvidedDevice) runQueue(queue <-chan *USBURBRequest) {
|
||||
for request := range queue {
|
||||
result := d.local.Submit(usbIPRequestFromLocalProvider(request))
|
||||
d.sendResponse(usbURBResponseFromUSBIP(request, result))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *usbDarwinProvidedDevice) detach() error {
|
||||
return d.session.session.DetachDevice(d.info.DeviceID)
|
||||
}
|
||||
|
||||
func (d *usbDarwinProvidedDevice) close(detach bool) {
|
||||
d.closeOnce.Do(func() {
|
||||
defer close(d.closeFinished)
|
||||
d.queueAccess.Lock()
|
||||
d.closed = true
|
||||
for endpoint, queue := range d.queues {
|
||||
close(queue)
|
||||
delete(d.queues, endpoint)
|
||||
}
|
||||
d.queueAccess.Unlock()
|
||||
if detach {
|
||||
_ = d.detach()
|
||||
}
|
||||
_ = d.local.Close()
|
||||
})
|
||||
<-d.closeFinished
|
||||
}
|
||||
|
||||
func (d *usbDarwinProvidedDevice) sendResponse(response *USBURBResponse) {
|
||||
err := d.session.session.SendURBResponse(response)
|
||||
if err != nil {
|
||||
d.manager.onSessionError(d.info.ServerTag, E.Cause(err, "send USB URB response").Error())
|
||||
}
|
||||
}
|
||||
|
||||
func usbLocalDeviceInfoFromUSBIP(info usbip.LocalDeviceInfo) *USBLocalDeviceInfo {
|
||||
entry := info.Entry
|
||||
interfaces := make([]*USBSharedDeviceInterface, 0, len(entry.Interfaces))
|
||||
for _, deviceInterface := range entry.Interfaces {
|
||||
interfaces = append(interfaces, &USBSharedDeviceInterface{
|
||||
InterfaceClass: int32(deviceInterface.BInterfaceClass),
|
||||
InterfaceSubClass: int32(deviceInterface.BInterfaceSubClass),
|
||||
InterfaceProtocol: int32(deviceInterface.BInterfaceProtocol),
|
||||
})
|
||||
}
|
||||
return &USBLocalDeviceInfo{
|
||||
StableID: info.StableID,
|
||||
BusID: entry.Info.BusIDString(),
|
||||
Backend: int32(info.Backend),
|
||||
BusNum: int32(entry.Info.BusNum),
|
||||
DevNum: int32(entry.Info.DevNum),
|
||||
Speed: int32(entry.Info.Speed),
|
||||
VendorID: int32(entry.Info.IDVendor),
|
||||
ProductID: int32(entry.Info.IDProduct),
|
||||
BCDDevice: int32(entry.Info.BCDDevice),
|
||||
DeviceClass: int32(entry.Info.BDeviceClass),
|
||||
DeviceSubClass: int32(entry.Info.BDeviceSubClass),
|
||||
DeviceProtocol: int32(entry.Info.BDeviceProtocol),
|
||||
ConfigurationValue: int32(entry.Info.BConfigurationValue),
|
||||
NumConfigurations: int32(entry.Info.BNumConfigurations),
|
||||
Serial: entry.Serial,
|
||||
Product: entry.Product,
|
||||
interfaces: interfaces,
|
||||
}
|
||||
}
|
||||
|
||||
func usbDeviceDescriptorFromLocalInfo(serverTag string, deviceID string, info *USBLocalDeviceInfo) *USBDeviceDescriptor {
|
||||
descriptor := &USBDeviceDescriptor{
|
||||
ServerTag: serverTag,
|
||||
DeviceID: deviceID,
|
||||
BusNum: info.BusNum,
|
||||
DevNum: info.DevNum,
|
||||
Speed: info.Speed,
|
||||
VendorID: info.VendorID,
|
||||
ProductID: info.ProductID,
|
||||
BCDDevice: info.BCDDevice,
|
||||
DeviceClass: info.DeviceClass,
|
||||
DeviceSubClass: info.DeviceSubClass,
|
||||
DeviceProtocol: info.DeviceProtocol,
|
||||
ConfigurationValue: info.ConfigurationValue,
|
||||
NumConfigurations: info.NumConfigurations,
|
||||
Serial: info.Serial,
|
||||
Product: info.Product,
|
||||
}
|
||||
for _, deviceInterface := range info.interfaces {
|
||||
descriptor.interfaces = append(descriptor.interfaces, &daemon.USBInterface{
|
||||
InterfaceClass: uint32(deviceInterface.InterfaceClass),
|
||||
InterfaceSubClass: uint32(deviceInterface.InterfaceSubClass),
|
||||
InterfaceProtocol: uint32(deviceInterface.InterfaceProtocol),
|
||||
})
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
|
||||
func usbIPRequestFromLocalProvider(request *USBURBRequest) usbip.URBRequest {
|
||||
endpoint := uint8(request.Endpoint)
|
||||
direction := usbip.USBIPDirOut
|
||||
if request.DirectionIn {
|
||||
direction = usbip.USBIPDirIn
|
||||
}
|
||||
var setup [8]byte
|
||||
copy(setup[:], request.Setup)
|
||||
buffer := request.OutData
|
||||
if request.DirectionIn {
|
||||
buffer = make([]byte, max(0, int(request.TransferBufferLength)))
|
||||
}
|
||||
isoPackets := make([]usbip.IsoPacketDescriptor, 0, request.IsoPacketCount())
|
||||
for i := int32(0); i < request.IsoPacketCount(); i++ {
|
||||
packet := request.GetIsoPacket(i)
|
||||
if packet == nil {
|
||||
continue
|
||||
}
|
||||
isoPackets = append(isoPackets, usbip.IsoPacketDescriptor{
|
||||
Offset: packet.Offset,
|
||||
Length: packet.Length,
|
||||
ActualLength: packet.ActualLength,
|
||||
Status: packet.Status,
|
||||
})
|
||||
}
|
||||
command := usbip.SubmitCommand{
|
||||
Header: usbip.DataHeader{
|
||||
Command: usbip.CmdSubmit,
|
||||
SeqNum: uint32(request.Seq),
|
||||
Direction: direction,
|
||||
Endpoint: uint32(endpoint & 0x0f),
|
||||
},
|
||||
TransferFlags: request.TransferFlags,
|
||||
TransferBufferLength: request.TransferBufferLength,
|
||||
StartFrame: request.StartFrame,
|
||||
NumberOfPackets: request.NumberOfPackets,
|
||||
Interval: request.Interval,
|
||||
Setup: setup,
|
||||
Buffer: buffer,
|
||||
IsoPackets: isoPackets,
|
||||
}
|
||||
return usbip.URBRequest{
|
||||
Command: command,
|
||||
Endpoint: endpoint,
|
||||
Buffer: buffer,
|
||||
IsoPackets: isoPackets,
|
||||
}
|
||||
}
|
||||
|
||||
func usbURBResponseFromUSBIP(request *USBURBRequest, result usbip.URBResponse) *USBURBResponse {
|
||||
if result.Error != nil {
|
||||
return usbURBErrorResponse(request)
|
||||
}
|
||||
response := NewUSBURBResponse(request.DeviceID, request.Seq)
|
||||
response.Status = result.Status
|
||||
response.ActualLength = result.ActualLength
|
||||
for _, packet := range result.IsoPackets {
|
||||
response.AddIsoPacket(packet.Offset, packet.Length, packet.ActualLength, packet.Status)
|
||||
}
|
||||
if request.DirectionIn && len(result.Buffer) > 0 {
|
||||
if request.NumberOfPackets > 0 {
|
||||
response.InData = result.Buffer
|
||||
} else {
|
||||
actual := int(result.ActualLength)
|
||||
if actual < 0 {
|
||||
actual = 0
|
||||
}
|
||||
response.InData = result.Buffer[:min(actual, len(result.Buffer))]
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func usbURBErrorResponse(request *USBURBRequest) *USBURBResponse {
|
||||
response := NewUSBURBResponse(request.DeviceID, request.Seq)
|
||||
response.Status = -5
|
||||
return response
|
||||
}
|
||||
27
experimental/libbox/command_types_usbip_local_stub.go
Normal file
27
experimental/libbox/command_types_usbip_local_stub.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//go:build !with_usbip || !darwin || ios || !cgo
|
||||
|
||||
package libbox
|
||||
|
||||
import "os"
|
||||
|
||||
type USBLocalProviderManager struct{}
|
||||
|
||||
func (c *CommandClient) NewUSBLocalProvider(handler USBLocalProviderHandler) (*USBLocalProviderManager, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) ListDevices() (USBLocalDeviceInfoIterator, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) Attach(serverTag string, localDeviceID string) (*USBLocalProvidedDevice, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) Detach(deviceID string) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (m *USBLocalProviderManager) Close() error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
142
experimental/libbox/command_types_usbip_status.go
Normal file
142
experimental/libbox/command_types_usbip_status.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package libbox
|
||||
|
||||
import "github.com/sagernet/sing-box/daemon"
|
||||
|
||||
type USBIPServerStatusUpdate struct {
|
||||
servers []*USBIPServerStatus
|
||||
}
|
||||
|
||||
func (u *USBIPServerStatusUpdate) Servers() USBIPServerStatusIterator {
|
||||
return newIterator(u.servers)
|
||||
}
|
||||
|
||||
type USBIPServerStatusIterator interface {
|
||||
Next() *USBIPServerStatus
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type USBIPServerStatus struct {
|
||||
ServerTag string
|
||||
devices []*USBSharedDevice
|
||||
}
|
||||
|
||||
func (s *USBIPServerStatus) Devices() USBSharedDeviceIterator {
|
||||
return newIterator(s.devices)
|
||||
}
|
||||
|
||||
type USBSharedDeviceIterator interface {
|
||||
Next() *USBSharedDevice
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
const (
|
||||
USBDeviceStateIdle int32 = iota
|
||||
USBDeviceStateAttached
|
||||
USBDeviceStateUnavailable
|
||||
)
|
||||
|
||||
const (
|
||||
USBBackendUnspecified int32 = iota
|
||||
USBBackendLinuxSysfs
|
||||
USBBackendDynamic
|
||||
USBBackendDarwinIOKit
|
||||
USBBackendWindowsVBoxUSB
|
||||
)
|
||||
|
||||
type USBSharedDevice struct {
|
||||
BusID string
|
||||
StableID string
|
||||
Backend int32
|
||||
State int32
|
||||
DeviceID string
|
||||
BusNum int32
|
||||
DevNum int32
|
||||
Speed int32
|
||||
VendorID int32
|
||||
ProductID int32
|
||||
BCDDevice int32
|
||||
DeviceClass int32
|
||||
DeviceSubClass int32
|
||||
DeviceProtocol int32
|
||||
ConfigurationValue int32
|
||||
NumConfigurations int32
|
||||
Serial string
|
||||
Product string
|
||||
interfaces []*USBSharedDeviceInterface
|
||||
}
|
||||
|
||||
func (d *USBSharedDevice) Interfaces() USBSharedDeviceInterfaceIterator {
|
||||
return newIterator(d.interfaces)
|
||||
}
|
||||
|
||||
type USBSharedDeviceInterfaceIterator interface {
|
||||
Next() *USBSharedDeviceInterface
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type USBSharedDeviceInterface struct {
|
||||
InterfaceClass int32
|
||||
InterfaceSubClass int32
|
||||
InterfaceProtocol int32
|
||||
}
|
||||
|
||||
type USBIPServerStatusHandler interface {
|
||||
OnStatusUpdate(status *USBIPServerStatusUpdate)
|
||||
OnError(message string)
|
||||
}
|
||||
|
||||
type USBIPServerStatusSubscription struct {
|
||||
streamSession
|
||||
}
|
||||
|
||||
func usbipServerStatusUpdateFromGRPC(update *daemon.USBIPServerStatusUpdate) *USBIPServerStatusUpdate {
|
||||
servers := make([]*USBIPServerStatus, len(update.Servers))
|
||||
for i, server := range update.Servers {
|
||||
servers[i] = usbipServerStatusFromGRPC(server)
|
||||
}
|
||||
return &USBIPServerStatusUpdate{servers: servers}
|
||||
}
|
||||
|
||||
func usbipServerStatusFromGRPC(status *daemon.USBIPServerStatus) *USBIPServerStatus {
|
||||
devices := make([]*USBSharedDevice, len(status.Devices))
|
||||
for i, device := range status.Devices {
|
||||
devices[i] = usbSharedDeviceFromGRPC(device)
|
||||
}
|
||||
return &USBIPServerStatus{
|
||||
ServerTag: status.GetServerTag(),
|
||||
devices: devices,
|
||||
}
|
||||
}
|
||||
|
||||
func usbSharedDeviceFromGRPC(device *daemon.USBSharedDevice) *USBSharedDevice {
|
||||
descriptor := device.GetDescriptor_()
|
||||
interfaces := make([]*USBSharedDeviceInterface, len(descriptor.GetInterfaces()))
|
||||
for i, deviceInterface := range descriptor.GetInterfaces() {
|
||||
interfaces[i] = &USBSharedDeviceInterface{
|
||||
InterfaceClass: int32(deviceInterface.GetInterfaceClass()),
|
||||
InterfaceSubClass: int32(deviceInterface.GetInterfaceSubClass()),
|
||||
InterfaceProtocol: int32(deviceInterface.GetInterfaceProtocol()),
|
||||
}
|
||||
}
|
||||
return &USBSharedDevice{
|
||||
BusID: device.GetBusId(),
|
||||
StableID: device.GetStableId(),
|
||||
Backend: int32(device.GetBackend()),
|
||||
State: int32(device.GetState()),
|
||||
DeviceID: descriptor.GetDeviceId(),
|
||||
BusNum: int32(descriptor.GetBusNum()),
|
||||
DevNum: int32(descriptor.GetDevNum()),
|
||||
Speed: int32(descriptor.GetSpeed()),
|
||||
VendorID: int32(descriptor.GetVendorId()),
|
||||
ProductID: int32(descriptor.GetProductId()),
|
||||
BCDDevice: int32(descriptor.GetBcdDevice()),
|
||||
DeviceClass: int32(descriptor.GetDeviceClass()),
|
||||
DeviceSubClass: int32(descriptor.GetDeviceSubClass()),
|
||||
DeviceProtocol: int32(descriptor.GetDeviceProtocol()),
|
||||
ConfigurationValue: int32(descriptor.GetConfigurationValue()),
|
||||
NumConfigurations: int32(descriptor.GetNumConfigurations()),
|
||||
Serial: descriptor.GetSerial(),
|
||||
Product: descriptor.GetProduct(),
|
||||
interfaces: interfaces,
|
||||
}
|
||||
}
|
||||
1
go.mod
1
go.mod
|
|
@ -146,6 +146,7 @@ require (
|
|||
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260516034431-d86a63399c27 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-mod.2 // indirect
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -268,6 +268,8 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG
|
|||
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-tun v0.8.11-0.20260603045801-6e76db79f94a h1:Wuf1SkZL0rISkm4HwNczFLjl8sWUBFGRGLbgZbO8inQ=
|
||||
github.com/sagernet/sing-tun v0.8.11-0.20260603045801-6e76db79f94a/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@ func ServiceRegistry() *service.Registry {
|
|||
registerCCMService(registry)
|
||||
registerOCMService(registry)
|
||||
registerOOMKillerService(registry)
|
||||
registerUSBIPServices(registry)
|
||||
registerUserManagerService(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
|
|
|||
12
include/usbip.go
Normal file
12
include/usbip.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/service/usbip"
|
||||
)
|
||||
|
||||
func registerUSBIPServices(registry *service.Registry) {
|
||||
usbip.RegisterService(registry)
|
||||
}
|
||||
23
include/usbip_stub.go
Normal file
23
include/usbip_stub.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
|
||||
|
||||
package include
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func registerUSBIPServices(registry *service.Registry) {
|
||||
service.Register[option.USBIPServerServiceOptions](registry, C.TypeUSBIPServer, func(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPServerServiceOptions) (adapter.Service, error) {
|
||||
return nil, E.New(`USB/IP is not included in this build, rebuild with -tags with_usbip (supported on Linux, Windows, and macOS with CGO)`)
|
||||
})
|
||||
service.Register[option.USBIPClientServiceOptions](registry, C.TypeUSBIPClient, func(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPClientServiceOptions) (adapter.Service, error) {
|
||||
return nil, E.New(`USB/IP is not included in this build, rebuild with -tags with_usbip (supported on Linux, Windows, and macOS with CGO)`)
|
||||
})
|
||||
}
|
||||
10
include/usermanager.go
Normal file
10
include/usermanager.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package include
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/service/usermanager"
|
||||
)
|
||||
|
||||
func registerUserManagerService(registry *service.Registry) {
|
||||
usermanager.RegisterService(registry)
|
||||
}
|
||||
|
|
@ -194,6 +194,8 @@ nav:
|
|||
- CCM: configuration/service/ccm.md
|
||||
- OCM: configuration/service/ocm.md
|
||||
- Hysteria Realm: configuration/service/hysteria-realm.md
|
||||
- USB/IP Server: configuration/service/usbip-server.md
|
||||
- USB/IP Client: configuration/service/usbip-client.md
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
slugify: !!python/object/apply:pymdownx.slugs.slugify
|
||||
|
|
|
|||
|
|
@ -1,11 +1,50 @@
|
|||
package option
|
||||
|
||||
import "github.com/sagernet/sing/common/json/badoption"
|
||||
import (
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type APIServiceOptions struct {
|
||||
ListenOptions
|
||||
Secret string `json:"secret,omitempty"`
|
||||
AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
|
||||
AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
|
||||
Dashboard *APIDashboardOptions `json:"dashboard,omitempty"`
|
||||
InboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type _APIDashboardOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
|
||||
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
|
||||
}
|
||||
|
||||
type APIDashboardOptions _APIDashboardOptions
|
||||
|
||||
func (o APIDashboardOptions) MarshalJSON() ([]byte, error) {
|
||||
if o.DownloadURL == "" && o.HTTPClient == nil && o.UpdateInterval == 0 {
|
||||
if o.Path == "" {
|
||||
return json.Marshal(o.Enabled)
|
||||
}
|
||||
if o.Enabled {
|
||||
return json.Marshal(o.Path)
|
||||
}
|
||||
}
|
||||
return json.Marshal(_APIDashboardOptions(o))
|
||||
}
|
||||
|
||||
func (o *APIDashboardOptions) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, &o.Enabled)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = json.Unmarshal(bytes, &o.Path)
|
||||
if err == nil {
|
||||
o.Enabled = true
|
||||
return nil
|
||||
}
|
||||
return json.UnmarshalDisallowUnknownFields(bytes, (*_APIDashboardOptions)(o))
|
||||
}
|
||||
|
|
|
|||
69
option/usbip.go
Normal file
69
option/usbip.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package option
|
||||
|
||||
import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
const (
|
||||
USBIPProviderDefault = "default"
|
||||
USBIPProviderDynamic = "dynamic"
|
||||
)
|
||||
|
||||
type _USBIPServerServiceOptions struct {
|
||||
ListenOptions
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Options any `json:"-"`
|
||||
}
|
||||
|
||||
type USBIPServerServiceOptions _USBIPServerServiceOptions
|
||||
|
||||
func (o USBIPServerServiceOptions) MarshalJSON() ([]byte, error) {
|
||||
if o.Options == nil {
|
||||
return json.Marshal((_USBIPServerServiceOptions)(o))
|
||||
}
|
||||
return badjson.MarshallObjects((_USBIPServerServiceOptions)(o), o.Options)
|
||||
}
|
||||
|
||||
func (o *USBIPServerServiceOptions) UnmarshalJSON(content []byte) error {
|
||||
err := json.Unmarshal(content, (*_USBIPServerServiceOptions)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var options any
|
||||
switch o.Provider {
|
||||
case "", USBIPProviderDefault:
|
||||
o.Provider = USBIPProviderDefault
|
||||
options = new(USBIPDefaultProviderOptions)
|
||||
case USBIPProviderDynamic:
|
||||
options = new(USBIPDynamicProviderOptions)
|
||||
default:
|
||||
return E.New("unknown usbip provider type: ", o.Provider)
|
||||
}
|
||||
err = badjson.UnmarshallExcluded(content, (*_USBIPServerServiceOptions)(o), options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Options = options
|
||||
return nil
|
||||
}
|
||||
|
||||
type USBIPClientServiceOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Devices []USBIPDeviceMatch `json:"devices,omitempty"`
|
||||
}
|
||||
|
||||
type USBIPDeviceMatch struct {
|
||||
BusID string `json:"bus_id,omitempty"`
|
||||
VendorID uint16 `json:"vendor_id,omitempty"`
|
||||
ProductID uint16 `json:"product_id,omitempty"`
|
||||
Serial string `json:"serial,omitempty"`
|
||||
}
|
||||
|
||||
type USBIPDefaultProviderOptions struct {
|
||||
Devices []USBIPDeviceMatch `json:"devices,omitempty"`
|
||||
}
|
||||
|
||||
type USBIPDynamicProviderOptions struct{}
|
||||
16
option/user_manager.go
Normal file
16
option/user_manager.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package option
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type UserManagerOptions struct {
|
||||
AuthServer string `json:"auth_server,omitempty"`
|
||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||
CacheTTL badoption.Duration `json:"cache_ttl,omitempty"`
|
||||
RefreshInterval badoption.Duration `json:"refresh_interval,omitempty"`
|
||||
ReportInterval badoption.Duration `json:"report_interval,omitempty"`
|
||||
ReportTraffic bool `json:"report_traffic,omitempty"`
|
||||
APISecret string `json:"api_secret,omitempty"`
|
||||
APIListen string `json:"api_listen,omitempty"`
|
||||
}
|
||||
65
patches.md
Normal file
65
patches.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Upstream Changes
|
||||
|
||||
## UserManager — External Authentication & Traffic Tracking
|
||||
|
||||
A new `user_manager` service that turns sing-box into a universal proxy server for external auth servers.
|
||||
|
||||
### New files
|
||||
|
||||
- `adapter/user_manager.go` — `UserManager` interface: `Authenticate`, `GetCredentials`, `ReportTraffic`, `GetTraffic`, `ListTraffic`, `KickUser`
|
||||
- `option/user_manager.go` — `UserManagerOptions`: `auth_server`, `cache_ttl`, `refresh_interval`, `report_interval`, `report_traffic`, `api_secret`, `api_listen`, `timeout`
|
||||
- `service/usermanager/manager.go` — full implementation:
|
||||
- On-demand HTTPS auth (`POST /api/auth`) with in-memory TTL cache
|
||||
- Periodic credential sync (`GET /api/credentials`) populates per-protocol user lists
|
||||
- Per-user traffic counters (`atomic.Int64`) reported via `POST /api/traffic`
|
||||
- `ConnectionTracker` impl wrapping conns with byte counters (`RoutedConnection`/`RoutedPacketConnection`)
|
||||
- HTTP API on configurable `api_listen`:
|
||||
- `GET /traffic` — all users traffic
|
||||
- `GET /traffic/{user}` — single user
|
||||
- `POST /kick/{user}` — evict user from auth cache
|
||||
- Bearer token auth on all API endpoints via `api_secret`
|
||||
- `service/usermanager/registry.go` — service registration
|
||||
- `include/usermanager.go` — include build hook
|
||||
- `constant/proxy.go` — `TypeUserManager` constant
|
||||
|
||||
### Modified files — Protocol inbound integration
|
||||
|
||||
All 10 protocol inbounds detect the global `user_manager` from service context and use it when present:
|
||||
|
||||
| Protocol | Type | Approach |
|
||||
|----------|------|----------|
|
||||
| hysteria2 | Service-based | `service.UpdateUsers` with password credentials, 30s refresh loop |
|
||||
| tuic | Service-based | `service.UpdateUsers` with UUID+password (format `"uuid:password"`), 30s refresh loop |
|
||||
| vless | Service-based | `service.UpdateUsers` with UUID credentials, 30s refresh loop |
|
||||
| vmess | Service-based | `service.UpdateUsers` with UUID credentials, 30s refresh loop |
|
||||
| trojan | Service-based | `service.UpdateUsers` with password credentials, 30s refresh loop |
|
||||
| shadowsocks (multi) | Service-based | `service.UpdateUsersWithPasswords` with password credentials, 30s refresh loop |
|
||||
| http | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop |
|
||||
| socks | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop |
|
||||
| mixed | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop |
|
||||
| naive | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop; skips `"missing users"` check when user_manager is configured |
|
||||
|
||||
Each inbound falls back to its original static config when no `user_manager` service is registered.
|
||||
|
||||
### Credential format per protocol
|
||||
|
||||
| Protocol | `UserInfo.Credential` format |
|
||||
|------------|------------------------------|
|
||||
| hysteria2 | password |
|
||||
| tuic | `uuid:password` |
|
||||
| vless | uuid |
|
||||
| vmess | uuid |
|
||||
| trojan | password |
|
||||
| shadowsocks| password (method from static config) |
|
||||
| http | password (username from `UserInfo.Username`) |
|
||||
| socks | password (username from `UserInfo.Username`) |
|
||||
| mixed | password (username from `UserInfo.Username`) |
|
||||
| naive | password (username from `UserInfo.Username`) |
|
||||
|
||||
### Auth server API contract
|
||||
|
||||
The auth server (`auth_server`) must implement:
|
||||
|
||||
- `POST /api/auth` — body `{"protocol":"...", "credential":"...", "addr":"..."}`, returns `{"ok":true, "id":"username"}`
|
||||
- `GET /api/credentials` — returns `{"credentials":[{"protocol":"...", "credential":"...", "username":"..."}]}`
|
||||
- `POST /api/traffic` (optional, when `report_traffic=true`) — body `{"traffic":{"user1":{"tx":123,"rx":456}}}`
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
std_bufio "bufio"
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
|
|
@ -18,6 +19,7 @@ import (
|
|||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/http"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
|
|
@ -33,14 +35,28 @@ type Inbound struct {
|
|||
listener *listener.Listener
|
||||
authenticator *auth.Authenticator
|
||||
tlsConfig tls.ServerConfig
|
||||
userManager adapter.UserManager
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
|
||||
authenticator := auth.NewAuthenticator(options.Users)
|
||||
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||
if userManager != nil {
|
||||
creds, err := userManager.GetCredentials(C.TypeHTTP)
|
||||
if err == nil && len(creds) > 0 {
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeHTTP, tag),
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
authenticator: auth.NewAuthenticator(options.Users),
|
||||
authenticator: authenticator,
|
||||
userManager: userManager,
|
||||
}
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
||||
|
|
@ -76,9 +92,28 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeHTTP)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
h.authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return common.Close(
|
||||
h.listener,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ type Inbound struct {
|
|||
tlsConfig tls.ServerConfig
|
||||
service *hysteria2.Service[int]
|
||||
userNameList []string
|
||||
userManager adapter.UserManager
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
|
||||
|
|
@ -183,17 +184,39 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userList := make([]int, 0, len(options.Users))
|
||||
userNameList := make([]string, 0, len(options.Users))
|
||||
userPasswordList := make([]string, 0, len(options.Users))
|
||||
for index, user := range options.Users {
|
||||
userList = append(userList, index)
|
||||
userNameList = append(userNameList, user.Name)
|
||||
userPasswordList = append(userPasswordList, user.Password)
|
||||
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||
|
||||
var userList []int
|
||||
var userNameList []string
|
||||
var userPasswordList []string
|
||||
|
||||
if userManager != nil {
|
||||
creds, err := userManager.GetCredentials(C.TypeHysteria2)
|
||||
if err == nil && len(creds) > 0 {
|
||||
userList = make([]int, len(creds))
|
||||
userNameList = make([]string, len(creds))
|
||||
userPasswordList = make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userList[i] = i
|
||||
userNameList[i] = c.Username
|
||||
userPasswordList[i] = c.Credential
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userList = make([]int, 0, len(options.Users))
|
||||
userNameList = make([]string, 0, len(options.Users))
|
||||
userPasswordList = make([]string, 0, len(options.Users))
|
||||
for index, user := range options.Users {
|
||||
userList = append(userList, index)
|
||||
userNameList = append(userNameList, user.Name)
|
||||
userPasswordList = append(userPasswordList, user.Password)
|
||||
}
|
||||
}
|
||||
|
||||
hysteriaService.UpdateUsers(userList, userPasswordList)
|
||||
inbound.service = hysteriaService
|
||||
inbound.userNameList = userNameList
|
||||
inbound.userManager = userManager
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +274,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
packetConn, err := h.listener.ListenUDP()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -258,6 +284,27 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return h.service.Start(packetConn)
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeHysteria2)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
userList := make([]int, len(creds))
|
||||
userNameList := make([]string, len(creds))
|
||||
userPasswordList := make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userList[i] = i
|
||||
userNameList[i] = c.Username
|
||||
userPasswordList[i] = c.Credential
|
||||
}
|
||||
h.service.UpdateUsers(userList, userPasswordList)
|
||||
h.userNameList = userNameList
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) InterfaceUpdated() {
|
||||
h.service.Reset()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/sagernet/sing/protocol/socks"
|
||||
"github.com/sagernet/sing/protocol/socks/socks4"
|
||||
"github.com/sagernet/sing/protocol/socks/socks5"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
|
|
@ -38,6 +39,7 @@ type Inbound struct {
|
|||
authenticator *auth.Authenticator
|
||||
tlsConfig tls.ServerConfig
|
||||
udpTimeout time.Duration
|
||||
userManager adapter.UserManager
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
|
||||
|
|
@ -47,12 +49,25 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
} else {
|
||||
udpTimeout = C.UDPTimeout
|
||||
}
|
||||
authenticator := auth.NewAuthenticator(options.Users)
|
||||
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||
if userManager != nil {
|
||||
creds, err := userManager.GetCredentials(C.TypeMixed)
|
||||
if err == nil && len(creds) > 0 {
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeMixed, tag),
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
authenticator: auth.NewAuthenticator(options.Users),
|
||||
authenticator: authenticator,
|
||||
udpTimeout: udpTimeout,
|
||||
userManager: userManager,
|
||||
}
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
||||
|
|
@ -88,9 +103,28 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeMixed)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
h.authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return common.Close(
|
||||
h.listener,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
|
|
@ -24,6 +25,7 @@ import (
|
|||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
sHttp "github.com/sagernet/sing/protocol/http"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
|
@ -51,9 +53,22 @@ type Inbound struct {
|
|||
tlsConfig tls.ServerConfig
|
||||
httpServer *http.Server
|
||||
h3Server io.Closer
|
||||
userManager adapter.UserManager
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (adapter.Inbound, error) {
|
||||
authenticator := auth.NewAuthenticator(options.Users)
|
||||
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||
if userManager != nil {
|
||||
creds, err := userManager.GetCredentials(C.TypeNaive)
|
||||
if err == nil && len(creds) > 0 {
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeNaive, tag),
|
||||
ctx: ctx,
|
||||
|
|
@ -66,14 +81,15 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
}),
|
||||
networkIsDefault: options.Network == "",
|
||||
network: options.Network.Build(),
|
||||
authenticator: auth.NewAuthenticator(options.Users),
|
||||
authenticator: authenticator,
|
||||
userManager: userManager,
|
||||
}
|
||||
if common.Contains(inbound.network, N.NetworkUDP) {
|
||||
if options.TLS == nil || !options.TLS.Enabled {
|
||||
return nil, E.New("TLS is required for QUIC server")
|
||||
}
|
||||
}
|
||||
if len(options.Users) == 0 {
|
||||
if len(options.Users) == 0 && userManager == nil {
|
||||
return nil, E.New("missing users")
|
||||
}
|
||||
if options.TLS != nil {
|
||||
|
|
@ -96,6 +112,9 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
|
|||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
if n.userManager != nil {
|
||||
go n.credentialLoop()
|
||||
}
|
||||
if common.Contains(n.network, N.NetworkTCP) {
|
||||
tcpListener, err := n.listener.ListenTCP()
|
||||
if err != nil {
|
||||
|
|
@ -138,6 +157,22 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := n.userManager.GetCredentials(C.TypeNaive)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
n.authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Inbound) Close() error {
|
||||
return common.Close(
|
||||
n.listener,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
|
|
@ -35,7 +36,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
} else if options.Managed && (len(options.Users) > 0 || len(options.Destinations) > 0) {
|
||||
return nil, E.New("users and destinations options are not supported in managed servers")
|
||||
}
|
||||
if len(options.Users) > 0 || options.Managed {
|
||||
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||
if len(options.Users) > 0 || options.Managed || userManager != nil {
|
||||
return newMultiInbound(ctx, router, logger, tag, options)
|
||||
} else if len(options.Destinations) > 0 {
|
||||
return newRelayInbound(ctx, router, logger, tag, options)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
sService "github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -35,13 +36,15 @@ var (
|
|||
|
||||
type MultiInbound struct {
|
||||
inbound.Adapter
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
service shadowsocks.MultiService[int]
|
||||
users []option.ShadowsocksUser
|
||||
tracker adapter.SSMTracker
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
service shadowsocks.MultiService[int]
|
||||
users []option.ShadowsocksUser
|
||||
userNameList []string
|
||||
userManager adapter.UserManager
|
||||
tracker adapter.SSMTracker
|
||||
}
|
||||
|
||||
func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) {
|
||||
|
|
@ -83,18 +86,35 @@ func newMultiInbound(ctx context.Context, router adapter.Router, logger log.Cont
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(options.Users) > 0 {
|
||||
err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int {
|
||||
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||
users := options.Users
|
||||
if len(users) == 0 && userManager != nil {
|
||||
creds, uErr := userManager.GetCredentials(C.TypeShadowsocks)
|
||||
if uErr == nil && len(creds) > 0 {
|
||||
users = make([]option.ShadowsocksUser, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = option.ShadowsocksUser{Name: c.Username, Password: c.Credential}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(users) > 0 {
|
||||
err = service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user option.ShadowsocksUser) int {
|
||||
return index
|
||||
}), common.Map(options.Users, func(user option.ShadowsocksUser) string {
|
||||
}), common.Map(users, func(user option.ShadowsocksUser) string {
|
||||
return user.Password
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
userNameList := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userNameList[i] = u.Name
|
||||
}
|
||||
inbound.service = service
|
||||
inbound.users = options.Users
|
||||
inbound.users = users
|
||||
inbound.userNameList = userNameList
|
||||
inbound.userManager = userManager
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
|
|
@ -111,9 +131,33 @@ func (h *MultiInbound) Start(stage adapter.StartStage) error {
|
|||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *MultiInbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeShadowsocks)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
userIndices := make([]int, len(creds))
|
||||
userPasswords := make([]string, len(creds))
|
||||
userNameList := make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userIndices[i] = i
|
||||
userPasswords[i] = c.Credential
|
||||
userNameList[i] = c.Username
|
||||
}
|
||||
h.service.UpdateUsersWithPasswords(userIndices, userPasswords)
|
||||
h.userNameList = userNameList
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MultiInbound) Close() error {
|
||||
return h.listener.Close()
|
||||
}
|
||||
|
|
@ -163,7 +207,7 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat
|
|||
if !loaded {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
@ -186,7 +230,7 @@ func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketCon
|
|||
if !loaded {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
|
|
@ -33,6 +34,7 @@ type Inbound struct {
|
|||
listener *listener.Listener
|
||||
authenticator *auth.Authenticator
|
||||
udpTimeout time.Duration
|
||||
userManager adapter.UserManager
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) (adapter.Inbound, error) {
|
||||
|
|
@ -42,12 +44,25 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
} else {
|
||||
udpTimeout = C.UDPTimeout
|
||||
}
|
||||
authenticator := auth.NewAuthenticator(options.Users)
|
||||
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||
if userManager != nil {
|
||||
creds, err := userManager.GetCredentials(C.TypeSOCKS)
|
||||
if err == nil && len(creds) > 0 {
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeSOCKS, tag),
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
authenticator: auth.NewAuthenticator(options.Users),
|
||||
authenticator: authenticator,
|
||||
udpTimeout: udpTimeout,
|
||||
userManager: userManager,
|
||||
}
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
|
|
@ -63,9 +78,28 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeSOCKS)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
users := make([]auth.User, len(creds))
|
||||
for i, c := range creds {
|
||||
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||
}
|
||||
h.authenticator = auth.NewAuthenticator(users)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return h.listener.Close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
|
|
@ -21,6 +22,7 @@ import (
|
|||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sService "github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
|
|
@ -36,6 +38,8 @@ type Inbound struct {
|
|||
listener *listener.Listener
|
||||
service *trojan.Service[int]
|
||||
users []option.TrojanUser
|
||||
userNameList []string
|
||||
userManager adapter.UserManager
|
||||
tlsConfig tls.ServerConfig
|
||||
fallbackAddr M.Socksaddr
|
||||
fallbackAddrTLSNextProto map[string]M.Socksaddr
|
||||
|
|
@ -87,13 +91,40 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil)
|
||||
}
|
||||
service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger)
|
||||
err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int {
|
||||
return index
|
||||
}), common.Map(options.Users, func(it option.TrojanUser) string {
|
||||
return it.Password
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var err error
|
||||
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||
|
||||
var userNameList []string
|
||||
if userManager != nil {
|
||||
creds, uErr := userManager.GetCredentials(C.TypeTrojan)
|
||||
if uErr == nil && len(creds) > 0 {
|
||||
userIndices := make([]int, len(creds))
|
||||
userPasswords := make([]string, len(creds))
|
||||
userNameList = make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userIndices[i] = i
|
||||
userPasswords[i] = c.Credential
|
||||
userNameList[i] = c.Username
|
||||
}
|
||||
uErr = service.UpdateUsers(userIndices, userPasswords)
|
||||
if uErr != nil {
|
||||
return nil, uErr
|
||||
}
|
||||
}
|
||||
inbound.userManager = userManager
|
||||
} else {
|
||||
userNameList = make([]string, len(options.Users))
|
||||
for i, u := range options.Users {
|
||||
userNameList[i] = u.Name
|
||||
}
|
||||
err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int {
|
||||
return index
|
||||
}), common.Map(options.Users, func(it option.TrojanUser) string {
|
||||
return it.Password
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.Transport != nil {
|
||||
inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound))
|
||||
|
|
@ -106,6 +137,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
return nil, err
|
||||
}
|
||||
inbound.service = service
|
||||
inbound.userNameList = userNameList
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
|
|
@ -126,6 +158,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
if h.transport == nil {
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
|
@ -156,6 +191,27 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeTrojan)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
userIndices := make([]int, len(creds))
|
||||
userPasswords := make([]string, len(creds))
|
||||
userNameList := make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userIndices[i] = i
|
||||
userPasswords[i] = c.Credential
|
||||
userNameList[i] = c.Username
|
||||
}
|
||||
h.service.UpdateUsers(userIndices, userPasswords)
|
||||
h.userNameList = userNameList
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return common.Close(
|
||||
h.listener,
|
||||
|
|
@ -189,7 +245,7 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
|
|||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
@ -207,7 +263,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me
|
|||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package tuic
|
|||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
|
|
@ -20,6 +21,7 @@ import (
|
|||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sService "github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
|
@ -36,6 +38,7 @@ type Inbound struct {
|
|||
tlsConfig tls.ServerConfig
|
||||
server *tuic.Service[int]
|
||||
userNameList []string
|
||||
userManager adapter.UserManager
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) {
|
||||
|
|
@ -87,26 +90,61 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var userList []int
|
||||
var userNameList []string
|
||||
var userUUIDList [][16]byte
|
||||
var userPasswordList []string
|
||||
for index, user := range options.Users {
|
||||
if user.UUID == "" {
|
||||
return nil, E.New("missing uuid for user ", index)
|
||||
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||
|
||||
if userManager != nil {
|
||||
creds, err := userManager.GetCredentials(C.TypeTUIC)
|
||||
if err == nil && len(creds) > 0 {
|
||||
userList := make([]int, 0, len(creds))
|
||||
userNameList := make([]string, 0, len(creds))
|
||||
userUUIDList := make([][16]byte, 0, len(creds))
|
||||
userPasswordList := make([]string, 0, len(creds))
|
||||
for i, c := range creds {
|
||||
parts := strings.SplitN(c.Credential, ":", 2)
|
||||
if len(parts) < 1 || parts[0] == "" {
|
||||
continue
|
||||
}
|
||||
userUUID, pErr := uuid.FromString(parts[0])
|
||||
if pErr != nil {
|
||||
continue
|
||||
}
|
||||
password := ""
|
||||
if len(parts) == 2 {
|
||||
password = parts[1]
|
||||
}
|
||||
userList = append(userList, i)
|
||||
userNameList = append(userNameList, c.Username)
|
||||
userUUIDList = append(userUUIDList, userUUID)
|
||||
userPasswordList = append(userPasswordList, password)
|
||||
}
|
||||
if len(userList) > 0 {
|
||||
service.UpdateUsers(userList, userUUIDList, userPasswordList)
|
||||
inbound.userNameList = userNameList
|
||||
}
|
||||
}
|
||||
userUUID, err := uuid.FromString(user.UUID)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid uuid for user ", index)
|
||||
inbound.userManager = userManager
|
||||
} else {
|
||||
userList := make([]int, 0, len(options.Users))
|
||||
userNameList := make([]string, 0, len(options.Users))
|
||||
userUUIDList := make([][16]byte, 0, len(options.Users))
|
||||
userPasswordList := make([]string, 0, len(options.Users))
|
||||
for index, user := range options.Users {
|
||||
if user.UUID == "" {
|
||||
return nil, E.New("missing uuid for user ", index)
|
||||
}
|
||||
userUUID, err := uuid.FromString(user.UUID)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid uuid for user ", index)
|
||||
}
|
||||
userList = append(userList, index)
|
||||
userNameList = append(userNameList, user.Name)
|
||||
userUUIDList = append(userUUIDList, userUUID)
|
||||
userPasswordList = append(userPasswordList, user.Password)
|
||||
}
|
||||
userList = append(userList, index)
|
||||
userNameList = append(userNameList, user.Name)
|
||||
userUUIDList = append(userUUIDList, userUUID)
|
||||
userPasswordList = append(userPasswordList, user.Password)
|
||||
service.UpdateUsers(userList, userUUIDList, userPasswordList)
|
||||
inbound.userNameList = userNameList
|
||||
}
|
||||
service.UpdateUsers(userList, userUUIDList, userPasswordList)
|
||||
inbound.server = service
|
||||
inbound.userNameList = userNameList
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +202,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
packetConn, err := h.listener.ListenUDP()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -171,6 +212,43 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return h.server.Start(packetConn)
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeTUIC)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
userList := make([]int, 0, len(creds))
|
||||
userNameList := make([]string, 0, len(creds))
|
||||
userUUIDList := make([][16]byte, 0, len(creds))
|
||||
userPasswordList := make([]string, 0, len(creds))
|
||||
for i, c := range creds {
|
||||
parts := strings.SplitN(c.Credential, ":", 2)
|
||||
if len(parts) < 1 || parts[0] == "" {
|
||||
continue
|
||||
}
|
||||
userUUID, pErr := uuid.FromString(parts[0])
|
||||
if pErr != nil {
|
||||
continue
|
||||
}
|
||||
password := ""
|
||||
if len(parts) == 2 {
|
||||
password = parts[1]
|
||||
}
|
||||
userList = append(userList, i)
|
||||
userNameList = append(userNameList, c.Username)
|
||||
userUUIDList = append(userUUIDList, userUUID)
|
||||
userPasswordList = append(userPasswordList, password)
|
||||
}
|
||||
if len(userList) > 0 {
|
||||
h.server.UpdateUsers(userList, userUUIDList, userPasswordList)
|
||||
h.userNameList = userNameList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return common.Close(
|
||||
h.listener,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
|
|
@ -25,6 +26,7 @@ import (
|
|||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sService "github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
|
|
@ -35,14 +37,16 @@ var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
|||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
users []option.VLESSUser
|
||||
service *vless.Service[int]
|
||||
tlsConfig tls.ServerConfig
|
||||
transport adapter.V2RayServerTransport
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
users []option.VLESSUser
|
||||
userNameList []string
|
||||
userManager adapter.UserManager
|
||||
service *vless.Service[int]
|
||||
tlsConfig tls.ServerConfig
|
||||
transport adapter.V2RayServerTransport
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) {
|
||||
|
|
@ -59,14 +63,40 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
return nil, err
|
||||
}
|
||||
service := vless.NewService[int](logger, adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx))
|
||||
service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int {
|
||||
return index
|
||||
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
||||
return it.UUID
|
||||
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
||||
return it.Flow
|
||||
}))
|
||||
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||
|
||||
var userNameList []string
|
||||
if userManager != nil {
|
||||
creds, err := userManager.GetCredentials(C.TypeVLESS)
|
||||
if err == nil && len(creds) > 0 {
|
||||
userIndices := make([]int, len(creds))
|
||||
userUUIDs := make([]string, len(creds))
|
||||
userFlows := make([]string, len(creds))
|
||||
userNameList = make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userIndices[i] = i
|
||||
userUUIDs[i] = c.Credential
|
||||
userFlows[i] = ""
|
||||
userNameList[i] = c.Username
|
||||
}
|
||||
service.UpdateUsers(userIndices, userUUIDs, userFlows)
|
||||
}
|
||||
inbound.userManager = userManager
|
||||
} else {
|
||||
userNameList = make([]string, len(inbound.users))
|
||||
for i, u := range inbound.users {
|
||||
userNameList[i] = u.Name
|
||||
}
|
||||
service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int {
|
||||
return index
|
||||
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
||||
return it.UUID
|
||||
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
||||
return it.Flow
|
||||
}))
|
||||
}
|
||||
inbound.service = service
|
||||
inbound.userNameList = userNameList
|
||||
if options.TLS != nil {
|
||||
inbound.tlsConfig, err = tls.NewServerWithOptions(tls.ServerOptions{
|
||||
Context: ctx,
|
||||
|
|
@ -108,6 +138,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
if h.transport == nil {
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
|
@ -138,6 +171,29 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeVLESS)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
userIndices := make([]int, len(creds))
|
||||
userUUIDs := make([]string, len(creds))
|
||||
userFlows := make([]string, len(creds))
|
||||
userNameList := make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userIndices[i] = i
|
||||
userUUIDs[i] = c.Credential
|
||||
userFlows[i] = ""
|
||||
userNameList[i] = c.Username
|
||||
}
|
||||
h.service.UpdateUsers(userIndices, userUUIDs, userFlows)
|
||||
h.userNameList = userNameList
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return common.Close(
|
||||
h.service,
|
||||
|
|
@ -172,7 +228,7 @@ func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
|||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
@ -190,7 +246,7 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
|
|
@ -26,6 +27,7 @@ import (
|
|||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
sService "github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
|
|
@ -36,14 +38,16 @@ var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
|||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
service *vmess.Service[int]
|
||||
users []option.VMessUser
|
||||
tlsConfig tls.ServerConfig
|
||||
transport adapter.V2RayServerTransport
|
||||
ctx context.Context
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
service *vmess.Service[int]
|
||||
users []option.VMessUser
|
||||
userNameList []string
|
||||
userManager adapter.UserManager
|
||||
tlsConfig tls.ServerConfig
|
||||
transport adapter.V2RayServerTransport
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) {
|
||||
|
|
@ -67,17 +71,46 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||
serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection())
|
||||
}
|
||||
service := vmess.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...)
|
||||
inbound.service = service
|
||||
err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int {
|
||||
return index
|
||||
}), common.Map(options.Users, func(it option.VMessUser) string {
|
||||
return it.UUID
|
||||
}), common.Map(options.Users, func(it option.VMessUser) int {
|
||||
return it.AlterId
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||
|
||||
var userNameList []string
|
||||
if userManager != nil {
|
||||
creds, uErr := userManager.GetCredentials(C.TypeVMess)
|
||||
if uErr == nil && len(creds) > 0 {
|
||||
userIndices := make([]int, len(creds))
|
||||
userUUIDs := make([]string, len(creds))
|
||||
userAlterIds := make([]int, len(creds))
|
||||
userNameList = make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userIndices[i] = i
|
||||
userUUIDs[i] = c.Credential
|
||||
userAlterIds[i] = 0
|
||||
userNameList[i] = c.Username
|
||||
}
|
||||
uErr = service.UpdateUsers(userIndices, userUUIDs, userAlterIds)
|
||||
if uErr != nil {
|
||||
return nil, uErr
|
||||
}
|
||||
}
|
||||
inbound.userManager = userManager
|
||||
} else {
|
||||
userNameList = make([]string, len(options.Users))
|
||||
for i, u := range options.Users {
|
||||
userNameList[i] = u.Name
|
||||
}
|
||||
err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int {
|
||||
return index
|
||||
}), common.Map(options.Users, func(it option.VMessUser) string {
|
||||
return it.UUID
|
||||
}), common.Map(options.Users, func(it option.VMessUser) int {
|
||||
return it.AlterId
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
inbound.service = service
|
||||
inbound.userNameList = userNameList
|
||||
if options.TLS != nil {
|
||||
inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
|
|
@ -114,6 +147,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if h.userManager != nil {
|
||||
go h.credentialLoop()
|
||||
}
|
||||
if h.transport == nil {
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
|
@ -144,6 +180,29 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *Inbound) credentialLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
creds, err := h.userManager.GetCredentials(C.TypeVMess)
|
||||
if err != nil || len(creds) == 0 {
|
||||
continue
|
||||
}
|
||||
userIndices := make([]int, len(creds))
|
||||
userUUIDs := make([]string, len(creds))
|
||||
userAlterIds := make([]int, len(creds))
|
||||
userNameList := make([]string, len(creds))
|
||||
for i, c := range creds {
|
||||
userIndices[i] = i
|
||||
userUUIDs[i] = c.Credential
|
||||
userAlterIds[i] = 0
|
||||
userNameList[i] = c.Username
|
||||
}
|
||||
h.service.UpdateUsers(userIndices, userUUIDs, userAlterIds)
|
||||
h.userNameList = userNameList
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return common.Close(
|
||||
h.service,
|
||||
|
|
@ -178,7 +237,7 @@ func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
|||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
@ -196,7 +255,7 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
user := h.userNameList[userIndex]
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,badlinkname,tfogo_checklinkname0
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_usbip,badlinkname,tfogo_checklinkname0
|
||||
|
|
@ -1 +1 @@
|
|||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,badlinkname,tfogo_checklinkname0
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_usbip,badlinkname,tfogo_checklinkname0
|
||||
|
|
@ -1 +1 @@
|
|||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_purego,with_usbip,badlinkname,tfogo_checklinkname0
|
||||
|
|
@ -40,6 +40,7 @@ type Router struct {
|
|||
leaseFiles []string
|
||||
ruleSets []adapter.RuleSet
|
||||
ruleSetMap map[string]adapter.RuleSet
|
||||
ruleSetUpdater *R.RuleSetUpdater
|
||||
processSearcher process.Searcher
|
||||
processCache freelru.Cache[processCacheKey, processCacheEntry]
|
||||
neighborResolver adapter.NeighborResolver
|
||||
|
|
@ -156,6 +157,7 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
|||
if startContext != nil {
|
||||
startContext.Close()
|
||||
}
|
||||
r.ruleSetUpdater = R.NewRuleSetUpdater(r.ctx, r.ruleSets)
|
||||
r.network.Initialize(r.ruleSets)
|
||||
needFindProcess := r.needFindProcess
|
||||
for _, ruleSet := range r.ruleSets {
|
||||
|
|
@ -201,13 +203,8 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
|||
return E.Cause(err, "initialize rule[", i, "]")
|
||||
}
|
||||
}
|
||||
for _, ruleSet := range r.ruleSets {
|
||||
monitor.Start("post start rule_set[", ruleSet.Name(), "]")
|
||||
err := ruleSet.PostStart()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]")
|
||||
}
|
||||
if r.ruleSetUpdater != nil {
|
||||
r.ruleSetUpdater.Start()
|
||||
}
|
||||
r.started = true
|
||||
return nil
|
||||
|
|
@ -237,6 +234,13 @@ func (r *Router) Close() error {
|
|||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
if r.ruleSetUpdater != nil {
|
||||
monitor.Start("close rule-set updater")
|
||||
err = E.Append(err, r.ruleSetUpdater.Close(), func(err error) error {
|
||||
return E.Cause(err, "close rule-set updater")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
for i, ruleSet := range r.ruleSets {
|
||||
monitor.Start("close rule-set[", i, "]")
|
||||
err = E.Append(err, ruleSet.Close(), func(err error) error {
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@ func (f *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) PostStart() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
return adapter.RuleSetMetadata{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,10 +153,6 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) PostStart() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -43,7 +42,6 @@ type RemoteRuleSet struct {
|
|||
metadata adapter.RuleSetMetadata
|
||||
lastUpdated time.Time
|
||||
lastEtag string
|
||||
updateTicker *time.Ticker
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
|
|
@ -102,12 +100,6 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.
|
|||
return E.Cause(err, "initial rule-set: ", s.options.Tag)
|
||||
}
|
||||
}
|
||||
s.updateTicker = time.NewTicker(s.updateInterval)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) PostStart() error {
|
||||
go s.loopUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -197,21 +189,6 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) loopUpdate() {
|
||||
if time.Since(s.lastUpdated) > s.updateInterval {
|
||||
s.updateOnce()
|
||||
}
|
||||
for {
|
||||
runtime.GC()
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-s.updateTicker.C:
|
||||
s.updateOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) updateOnce() {
|
||||
err := s.fetch(s.ctx, false)
|
||||
if err != nil {
|
||||
|
|
@ -312,9 +289,6 @@ func (s *RemoteRuleSet) resolveTransport() (adapter.HTTPTransport, error) {
|
|||
func (s *RemoteRuleSet) Close() error {
|
||||
s.rules = nil
|
||||
s.cancel()
|
||||
if s.updateTicker != nil {
|
||||
s.updateTicker.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
87
route/rule/rule_set_updater.go
Normal file
87
route/rule/rule_set_updater.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
type RuleSetUpdater struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
ruleSets []*RemoteRuleSet
|
||||
}
|
||||
|
||||
func NewRuleSetUpdater(ctx context.Context, ruleSets []adapter.RuleSet) *RuleSetUpdater {
|
||||
var remoteRuleSets []*RemoteRuleSet
|
||||
for _, ruleSet := range ruleSets {
|
||||
remoteRuleSet, isRemote := ruleSet.(*RemoteRuleSet)
|
||||
if isRemote {
|
||||
remoteRuleSets = append(remoteRuleSets, remoteRuleSet)
|
||||
}
|
||||
}
|
||||
if len(remoteRuleSets) == 0 {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &RuleSetUpdater{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
ruleSets: remoteRuleSets,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *RuleSetUpdater) Start() {
|
||||
go u.loopUpdate()
|
||||
}
|
||||
|
||||
func (u *RuleSetUpdater) Close() error {
|
||||
u.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *RuleSetUpdater) loopUpdate() {
|
||||
nextUpdates := make([]time.Time, len(u.ruleSets))
|
||||
for i, ruleSet := range u.ruleSets {
|
||||
nextUpdates[i] = ruleSet.lastUpdated.Add(ruleSet.updateInterval)
|
||||
}
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
now := time.Now()
|
||||
var updated bool
|
||||
for i, ruleSet := range u.ruleSets {
|
||||
if now.Before(nextUpdates[i]) {
|
||||
continue
|
||||
}
|
||||
ruleSet.updateOnce()
|
||||
nextUpdates[i] = now.Add(ruleSet.updateInterval)
|
||||
updated = true
|
||||
}
|
||||
if updated {
|
||||
runtime.GC()
|
||||
}
|
||||
timer.Reset(waitUntilNext(nextUpdates))
|
||||
}
|
||||
}
|
||||
|
||||
func waitUntilNext(nextUpdates []time.Time) time.Duration {
|
||||
next := nextUpdates[0]
|
||||
for _, nextUpdate := range nextUpdates[1:] {
|
||||
if nextUpdate.Before(next) {
|
||||
next = nextUpdate
|
||||
}
|
||||
}
|
||||
wait := time.Until(next)
|
||||
if wait < 0 {
|
||||
return 0
|
||||
}
|
||||
return wait
|
||||
}
|
||||
310
service/api/dashboard.go
Normal file
310
service/api/dashboard.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
const (
|
||||
dashboardRoutePrefix = "/dashboard/"
|
||||
dashboardEtagFileName = ".etag"
|
||||
defaultDashboardURL = "https://github.com/SagerNet/sing-box-dashboard/archive/refs/heads/gh-pages.zip"
|
||||
)
|
||||
|
||||
type dashboardStatus int
|
||||
|
||||
const (
|
||||
dashboardEmpty dashboardStatus = iota
|
||||
dashboardManaged
|
||||
dashboardUserProvided
|
||||
)
|
||||
|
||||
type dashboard struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
options option.APIDashboardOptions
|
||||
path string
|
||||
url string
|
||||
updateInterval time.Duration
|
||||
fileServer http.Handler
|
||||
httpClient *http.Client
|
||||
lastEtag string
|
||||
lastUpdated time.Time
|
||||
}
|
||||
|
||||
func newDashboard(ctx context.Context, logger log.ContextLogger, options option.APIDashboardOptions) *dashboard {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
path = "dashboard"
|
||||
}
|
||||
path = filemanager.BasePath(ctx, os.ExpandEnv(path))
|
||||
url := options.DownloadURL
|
||||
if url == "" {
|
||||
url = defaultDashboardURL
|
||||
}
|
||||
updateInterval := 24 * time.Hour
|
||||
if options.UpdateInterval > 0 {
|
||||
updateInterval = time.Duration(options.UpdateInterval)
|
||||
}
|
||||
return &dashboard{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
options: options,
|
||||
path: path,
|
||||
url: url,
|
||||
updateInterval: updateInterval,
|
||||
fileServer: http.StripPrefix(dashboardRoutePrefix, http.FileServer(dashboardDir(path))),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dashboard) start() error {
|
||||
transport, err := d.resolveTransport()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create dashboard http client")
|
||||
}
|
||||
d.httpClient = &http.Client{Transport: transport}
|
||||
go d.loopUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dashboard) close() error {
|
||||
d.cancel()
|
||||
if d.httpClient != nil {
|
||||
d.httpClient.CloseIdleConnections()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dashboard) resolveTransport() (adapter.HTTPTransport, error) {
|
||||
httpClientManager := service.FromContext[adapter.HTTPClientManager](d.ctx)
|
||||
if httpClientManager == nil {
|
||||
return nil, E.New("missing http client manager in context")
|
||||
}
|
||||
if d.options.HTTPClient != nil && !d.options.HTTPClient.IsEmpty() {
|
||||
return httpClientManager.ResolveTransport(d.ctx, d.logger, *d.options.HTTPClient)
|
||||
}
|
||||
defaultTransport := httpClientManager.DefaultTransport()
|
||||
if defaultTransport == nil {
|
||||
return nil, E.New("default http client transport is not initialized")
|
||||
}
|
||||
return defaultTransport, nil
|
||||
}
|
||||
|
||||
func (d *dashboard) serveHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
if strings.HasPrefix(request.URL.Path, dashboardRoutePrefix) {
|
||||
d.fileServer.ServeHTTP(writer, request)
|
||||
return
|
||||
}
|
||||
http.Redirect(writer, request, dashboardRoutePrefix, http.StatusFound)
|
||||
}
|
||||
|
||||
func (d *dashboard) loopUpdate() {
|
||||
status := d.loadState()
|
||||
if status == dashboardUserProvided {
|
||||
d.logger.Info("dashboard: serving user-provided files at ", d.path, ", auto-update disabled")
|
||||
return
|
||||
}
|
||||
var nextUpdate time.Time
|
||||
if status == dashboardManaged {
|
||||
nextUpdate = d.lastUpdated.Add(d.updateInterval)
|
||||
}
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
now := time.Now()
|
||||
if !now.Before(nextUpdate) {
|
||||
err := d.fetch(d.ctx)
|
||||
if err != nil {
|
||||
d.logger.Error(E.Cause(err, "update dashboard"))
|
||||
nextUpdate = now.Add(d.updateInterval)
|
||||
} else {
|
||||
nextUpdate = d.lastUpdated.Add(d.updateInterval)
|
||||
}
|
||||
}
|
||||
timer.Reset(max(time.Until(nextUpdate), 0))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dashboard) loadState() dashboardStatus {
|
||||
entries, err := os.ReadDir(d.path)
|
||||
if err != nil {
|
||||
return dashboardEmpty
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return dashboardEmpty
|
||||
}
|
||||
etagPath := filepath.Join(d.path, dashboardEtagFileName)
|
||||
etagBytes, err := os.ReadFile(etagPath)
|
||||
if err != nil {
|
||||
return dashboardUserProvided
|
||||
}
|
||||
d.lastEtag = strings.TrimSpace(string(etagBytes))
|
||||
info, err := os.Stat(etagPath)
|
||||
if err == nil {
|
||||
d.lastUpdated = info.ModTime()
|
||||
}
|
||||
return dashboardManaged
|
||||
}
|
||||
|
||||
func (d *dashboard) fetch(ctx context.Context) error {
|
||||
d.logger.Info("updating dashboard from URL: ", d.url)
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, d.url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.lastEtag != "" {
|
||||
request.Header.Set("If-None-Match", d.lastEtag)
|
||||
}
|
||||
defer d.httpClient.CloseIdleConnections()
|
||||
response, err := d.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
case http.StatusNotModified:
|
||||
d.lastUpdated = time.Now()
|
||||
err = filemanager.WriteFile(d.ctx, filepath.Join(d.path, dashboardEtagFileName), []byte(d.lastEtag), 0o644)
|
||||
if err != nil {
|
||||
d.logger.Warn(E.Cause(err, "save dashboard update time"))
|
||||
}
|
||||
d.logger.Info("dashboard: not modified")
|
||||
return nil
|
||||
default:
|
||||
return E.New("unexpected status: ", response.Status)
|
||||
}
|
||||
etag := response.Header.Get("Etag")
|
||||
err = d.extract(response.Body, etag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.lastEtag = etag
|
||||
d.lastUpdated = time.Now()
|
||||
d.logger.Info("dashboard: updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dashboard) extract(body io.Reader, etag string) error {
|
||||
tempFile, err := filemanager.CreateTemp(d.ctx, "sing-box-dashboard-*.zip")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempZipPath := tempFile.Name()
|
||||
defer os.Remove(tempZipPath)
|
||||
_, err = io.Copy(tempFile, body)
|
||||
tempFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := zip.OpenReader(tempZipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
tempDir := d.path + ".tmp"
|
||||
err = filemanager.RemoveAll(d.ctx, tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = filemanager.MkdirAll(d.ctx, tempDir, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trimDir := zipIsInSingleDirectory(reader.File)
|
||||
for _, file := range reader.File {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
pathElements := strings.Split(file.Name, "/")
|
||||
if trimDir {
|
||||
pathElements = pathElements[1:]
|
||||
}
|
||||
if len(pathElements) == 0 {
|
||||
continue
|
||||
}
|
||||
relativePath := filepath.Join(pathElements...)
|
||||
if !filepath.IsLocal(relativePath) {
|
||||
filemanager.RemoveAll(d.ctx, tempDir)
|
||||
return E.New("invalid dashboard archive entry: ", file.Name)
|
||||
}
|
||||
savePath := filepath.Join(tempDir, relativePath)
|
||||
err = filemanager.MkdirAll(d.ctx, filepath.Dir(savePath), 0o755)
|
||||
if err != nil {
|
||||
filemanager.RemoveAll(d.ctx, tempDir)
|
||||
return err
|
||||
}
|
||||
err = extractZipEntry(d.ctx, file, savePath)
|
||||
if err != nil {
|
||||
filemanager.RemoveAll(d.ctx, tempDir)
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = filemanager.WriteFile(d.ctx, filepath.Join(tempDir, dashboardEtagFileName), []byte(etag), 0o644)
|
||||
if err != nil {
|
||||
filemanager.RemoveAll(d.ctx, tempDir)
|
||||
return err
|
||||
}
|
||||
err = filemanager.RemoveAll(d.ctx, d.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempDir, d.path)
|
||||
}
|
||||
|
||||
func extractZipEntry(ctx context.Context, file *zip.File, savePath string) error {
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
writer, err := filemanager.Create(ctx, savePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer writer.Close()
|
||||
_, err = io.Copy(writer, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
// GitHub archives wrap every file under a single "<repo>-<branch>/" top-level directory.
|
||||
func zipIsInSingleDirectory(files []*zip.File) bool {
|
||||
var dirName string
|
||||
for _, file := range files {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
pathElements := strings.Split(file.Name, "/")
|
||||
if len(pathElements) < 2 {
|
||||
return false
|
||||
}
|
||||
if dirName == "" {
|
||||
dirName = pathElements[0]
|
||||
} else if dirName != pathElements[0] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return dirName != ""
|
||||
}
|
||||
18
service/api/dashboard_fs.go
Normal file
18
service/api/dashboard_fs.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package api
|
||||
|
||||
import "net/http"
|
||||
|
||||
type dashboardDir http.Dir
|
||||
|
||||
func (d dashboardDir) Open(name string) (http.File, error) {
|
||||
file, err := http.Dir(d).Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fileWrapper{file}, nil
|
||||
}
|
||||
|
||||
// workaround for #2345 #2596
|
||||
type fileWrapper struct {
|
||||
http.File
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ type Service struct {
|
|||
startedService *daemon.StartedService
|
||||
grpcServer *grpc.Server
|
||||
httpServer *http.Server
|
||||
dashboard *dashboard
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.APIServiceOptions) (adapter.Service, error) {
|
||||
|
|
@ -63,6 +64,9 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
|||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
}
|
||||
if options.Dashboard != nil && options.Dashboard.Enabled {
|
||||
s.dashboard = newDashboard(ctx, logger, *options.Dashboard)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
|
@ -72,8 +76,14 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
|||
}
|
||||
s.startedService = daemon.NewAttachedService(s.ctx)
|
||||
s.grpcServer = daemon.NewServer(s.startedService, s.options.Secret)
|
||||
if s.dashboard != nil {
|
||||
err := s.dashboard.start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start dashboard")
|
||||
}
|
||||
}
|
||||
s.httpServer = &http.Server{
|
||||
Handler: h2c.NewHandler(newHTTPHandler(s.logger, s.grpcServer, s.options), new(http2.Server)),
|
||||
Handler: h2c.NewHandler(newHTTPHandler(s.logger, s.grpcServer, s.options, s.dashboard), new(http2.Server)),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return s.ctx
|
||||
},
|
||||
|
|
@ -108,6 +118,9 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
|||
|
||||
func (s *Service) Close() error {
|
||||
s.cancel()
|
||||
if s.dashboard != nil {
|
||||
s.dashboard.close()
|
||||
}
|
||||
if s.httpServer != nil {
|
||||
s.httpServer.Close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ const (
|
|||
// (https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) and gRPC-Web
|
||||
// streams over WebSocket, wire compatible with the improbable-eng/grpc-web
|
||||
// client transports.
|
||||
func newHTTPHandler(logger log.ContextLogger, grpcServer *grpc.Server, options option.APIServiceOptions) http.Handler {
|
||||
func newHTTPHandler(logger log.ContextLogger, grpcServer *grpc.Server, options option.APIServiceOptions, dashboard *dashboard) http.Handler {
|
||||
allowedOrigins := options.AccessControlAllowOrigin
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowedOrigins = []string{"*"}
|
||||
}
|
||||
corsHandler := cors.New(cors.Options{
|
||||
AllowedOrigins: allowedOrigins,
|
||||
AllowedMethods: []string{http.MethodPost, http.MethodOptions},
|
||||
AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Grpc-Web", "X-User-Agent", "Grpc-Timeout"},
|
||||
ExposedHeaders: []string{"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
|
||||
AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork,
|
||||
|
|
@ -42,12 +42,14 @@ func newHTTPHandler(logger log.ContextLogger, grpcServer *grpc.Server, options o
|
|||
return corsHandler.Handler(&webBridge{
|
||||
logger: logger,
|
||||
grpcServer: grpcServer,
|
||||
dashboard: dashboard,
|
||||
})
|
||||
}
|
||||
|
||||
type webBridge struct {
|
||||
logger log.ContextLogger
|
||||
grpcServer *grpc.Server
|
||||
dashboard *dashboard
|
||||
}
|
||||
|
||||
func (b *webBridge) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
|
|
@ -59,6 +61,8 @@ func (b *webBridge) ServeHTTP(writer http.ResponseWriter, request *http.Request)
|
|||
b.serveWeb(writer, request)
|
||||
case request.ProtoMajor == 2 && strings.HasPrefix(contentType, contentTypeGRPC):
|
||||
b.grpcServer.ServeHTTP(writer, request)
|
||||
case b.dashboard != nil:
|
||||
b.dashboard.serveHTTP(writer, request)
|
||||
default:
|
||||
http.NotFound(writer, request)
|
||||
}
|
||||
|
|
|
|||
62
service/usbip/client.go
Normal file
62
service/usbip/client.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-usbip"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ClientService struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
inner *usbip.ClientService
|
||||
}
|
||||
|
||||
func NewClientService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPClientServiceOptions) (adapter.Service, error) {
|
||||
serviceDialer, err := dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: option.DialerOptions{
|
||||
Detour: options.Detour,
|
||||
},
|
||||
RemoteIsDomain: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create dialer")
|
||||
}
|
||||
inner, err := usbip.NewClientService(ctx, usbip.ClientOptions{
|
||||
Logger: logger,
|
||||
Dialer: serviceDialer,
|
||||
ServerAddress: options.ServerOptions.Build(),
|
||||
Devices: toDeviceMatches(options.Devices),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ClientService{
|
||||
Adapter: boxService.NewAdapter(C.TypeUSBIPClient, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
inner: inner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return s.inner.Start()
|
||||
}
|
||||
|
||||
func (s *ClientService) Close() error {
|
||||
return s.inner.Close()
|
||||
}
|
||||
107
service/usbip/server.go
Normal file
107
service/usbip/server.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-usbip"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ServerService struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
inner *usbip.ServerService
|
||||
}
|
||||
|
||||
type dynamicServerService struct {
|
||||
ServerService
|
||||
host *usbip.DynamicHost
|
||||
}
|
||||
|
||||
var _ adapter.USBIPDynamicServer = (*dynamicServerService)(nil)
|
||||
|
||||
func NewServerService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPServerServiceOptions) (adapter.Service, error) {
|
||||
listenOptions := options.ListenOptions
|
||||
if listenOptions.ListenPort == 0 {
|
||||
listenOptions.ListenPort = usbip.DefaultPort
|
||||
}
|
||||
boxListener := listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: listenOptions,
|
||||
})
|
||||
serverOptions := usbip.ServerOptions{
|
||||
Logger: logger,
|
||||
Listen: func(context.Context) (net.Listener, error) {
|
||||
return boxListener.ListenTCP()
|
||||
},
|
||||
}
|
||||
base := ServerService{
|
||||
Adapter: boxService.NewAdapter(C.TypeUSBIPServer, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
providerType := options.Provider
|
||||
if providerType == "" {
|
||||
providerType = option.USBIPProviderDefault
|
||||
}
|
||||
switch providerType {
|
||||
case option.USBIPProviderDefault:
|
||||
defaultOptions, isDefault := options.Options.(*option.USBIPDefaultProviderOptions)
|
||||
if isDefault {
|
||||
serverOptions.Devices = toDeviceMatches(defaultOptions.Devices)
|
||||
}
|
||||
inner, err := usbip.NewServerService(ctx, serverOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.inner = inner
|
||||
return &base, nil
|
||||
case option.USBIPProviderDynamic:
|
||||
host := usbip.NewDynamicHost(logger)
|
||||
inner, err := usbip.NewDynamicServerService(ctx, serverOptions, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.inner = inner
|
||||
return &dynamicServerService{ServerService: base, host: host}, nil
|
||||
default:
|
||||
return nil, E.New("unknown usbip provider type: ", providerType)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerService) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return s.inner.Start()
|
||||
}
|
||||
|
||||
func (s *ServerService) Close() error {
|
||||
return s.inner.Close()
|
||||
}
|
||||
|
||||
func (s *dynamicServerService) AddDevice(info usbip.ProvidedDeviceInfo, transport usbip.DeviceTransport) (string, error) {
|
||||
return s.host.AddDevice(info, transport)
|
||||
}
|
||||
|
||||
func (s *dynamicServerService) RemoveDevice(busID string) {
|
||||
s.host.RemoveDevice(busID)
|
||||
}
|
||||
|
||||
func (s *dynamicServerService) SubscribeDevices(ctx context.Context, listener func([]usbip.ControlDeviceInfo)) {
|
||||
s.inner.SubscribeDevices(ctx, listener)
|
||||
}
|
||||
31
service/usbip/service.go
Normal file
31
service/usbip/service.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-usbip"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.USBIPServerServiceOptions](registry, C.TypeUSBIPServer, NewServerService)
|
||||
boxService.Register[option.USBIPClientServiceOptions](registry, C.TypeUSBIPClient, NewClientService)
|
||||
}
|
||||
|
||||
func toDeviceMatches(matches []option.USBIPDeviceMatch) []usbip.DeviceMatch {
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
deviceMatches := make([]usbip.DeviceMatch, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
deviceMatches = append(deviceMatches, usbip.DeviceMatch{
|
||||
BusID: match.BusID,
|
||||
VendorID: match.VendorID,
|
||||
ProductID: match.ProductID,
|
||||
Serial: match.Serial,
|
||||
})
|
||||
}
|
||||
return deviceMatches
|
||||
}
|
||||
3
service/usbip/stub.go
Normal file
3
service/usbip/stub.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
|
||||
|
||||
package usbip
|
||||
662
service/usermanager/manager.go
Normal file
662
service/usermanager/manager.go
Normal file
|
|
@ -0,0 +1,662 @@
|
|||
package usermanager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.UserManager = (*Manager)(nil)
|
||||
var _ adapter.ConnectionTracker = (*Manager)(nil)
|
||||
|
||||
type authCacheEntry struct {
|
||||
username string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type userConnCounter struct {
|
||||
username string
|
||||
tx int64
|
||||
rx int64
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
|
||||
authServer string
|
||||
timeout time.Duration
|
||||
cacheTTL time.Duration
|
||||
refreshInt time.Duration
|
||||
reportInt time.Duration
|
||||
reportTraffic bool
|
||||
apiSecret string
|
||||
apiListen string
|
||||
|
||||
httpClient *http.Client
|
||||
|
||||
access sync.RWMutex
|
||||
authCache map[string]*authCacheEntry
|
||||
userCreds map[string]string
|
||||
userConns map[string]map[*userConnCounter]struct{}
|
||||
|
||||
protocolUsers map[string][]adapter.UserInfo
|
||||
protocolLastSeq map[string]int64
|
||||
|
||||
trafficAccess sync.Mutex
|
||||
traffic map[string]*UserTrafficAtomic
|
||||
|
||||
stopRefresh chan struct{}
|
||||
stopReport chan struct{}
|
||||
stopAPI chan struct{}
|
||||
}
|
||||
|
||||
type UserTrafficAtomic struct {
|
||||
Tx atomic.Int64
|
||||
Rx atomic.Int64
|
||||
}
|
||||
|
||||
func New(ctx context.Context, logger log.ContextLogger, tag string, options option.UserManagerOptions) (*Manager, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
m := &Manager{
|
||||
Adapter: boxService.NewAdapter(C.TypeUserManager, tag),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
authServer: options.AuthServer,
|
||||
timeout: time.Duration(options.Timeout),
|
||||
cacheTTL: time.Duration(options.CacheTTL),
|
||||
refreshInt: time.Duration(options.RefreshInterval),
|
||||
reportInt: time.Duration(options.ReportInterval),
|
||||
reportTraffic: options.ReportTraffic,
|
||||
apiSecret: options.APISecret,
|
||||
apiListen: options.APIListen,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Duration(options.Timeout),
|
||||
},
|
||||
authCache: make(map[string]*authCacheEntry),
|
||||
userCreds: make(map[string]string),
|
||||
userConns: make(map[string]map[*userConnCounter]struct{}),
|
||||
protocolUsers: make(map[string][]adapter.UserInfo),
|
||||
protocolLastSeq: make(map[string]int64),
|
||||
traffic: make(map[string]*UserTrafficAtomic),
|
||||
stopRefresh: make(chan struct{}),
|
||||
stopReport: make(chan struct{}),
|
||||
stopAPI: make(chan struct{}),
|
||||
}
|
||||
if m.timeout <= 0 {
|
||||
m.timeout = 10 * time.Second
|
||||
}
|
||||
if m.cacheTTL <= 0 {
|
||||
m.cacheTTL = 5 * time.Minute
|
||||
}
|
||||
if m.refreshInt <= 0 {
|
||||
m.refreshInt = 30 * time.Second
|
||||
}
|
||||
if m.reportInt <= 0 {
|
||||
m.reportInt = 60 * time.Second
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
m.logger.Info("starting user manager")
|
||||
|
||||
if err := m.refreshCredentials(); err != nil {
|
||||
m.logger.Warn("initial credential fetch failed: ", err)
|
||||
}
|
||||
|
||||
router := service.FromContext[adapter.Router](m.ctx)
|
||||
if router != nil {
|
||||
router.AppendTracker(m)
|
||||
m.logger.Debug("registered as connection tracker")
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(m.refreshInt)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := m.refreshCredentials(); err != nil {
|
||||
m.logger.Warn("credential refresh failed: ", err)
|
||||
}
|
||||
case <-m.stopRefresh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if m.reportTraffic && m.authServer != "" {
|
||||
go func() {
|
||||
ticker := time.NewTicker(m.reportInt)
|
||||
defer ticker.Stop()
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
case <-m.stopReport:
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.reportTrafficToServer()
|
||||
case <-m.stopReport:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if m.apiListen != "" {
|
||||
go m.serveAPI()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.cancel()
|
||||
close(m.stopRefresh)
|
||||
close(m.stopReport)
|
||||
close(m.stopAPI)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Authenticate(ctx context.Context, protocol string, credential string, addr string) (string, error) {
|
||||
if credential == "" {
|
||||
return "", E.New("empty credential")
|
||||
}
|
||||
|
||||
m.access.RLock()
|
||||
entry, cached := m.authCache[credential]
|
||||
m.access.RUnlock()
|
||||
if cached && time.Now().Before(entry.expiresAt) {
|
||||
return entry.username, nil
|
||||
}
|
||||
|
||||
username, err := m.remoteAuth(protocol, credential, addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
m.access.Lock()
|
||||
m.authCache[credential] = &authCacheEntry{
|
||||
username: username,
|
||||
expiresAt: time.Now().Add(m.cacheTTL),
|
||||
}
|
||||
m.userCreds[username] = credential
|
||||
m.access.Unlock()
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
type authRequest struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Credential string `json:"credential"`
|
||||
Addr string `json:"addr,omitempty"`
|
||||
}
|
||||
|
||||
type authResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Manager) remoteAuth(protocol string, credential string, addr string) (string, error) {
|
||||
if m.authServer == "" {
|
||||
return "", E.New("auth server not configured")
|
||||
}
|
||||
req := authRequest{
|
||||
Protocol: protocol,
|
||||
Credential: credential,
|
||||
Addr: addr,
|
||||
}
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", E.Cause(err, "marshal auth request")
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(m.ctx, "POST", m.authServer+"/api/auth", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", E.Cause(err, "create auth request")
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
resp, err := m.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", E.Cause(err, "auth request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden {
|
||||
return "", E.New("authentication rejected")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", E.New("auth server returned ", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", E.Cause(err, "read auth response")
|
||||
}
|
||||
var authResp authResponse
|
||||
if err := json.Unmarshal(body, &authResp); err != nil {
|
||||
return "", E.Cause(err, "parse auth response")
|
||||
}
|
||||
if !authResp.Ok {
|
||||
return "", E.New("authentication rejected")
|
||||
}
|
||||
if authResp.ID == "" {
|
||||
return "", E.New("auth response missing user ID")
|
||||
}
|
||||
return authResp.ID, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetCredentials(protocol string) ([]adapter.UserInfo, error) {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
users := m.protocolUsers[protocol]
|
||||
if users == nil {
|
||||
return nil, nil
|
||||
}
|
||||
result := make([]adapter.UserInfo, len(users))
|
||||
copy(result, users)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type credentialsResponse struct {
|
||||
Credentials []credentialEntry `json:"credentials"`
|
||||
}
|
||||
|
||||
type credentialEntry struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Credential string `json:"credential"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func (m *Manager) refreshCredentials() error {
|
||||
if m.authServer == "" {
|
||||
return nil
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(m.ctx, "GET", m.authServer+"/api/credentials", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := m.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return E.Cause(err, "fetch credentials")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return E.New("credentials server returned ", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read credentials response")
|
||||
}
|
||||
var credResp credentialsResponse
|
||||
if err := json.Unmarshal(body, &credResp); err != nil {
|
||||
return E.Cause(err, "parse credentials response")
|
||||
}
|
||||
|
||||
newCache := make(map[string]*authCacheEntry)
|
||||
newUserCreds := make(map[string]string)
|
||||
byProtocol := make(map[string][]adapter.UserInfo)
|
||||
|
||||
for _, c := range credResp.Credentials {
|
||||
newCache[c.Credential] = &authCacheEntry{
|
||||
username: c.Username,
|
||||
expiresAt: time.Now().Add(m.cacheTTL),
|
||||
}
|
||||
newUserCreds[c.Username] = c.Credential
|
||||
byProtocol[c.Protocol] = append(byProtocol[c.Protocol], adapter.UserInfo{
|
||||
Username: c.Username,
|
||||
Credential: c.Credential,
|
||||
})
|
||||
}
|
||||
|
||||
m.access.Lock()
|
||||
for k, v := range newCache {
|
||||
if _, exists := m.authCache[k]; !exists {
|
||||
m.authCache[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range newUserCreds {
|
||||
m.userCreds[k] = v
|
||||
}
|
||||
m.protocolUsers = byProtocol
|
||||
m.access.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ReportTraffic(username string, tx int64, rx int64) {
|
||||
if username == "" {
|
||||
return
|
||||
}
|
||||
m.trafficAccess.Lock()
|
||||
t, ok := m.traffic[username]
|
||||
if !ok {
|
||||
t = &UserTrafficAtomic{}
|
||||
m.traffic[username] = t
|
||||
}
|
||||
m.trafficAccess.Unlock()
|
||||
if tx > 0 {
|
||||
t.Tx.Add(tx)
|
||||
}
|
||||
if rx > 0 {
|
||||
t.Rx.Add(rx)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetTraffic(username string) (int64, int64) {
|
||||
m.trafficAccess.Lock()
|
||||
t, ok := m.traffic[username]
|
||||
m.trafficAccess.Unlock()
|
||||
if !ok {
|
||||
return 0, 0
|
||||
}
|
||||
return t.Tx.Load(), t.Rx.Load()
|
||||
}
|
||||
|
||||
func (m *Manager) ListTraffic() map[string]adapter.UserTraffic {
|
||||
m.trafficAccess.Lock()
|
||||
defer m.trafficAccess.Unlock()
|
||||
result := make(map[string]adapter.UserTraffic, len(m.traffic))
|
||||
for username, t := range m.traffic {
|
||||
result[username] = adapter.UserTraffic{
|
||||
Tx: t.Tx.Load(),
|
||||
Rx: t.Rx.Load(),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Manager) KickUser(username string) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
cred, ok := m.userCreds[username]
|
||||
if ok {
|
||||
delete(m.authCache, cred)
|
||||
delete(m.userCreds, username)
|
||||
}
|
||||
for proto, users := range m.protocolUsers {
|
||||
filtered := make([]adapter.UserInfo, 0, len(users))
|
||||
for _, u := range users {
|
||||
if u.Username != username {
|
||||
filtered = append(filtered, u)
|
||||
}
|
||||
}
|
||||
m.protocolUsers[proto] = filtered
|
||||
}
|
||||
m.logger.Info("kicked user ", username)
|
||||
}
|
||||
|
||||
// ConnectionTracker implementation
|
||||
|
||||
func (m *Manager) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
|
||||
username := metadata.User
|
||||
if username == "" {
|
||||
return conn
|
||||
}
|
||||
counter := &userConnCounter{
|
||||
username: username,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
m.access.Lock()
|
||||
if m.userConns[username] == nil {
|
||||
m.userConns[username] = make(map[*userConnCounter]struct{})
|
||||
}
|
||||
m.userConns[username][counter] = struct{}{}
|
||||
m.access.Unlock()
|
||||
|
||||
wrapped := bufio.NewCounterConn(conn,
|
||||
[]N.CountFunc{func(n int64) { counter.tx += n }},
|
||||
[]N.CountFunc{func(n int64) { counter.rx += n }},
|
||||
)
|
||||
|
||||
return &trackedConn{
|
||||
ExtendedConn: wrapped,
|
||||
onClose: func() {
|
||||
m.ReportTraffic(username, counter.tx, counter.rx)
|
||||
m.access.Lock()
|
||||
if conns, ok := m.userConns[username]; ok {
|
||||
delete(conns, counter)
|
||||
if len(conns) == 0 {
|
||||
delete(m.userConns, username)
|
||||
}
|
||||
}
|
||||
m.access.Unlock()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn {
|
||||
username := metadata.User
|
||||
if username == "" {
|
||||
return conn
|
||||
}
|
||||
counter := &userConnCounter{
|
||||
username: username,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
m.access.Lock()
|
||||
if m.userConns[username] == nil {
|
||||
m.userConns[username] = make(map[*userConnCounter]struct{})
|
||||
}
|
||||
m.userConns[username][counter] = struct{}{}
|
||||
m.access.Unlock()
|
||||
|
||||
wrapped := bufio.NewCounterPacketConn(conn,
|
||||
[]N.CountFunc{func(n int64) { counter.tx += n }},
|
||||
[]N.CountFunc{func(n int64) { counter.rx += n }},
|
||||
)
|
||||
|
||||
return &trackedPacketConn{
|
||||
PacketConn: wrapped,
|
||||
onClose: func() {
|
||||
m.ReportTraffic(username, counter.tx, counter.rx)
|
||||
m.access.Lock()
|
||||
if conns, ok := m.userConns[username]; ok {
|
||||
delete(conns, counter)
|
||||
if len(conns) == 0 {
|
||||
delete(m.userConns, username)
|
||||
}
|
||||
}
|
||||
m.access.Unlock()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type trackedConn struct {
|
||||
N.ExtendedConn
|
||||
onClose func()
|
||||
}
|
||||
|
||||
func (c *trackedConn) Close() error {
|
||||
c.onClose()
|
||||
return c.ExtendedConn.Close()
|
||||
}
|
||||
|
||||
func (c *trackedConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
func (c *trackedConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *trackedConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type trackedPacketConn struct {
|
||||
N.PacketConn
|
||||
onClose func()
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) Close() error {
|
||||
c.onClose()
|
||||
return c.PacketConn.Close()
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) Upstream() any {
|
||||
return c.PacketConn
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Traffic report
|
||||
|
||||
type trafficReport struct {
|
||||
Traffic map[string]adapter.UserTraffic `json:"traffic"`
|
||||
}
|
||||
|
||||
func (m *Manager) reportTrafficToServer() {
|
||||
traffic := m.ListTraffic()
|
||||
if len(traffic) == 0 {
|
||||
return
|
||||
}
|
||||
report := trafficReport{Traffic: traffic}
|
||||
data, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
m.logger.Warn("marshal traffic report: ", err)
|
||||
return
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(m.ctx, "POST", m.authServer+"/api/traffic", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
m.logger.Warn("create traffic report: ", err)
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
resp, err := m.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
m.logger.Warn("traffic report failed: ", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
m.logger.Debug("traffic reported, users=", len(traffic))
|
||||
}
|
||||
|
||||
// HTTP API
|
||||
|
||||
func (m *Manager) serveAPI() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/traffic", m.handleTraffic)
|
||||
mux.HandleFunc("/traffic/", m.handleTrafficUser)
|
||||
mux.HandleFunc("/kick/", m.handleKick)
|
||||
|
||||
apiSecret := m.apiSecret
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if apiSecret != "" {
|
||||
auth := r.Header.Get("Authorization")
|
||||
expected := "Bearer " + apiSecret
|
||||
if subtle.ConstantTimeCompare([]byte(auth), []byte(expected)) != 1 {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
mux.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: m.apiListen,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", m.apiListen)
|
||||
if err != nil {
|
||||
m.logger.Error("api listen: ", err)
|
||||
return
|
||||
}
|
||||
m.logger.Info("user manager API listening on ", m.apiListen)
|
||||
|
||||
err = server.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
m.logger.Error("api serve: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) handleTraffic(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
traffic := m.ListTraffic()
|
||||
writeJSON(w, http.StatusOK, map[string]any{"users": traffic})
|
||||
}
|
||||
|
||||
func (m *Manager) handleTrafficUser(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
username := r.URL.Path[len("/traffic/"):]
|
||||
if username == "" {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tx, rx := m.GetTraffic(username)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"tx": tx, "rx": rx})
|
||||
}
|
||||
|
||||
func (m *Manager) handleKick(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
username := r.URL.Path[len("/kick/"):]
|
||||
if username == "" {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
m.KickUser(username)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func (m *Manager) GetCachedUsers() []string {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
users := make([]string, 0, len(m.userCreds))
|
||||
for u := range m.userCreds {
|
||||
users = append(users, u)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
var _ = fmt.Sprintf
|
||||
var _ = io.Discard
|
||||
18
service/usermanager/registry.go
Normal file
18
service/usermanager/registry.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package usermanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func RegisterService(registry *service.Registry) {
|
||||
service.Register[option.UserManagerOptions](registry, "user_manager", NewUserManager)
|
||||
}
|
||||
|
||||
func NewUserManager(ctx context.Context, logger log.ContextLogger, tag string, options option.UserManagerOptions) (adapter.Service, error) {
|
||||
return New(ctx, logger, tag, options)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue