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