2023-03-01 10:37:47 +08:00
package libbox
import (
2025-10-07 15:40:11 +08:00
"context"
2026-05-25 11:19:06 +08:00
"io"
2023-03-01 10:37:47 +08:00
"net"
2023-08-24 21:52:38 +08:00
"os"
2023-03-01 10:37:47 +08:00
"path/filepath"
2025-10-07 15:40:11 +08:00
"strconv"
"sync"
2023-09-03 21:06:21 +08:00
"time"
2023-03-01 10:37:47 +08:00
2025-10-07 15:40:11 +08:00
"github.com/sagernet/sing-box/daemon"
2023-03-01 10:37:47 +08:00
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
2025-10-07 15:40:11 +08:00
"google.golang.org/grpc"
2026-04-10 09:24:42 +08:00
"google.golang.org/grpc/codes"
2025-10-07 15:40:11 +08:00
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
2026-04-10 09:24:42 +08:00
"google.golang.org/grpc/status"
2025-10-07 15:40:11 +08:00
"google.golang.org/protobuf/types/known/emptypb"
2023-03-01 10:37:47 +08:00
)
type CommandClient struct {
2026-06-11 08:11:06 +08:00
handler CommandClientHandler
grpcConn * grpc . ClientConn
grpcClient daemon . StartedServiceClient
grpcManagedClient daemon . ManagedServiceClient
options CommandClientOptions
2026-06-11 21:11:10 +08:00
remote * remoteConnection
2026-06-11 08:11:06 +08:00
ctx context . Context
cancel context . CancelFunc
clientMutex sync . RWMutex
standalone bool
2023-03-01 10:37:47 +08:00
}
type CommandClientOptions struct {
2025-10-07 15:40:11 +08:00
commands [] int32
2023-03-01 10:37:47 +08:00
StatusInterval int64
}
2025-10-07 15:40:11 +08:00
func ( o * CommandClientOptions ) AddCommand ( command int32 ) {
o . commands = append ( o . commands , command )
}
2023-03-01 10:37:47 +08:00
type CommandClientHandler interface {
Connected ()
Disconnected ( message string )
2025-10-07 15:40:11 +08:00
SetDefaultLogLevel ( level int32 )
2024-06-18 17:49:06 +08:00
ClearLogs ()
2025-10-07 15:40:11 +08:00
WriteLogs ( messageList LogIterator )
2023-03-01 10:37:47 +08:00
WriteStatus ( message * StatusMessage )
2023-07-02 16:45:30 +08:00
WriteGroups ( message OutboundGroupIterator )
2026-04-08 14:44:14 +08:00
WriteOutbounds ( message OutboundGroupItemIterator )
2023-08-24 21:52:38 +08:00
InitializeClashMode ( modeList StringIterator , currentMode string )
UpdateClashMode ( newMode string )
2026-01-14 16:58:20 +08:00
WriteConnectionEvents ( events * ConnectionEvents )
2023-07-02 16:45:30 +08:00
}
2025-10-07 15:40:11 +08:00
type LogEntry struct {
Level int32
Message string
}
type LogIterator interface {
Len () int32
HasNext () bool
Next () * LogEntry
}
2026-01-04 02:53:33 +08:00
type XPCDialer interface {
DialXPC () ( int32 , error )
}
var sXPCDialer XPCDialer
func SetXPCDialer ( dialer XPCDialer ) {
sXPCDialer = dialer
}
2023-07-29 08:37:10 +08:00
func NewStandaloneCommandClient () * CommandClient {
2026-01-14 16:58:20 +08:00
return & CommandClient { standalone : true }
2023-03-01 10:37:47 +08:00
}
2023-08-16 16:48:23 +08:00
func NewCommandClient ( handler CommandClientHandler , options * CommandClientOptions ) * CommandClient {
2023-03-01 10:37:47 +08:00
return & CommandClient {
2023-07-29 08:37:10 +08:00
handler : handler ,
options : common . PtrValueOrDefault ( options ),
2023-03-01 10:37:47 +08:00
}
}
2025-10-07 15:40:11 +08:00
func unaryClientAuthInterceptor ( ctx context . Context , method string , req , reply any , cc * grpc . ClientConn , invoker grpc . UnaryInvoker , opts ... grpc . CallOption ) error {
if sCommandServerSecret != "" {
ctx = metadata . AppendToOutgoingContext ( ctx , "x-command-secret" , sCommandServerSecret )
2023-07-29 08:37:10 +08:00
}
2025-10-07 15:40:11 +08:00
return invoker ( ctx , method , req , reply , cc , opts ... )
2023-03-03 19:26:54 +08:00
}
2025-10-07 15:40:11 +08:00
func streamClientAuthInterceptor ( ctx context . Context , desc * grpc . StreamDesc , cc * grpc . ClientConn , method string , streamer grpc . Streamer , opts ... grpc . CallOption ) ( grpc . ClientStream , error ) {
if sCommandServerSecret != "" {
ctx = metadata . AppendToOutgoingContext ( ctx , "x-command-secret" , sCommandServerSecret )
}
return streamer ( ctx , desc , cc , method , opts ... )
}
2026-01-14 16:58:20 +08:00
const (
commandClientDialAttempts = 10
commandClientDialBaseDelay = 100 * time . Millisecond
commandClientDialStepDelay = 50 * time . Millisecond
)
2023-09-03 21:06:21 +08:00
2026-01-14 16:58:20 +08:00
func commandClientDialDelay ( attempt int ) time . Duration {
return commandClientDialBaseDelay + time . Duration ( attempt ) * commandClientDialStepDelay
}
2025-10-07 15:40:11 +08:00
2026-01-14 16:58:20 +08:00
func dialTarget () ( string , func ( context . Context , string ) ( net . Conn , error )) {
2026-01-04 02:53:33 +08:00
if sXPCDialer != nil {
2026-01-14 16:58:20 +08:00
return "passthrough:///xpc" , func ( ctx context . Context , _ string ) ( net . Conn , error ) {
fileDescriptor , err := sXPCDialer . DialXPC ()
if err != nil {
2026-04-10 12:04:55 +08:00
return nil , E . Cause ( err , "dial xpc" )
2026-01-14 16:58:20 +08:00
}
return networkConnectionFromFileDescriptor ( fileDescriptor )
2026-01-04 02:53:33 +08:00
}
2026-01-14 16:58:20 +08:00
}
if sCommandServerListenPort == 0 {
2026-02-12 16:39:50 +08:00
socketPath := filepath . Join ( sBasePath , "command.sock" )
return "passthrough:///command-socket" , func ( ctx context . Context , _ string ) ( net . Conn , error ) {
var networkDialer net . Dialer
return networkDialer . DialContext ( ctx , "unix" , socketPath )
}
2026-01-14 16:58:20 +08:00
}
return net . JoinHostPort ( "127.0.0.1" , strconv . Itoa ( int ( sCommandServerListenPort ))), nil
}
func networkConnectionFromFileDescriptor ( fileDescriptor int32 ) ( net . Conn , error ) {
file := os . NewFile ( uintptr ( fileDescriptor ), "xpc-command-socket" )
if file == nil {
return nil , E . New ( "invalid file descriptor" )
}
networkConnection , err := net . FileConn ( file )
if err != nil {
2026-01-04 02:53:33 +08:00
file . Close ()
2026-01-14 16:58:20 +08:00
return nil , E . Cause ( err , "create connection from fd" )
}
file . Close ()
return networkConnection , nil
}
2026-01-04 02:53:33 +08:00
2026-06-11 21:11:10 +08:00
func localDialOptions ( contextDialer func ( context . Context , string ) ( net . Conn , error )) [] grpc . DialOption {
options := [] grpc . DialOption {
grpc . WithTransportCredentials ( insecure . NewCredentials ()),
2026-07-18 00:28:16 +08:00
grpc . WithChainUnaryInterceptor ( daemon . UnaryClientLocaleInterceptor , unaryClientAuthInterceptor ),
grpc . WithChainStreamInterceptor ( daemon . StreamClientLocaleInterceptor , streamClientAuthInterceptor ),
2026-06-11 21:11:10 +08:00
}
if contextDialer != nil {
options = append ( options , grpc . WithContextDialer ( contextDialer ))
}
return options
}
// establishConnection dials the command server the client is bound to: the
// local command server (over socket/XPC) or a remote API service.
func ( c * CommandClient ) establishConnection () ( * grpc . ClientConn , daemon . StartedServiceClient , error ) {
if c . remote != nil {
return c . dialRemote ()
}
target , contextDialer := dialTarget ()
return c . dialWithRetry ( target , localDialOptions ( contextDialer ), true )
}
// dialWithRetry connects to the local command server. The retry loop exists to
// wait out the server starting up: WaitForReady keeps the probe redialing and
// the loop reissues it with a growing delay, so a freshly launched extension is
// picked up without surfacing a transient "unavailable" to the UI.
func ( c * CommandClient ) dialWithRetry ( target string , dialOptions [] grpc . DialOption , retryDial bool ) ( * grpc . ClientConn , daemon . StartedServiceClient , error ) {
2026-01-14 16:58:20 +08:00
var connection * grpc . ClientConn
var client daemon . StartedServiceClient
var lastError error
2026-01-04 02:53:33 +08:00
2026-05-13 22:37:05 +08:00
for attempt := range commandClientDialAttempts {
2026-01-14 16:58:20 +08:00
if connection == nil {
var err error
2026-06-11 21:11:10 +08:00
connection , err = grpc . NewClient ( target , dialOptions ... )
2026-01-14 16:58:20 +08:00
if err != nil {
lastError = err
if ! retryDial {
2026-04-10 12:04:55 +08:00
return nil , nil , E . Cause ( err , "create command client" )
2026-01-14 16:58:20 +08:00
}
time . Sleep ( commandClientDialDelay ( attempt ))
continue
}
client = daemon . NewStartedServiceClient ( connection )
2026-01-04 02:53:33 +08:00
}
2026-06-11 21:11:10 +08:00
ctx , cancel := context . WithTimeout ( context . Background (), commandClientDialDelay ( attempt ))
2026-01-14 16:58:20 +08:00
_ , err := client . GetStartedAt ( ctx , & emptypb . Empty {}, grpc . WaitForReady ( true ))
cancel ()
if err == nil {
return connection , client , nil
2026-01-04 02:53:33 +08:00
}
2026-01-14 16:58:20 +08:00
lastError = err
2026-01-04 02:53:33 +08:00
}
2026-01-14 16:58:20 +08:00
if connection != nil {
connection . Close ()
2026-01-04 02:53:33 +08:00
}
2026-04-10 12:04:55 +08:00
return nil , nil , E . Cause ( lastError , "probe command server" )
2026-01-04 02:53:33 +08:00
}
2026-06-11 21:11:10 +08:00
func ( c * CommandClient ) dialRemote () ( * grpc . ClientConn , daemon . StartedServiceClient , error ) {
connection , err := grpc . NewClient ( c . remote . target , c . remote . dialOptions ... )
if err != nil {
return nil , nil , E . Cause ( err , "create remote command client" )
}
client := daemon . NewStartedServiceClient ( connection )
ctx , cancel := context . WithTimeout ( context . Background (), commandClientRemoteProbeTimeout )
defer cancel ()
_ , err = client . GetStartedAt ( ctx , & emptypb . Empty {})
if err != nil {
connection . Close ()
return nil , nil , E . Cause ( err , "connect to remote server" )
}
return connection , client , nil
}
2026-01-14 16:58:20 +08:00
func ( c * CommandClient ) Connect () error {
2026-01-04 02:53:33 +08:00
c . clientMutex . Lock ()
common . Close ( common . PtrOrNil ( c . grpcConn ))
2026-06-11 21:11:10 +08:00
connection , client , err := c . establishConnection ()
2026-01-14 16:58:20 +08:00
if err != nil {
2026-01-04 02:53:33 +08:00
c . clientMutex . Unlock ()
2026-01-14 16:58:20 +08:00
return err
2026-01-04 02:53:33 +08:00
}
2026-01-14 16:58:20 +08:00
c . grpcConn = connection
c . grpcClient = client
2026-06-11 08:11:06 +08:00
c . grpcManagedClient = daemon . NewManagedServiceClient ( connection )
2026-01-14 16:58:20 +08:00
c . ctx , c . cancel = context . WithCancel ( context . Background ())
c . clientMutex . Unlock ()
c . handler . Connected ()
return c . dispatchCommands ()
}
2026-01-04 02:53:33 +08:00
2026-01-14 16:58:20 +08:00
func ( c * CommandClient ) ConnectWithFD ( fd int32 ) error {
c . clientMutex . Lock ()
common . Close ( common . PtrOrNil ( c . grpcConn ))
networkConnection , err := networkConnectionFromFileDescriptor ( fd )
2026-01-04 02:53:33 +08:00
if err != nil {
c . clientMutex . Unlock ()
2026-01-14 16:58:20 +08:00
return err
2026-01-04 02:53:33 +08:00
}
2026-06-11 21:11:10 +08:00
connection , client , err := c . dialWithRetry ( "passthrough:///xpc" , localDialOptions ( func ( ctx context . Context , _ string ) ( net . Conn , error ) {
2026-01-14 16:58:20 +08:00
return networkConnection , nil
2026-06-11 21:11:10 +08:00
}), false )
2023-03-01 10:37:47 +08:00
if err != nil {
2026-01-14 16:58:20 +08:00
networkConnection . Close ()
2025-10-07 15:40:11 +08:00
c . clientMutex . Unlock ()
2023-03-01 10:37:47 +08:00
return err
}
2026-01-14 16:58:20 +08:00
c . grpcConn = connection
c . grpcClient = client
2026-06-11 08:11:06 +08:00
c . grpcManagedClient = daemon . NewManagedServiceClient ( connection )
2025-10-07 15:40:11 +08:00
c . ctx , c . cancel = context . WithCancel ( context . Background ())
c . clientMutex . Unlock ()
c . handler . Connected ()
2026-01-14 16:58:20 +08:00
return c . dispatchCommands ()
}
func ( c * CommandClient ) dispatchCommands () error {
2025-10-07 15:40:11 +08:00
for _ , command := range c . options . commands {
switch command {
case CommandLog :
go c . handleLogStream ()
case CommandStatus :
go c . handleStatusStream ()
case CommandGroup :
go c . handleGroupStream ()
case CommandClashMode :
go c . handleClashModeStream ()
case CommandConnections :
go c . handleConnectionsStream ()
2026-04-08 14:44:14 +08:00
case CommandOutbounds :
go c . handleOutboundsStream ()
2025-10-07 15:40:11 +08:00
default :
return E . New ( "unknown command: " , command )
}
}
return nil
}
func ( c * CommandClient ) Disconnect () error {
c . clientMutex . Lock ()
defer c . clientMutex . Unlock ()
if c . cancel != nil {
c . cancel ()
}
return common . Close ( common . PtrOrNil ( c . grpcConn ))
}
2026-05-26 11:24:39 +08:00
func ( c * CommandClient ) getClientForCall () ( daemon . StartedServiceClient , context . Context , error ) {
2025-10-07 15:40:11 +08:00
c . clientMutex . RLock ()
if c . grpcClient != nil {
defer c . clientMutex . RUnlock ()
2026-05-26 11:24:39 +08:00
return c . grpcClient , c . ctx , nil
2025-10-07 15:40:11 +08:00
}
c . clientMutex . RUnlock ()
c . clientMutex . Lock ()
defer c . clientMutex . Unlock ()
if c . grpcClient != nil {
2026-05-26 11:24:39 +08:00
return c . grpcClient , c . ctx , nil
2025-10-07 15:40:11 +08:00
}
2026-06-11 21:11:10 +08:00
connection , client , err := c . establishConnection ()
2023-03-01 10:37:47 +08:00
if err != nil {
2026-05-26 11:24:39 +08:00
return nil , nil , E . Cause ( err , "get command client" )
2025-10-07 15:40:11 +08:00
}
2026-01-14 16:58:20 +08:00
c . grpcConn = connection
c . grpcClient = client
2026-06-11 08:11:06 +08:00
c . grpcManagedClient = daemon . NewManagedServiceClient ( connection )
2025-10-07 15:40:11 +08:00
if c . ctx == nil {
c . ctx , c . cancel = context . WithCancel ( context . Background ())
}
2026-05-26 11:24:39 +08:00
return c . grpcClient , c . ctx , nil
2025-10-07 15:40:11 +08:00
}
2026-01-14 16:58:20 +08:00
func ( c * CommandClient ) closeConnection () {
c . clientMutex . Lock ()
defer c . clientMutex . Unlock ()
if c . grpcConn != nil {
c . grpcConn . Close ()
c . grpcConn = nil
c . grpcClient = nil
2026-06-11 08:11:06 +08:00
c . grpcManagedClient = nil
2026-01-14 16:58:20 +08:00
}
}
2026-05-26 11:24:39 +08:00
func callWithResult [ T any ]( c * CommandClient , call func ( ctx context . Context , client daemon . StartedServiceClient ) ( T , error )) ( T , error ) {
client , ctx , err := c . getClientForCall ()
2026-01-14 16:58:20 +08:00
if err != nil {
var zero T
return zero , err
}
if c . standalone {
defer c . closeConnection ()
}
2026-05-26 11:24:39 +08:00
return call ( ctx , client )
2026-01-14 16:58:20 +08:00
}
2026-06-11 08:11:06 +08:00
func callManagedWithResult [ T any ]( c * CommandClient , call func ( ctx context . Context , client daemon . ManagedServiceClient ) ( T , error )) ( T , error ) {
_ , ctx , err := c . getClientForCall ()
if err != nil {
var zero T
return zero , err
}
if c . standalone {
defer c . closeConnection ()
}
c . clientMutex . RLock ()
client := c . grpcManagedClient
c . clientMutex . RUnlock ()
if client == nil {
var zero T
return zero , os . ErrClosed
}
return call ( ctx , client )
}
2025-10-07 15:40:11 +08:00
func ( c * CommandClient ) getStreamContext () ( daemon . StartedServiceClient , context . Context ) {
c . clientMutex . RLock ()
defer c . clientMutex . RUnlock ()
return c . grpcClient , c . ctx
}
func ( c * CommandClient ) handleLogStream () {
client , ctx := c . getStreamContext ()
stream , err := client . SubscribeLog ( ctx , & emptypb . Empty {})
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "subscribe log" ). Error ())
2025-10-07 15:40:11 +08:00
return
}
defaultLogLevel , err := client . GetDefaultLogLevel ( ctx , & emptypb . Empty {})
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "get default log level" ). Error ())
2025-10-07 15:40:11 +08:00
return
2023-03-01 10:37:47 +08:00
}
2025-10-07 15:40:11 +08:00
c . handler . SetDefaultLogLevel ( int32 ( defaultLogLevel . Level ))
for {
logMessage , err := stream . Recv ()
2024-06-18 17:49:06 +08:00
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "log stream recv" ). Error ())
2025-10-07 15:40:11 +08:00
return
2024-06-18 17:49:06 +08:00
}
2025-10-07 15:40:11 +08:00
if logMessage . Reset_ {
c . handler . ClearLogs ()
}
var messages [] * LogEntry
for _ , msg := range logMessage . Messages {
messages = append ( messages , & LogEntry {
Level : int32 ( msg . Level ),
Message : msg . Message ,
})
2023-03-01 10:37:47 +08:00
}
2025-10-07 15:40:11 +08:00
c . handler . WriteLogs ( newIterator ( messages ))
}
}
func ( c * CommandClient ) handleStatusStream () {
client , ctx := c . getStreamContext ()
interval := c . options . StatusInterval
stream , err := client . SubscribeStatus ( ctx , & daemon . SubscribeStatusRequest {
Interval : interval ,
})
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "subscribe status" ). Error ())
2025-10-07 15:40:11 +08:00
return
}
for {
status , err := stream . Recv ()
2023-07-02 16:45:30 +08:00
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "status stream recv" ). Error ())
2025-10-07 15:40:11 +08:00
return
2023-07-02 16:45:30 +08:00
}
2026-02-07 08:19:24 +08:00
c . handler . WriteStatus ( statusMessageFromGRPC ( status ))
2025-10-07 15:40:11 +08:00
}
}
func ( c * CommandClient ) handleGroupStream () {
client , ctx := c . getStreamContext ()
stream , err := client . SubscribeGroups ( ctx , & emptypb . Empty {})
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "subscribe groups" ). Error ())
2025-10-07 15:40:11 +08:00
return
}
for {
groups , err := stream . Recv ()
2023-08-24 21:52:38 +08:00
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "groups stream recv" ). Error ())
2025-10-07 15:40:11 +08:00
return
2023-08-24 21:52:38 +08:00
}
2026-02-07 08:19:24 +08:00
c . handler . WriteGroups ( outboundGroupIteratorFromGRPC ( groups ))
2025-10-07 15:40:11 +08:00
}
}
func ( c * CommandClient ) handleClashModeStream () {
client , ctx := c . getStreamContext ()
modeStatus , err := client . GetClashModeStatus ( ctx , & emptypb . Empty {})
if err != nil {
2026-06-15 13:02:09 +08:00
if status . Code ( err ) != codes . NotFound {
c . handler . Disconnected ( E . Cause ( err , "get clash mode status" ). Error ())
return
}
modeStatus = & daemon . ClashModeStatus {}
2025-10-07 15:40:11 +08:00
}
if sFixAndroidStack {
2026-06-15 13:02:09 +08:00
go c . handler . InitializeClashMode ( newIterator ( modeStatus . ModeList ), modeStatus . CurrentMode )
2025-10-07 15:40:11 +08:00
} else {
c . handler . InitializeClashMode ( newIterator ( modeStatus . ModeList ), modeStatus . CurrentMode )
}
if len ( modeStatus . ModeList ) == 0 {
return
}
stream , err := client . SubscribeClashMode ( ctx , & emptypb . Empty {})
if err != nil {
2026-06-15 13:02:09 +08:00
if status . Code ( err ) == codes . NotFound {
return
}
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "subscribe clash mode" ). Error ())
2025-10-07 15:40:11 +08:00
return
}
for {
mode , err := stream . Recv ()
if err != nil {
2026-06-15 13:02:09 +08:00
if status . Code ( err ) == codes . NotFound {
return
}
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "clash mode stream recv" ). Error ())
2025-10-07 15:40:11 +08:00
return
2023-08-24 21:52:38 +08:00
}
2025-10-07 15:40:11 +08:00
c . handler . UpdateClashMode ( mode . Mode )
}
}
func ( c * CommandClient ) handleConnectionsStream () {
client , ctx := c . getStreamContext ()
interval := c . options . StatusInterval
stream , err := client . SubscribeConnections ( ctx , & daemon . SubscribeConnectionsRequest {
Interval : interval ,
})
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "subscribe connections" ). Error ())
2025-10-07 15:40:11 +08:00
return
}
for {
2026-01-14 16:58:20 +08:00
events , err := stream . Recv ()
2024-06-11 21:16:33 +08:00
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "connections stream recv" ). Error ())
2025-10-07 15:40:11 +08:00
return
2024-06-11 21:16:33 +08:00
}
2026-02-07 08:19:24 +08:00
libboxEvents := connectionEventsFromGRPC ( events )
2026-01-14 16:58:20 +08:00
c . handler . WriteConnectionEvents ( libboxEvents )
2023-03-01 10:37:47 +08:00
}
}
2026-04-08 14:44:14 +08:00
func ( c * CommandClient ) handleOutboundsStream () {
client , ctx := c . getStreamContext ()
stream , err := client . SubscribeOutbounds ( ctx , & emptypb . Empty {})
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "subscribe outbounds" ). Error ())
2026-04-08 14:44:14 +08:00
return
}
for {
list , err := stream . Recv ()
if err != nil {
2026-04-10 12:04:55 +08:00
c . handler . Disconnected ( E . Cause ( err , "outbounds stream recv" ). Error ())
2026-04-08 14:44:14 +08:00
return
}
c . handler . WriteOutbounds ( outboundGroupItemListFromGRPC ( list ))
}
}
2025-10-07 15:40:11 +08:00
func ( c * CommandClient ) SelectOutbound ( groupTag string , outboundTag string ) error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . SelectOutbound ( ctx , & daemon . SelectOutboundRequest {
2026-01-14 16:58:20 +08:00
GroupTag : groupTag ,
OutboundTag : outboundTag ,
})
2025-10-07 15:40:11 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "select outbound" )
}
return nil
2025-10-07 15:40:11 +08:00
}
2026-07-25 01:33:11 +08:00
func ( c * CommandClient ) URLTest ( outboundTag string ) error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . URLTest ( ctx , & daemon . URLTestRequest {
2026-07-25 01:33:11 +08:00
OutboundTag : outboundTag ,
2026-01-14 16:58:20 +08:00
})
2025-10-07 15:40:11 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "url test" )
}
return nil
2025-10-07 15:40:11 +08:00
}
func ( c * CommandClient ) SetClashMode ( newMode string ) error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . SetClashMode ( ctx , & daemon . ClashMode {
2026-01-14 16:58:20 +08:00
Mode : newMode ,
})
2025-10-07 15:40:11 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "set clash mode" )
}
return nil
2025-10-07 15:40:11 +08:00
}
func ( c * CommandClient ) CloseConnection ( connId string ) error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . CloseConnection ( ctx , & daemon . CloseConnectionRequest {
2026-01-14 16:58:20 +08:00
Id : connId ,
})
2025-10-07 15:40:11 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "close connection" )
}
return nil
2025-10-07 15:40:11 +08:00
}
func ( c * CommandClient ) CloseConnections () error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . CloseAllConnections ( ctx , & emptypb . Empty {})
2026-01-14 16:58:20 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "close all connections" )
}
return nil
2025-10-07 15:40:11 +08:00
}
func ( c * CommandClient ) ServiceReload () error {
2026-06-11 08:11:06 +08:00
_ , err := callManagedWithResult ( c , func ( ctx context . Context , client daemon . ManagedServiceClient ) ( * emptypb . Empty , error ) {
2026-05-26 11:24:39 +08:00
return client . ReloadService ( ctx , & emptypb . Empty {})
2026-01-14 16:58:20 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "reload service" )
}
return nil
2025-10-07 15:40:11 +08:00
}
func ( c * CommandClient ) ServiceClose () error {
2026-06-11 08:11:06 +08:00
_ , err := callManagedWithResult ( c , func ( ctx context . Context , client daemon . ManagedServiceClient ) ( * emptypb . Empty , error ) {
2026-05-26 11:24:39 +08:00
return client . StopService ( ctx , & emptypb . Empty {})
2026-01-14 16:58:20 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "stop service" )
}
return nil
2025-10-07 15:40:11 +08:00
}
2025-10-22 23:26:05 +08:00
func ( c * CommandClient ) ClearLogs () error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . ClearLogs ( ctx , & emptypb . Empty {})
2026-01-14 16:58:20 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "clear logs" )
}
return nil
2025-10-22 23:26:05 +08:00
}
2025-10-07 15:40:11 +08:00
func ( c * CommandClient ) GetSystemProxyStatus () ( * SystemProxyStatus , error ) {
2026-06-11 08:11:06 +08:00
return callManagedWithResult ( c , func ( ctx context . Context , client daemon . ManagedServiceClient ) ( * SystemProxyStatus , error ) {
2026-05-26 11:24:39 +08:00
status , err := client . GetSystemProxyStatus ( ctx , & emptypb . Empty {})
2026-01-14 16:58:20 +08:00
if err != nil {
2026-04-10 12:04:55 +08:00
return nil , E . Cause ( err , "get system proxy status" )
2026-01-14 16:58:20 +08:00
}
2026-02-07 08:19:24 +08:00
return systemProxyStatusFromGRPC ( status ), nil
2026-01-14 16:58:20 +08:00
})
2025-10-07 15:40:11 +08:00
}
func ( c * CommandClient ) SetSystemProxyEnabled ( isEnabled bool ) error {
2026-06-11 08:11:06 +08:00
_ , err := callManagedWithResult ( c , func ( ctx context . Context , client daemon . ManagedServiceClient ) ( * emptypb . Empty , error ) {
2026-05-26 11:24:39 +08:00
return client . SetSystemProxyEnabled ( ctx , & daemon . SetSystemProxyEnabledRequest {
2026-01-14 16:58:20 +08:00
Enabled : isEnabled ,
})
2025-10-07 15:40:11 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "set system proxy enabled" )
}
return nil
2025-10-07 15:40:11 +08:00
}
2026-04-02 16:39:34 +08:00
func ( c * CommandClient ) TriggerGoCrash () error {
2026-06-11 08:11:06 +08:00
_ , err := callManagedWithResult ( c , func ( ctx context . Context , client daemon . ManagedServiceClient ) ( * emptypb . Empty , error ) {
2026-05-26 11:24:39 +08:00
return client . TriggerDebugCrash ( ctx , & daemon . DebugCrashRequest {
2026-04-02 16:39:34 +08:00
Type : daemon . DebugCrashRequest_GO ,
})
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "trigger debug crash" )
}
return nil
2026-04-02 16:39:34 +08:00
}
func ( c * CommandClient ) TriggerNativeCrash () error {
2026-06-11 08:11:06 +08:00
_ , err := callManagedWithResult ( c , func ( ctx context . Context , client daemon . ManagedServiceClient ) ( * emptypb . Empty , error ) {
2026-05-26 11:24:39 +08:00
return client . TriggerDebugCrash ( ctx , & daemon . DebugCrashRequest {
2026-04-02 16:39:34 +08:00
Type : daemon . DebugCrashRequest_NATIVE ,
})
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "trigger native crash" )
}
return nil
2026-04-02 16:39:34 +08:00
}
func ( c * CommandClient ) TriggerOOMReport () error {
2026-06-14 19:19:46 +08:00
_ , err := callManagedWithResult ( c , func ( ctx context . Context , client daemon . ManagedServiceClient ) ( * emptypb . Empty , error ) {
2026-05-26 11:24:39 +08:00
return client . TriggerOOMReport ( ctx , & emptypb . Empty {})
2026-04-02 16:39:34 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "trigger oom report" )
}
return nil
2026-04-02 16:39:34 +08:00
}
2025-10-07 15:40:11 +08:00
func ( c * CommandClient ) GetDeprecatedNotes () ( DeprecatedNoteIterator , error ) {
2026-05-26 11:24:39 +08:00
return callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( DeprecatedNoteIterator , error ) {
warnings , err := client . GetDeprecatedWarnings ( ctx , & emptypb . Empty {})
2026-01-14 16:58:20 +08:00
if err != nil {
2026-04-10 12:04:55 +08:00
return nil , E . Cause ( err , "get deprecated warnings" )
2026-01-14 16:58:20 +08:00
}
var notes [] * DeprecatedNote
for _ , warning := range warnings . Warnings {
notes = append ( notes , & DeprecatedNote {
2026-04-07 20:53:53 +08:00
Description : warning . Description ,
DeprecatedVersion : warning . DeprecatedVersion ,
ScheduledVersion : warning . ScheduledVersion ,
MigrationLink : warning . MigrationLink ,
2026-01-14 16:58:20 +08:00
})
}
return newIterator ( notes ), nil
})
2025-10-07 15:40:11 +08:00
}
2025-12-27 13:53:01 +08:00
func ( c * CommandClient ) GetStartedAt () ( int64 , error ) {
2026-05-26 11:24:39 +08:00
return callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( int64 , error ) {
startedAt , err := client . GetStartedAt ( ctx , & emptypb . Empty {})
2026-01-14 16:58:20 +08:00
if err != nil {
2026-04-10 12:04:55 +08:00
return 0 , E . Cause ( err , "get started at" )
2026-01-14 16:58:20 +08:00
}
return startedAt . StartedAt , nil
})
2025-12-27 13:53:01 +08:00
}
2026-06-21 18:38:08 +08:00
func ( c * CommandClient ) GetAPIVersion () ( int32 , error ) {
return callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( int32 , error ) {
version , err := client . GetVersion ( ctx , & emptypb . Empty {})
if err != nil {
return 0 , E . Cause ( err , "get version" )
}
return version . ApiVersion , nil
})
}
2025-10-07 15:40:11 +08:00
func ( c * CommandClient ) SetGroupExpand ( groupTag string , isExpand bool ) error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . SetGroupExpand ( ctx , & daemon . SetGroupExpandRequest {
2026-01-14 16:58:20 +08:00
GroupTag : groupTag ,
IsExpand : isExpand ,
})
2025-10-07 15:40:11 +08:00
})
2026-04-10 12:04:55 +08:00
if err != nil {
return E . Cause ( err , "set group expand" )
}
return nil
2023-03-01 10:37:47 +08:00
}
2026-04-08 14:44:14 +08:00
2026-05-26 11:24:39 +08:00
func ( c * CommandClient ) StartNetworkQualityTest ( configURL string , outboundTag string , serial bool , maxRuntimeSeconds int32 , http3 bool , handler NetworkQualityTestHandler ) ( * NetworkQualityTestSession , error ) {
client , parentCtx , err := c . getClientForCall ()
2026-04-08 14:44:14 +08:00
if err != nil {
2026-05-26 11:24:39 +08:00
return nil , E . Cause ( err , "start network quality test" )
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
streamCtx , cancel := context . WithCancel ( parentCtx )
session := & NetworkQualityTestSession {
streamSession : streamSession {
ctx : streamCtx ,
cancel : cancel ,
closeDone : make ( chan struct {}),
},
}
failStart := func ( cause error , message string ) ( * NetworkQualityTestSession , error ) {
cancel ()
if c . standalone {
c . closeConnection ()
}
return nil , E . Cause ( cause , message )
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
stream , err := client . StartNetworkQualityTest ( streamCtx , & daemon . NetworkQualityTestRequest {
2026-04-08 14:44:14 +08:00
ConfigURL : configURL ,
OutboundTag : outboundTag ,
Serial : serial ,
MaxRuntimeSeconds : maxRuntimeSeconds ,
Http3 : http3 ,
})
if err != nil {
2026-05-26 11:24:39 +08:00
return failStart ( err , "start network quality test" )
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
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
}
handler . OnError ( E . Cause ( recvErr , "network quality test recv" ). Error ())
return
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
if event . IsFinal {
if event . Error != "" {
handler . OnError ( event . Error )
} else {
handler . OnResult ( & NetworkQualityResult {
DownloadCapacity : event . DownloadCapacity ,
UploadCapacity : event . UploadCapacity ,
DownloadRPM : event . DownloadRPM ,
UploadRPM : event . UploadRPM ,
IdleLatencyMs : event . IdleLatencyMs ,
DownloadCapacityAccuracy : event . DownloadCapacityAccuracy ,
UploadCapacityAccuracy : event . UploadCapacityAccuracy ,
DownloadRPMAccuracy : event . DownloadRPMAccuracy ,
UploadRPMAccuracy : event . UploadRPMAccuracy ,
})
}
return
}
handler . OnProgress ( networkQualityProgressFromGRPC ( event ))
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
}()
return session , nil
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
func ( c * CommandClient ) StartSTUNTest ( server string , outboundTag string , handler STUNTestHandler ) ( * STUNTestSession , error ) {
client , parentCtx , err := c . getClientForCall ()
2026-04-08 14:44:14 +08:00
if err != nil {
2026-05-26 11:24:39 +08:00
return nil , E . Cause ( err , "start stun test" )
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
streamCtx , cancel := context . WithCancel ( parentCtx )
session := & STUNTestSession {
streamSession : streamSession {
ctx : streamCtx ,
cancel : cancel ,
closeDone : make ( chan struct {}),
},
}
failStart := func ( cause error , message string ) ( * STUNTestSession , error ) {
cancel ()
if c . standalone {
c . closeConnection ()
}
return nil , E . Cause ( cause , message )
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
stream , err := client . StartSTUNTest ( streamCtx , & daemon . STUNTestRequest {
2026-04-08 14:44:14 +08:00
Server : server ,
OutboundTag : outboundTag ,
})
if err != nil {
2026-05-26 11:24:39 +08:00
return failStart ( err , "start stun test" )
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
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
}
handler . OnError ( E . Cause ( recvErr , "stun test recv" ). Error ())
return
}
if event . IsFinal {
if event . Error != "" {
handler . OnError ( event . Error )
} else {
handler . OnResult ( & STUNTestResult {
ExternalAddr : event . ExternalAddr ,
LatencyMs : event . LatencyMs ,
NATMapping : event . NatMapping ,
NATFiltering : event . NatFiltering ,
NATTypeSupported : event . NatTypeSupported ,
})
}
return
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
handler . OnProgress ( stunTestProgressFromGRPC ( event ))
2026-04-08 14:44:14 +08:00
}
2026-05-26 11:24:39 +08:00
}()
return session , nil
2026-04-08 14:44:14 +08:00
}
2026-04-09 19:34:27 +08:00
2026-07-17 16:21:03 +08:00
func subscribeStatus [ T any ]( c * CommandClient , session * streamSession , name string , start func ( context . Context , daemon . StartedServiceClient ) ( grpc . ServerStreamingClient [ T ], error ), onUpdate func ( * T ), onError func ( string )) error {
2026-05-26 11:24:39 +08:00
client , parentCtx , err := c . getClientForCall ()
2026-04-09 19:34:27 +08:00
if err != nil {
2026-07-17 16:21:03 +08:00
return E . Cause ( err , "subscribe " , name )
2026-04-09 19:34:27 +08:00
}
2026-05-26 11:24:39 +08:00
streamCtx , cancel := context . WithCancel ( parentCtx )
2026-07-17 16:21:03 +08:00
* session = streamSession {
ctx : streamCtx ,
cancel : cancel ,
closeDone : make ( chan struct {}),
2026-05-26 11:24:39 +08:00
}
2026-07-17 16:21:03 +08:00
stream , err := start ( streamCtx , client )
if err != nil {
2026-05-26 11:24:39 +08:00
cancel ()
if c . standalone {
c . closeConnection ()
}
2026-07-17 16:21:03 +08:00
return E . Cause ( err , "subscribe " , name )
2026-04-09 19:34:27 +08:00
}
2026-05-26 11:24:39 +08:00
standalone := c . standalone
go func () {
defer func () {
close ( session . closeDone )
if standalone {
c . closeConnection ()
2026-04-10 09:24:42 +08:00
}
2026-05-26 11:24:39 +08:00
}()
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
}
2026-07-17 16:21:03 +08:00
onError ( E . Cause ( recvErr , name , " recv" ). Error ())
2026-05-26 11:24:39 +08:00
return
}
2026-07-17 16:21:03 +08:00
onUpdate ( event )
2026-04-09 19:34:27 +08:00
}
2026-05-26 11:24:39 +08:00
}()
2026-07-17 16:21:03 +08:00
return nil
}
2026-05-26 11:24:39 +08:00
2026-07-17 16:21:03 +08:00
func ( c * CommandClient ) SubscribeTailscaleStatus ( handler TailscaleStatusHandler ) ( * TailscaleStatusSubscription , error ) {
session := new ( TailscaleStatusSubscription )
err := subscribeStatus ( c , & session . streamSession , "tailscale status" , func ( ctx context . Context , client daemon . StartedServiceClient ) ( grpc . ServerStreamingClient [ daemon . TailscaleStatusUpdate ], error ) {
return client . SubscribeTailscaleStatus ( ctx , & emptypb . Empty {})
}, func ( update * daemon . TailscaleStatusUpdate ) {
handler . OnStatusUpdate ( tailscaleStatusUpdateFromGRPC ( update ))
}, handler . OnError )
if err != nil {
return nil , err
}
2026-05-26 11:24:39 +08:00
return session , nil
2026-04-09 19:34:27 +08:00
}
2026-04-10 09:24:42 +08:00
2026-06-14 19:19:46 +08:00
func ( c * CommandClient ) SubscribeUSBIPServerStatus ( handler USBIPServerStatusHandler ) ( * USBIPServerStatusSubscription , error ) {
2026-07-17 16:21:03 +08:00
session := new ( USBIPServerStatusSubscription )
err := subscribeStatus ( c , & session . streamSession , "usbip server status" , func ( ctx context . Context , client daemon . StartedServiceClient ) ( grpc . ServerStreamingClient [ daemon . USBIPServerStatusUpdate ], error ) {
return client . SubscribeUSBIPServerStatus ( ctx , & emptypb . Empty {})
}, func ( update * daemon . USBIPServerStatusUpdate ) {
handler . OnStatusUpdate ( usbipServerStatusUpdateFromGRPC ( update ))
}, handler . OnError )
2026-06-14 19:19:46 +08:00
if err != nil {
2026-07-17 16:21:03 +08:00
return nil , err
2026-06-14 19:19:46 +08:00
}
2026-07-17 16:21:03 +08:00
return session , nil
}
2026-06-14 19:19:46 +08:00
2026-07-17 16:21:03 +08:00
func ( c * CommandClient ) SubscribeOpenConnectStatus ( handler OpenConnectStatusHandler ) ( * OpenConnectStatusSubscription , error ) {
session := new ( OpenConnectStatusSubscription )
err := subscribeStatus ( c , & session . streamSession , "openconnect status" , func ( ctx context . Context , client daemon . StartedServiceClient ) ( grpc . ServerStreamingClient [ daemon . OpenConnectStatusUpdate ], error ) {
return client . SubscribeOpenConnectStatus ( ctx , & emptypb . Empty {})
}, func ( update * daemon . OpenConnectStatusUpdate ) {
handler . OnStatusUpdate ( openConnectStatusUpdateFromGRPC ( update ))
}, handler . OnError )
if err != nil {
return nil , err
2026-06-14 19:19:46 +08:00
}
2026-07-17 16:21:03 +08:00
return session , nil
}
2026-06-14 19:19:46 +08:00
2026-07-18 22:07:16 +08:00
func ( c * CommandClient ) SubmitOpenConnectAuthResponse ( endpointTag string , challengeID string , response * OpenConnectAuthResponse ) error {
submission := & daemon . OpenConnectAuthResponseSubmission {
EndpointTag : endpointTag ,
ChallengeID : challengeID ,
}
if response . formValues != nil {
submission . Response = & daemon . OpenConnectAuthResponseSubmission_Form { Form : & daemon . OpenConnectAuthFormResponse {
Values : response . formValues . values ,
}}
}
if response . browserResult != nil {
submission . Response = & daemon . OpenConnectAuthResponseSubmission_Browser { Browser : & daemon . OpenConnectBrowserResult {
FinalURL : response . browserResult . FinalURL ,
Cookies : common . Map ( response . browserResult . cookies , func ( cookie openConnectBrowserCookie ) * daemon . OpenConnectBrowserCookie {
return & daemon . OpenConnectBrowserCookie { Name : cookie . Name , Value : cookie . Value }
}),
Headers : common . Map ( response . browserResult . headers , func ( header openConnectBrowserHeader ) * daemon . OpenConnectBrowserHeader {
return & daemon . OpenConnectBrowserHeader { Name : header . Name , Values : header . Values }
}),
}}
}
2026-07-17 16:21:03 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
2026-07-18 22:07:16 +08:00
return client . SubmitOpenConnectAuthResponse ( ctx , submission )
2026-07-17 16:21:03 +08:00
})
if err != nil {
2026-07-18 22:07:16 +08:00
return E . Cause ( err , "submit openconnect authentication response" )
2026-07-17 16:21:03 +08:00
}
return nil
}
2026-07-18 22:07:16 +08:00
func ( c * CommandClient ) CancelOpenConnectAuthChallenge ( endpointTag string , challengeID string ) error {
2026-07-17 16:21:03 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
2026-07-18 22:07:16 +08:00
return client . CancelOpenConnectAuthChallenge ( ctx , & daemon . OpenConnectAuthChallengeCancel {
2026-07-17 16:21:03 +08:00
EndpointTag : endpointTag ,
2026-07-18 22:07:16 +08:00
ChallengeID : challengeID ,
2026-07-17 16:21:03 +08:00
})
})
if err != nil {
2026-07-18 22:07:16 +08:00
return E . Cause ( err , "cancel openconnect authentication challenge" )
2026-06-14 19:19:46 +08:00
}
2026-07-17 16:21:03 +08:00
return nil
}
2026-06-14 19:19:46 +08:00
2026-07-17 16:21:03 +08:00
func ( c * CommandClient ) SubscribeOpenVPNStatus ( handler OpenVPNStatusHandler ) ( * OpenVPNStatusSubscription , error ) {
session := new ( OpenVPNStatusSubscription )
err := subscribeStatus ( c , & session . streamSession , "openvpn status" , func ( ctx context . Context , client daemon . StartedServiceClient ) ( grpc . ServerStreamingClient [ daemon . OpenVPNStatusUpdate ], error ) {
return client . SubscribeOpenVPNStatus ( ctx , & emptypb . Empty {})
}, func ( update * daemon . OpenVPNStatusUpdate ) {
handler . OnStatusUpdate ( openVPNStatusUpdateFromGRPC ( update ))
}, handler . OnError )
2026-06-14 19:19:46 +08:00
if err != nil {
2026-07-17 16:21:03 +08:00
return nil , err
2026-06-14 19:19:46 +08:00
}
2026-07-17 16:21:03 +08:00
return session , nil
}
2026-06-14 19:19:46 +08:00
2026-07-17 16:21:03 +08:00
func ( c * CommandClient ) SubmitOpenVPNChallengeResponse ( endpointTag string , challengeID string , response * OpenVPNChallengeResponse ) error {
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . SubmitOpenVPNChallengeResponse ( ctx , & daemon . OpenVPNChallengeSubmission {
EndpointTag : endpointTag ,
ChallengeID : challengeID ,
Username : response . Username ,
Password : response . Password ,
Secret : response . Secret ,
})
})
if err != nil {
return E . Cause ( err , "submit openvpn challenge response" )
}
return nil
}
2026-06-14 19:19:46 +08:00
2026-07-17 16:21:03 +08:00
func ( c * CommandClient ) CancelOpenVPNChallenge ( endpointTag string , challengeID string ) error {
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . CancelOpenVPNChallenge ( ctx , & daemon . OpenVPNChallengeCancel {
EndpointTag : endpointTag ,
ChallengeID : challengeID ,
})
})
if err != nil {
return E . Cause ( err , "cancel openvpn challenge" )
}
return nil
2026-06-14 19:19:46 +08:00
}
2026-05-21 12:37:15 +08:00
func ( c * CommandClient ) SetTailscaleExitNode ( endpointTag string , stableID string ) error {
2026-05-26 11:24:39 +08:00
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . SetTailscaleExitNode ( ctx , & daemon . SetTailscaleExitNodeRequest {
2026-05-21 12:37:15 +08:00
EndpointTag : endpointTag ,
StableID : stableID ,
})
})
if err != nil {
return E . Cause ( err , "set tailscale exit node" )
}
return nil
}
2026-06-02 11:29:06 +08:00
func ( c * CommandClient ) TailscaleLogout ( endpointTag string ) error {
_ , err := callWithResult ( c , func ( ctx context . Context , client daemon . StartedServiceClient ) ( * emptypb . Empty , error ) {
return client . TailscaleLogout ( ctx , & daemon . TailscaleLogoutRequest {
EndpointTag : endpointTag ,
})
})
if err != nil {
return E . Cause ( err , "tailscale logout" )
}
return nil
}
2026-05-26 11:24:39 +08:00
func ( c * CommandClient ) StartTailscalePing ( endpointTag string , peerIP string , handler TailscalePingHandler ) ( * TailscalePingSession , error ) {
client , parentCtx , err := c . getClientForCall ()
2026-04-10 09:24:42 +08:00
if err != nil {
2026-05-26 11:24:39 +08:00
return nil , E . Cause ( err , "start tailscale ping" )
2026-04-10 09:24:42 +08:00
}
2026-05-26 11:24:39 +08:00
streamCtx , cancel := context . WithCancel ( parentCtx )
session := & TailscalePingSession {
streamSession : streamSession {
ctx : streamCtx ,
cancel : cancel ,
closeDone : make ( chan struct {}),
},
}
failStart := func ( cause error , message string ) ( * TailscalePingSession , error ) {
cancel ()
if c . standalone {
c . closeConnection ()
}
return nil , E . Cause ( cause , message )
2026-04-10 09:24:42 +08:00
}
2026-05-26 11:24:39 +08:00
stream , err := client . StartTailscalePing ( streamCtx , & daemon . TailscalePingRequest {
2026-04-10 09:24:42 +08:00
EndpointTag : endpointTag ,
PeerIP : peerIP ,
})
if err != nil {
2026-05-26 11:24:39 +08:00
return failStart ( err , "start tailscale ping" )
2026-04-10 09:24:42 +08:00
}
2026-05-26 11:24:39 +08:00
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
}
handler . OnError ( E . Cause ( recvErr , "tailscale ping recv" ). Error ())
return
}
handler . OnPingResult ( tailscalePingResultFromGRPC ( event ))
2026-04-10 09:24:42 +08:00
}
2026-05-26 11:24:39 +08:00
}()
return session , nil
2026-04-10 09:24:42 +08:00
}
2026-05-25 11:19:06 +08:00
func ( c * CommandClient ) StartTailscaleSSHSession ( opts * TailscaleSSHOptions , handler TailscaleSSHHandler ) ( * TailscaleSSHSession , error ) {
2026-05-26 11:24:39 +08:00
client , parentCtx , err := c . getClientForCall ()
2026-05-25 11:19:06 +08:00
if err != nil {
return nil , E . Cause ( err , "start tailscale ssh session" )
}
2026-05-26 11:24:39 +08:00
streamCtx , cancel := context . WithCancel ( parentCtx )
2026-05-25 11:19:06 +08:00
failStart := func ( cause error , message string ) ( * TailscaleSSHSession , error ) {
cancel ()
if c . standalone {
c . closeConnection ()
}
return nil , E . Cause ( cause , message )
}
stream , err := client . StartTailscaleSSHSession ( streamCtx )
if err != nil {
return failStart ( err , "start tailscale ssh session" )
}
sendErr := stream . Send ( & daemon . TailscaleSSHClientMessage {
Message : & daemon . TailscaleSSHClientMessage_Start { Start : & daemon . TailscaleSSHStart {
EndpointTag : opts . EndpointTag ,
PeerAddress : opts . PeerAddress ,
Username : opts . Username ,
TerminalType : opts . TerminalType ,
Columns : opts . Columns ,
Rows : opts . Rows ,
WidthPixels : opts . WidthPixels ,
HeightPixels : opts . HeightPixels ,
HostKeys : iteratorToArray [ string ]( opts . HostKeys ),
ForwardAgent : opts . ForwardAgent ,
}},
})
if sendErr != nil {
return failStart ( sendErr , "send tailscale ssh start" )
}
session := & TailscaleSSHSession {
stream : stream ,
inputCh : make ( chan [] byte , 8 ),
resizeCh : make ( chan tailscaleSSHResize , 1 ),
ctx : streamCtx ,
cancel : cancel ,
closeDone : make ( chan struct {}),
}
2026-08-04 20:33:23 +08:00
session . wg . Go ( func () {
2026-05-25 11:19:06 +08:00
for {
select {
case <- streamCtx . Done ():
return
case data := <- session . inputCh :
sendErr := stream . Send ( & daemon . TailscaleSSHClientMessage {
Message : & daemon . TailscaleSSHClientMessage_Input { Input : & daemon . TailscaleSSHInput { Data : data }},
})
if sendErr != nil {
cancel ()
return
}
case resize := <- session . resizeCh :
sendErr := stream . Send ( & daemon . TailscaleSSHClientMessage {
Message : & daemon . TailscaleSSHClientMessage_Resize { Resize : & daemon . TailscaleSSHResize {
Columns : resize . columns ,
Rows : resize . rows ,
WidthPixels : resize . widthPixels ,
HeightPixels : resize . heightPixels ,
}},
})
if sendErr != nil {
cancel ()
return
}
}
}
2026-08-04 20:33:23 +08:00
})
2026-05-25 11:19:06 +08:00
2026-08-04 20:33:23 +08:00
session . wg . Go ( func () {
2026-05-25 11:19:06 +08:00
for {
msg , recvErr := stream . Recv ()
if recvErr == io . EOF {
cancel ()
return
}
if recvErr != nil {
handler . OnError ( E . Cause ( recvErr , "tailscale ssh recv" ). Error ())
cancel ()
return
}
switch payload := msg . GetMessage ().( type ) {
case * daemon . TailscaleSSHServerMessage_AuthBanner :
handler . OnAuthBanner ( payload . AuthBanner . Message )
case * daemon . TailscaleSSHServerMessage_Ready :
handler . OnReady ()
case * daemon . TailscaleSSHServerMessage_Output :
handler . OnOutput ( payload . Output . Data )
case * daemon . TailscaleSSHServerMessage_Exit :
handler . OnExit ( payload . Exit . ExitCode , payload . Exit . Signal , payload . Exit . ErrorMessage )
cancel ()
return
case * daemon . TailscaleSSHServerMessage_Error :
handler . OnError ( payload . Error . Message )
}
}
2026-08-04 20:33:23 +08:00
})
2026-05-25 11:19:06 +08:00
standalone := c . standalone
go func () {
session . wg . Wait ()
close ( session . closeDone )
if standalone {
c . closeConnection ()
}
}()
return session , nil
}
2026-06-14 19:19:46 +08:00
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
}