Add Taildrop support

This commit is contained in:
世界
2026-08-30 17:41:46 +08:00
parent c86164bd1b
commit 4b30a03adf
51 changed files with 5247 additions and 602 deletions
+1
View File
@@ -38,6 +38,7 @@ type PlatformInterface interface {
UsePlatformNotification() bool
SendNotification(notification *Notification) error
CancelNotification(identifier string, typeID int32) error
MyInterfaceAddress() []netip.Addr
+61 -26
View File
@@ -1,32 +1,66 @@
package adapter
import "context"
import (
"context"
"io"
)
type TailscaleEndpoint interface {
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
SetTailscaleExitNode(ctx context.Context, stableID string) error
Logout(ctx context.Context) error
SubscribeTaildropInbox(ctx context.Context, fn func(*TaildropInbox)) error
MarkTaildropInboxRead() error
SendTaildropFile(ctx context.Context, peerStableID string, fileName string, size int64, content io.Reader, progress func(sentBytes int64)) error
OpenTaildropFile(fileName string) (io.ReadCloser, int64, error)
DeleteTaildropFile(fileName string) error
CancelTaildropReceiving(senderID string, fileName string) error
}
type TaildropInbox struct {
Files []*TaildropFile
Receiving []*TaildropReceivingFile
}
type TaildropFile struct {
Name string
Size int64
SenderName string
ModifiedAt int64
}
type TaildropReceivingFile struct {
Name string
Size int64
ReceivedBytes int64
SenderID string
SenderName string
}
type TailscalePingResult struct {
LatencyMs float64
IsDirect bool
Endpoint string
PeerRelay string
DERPRegionID int32
DERPRegionCode string
Error string
}
type TailscaleEndpointStatus struct {
BackendState string
AuthURL string
NetworkName string
MagicDNSSuffix string
Self *TailscalePeer
ExitNode *TailscalePeer
UserGroups []*TailscaleUserGroup
KeyAuth bool
BackendState string
AuthURL string
NetworkName string
MagicDNSSuffix string
Self *TailscalePeer
ExitNode *TailscalePeer
UserGroups []*TailscaleUserGroup
KeyAuth bool
CanShareFiles bool
WaitingFileCount int32
ReceivingFileCount int32
UnreadFileCount int32
}
type TailscaleUserGroup struct {
@@ -38,23 +72,24 @@ type TailscaleUserGroup struct {
}
type TailscalePeer struct {
StableID string
HostName string
DNSName string
OS string
TailscaleIPs []string
SSHHostKeys []string
Online bool
ExitNode bool
ExitNodeOption bool
ShareeNode bool
Expired bool
Active bool
RxBytes int64
TxBytes int64
UserID int64
KeyExpiry int64
LastSeen int64
StableID string
HostName string
DNSName string
OS string
TailscaleIPs []string
SSHHostKeys []string
Online bool
ExitNode bool
ExitNodeOption bool
ShareeNode bool
Expired bool
Active bool
CanReceiveFiles bool
RxBytes int64
TxBytes int64
UserID int64
KeyExpiry int64
LastSeen int64
}
type ShellSession interface {
+13 -3
View File
@@ -79,7 +79,10 @@ func runAPITailscalePing(selector string) error {
}()
timer := time.NewTimer(commandAPITailscalePingTimeout)
defer timer.Stop()
var pongCount int
var (
pongCount int
lastPeerRelay string
)
for pongCount < commandAPITailscalePingCount {
var (
response *daemon.TailscalePingResponse
@@ -109,16 +112,23 @@ func runAPITailscalePing(selector string) error {
if response.GetEndpoint() != "" {
return nil
}
lastPeerRelay = response.GetPeerRelay()
timer.Reset(commandAPITailscalePingTimeout)
}
os.Stdout.WriteString("direct connection not established\n")
if lastPeerRelay != "" {
os.Stdout.WriteString(F.ToString("direct connection not established, relayed by peer relay ", lastPeerRelay, "\n"))
} else {
os.Stdout.WriteString("direct connection not established\n")
}
return nil
}
func formatTailscalePong(peerName string, peerAddress string, response *daemon.TailscalePingResponse) string {
via := response.GetEndpoint()
if via == "" {
if response.GetDerpRegionCode() != "" {
if response.GetPeerRelay() != "" {
via = F.ToString("peer relay ", response.GetPeerRelay())
} else if response.GetDerpRegionCode() != "" {
via = F.ToString("DERP(", response.GetDerpRegionCode(), ")")
} else {
via = F.ToString("DERP(", response.GetDerpRegionID(), ")")
@@ -0,0 +1,68 @@
package main
import (
"context"
"strconv"
"time"
"github.com/sagernet/sing-box/daemon"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
)
var commandAPITailscaleTaildrop = &cobra.Command{
Use: "taildrop",
Short: "Send and receive files over Tailscale",
}
func init() {
commandAPITailscaleTaildrop.PersistentFlags().StringVar(&commandAPITailscaleFlagEndpoint, "endpoint", "", commandAPITailscaleEndpointUsage)
commandAPITailscale.AddCommand(commandAPITailscaleTaildrop)
}
func fetchTaildropInbox(client daemon.StartedServiceClient, endpointTag string) (*daemon.TaildropInbox, error) {
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
stream, err := client.SubscribeTaildropInbox(ctx, &daemon.SubscribeTaildropInboxRequest{
EndpointTag: endpointTag,
})
if err != nil {
return nil, err
}
return stream.Recv()
}
func formatTaildropSize(size int64) string {
switch {
case size < 0:
return "-"
case size < 1000:
return F.ToString(size, " B")
case size < 1000*1000:
return formatTaildropSizeUnit(size, 1000, "KB")
case size < 1000*1000*1000:
return formatTaildropSizeUnit(size, 1000*1000, "MB")
default:
return formatTaildropSizeUnit(size, 1000*1000*1000, "GB")
}
}
func formatTaildropSizeUnit(size int64, unit int64, unitName string) string {
value := float64(size) / float64(unit)
var precision int
switch {
case value < 10:
precision = 2
case value < 100:
precision = 1
}
return strconv.FormatFloat(value, 'f', precision, 64) + " " + unitName
}
func formatTaildropModifiedAt(timestamp int64) string {
if timestamp == 0 {
return ""
}
return time.Unix(timestamp, 0).Local().Format("2006-01-02 15:04")
}
@@ -0,0 +1,42 @@
package main
import (
"github.com/sagernet/sing-box/daemon"
"github.com/spf13/cobra"
)
var commandAPITailscaleTaildropDelete = &cobra.Command{
Use: "delete <name>...",
Short: "Delete received files",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPITaildropDelete(args)
},
}
func init() {
commandAPITailscaleTaildrop.AddCommand(commandAPITailscaleTaildropDelete)
}
func runAPITaildropDelete(names []string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
endpointTag, err := resolveTailscaleEndpointTag(client)
if err != nil {
return err
}
for _, name := range names {
_, err = client.DeleteTaildropFile(globalCtx, &daemon.DeleteTaildropFileRequest{
EndpointTag: endpointTag,
Name: name,
})
if err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,119 @@
package main
import (
"io"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/sagernet/sing-box/daemon"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
)
var commandAPITailscaleTaildropGet = &cobra.Command{
Use: "get <name> [output]",
Short: "Save a received file",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
var outputPath string
if len(args) > 1 {
outputPath = args[1]
}
return runAPITaildropGet(args[0], outputPath)
},
}
func init() {
commandAPITailscaleTaildrop.AddCommand(commandAPITailscaleTaildropGet)
}
func runAPITaildropGet(name string, outputPath string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
endpointTag, err := resolveTailscaleEndpointTag(client)
if err != nil {
return err
}
if outputPath == "" {
outputPath = name
} else {
information, statErr := os.Stat(outputPath)
if statErr == nil && information.IsDir() {
outputPath = filepath.Join(outputPath, name)
}
}
ctx, cancel := signal.NotifyContext(globalCtx, os.Interrupt, syscall.SIGTERM)
defer cancel()
stream, err := client.DownloadTaildropFile(ctx, &daemon.DownloadTaildropFileRequest{
EndpointTag: endpointTag,
Name: name,
})
if err != nil {
return err
}
firstChunk, err := stream.Recv()
if err != nil {
return err
}
totalSize := firstChunk.GetSize()
var outputFile *os.File
if outputPath == "-" {
outputFile = os.Stdout
} else {
outputFile, err = os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644)
if err != nil {
return err
}
}
var downloaded int64
writeChunk := func(data []byte) error {
if len(data) == 0 {
return nil
}
_, writeErr := outputFile.Write(data)
if writeErr != nil {
return writeErr
}
downloaded += int64(len(data))
writeProgress(F.ToString(name, ": ", formatTaildropSize(downloaded), " / ", formatTaildropSize(totalSize), " "))
return nil
}
err = writeChunk(firstChunk.GetData())
for err == nil {
var chunk *daemon.DownloadTaildropFileChunk
chunk, err = stream.Recv()
if err == io.EOF {
err = nil
break
}
if err != nil {
break
}
err = writeChunk(chunk.GetData())
}
if outputPath != "-" {
closeErr := outputFile.Close()
if err == nil {
err = closeErr
}
if err != nil {
os.Remove(outputPath)
}
}
if err != nil {
if ctx.Err() != nil {
return E.New("interrupted")
}
return err
}
writeProgress("")
writeStderrLine(F.ToString("saved ", name, " (", formatTaildropSize(downloaded), ")"))
return nil
}
@@ -0,0 +1,55 @@
package main
import (
"github.com/spf13/cobra"
)
var commandAPITailscaleTaildropList = &cobra.Command{
Use: "list",
Short: "List received files",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPITaildropList()
},
}
func init() {
commandAPITailscaleTaildrop.AddCommand(commandAPITailscaleTaildropList)
}
func runAPITaildropList() error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
endpointTag, err := resolveTailscaleEndpointTag(client)
if err != nil {
return err
}
inbox, err := fetchTaildropInbox(client, endpointTag)
if err != nil {
return err
}
for _, file := range inbox.GetReceiving() {
writeStderrLine("receiving " + file.GetName() + ": " + formatTaildropSize(file.GetReceivedBytes()) + " / " + formatTaildropSize(file.GetSize()))
}
table := tableWriter{
header: []string{"NAME", "SIZE", "FROM", "RECEIVED"},
emptyMessage: "no received files",
}
for _, file := range inbox.GetFiles() {
sender := file.GetSenderName()
if sender == "" {
sender = "-"
}
table.addRow(
file.GetName(),
formatTaildropSize(file.GetSize()),
sender,
formatTaildropModifiedAt(file.GetModifiedAt()),
)
}
table.flush()
return nil
}
@@ -0,0 +1,182 @@
package main
import (
"io"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/sagernet/sing-box/daemon"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var commandAPITailscaleTaildropSend = &cobra.Command{
Use: "send <peer> <file>...",
Short: "Send files to a Tailscale peer",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPITaildropSend(args[0], args[1:])
},
}
func init() {
commandAPITailscaleTaildrop.AddCommand(commandAPITailscaleTaildropSend)
}
func runAPITaildropSend(selector string, filePaths []string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
endpoint, err := fetchTailscaleEndpoint(client)
if err != nil {
return err
}
if !endpoint.GetCanShareFiles() {
return E.New("file sharing not enabled by Tailscale admin")
}
entry, err := resolveTailscalePeer(tailscalePeerEntries(endpoint), selector)
if err != nil {
return err
}
if entry.self {
return E.New("cannot send files to this device")
}
if !entry.peer.GetCanReceiveFiles() {
return E.New("peer cannot receive files: ", tailscalePeerName(entry.peer))
}
type outgoingFile struct {
path string
name string
size int64
}
files := make([]outgoingFile, 0, len(filePaths))
manifest := make([]*daemon.TaildropOutgoingFile, 0, len(filePaths))
for _, filePath := range filePaths {
information, statErr := os.Stat(filePath)
if statErr != nil {
return statErr
}
if information.IsDir() {
return E.New("directories are not supported: ", filePath)
}
file := outgoingFile{
path: filePath,
name: filepath.Base(filePath),
size: information.Size(),
}
files = append(files, file)
manifest = append(manifest, &daemon.TaildropOutgoingFile{
Name: file.name,
Size: file.size,
})
}
ctx, cancel := signal.NotifyContext(globalCtx, os.Interrupt, syscall.SIGTERM)
defer cancel()
stream, err := client.SendTaildropFiles(ctx)
if err != nil {
return err
}
err = stream.Send(&daemon.TaildropSendClientMessage{
Message: &daemon.TaildropSendClientMessage_Start{Start: &daemon.TaildropSendStart{
EndpointTag: endpoint.GetEndpointTag(),
PeerStableID: entry.peer.GetStableID(),
Files: manifest,
}},
})
if err != nil {
return err
}
var uploadErr error
uploadDone := make(chan struct{})
go func() {
defer close(uploadDone)
failUpload := func(cause error) {
uploadErr = cause
cancel()
}
buffer := make([]byte, daemon.TaildropChunkSize)
for _, file := range files {
sourceFile, openErr := os.Open(file.path)
if openErr != nil {
failUpload(openErr)
return
}
for {
n, readErr := sourceFile.Read(buffer)
if n > 0 {
sendErr := stream.Send(&daemon.TaildropSendClientMessage{
Message: &daemon.TaildropSendClientMessage_Chunk{Chunk: &daemon.TaildropFileChunk{Data: buffer[:n]}},
})
if sendErr != nil {
sourceFile.Close()
failUpload(sendErr)
return
}
}
if readErr == io.EOF {
break
}
if readErr != nil {
sourceFile.Close()
failUpload(readErr)
return
}
}
sourceFile.Close()
sendErr := stream.Send(&daemon.TaildropSendClientMessage{
Message: &daemon.TaildropSendClientMessage_FileDone{FileDone: &daemon.TaildropFileDone{}},
})
if sendErr != nil {
failUpload(sendErr)
return
}
}
}()
peerName := tailscalePeerName(entry.peer)
for {
message, recvErr := stream.Recv()
if recvErr != nil {
cancel()
<-uploadDone
if uploadErr != nil && !E.IsCanceled(uploadErr) && status.Code(uploadErr) != codes.Canceled {
return uploadErr
}
if ctx.Err() != nil {
return E.New("interrupted")
}
if recvErr == io.EOF {
writeProgress("")
writeStderrLine(F.ToString("sent ", len(files), " file(s) to ", peerName))
return nil
}
return recvErr
}
progress := message.GetProgress()
if progress == nil {
continue
}
fileIndex := progress.FileIndex
if fileIndex < 0 || int(fileIndex) >= len(files) {
return E.New("invalid file index: ", fileIndex)
}
file := files[fileIndex]
if progress.FileCompleted {
writeProgress("")
writeStderrLine(F.ToString(file.name, ": done"))
} else {
writeProgress(F.ToString(file.name, ": ", formatTaildropSize(progress.SentBytes), " / ", formatTaildropSize(file.size), " "))
}
}
}
@@ -0,0 +1,56 @@
package main
import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var commandAPITailscaleTaildropTargets = &cobra.Command{
Use: "targets",
Short: "List peers that can receive files",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPITaildropTargets()
},
}
func init() {
commandAPITailscaleTaildrop.AddCommand(commandAPITailscaleTaildropTargets)
}
func runAPITaildropTargets() error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
endpoint, err := fetchTailscaleEndpoint(client)
if err != nil {
return err
}
if !endpoint.GetCanShareFiles() {
return E.New("file sharing not enabled by Tailscale admin")
}
entries := tailscalePeerEntries(endpoint)
if len(entries) > 0 && entries[0].self {
entries = entries[1:]
}
sortTailscalePeerEntries(entries)
table := tableWriter{
header: []string{"DNS NAME", "IP", "ONLINE"},
emptyMessage: "no file targets",
}
for _, entry := range entries {
if !entry.peer.GetCanReceiveFiles() {
continue
}
table.addRow(
tailscalePeerName(entry.peer),
tailscalePeerAddress(entry.peer),
formatYesNo(entry.peer.GetOnline()),
)
}
table.flush()
return nil
}
+95 -55
View File
@@ -34,9 +34,12 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
)
const APIVersion = 3
const APIVersion = 4
const urlTestPushMinInterval = 250 * time.Millisecond
const (
urlTestPushMinInterval = 250 * time.Millisecond
notificationQueueSize = 16
)
var _ StartedServiceServer = (*StartedService)(nil)
@@ -68,6 +71,8 @@ type StartedService struct {
urlTestObserver *observable.Observer[struct{}]
clashModeSubscriber *observable.Subscriber[struct{}]
clashModeObserver *observable.Observer[struct{}]
notificationSubscriber *observable.Subscriber[*NotificationEvent]
notificationObserver *observable.Observer[*NotificationEvent]
}
type ServiceOptions struct {
@@ -106,11 +111,13 @@ func NewStartedService(options ServiceOptions) *StartedService {
logSubscriber: observable.NewSubscriber[*log.Entry](128),
urlTestSubscriber: observable.NewSubscriber[struct{}](1),
clashModeSubscriber: observable.NewSubscriber[struct{}](1),
notificationSubscriber: observable.NewSubscriber[*NotificationEvent](notificationQueueSize),
}
s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2)
s.logObserver = observable.NewObserver(s.logSubscriber, 64)
s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1)
s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1)
s.notificationObserver = observable.NewObserver(s.notificationSubscriber, notificationQueueSize)
return s
}
@@ -283,6 +290,7 @@ func (s *StartedService) Close() {
s.logSubscriber.Close()
s.urlTestSubscriber.Close()
s.clashModeSubscriber.Close()
s.notificationSubscriber.Close()
}
func (s *StartedService) CloseService() error {
@@ -1625,14 +1633,18 @@ func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStat
}
}
result := &TailscaleEndpointStatus{
EndpointTag: tag,
BackendState: s.BackendState,
StateText: selectedLocale.TailscaleStateText(s.BackendState),
AuthURL: s.AuthURL,
NetworkName: s.NetworkName,
MagicDNSSuffix: s.MagicDNSSuffix,
UserGroups: userGroups,
KeyAuth: s.KeyAuth,
EndpointTag: tag,
BackendState: s.BackendState,
StateText: selectedLocale.TailscaleStateText(s.BackendState),
AuthURL: s.AuthURL,
NetworkName: s.NetworkName,
MagicDNSSuffix: s.MagicDNSSuffix,
UserGroups: userGroups,
KeyAuth: s.KeyAuth,
CanShareFiles: s.CanShareFiles,
WaitingFileCount: s.WaitingFileCount,
ReceivingFileCount: s.ReceivingFileCount,
UnreadFileCount: s.UnreadFileCount,
}
if s.Self != nil {
result.Self = tailscalePeerToProto(s.Self)
@@ -1645,22 +1657,23 @@ func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStat
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
return &TailscalePeer{
StableID: peer.StableID,
HostName: peer.HostName,
DnsName: peer.DNSName,
Os: peer.OS,
TailscaleIPs: peer.TailscaleIPs,
SshHostKeys: peer.SSHHostKeys,
Online: peer.Online,
ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption,
ShareeNode: peer.ShareeNode,
Expired: peer.Expired,
Active: peer.Active,
RxBytes: peer.RxBytes,
TxBytes: peer.TxBytes,
KeyExpiry: peer.KeyExpiry,
LastSeen: peer.LastSeen,
StableID: peer.StableID,
HostName: peer.HostName,
DnsName: peer.DNSName,
Os: peer.OS,
TailscaleIPs: peer.TailscaleIPs,
SshHostKeys: peer.SSHHostKeys,
Online: peer.Online,
ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption,
ShareeNode: peer.ShareeNode,
Expired: peer.Expired,
Active: peer.Active,
CanReceiveFiles: peer.CanReceiveFiles,
RxBytes: peer.RxBytes,
TxBytes: peer.TxBytes,
KeyExpiry: peer.KeyExpiry,
LastSeen: peer.LastSeen,
}
}
@@ -1676,35 +1689,9 @@ func (s *StartedService) StartTailscalePing(
boxService := s.instance
s.serviceAccess.RUnlock()
var provider adapter.TailscaleEndpoint
if request.EndpointTag != "" {
endpoint, err := resolveTailscaleEndpoint(boxService, request.EndpointTag)
if err != nil {
return err
}
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
if !loaded {
return status.Error(codes.FailedPrecondition, "endpoint does not support ping")
}
provider = pingProvider
} else {
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
if endpointManager == nil {
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
}
for _, endpoint := range endpointManager.Endpoints() {
if endpoint.Type() != C.TypeTailscale {
continue
}
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
if loaded {
provider = pingProvider
break
}
}
if provider == nil {
return status.Error(codes.NotFound, "no Tailscale endpoint found")
}
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return err
}
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
@@ -1712,6 +1699,7 @@ func (s *StartedService) StartTailscalePing(
LatencyMs: result.LatencyMs,
IsDirect: result.IsDirect,
Endpoint: result.Endpoint,
PeerRelay: result.PeerRelay,
DerpRegionID: result.DERPRegionID,
DerpRegionCode: result.DERPRegionCode,
Error: result.Error,
@@ -2019,6 +2007,58 @@ func (s *StartedService) CancelOpenVPNChallenge(ctx context.Context, request *Op
return &emptypb.Empty{}, nil
}
func (s *StartedService) SendNotification(notification *adapter.Notification) error {
s.notificationSubscriber.Emit(&NotificationEvent{
Event: &NotificationEvent_Send{
Send: &Notification{
Identifier: notification.Identifier,
TypeName: notification.TypeName,
TypeID: notification.TypeID,
Title: notification.Title,
Subtitle: notification.Subtitle,
Body: notification.Body,
OpenURL: notification.OpenURL,
},
},
})
return nil
}
func (s *StartedService) CancelNotification(identifier string, typeID int32) error {
s.notificationSubscriber.Emit(&NotificationEvent{
Event: &NotificationEvent_Cancel{
Cancel: &NotificationCancel{
Identifier: identifier,
TypeID: typeID,
},
},
})
return nil
}
func (s *StartedService) SubscribeNotifications(empty *emptypb.Empty, server grpc.ServerStreamingServer[NotificationEvent]) error {
subscription, done, err := s.notificationObserver.Subscribe()
if err != nil {
return err
}
defer s.notificationObserver.UnSubscribe(subscription)
for {
select {
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
case event := <-subscription:
err = server.Send(event)
if err != nil {
return err
}
}
}
}
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
}
+1685 -323
View File
File diff suppressed because it is too large Load Diff
+123
View File
@@ -36,6 +36,12 @@ service StartedService {
rpc SetTailscaleExitNode(SetTailscaleExitNodeRequest) returns (google.protobuf.Empty) {}
rpc TailscaleLogout(TailscaleLogoutRequest) returns (google.protobuf.Empty) {}
rpc StartTailscaleSSHSession(stream TailscaleSSHClientMessage) returns (stream TailscaleSSHServerMessage) {}
rpc SubscribeTaildropInbox(SubscribeTaildropInboxRequest) returns (stream TaildropInbox) {}
rpc MarkTaildropInboxRead(MarkTaildropInboxReadRequest) returns (google.protobuf.Empty) {}
rpc SendTaildropFiles(stream TaildropSendClientMessage) returns (stream TaildropSendServerMessage) {}
rpc DownloadTaildropFile(DownloadTaildropFileRequest) returns (stream DownloadTaildropFileChunk) {}
rpc DeleteTaildropFile(DeleteTaildropFileRequest) returns (google.protobuf.Empty) {}
rpc CancelTaildropReceiving(CancelTaildropReceivingRequest) returns (google.protobuf.Empty) {}
rpc ProvideUSBDevices(stream USBProviderMessage) returns (stream USBServerMessage) {}
rpc SubscribeUSBIPServerStatus(google.protobuf.Empty) returns (stream USBIPServerStatusUpdate) {}
rpc SubscribeOpenConnectStatus(google.protobuf.Empty) returns (stream OpenConnectStatusUpdate) {}
@@ -44,6 +50,7 @@ service StartedService {
rpc SubscribeOpenVPNStatus(google.protobuf.Empty) returns (stream OpenVPNStatusUpdate) {}
rpc SubmitOpenVPNChallengeResponse(OpenVPNChallengeSubmission) returns (google.protobuf.Empty) {}
rpc CancelOpenVPNChallenge(OpenVPNChallengeCancel) returns (google.protobuf.Empty) {}
rpc SubscribeNotifications(google.protobuf.Empty) returns (stream NotificationEvent) {}
}
message Version {
@@ -282,6 +289,10 @@ message TailscaleEndpointStatus {
repeated TailscaleUserGroup userGroups = 8;
TailscalePeer exitNode = 9;
bool keyAuth = 10;
bool canShareFiles = 11;
int32 waitingFileCount = 12;
int32 receivingFileCount = 13;
int32 unreadFileCount = 14;
}
message TailscaleUserGroup {
@@ -309,6 +320,7 @@ message TailscalePeer {
repeated string sshHostKeys = 14;
bool shareeNode = 15;
int64 lastSeen = 16;
bool canReceiveFiles = 17;
}
message TailscalePingRequest {
@@ -323,6 +335,7 @@ message TailscalePingResponse {
int32 derpRegionID = 4;
string derpRegionCode = 5;
string error = 6;
string peerRelay = 7;
}
message SetTailscaleExitNodeRequest {
@@ -397,6 +410,94 @@ message TailscaleSSHError {
string message = 1;
}
message SubscribeTaildropInboxRequest {
string endpointTag = 1;
}
message MarkTaildropInboxReadRequest {
string endpointTag = 1;
}
message TaildropInbox {
string endpointTag = 1;
repeated TaildropFile files = 2;
repeated TaildropReceivingFile receiving = 3;
}
message TaildropFile {
string name = 1;
int64 size = 2;
string senderName = 3;
int64 modifiedAt = 4;
}
message TaildropReceivingFile {
string name = 1;
int64 size = 2;
int64 receivedBytes = 3;
string senderID = 4;
string senderName = 5;
}
message TaildropSendClientMessage {
oneof message {
TaildropSendStart start = 1;
TaildropFileChunk chunk = 2;
TaildropFileDone fileDone = 3;
}
}
message TaildropSendStart {
string endpointTag = 1;
string peerStableID = 2;
repeated TaildropOutgoingFile files = 3;
}
message TaildropOutgoingFile {
string name = 1;
int64 size = 2;
}
message TaildropFileChunk {
bytes data = 1;
}
message TaildropFileDone {}
message TaildropSendServerMessage {
oneof message {
TaildropSendProgress progress = 1;
int64 receivedBytes = 2;
}
}
message TaildropSendProgress {
int32 fileIndex = 1;
int64 sentBytes = 2;
bool fileCompleted = 3;
}
message DownloadTaildropFileRequest {
string endpointTag = 1;
string name = 2;
}
message DownloadTaildropFileChunk {
int64 size = 1;
bytes data = 2;
}
message DeleteTaildropFileRequest {
string endpointTag = 1;
string name = 2;
}
message CancelTaildropReceivingRequest {
string endpointTag = 1;
string senderID = 2;
string name = 3;
}
message USBProviderMessage {
oneof message {
USBDeviceAttach attach = 1;
@@ -670,3 +771,25 @@ message OpenVPNChallengeCancel {
string endpointTag = 1;
string challengeID = 2;
}
message NotificationEvent {
oneof event {
Notification send = 1;
NotificationCancel cancel = 2;
}
}
message Notification {
string identifier = 1;
string typeName = 2;
int32 typeID = 3;
string title = 4;
string subtitle = 5;
string body = 6;
string openURL = 7;
}
message NotificationCancel {
string identifier = 1;
int32 typeID = 2;
}
+280 -4
View File
@@ -41,6 +41,12 @@ const (
StartedService_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode"
StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout"
StartedService_StartTailscaleSSHSession_FullMethodName = "/daemon.StartedService/StartTailscaleSSHSession"
StartedService_SubscribeTaildropInbox_FullMethodName = "/daemon.StartedService/SubscribeTaildropInbox"
StartedService_MarkTaildropInboxRead_FullMethodName = "/daemon.StartedService/MarkTaildropInboxRead"
StartedService_SendTaildropFiles_FullMethodName = "/daemon.StartedService/SendTaildropFiles"
StartedService_DownloadTaildropFile_FullMethodName = "/daemon.StartedService/DownloadTaildropFile"
StartedService_DeleteTaildropFile_FullMethodName = "/daemon.StartedService/DeleteTaildropFile"
StartedService_CancelTaildropReceiving_FullMethodName = "/daemon.StartedService/CancelTaildropReceiving"
StartedService_ProvideUSBDevices_FullMethodName = "/daemon.StartedService/ProvideUSBDevices"
StartedService_SubscribeUSBIPServerStatus_FullMethodName = "/daemon.StartedService/SubscribeUSBIPServerStatus"
StartedService_SubscribeOpenConnectStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenConnectStatus"
@@ -49,6 +55,7 @@ const (
StartedService_SubscribeOpenVPNStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenVPNStatus"
StartedService_SubmitOpenVPNChallengeResponse_FullMethodName = "/daemon.StartedService/SubmitOpenVPNChallengeResponse"
StartedService_CancelOpenVPNChallenge_FullMethodName = "/daemon.StartedService/CancelOpenVPNChallenge"
StartedService_SubscribeNotifications_FullMethodName = "/daemon.StartedService/SubscribeNotifications"
)
// StartedServiceClient is the client API for StartedService service.
@@ -81,6 +88,12 @@ 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)
SubscribeTaildropInbox(ctx context.Context, in *SubscribeTaildropInboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TaildropInbox], error)
MarkTaildropInboxRead(ctx context.Context, in *MarkTaildropInboxReadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SendTaildropFiles(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TaildropSendClientMessage, TaildropSendServerMessage], error)
DownloadTaildropFile(ctx context.Context, in *DownloadTaildropFileRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DownloadTaildropFileChunk], error)
DeleteTaildropFile(ctx context.Context, in *DeleteTaildropFileRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CancelTaildropReceiving(ctx context.Context, in *CancelTaildropReceivingRequest, opts ...grpc.CallOption) (*emptypb.Empty, 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)
SubscribeOpenConnectStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenConnectStatusUpdate], error)
@@ -89,6 +102,7 @@ type StartedServiceClient interface {
SubscribeOpenVPNStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenVPNStatusUpdate], error)
SubmitOpenVPNChallengeResponse(ctx context.Context, in *OpenVPNChallengeSubmission, opts ...grpc.CallOption) (*emptypb.Empty, error)
CancelOpenVPNChallenge(ctx context.Context, in *OpenVPNChallengeCancel, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeNotifications(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NotificationEvent], error)
}
type startedServiceClient struct {
@@ -461,9 +475,90 @@ 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) SubscribeTaildropInbox(ctx context.Context, in *SubscribeTaildropInboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TaildropInbox], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[12], StartedService_SubscribeTaildropInbox_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SubscribeTaildropInboxRequest, TaildropInbox]{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_SubscribeTaildropInboxClient = grpc.ServerStreamingClient[TaildropInbox]
func (c *startedServiceClient) MarkTaildropInboxRead(ctx context.Context, in *MarkTaildropInboxReadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_MarkTaildropInboxRead_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SendTaildropFiles(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TaildropSendClientMessage, TaildropSendServerMessage], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[13], StartedService_SendTaildropFiles_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[TaildropSendClientMessage, TaildropSendServerMessage]{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_SendTaildropFilesClient = grpc.BidiStreamingClient[TaildropSendClientMessage, TaildropSendServerMessage]
func (c *startedServiceClient) DownloadTaildropFile(ctx context.Context, in *DownloadTaildropFileRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DownloadTaildropFileChunk], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[14], StartedService_DownloadTaildropFile_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[DownloadTaildropFileRequest, DownloadTaildropFileChunk]{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_DownloadTaildropFileClient = grpc.ServerStreamingClient[DownloadTaildropFileChunk]
func (c *startedServiceClient) DeleteTaildropFile(ctx context.Context, in *DeleteTaildropFileRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_DeleteTaildropFile_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) CancelTaildropReceiving(ctx context.Context, in *CancelTaildropReceivingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_CancelTaildropReceiving_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
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...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[15], StartedService_ProvideUSBDevices_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
@@ -476,7 +571,7 @@ type StartedService_ProvideUSBDevicesClient = grpc.BidiStreamingClient[USBProvid
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...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[16], StartedService_SubscribeUSBIPServerStatus_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
@@ -495,7 +590,7 @@ type StartedService_SubscribeUSBIPServerStatusClient = grpc.ServerStreamingClien
func (c *startedServiceClient) SubscribeOpenConnectStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenConnectStatusUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[14], StartedService_SubscribeOpenConnectStatus_FullMethodName, cOpts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[17], StartedService_SubscribeOpenConnectStatus_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
@@ -534,7 +629,7 @@ func (c *startedServiceClient) CancelOpenConnectAuthChallenge(ctx context.Contex
func (c *startedServiceClient) SubscribeOpenVPNStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenVPNStatusUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[15], StartedService_SubscribeOpenVPNStatus_FullMethodName, cOpts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[18], StartedService_SubscribeOpenVPNStatus_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
@@ -571,6 +666,25 @@ func (c *startedServiceClient) CancelOpenVPNChallenge(ctx context.Context, in *O
return out, nil
}
func (c *startedServiceClient) SubscribeNotifications(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NotificationEvent], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[19], StartedService_SubscribeNotifications_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, NotificationEvent]{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_SubscribeNotificationsClient = grpc.ServerStreamingClient[NotificationEvent]
// StartedServiceServer is the server API for StartedService service.
// All implementations must embed UnimplementedStartedServiceServer
// for forward compatibility.
@@ -601,6 +715,12 @@ type StartedServiceServer interface {
SetTailscaleExitNode(context.Context, *SetTailscaleExitNodeRequest) (*emptypb.Empty, error)
TailscaleLogout(context.Context, *TailscaleLogoutRequest) (*emptypb.Empty, error)
StartTailscaleSSHSession(grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]) error
SubscribeTaildropInbox(*SubscribeTaildropInboxRequest, grpc.ServerStreamingServer[TaildropInbox]) error
MarkTaildropInboxRead(context.Context, *MarkTaildropInboxReadRequest) (*emptypb.Empty, error)
SendTaildropFiles(grpc.BidiStreamingServer[TaildropSendClientMessage, TaildropSendServerMessage]) error
DownloadTaildropFile(*DownloadTaildropFileRequest, grpc.ServerStreamingServer[DownloadTaildropFileChunk]) error
DeleteTaildropFile(context.Context, *DeleteTaildropFileRequest) (*emptypb.Empty, error)
CancelTaildropReceiving(context.Context, *CancelTaildropReceivingRequest) (*emptypb.Empty, error)
ProvideUSBDevices(grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error
SubscribeUSBIPServerStatus(*emptypb.Empty, grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error
SubscribeOpenConnectStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenConnectStatusUpdate]) error
@@ -609,6 +729,7 @@ type StartedServiceServer interface {
SubscribeOpenVPNStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenVPNStatusUpdate]) error
SubmitOpenVPNChallengeResponse(context.Context, *OpenVPNChallengeSubmission) (*emptypb.Empty, error)
CancelOpenVPNChallenge(context.Context, *OpenVPNChallengeCancel) (*emptypb.Empty, error)
SubscribeNotifications(*emptypb.Empty, grpc.ServerStreamingServer[NotificationEvent]) error
mustEmbedUnimplementedStartedServiceServer()
}
@@ -723,6 +844,30 @@ func (UnimplementedStartedServiceServer) StartTailscaleSSHSession(grpc.BidiStrea
return status.Error(codes.Unimplemented, "method StartTailscaleSSHSession not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeTaildropInbox(*SubscribeTaildropInboxRequest, grpc.ServerStreamingServer[TaildropInbox]) error {
return status.Error(codes.Unimplemented, "method SubscribeTaildropInbox not implemented")
}
func (UnimplementedStartedServiceServer) MarkTaildropInboxRead(context.Context, *MarkTaildropInboxReadRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method MarkTaildropInboxRead not implemented")
}
func (UnimplementedStartedServiceServer) SendTaildropFiles(grpc.BidiStreamingServer[TaildropSendClientMessage, TaildropSendServerMessage]) error {
return status.Error(codes.Unimplemented, "method SendTaildropFiles not implemented")
}
func (UnimplementedStartedServiceServer) DownloadTaildropFile(*DownloadTaildropFileRequest, grpc.ServerStreamingServer[DownloadTaildropFileChunk]) error {
return status.Error(codes.Unimplemented, "method DownloadTaildropFile not implemented")
}
func (UnimplementedStartedServiceServer) DeleteTaildropFile(context.Context, *DeleteTaildropFileRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteTaildropFile not implemented")
}
func (UnimplementedStartedServiceServer) CancelTaildropReceiving(context.Context, *CancelTaildropReceivingRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CancelTaildropReceiving not implemented")
}
func (UnimplementedStartedServiceServer) ProvideUSBDevices(grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error {
return status.Error(codes.Unimplemented, "method ProvideUSBDevices not implemented")
}
@@ -754,6 +899,10 @@ func (UnimplementedStartedServiceServer) SubmitOpenVPNChallengeResponse(context.
func (UnimplementedStartedServiceServer) CancelOpenVPNChallenge(context.Context, *OpenVPNChallengeCancel) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CancelOpenVPNChallenge not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeNotifications(*emptypb.Empty, grpc.ServerStreamingServer[NotificationEvent]) error {
return status.Error(codes.Unimplemented, "method SubscribeNotifications not implemented")
}
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
@@ -1155,6 +1304,89 @@ 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_SubscribeTaildropInbox_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeTaildropInboxRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeTaildropInbox(m, &grpc.GenericServerStream[SubscribeTaildropInboxRequest, TaildropInbox]{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_SubscribeTaildropInboxServer = grpc.ServerStreamingServer[TaildropInbox]
func _StartedService_MarkTaildropInboxRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MarkTaildropInboxReadRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).MarkTaildropInboxRead(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_MarkTaildropInboxRead_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).MarkTaildropInboxRead(ctx, req.(*MarkTaildropInboxReadRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SendTaildropFiles_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(StartedServiceServer).SendTaildropFiles(&grpc.GenericServerStream[TaildropSendClientMessage, TaildropSendServerMessage]{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_SendTaildropFilesServer = grpc.BidiStreamingServer[TaildropSendClientMessage, TaildropSendServerMessage]
func _StartedService_DownloadTaildropFile_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(DownloadTaildropFileRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).DownloadTaildropFile(m, &grpc.GenericServerStream[DownloadTaildropFileRequest, DownloadTaildropFileChunk]{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_DownloadTaildropFileServer = grpc.ServerStreamingServer[DownloadTaildropFileChunk]
func _StartedService_DeleteTaildropFile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteTaildropFileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).DeleteTaildropFile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_DeleteTaildropFile_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).DeleteTaildropFile(ctx, req.(*DeleteTaildropFileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_CancelTaildropReceiving_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CancelTaildropReceivingRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).CancelTaildropReceiving(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_CancelTaildropReceiving_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).CancelTaildropReceiving(ctx, req.(*CancelTaildropReceivingRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_ProvideUSBDevices_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(StartedServiceServer).ProvideUSBDevices(&grpc.GenericServerStream[USBProviderMessage, USBServerMessage]{ServerStream: stream})
}
@@ -1267,6 +1499,17 @@ func _StartedService_CancelOpenVPNChallenge_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeNotifications_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeNotifications(m, &grpc.GenericServerStream[emptypb.Empty, NotificationEvent]{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_SubscribeNotificationsServer = grpc.ServerStreamingServer[NotificationEvent]
// 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)
@@ -1330,6 +1573,18 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "TailscaleLogout",
Handler: _StartedService_TailscaleLogout_Handler,
},
{
MethodName: "MarkTaildropInboxRead",
Handler: _StartedService_MarkTaildropInboxRead_Handler,
},
{
MethodName: "DeleteTaildropFile",
Handler: _StartedService_DeleteTaildropFile_Handler,
},
{
MethodName: "CancelTaildropReceiving",
Handler: _StartedService_CancelTaildropReceiving_Handler,
},
{
MethodName: "SubmitOpenConnectAuthResponse",
Handler: _StartedService_SubmitOpenConnectAuthResponse_Handler,
@@ -1409,6 +1664,22 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true,
ClientStreams: true,
},
{
StreamName: "SubscribeTaildropInbox",
Handler: _StartedService_SubscribeTaildropInbox_Handler,
ServerStreams: true,
},
{
StreamName: "SendTaildropFiles",
Handler: _StartedService_SendTaildropFiles_Handler,
ServerStreams: true,
ClientStreams: true,
},
{
StreamName: "DownloadTaildropFile",
Handler: _StartedService_DownloadTaildropFile_Handler,
ServerStreams: true,
},
{
StreamName: "ProvideUSBDevices",
Handler: _StartedService_ProvideUSBDevices_Handler,
@@ -1430,6 +1701,11 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_SubscribeOpenVPNStatus_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeNotifications",
Handler: _StartedService_SubscribeNotifications_Handler,
ServerStreams: true,
},
},
Metadata: "daemon/started_service.proto",
}
+394
View File
@@ -0,0 +1,394 @@
package daemon
import (
"context"
"errors"
"io"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"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"
)
const (
// gRPC splits DATA frames at the unexported transport.http2MaxFrameLen (16384),
// counting the 5-byte message prefix. A chunk message that stays inside one frame
// arrives as a single mem.Buffer, so BufferSlice.MaterializeToBuffer returns it
// without taking a pooled buffer, and codecV2.Marshal fits the 16 KiB pool tier.
TaildropChunkSize = 16*1024 - 64
TaildropProgressMinInterval = 200 * time.Millisecond
// Kept well below the sender's flow control window, so a sender that stops at
// the window edge always has an acknowledgement pending.
TaildropReceiveAckInterval = 1 << 20
)
func resolveTailscaleProvider(instance *Instance, tag string) (adapter.TailscaleEndpoint, string, error) {
if tag != "" {
endpoint, err := resolveTailscaleEndpoint(instance, tag)
if err != nil {
return nil, "", err
}
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
if !loaded {
return nil, "", status.Error(codes.FailedPrecondition, "endpoint does not support tailscale")
}
return provider, endpoint.Tag(), nil
}
endpointManager := service.FromContext[adapter.EndpointManager](instance.ctx)
if endpointManager == nil {
return nil, "", status.Error(codes.FailedPrecondition, "endpoint manager not available")
}
for _, endpoint := range endpointManager.Endpoints() {
if endpoint.Type() != C.TypeTailscale {
continue
}
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
if loaded {
return provider, endpoint.Tag(), nil
}
}
return nil, "", status.Error(codes.NotFound, "no Tailscale endpoint found")
}
func taildropError(err error) error {
if errors.Is(err, os.ErrNotExist) {
return status.Error(codes.NotFound, err.Error())
}
return err
}
func (s *StartedService) SubscribeTaildropInbox(
request *SubscribeTaildropInboxRequest,
server grpc.ServerStreamingServer[TaildropInbox],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
return s.followInstance(server.Context(), func(ctx context.Context, instance *Instance) error {
var (
provider adapter.TailscaleEndpoint
endpointTag string
)
if instance != nil {
var resolveErr error
provider, endpointTag, resolveErr = resolveTailscaleProvider(instance, request.EndpointTag)
if resolveErr != nil && status.Code(resolveErr) != codes.NotFound {
return resolveErr
}
}
if provider == nil {
sendErr := server.Send(&TaildropInbox{})
if sendErr != nil {
return sendErr
}
<-ctx.Done()
return nil
}
subscribeCtx, cancel := context.WithCancel(ctx)
defer cancel()
var sendErr error
subscribeErr := provider.SubscribeTaildropInbox(subscribeCtx, func(inbox *adapter.TaildropInbox) {
if sendErr != nil {
return
}
sendErr = server.Send(taildropInboxToProto(endpointTag, inbox))
if sendErr != nil {
cancel()
}
})
if sendErr != nil {
return sendErr
}
if subscribeErr != nil && !errors.Is(subscribeErr, context.Canceled) {
if !errors.Is(subscribeErr, os.ErrClosed) {
return taildropError(subscribeErr)
}
sendErr = server.Send(&TaildropInbox{EndpointTag: endpointTag})
if sendErr != nil {
return sendErr
}
}
<-ctx.Done()
return nil
})
}
func taildropInboxToProto(endpointTag string, inbox *adapter.TaildropInbox) *TaildropInbox {
result := &TaildropInbox{EndpointTag: endpointTag}
for _, file := range inbox.Files {
result.Files = append(result.Files, &TaildropFile{
Name: file.Name,
Size: file.Size,
SenderName: file.SenderName,
ModifiedAt: file.ModifiedAt,
})
}
for _, file := range inbox.Receiving {
result.Receiving = append(result.Receiving, &TaildropReceivingFile{
Name: file.Name,
Size: file.Size,
ReceivedBytes: file.ReceivedBytes,
SenderID: file.SenderID,
SenderName: file.SenderName,
})
}
return result
}
func (s *StartedService) MarkTaildropInboxRead(ctx context.Context, request *MarkTaildropInboxReadRequest) (*emptypb.Empty, error) {
err := s.waitForStarted(ctx)
if err != nil {
return nil, err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return nil, err
}
err = provider.MarkTaildropInboxRead()
if err != nil {
return nil, taildropError(err)
}
return &emptypb.Empty{}, nil
}
func (s *StartedService) SendTaildropFiles(
server grpc.BidiStreamingServer[TaildropSendClientMessage, TaildropSendServerMessage],
) error {
streamCtx := server.Context()
err := s.waitForStarted(streamCtx)
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
firstMessage, err := server.Recv()
if err != nil {
return err
}
start := firstMessage.GetStart()
if start == nil {
return status.Error(codes.InvalidArgument, "expected start message")
}
if len(start.Files) == 0 {
return status.Error(codes.InvalidArgument, "no files to send")
}
provider, _, err := resolveTailscaleProvider(boxService, start.EndpointTag)
if err != nil {
return err
}
var sendAccess sync.Mutex
sendMessage := func(message *TaildropSendServerMessage) {
sendAccess.Lock()
defer sendAccess.Unlock()
_ = server.Send(message)
}
chunkSource := &taildropChunkReader{server: server}
chunkSource.reportReceived = func(receivedBytes int64) {
sendMessage(&TaildropSendServerMessage{
Message: &TaildropSendServerMessage_ReceivedBytes{ReceivedBytes: receivedBytes},
})
}
for index, file := range start.Files {
fileIndex := int32(index)
chunkSource.beginFile()
var lastProgress time.Time
err = provider.SendTaildropFile(streamCtx, start.PeerStableID, file.Name, file.Size, chunkSource, func(sentBytes int64) {
now := time.Now()
if now.Sub(lastProgress) < TaildropProgressMinInterval {
return
}
lastProgress = now
sendMessage(&TaildropSendServerMessage{
Message: &TaildropSendServerMessage_Progress{
Progress: &TaildropSendProgress{FileIndex: fileIndex, SentBytes: sentBytes},
},
})
})
if err != nil {
return taildropError(err)
}
err = chunkSource.finishFile(file.Size)
if err != nil {
return err
}
sendMessage(&TaildropSendServerMessage{
Message: &TaildropSendServerMessage_Progress{
Progress: &TaildropSendProgress{FileIndex: fileIndex, SentBytes: chunkSource.fileReceived, FileCompleted: true},
},
})
}
return nil
}
type taildropChunkReader struct {
server grpc.BidiStreamingServer[TaildropSendClientMessage, TaildropSendServerMessage]
buffer []byte
fileDone bool
fileReceived int64
reportReceived func(receivedBytes int64)
received int64
acknowledged int64
}
func (r *taildropChunkReader) beginFile() {
r.fileDone = false
r.fileReceived = 0
}
func (r *taildropChunkReader) Read(destination []byte) (int, error) {
for len(r.buffer) == 0 {
if r.fileDone {
return 0, io.EOF
}
message, err := r.server.Recv()
if err != nil {
return 0, err
}
switch content := message.Message.(type) {
case *TaildropSendClientMessage_Chunk:
r.buffer = content.Chunk.Data
case *TaildropSendClientMessage_FileDone:
r.fileDone = true
default:
return 0, status.Error(codes.InvalidArgument, "expected chunk message")
}
}
n := copy(destination, r.buffer)
r.buffer = r.buffer[n:]
r.fileReceived += int64(n)
r.received += int64(n)
if r.reportReceived != nil && r.received-r.acknowledged >= TaildropReceiveAckInterval {
r.acknowledged = r.received
r.reportReceived(r.received)
}
return n, nil
}
func (r *taildropChunkReader) finishFile(declaredSize int64) error {
for !r.fileDone {
if len(r.buffer) > 0 {
return status.Error(codes.InvalidArgument, "file changed while sending")
}
message, err := r.server.Recv()
if err != nil {
return err
}
switch content := message.Message.(type) {
case *TaildropSendClientMessage_Chunk:
r.buffer = content.Chunk.Data
case *TaildropSendClientMessage_FileDone:
r.fileDone = true
default:
return status.Error(codes.InvalidArgument, "expected chunk message")
}
}
if len(r.buffer) > 0 {
return status.Error(codes.InvalidArgument, "file changed while sending")
}
if declaredSize >= 0 && r.fileReceived != declaredSize {
return status.Error(codes.InvalidArgument, "file changed while sending")
}
return nil
}
func (s *StartedService) DownloadTaildropFile(
request *DownloadTaildropFileRequest,
server grpc.ServerStreamingServer[DownloadTaildropFileChunk],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return err
}
file, size, err := provider.OpenTaildropFile(request.Name)
if err != nil {
return taildropError(err)
}
defer file.Close()
err = server.Send(&DownloadTaildropFileChunk{Size: size})
if err != nil {
return err
}
buffer := make([]byte, TaildropChunkSize)
for {
n, readErr := file.Read(buffer)
if n > 0 {
err = server.Send(&DownloadTaildropFileChunk{Data: buffer[:n]})
if err != nil {
return err
}
}
if readErr == io.EOF {
return nil
}
if readErr != nil {
return readErr
}
}
}
func (s *StartedService) DeleteTaildropFile(ctx context.Context, request *DeleteTaildropFileRequest) (*emptypb.Empty, error) {
err := s.waitForStarted(ctx)
if err != nil {
return nil, err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return nil, err
}
err = provider.DeleteTaildropFile(request.Name)
if err != nil {
return nil, taildropError(err)
}
return &emptypb.Empty{}, nil
}
func (s *StartedService) CancelTaildropReceiving(ctx context.Context, request *CancelTaildropReceivingRequest) (*emptypb.Empty, error) {
err := s.waitForStarted(ctx)
if err != nil {
return nil, err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return nil, err
}
err = provider.CancelTaildropReceiving(request.SenderID, request.Name)
if err != nil {
return nil, taildropError(err)
}
return &emptypb.Empty{}, nil
}
+14 -1
View File
@@ -5,7 +5,8 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [listen_port](#listen_port)
:material-plus: [ssh_server](#ssh_server)
:material-plus: [ssh_server](#ssh_server)
:material-plus: [taildrop_directory](#taildrop_directory)
!!! quote "Changes in sing-box 1.13.0"
@@ -43,6 +44,7 @@ icon: material/new-box
"system_interface_mtu": 0,
"udp_timeout": "5m",
"ssh_server": false,
"taildrop_directory": "",
... // Dial Fields
}
@@ -211,6 +213,17 @@ Refuse the SFTP subsystem.
Refuse local and remote TCP and Unix-socket forwarding, including SSH agent forwarding.
#### taildrop_directory
!!! question "Since sing-box 1.14.0"
The directory where files received from tailnet peers are stored.
Relative paths are resolved against the working directory, as [state_directory](#state_directory)
is.
`Taildrop` is used by default.
### Dial Fields
!!! note
+13 -1
View File
@@ -5,7 +5,8 @@ icon: material/new-box
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [listen_port](#listen_port)
:material-plus: [ssh_server](#ssh_server)
:material-plus: [ssh_server](#ssh_server)
:material-plus: [taildrop_directory](#taildrop_directory)
!!! quote "sing-box 1.13.0 中的更改"
@@ -43,6 +44,7 @@ icon: material/new-box
"system_interface_mtu": 0,
"udp_timeout": "5m",
"ssh_server": false,
"taildrop_directory": "",
... // 拨号字段
}
@@ -210,6 +212,16 @@ UDP NAT 过期时间。
拒绝本地和远程的 TCP 与 Unix 套接字转发,包括 SSH agent 转发。
#### taildrop_directory
!!! question "自 sing-box 1.14.0 起"
存储从 tailnet 对等节点接收到的文件的目录。
相对路径基于工作目录解析,与 [state_directory](#state_directory) 相同。
默认使用 `Taildrop`
### 拨号字段
!!! note
+3
View File
@@ -4588,6 +4588,9 @@
"additionalProperties": false
}
]
},
"taildrop_directory": {
"type": "string"
}
},
"required": [
+3 -1
View File
@@ -64,7 +64,9 @@ func serviceSetInsecureMode(value string) error {
if err != nil {
return E.Cause(err, "validate working directory")
}
return saveSecuritySettings(directory, securitySettings{InsecureModeEnabled: enabled})
return updateDaemonSettings(directory, func(settings *daemonSettings) {
settings.InsecureModeEnabled = enabled
})
}
func validateProtectedLinuxDirectory(directory string) error {
+3 -1
View File
@@ -93,7 +93,9 @@ func serviceSetInsecureMode(value string) error {
if err != nil {
return E.Cause(err, "validate working directory")
}
return saveSecuritySettings(directory, securitySettings{InsecureModeEnabled: enabled})
return updateDaemonSettings(directory, func(settings *daemonSettings) {
settings.InsecureModeEnabled = enabled
})
}
func installedServiceWorkingDirectory() (string, error) {
+21 -1
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/tailscale/atomicfile"
@@ -258,7 +259,9 @@ func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *Se
return nil, err
}
wasEnabled := s.daemon.insecureModeEnabled()
err = saveSecuritySettings(workingDirectory, securitySettings{InsecureModeEnabled: false})
err = updateDaemonSettings(workingDirectory, func(settings *daemonSettings) {
settings.InsecureModeEnabled = false
})
if err != nil {
return nil, err
}
@@ -276,6 +279,23 @@ func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *Se
return &emptypb.Empty{}, nil
}
func (s *desktopService) SetLocale(ctx context.Context, request *SetLocaleRequest) (*emptypb.Empty, error) {
_, err := peerIdentityFromContext(ctx)
if err != nil {
return nil, err
}
if !locale.Set(request.Locale) {
return nil, status.Error(codes.InvalidArgument, "unsupported locale: "+request.Locale)
}
err = updateDaemonSettings(workingDirectory, func(settings *daemonSettings) {
settings.Locale = request.Locale
})
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
func (d *Daemon) cleanFailedStartLocked(ownerUserID string, options startOptions, startError error) error {
var platformError error
if d.platform != nil {
+116 -66
View File
@@ -1519,6 +1519,50 @@ func (x *SetInsecureModeEnabledRequest) GetEnabled() bool {
return false
}
type SetLocaleRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Locale string `protobuf:"bytes,1,opt,name=locale,proto3" json:"locale,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetLocaleRequest) Reset() {
*x = SetLocaleRequest{}
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetLocaleRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetLocaleRequest) ProtoMessage() {}
func (x *SetLocaleRequest) ProtoReflect() protoreflect.Message {
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetLocaleRequest.ProtoReflect.Descriptor instead.
func (*SetLocaleRequest) Descriptor() ([]byte, []int) {
return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{25}
}
func (x *SetLocaleRequest) GetLocale() string {
if x != nil {
return x.Locale
}
return ""
}
type InstallUpdateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
InstallerPath string `protobuf:"bytes,1,opt,name=installer_path,json=installerPath,proto3" json:"installer_path,omitempty"`
@@ -1528,7 +1572,7 @@ type InstallUpdateRequest struct {
func (x *InstallUpdateRequest) Reset() {
*x = InstallUpdateRequest{}
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[25]
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1540,7 +1584,7 @@ func (x *InstallUpdateRequest) String() string {
func (*InstallUpdateRequest) ProtoMessage() {}
func (x *InstallUpdateRequest) ProtoReflect() protoreflect.Message {
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[25]
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1553,7 +1597,7 @@ func (x *InstallUpdateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InstallUpdateRequest.ProtoReflect.Descriptor instead.
func (*InstallUpdateRequest) Descriptor() ([]byte, []int) {
return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{25}
return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{26}
}
func (x *InstallUpdateRequest) GetInstallerPath() string {
@@ -1572,7 +1616,7 @@ type InstallUpdateResponse struct {
func (x *InstallUpdateResponse) Reset() {
*x = InstallUpdateResponse{}
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[26]
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1584,7 +1628,7 @@ func (x *InstallUpdateResponse) String() string {
func (*InstallUpdateResponse) ProtoMessage() {}
func (x *InstallUpdateResponse) ProtoReflect() protoreflect.Message {
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[26]
mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1597,7 +1641,7 @@ func (x *InstallUpdateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InstallUpdateResponse.ProtoReflect.Descriptor instead.
func (*InstallUpdateResponse) Descriptor() ([]byte, []int) {
return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{26}
return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{27}
}
func (x *InstallUpdateResponse) GetResult() InstallUpdateResult {
@@ -1706,7 +1750,9 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"\tavailable\x18\x01 \x01(\bR\tavailable\x122\n" +
"\x15insecure_mode_enabled\x18\x02 \x01(\bR\x13insecureModeEnabled\"9\n" +
"\x1dSetInsecureModeEnabledRequest\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"=\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"*\n" +
"\x10SetLocaleRequest\x12\x16\n" +
"\x06locale\x18\x01 \x01(\tR\x06locale\"=\n" +
"\x14InstallUpdateRequest\x12%\n" +
"\x0einstaller_path\x18\x01 \x01(\tR\rinstallerPath\"M\n" +
"\x15InstallUpdateResponse\x124\n" +
@@ -1720,7 +1766,7 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"!INSTALL_UPDATE_RESULT_UNSPECIFIED\x10\x00\x12!\n" +
"\x1dINSTALL_UPDATE_RESULT_STARTED\x10\x01\x12)\n" +
"%INSTALL_UPDATE_RESULT_SIGNER_MISMATCH\x10\x02\x12#\n" +
"\x1fINSTALL_UPDATE_RESULT_NOT_NEWER\x10\x032\xc4\f\n" +
"\x1fINSTALL_UPDATE_RESULT_NOT_NEWER\x10\x032\x86\r\n" +
"\x0eDesktopService\x12>\n" +
"\rGetDaemonInfo\x12\x16.google.protobuf.Empty\x1a\x13.desktop.DaemonInfo\"\x00\x12@\n" +
"\fClaimService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12C\n" +
@@ -1742,7 +1788,8 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"\x13DeleteAllOOMReports\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12P\n" +
"\rInstallUpdate\x12\x1d.desktop.InstallUpdateRequest\x1a\x1e.desktop.InstallUpdateResponse\"\x00\x12J\n" +
"\x13GetSecuritySettings\x12\x16.google.protobuf.Empty\x1a\x19.desktop.SecuritySettings\"\x00\x12Z\n" +
"\x16SetInsecureModeEnabled\x12&.desktop.SetInsecureModeEnabledRequest\x1a\x16.google.protobuf.Empty\"\x002\x87\x05\n" +
"\x16SetInsecureModeEnabled\x12&.desktop.SetInsecureModeEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n" +
"\tSetLocale\x12\x19.desktop.SetLocaleRequest\x1a\x16.google.protobuf.Empty\"\x002\x87\x05\n" +
"\x12ApplicationService\x12?\n" +
"\vCheckConfig\x12\x16.desktop.ConfigContent\x1a\x16.google.protobuf.Empty\"\x00\x12@\n" +
"\fFormatConfig\x12\x16.desktop.ConfigContent\x1a\x16.desktop.ConfigContent\"\x00\x12H\n" +
@@ -1767,7 +1814,7 @@ func file_experimental_boxdd_desktop_service_proto_rawDescGZIP() []byte {
var (
file_experimental_boxdd_desktop_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
file_experimental_boxdd_desktop_service_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
file_experimental_boxdd_desktop_service_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
file_experimental_boxdd_desktop_service_proto_goTypes = []any{
(DaemonOwnership)(0), // 0: desktop.DaemonOwnership
(InstallUpdateResult)(0), // 1: desktop.InstallUpdateResult
@@ -1797,11 +1844,12 @@ var (
(*OOMReportFile)(nil), // 25: desktop.OOMReportFile
(*SecuritySettings)(nil), // 26: desktop.SecuritySettings
(*SetInsecureModeEnabledRequest)(nil), // 27: desktop.SetInsecureModeEnabledRequest
(*InstallUpdateRequest)(nil), // 28: desktop.InstallUpdateRequest
(*InstallUpdateResponse)(nil), // 29: desktop.InstallUpdateResponse
(*emptypb.Empty)(nil), // 30: google.protobuf.Empty
(*daemon.NetworkQualityTestProgress)(nil), // 31: daemon.NetworkQualityTestProgress
(*daemon.STUNTestProgress)(nil), // 32: daemon.STUNTestProgress
(*SetLocaleRequest)(nil), // 28: desktop.SetLocaleRequest
(*InstallUpdateRequest)(nil), // 29: desktop.InstallUpdateRequest
(*InstallUpdateResponse)(nil), // 30: desktop.InstallUpdateResponse
(*emptypb.Empty)(nil), // 31: google.protobuf.Empty
(*daemon.NetworkQualityTestProgress)(nil), // 32: daemon.NetworkQualityTestProgress
(*daemon.STUNTestProgress)(nil), // 33: daemon.STUNTestProgress
}
)
@@ -1814,66 +1862,68 @@ var file_experimental_boxdd_desktop_service_proto_depIdxs = []int32{
21, // 5: desktop.OOMReportList.reports:type_name -> desktop.OOMReportEntry
25, // 6: desktop.OOMReportContent.files:type_name -> desktop.OOMReportFile
1, // 7: desktop.InstallUpdateResponse.result:type_name -> desktop.InstallUpdateResult
30, // 8: desktop.DesktopService.GetDaemonInfo:input_type -> google.protobuf.Empty
30, // 9: desktop.DesktopService.ClaimService:input_type -> google.protobuf.Empty
30, // 10: desktop.DesktopService.TakeOverService:input_type -> google.protobuf.Empty
31, // 8: desktop.DesktopService.GetDaemonInfo:input_type -> google.protobuf.Empty
31, // 9: desktop.DesktopService.ClaimService:input_type -> google.protobuf.Empty
31, // 10: desktop.DesktopService.TakeOverService:input_type -> google.protobuf.Empty
7, // 11: desktop.DesktopService.StartService:input_type -> desktop.StartServiceRequest
30, // 12: desktop.DesktopService.GetWorkingDirectory:input_type -> google.protobuf.Empty
30, // 13: desktop.DesktopService.DestroyWorkingDirectory:input_type -> google.protobuf.Empty
30, // 14: desktop.DesktopService.ListCrashReports:input_type -> google.protobuf.Empty
31, // 12: desktop.DesktopService.GetWorkingDirectory:input_type -> google.protobuf.Empty
31, // 13: desktop.DesktopService.DestroyWorkingDirectory:input_type -> google.protobuf.Empty
31, // 14: desktop.DesktopService.ListCrashReports:input_type -> google.protobuf.Empty
15, // 15: desktop.DesktopService.ReadCrashReport:input_type -> desktop.CrashReportRequest
15, // 16: desktop.DesktopService.MarkCrashReportRead:input_type -> desktop.CrashReportRequest
16, // 17: desktop.DesktopService.ExportCrashReport:input_type -> desktop.CrashReportExportRequest
15, // 18: desktop.DesktopService.DeleteCrashReport:input_type -> desktop.CrashReportRequest
30, // 19: desktop.DesktopService.DeleteAllCrashReports:input_type -> google.protobuf.Empty
30, // 20: desktop.DesktopService.ListOOMReports:input_type -> google.protobuf.Empty
31, // 19: desktop.DesktopService.DeleteAllCrashReports:input_type -> google.protobuf.Empty
31, // 20: desktop.DesktopService.ListOOMReports:input_type -> google.protobuf.Empty
22, // 21: desktop.DesktopService.ReadOOMReport:input_type -> desktop.OOMReportRequest
22, // 22: desktop.DesktopService.MarkOOMReportRead:input_type -> desktop.OOMReportRequest
23, // 23: desktop.DesktopService.ExportOOMReport:input_type -> desktop.OOMReportExportRequest
22, // 24: desktop.DesktopService.DeleteOOMReport:input_type -> desktop.OOMReportRequest
30, // 25: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty
28, // 26: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest
30, // 27: desktop.DesktopService.GetSecuritySettings:input_type -> google.protobuf.Empty
31, // 25: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty
29, // 26: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest
31, // 27: desktop.DesktopService.GetSecuritySettings:input_type -> google.protobuf.Empty
27, // 28: desktop.DesktopService.SetInsecureModeEnabled:input_type -> desktop.SetInsecureModeEnabledRequest
9, // 29: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent
9, // 30: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent
30, // 31: desktop.ApplicationService.GenerateConfigSchema:input_type -> google.protobuf.Empty
10, // 32: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent
11, // 33: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData
3, // 34: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest
4, // 35: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest
5, // 36: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest
6, // 37: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo
30, // 38: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty
30, // 39: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty
30, // 40: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty
12, // 41: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo
30, // 42: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty
13, // 43: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList
17, // 44: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent
30, // 45: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty
19, // 46: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive
30, // 47: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty
30, // 48: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty
20, // 49: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList
24, // 50: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent
30, // 51: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty
19, // 52: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive
30, // 53: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty
30, // 54: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty
29, // 55: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse
26, // 56: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings
30, // 57: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty
30, // 58: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty
9, // 59: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent
9, // 60: desktop.ApplicationService.GenerateConfigSchema:output_type -> desktop.ConfigContent
11, // 61: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData
10, // 62: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent
30, // 63: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty
31, // 64: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
32, // 65: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress
37, // [37:66] is the sub-list for method output_type
8, // [8:37] is the sub-list for method input_type
28, // 29: desktop.DesktopService.SetLocale:input_type -> desktop.SetLocaleRequest
9, // 30: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent
9, // 31: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent
31, // 32: desktop.ApplicationService.GenerateConfigSchema:input_type -> google.protobuf.Empty
10, // 33: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent
11, // 34: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData
3, // 35: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest
4, // 36: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest
5, // 37: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest
6, // 38: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo
31, // 39: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty
31, // 40: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty
31, // 41: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty
12, // 42: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo
31, // 43: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty
13, // 44: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList
17, // 45: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent
31, // 46: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty
19, // 47: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive
31, // 48: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty
31, // 49: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty
20, // 50: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList
24, // 51: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent
31, // 52: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty
19, // 53: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive
31, // 54: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty
31, // 55: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty
30, // 56: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse
26, // 57: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings
31, // 58: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty
31, // 59: desktop.DesktopService.SetLocale:output_type -> google.protobuf.Empty
31, // 60: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty
9, // 61: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent
9, // 62: desktop.ApplicationService.GenerateConfigSchema:output_type -> desktop.ConfigContent
11, // 63: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData
10, // 64: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent
31, // 65: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty
32, // 66: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
33, // 67: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress
38, // [38:68] is the sub-list for method output_type
8, // [8:38] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
@@ -1890,7 +1940,7 @@ func file_experimental_boxdd_desktop_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_experimental_boxdd_desktop_service_proto_rawDesc), len(file_experimental_boxdd_desktop_service_proto_rawDesc)),
NumEnums: 3,
NumMessages: 27,
NumMessages: 28,
NumExtensions: 0,
NumServices: 2,
},
+5
View File
@@ -28,6 +28,7 @@ service DesktopService {
rpc InstallUpdate(InstallUpdateRequest) returns (InstallUpdateResponse) {}
rpc GetSecuritySettings(google.protobuf.Empty) returns (SecuritySettings) {}
rpc SetInsecureModeEnabled(SetInsecureModeEnabledRequest) returns (google.protobuf.Empty) {}
rpc SetLocale(SetLocaleRequest) returns (google.protobuf.Empty) {}
}
service ApplicationService {
@@ -185,6 +186,10 @@ message SetInsecureModeEnabledRequest {
bool enabled = 1;
}
message SetLocaleRequest {
string locale = 1;
}
message InstallUpdateRequest {
string installer_path = 1;
}
@@ -37,6 +37,7 @@ const (
DesktopService_InstallUpdate_FullMethodName = "/desktop.DesktopService/InstallUpdate"
DesktopService_GetSecuritySettings_FullMethodName = "/desktop.DesktopService/GetSecuritySettings"
DesktopService_SetInsecureModeEnabled_FullMethodName = "/desktop.DesktopService/SetInsecureModeEnabled"
DesktopService_SetLocale_FullMethodName = "/desktop.DesktopService/SetLocale"
)
// DesktopServiceClient is the client API for DesktopService service.
@@ -64,6 +65,7 @@ type DesktopServiceClient interface {
InstallUpdate(ctx context.Context, in *InstallUpdateRequest, opts ...grpc.CallOption) (*InstallUpdateResponse, error)
GetSecuritySettings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SecuritySettings, error)
SetInsecureModeEnabled(ctx context.Context, in *SetInsecureModeEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SetLocale(ctx context.Context, in *SetLocaleRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type desktopServiceClient struct {
@@ -284,6 +286,16 @@ func (c *desktopServiceClient) SetInsecureModeEnabled(ctx context.Context, in *S
return out, nil
}
func (c *desktopServiceClient) SetLocale(ctx context.Context, in *SetLocaleRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, DesktopService_SetLocale_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// DesktopServiceServer is the server API for DesktopService service.
// All implementations must embed UnimplementedDesktopServiceServer
// for forward compatibility.
@@ -309,6 +321,7 @@ type DesktopServiceServer interface {
InstallUpdate(context.Context, *InstallUpdateRequest) (*InstallUpdateResponse, error)
GetSecuritySettings(context.Context, *emptypb.Empty) (*SecuritySettings, error)
SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error)
SetLocale(context.Context, *SetLocaleRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedDesktopServiceServer()
}
@@ -402,6 +415,10 @@ func (UnimplementedDesktopServiceServer) GetSecuritySettings(context.Context, *e
func (UnimplementedDesktopServiceServer) SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetInsecureModeEnabled not implemented")
}
func (UnimplementedDesktopServiceServer) SetLocale(context.Context, *SetLocaleRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetLocale not implemented")
}
func (UnimplementedDesktopServiceServer) mustEmbedUnimplementedDesktopServiceServer() {}
func (UnimplementedDesktopServiceServer) testEmbeddedByValue() {}
@@ -801,6 +818,24 @@ func _DesktopService_SetInsecureModeEnabled_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler)
}
func _DesktopService_SetLocale_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetLocaleRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DesktopServiceServer).SetLocale(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DesktopService_SetLocale_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DesktopServiceServer).SetLocale(ctx, req.(*SetLocaleRequest))
}
return interceptor(ctx, in, info, handler)
}
// DesktopService_ServiceDesc is the grpc.ServiceDesc for DesktopService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -892,6 +927,10 @@ var DesktopService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetInsecureModeEnabled",
Handler: _DesktopService_SetInsecureModeEnabled_Handler,
},
{
MethodName: "SetLocale",
Handler: _DesktopService_SetLocale_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "experimental/boxdd/desktop_service.proto",
-22
View File
@@ -1,22 +0,0 @@
package main
import (
"path/filepath"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/tailscale/atomicfile"
)
const securitySettingsFileName = "security.json"
type securitySettings struct {
InsecureModeEnabled bool `json:"insecure_mode_enabled"`
}
func saveSecuritySettings(directory string, settings securitySettings) error {
content, err := json.Marshal(settings)
if err != nil {
return err
}
return atomicfile.WriteFile(filepath.Join(directory, securitySettingsFileName), content, 0o600)
}
+1 -16
View File
@@ -4,11 +4,8 @@ package main
import (
"context"
"os"
"path/filepath"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
)
@@ -26,20 +23,8 @@ func insecureModePlatformName() string {
return "Linux"
}
func loadSecuritySettings(directory string) (securitySettings, error) {
content, err := os.ReadFile(filepath.Join(directory, securitySettingsFileName))
if err != nil {
return securitySettings{}, err
}
settings, err := json.UnmarshalExtended[securitySettings](content)
if err != nil {
return securitySettings{}, err
}
return settings, nil
}
func (d *Daemon) insecureModeEnabled() bool {
settings, err := loadSecuritySettings(workingDirectory)
settings, err := loadDaemonSettings(workingDirectory)
if err != nil {
return false
}
+1 -16
View File
@@ -4,11 +4,8 @@ package main
import (
"context"
"os"
"path/filepath"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
)
@@ -26,20 +23,8 @@ func insecureModePlatformName() string {
return "Windows"
}
func loadSecuritySettings(directory string) (securitySettings, error) {
content, err := os.ReadFile(filepath.Join(directory, securitySettingsFileName))
if err != nil {
return securitySettings{}, err
}
settings, err := json.UnmarshalExtended[securitySettings](content)
if err != nil {
return securitySettings{}, err
}
return settings, nil
}
func (d *Daemon) insecureModeEnabled() bool {
settings, err := loadSecuritySettings(workingDirectory)
settings, err := loadDaemonSettings(workingDirectory)
if err != nil {
return false
}
+6 -2
View File
@@ -137,11 +137,15 @@ func (p *linuxPlatformInterface) UsePlatformWIFIMonitor() bool {
}
func (p *linuxPlatformInterface) UsePlatformNotification() bool {
return false
return true
}
func (p *linuxPlatformInterface) SendNotification(notification *adapter.Notification) error {
return nil
return p.daemon.startedService.SendNotification(notification)
}
func (p *linuxPlatformInterface) CancelNotification(identifier string, typeID int32) error {
return p.daemon.startedService.CancelNotification(identifier, typeID)
}
func (p *linuxPlatformInterface) MyInterfaceAddress() []netip.Addr {
+6 -2
View File
@@ -152,11 +152,15 @@ func (p *windowsPlatformInterface) UsePlatformWIFIMonitor() bool {
}
func (p *windowsPlatformInterface) UsePlatformNotification() bool {
return false
return true
}
func (p *windowsPlatformInterface) SendNotification(notification *adapter.Notification) error {
return nil
return p.daemon.startedService.SendNotification(notification)
}
func (p *windowsPlatformInterface) CancelNotification(identifier string, typeID int32) error {
return p.daemon.startedService.CancelNotification(identifier, typeID)
}
func (p *windowsPlatformInterface) MyInterfaceAddress() []netip.Addr {
+1
View File
@@ -40,6 +40,7 @@ type Daemon struct {
}
func newDaemon() (*Daemon, error) {
restoreLocale()
ctx := include.Context(context.Background())
d := &Daemon{
ctx: ctx,
+62
View File
@@ -0,0 +1,62 @@
package main
import (
"os"
"path/filepath"
"sync"
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/tailscale/atomicfile"
)
const (
settingsFileName = "settings.json"
legacySecuritySettingsFileName = "security.json"
)
type daemonSettings struct {
InsecureModeEnabled bool `json:"insecure_mode_enabled"`
Locale string `json:"locale,omitempty"`
}
var settingsAccess sync.Mutex
func loadDaemonSettings(directory string) (daemonSettings, error) {
content, err := os.ReadFile(filepath.Join(directory, settingsFileName))
if os.IsNotExist(err) {
content, err = os.ReadFile(filepath.Join(directory, legacySecuritySettingsFileName))
}
if err != nil {
return daemonSettings{}, err
}
return json.UnmarshalExtended[daemonSettings](content)
}
func updateDaemonSettings(directory string, modify func(settings *daemonSettings)) error {
settingsAccess.Lock()
defer settingsAccess.Unlock()
settings, err := loadDaemonSettings(directory)
if err != nil && !os.IsNotExist(err) {
return err
}
modify(&settings)
content, err := json.Marshal(settings)
if err != nil {
return err
}
err = atomicfile.WriteFile(filepath.Join(directory, settingsFileName), content, 0o600)
if err != nil {
return err
}
_ = os.Remove(filepath.Join(directory, legacySecuritySettingsFileName))
return nil
}
func restoreLocale() {
settings, err := loadDaemonSettings(workingDirectory)
if err != nil {
return
}
locale.Set(settings.Locale)
}
+253 -8
View File
@@ -108,6 +108,7 @@ const (
commandClientDialAttempts = 10
commandClientDialBaseDelay = 100 * time.Millisecond
commandClientDialStepDelay = 50 * time.Millisecond
commandClientProbeTimeout = 2 * time.Second
)
func commandClientDialDelay(attempt int) time.Duration {
@@ -167,14 +168,33 @@ func (c *CommandClient) establishConnection() (*grpc.ClientConn, daemon.StartedS
return c.dialRemote()
}
target, contextDialer := dialTarget()
return c.dialWithRetry(target, localDialOptions(contextDialer), true)
return c.dialWithRetry(target, localDialOptions(contextDialer), !c.standalone)
}
// 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.
// dialWithRetry connects to the local command server. For a handler-bound
// client the retry loop waits 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. A standalone client issues a single fail-fast
// probe instead: it serves a query from a UI that does not own the service
// lifecycle, and a server that is not running is reported immediately.
func (c *CommandClient) dialWithRetry(target string, dialOptions []grpc.DialOption, retryDial bool) (*grpc.ClientConn, daemon.StartedServiceClient, error) {
if !retryDial {
connection, err := grpc.NewClient(target, dialOptions...)
if err != nil {
return nil, nil, E.Cause(err, "create command client")
}
client := daemon.NewStartedServiceClient(connection)
ctx, cancel := context.WithTimeout(context.Background(), commandClientProbeTimeout)
_, err = client.GetStartedAt(ctx, &emptypb.Empty{}, grpc.WaitForReady(false))
cancel()
if err != nil {
connection.Close()
return nil, nil, E.Cause(err, "probe command server")
}
return connection, client, nil
}
var connection *grpc.ClientConn
var client daemon.StartedServiceClient
var lastError error
@@ -185,9 +205,6 @@ func (c *CommandClient) dialWithRetry(target string, dialOptions []grpc.DialOpti
connection, err = grpc.NewClient(target, dialOptions...)
if err != nil {
lastError = err
if !retryDial {
return nil, nil, E.Cause(err, "create command client")
}
time.Sleep(commandClientDialDelay(attempt))
continue
}
@@ -1304,3 +1321,231 @@ func (c *CommandClient) ProvideUSBDevices(handler USBProviderHandler) (*USBProvi
return session, nil
}
func (c *CommandClient) SubscribeTaildropInbox(endpointTag string, handler TaildropInboxHandler) (*TaildropInboxSubscription, error) {
session := new(TaildropInboxSubscription)
err := subscribeStatus(c, &session.streamSession, "taildrop inbox", func(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.TaildropInbox], error) {
return client.SubscribeTaildropInbox(ctx, &daemon.SubscribeTaildropInboxRequest{EndpointTag: endpointTag})
}, func(update *daemon.TaildropInbox) {
handler.OnInboxUpdate(taildropInboxFromGRPC(update))
}, handler.OnError)
if err != nil {
return nil, err
}
return session, nil
}
func (c *CommandClient) SendTaildropFiles(options *TaildropSendOptions, handler TaildropSendHandler) (*TaildropSendSession, error) {
client, parentCtx, err := c.getClientForCall()
if err != nil {
return nil, E.Cause(err, "send taildrop files")
}
streamCtx, cancel := context.WithCancel(parentCtx)
failStart := func(cause error, message string) (*TaildropSendSession, error) {
cancel()
if c.standalone {
c.closeConnection()
}
return nil, E.Cause(cause, message)
}
stream, err := client.SendTaildropFiles(streamCtx)
if err != nil {
return failStart(err, "send taildrop files")
}
sendErr := stream.Send(&daemon.TaildropSendClientMessage{
Message: &daemon.TaildropSendClientMessage_Start{Start: &daemon.TaildropSendStart{
EndpointTag: options.EndpointTag,
PeerStableID: options.PeerStableID,
Files: options.files,
}},
})
if sendErr != nil {
return failStart(sendErr, "send taildrop start")
}
session := &TaildropSendSession{
streamSession: streamSession{
ctx: streamCtx,
cancel: cancel,
closeDone: make(chan struct{}),
},
stream: stream,
}
standalone := c.standalone
go func() {
defer func() {
close(session.closeDone)
if standalone {
c.closeConnection()
}
}()
for {
message, recvErr := stream.Recv()
if recvErr != nil {
switch {
case recvErr == io.EOF:
handler.OnFinish("")
case streamCtx.Err() == nil:
handler.OnFinish(E.Cause(recvErr, "taildrop send").Error())
}
cancel()
return
}
progress := message.GetProgress()
if progress == nil {
continue
}
if progress.FileCompleted {
handler.OnFileCompleted(progress.FileIndex, progress.SentBytes)
} else {
handler.OnProgress(progress.FileIndex, progress.SentBytes)
}
}
}()
return session, nil
}
func (c *CommandClient) DownloadTaildropFile(endpointTag string, name string, destinationPath string, handler TaildropDownloadHandler) (*TaildropDownloadSession, error) {
client, parentCtx, err := c.getClientForCall()
if err != nil {
return nil, E.Cause(err, "download taildrop file")
}
streamCtx, cancel := context.WithCancel(parentCtx)
failStart := func(cause error, message string) (*TaildropDownloadSession, error) {
cancel()
if c.standalone {
c.closeConnection()
}
return nil, E.Cause(cause, message)
}
stream, err := client.DownloadTaildropFile(streamCtx, &daemon.DownloadTaildropFileRequest{
EndpointTag: endpointTag,
Name: name,
})
if err != nil {
return failStart(err, "download taildrop file")
}
firstChunk, err := stream.Recv()
if err != nil {
return failStart(err, "download taildrop file")
}
destinationFile, err := os.Create(destinationPath)
if err != nil {
return failStart(err, "download taildrop file")
}
session := &TaildropDownloadSession{
streamSession: streamSession{
ctx: streamCtx,
cancel: cancel,
closeDone: make(chan struct{}),
},
}
standalone := c.standalone
go func() {
defer func() {
close(session.closeDone)
if standalone {
c.closeConnection()
}
}()
totalSize := firstChunk.Size
var (
downloaded int64
lastProgress time.Time
)
writeChunk := func(data []byte) error {
if len(data) == 0 {
return nil
}
_, writeErr := destinationFile.Write(data)
if writeErr != nil {
return writeErr
}
downloaded += int64(len(data))
now := time.Now()
if downloaded == totalSize || now.Sub(lastProgress) >= daemon.TaildropProgressMinInterval {
lastProgress = now
handler.OnProgress(downloaded, totalSize)
}
return nil
}
downloadErr := writeChunk(firstChunk.Data)
for downloadErr == nil {
var chunk *daemon.DownloadTaildropFileChunk
chunk, downloadErr = stream.Recv()
if downloadErr == io.EOF {
downloadErr = nil
break
}
if downloadErr != nil {
downloadErr = E.Cause(downloadErr, "download taildrop file")
break
}
downloadErr = writeChunk(chunk.Data)
}
if downloadErr == nil {
downloadErr = destinationFile.Close()
} else {
destinationFile.Close()
}
if downloadErr != nil {
os.Remove(destinationPath)
if streamCtx.Err() == nil {
handler.OnFinish(downloadErr.Error())
}
cancel()
return
}
handler.OnFinish("")
cancel()
}()
return session, nil
}
func (c *CommandClient) DeleteTaildropFile(endpointTag string, name string) error {
_, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.DeleteTaildropFile(ctx, &daemon.DeleteTaildropFileRequest{
EndpointTag: endpointTag,
Name: name,
})
})
if err != nil {
return E.Cause(err, "delete taildrop file")
}
return nil
}
func (c *CommandClient) CancelTaildropReceiving(endpointTag string, senderID string, name string) error {
_, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.CancelTaildropReceiving(ctx, &daemon.CancelTaildropReceivingRequest{
EndpointTag: endpointTag,
SenderID: senderID,
Name: name,
})
})
if err != nil {
return E.Cause(err, "cancel taildrop receiving")
}
return nil
}
func (c *CommandClient) MarkTaildropInboxRead(endpointTag string) error {
_, err := callWithResult(c, func(ctx context.Context, client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.MarkTaildropInboxRead(ctx, &daemon.MarkTaildropInboxReadRequest{
EndpointTag: endpointTag,
})
})
if err != nil {
return E.Cause(err, "mark taildrop inbox read")
}
return nil
}
@@ -0,0 +1,155 @@
package libbox
import (
"os"
"github.com/sagernet/sing-box/daemon"
)
const TaildropChunkSize = daemon.TaildropChunkSize
type TaildropInbox struct {
EndpointTag string
files []*TaildropFile
receiving []*TaildropReceivingFile
}
func (i *TaildropInbox) Files() TaildropFileIterator {
return newIterator(i.files)
}
func (i *TaildropInbox) Receiving() TaildropReceivingFileIterator {
return newIterator(i.receiving)
}
type TaildropFile struct {
Name string
Size int64
SenderName string
ModifiedAt int64
}
type TaildropFileIterator interface {
Next() *TaildropFile
HasNext() bool
}
type TaildropReceivingFile struct {
Name string
Size int64
ReceivedBytes int64
SenderID string
SenderName string
}
type TaildropReceivingFileIterator interface {
Next() *TaildropReceivingFile
HasNext() bool
}
type TaildropInboxHandler interface {
OnInboxUpdate(inbox *TaildropInbox)
OnError(message string)
}
type TaildropInboxSubscription struct {
streamSession
}
func taildropInboxFromGRPC(inbox *daemon.TaildropInbox) *TaildropInbox {
result := &TaildropInbox{EndpointTag: inbox.EndpointTag}
for _, file := range inbox.Files {
result.files = append(result.files, &TaildropFile{
Name: file.Name,
Size: file.Size,
SenderName: file.SenderName,
ModifiedAt: file.ModifiedAt,
})
}
for _, file := range inbox.Receiving {
result.receiving = append(result.receiving, &TaildropReceivingFile{
Name: file.Name,
Size: file.Size,
ReceivedBytes: file.ReceivedBytes,
SenderID: file.SenderID,
SenderName: file.SenderName,
})
}
return result
}
type TaildropSendOptions struct {
EndpointTag string
PeerStableID string
files []*daemon.TaildropOutgoingFile
}
func NewTaildropSendOptions() *TaildropSendOptions {
return &TaildropSendOptions{}
}
// AddFile queues a file whose content the caller writes through
// TaildropSendSession.WriteChunk in queue order, terminated by FinishFile.
// A negative size declares the size as unknown; a non-negative size is
// verified against the written byte count.
func (o *TaildropSendOptions) AddFile(name string, size int64) {
o.files = append(o.files, &daemon.TaildropOutgoingFile{
Name: name,
Size: size,
})
}
type TaildropDownloadHandler interface {
OnProgress(downloaded int64, total int64)
OnFinish(errorMessage string)
}
type TaildropDownloadSession struct {
streamSession
}
type TaildropSendHandler interface {
OnProgress(fileIndex int32, sentBytes int64)
OnFileCompleted(fileIndex int32, sentBytes int64)
OnFinish(errorMessage string)
}
type TaildropSendSession struct {
streamSession
stream daemon.StartedService_SendTaildropFilesClient
}
func (s *TaildropSendSession) WriteChunk(data []byte) error {
if s.ctx.Err() != nil {
return os.ErrClosed
}
err := s.stream.Send(&daemon.TaildropSendClientMessage{
Message: &daemon.TaildropSendClientMessage_Chunk{Chunk: &daemon.TaildropFileChunk{Data: data}},
})
if err != nil {
return s.finishSend()
}
return nil
}
func (s *TaildropSendSession) FinishFile() error {
if s.ctx.Err() != nil {
return os.ErrClosed
}
err := s.stream.Send(&daemon.TaildropSendClientMessage{
Message: &daemon.TaildropSendClientMessage_FileDone{FileDone: &daemon.TaildropFileDone{}},
})
if err != nil {
return s.finishSend()
}
return nil
}
// grpc-go reports the RPC status only through RecvMsg; a Send on a stream
// terminated by the server returns bare io.EOF. Wait for the receive
// goroutine to deliver the status through OnFinish, then return a sentinel
// the caller does not report.
func (s *TaildropSendSession) finishSend() error {
<-s.closeDone
return os.ErrClosed
}
+60 -50
View File
@@ -16,16 +16,20 @@ type TailscaleEndpointStatusIterator interface {
}
type TailscaleEndpointStatus struct {
EndpointTag string
BackendState string
StateText string
AuthURL string
NetworkName string
MagicDNSSuffix string
Self *TailscalePeer
ExitNode *TailscalePeer
KeyAuth bool
userGroups []*TailscaleUserGroup
EndpointTag string
BackendState string
StateText string
AuthURL string
NetworkName string
MagicDNSSuffix string
Self *TailscalePeer
ExitNode *TailscalePeer
KeyAuth bool
CanShareFiles bool
WaitingFileCount int32
ReceivingFileCount int32
UnreadFileCount int32
userGroups []*TailscaleUserGroup
}
func (s *TailscaleEndpointStatus) UserGroups() TailscaleUserGroupIterator {
@@ -55,22 +59,23 @@ type TailscalePeerIterator interface {
}
type TailscalePeer struct {
StableID string
HostName string
DNSName string
OS string
tailscaleIPs []string
sshHostKeys []string
Online bool
ExitNode bool
ExitNodeOption bool
ShareeNode bool
Expired bool
Active bool
RxBytes int64
TxBytes int64
KeyExpiry int64
LastSeen int64
StableID string
HostName string
DNSName string
OS string
tailscaleIPs []string
sshHostKeys []string
Online bool
ExitNode bool
ExitNodeOption bool
ShareeNode bool
Expired bool
Active bool
CanReceiveFiles bool
RxBytes int64
TxBytes int64
KeyExpiry int64
LastSeen int64
}
func (p *TailscalePeer) TailscaleIPs() StringIterator {
@@ -104,14 +109,18 @@ func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *Ta
userGroups[i] = tailscaleUserGroupFromGRPC(group)
}
result := &TailscaleEndpointStatus{
EndpointTag: status.EndpointTag,
BackendState: status.BackendState,
StateText: status.StateText,
AuthURL: status.AuthURL,
NetworkName: status.NetworkName,
MagicDNSSuffix: status.MagicDNSSuffix,
KeyAuth: status.GetKeyAuth(),
userGroups: userGroups,
EndpointTag: status.EndpointTag,
BackendState: status.BackendState,
StateText: status.StateText,
AuthURL: status.AuthURL,
NetworkName: status.NetworkName,
MagicDNSSuffix: status.MagicDNSSuffix,
KeyAuth: status.GetKeyAuth(),
CanShareFiles: status.CanShareFiles,
WaitingFileCount: status.WaitingFileCount,
ReceivingFileCount: status.ReceivingFileCount,
UnreadFileCount: status.UnreadFileCount,
userGroups: userGroups,
}
if status.Self != nil {
result.Self = tailscalePeerFromGRPC(status.Self)
@@ -138,21 +147,22 @@ func tailscaleUserGroupFromGRPC(group *daemon.TailscaleUserGroup) *TailscaleUser
func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer {
return &TailscalePeer{
StableID: peer.StableID,
HostName: peer.HostName,
DNSName: peer.DnsName,
OS: peer.Os,
tailscaleIPs: peer.TailscaleIPs,
sshHostKeys: peer.SshHostKeys,
Online: peer.Online,
ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption,
ShareeNode: peer.ShareeNode,
Expired: peer.Expired,
Active: peer.Active,
RxBytes: peer.RxBytes,
TxBytes: peer.TxBytes,
KeyExpiry: peer.KeyExpiry,
LastSeen: peer.LastSeen,
StableID: peer.StableID,
HostName: peer.HostName,
DNSName: peer.DnsName,
OS: peer.Os,
tailscaleIPs: peer.TailscaleIPs,
sshHostKeys: peer.SshHostKeys,
Online: peer.Online,
ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption,
ShareeNode: peer.ShareeNode,
Expired: peer.Expired,
Active: peer.Active,
CanReceiveFiles: peer.CanReceiveFiles,
RxBytes: peer.RxBytes,
TxBytes: peer.TxBytes,
KeyExpiry: peer.KeyExpiry,
LastSeen: peer.LastSeen,
}
}
@@ -6,6 +6,7 @@ type TailscalePingResult struct {
LatencyMs float64
IsDirect bool
Endpoint string
PeerRelay string
DERPRegionID int32
DERPRegionCode string
Error string
@@ -25,6 +26,7 @@ func tailscalePingResultFromGRPC(response *daemon.TailscalePingResponse) *Tailsc
LatencyMs: response.LatencyMs,
IsDirect: response.IsDirect,
Endpoint: response.Endpoint,
PeerRelay: response.PeerRelay,
DERPRegionID: response.DerpRegionID,
DERPRegionCode: response.DerpRegionCode,
Error: response.Error,
+4
View File
@@ -147,6 +147,10 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
return nil
}
func (s *platformInterfaceStub) CancelNotification(identifier string, typeID int32) error {
return nil
}
func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr {
return nil
}
+1
View File
@@ -17,6 +17,7 @@ type PlatformInterface interface {
ReadWIFIState() *WIFIState
ClearDNSCache()
SendNotification(notification *Notification) error
CancelNotification(identifier string, typeID int32) error
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
RegisterMyInterface(name string)
+4
View File
@@ -239,6 +239,10 @@ func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notifi
return w.iif.SendNotification((*Notification)(notification))
}
func (w *platformInterfaceWrapper) CancelNotification(identifier string, typeID int32) error {
return w.iif.CancelNotification(identifier, typeID)
}
func (w *platformInterfaceWrapper) UsePlatformNeighborResolver() bool {
return true
}
+6
View File
@@ -39,6 +39,9 @@ type Locale struct {
TailscaleStopped string
TailscaleStarting string
TailscaleRunning string
TaildropReceiving string
TaildropReceived string
TaildropSendCanceled string
VPNConnecting string
VPNAuthentication string
VPNConnected string
@@ -59,6 +62,9 @@ var defaultLocale = &Locale{
TailscaleStopped: "Stopped",
TailscaleStarting: "Starting",
TailscaleRunning: "Running",
TaildropReceiving: "Receiving %s from %s",
TaildropReceived: "%s received from %s",
TaildropSendCanceled: "Sending %s canceled by receiver",
VPNConnecting: "Connecting",
VPNAuthentication: "Authentication required",
VPNConnected: "Connected",
+3
View File
@@ -14,6 +14,9 @@ func init() {
TailscaleStopped: "متوقف\u200cشده",
TailscaleStarting: "در حال شروع",
TailscaleRunning: "در حال اجرا",
TaildropReceiving: "در حال دریافت %[1]s از %[2]s",
TaildropReceived: "%[1]s از %[2]s دریافت شد",
TaildropSendCanceled: "ارسال %s توسط گیرنده لغو شد",
VPNConnecting: "در حال اتصال",
VPNAuthentication: "نیاز به احراز هویت",
VPNConnected: "متصل",
+3
View File
@@ -14,6 +14,9 @@ func init() {
TailscaleStopped: "Остановлено",
TailscaleStarting: "Запуск",
TailscaleRunning: "Работает",
TaildropReceiving: "Получение %[1]s от %[2]s",
TaildropReceived: "%[1]s получен от %[2]s",
TaildropSendCanceled: "Отправка %s отменена получателем",
VPNConnecting: "Подключение",
VPNAuthentication: "Требуется аутентификация",
VPNConnected: "Подключено",
+3
View File
@@ -16,6 +16,9 @@ func init() {
TailscaleStopped: "已停止",
TailscaleStarting: "启动中",
TailscaleRunning: "运行中",
TaildropReceiving: "正在接收来自 %[2]s 的 %[1]s",
TaildropReceived: "已收到来自 %[2]s 的 %[1]s",
TaildropSendCanceled: "发送 %s 已被接收方取消",
VPNConnecting: "正在连接",
VPNAuthentication: "需要认证",
VPNConnected: "已连接",
+3
View File
@@ -14,6 +14,9 @@ func init() {
TailscaleStopped: "已停止",
TailscaleStarting: "啟動中",
TailscaleRunning: "執行中",
TaildropReceiving: "正在接收來自 %[2]s 的 %[1]s",
TaildropReceived: "已收到來自 %[2]s 的 %[1]s",
TaildropSendCanceled: "傳送 %s 已被接收方取消",
VPNConnecting: "正在連線",
VPNAuthentication: "需要認證",
VPNConnected: "已連線",
+1 -1
View File
@@ -59,7 +59,7 @@ require (
github.com/sagernet/sing-usbip v0.0.0-20260813125128-908a3a2fa917
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.2
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.3
github.com/sagernet/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.10.2
+2 -2
View File
@@ -348,8 +348,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV
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=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.2 h1:JXYUrqxvIYGVVvTz0xzYR6DufhLBfnV5wh5lwhbdJVM=
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.2/go.mod h1:WLUSOPmTcf7VN9gLCe01qUSIvD+/cKC177neENyZPkI=
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.3 h1:c7jWyEt7n3WdbDxkc7EqmPQtPT+Hct2l1CtmEDztMuo=
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.3/go.mod h1:WLUSOPmTcf7VN9gLCe01qUSIvD+/cKC177neENyZPkI=
github.com/sagernet/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70 h1:WTVgbkDDGnZqxIUnBsE9QyJz2dtSaeoh9mFwxmQqfgI=
github.com/sagernet/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
+1
View File
@@ -33,6 +33,7 @@ type TailscaleEndpointOptions struct {
SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
SSHServer *TailscaleSSHServerOptions `json:"ssh_server,omitempty"`
TaildropDirectory string `json:"taildrop_directory,omitempty"`
}
type _TailscaleSSHServerOptions struct {
+28 -1
View File
@@ -47,6 +47,7 @@ import (
tailscaleroot "github.com/sagernet/tailscale"
_ "github.com/sagernet/tailscale/feature/relayserver"
"github.com/sagernet/tailscale/ipn"
"github.com/sagernet/tailscale/ipn/ipnlocal"
tsDNS "github.com/sagernet/tailscale/net/dns"
"github.com/sagernet/tailscale/net/netmon"
"github.com/sagernet/tailscale/net/netns"
@@ -117,6 +118,8 @@ type Endpoint struct {
sshServerInstance *tailssh.Server
sshServerOptions *option.TailscaleSSHServerOptions
taildrop *taildropManager
localBackend *ipnlocal.LocalBackend
systemInterface bool
systemInterfaceName string
@@ -181,6 +184,12 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
}
dialerQueryOptions := outboundDialer.(dialer.ResolveDialer).QueryOptions()
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
taildropDirectory := options.TaildropDirectory
if taildropDirectory == "" {
taildropDirectory = "Taildrop"
}
taildropDirectory = filemanager.BasePath(ctx, os.ExpandEnv(taildropDirectory))
taildropDirectory, _ = filepath.Abs(taildropDirectory)
return &Endpoint{
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
ctx: ctx,
@@ -231,6 +240,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
relayServerPort: options.RelayServerPort,
relayServerStaticEndpoints: options.RelayServerStaticEndpoints,
sshServerOptions: options.SSHServer,
taildrop: newTaildropManager(ctx, logger, tag, taildropDirectory, platformInterface),
udpTimeout: udpTimeout,
icmpTimeout: C.ICMPTimeout,
systemInterface: options.SystemInterface,
@@ -247,6 +257,12 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
if mkdirErr != nil {
return E.Cause(mkdirErr, "create state directory")
}
if !version.IsAppleTV() {
mkdirErr = filemanager.MkdirAll(t.ctx, t.taildrop.directory, 0o700)
if mkdirErr != nil {
return E.Cause(mkdirErr, "create taildrop directory")
}
}
t.server.PeerDNSQueryHandler = (*peerDNSQueryHandler)(t)
case adapter.StartStateStart:
return t.start()
@@ -400,7 +416,13 @@ func (t *Endpoint) postStart() error {
}, true
})
}
wgEngine := t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine)
localBackend := t.server.ExportLocalBackend()
t.localBackend = localBackend
if !version.IsAppleTV() {
registerTaildropEndpoint(localBackend, t)
go t.taildrop.start()
}
wgEngine := localBackend.ExportEngine().(wgengine.ExportedUserspaceEngine)
wgEngine.SetOnReconfigListener(t.onReconfig)
t.wgEngine = wgEngine
@@ -689,6 +711,11 @@ func (t *Endpoint) Logout(ctx context.Context) error {
func (t *Endpoint) Close() error {
var err error
t.started.Store(false)
if t.localBackend != nil {
unregisterTaildropEndpoint(t.localBackend)
t.localBackend = nil
}
t.taildrop.close()
if t.icmpForwarder != nil {
t.icmpForwarder.Close()
t.icmpForwarder = nil
+1
View File
@@ -48,6 +48,7 @@ func convertPingResult(result *ipnstate.PingResult) *adapter.TailscalePingResult
LatencyMs: result.LatencySeconds * 1000,
IsDirect: result.Endpoint != "",
Endpoint: result.Endpoint,
PeerRelay: result.PeerRelay,
DERPRegionID: int32(result.DERPRegionID),
DERPRegionCode: result.DERPRegionCode,
Error: result.Err,
+9
View File
@@ -0,0 +1,9 @@
//go:build tvos
package tailscale
import "github.com/sagernet/tailscale/version"
func init() {
version.SetAppleTV()
}
+27
View File
@@ -37,9 +37,36 @@ func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapte
status := localBackend.Status()
result := convertTailscaleStatus(status)
result.KeyAuth = t.keyAuth
canShareFiles, taildropTargets := t.taildropTargets()
result.CanShareFiles = canShareFiles
result.WaitingFileCount = t.taildrop.waitingFileCount()
result.ReceivingFileCount = t.taildrop.receivingFileCount()
result.UnreadFileCount = t.taildrop.unreadFileCount()
if len(taildropTargets) > 0 {
for _, group := range result.UserGroups {
for _, peer := range group.Peers {
peer.CanReceiveFiles = taildropTargets[peer.StableID]
}
}
}
fn(result)
}
}()
fileSignal := make(chan struct{}, 1)
watchErr := t.taildrop.watch(t.taildrop.fileWatchers, fileSignal)
if watchErr == nil {
defer t.taildrop.unwatch(t.taildrop.fileWatchers, fileSignal)
go func() {
for {
select {
case <-ctx.Done():
return
case <-fileSignal:
scheduleUpdate()
}
}
}()
}
scheduleUpdate()
for {
var busError string
File diff suppressed because it is too large Load Diff
+202
View File
@@ -0,0 +1,202 @@
//go:build with_gvisor
package tailscale
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/sagernet/sing-box/experimental/locale"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/tailscale/ipn"
"github.com/sagernet/tailscale/tailcfg"
)
func (t *Endpoint) SendTaildropFile(ctx context.Context, peerStableID string, fileName string, size int64, content io.Reader, progress func(sentBytes int64)) error {
if !t.started.Load() {
return E.New("Tailscale is not ready yet")
}
err := validateTaildropFileName(fileName)
if err != nil {
return err
}
localBackend := t.server.ExportLocalBackend()
if localBackend.State() != ipn.Running {
return E.New("taildrop: not connected to the tailnet")
}
nodeBackend := localBackend.NodeBackend()
self := nodeBackend.Self()
if !self.Valid() {
return E.New("taildrop: not connected to the tailnet")
}
if !self.CapMap().Contains(tailcfg.CapabilityFileSharing) {
return E.New("taildrop: file sharing not enabled by Tailscale admin")
}
var peer tailcfg.NodeView
for _, candidate := range nodeBackend.Peers() {
if string(candidate.StableID()) == peerStableID {
peer = candidate
break
}
}
if !peer.Valid() {
return E.New("taildrop: peer not found: ", peerStableID)
}
if peer.Hostinfo().OS() == "tvOS" {
return E.New("taildrop: peer cannot receive files")
}
if self.User() != peer.User() && !nodeBackend.PeerHasCap(peer, tailcfg.PeerCapabilityFileSharingTarget) {
return E.New("taildrop: peer is not a permitted file target")
}
peerAPIBase := nodeBackend.PeerAPIBase(peer)
if peerAPIBase == "" {
return E.New("taildrop: peer does not support peer API")
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return t.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(address))
},
},
}
defer httpClient.CloseIdleConnections()
fileURL := peerAPIBase + "/v0/put/" + url.PathEscape(fileName)
offset, remaining := taildropResume(ctx, httpClient, fileURL, content)
request, err := http.NewRequestWithContext(ctx, http.MethodPut, fileURL, &taildropProgressReader{
reader: remaining,
sent: offset,
progress: progress,
})
if err != nil {
return err
}
if size >= 0 {
request.ContentLength = size - offset
}
if offset > 0 {
request.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-")
}
if progress != nil {
progress(offset)
}
response, err := httpClient.Do(request)
if err != nil {
return E.Cause(err, "taildrop: send ", fileName)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
messageBytes, _ := io.ReadAll(io.LimitReader(response.Body, 1024))
message := strings.TrimSpace(string(messageBytes))
if message == errTaildropCanceled.Error() {
return E.New(fmt.Sprintf(locale.Current().TaildropSendCanceled, fileName))
}
return E.New("taildrop: send ", fileName, ": peer responded ", response.Status, ": ", message)
}
_, err = io.Copy(io.Discard, response.Body)
if err != nil {
return err
}
return nil
}
func taildropResume(ctx context.Context, httpClient *http.Client, fileURL string, content io.Reader) (int64, io.Reader) {
probeCtx, cancelProbe := context.WithTimeout(ctx, 10*time.Second)
defer cancelProbe()
request, err := http.NewRequestWithContext(probeCtx, http.MethodGet, fileURL, nil)
if err != nil {
return 0, content
}
response, err := httpClient.Do(request)
if err != nil {
return 0, content
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return 0, content
}
decoder := json.NewDecoder(response.Body)
var offset int64
block := make([]byte, 0, taildropBlockSize)
for {
var remoteChecksum taildropBlockChecksum
err = decoder.Decode(&remoteChecksum)
if err != nil || remoteChecksum.Algorithm != "sha256" || remoteChecksum.Size < 0 || remoteChecksum.Size > taildropBlockSize {
break
}
var n int
n, err = io.ReadFull(content, block[:remoteChecksum.Size])
block = block[:n]
if n == 0 || (err != nil && err != io.EOF && err != io.ErrUnexpectedEOF) {
break
}
localSum := sha256.Sum256(block)
if hex.EncodeToString(localSum[:]) != remoteChecksum.Checksum {
break
}
offset += int64(n)
block = block[:0]
}
if len(block) > 0 {
return offset, io.MultiReader(bytes.NewReader(block), content)
}
return offset, content
}
type taildropProgressReader struct {
reader io.Reader
sent int64
progress func(sentBytes int64)
}
func (r *taildropProgressReader) Read(buffer []byte) (int, error) {
n, err := r.reader.Read(buffer)
if n > 0 {
r.sent += int64(n)
if r.progress != nil {
r.progress(r.sent)
}
}
return n, err
}
func (t *Endpoint) taildropTargets() (canShareFiles bool, targets map[string]bool) {
if !t.started.Load() {
return false, nil
}
localBackend := t.server.ExportLocalBackend()
if localBackend.State() != ipn.Running {
return false, nil
}
nodeBackend := localBackend.NodeBackend()
self := nodeBackend.Self()
if !self.Valid() || !self.CapMap().Contains(tailcfg.CapabilityFileSharing) {
return false, nil
}
targets = make(map[string]bool)
for _, peer := range nodeBackend.Peers() {
if !peer.Valid() || peer.Hostinfo().OS() == "tvOS" {
continue
}
if self.User() != peer.User() && !nodeBackend.PeerHasCap(peer, tailcfg.PeerCapabilityFileSharingTarget) {
continue
}
if !nodeBackend.PeerHasPeerAPI(peer) {
continue
}
targets[string(peer.StableID())] = true
}
return true, targets
}