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 UsePlatformNotification() bool
SendNotification(notification *Notification) error SendNotification(notification *Notification) error
CancelNotification(identifier string, typeID int32) error
MyInterfaceAddress() []netip.Addr MyInterfaceAddress() []netip.Addr
+61 -26
View File
@@ -1,32 +1,66 @@
package adapter package adapter
import "context" import (
"context"
"io"
)
type TailscaleEndpoint interface { type TailscaleEndpoint interface {
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
SetTailscaleExitNode(ctx context.Context, stableID string) error SetTailscaleExitNode(ctx context.Context, stableID string) error
Logout(ctx context.Context) 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 { type TailscalePingResult struct {
LatencyMs float64 LatencyMs float64
IsDirect bool IsDirect bool
Endpoint string Endpoint string
PeerRelay string
DERPRegionID int32 DERPRegionID int32
DERPRegionCode string DERPRegionCode string
Error string Error string
} }
type TailscaleEndpointStatus struct { type TailscaleEndpointStatus struct {
BackendState string BackendState string
AuthURL string AuthURL string
NetworkName string NetworkName string
MagicDNSSuffix string MagicDNSSuffix string
Self *TailscalePeer Self *TailscalePeer
ExitNode *TailscalePeer ExitNode *TailscalePeer
UserGroups []*TailscaleUserGroup UserGroups []*TailscaleUserGroup
KeyAuth bool KeyAuth bool
CanShareFiles bool
WaitingFileCount int32
ReceivingFileCount int32
UnreadFileCount int32
} }
type TailscaleUserGroup struct { type TailscaleUserGroup struct {
@@ -38,23 +72,24 @@ type TailscaleUserGroup struct {
} }
type TailscalePeer struct { type TailscalePeer struct {
StableID string StableID string
HostName string HostName string
DNSName string DNSName string
OS string OS string
TailscaleIPs []string TailscaleIPs []string
SSHHostKeys []string SSHHostKeys []string
Online bool Online bool
ExitNode bool ExitNode bool
ExitNodeOption bool ExitNodeOption bool
ShareeNode bool ShareeNode bool
Expired bool Expired bool
Active bool Active bool
RxBytes int64 CanReceiveFiles bool
TxBytes int64 RxBytes int64
UserID int64 TxBytes int64
KeyExpiry int64 UserID int64
LastSeen int64 KeyExpiry int64
LastSeen int64
} }
type ShellSession interface { type ShellSession interface {
+13 -3
View File
@@ -79,7 +79,10 @@ func runAPITailscalePing(selector string) error {
}() }()
timer := time.NewTimer(commandAPITailscalePingTimeout) timer := time.NewTimer(commandAPITailscalePingTimeout)
defer timer.Stop() defer timer.Stop()
var pongCount int var (
pongCount int
lastPeerRelay string
)
for pongCount < commandAPITailscalePingCount { for pongCount < commandAPITailscalePingCount {
var ( var (
response *daemon.TailscalePingResponse response *daemon.TailscalePingResponse
@@ -109,16 +112,23 @@ func runAPITailscalePing(selector string) error {
if response.GetEndpoint() != "" { if response.GetEndpoint() != "" {
return nil return nil
} }
lastPeerRelay = response.GetPeerRelay()
timer.Reset(commandAPITailscalePingTimeout) 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 return nil
} }
func formatTailscalePong(peerName string, peerAddress string, response *daemon.TailscalePingResponse) string { func formatTailscalePong(peerName string, peerAddress string, response *daemon.TailscalePingResponse) string {
via := response.GetEndpoint() via := response.GetEndpoint()
if via == "" { 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(), ")") via = F.ToString("DERP(", response.GetDerpRegionCode(), ")")
} else { } else {
via = F.ToString("DERP(", response.GetDerpRegionID(), ")") 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" "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) var _ StartedServiceServer = (*StartedService)(nil)
@@ -68,6 +71,8 @@ type StartedService struct {
urlTestObserver *observable.Observer[struct{}] urlTestObserver *observable.Observer[struct{}]
clashModeSubscriber *observable.Subscriber[struct{}] clashModeSubscriber *observable.Subscriber[struct{}]
clashModeObserver *observable.Observer[struct{}] clashModeObserver *observable.Observer[struct{}]
notificationSubscriber *observable.Subscriber[*NotificationEvent]
notificationObserver *observable.Observer[*NotificationEvent]
} }
type ServiceOptions struct { type ServiceOptions struct {
@@ -106,11 +111,13 @@ func NewStartedService(options ServiceOptions) *StartedService {
logSubscriber: observable.NewSubscriber[*log.Entry](128), logSubscriber: observable.NewSubscriber[*log.Entry](128),
urlTestSubscriber: observable.NewSubscriber[struct{}](1), urlTestSubscriber: observable.NewSubscriber[struct{}](1),
clashModeSubscriber: observable.NewSubscriber[struct{}](1), clashModeSubscriber: observable.NewSubscriber[struct{}](1),
notificationSubscriber: observable.NewSubscriber[*NotificationEvent](notificationQueueSize),
} }
s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2) s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2)
s.logObserver = observable.NewObserver(s.logSubscriber, 64) s.logObserver = observable.NewObserver(s.logSubscriber, 64)
s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1) s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1)
s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1) s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1)
s.notificationObserver = observable.NewObserver(s.notificationSubscriber, notificationQueueSize)
return s return s
} }
@@ -283,6 +290,7 @@ func (s *StartedService) Close() {
s.logSubscriber.Close() s.logSubscriber.Close()
s.urlTestSubscriber.Close() s.urlTestSubscriber.Close()
s.clashModeSubscriber.Close() s.clashModeSubscriber.Close()
s.notificationSubscriber.Close()
} }
func (s *StartedService) CloseService() error { func (s *StartedService) CloseService() error {
@@ -1625,14 +1633,18 @@ func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStat
} }
} }
result := &TailscaleEndpointStatus{ result := &TailscaleEndpointStatus{
EndpointTag: tag, EndpointTag: tag,
BackendState: s.BackendState, BackendState: s.BackendState,
StateText: selectedLocale.TailscaleStateText(s.BackendState), StateText: selectedLocale.TailscaleStateText(s.BackendState),
AuthURL: s.AuthURL, AuthURL: s.AuthURL,
NetworkName: s.NetworkName, NetworkName: s.NetworkName,
MagicDNSSuffix: s.MagicDNSSuffix, MagicDNSSuffix: s.MagicDNSSuffix,
UserGroups: userGroups, UserGroups: userGroups,
KeyAuth: s.KeyAuth, KeyAuth: s.KeyAuth,
CanShareFiles: s.CanShareFiles,
WaitingFileCount: s.WaitingFileCount,
ReceivingFileCount: s.ReceivingFileCount,
UnreadFileCount: s.UnreadFileCount,
} }
if s.Self != nil { if s.Self != nil {
result.Self = tailscalePeerToProto(s.Self) result.Self = tailscalePeerToProto(s.Self)
@@ -1645,22 +1657,23 @@ func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStat
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer { func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
return &TailscalePeer{ return &TailscalePeer{
StableID: peer.StableID, StableID: peer.StableID,
HostName: peer.HostName, HostName: peer.HostName,
DnsName: peer.DNSName, DnsName: peer.DNSName,
Os: peer.OS, Os: peer.OS,
TailscaleIPs: peer.TailscaleIPs, TailscaleIPs: peer.TailscaleIPs,
SshHostKeys: peer.SSHHostKeys, SshHostKeys: peer.SSHHostKeys,
Online: peer.Online, Online: peer.Online,
ExitNode: peer.ExitNode, ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption, ExitNodeOption: peer.ExitNodeOption,
ShareeNode: peer.ShareeNode, ShareeNode: peer.ShareeNode,
Expired: peer.Expired, Expired: peer.Expired,
Active: peer.Active, Active: peer.Active,
RxBytes: peer.RxBytes, CanReceiveFiles: peer.CanReceiveFiles,
TxBytes: peer.TxBytes, RxBytes: peer.RxBytes,
KeyExpiry: peer.KeyExpiry, TxBytes: peer.TxBytes,
LastSeen: peer.LastSeen, KeyExpiry: peer.KeyExpiry,
LastSeen: peer.LastSeen,
} }
} }
@@ -1676,35 +1689,9 @@ func (s *StartedService) StartTailscalePing(
boxService := s.instance boxService := s.instance
s.serviceAccess.RUnlock() s.serviceAccess.RUnlock()
var provider adapter.TailscaleEndpoint provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if request.EndpointTag != "" { if err != nil {
endpoint, err := resolveTailscaleEndpoint(boxService, request.EndpointTag) return err
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")
}
} }
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) { return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
@@ -1712,6 +1699,7 @@ func (s *StartedService) StartTailscalePing(
LatencyMs: result.LatencyMs, LatencyMs: result.LatencyMs,
IsDirect: result.IsDirect, IsDirect: result.IsDirect,
Endpoint: result.Endpoint, Endpoint: result.Endpoint,
PeerRelay: result.PeerRelay,
DerpRegionID: result.DERPRegionID, DerpRegionID: result.DERPRegionID,
DerpRegionCode: result.DERPRegionCode, DerpRegionCode: result.DERPRegionCode,
Error: result.Error, Error: result.Error,
@@ -2019,6 +2007,58 @@ func (s *StartedService) CancelOpenVPNChallenge(ctx context.Context, request *Op
return &emptypb.Empty{}, nil 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() { 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 SetTailscaleExitNode(SetTailscaleExitNodeRequest) returns (google.protobuf.Empty) {}
rpc TailscaleLogout(TailscaleLogoutRequest) returns (google.protobuf.Empty) {} rpc TailscaleLogout(TailscaleLogoutRequest) returns (google.protobuf.Empty) {}
rpc StartTailscaleSSHSession(stream TailscaleSSHClientMessage) returns (stream TailscaleSSHServerMessage) {} 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 ProvideUSBDevices(stream USBProviderMessage) returns (stream USBServerMessage) {}
rpc SubscribeUSBIPServerStatus(google.protobuf.Empty) returns (stream USBIPServerStatusUpdate) {} rpc SubscribeUSBIPServerStatus(google.protobuf.Empty) returns (stream USBIPServerStatusUpdate) {}
rpc SubscribeOpenConnectStatus(google.protobuf.Empty) returns (stream OpenConnectStatusUpdate) {} rpc SubscribeOpenConnectStatus(google.protobuf.Empty) returns (stream OpenConnectStatusUpdate) {}
@@ -44,6 +50,7 @@ service StartedService {
rpc SubscribeOpenVPNStatus(google.protobuf.Empty) returns (stream OpenVPNStatusUpdate) {} rpc SubscribeOpenVPNStatus(google.protobuf.Empty) returns (stream OpenVPNStatusUpdate) {}
rpc SubmitOpenVPNChallengeResponse(OpenVPNChallengeSubmission) returns (google.protobuf.Empty) {} rpc SubmitOpenVPNChallengeResponse(OpenVPNChallengeSubmission) returns (google.protobuf.Empty) {}
rpc CancelOpenVPNChallenge(OpenVPNChallengeCancel) returns (google.protobuf.Empty) {} rpc CancelOpenVPNChallenge(OpenVPNChallengeCancel) returns (google.protobuf.Empty) {}
rpc SubscribeNotifications(google.protobuf.Empty) returns (stream NotificationEvent) {}
} }
message Version { message Version {
@@ -282,6 +289,10 @@ message TailscaleEndpointStatus {
repeated TailscaleUserGroup userGroups = 8; repeated TailscaleUserGroup userGroups = 8;
TailscalePeer exitNode = 9; TailscalePeer exitNode = 9;
bool keyAuth = 10; bool keyAuth = 10;
bool canShareFiles = 11;
int32 waitingFileCount = 12;
int32 receivingFileCount = 13;
int32 unreadFileCount = 14;
} }
message TailscaleUserGroup { message TailscaleUserGroup {
@@ -309,6 +320,7 @@ message TailscalePeer {
repeated string sshHostKeys = 14; repeated string sshHostKeys = 14;
bool shareeNode = 15; bool shareeNode = 15;
int64 lastSeen = 16; int64 lastSeen = 16;
bool canReceiveFiles = 17;
} }
message TailscalePingRequest { message TailscalePingRequest {
@@ -323,6 +335,7 @@ message TailscalePingResponse {
int32 derpRegionID = 4; int32 derpRegionID = 4;
string derpRegionCode = 5; string derpRegionCode = 5;
string error = 6; string error = 6;
string peerRelay = 7;
} }
message SetTailscaleExitNodeRequest { message SetTailscaleExitNodeRequest {
@@ -397,6 +410,94 @@ message TailscaleSSHError {
string message = 1; 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 { message USBProviderMessage {
oneof message { oneof message {
USBDeviceAttach attach = 1; USBDeviceAttach attach = 1;
@@ -670,3 +771,25 @@ message OpenVPNChallengeCancel {
string endpointTag = 1; string endpointTag = 1;
string challengeID = 2; 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_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode"
StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout" StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout"
StartedService_StartTailscaleSSHSession_FullMethodName = "/daemon.StartedService/StartTailscaleSSHSession" 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_ProvideUSBDevices_FullMethodName = "/daemon.StartedService/ProvideUSBDevices"
StartedService_SubscribeUSBIPServerStatus_FullMethodName = "/daemon.StartedService/SubscribeUSBIPServerStatus" StartedService_SubscribeUSBIPServerStatus_FullMethodName = "/daemon.StartedService/SubscribeUSBIPServerStatus"
StartedService_SubscribeOpenConnectStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenConnectStatus" StartedService_SubscribeOpenConnectStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenConnectStatus"
@@ -49,6 +55,7 @@ const (
StartedService_SubscribeOpenVPNStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenVPNStatus" StartedService_SubscribeOpenVPNStatus_FullMethodName = "/daemon.StartedService/SubscribeOpenVPNStatus"
StartedService_SubmitOpenVPNChallengeResponse_FullMethodName = "/daemon.StartedService/SubmitOpenVPNChallengeResponse" StartedService_SubmitOpenVPNChallengeResponse_FullMethodName = "/daemon.StartedService/SubmitOpenVPNChallengeResponse"
StartedService_CancelOpenVPNChallenge_FullMethodName = "/daemon.StartedService/CancelOpenVPNChallenge" StartedService_CancelOpenVPNChallenge_FullMethodName = "/daemon.StartedService/CancelOpenVPNChallenge"
StartedService_SubscribeNotifications_FullMethodName = "/daemon.StartedService/SubscribeNotifications"
) )
// StartedServiceClient is the client API for StartedService service. // 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) SetTailscaleExitNode(ctx context.Context, in *SetTailscaleExitNodeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TailscaleLogout(ctx context.Context, in *TailscaleLogoutRequest, 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) 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) 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) 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) 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) 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) SubmitOpenVPNChallengeResponse(ctx context.Context, in *OpenVPNChallengeSubmission, opts ...grpc.CallOption) (*emptypb.Empty, error)
CancelOpenVPNChallenge(ctx context.Context, in *OpenVPNChallengeCancel, 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 { 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. // 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] 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) { func (c *startedServiceClient) ProvideUSBDevices(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[USBProviderMessage, USBServerMessage], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 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 { if err != nil {
return nil, err 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) { func (c *startedServiceClient) SubscribeUSBIPServerStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[USBIPServerStatusUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 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 { if err != nil {
return nil, err 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) { func (c *startedServiceClient) SubscribeOpenConnectStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenConnectStatusUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 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 { if err != nil {
return nil, err 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) { func (c *startedServiceClient) SubscribeOpenVPNStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OpenVPNStatusUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -571,6 +666,25 @@ func (c *startedServiceClient) CancelOpenVPNChallenge(ctx context.Context, in *O
return out, nil 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. // StartedServiceServer is the server API for StartedService service.
// All implementations must embed UnimplementedStartedServiceServer // All implementations must embed UnimplementedStartedServiceServer
// for forward compatibility. // for forward compatibility.
@@ -601,6 +715,12 @@ type StartedServiceServer interface {
SetTailscaleExitNode(context.Context, *SetTailscaleExitNodeRequest) (*emptypb.Empty, error) SetTailscaleExitNode(context.Context, *SetTailscaleExitNodeRequest) (*emptypb.Empty, error)
TailscaleLogout(context.Context, *TailscaleLogoutRequest) (*emptypb.Empty, error) TailscaleLogout(context.Context, *TailscaleLogoutRequest) (*emptypb.Empty, error)
StartTailscaleSSHSession(grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]) 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 ProvideUSBDevices(grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error
SubscribeUSBIPServerStatus(*emptypb.Empty, grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error SubscribeUSBIPServerStatus(*emptypb.Empty, grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error
SubscribeOpenConnectStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenConnectStatusUpdate]) error SubscribeOpenConnectStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenConnectStatusUpdate]) error
@@ -609,6 +729,7 @@ type StartedServiceServer interface {
SubscribeOpenVPNStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenVPNStatusUpdate]) error SubscribeOpenVPNStatus(*emptypb.Empty, grpc.ServerStreamingServer[OpenVPNStatusUpdate]) error
SubmitOpenVPNChallengeResponse(context.Context, *OpenVPNChallengeSubmission) (*emptypb.Empty, error) SubmitOpenVPNChallengeResponse(context.Context, *OpenVPNChallengeSubmission) (*emptypb.Empty, error)
CancelOpenVPNChallenge(context.Context, *OpenVPNChallengeCancel) (*emptypb.Empty, error) CancelOpenVPNChallenge(context.Context, *OpenVPNChallengeCancel) (*emptypb.Empty, error)
SubscribeNotifications(*emptypb.Empty, grpc.ServerStreamingServer[NotificationEvent]) error
mustEmbedUnimplementedStartedServiceServer() mustEmbedUnimplementedStartedServiceServer()
} }
@@ -723,6 +844,30 @@ func (UnimplementedStartedServiceServer) StartTailscaleSSHSession(grpc.BidiStrea
return status.Error(codes.Unimplemented, "method StartTailscaleSSHSession not implemented") 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 { func (UnimplementedStartedServiceServer) ProvideUSBDevices(grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error {
return status.Error(codes.Unimplemented, "method ProvideUSBDevices not implemented") 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) { func (UnimplementedStartedServiceServer) CancelOpenVPNChallenge(context.Context, *OpenVPNChallengeCancel) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CancelOpenVPNChallenge not implemented") 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) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} 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. // 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] 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 { func _StartedService_ProvideUSBDevices_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(StartedServiceServer).ProvideUSBDevices(&grpc.GenericServerStream[USBProviderMessage, USBServerMessage]{ServerStream: stream}) 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) 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. // StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -1330,6 +1573,18 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "TailscaleLogout", MethodName: "TailscaleLogout",
Handler: _StartedService_TailscaleLogout_Handler, Handler: _StartedService_TailscaleLogout_Handler,
}, },
{
MethodName: "MarkTaildropInboxRead",
Handler: _StartedService_MarkTaildropInboxRead_Handler,
},
{
MethodName: "DeleteTaildropFile",
Handler: _StartedService_DeleteTaildropFile_Handler,
},
{
MethodName: "CancelTaildropReceiving",
Handler: _StartedService_CancelTaildropReceiving_Handler,
},
{ {
MethodName: "SubmitOpenConnectAuthResponse", MethodName: "SubmitOpenConnectAuthResponse",
Handler: _StartedService_SubmitOpenConnectAuthResponse_Handler, Handler: _StartedService_SubmitOpenConnectAuthResponse_Handler,
@@ -1409,6 +1664,22 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true, ServerStreams: true,
ClientStreams: 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", StreamName: "ProvideUSBDevices",
Handler: _StartedService_ProvideUSBDevices_Handler, Handler: _StartedService_ProvideUSBDevices_Handler,
@@ -1430,6 +1701,11 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_SubscribeOpenVPNStatus_Handler, Handler: _StartedService_SubscribeOpenVPNStatus_Handler,
ServerStreams: true, ServerStreams: true,
}, },
{
StreamName: "SubscribeNotifications",
Handler: _StartedService_SubscribeNotifications_Handler,
ServerStreams: true,
},
}, },
Metadata: "daemon/started_service.proto", 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" !!! quote "Changes in sing-box 1.14.0"
:material-plus: [listen_port](#listen_port) :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" !!! quote "Changes in sing-box 1.13.0"
@@ -43,6 +44,7 @@ icon: material/new-box
"system_interface_mtu": 0, "system_interface_mtu": 0,
"udp_timeout": "5m", "udp_timeout": "5m",
"ssh_server": false, "ssh_server": false,
"taildrop_directory": "",
... // Dial Fields ... // Dial Fields
} }
@@ -211,6 +213,17 @@ Refuse the SFTP subsystem.
Refuse local and remote TCP and Unix-socket forwarding, including SSH agent forwarding. 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 ### Dial Fields
!!! note !!! note
+13 -1
View File
@@ -5,7 +5,8 @@ icon: material/new-box
!!! quote "sing-box 1.14.0 中的更改" !!! quote "sing-box 1.14.0 中的更改"
:material-plus: [listen_port](#listen_port) :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 中的更改" !!! quote "sing-box 1.13.0 中的更改"
@@ -43,6 +44,7 @@ icon: material/new-box
"system_interface_mtu": 0, "system_interface_mtu": 0,
"udp_timeout": "5m", "udp_timeout": "5m",
"ssh_server": false, "ssh_server": false,
"taildrop_directory": "",
... // 拨号字段 ... // 拨号字段
} }
@@ -210,6 +212,16 @@ UDP NAT 过期时间。
拒绝本地和远程的 TCP 与 Unix 套接字转发,包括 SSH agent 转发。 拒绝本地和远程的 TCP 与 Unix 套接字转发,包括 SSH agent 转发。
#### taildrop_directory
!!! question "自 sing-box 1.14.0 起"
存储从 tailnet 对等节点接收到的文件的目录。
相对路径基于工作目录解析,与 [state_directory](#state_directory) 相同。
默认使用 `Taildrop`
### 拨号字段 ### 拨号字段
!!! note !!! note
+3
View File
@@ -4588,6 +4588,9 @@
"additionalProperties": false "additionalProperties": false
} }
] ]
},
"taildrop_directory": {
"type": "string"
} }
}, },
"required": [ "required": [
+3 -1
View File
@@ -64,7 +64,9 @@ func serviceSetInsecureMode(value string) error {
if err != nil { if err != nil {
return E.Cause(err, "validate working directory") 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 { func validateProtectedLinuxDirectory(directory string) error {
+3 -1
View File
@@ -93,7 +93,9 @@ func serviceSetInsecureMode(value string) error {
if err != nil { if err != nil {
return E.Cause(err, "validate working directory") 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) { func installedServiceWorkingDirectory() (string, error) {
+21 -1
View File
@@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/tailscale/atomicfile" "github.com/sagernet/tailscale/atomicfile"
@@ -258,7 +259,9 @@ func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *Se
return nil, err return nil, err
} }
wasEnabled := s.daemon.insecureModeEnabled() wasEnabled := s.daemon.insecureModeEnabled()
err = saveSecuritySettings(workingDirectory, securitySettings{InsecureModeEnabled: false}) err = updateDaemonSettings(workingDirectory, func(settings *daemonSettings) {
settings.InsecureModeEnabled = false
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -276,6 +279,23 @@ func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *Se
return &emptypb.Empty{}, nil 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 { func (d *Daemon) cleanFailedStartLocked(ownerUserID string, options startOptions, startError error) error {
var platformError error var platformError error
if d.platform != nil { if d.platform != nil {
+116 -66
View File
@@ -1519,6 +1519,50 @@ func (x *SetInsecureModeEnabledRequest) GetEnabled() bool {
return false 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 { type InstallUpdateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
InstallerPath string `protobuf:"bytes,1,opt,name=installer_path,json=installerPath,proto3" json:"installer_path,omitempty"` 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() { func (x *InstallUpdateRequest) Reset() {
*x = InstallUpdateRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1540,7 +1584,7 @@ func (x *InstallUpdateRequest) String() string {
func (*InstallUpdateRequest) ProtoMessage() {} func (*InstallUpdateRequest) ProtoMessage() {}
func (x *InstallUpdateRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1553,7 +1597,7 @@ func (x *InstallUpdateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InstallUpdateRequest.ProtoReflect.Descriptor instead. // Deprecated: Use InstallUpdateRequest.ProtoReflect.Descriptor instead.
func (*InstallUpdateRequest) Descriptor() ([]byte, []int) { 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 { func (x *InstallUpdateRequest) GetInstallerPath() string {
@@ -1572,7 +1616,7 @@ type InstallUpdateResponse struct {
func (x *InstallUpdateResponse) Reset() { func (x *InstallUpdateResponse) Reset() {
*x = InstallUpdateResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1584,7 +1628,7 @@ func (x *InstallUpdateResponse) String() string {
func (*InstallUpdateResponse) ProtoMessage() {} func (*InstallUpdateResponse) ProtoMessage() {}
func (x *InstallUpdateResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1597,7 +1641,7 @@ func (x *InstallUpdateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InstallUpdateResponse.ProtoReflect.Descriptor instead. // Deprecated: Use InstallUpdateResponse.ProtoReflect.Descriptor instead.
func (*InstallUpdateResponse) Descriptor() ([]byte, []int) { 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 { 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" + "\tavailable\x18\x01 \x01(\bR\tavailable\x122\n" +
"\x15insecure_mode_enabled\x18\x02 \x01(\bR\x13insecureModeEnabled\"9\n" + "\x15insecure_mode_enabled\x18\x02 \x01(\bR\x13insecureModeEnabled\"9\n" +
"\x1dSetInsecureModeEnabledRequest\x12\x18\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" + "\x14InstallUpdateRequest\x12%\n" +
"\x0einstaller_path\x18\x01 \x01(\tR\rinstallerPath\"M\n" + "\x0einstaller_path\x18\x01 \x01(\tR\rinstallerPath\"M\n" +
"\x15InstallUpdateResponse\x124\n" + "\x15InstallUpdateResponse\x124\n" +
@@ -1720,7 +1766,7 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"!INSTALL_UPDATE_RESULT_UNSPECIFIED\x10\x00\x12!\n" + "!INSTALL_UPDATE_RESULT_UNSPECIFIED\x10\x00\x12!\n" +
"\x1dINSTALL_UPDATE_RESULT_STARTED\x10\x01\x12)\n" + "\x1dINSTALL_UPDATE_RESULT_STARTED\x10\x01\x12)\n" +
"%INSTALL_UPDATE_RESULT_SIGNER_MISMATCH\x10\x02\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" + "\x0eDesktopService\x12>\n" +
"\rGetDaemonInfo\x12\x16.google.protobuf.Empty\x1a\x13.desktop.DaemonInfo\"\x00\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" + "\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" + "\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" + "\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" + "\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" + "\x12ApplicationService\x12?\n" +
"\vCheckConfig\x12\x16.desktop.ConfigContent\x1a\x16.google.protobuf.Empty\"\x00\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" + "\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 ( var (
file_experimental_boxdd_desktop_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) 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{ file_experimental_boxdd_desktop_service_proto_goTypes = []any{
(DaemonOwnership)(0), // 0: desktop.DaemonOwnership (DaemonOwnership)(0), // 0: desktop.DaemonOwnership
(InstallUpdateResult)(0), // 1: desktop.InstallUpdateResult (InstallUpdateResult)(0), // 1: desktop.InstallUpdateResult
@@ -1797,11 +1844,12 @@ var (
(*OOMReportFile)(nil), // 25: desktop.OOMReportFile (*OOMReportFile)(nil), // 25: desktop.OOMReportFile
(*SecuritySettings)(nil), // 26: desktop.SecuritySettings (*SecuritySettings)(nil), // 26: desktop.SecuritySettings
(*SetInsecureModeEnabledRequest)(nil), // 27: desktop.SetInsecureModeEnabledRequest (*SetInsecureModeEnabledRequest)(nil), // 27: desktop.SetInsecureModeEnabledRequest
(*InstallUpdateRequest)(nil), // 28: desktop.InstallUpdateRequest (*SetLocaleRequest)(nil), // 28: desktop.SetLocaleRequest
(*InstallUpdateResponse)(nil), // 29: desktop.InstallUpdateResponse (*InstallUpdateRequest)(nil), // 29: desktop.InstallUpdateRequest
(*emptypb.Empty)(nil), // 30: google.protobuf.Empty (*InstallUpdateResponse)(nil), // 30: desktop.InstallUpdateResponse
(*daemon.NetworkQualityTestProgress)(nil), // 31: daemon.NetworkQualityTestProgress (*emptypb.Empty)(nil), // 31: google.protobuf.Empty
(*daemon.STUNTestProgress)(nil), // 32: daemon.STUNTestProgress (*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 21, // 5: desktop.OOMReportList.reports:type_name -> desktop.OOMReportEntry
25, // 6: desktop.OOMReportContent.files:type_name -> desktop.OOMReportFile 25, // 6: desktop.OOMReportContent.files:type_name -> desktop.OOMReportFile
1, // 7: desktop.InstallUpdateResponse.result:type_name -> desktop.InstallUpdateResult 1, // 7: desktop.InstallUpdateResponse.result:type_name -> desktop.InstallUpdateResult
30, // 8: desktop.DesktopService.GetDaemonInfo:input_type -> google.protobuf.Empty 31, // 8: desktop.DesktopService.GetDaemonInfo:input_type -> google.protobuf.Empty
30, // 9: desktop.DesktopService.ClaimService:input_type -> google.protobuf.Empty 31, // 9: desktop.DesktopService.ClaimService:input_type -> google.protobuf.Empty
30, // 10: desktop.DesktopService.TakeOverService:input_type -> google.protobuf.Empty 31, // 10: desktop.DesktopService.TakeOverService:input_type -> google.protobuf.Empty
7, // 11: desktop.DesktopService.StartService:input_type -> desktop.StartServiceRequest 7, // 11: desktop.DesktopService.StartService:input_type -> desktop.StartServiceRequest
30, // 12: desktop.DesktopService.GetWorkingDirectory:input_type -> google.protobuf.Empty 31, // 12: desktop.DesktopService.GetWorkingDirectory:input_type -> google.protobuf.Empty
30, // 13: desktop.DesktopService.DestroyWorkingDirectory:input_type -> google.protobuf.Empty 31, // 13: desktop.DesktopService.DestroyWorkingDirectory:input_type -> google.protobuf.Empty
30, // 14: desktop.DesktopService.ListCrashReports: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, // 15: desktop.DesktopService.ReadCrashReport:input_type -> desktop.CrashReportRequest
15, // 16: desktop.DesktopService.MarkCrashReportRead:input_type -> desktop.CrashReportRequest 15, // 16: desktop.DesktopService.MarkCrashReportRead:input_type -> desktop.CrashReportRequest
16, // 17: desktop.DesktopService.ExportCrashReport:input_type -> desktop.CrashReportExportRequest 16, // 17: desktop.DesktopService.ExportCrashReport:input_type -> desktop.CrashReportExportRequest
15, // 18: desktop.DesktopService.DeleteCrashReport:input_type -> desktop.CrashReportRequest 15, // 18: desktop.DesktopService.DeleteCrashReport:input_type -> desktop.CrashReportRequest
30, // 19: desktop.DesktopService.DeleteAllCrashReports:input_type -> google.protobuf.Empty 31, // 19: desktop.DesktopService.DeleteAllCrashReports:input_type -> google.protobuf.Empty
30, // 20: desktop.DesktopService.ListOOMReports: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, // 21: desktop.DesktopService.ReadOOMReport:input_type -> desktop.OOMReportRequest
22, // 22: desktop.DesktopService.MarkOOMReportRead:input_type -> desktop.OOMReportRequest 22, // 22: desktop.DesktopService.MarkOOMReportRead:input_type -> desktop.OOMReportRequest
23, // 23: desktop.DesktopService.ExportOOMReport:input_type -> desktop.OOMReportExportRequest 23, // 23: desktop.DesktopService.ExportOOMReport:input_type -> desktop.OOMReportExportRequest
22, // 24: desktop.DesktopService.DeleteOOMReport:input_type -> desktop.OOMReportRequest 22, // 24: desktop.DesktopService.DeleteOOMReport:input_type -> desktop.OOMReportRequest
30, // 25: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty 31, // 25: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty
28, // 26: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest 29, // 26: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest
30, // 27: desktop.DesktopService.GetSecuritySettings:input_type -> google.protobuf.Empty 31, // 27: desktop.DesktopService.GetSecuritySettings:input_type -> google.protobuf.Empty
27, // 28: desktop.DesktopService.SetInsecureModeEnabled:input_type -> desktop.SetInsecureModeEnabledRequest 27, // 28: desktop.DesktopService.SetInsecureModeEnabled:input_type -> desktop.SetInsecureModeEnabledRequest
9, // 29: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent 28, // 29: desktop.DesktopService.SetLocale:input_type -> desktop.SetLocaleRequest
9, // 30: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent 9, // 30: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent
30, // 31: desktop.ApplicationService.GenerateConfigSchema:input_type -> google.protobuf.Empty 9, // 31: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent
10, // 32: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent 31, // 32: desktop.ApplicationService.GenerateConfigSchema:input_type -> google.protobuf.Empty
11, // 33: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData 10, // 33: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent
3, // 34: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest 11, // 34: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData
4, // 35: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest 3, // 35: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest
5, // 36: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest 4, // 36: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest
6, // 37: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo 5, // 37: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest
30, // 38: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty 6, // 38: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo
30, // 39: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty 31, // 39: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty
30, // 40: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty 31, // 40: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty
12, // 41: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo 31, // 41: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty
30, // 42: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty 12, // 42: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo
13, // 43: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList 31, // 43: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty
17, // 44: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent 13, // 44: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList
30, // 45: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty 17, // 45: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent
19, // 46: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive 31, // 46: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty
30, // 47: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty 19, // 47: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive
30, // 48: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty 31, // 48: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty
20, // 49: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList 31, // 49: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty
24, // 50: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent 20, // 50: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList
30, // 51: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty 24, // 51: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent
19, // 52: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive 31, // 52: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty
30, // 53: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty 19, // 53: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive
30, // 54: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty 31, // 54: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty
29, // 55: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse 31, // 55: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty
26, // 56: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings 30, // 56: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse
30, // 57: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty 26, // 57: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings
30, // 58: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty 31, // 58: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty
9, // 59: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent 31, // 59: desktop.DesktopService.SetLocale:output_type -> google.protobuf.Empty
9, // 60: desktop.ApplicationService.GenerateConfigSchema:output_type -> desktop.ConfigContent 31, // 60: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty
11, // 61: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData 9, // 61: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent
10, // 62: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent 9, // 62: desktop.ApplicationService.GenerateConfigSchema:output_type -> desktop.ConfigContent
30, // 63: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty 11, // 63: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData
31, // 64: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress 10, // 64: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent
32, // 65: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress 31, // 65: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty
37, // [37:66] is the sub-list for method output_type 32, // 66: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
8, // [8:37] is the sub-list for method input_type 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 type_name
8, // [8:8] is the sub-list for extension extendee 8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name 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(), 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)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_experimental_boxdd_desktop_service_proto_rawDesc), len(file_experimental_boxdd_desktop_service_proto_rawDesc)),
NumEnums: 3, NumEnums: 3,
NumMessages: 27, NumMessages: 28,
NumExtensions: 0, NumExtensions: 0,
NumServices: 2, NumServices: 2,
}, },
+5
View File
@@ -28,6 +28,7 @@ service DesktopService {
rpc InstallUpdate(InstallUpdateRequest) returns (InstallUpdateResponse) {} rpc InstallUpdate(InstallUpdateRequest) returns (InstallUpdateResponse) {}
rpc GetSecuritySettings(google.protobuf.Empty) returns (SecuritySettings) {} rpc GetSecuritySettings(google.protobuf.Empty) returns (SecuritySettings) {}
rpc SetInsecureModeEnabled(SetInsecureModeEnabledRequest) returns (google.protobuf.Empty) {} rpc SetInsecureModeEnabled(SetInsecureModeEnabledRequest) returns (google.protobuf.Empty) {}
rpc SetLocale(SetLocaleRequest) returns (google.protobuf.Empty) {}
} }
service ApplicationService { service ApplicationService {
@@ -185,6 +186,10 @@ message SetInsecureModeEnabledRequest {
bool enabled = 1; bool enabled = 1;
} }
message SetLocaleRequest {
string locale = 1;
}
message InstallUpdateRequest { message InstallUpdateRequest {
string installer_path = 1; string installer_path = 1;
} }
@@ -37,6 +37,7 @@ const (
DesktopService_InstallUpdate_FullMethodName = "/desktop.DesktopService/InstallUpdate" DesktopService_InstallUpdate_FullMethodName = "/desktop.DesktopService/InstallUpdate"
DesktopService_GetSecuritySettings_FullMethodName = "/desktop.DesktopService/GetSecuritySettings" DesktopService_GetSecuritySettings_FullMethodName = "/desktop.DesktopService/GetSecuritySettings"
DesktopService_SetInsecureModeEnabled_FullMethodName = "/desktop.DesktopService/SetInsecureModeEnabled" DesktopService_SetInsecureModeEnabled_FullMethodName = "/desktop.DesktopService/SetInsecureModeEnabled"
DesktopService_SetLocale_FullMethodName = "/desktop.DesktopService/SetLocale"
) )
// DesktopServiceClient is the client API for DesktopService service. // 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) InstallUpdate(ctx context.Context, in *InstallUpdateRequest, opts ...grpc.CallOption) (*InstallUpdateResponse, error)
GetSecuritySettings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SecuritySettings, 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) 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 { type desktopServiceClient struct {
@@ -284,6 +286,16 @@ func (c *desktopServiceClient) SetInsecureModeEnabled(ctx context.Context, in *S
return out, nil 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. // DesktopServiceServer is the server API for DesktopService service.
// All implementations must embed UnimplementedDesktopServiceServer // All implementations must embed UnimplementedDesktopServiceServer
// for forward compatibility. // for forward compatibility.
@@ -309,6 +321,7 @@ type DesktopServiceServer interface {
InstallUpdate(context.Context, *InstallUpdateRequest) (*InstallUpdateResponse, error) InstallUpdate(context.Context, *InstallUpdateRequest) (*InstallUpdateResponse, error)
GetSecuritySettings(context.Context, *emptypb.Empty) (*SecuritySettings, error) GetSecuritySettings(context.Context, *emptypb.Empty) (*SecuritySettings, error)
SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error)
SetLocale(context.Context, *SetLocaleRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedDesktopServiceServer() mustEmbedUnimplementedDesktopServiceServer()
} }
@@ -402,6 +415,10 @@ func (UnimplementedDesktopServiceServer) GetSecuritySettings(context.Context, *e
func (UnimplementedDesktopServiceServer) SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) { func (UnimplementedDesktopServiceServer) SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetInsecureModeEnabled not implemented") 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) mustEmbedUnimplementedDesktopServiceServer() {}
func (UnimplementedDesktopServiceServer) testEmbeddedByValue() {} func (UnimplementedDesktopServiceServer) testEmbeddedByValue() {}
@@ -801,6 +818,24 @@ func _DesktopService_SetInsecureModeEnabled_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler) 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. // DesktopService_ServiceDesc is the grpc.ServiceDesc for DesktopService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -892,6 +927,10 @@ var DesktopService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetInsecureModeEnabled", MethodName: "SetInsecureModeEnabled",
Handler: _DesktopService_SetInsecureModeEnabled_Handler, Handler: _DesktopService_SetInsecureModeEnabled_Handler,
}, },
{
MethodName: "SetLocale",
Handler: _DesktopService_SetLocale_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "experimental/boxdd/desktop_service.proto", 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 ( import (
"context" "context"
"os"
"path/filepath"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
) )
@@ -26,20 +23,8 @@ func insecureModePlatformName() string {
return "Linux" 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 { func (d *Daemon) insecureModeEnabled() bool {
settings, err := loadSecuritySettings(workingDirectory) settings, err := loadDaemonSettings(workingDirectory)
if err != nil { if err != nil {
return false return false
} }
+1 -16
View File
@@ -4,11 +4,8 @@ package main
import ( import (
"context" "context"
"os"
"path/filepath"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
) )
@@ -26,20 +23,8 @@ func insecureModePlatformName() string {
return "Windows" 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 { func (d *Daemon) insecureModeEnabled() bool {
settings, err := loadSecuritySettings(workingDirectory) settings, err := loadDaemonSettings(workingDirectory)
if err != nil { if err != nil {
return false return false
} }
+6 -2
View File
@@ -137,11 +137,15 @@ func (p *linuxPlatformInterface) UsePlatformWIFIMonitor() bool {
} }
func (p *linuxPlatformInterface) UsePlatformNotification() bool { func (p *linuxPlatformInterface) UsePlatformNotification() bool {
return false return true
} }
func (p *linuxPlatformInterface) SendNotification(notification *adapter.Notification) error { 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 { func (p *linuxPlatformInterface) MyInterfaceAddress() []netip.Addr {
+6 -2
View File
@@ -152,11 +152,15 @@ func (p *windowsPlatformInterface) UsePlatformWIFIMonitor() bool {
} }
func (p *windowsPlatformInterface) UsePlatformNotification() bool { func (p *windowsPlatformInterface) UsePlatformNotification() bool {
return false return true
} }
func (p *windowsPlatformInterface) SendNotification(notification *adapter.Notification) error { 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 { func (p *windowsPlatformInterface) MyInterfaceAddress() []netip.Addr {
+1
View File
@@ -40,6 +40,7 @@ type Daemon struct {
} }
func newDaemon() (*Daemon, error) { func newDaemon() (*Daemon, error) {
restoreLocale()
ctx := include.Context(context.Background()) ctx := include.Context(context.Background())
d := &Daemon{ d := &Daemon{
ctx: ctx, 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 commandClientDialAttempts = 10
commandClientDialBaseDelay = 100 * time.Millisecond commandClientDialBaseDelay = 100 * time.Millisecond
commandClientDialStepDelay = 50 * time.Millisecond commandClientDialStepDelay = 50 * time.Millisecond
commandClientProbeTimeout = 2 * time.Second
) )
func commandClientDialDelay(attempt int) time.Duration { func commandClientDialDelay(attempt int) time.Duration {
@@ -167,14 +168,33 @@ func (c *CommandClient) establishConnection() (*grpc.ClientConn, daemon.StartedS
return c.dialRemote() return c.dialRemote()
} }
target, contextDialer := dialTarget() 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 // dialWithRetry connects to the local command server. For a handler-bound
// wait out the server starting up: WaitForReady keeps the probe redialing and // client the retry loop waits out the server starting up: WaitForReady keeps
// the loop reissues it with a growing delay, so a freshly launched extension is // the probe redialing and the loop reissues it with a growing delay, so a
// picked up without surfacing a transient "unavailable" to the UI. // 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) { 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 connection *grpc.ClientConn
var client daemon.StartedServiceClient var client daemon.StartedServiceClient
var lastError error var lastError error
@@ -185,9 +205,6 @@ func (c *CommandClient) dialWithRetry(target string, dialOptions []grpc.DialOpti
connection, err = grpc.NewClient(target, dialOptions...) connection, err = grpc.NewClient(target, dialOptions...)
if err != nil { if err != nil {
lastError = err lastError = err
if !retryDial {
return nil, nil, E.Cause(err, "create command client")
}
time.Sleep(commandClientDialDelay(attempt)) time.Sleep(commandClientDialDelay(attempt))
continue continue
} }
@@ -1304,3 +1321,231 @@ func (c *CommandClient) ProvideUSBDevices(handler USBProviderHandler) (*USBProvi
return session, nil 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 { type TailscaleEndpointStatus struct {
EndpointTag string EndpointTag string
BackendState string BackendState string
StateText string StateText string
AuthURL string AuthURL string
NetworkName string NetworkName string
MagicDNSSuffix string MagicDNSSuffix string
Self *TailscalePeer Self *TailscalePeer
ExitNode *TailscalePeer ExitNode *TailscalePeer
KeyAuth bool KeyAuth bool
userGroups []*TailscaleUserGroup CanShareFiles bool
WaitingFileCount int32
ReceivingFileCount int32
UnreadFileCount int32
userGroups []*TailscaleUserGroup
} }
func (s *TailscaleEndpointStatus) UserGroups() TailscaleUserGroupIterator { func (s *TailscaleEndpointStatus) UserGroups() TailscaleUserGroupIterator {
@@ -55,22 +59,23 @@ type TailscalePeerIterator interface {
} }
type TailscalePeer struct { type TailscalePeer struct {
StableID string StableID string
HostName string HostName string
DNSName string DNSName string
OS string OS string
tailscaleIPs []string tailscaleIPs []string
sshHostKeys []string sshHostKeys []string
Online bool Online bool
ExitNode bool ExitNode bool
ExitNodeOption bool ExitNodeOption bool
ShareeNode bool ShareeNode bool
Expired bool Expired bool
Active bool Active bool
RxBytes int64 CanReceiveFiles bool
TxBytes int64 RxBytes int64
KeyExpiry int64 TxBytes int64
LastSeen int64 KeyExpiry int64
LastSeen int64
} }
func (p *TailscalePeer) TailscaleIPs() StringIterator { func (p *TailscalePeer) TailscaleIPs() StringIterator {
@@ -104,14 +109,18 @@ func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *Ta
userGroups[i] = tailscaleUserGroupFromGRPC(group) userGroups[i] = tailscaleUserGroupFromGRPC(group)
} }
result := &TailscaleEndpointStatus{ result := &TailscaleEndpointStatus{
EndpointTag: status.EndpointTag, EndpointTag: status.EndpointTag,
BackendState: status.BackendState, BackendState: status.BackendState,
StateText: status.StateText, StateText: status.StateText,
AuthURL: status.AuthURL, AuthURL: status.AuthURL,
NetworkName: status.NetworkName, NetworkName: status.NetworkName,
MagicDNSSuffix: status.MagicDNSSuffix, MagicDNSSuffix: status.MagicDNSSuffix,
KeyAuth: status.GetKeyAuth(), KeyAuth: status.GetKeyAuth(),
userGroups: userGroups, CanShareFiles: status.CanShareFiles,
WaitingFileCount: status.WaitingFileCount,
ReceivingFileCount: status.ReceivingFileCount,
UnreadFileCount: status.UnreadFileCount,
userGroups: userGroups,
} }
if status.Self != nil { if status.Self != nil {
result.Self = tailscalePeerFromGRPC(status.Self) result.Self = tailscalePeerFromGRPC(status.Self)
@@ -138,21 +147,22 @@ func tailscaleUserGroupFromGRPC(group *daemon.TailscaleUserGroup) *TailscaleUser
func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer { func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer {
return &TailscalePeer{ return &TailscalePeer{
StableID: peer.StableID, StableID: peer.StableID,
HostName: peer.HostName, HostName: peer.HostName,
DNSName: peer.DnsName, DNSName: peer.DnsName,
OS: peer.Os, OS: peer.Os,
tailscaleIPs: peer.TailscaleIPs, tailscaleIPs: peer.TailscaleIPs,
sshHostKeys: peer.SshHostKeys, sshHostKeys: peer.SshHostKeys,
Online: peer.Online, Online: peer.Online,
ExitNode: peer.ExitNode, ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption, ExitNodeOption: peer.ExitNodeOption,
ShareeNode: peer.ShareeNode, ShareeNode: peer.ShareeNode,
Expired: peer.Expired, Expired: peer.Expired,
Active: peer.Active, Active: peer.Active,
RxBytes: peer.RxBytes, CanReceiveFiles: peer.CanReceiveFiles,
TxBytes: peer.TxBytes, RxBytes: peer.RxBytes,
KeyExpiry: peer.KeyExpiry, TxBytes: peer.TxBytes,
LastSeen: peer.LastSeen, KeyExpiry: peer.KeyExpiry,
LastSeen: peer.LastSeen,
} }
} }
@@ -6,6 +6,7 @@ type TailscalePingResult struct {
LatencyMs float64 LatencyMs float64
IsDirect bool IsDirect bool
Endpoint string Endpoint string
PeerRelay string
DERPRegionID int32 DERPRegionID int32
DERPRegionCode string DERPRegionCode string
Error string Error string
@@ -25,6 +26,7 @@ func tailscalePingResultFromGRPC(response *daemon.TailscalePingResponse) *Tailsc
LatencyMs: response.LatencyMs, LatencyMs: response.LatencyMs,
IsDirect: response.IsDirect, IsDirect: response.IsDirect,
Endpoint: response.Endpoint, Endpoint: response.Endpoint,
PeerRelay: response.PeerRelay,
DERPRegionID: response.DerpRegionID, DERPRegionID: response.DerpRegionID,
DERPRegionCode: response.DerpRegionCode, DERPRegionCode: response.DerpRegionCode,
Error: response.Error, Error: response.Error,
+4
View File
@@ -147,6 +147,10 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
return nil return nil
} }
func (s *platformInterfaceStub) CancelNotification(identifier string, typeID int32) error {
return nil
}
func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr { func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr {
return nil return nil
} }
+1
View File
@@ -17,6 +17,7 @@ type PlatformInterface interface {
ReadWIFIState() *WIFIState ReadWIFIState() *WIFIState
ClearDNSCache() ClearDNSCache()
SendNotification(notification *Notification) error SendNotification(notification *Notification) error
CancelNotification(identifier string, typeID int32) error
StartNeighborMonitor(listener NeighborUpdateListener) error StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error CloseNeighborMonitor(listener NeighborUpdateListener) error
RegisterMyInterface(name string) RegisterMyInterface(name string)
+4
View File
@@ -239,6 +239,10 @@ func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notifi
return w.iif.SendNotification((*Notification)(notification)) 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 { func (w *platformInterfaceWrapper) UsePlatformNeighborResolver() bool {
return true return true
} }
+6
View File
@@ -39,6 +39,9 @@ type Locale struct {
TailscaleStopped string TailscaleStopped string
TailscaleStarting string TailscaleStarting string
TailscaleRunning string TailscaleRunning string
TaildropReceiving string
TaildropReceived string
TaildropSendCanceled string
VPNConnecting string VPNConnecting string
VPNAuthentication string VPNAuthentication string
VPNConnected string VPNConnected string
@@ -59,6 +62,9 @@ var defaultLocale = &Locale{
TailscaleStopped: "Stopped", TailscaleStopped: "Stopped",
TailscaleStarting: "Starting", TailscaleStarting: "Starting",
TailscaleRunning: "Running", TailscaleRunning: "Running",
TaildropReceiving: "Receiving %s from %s",
TaildropReceived: "%s received from %s",
TaildropSendCanceled: "Sending %s canceled by receiver",
VPNConnecting: "Connecting", VPNConnecting: "Connecting",
VPNAuthentication: "Authentication required", VPNAuthentication: "Authentication required",
VPNConnected: "Connected", VPNConnected: "Connected",
+3
View File
@@ -14,6 +14,9 @@ func init() {
TailscaleStopped: "متوقف\u200cشده", TailscaleStopped: "متوقف\u200cشده",
TailscaleStarting: "در حال شروع", TailscaleStarting: "در حال شروع",
TailscaleRunning: "در حال اجرا", TailscaleRunning: "در حال اجرا",
TaildropReceiving: "در حال دریافت %[1]s از %[2]s",
TaildropReceived: "%[1]s از %[2]s دریافت شد",
TaildropSendCanceled: "ارسال %s توسط گیرنده لغو شد",
VPNConnecting: "در حال اتصال", VPNConnecting: "در حال اتصال",
VPNAuthentication: "نیاز به احراز هویت", VPNAuthentication: "نیاز به احراز هویت",
VPNConnected: "متصل", VPNConnected: "متصل",
+3
View File
@@ -14,6 +14,9 @@ func init() {
TailscaleStopped: "Остановлено", TailscaleStopped: "Остановлено",
TailscaleStarting: "Запуск", TailscaleStarting: "Запуск",
TailscaleRunning: "Работает", TailscaleRunning: "Работает",
TaildropReceiving: "Получение %[1]s от %[2]s",
TaildropReceived: "%[1]s получен от %[2]s",
TaildropSendCanceled: "Отправка %s отменена получателем",
VPNConnecting: "Подключение", VPNConnecting: "Подключение",
VPNAuthentication: "Требуется аутентификация", VPNAuthentication: "Требуется аутентификация",
VPNConnected: "Подключено", VPNConnected: "Подключено",
+3
View File
@@ -16,6 +16,9 @@ func init() {
TailscaleStopped: "已停止", TailscaleStopped: "已停止",
TailscaleStarting: "启动中", TailscaleStarting: "启动中",
TailscaleRunning: "运行中", TailscaleRunning: "运行中",
TaildropReceiving: "正在接收来自 %[2]s 的 %[1]s",
TaildropReceived: "已收到来自 %[2]s 的 %[1]s",
TaildropSendCanceled: "发送 %s 已被接收方取消",
VPNConnecting: "正在连接", VPNConnecting: "正在连接",
VPNAuthentication: "需要认证", VPNAuthentication: "需要认证",
VPNConnected: "已连接", VPNConnected: "已连接",
+3
View File
@@ -14,6 +14,9 @@ func init() {
TailscaleStopped: "已停止", TailscaleStopped: "已停止",
TailscaleStarting: "啟動中", TailscaleStarting: "啟動中",
TailscaleRunning: "執行中", TailscaleRunning: "執行中",
TaildropReceiving: "正在接收來自 %[2]s 的 %[1]s",
TaildropReceived: "已收到來自 %[2]s 的 %[1]s",
TaildropSendCanceled: "傳送 %s 已被接收方取消",
VPNConnecting: "正在連線", VPNConnecting: "正在連線",
VPNAuthentication: "需要認證", VPNAuthentication: "需要認證",
VPNConnected: "已連線", VPNConnected: "已連線",
+1 -1
View File
@@ -59,7 +59,7 @@ require (
github.com/sagernet/sing-usbip v0.0.0-20260813125128-908a3a2fa917 github.com/sagernet/sing-usbip v0.0.0-20260813125128-908a3a2fa917
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 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/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/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.10.2 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/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 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= 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.3 h1:c7jWyEt7n3WdbDxkc7EqmPQtPT+Hct2l1CtmEDztMuo=
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/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 h1:WTVgbkDDGnZqxIUnBsE9QyJz2dtSaeoh9mFwxmQqfgI=
github.com/sagernet/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA= 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= 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"` SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
SSHServer *TailscaleSSHServerOptions `json:"ssh_server,omitempty"` SSHServer *TailscaleSSHServerOptions `json:"ssh_server,omitempty"`
TaildropDirectory string `json:"taildrop_directory,omitempty"`
} }
type _TailscaleSSHServerOptions struct { type _TailscaleSSHServerOptions struct {
+28 -1
View File
@@ -47,6 +47,7 @@ import (
tailscaleroot "github.com/sagernet/tailscale" tailscaleroot "github.com/sagernet/tailscale"
_ "github.com/sagernet/tailscale/feature/relayserver" _ "github.com/sagernet/tailscale/feature/relayserver"
"github.com/sagernet/tailscale/ipn" "github.com/sagernet/tailscale/ipn"
"github.com/sagernet/tailscale/ipn/ipnlocal"
tsDNS "github.com/sagernet/tailscale/net/dns" tsDNS "github.com/sagernet/tailscale/net/dns"
"github.com/sagernet/tailscale/net/netmon" "github.com/sagernet/tailscale/net/netmon"
"github.com/sagernet/tailscale/net/netns" "github.com/sagernet/tailscale/net/netns"
@@ -117,6 +118,8 @@ type Endpoint struct {
sshServerInstance *tailssh.Server sshServerInstance *tailssh.Server
sshServerOptions *option.TailscaleSSHServerOptions sshServerOptions *option.TailscaleSSHServerOptions
taildrop *taildropManager
localBackend *ipnlocal.LocalBackend
systemInterface bool systemInterface bool
systemInterfaceName string systemInterfaceName string
@@ -181,6 +184,12 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
} }
dialerQueryOptions := outboundDialer.(dialer.ResolveDialer).QueryOptions() dialerQueryOptions := outboundDialer.(dialer.ResolveDialer).QueryOptions()
dnsRouter := service.FromContext[adapter.DNSRouter](ctx) 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{ return &Endpoint{
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
ctx: ctx, ctx: ctx,
@@ -231,6 +240,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
relayServerPort: options.RelayServerPort, relayServerPort: options.RelayServerPort,
relayServerStaticEndpoints: options.RelayServerStaticEndpoints, relayServerStaticEndpoints: options.RelayServerStaticEndpoints,
sshServerOptions: options.SSHServer, sshServerOptions: options.SSHServer,
taildrop: newTaildropManager(ctx, logger, tag, taildropDirectory, platformInterface),
udpTimeout: udpTimeout, udpTimeout: udpTimeout,
icmpTimeout: C.ICMPTimeout, icmpTimeout: C.ICMPTimeout,
systemInterface: options.SystemInterface, systemInterface: options.SystemInterface,
@@ -247,6 +257,12 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
if mkdirErr != nil { if mkdirErr != nil {
return E.Cause(mkdirErr, "create state directory") 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) t.server.PeerDNSQueryHandler = (*peerDNSQueryHandler)(t)
case adapter.StartStateStart: case adapter.StartStateStart:
return t.start() return t.start()
@@ -400,7 +416,13 @@ func (t *Endpoint) postStart() error {
}, true }, 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) wgEngine.SetOnReconfigListener(t.onReconfig)
t.wgEngine = wgEngine t.wgEngine = wgEngine
@@ -689,6 +711,11 @@ func (t *Endpoint) Logout(ctx context.Context) error {
func (t *Endpoint) Close() error { func (t *Endpoint) Close() error {
var err error var err error
t.started.Store(false) t.started.Store(false)
if t.localBackend != nil {
unregisterTaildropEndpoint(t.localBackend)
t.localBackend = nil
}
t.taildrop.close()
if t.icmpForwarder != nil { if t.icmpForwarder != nil {
t.icmpForwarder.Close() t.icmpForwarder.Close()
t.icmpForwarder = nil t.icmpForwarder = nil
+1
View File
@@ -48,6 +48,7 @@ func convertPingResult(result *ipnstate.PingResult) *adapter.TailscalePingResult
LatencyMs: result.LatencySeconds * 1000, LatencyMs: result.LatencySeconds * 1000,
IsDirect: result.Endpoint != "", IsDirect: result.Endpoint != "",
Endpoint: result.Endpoint, Endpoint: result.Endpoint,
PeerRelay: result.PeerRelay,
DERPRegionID: int32(result.DERPRegionID), DERPRegionID: int32(result.DERPRegionID),
DERPRegionCode: result.DERPRegionCode, DERPRegionCode: result.DERPRegionCode,
Error: result.Err, 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() status := localBackend.Status()
result := convertTailscaleStatus(status) result := convertTailscaleStatus(status)
result.KeyAuth = t.keyAuth 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) 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() scheduleUpdate()
for { for {
var busError string 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
}