daemon: Add Tailssh

This commit is contained in:
世界
2026-05-25 11:19:06 +08:00
parent 72ce97f7c8
commit 3488377cf3
9 changed files with 1505 additions and 98 deletions
+123
View File
@@ -2,6 +2,7 @@ package libbox
import (
"context"
"io"
"net"
"os"
"path/filepath"
@@ -818,3 +819,125 @@ func (c *CommandClient) StartTailscalePing(endpointTag string, peerIP string, ha
handler.OnPingResult(tailscalePingResultFromGRPC(event))
}
}
func (c *CommandClient) StartTailscaleSSHSession(opts *TailscaleSSHOptions, handler TailscaleSSHHandler) (*TailscaleSSHSession, error) {
client, err := c.getClientForCall()
if err != nil {
return nil, E.Cause(err, "start tailscale ssh session")
}
streamCtx, cancel := context.WithCancel(context.Background())
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{}),
}
session.wg.Add(1)
go func() {
defer session.wg.Done()
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
}
}
}
}()
session.wg.Add(1)
go func() {
defer session.wg.Done()
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)
}
}
}()
standalone := c.standalone
go func() {
session.wg.Wait()
close(session.closeDone)
if standalone {
c.closeConnection()
}
}()
return session, nil
}
+5
View File
@@ -41,6 +41,7 @@ type CommandServerHandler interface {
SetSystemProxyEnabled(enabled bool) error
TriggerNativeCrash() error
WriteDebugMessage(message string)
ConnectSSHAgent() (int32, error)
}
func NewCommandServer(handler CommandServerHandler, platformInterface PlatformInterface) (*CommandServer, error) {
@@ -286,3 +287,7 @@ func (h *platformHandler) TriggerNativeCrash() error {
func (h *platformHandler) WriteDebugMessage(message string) {
(*CommandServer)(h).handler.WriteDebugMessage(message)
}
func (h *platformHandler) ConnectSSHAgent() (int32, error) {
return (*CommandServer)(h).handler.ConnectSSHAgent()
}
@@ -0,0 +1,82 @@
package libbox
import (
"bytes"
"context"
"os"
"sync"
"github.com/sagernet/sing-box/daemon"
)
type TailscaleSSHOptions struct {
EndpointTag string
PeerAddress string
Username string
TerminalType string
Columns int32
Rows int32
WidthPixels int32
HeightPixels int32
HostKeys StringIterator
ForwardAgent bool
}
type TailscaleSSHHandler interface {
OnReady()
OnOutput(data []byte)
OnAuthBanner(message string)
OnExit(exitCode int32, signal string, errorMessage string)
OnError(message string)
}
type tailscaleSSHResize struct {
columns int32
rows int32
widthPixels int32
heightPixels int32
}
type TailscaleSSHSession struct {
stream daemon.StartedService_StartTailscaleSSHSessionClient
inputCh chan []byte
resizeCh chan tailscaleSSHResize
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
closeOnce sync.Once
closeDone chan struct{}
}
func (s *TailscaleSSHSession) SendInput(data []byte) error {
select {
case <-s.ctx.Done():
return os.ErrClosed
case s.inputCh <- bytes.Clone(data):
return nil
}
}
func (s *TailscaleSSHSession) SendResize(columns int32, rows int32, widthPixels int32, heightPixels int32) error {
resize := tailscaleSSHResize{
columns: columns,
rows: rows,
widthPixels: widthPixels,
heightPixels: heightPixels,
}
select {
case s.resizeCh <- resize:
return nil
case <-s.ctx.Done():
return os.ErrClosed
}
}
func (s *TailscaleSSHSession) Close() error {
s.closeOnce.Do(func() {
s.cancel()
_ = s.stream.CloseSend()
})
<-s.closeDone
return nil
}