Add USB/IP service

This commit is contained in:
世界 2026-06-14 19:19:46 +08:00
parent 12f160ab5f
commit 7cffd47dd4
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
30 changed files with 3117 additions and 246 deletions

15
adapter/usbip.go Normal file
View 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.DynamicDeviceInfo, transport usbip.DeviceTransport) (string, error)
RemoveDevice(busID string)
SubscribeDevices(ctx context.Context, listener func([]usbip.ControlDeviceInfo))
}

7
adapter/usbip_stub.go Normal file
View File

@ -0,0 +1,7 @@
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
package adapter
type USBIPDynamicServer interface {
usbipNotIncluded()
}

View File

@ -33,6 +33,8 @@ const (
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
TypeUSBIPServer = "usbip-server"
TypeUSBIPClient = "usbip-client"
TypeHysteriaRealm = "hysteria-realm"
TypeACME = "acme"
TypeCloudflareOriginCA = "cloudflare-origin-ca"

View File

@ -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() {
}

View File

@ -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

View File

@ -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 {

View File

@ -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",

View File

@ -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)
@ -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

View File

@ -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;
}

View File

@ -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",
}

View File

@ -0,0 +1,459 @@
//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),
}
busID, err := provider.AddDevice(usbip.DynamicDeviceInfo{
BusID: descriptor.GetDeviceId(),
BusNum: descriptor.GetBusNum(),
DevNum: descriptor.GetDevNum(),
Speed: descriptor.GetSpeed(),
VendorID: uint16(descriptor.GetVendorId()),
ProductID: uint16(descriptor.GetProductId()),
BCDDevice: uint16(descriptor.GetBcdDevice()),
DeviceClass: uint8(descriptor.GetDeviceClass()),
DeviceSubClass: uint8(descriptor.GetDeviceSubClass()),
DeviceProtocol: uint8(descriptor.GetDeviceProtocol()),
ConfigurationValue: uint8(descriptor.GetConfigurationValue()),
NumConfigurations: uint8(descriptor.GetNumConfigurations()),
Interfaces: usbInterfacesFromProto(descriptor.GetInterfaces()),
Serial: descriptor.GetSerial(),
Product: descriptor.GetProduct(),
}, device)
if err != nil {
return nil, err
}
device.busID = busID
return device, nil
}
// 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
}

View 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")
}

View File

@ -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
}

View File

@ -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
}

View 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
}

View 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
View File

@ -146,6 +146,7 @@ require (
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992 // 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-20260615120724-630651f2af95
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
View File

@ -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.12-0.20260623031040-54c9dbda5fe7 h1:UZu+fMnbfOJ14v7eXQe0UADNiPkR7gO70BsDFtLXZbg=
github.com/sagernet/sing-tun v0.8.12-0.20260623031040-54c9dbda5fe7/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
github.com/sagernet/sing-usbip v0.0.0-20260615120724-630651f2af95 h1:1z+EwDwim2C1Lt5HRRFeT8MbvEKxb/LSz/F0vbBwm6g=
github.com/sagernet/sing-usbip v0.0.0-20260615120724-630651f2af95/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=

View File

@ -143,6 +143,7 @@ func ServiceRegistry() *service.Registry {
registerCCMService(registry)
registerOCMService(registry)
registerOOMKillerService(registry)
registerUSBIPServices(registry)
return registry
}

12
include/usbip.go Normal file
View 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
View 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)`)
})
}

69
option/usbip.go Normal file
View 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
Server string `json:"server,omitempty"`
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{}

View File

@ -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

View File

@ -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

View File

@ -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

63
service/usbip/client.go Normal file
View File

@ -0,0 +1,63 @@
//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"
M "github.com/sagernet/sing/common/metadata"
)
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: M.ParseSocksaddr(options.Server),
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
View 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.DynamicDeviceInfo, 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
View 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
View File

@ -0,0 +1,3 @@
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
package usbip