From a8a69228d8e71eab98eb0357d2956f0602b00f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 13 Jul 2026 11:23:59 +0800 Subject: [PATCH] platform: Add boxdd --- Makefile | 3 + cmd/internal/build_boxdd/main.go | 115 ++ cmd/internal/build_libbox/main.go | 4 +- cmd/internal/build_shared/flags.go | 15 + cmd/internal/update_desktop_version/main.go | 55 + experimental/boxdd/application_service.go | 128 ++ experimental/boxdd/authenticode_windows.go | 131 ++ experimental/boxdd/cmd_run.go | 93 + experimental/boxdd/cmd_run_linux.go | 18 + experimental/boxdd/cmd_run_stub.go | 13 + experimental/boxdd/cmd_run_windows.go | 101 + experimental/boxdd/cmd_service.go | 69 + experimental/boxdd/cmd_service_linux.go | 102 + experimental/boxdd/cmd_service_stub.go | 24 + experimental/boxdd/cmd_service_windows.go | 329 ++++ experimental/boxdd/cmd_worker.go | 104 ++ experimental/boxdd/cmd_worker_unix.go | 48 + experimental/boxdd/cmd_worker_windows.go | 500 +++++ experimental/boxdd/crash_report.go | 131 ++ experimental/boxdd/debug.go | 5 + experimental/boxdd/debug_stub.go | 5 + experimental/boxdd/desktop_service.go | 225 +++ experimental/boxdd/desktop_service.pb.go | 1639 +++++++++++++++++ experimental/boxdd/desktop_service.proto | 173 ++ experimental/boxdd/desktop_service_grpc.pb.go | 1125 +++++++++++ experimental/boxdd/main.go | 46 + experimental/boxdd/managed.go | 63 + experimental/boxdd/oom_report.go | 158 ++ experimental/boxdd/peer.go | 45 + experimental/boxdd/peer_stub.go | 17 + experimental/boxdd/peer_windows.go | 555 ++++++ experimental/boxdd/report.go | 243 +++ experimental/boxdd/security_windows.go | 515 ++++++ experimental/boxdd/server.go | 343 ++++ experimental/boxdd/server_unix.go | 30 + experimental/boxdd/server_windows.go | 38 + experimental/boxdd/snapshot.go | 50 + 37 files changed, 7256 insertions(+), 2 deletions(-) create mode 100644 cmd/internal/build_boxdd/main.go create mode 100644 cmd/internal/build_shared/flags.go create mode 100644 cmd/internal/update_desktop_version/main.go create mode 100644 experimental/boxdd/application_service.go create mode 100644 experimental/boxdd/authenticode_windows.go create mode 100644 experimental/boxdd/cmd_run.go create mode 100644 experimental/boxdd/cmd_run_linux.go create mode 100644 experimental/boxdd/cmd_run_stub.go create mode 100644 experimental/boxdd/cmd_run_windows.go create mode 100644 experimental/boxdd/cmd_service.go create mode 100644 experimental/boxdd/cmd_service_linux.go create mode 100644 experimental/boxdd/cmd_service_stub.go create mode 100644 experimental/boxdd/cmd_service_windows.go create mode 100644 experimental/boxdd/cmd_worker.go create mode 100644 experimental/boxdd/cmd_worker_unix.go create mode 100644 experimental/boxdd/cmd_worker_windows.go create mode 100644 experimental/boxdd/crash_report.go create mode 100644 experimental/boxdd/debug.go create mode 100644 experimental/boxdd/debug_stub.go create mode 100644 experimental/boxdd/desktop_service.go create mode 100644 experimental/boxdd/desktop_service.pb.go create mode 100644 experimental/boxdd/desktop_service.proto create mode 100644 experimental/boxdd/desktop_service_grpc.pb.go create mode 100644 experimental/boxdd/main.go create mode 100644 experimental/boxdd/managed.go create mode 100644 experimental/boxdd/oom_report.go create mode 100644 experimental/boxdd/peer.go create mode 100644 experimental/boxdd/peer_stub.go create mode 100644 experimental/boxdd/peer_windows.go create mode 100644 experimental/boxdd/report.go create mode 100644 experimental/boxdd/security_windows.go create mode 100644 experimental/boxdd/server.go create mode 100644 experimental/boxdd/server_unix.go create mode 100644 experimental/boxdd/server_windows.go create mode 100644 experimental/boxdd/snapshot.go diff --git a/Makefile b/Makefile index 340f92e46..b8ed34b96 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,9 @@ release_install: update_android_version: go run ./cmd/internal/update_android_version +update_desktop_version: + go run ./cmd/internal/update_desktop_version + build_android: cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease :app:assembleOtherLegacyRelease && ./gradlew --stop diff --git a/cmd/internal/build_boxdd/main.go b/cmd/internal/build_boxdd/main.go new file mode 100644 index 000000000..0e0a6fa75 --- /dev/null +++ b/cmd/internal/build_boxdd/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "flag" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" +) + +var ( + debugEnabled bool + outputPath string + target string +) + +func init() { + flag.BoolVar(&debugEnabled, "debug", false, "enable debug") + flag.StringVar(&outputPath, "output", "", "output path") + flag.StringVar(&target, "target", runtime.GOOS+"/"+runtime.GOARCH, "target platform") +} + +func main() { + flag.Parse() + err := build() + if err != nil { + log.Fatal(err) + } +} + +func build() error { + targetParts := strings.Split(target, "/") + if len(targetParts) != 2 || targetParts[0] == "" || targetParts[1] == "" { + return E.New("invalid target: ", target) + } + operatingSystem := targetParts[0] + architecture := targetParts[1] + if outputPath == "" { + outputPath = "sing-box-daemon" + if operatingSystem == "windows" { + outputPath += ".exe" + } + } + absoluteOutputPath, err := filepath.Abs(outputPath) + if err != nil { + return E.Cause(err, "resolve output path") + } + err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755) + if err != nil { + return E.Cause(err, "create output directory") + } + version, err := build_shared.ReadTag() + if err != nil { + return E.Cause(err, "read version") + } + tags, err := buildTags(operatingSystem, architecture) + if err != nil { + return err + } + arguments := []string{ + "build", + "-v", + "-trimpath", + "-buildvcs=false", + "-tags", strings.Join(tags, ","), + "-ldflags", build_shared.LinkerFlags(version, debugEnabled), + "-o", absoluteOutputPath, + } + if operatingSystem == "windows" && architecture == "386" { + arguments = append(arguments, "-gcflags=net=-l") + } + arguments = append(arguments, "./experimental/boxdd") + command := exec.Command("go", arguments...) + command.Env = append(os.Environ(), + "CGO_ENABLED=0", + "GOOS="+operatingSystem, + "GOARCH="+architecture, + "GOTOOLCHAIN=local", + ) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err = command.Run() + if err != nil { + return E.Cause(err, "build sing-box daemon") + } + return nil +} + +func buildTags(operatingSystem string, architecture string) ([]string, error) { + tagsFile := "release/DEFAULT_BUILD_TAGS" + if operatingSystem == "windows" { + if architecture == "386" { + tagsFile = "release/DEFAULT_BUILD_TAGS_OTHERS" + } else { + tagsFile = "release/DEFAULT_BUILD_TAGS_WINDOWS" + } + } + content, err := os.ReadFile(tagsFile) + if err != nil { + return nil, E.Cause(err, "read build tags") + } + tags := strings.Split(strings.TrimSpace(string(content)), ",") + if operatingSystem != "windows" { + tags = append(tags, "with_purego") + } + if debugEnabled { + tags = append(tags, "debug") + } + return tags, nil +} diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index 97dc4a635..8b2c03526 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -60,8 +60,8 @@ func init() { if err != nil { currentTag = "unknown" } - sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0") - debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0") + sharedFlags = append(sharedFlags, "-ldflags", build_shared.LinkerFlags(currentTag, false)) + debugFlags = append(debugFlags, "-ldflags", build_shared.LinkerFlags(currentTag, true)) sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_usbip", "badlinkname", "tfogo_checklinkname0") darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace") diff --git a/cmd/internal/build_shared/flags.go b/cmd/internal/build_shared/flags.go new file mode 100644 index 000000000..88b501b3d --- /dev/null +++ b/cmd/internal/build_shared/flags.go @@ -0,0 +1,15 @@ +package build_shared + +import "strings" + +func LinkerFlags(version string, debug bool) string { + flags := []string{ + "-X github.com/sagernet/sing-box/constant.Version=" + version, + "-X internal/godebug.defaultGODEBUG=multipathtcp=0", + "-checklinkname=0", + } + if !debug { + flags = append(flags, "-s", "-w", "-buildid=") + } + return strings.Join(flags, " ") +} diff --git a/cmd/internal/update_desktop_version/main.go b/cmd/internal/update_desktop_version/main.go new file mode 100644 index 000000000..b99ead6a7 --- /dev/null +++ b/cmd/internal/update_desktop_version/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "encoding/json" + "flag" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" +) + +var ( + flagRunInCI bool + flagRunNightly bool +) + +type versionMetadata struct { + Version string `json:"version"` +} + +func init() { + flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") + flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly") +} + +func main() { + flag.Parse() + newVersion := common.Must1(build_shared.ReadTag()) + desktopPath := "../sing-box-for-desktop" + if flagRunInCI { + desktopPath = "clients/desktop" + } + desktopPath = common.Must1(filepath.Abs(desktopPath)) + versionPath := filepath.Join(desktopPath, "version.json") + versionFile := common.Must1(os.Open(versionPath)) + var metadata versionMetadata + common.Must(json.NewDecoder(versionFile).Decode(&metadata)) + common.Must(versionFile.Close()) + if metadata.Version == newVersion { + log.Info("version not changed") + return + } + log.Info("updated version from ", metadata.Version, " to ", newVersion) + if flagRunInCI && !flagRunNightly { + log.Fatal("version changed, commit changes first.") + } + metadata.Version = newVersion + outputFile := common.Must1(os.Create(versionPath)) + encoder := json.NewEncoder(outputFile) + encoder.SetIndent("", " ") + common.Must(encoder.Encode(metadata)) + common.Must(outputFile.Close()) +} diff --git a/experimental/boxdd/application_service.go b/experimental/boxdd/application_service.go new file mode 100644 index 000000000..4c44c29f8 --- /dev/null +++ b/experimental/boxdd/application_service.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "time" + + "github.com/sagernet/sing-box/common/networkquality" + "github.com/sagernet/sing-box/common/stun" + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing-box/experimental/libbox" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +var _ ApplicationServiceServer = (*applicationService)(nil) + +type applicationService struct { + UnimplementedApplicationServiceServer + startedService *daemon.StartedService +} + +func (s *applicationService) CheckConfig(ctx context.Context, request *ConfigContent) (*emptypb.Empty, error) { + err := s.startedService.CheckConfig(request.Content) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &emptypb.Empty{}, nil +} + +func (s *applicationService) FormatConfig(ctx context.Context, request *ConfigContent) (*ConfigContent, error) { + content, err := s.startedService.FormatConfig(request.Content) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &ConfigContent{Content: content}, nil +} + +func (s *applicationService) EncodeProfile(ctx context.Context, request *ProfileContent) (*ProfileData, error) { + content := libbox.ProfileContent{ + Name: request.Name, + Type: int32(request.Type), + Config: request.Config, + RemotePath: request.RemotePath, + AutoUpdate: request.AutoUpdate, + AutoUpdateInterval: request.AutoUpdateInterval, + LastUpdated: request.LastUpdated, + } + return &ProfileData{Data: content.Encode()}, nil +} + +func (s *applicationService) DecodeProfile(ctx context.Context, request *ProfileData) (*ProfileContent, error) { + content, err := libbox.DecodeProfileContent(request.Data) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &ProfileContent{ + Type: ProfileContent_Type(content.Type), + Name: content.Name, + Config: content.Config, + RemotePath: content.RemotePath, + AutoUpdate: content.AutoUpdate, + AutoUpdateInterval: content.AutoUpdateInterval, + LastUpdated: content.LastUpdated, + }, nil +} + +func (s *applicationService) ArchiveReport(ctx context.Context, request *ArchiveReportRequest) (*emptypb.Empty, error) { + err := libbox.CreateZipArchive(request.SourcePath, request.DestinationPath, request.Encrypt) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *applicationService) StartStandaloneNetworkQualityTest( + request *StandaloneNetworkQualityTestRequest, + server grpc.ServerStreamingServer[daemon.NetworkQualityTestProgress], +) error { + httpClient := networkquality.NewHTTPClient(nil) + defer httpClient.CloseIdleConnections() + + measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(nil, request.Http3) + if err != nil { + return err + } + + result, err := networkquality.Run(networkquality.Options{ + ConfigURL: request.ConfigUrl, + HTTPClient: httpClient, + NewMeasurementClient: measurementClientFactory, + Serial: request.Serial, + MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second, + Context: server.Context(), + OnProgress: func(progress networkquality.Progress) { + _ = server.Send(daemon.NewNetworkQualityTestProgress(progress)) + }, + }) + if err != nil { + return server.Send(&daemon.NetworkQualityTestProgress{ + IsFinal: true, + Error: err.Error(), + }) + } + return server.Send(daemon.NewNetworkQualityTestResult(result)) +} + +func (s *applicationService) StartStandaloneSTUNTest( + request *StandaloneSTUNTestRequest, + server grpc.ServerStreamingServer[daemon.STUNTestProgress], +) error { + result, err := stun.Run(stun.Options{ + Server: request.Server, + Context: server.Context(), + OnProgress: func(progress stun.Progress) { + _ = server.Send(daemon.NewSTUNTestProgress(progress)) + }, + }) + if err != nil { + return server.Send(&daemon.STUNTestProgress{ + IsFinal: true, + Error: err.Error(), + }) + } + return server.Send(daemon.NewSTUNTestResult(result)) +} diff --git a/experimental/boxdd/authenticode_windows.go b/experimental/boxdd/authenticode_windows.go new file mode 100644 index 000000000..c29dc5430 --- /dev/null +++ b/experimental/boxdd/authenticode_windows.go @@ -0,0 +1,131 @@ +//go:build windows + +package main + +import ( + "bytes" + "crypto/x509" + "time" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/windows" +) + +var ( + winTrustLibrary = windows.NewLazySystemDLL("wintrust.dll") + winTrustProviderDataFromStateDataProcedure = winTrustLibrary.NewProc("WTHelperProvDataFromStateData") + winTrustProviderSignerFromChainProcedure = winTrustLibrary.NewProc("WTHelperGetProvSignerFromChain") + winTrustProviderCertificateFromChainProcedure = winTrustLibrary.NewProc("WTHelperGetProvCertFromChain") +) + +type cryptProviderCertificate struct { + structureSize uint32 + certificateContext *windows.CertContext +} + +func authenticodeSigner(path string, file windows.Handle) ([]byte, error) { + pathPointer, err := windows.UTF16PtrFromString(path) + if err != nil { + return nil, err + } + fileInformation := windows.WinTrustFileInfo{ + Size: uint32(unsafe.Sizeof(windows.WinTrustFileInfo{})), + FilePath: pathPointer, + File: file, + } + trustData := windows.WinTrustData{ + Size: uint32(unsafe.Sizeof(windows.WinTrustData{})), + UIChoice: windows.WTD_UI_NONE, + RevocationChecks: windows.WTD_REVOKE_NONE, + UnionChoice: windows.WTD_CHOICE_FILE, + StateAction: windows.WTD_STATEACTION_VERIFY, + FileOrCatalogOrBlobOrSgnrOrCert: unsafe.Pointer(&fileInformation), + ProvFlags: windows.WTD_CACHE_ONLY_URL_RETRIEVAL | + windows.WTD_REVOCATION_CHECK_NONE | + windows.WTD_DISABLE_MD2_MD4, + UIContext: windows.WTD_UICONTEXT_EXECUTE, + } + trustError := windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, &trustData) + if trustError != nil && !E.IsMulti( + trustError, + windows.Errno(windows.CERT_E_UNTRUSTEDROOT), + windows.Errno(windows.CERT_E_CHAINING), + ) { + trustData.StateAction = windows.WTD_STATEACTION_CLOSE + windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, &trustData) + return nil, E.Cause(trustError, "verify Authenticode signature") + } + signer, signerError := verifiedSignerCertificate(trustData.StateData) + trustData.StateAction = windows.WTD_STATEACTION_CLOSE + closeError := windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, &trustData) + if signerError != nil { + return nil, signerError + } + if closeError != nil { + return nil, E.Cause(closeError, "close Authenticode verification") + } + certificate, err := validateCodeSigningCertificate(signer) + if err != nil { + return nil, err + } + if trustError != nil { + err = validateUntrustedSelfSignedCertificate(certificate, time.Now()) + if err != nil { + return nil, err + } + } + return signer, nil +} + +func verifiedSignerCertificate(stateData windows.Handle) ([]byte, error) { + providerData, _, _ := winTrustProviderDataFromStateDataProcedure.Call(uintptr(stateData)) + if providerData == 0 { + return nil, E.New("missing Authenticode provider data") + } + providerSigner, _, _ := winTrustProviderSignerFromChainProcedure.Call(providerData, 0, 0, 0) + if providerSigner == 0 { + return nil, E.New("missing Authenticode provider signer") + } + providerCertificate, _, _ := winTrustProviderCertificateFromChainProcedure.Call(providerSigner, 0) + if providerCertificate == 0 { + return nil, E.New("missing Authenticode provider certificate") + } + certificateContext := (*cryptProviderCertificate)(unsafe.Pointer(providerCertificate)).certificateContext + if certificateContext == nil { + return nil, E.New("empty Authenticode signer certificate context") + } + if certificateContext.Length == 0 || certificateContext.EncodedCert == nil { + return nil, E.New("empty Authenticode signer certificate") + } + encodedCertificate := unsafe.Slice(certificateContext.EncodedCert, int(certificateContext.Length)) + return append([]byte(nil), encodedCertificate...), nil +} + +func validateCodeSigningCertificate(encodedCertificate []byte) (*x509.Certificate, error) { + certificate, err := x509.ParseCertificate(encodedCertificate) + if err != nil { + return nil, E.Cause(err, "parse Authenticode signer certificate") + } + for _, usage := range certificate.ExtKeyUsage { + if usage == x509.ExtKeyUsageCodeSigning || usage == x509.ExtKeyUsageAny { + return certificate, nil + } + } + return nil, E.New("Authenticode signer certificate is not valid for code signing") +} + +func validateUntrustedSelfSignedCertificate(certificate *x509.Certificate, currentTime time.Time) error { + if !bytes.Equal(certificate.RawSubject, certificate.RawIssuer) { + return E.New("untrusted Authenticode signer certificate is not self-signed") + } + err := certificate.CheckSignature(certificate.SignatureAlgorithm, certificate.RawTBSCertificate, certificate.Signature) + if err != nil { + return E.Cause(err, "verify untrusted Authenticode signer self-signature") + } + if currentTime.Before(certificate.NotBefore) || currentTime.After(certificate.NotAfter) { + return E.New("untrusted Authenticode signer certificate is not currently valid") + } + return nil +} diff --git a/experimental/boxdd/cmd_run.go b/experimental/boxdd/cmd_run.go new file mode 100644 index 000000000..b054dc1f2 --- /dev/null +++ b/experimental/boxdd/cmd_run.go @@ -0,0 +1,93 @@ +package main + +import ( + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/sagernet/sing-box/experimental/libbox" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + workingDirectory string + socketPath string + listenAddress string +) + +var commandRun = &cobra.Command{ + Use: "run", + Short: "Run the daemon", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + err := run() + if err != nil { + log.Fatal(E.Cause(err, "run daemon")) + } + }, +} + +func init() { + commandRun.Flags().StringVarP(&workingDirectory, "working-directory", "D", "", "working directory") + commandRun.Flags().StringVar(&socketPath, "socket", "", "listen on the specified unix domain socket path, or named pipe path on Windows") + commandRun.Flags().StringVar(&listenAddress, "listen", "", "listen on the specified TCP address (development only)") + mainCommand.AddCommand(commandRun) +} + +func prepareWorkingDirectory() error { + if workingDirectory == "" { + return E.New("missing working directory") + } + absoluteWorkingDirectory, err := filepath.Abs(workingDirectory) + if err != nil { + return err + } + workingDirectory = absoluteWorkingDirectory + err = preparePlatformWorkingDirectory() + if err != nil { + return err + } + err = os.Chdir(workingDirectory) + if err != nil { + return err + } + err = libbox.Setup(&libbox.SetupOptions{ + BasePath: workingDirectory, + WorkingPath: workingDirectory, + TempPath: workingDirectory, + CrashReportSource: "Daemon", + }) + if err != nil { + return err + } + libbox.PromoteOOMDraft() + return nil +} + +func run() error { + handled, err := runService() + if handled { + return err + } + err = prepareWorkingDirectory() + if err != nil { + return err + } + d, err := newDaemon() + if err != nil { + return err + } + err = d.Start() + if err != nil { + return err + } + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) + <-signalChannel + d.Close() + return nil +} diff --git a/experimental/boxdd/cmd_run_linux.go b/experimental/boxdd/cmd_run_linux.go new file mode 100644 index 000000000..96b8ceb04 --- /dev/null +++ b/experimental/boxdd/cmd_run_linux.go @@ -0,0 +1,18 @@ +package main + +import ( + "os" + + E "github.com/sagernet/sing/common/exceptions" +) + +func runService() (bool, error) { + if os.Getenv("INVOCATION_ID") != "" && listenAddress != "" { + return true, E.New("--listen is not allowed in service mode") + } + return false, nil +} + +func preparePlatformWorkingDirectory() error { + return os.MkdirAll(workingDirectory, 0o700) +} diff --git a/experimental/boxdd/cmd_run_stub.go b/experimental/boxdd/cmd_run_stub.go new file mode 100644 index 000000000..71c0916bb --- /dev/null +++ b/experimental/boxdd/cmd_run_stub.go @@ -0,0 +1,13 @@ +//go:build !windows && !linux + +package main + +import "os" + +func runService() (bool, error) { + return false, nil +} + +func preparePlatformWorkingDirectory() error { + return os.MkdirAll(workingDirectory, 0o700) +} diff --git a/experimental/boxdd/cmd_run_windows.go b/experimental/boxdd/cmd_run_windows.go new file mode 100644 index 000000000..d0e61aa23 --- /dev/null +++ b/experimental/boxdd/cmd_run_windows.go @@ -0,0 +1,101 @@ +package main + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "time" + + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/eventlog" +) + +func runService() (bool, error) { + isWindowsService, err := svc.IsWindowsService() + if err != nil { + return true, E.Cause(err, "check windows service") + } + if !isWindowsService { + return false, nil + } + return true, svc.Run(serviceName, &windowsService{}) +} + +func preparePlatformWorkingDirectory() error { + if listenAddress != "" { + return os.MkdirAll(workingDirectory, 0o700) + } + if !strings.EqualFold(filepath.Clean(workingDirectory), filepath.Clean(defaultServiceWorkingDirectory)) { + return E.New("the Windows service working directory must be ", defaultServiceWorkingDirectory) + } + return ensureWindowsWorkingDirectory(workingDirectory) +} + +type windowsService struct{} + +func (s *windowsService) Execute(arguments []string, requests <-chan svc.ChangeRequest, statuses chan<- svc.Status) (serviceSpecific bool, exitCode uint32) { + statuses <- svc.Status{State: svc.StartPending} + if listenAddress != "" { + exitCode = 1 + serviceLogError(E.New("--listen is not allowed in service mode")) + return + } + err := allowAuthenticatedUsersToQueryCurrentProcess() + if err != nil { + exitCode = 1 + serviceLogError(E.Cause(err, "secure daemon process")) + return + } + err = prepareWorkingDirectory() + if err != nil { + exitCode = 1 + serviceLogError(err) + return + } + d, err := newDaemon() + if err != nil { + exitCode = 1 + serviceLogError(err) + return + } + err = d.Start() + if err != nil { + exitCode = 1 + serviceLogError(err) + return + } + statuses <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} + runtime.GC() + for request := range requests { + if request.Cmd == svc.Interrogate { + statuses <- request.CurrentStatus + continue + } + if request.Cmd == svc.Stop || request.Cmd == svc.Shutdown { + break + } + serviceLogError(E.New("unexpected service command: ", request.Cmd)) + } + statuses <- svc.Status{State: svc.StopPending} + watchdog := time.AfterFunc(3*time.Second, func() { + serviceLogError(E.New("daemon did not close")) + os.Exit(1) + }) + d.Close() + watchdog.Stop() + statuses <- svc.Status{State: svc.Stopped} + return +} + +func serviceLogError(err error) { + eventLog, openError := eventlog.Open(serviceName) + if openError != nil { + return + } + eventLog.Error(1, F.ToString(err)) + eventLog.Close() +} diff --git a/experimental/boxdd/cmd_service.go b/experimental/boxdd/cmd_service.go new file mode 100644 index 000000000..6451aabda --- /dev/null +++ b/experimental/boxdd/cmd_service.go @@ -0,0 +1,69 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandServiceFlagWorkingDirectory string + +var commandService = &cobra.Command{ + Use: "service", + Short: "Manage the system service", +} + +var commandServiceStart = &cobra.Command{ + Use: "start", + Short: "Start the system service", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + err := serviceStart() + if err != nil { + log.Fatal(E.Cause(err, "start service")) + } + }, +} + +var commandServiceStop = &cobra.Command{ + Use: "stop", + Short: "Stop the system service", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + err := serviceStop() + if err != nil { + log.Fatal(E.Cause(err, "stop service")) + } + }, +} + +var commandServiceStatus = &cobra.Command{ + Use: "status", + Short: "Print the system service status", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + status, err := serviceStatus() + if err != nil { + log.Fatal(E.Cause(err, "query service status")) + } + os.Stdout.WriteString(status.description + "\n") + os.Exit(status.exitCode) + }, +} + +type serviceStatusResult struct { + exitCode int + description string +} + +func init() { + commandService.PersistentFlags().StringVarP(&commandServiceFlagWorkingDirectory, "working-directory", "D", defaultServiceWorkingDirectory, "daemon working directory") + commandService.AddCommand(commandServiceStart) + commandService.AddCommand(commandServiceStop) + commandService.AddCommand(commandServiceStatus) + addPlatformServiceCommands() + mainCommand.AddCommand(commandService) +} diff --git a/experimental/boxdd/cmd_service_linux.go b/experimental/boxdd/cmd_service_linux.go new file mode 100644 index 000000000..45497cc9e --- /dev/null +++ b/experimental/boxdd/cmd_service_linux.go @@ -0,0 +1,102 @@ +package main + +import ( + "os/exec" + "strings" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +const ( + defaultServiceWorkingDirectory = "/var/lib/sing-box-daemon" + serviceUnitName = serviceName + ".service" +) + +var commandServiceRestart = &cobra.Command{ + Use: "restart", + Short: "Restart the system service", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + err := serviceRestart() + if err != nil { + log.Fatal(E.Cause(err, "restart service")) + } + }, +} + +func addPlatformServiceCommands() { + commandService.AddCommand(commandServiceRestart) +} + +func runSystemctl(arguments ...string) error { + output, err := exec.Command("systemctl", arguments...).CombinedOutput() + if err != nil { + message := strings.TrimSpace(string(output)) + if message == "" { + return E.Cause(err, "systemctl ", strings.Join(arguments, " ")) + } + return E.New("systemctl ", strings.Join(arguments, " "), ": ", message) + } + return nil +} + +func serviceStart() error { + return runSystemctl("start", serviceUnitName) +} + +func serviceStop() error { + installed, err := serviceInstalled() + if err != nil { + return err + } + if !installed { + log.Info("service not installed") + return nil + } + return runSystemctl("stop", serviceUnitName) +} + +func serviceRestart() error { + return runSystemctl("restart", serviceUnitName) +} + +func serviceInstalled() (bool, error) { + loadState, err := systemctlProperty("LoadState") + if err != nil { + return false, err + } + return loadState != "" && loadState != "not-found", nil +} + +func systemctlProperty(property string) (string, error) { + output, err := exec.Command("systemctl", "show", "--property="+property, "--value", serviceUnitName).CombinedOutput() + if err != nil { + message := strings.TrimSpace(string(output)) + if message == "" { + return "", E.Cause(err, "query system service") + } + return "", E.New("query system service: ", message) + } + return strings.TrimSpace(string(output)), nil +} + +func serviceStatus() (*serviceStatusResult, error) { + installed, err := serviceInstalled() + if err != nil { + return nil, err + } + if !installed { + return &serviceStatusResult{exitCode: 3, description: "not installed"}, nil + } + activeState, err := systemctlProperty("ActiveState") + if err != nil { + return nil, err + } + if activeState == "active" { + return &serviceStatusResult{exitCode: 0, description: "running"}, nil + } + return &serviceStatusResult{exitCode: 2, description: "stopped"}, nil +} diff --git a/experimental/boxdd/cmd_service_stub.go b/experimental/boxdd/cmd_service_stub.go new file mode 100644 index 000000000..58730bdc3 --- /dev/null +++ b/experimental/boxdd/cmd_service_stub.go @@ -0,0 +1,24 @@ +//go:build !windows && !linux + +package main + +import ( + E "github.com/sagernet/sing/common/exceptions" +) + +const defaultServiceWorkingDirectory = "" + +func addPlatformServiceCommands() { +} + +func serviceStart() error { + return E.New("service management is not supported on this platform") +} + +func serviceStop() error { + return E.New("service management is not supported on this platform") +} + +func serviceStatus() (*serviceStatusResult, error) { + return nil, E.New("service management is not supported on this platform") +} diff --git a/experimental/boxdd/cmd_service_windows.go b/experimental/boxdd/cmd_service_windows.go new file mode 100644 index 000000000..80d6ddd37 --- /dev/null +++ b/experimental/boxdd/cmd_service_windows.go @@ -0,0 +1,329 @@ +package main + +import ( + "errors" + "os" + "path/filepath" + "strings" + "time" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +const ( + serviceDisplayName = "sing-box Service" + serviceDescriptionText = "Privileged service for sing-box" + defaultServiceWorkingDirectory = `C:\ProgramData\sing-box-daemon` +) + +var commandServiceFlagAllowUnsafeInstallation bool + +var commandServiceInstall = &cobra.Command{ + Use: "install", + Short: "Install or update the system service", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + err := serviceInstall() + if err != nil { + log.Fatal(E.Cause(err, "install service")) + } + }, +} + +var commandServiceUninstall = &cobra.Command{ + Use: "uninstall", + Short: "Uninstall the system service", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + err := serviceUninstall() + if err != nil { + log.Fatal(E.Cause(err, "uninstall service")) + } + }, +} + +func addPlatformServiceCommands() { + commandServiceInstall.Flags().BoolVar( + &commandServiceFlagAllowUnsafeInstallation, + "allow-unsafe-installation-directory-permissions", + false, + "skip installation path ancestor permission validation", + ) + commandService.AddCommand(commandServiceInstall) + commandService.AddCommand(commandServiceUninstall) +} + +func serviceInstall() error { + executablePath, err := os.Executable() + if err != nil { + return E.Cause(err, "get executable path") + } + if !strings.EqualFold(filepath.Clean(commandServiceFlagWorkingDirectory), filepath.Clean(defaultServiceWorkingDirectory)) { + return E.New("the Windows service working directory must be ", defaultServiceWorkingDirectory) + } + executablePath, err = secureWindowsInstallation(executablePath, commandServiceFlagAllowUnsafeInstallation) + if err != nil { + return E.Cause(err, "secure installation") + } + manager, err := mgr.Connect() + if err != nil { + return E.Cause(err, "connect to service manager") + } + defer manager.Disconnect() + arguments := []string{"run", "--working-directory", defaultServiceWorkingDirectory} + config := mgr.Config{ + DisplayName: serviceDisplayName, + Description: serviceDescriptionText, + StartType: mgr.StartAutomatic, + Dependencies: []string{"Tcpip"}, + SidType: windows.SERVICE_SID_TYPE_UNRESTRICTED, + } + created := false + service, err := manager.OpenService(serviceName) + if err != nil { + if !errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) { + return E.Cause(err, "open service") + } + service, err = manager.CreateService(serviceName, executablePath, config, arguments...) + if err != nil { + return E.Cause(err, "create service") + } + created = true + } else { + err = updateServiceConfig(service, config, executablePath, arguments) + if err != nil { + service.Close() + return err + } + } + defer service.Close() + rollback := func() { + if created { + _ = service.Delete() + } + } + installedConfig, err := service.Config() + if err != nil { + rollback() + return E.Cause(err, "query installed service config") + } + if installedConfig.SidType != windows.SERVICE_SID_TYPE_UNRESTRICTED { + rollback() + return E.New("unexpected installed service SID type: ", installedConfig.SidType) + } + err = service.SetRecoveryActions([]mgr.RecoveryAction{ + {Type: mgr.ServiceRestart, Delay: 5 * time.Second}, + {Type: mgr.ServiceRestart, Delay: 5 * time.Second}, + {Type: mgr.ServiceRestart, Delay: 5 * time.Second}, + }, 86400) + if err != nil { + rollback() + return E.Cause(err, "set recovery actions") + } + err = applyProtectedServiceSecurity(service) + if err != nil { + rollback() + return E.Cause(err, "secure service") + } + err = ensureWindowsWorkingDirectory(defaultServiceWorkingDirectory) + if err != nil { + rollback() + return E.Cause(err, "secure working directory") + } + err = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info) + if err != nil && !strings.Contains(err.Error(), "already exists") { + rollback() + return E.Cause(err, "install event log source") + } + if !created { + err = stopServiceAndWait(service) + if err != nil { + return E.Cause(err, "stop service") + } + } + err = startServiceAndWait(service) + if err != nil { + return E.Cause(err, "start service") + } + return nil +} + +func updateServiceConfig(service *mgr.Service, config mgr.Config, executablePath string, arguments []string) error { + binaryPathName := windows.ComposeCommandLine(append([]string{executablePath}, arguments...)) + currentConfig, err := service.Config() + if err != nil { + return E.Cause(err, "query service config") + } + currentConfig.DisplayName = config.DisplayName + currentConfig.Description = config.Description + currentConfig.StartType = config.StartType + currentConfig.Dependencies = config.Dependencies + currentConfig.SidType = config.SidType + currentConfig.BinaryPathName = binaryPathName + err = service.UpdateConfig(currentConfig) + if err != nil { + return E.Cause(err, "update service config") + } + return nil +} + +func serviceUninstall() error { + manager, err := mgr.Connect() + if err != nil { + return E.Cause(err, "connect to service manager") + } + defer manager.Disconnect() + service, err := manager.OpenService(serviceName) + if err != nil { + if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) { + log.Info("service not installed") + return nil + } + return E.Cause(err, "open service") + } + defer service.Close() + err = stopServiceAndWait(service) + if err != nil { + log.Warn("stop service: ", err) + } + err = service.Delete() + if err != nil { + return E.Cause(err, "delete service") + } + err = eventlog.Remove(serviceName) + if err != nil { + log.Warn("remove event log source: ", err) + } + return nil +} + +func serviceStart() error { + manager, err := mgr.Connect() + if err != nil { + return E.Cause(err, "connect to service manager") + } + defer manager.Disconnect() + service, err := manager.OpenService(serviceName) + if err != nil { + return E.Cause(err, "open service") + } + defer service.Close() + return startServiceAndWait(service) +} + +func serviceStop() error { + manager, err := mgr.Connect() + if err != nil { + return E.Cause(err, "connect to service manager") + } + defer manager.Disconnect() + service, err := manager.OpenService(serviceName) + if err != nil { + if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) { + log.Info("service not installed") + return nil + } + return E.Cause(err, "open service") + } + defer service.Close() + return stopServiceAndWait(service) +} + +func startServiceAndWait(service *mgr.Service) error { + status, err := service.Query() + if err != nil { + return E.Cause(err, "query service status") + } + if status.State == svc.Running { + return nil + } + if status.State == svc.Stopped { + err = service.Start() + if err != nil { + return E.Cause(err, "start service") + } + } + return waitServiceState(service, svc.Running) +} + +func stopServiceAndWait(service *mgr.Service) error { + status, err := service.Query() + if err != nil { + return E.Cause(err, "query service status") + } + if status.State == svc.Stopped { + return nil + } + if status.State != svc.StopPending { + _, err = service.Control(svc.Stop) + if err != nil { + return E.Cause(err, "stop service") + } + } + return waitServiceState(service, svc.Stopped) +} + +func waitServiceState(service *mgr.Service, state svc.State) error { + timeout := time.Now().Add(10 * time.Second) + var currentStatus svc.Status + for time.Now().Before(timeout) { + status, err := service.Query() + if err != nil { + return E.Cause(err, "query service status") + } + currentStatus = status + if status.State == state { + return nil + } + if state == svc.Running && status.State == svc.Stopped { + return E.New( + "service stopped while starting, Windows exit code ", status.Win32ExitCode, + ", service exit code ", status.ServiceSpecificExitCode, + ) + } + time.Sleep(500 * time.Millisecond) + } + return E.New( + "timeout waiting for service state ", state, + ", current state ", currentStatus.State, + ", process ID ", currentStatus.ProcessId, + ", Windows exit code ", currentStatus.Win32ExitCode, + ", service exit code ", currentStatus.ServiceSpecificExitCode, + ) +} + +func serviceStatus() (*serviceStatusResult, error) { + manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT) + if err != nil { + return nil, E.Cause(err, "connect to service manager") + } + defer windows.CloseServiceHandle(manager) + namePointer, err := windows.UTF16PtrFromString(serviceName) + if err != nil { + return nil, err + } + service, err := windows.OpenService(manager, namePointer, windows.SERVICE_QUERY_STATUS) + if err != nil { + if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) { + return &serviceStatusResult{exitCode: 3, description: "not installed"}, nil + } + return nil, E.Cause(err, "open service") + } + defer windows.CloseServiceHandle(service) + var status windows.SERVICE_STATUS + err = windows.QueryServiceStatus(service, &status) + if err != nil { + return nil, E.Cause(err, "query service status") + } + if status.CurrentState == windows.SERVICE_RUNNING { + return &serviceStatusResult{exitCode: 0, description: "running"}, nil + } + return &serviceStatusResult{exitCode: 2, description: "stopped"}, nil +} diff --git a/experimental/boxdd/cmd_worker.go b/experimental/boxdd/cmd_worker.go new file mode 100644 index 000000000..38ccbef5d --- /dev/null +++ b/experimental/boxdd/cmd_worker.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" + "google.golang.org/grpc" +) + +var ( + workerSocketPath string + workerDaemonRelaySocketPath string + workerParentProcessID uint32 +) + +type workerParent interface { + Close() error +} + +var commandWorker = &cobra.Command{ + Use: "worker", + Short: "Serve the non-privileged application worker for the application process", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + err := runWorker() + if err != nil { + log.Fatal(E.Cause(err, "run application worker")) + } + }, +} + +func init() { + commandWorker.Flags().StringVar(&workerSocketPath, "socket", "", "listen on the specified unix domain socket path, or named pipe path on Windows") + commandWorker.Flags().StringVar(&workerDaemonRelaySocketPath, "daemon-relay-socket", "", "relay the authenticated Windows daemon connection on the specified named pipe path") + commandWorker.Flags().Uint32Var(&workerParentProcessID, "parent-pid", 0, "expected application parent process ID") + mainCommand.AddCommand(commandWorker) +} + +func runWorker() error { + if workerSocketPath == "" { + return E.New("missing --socket") + } + if workerParentProcessID == 0 { + return E.New("missing --parent-pid") + } + parent, err := prepareWorkerParent(workerParentProcessID) + if err != nil { + return err + } + defer parent.Close() + listener, err := listenWorkerEndpoint(workerSocketPath, parent) + if err != nil { + return err + } + defer listener.Close() + server := grpc.NewServer( + grpc.ChainUnaryInterceptor(daemon.UnaryErrorInterceptor), + grpc.ChainStreamInterceptor(daemon.StreamErrorInterceptor), + ) + RegisterApplicationServiceServer(server, &applicationService{ + startedService: daemon.NewStartedService(daemon.ServiceOptions{Context: include.Context(context.Background())}), + }) + relayErrorChannel := make(chan error, 1) + relay, err := startWorkerDaemonRelay(workerDaemonRelaySocketPath, parent, func(relayError error) { + select { + case relayErrorChannel <- relayError: + default: + } + server.Stop() + }) + if err != nil { + return err + } + if relay != nil { + defer relay.Close() + } + go func() { + _, _ = io.Copy(io.Discard, os.Stdin) + if relay != nil { + relay.Close() + } + server.Stop() + }() + fmt.Println("READY") + err = server.Serve(listener) + select { + case relayError := <-relayErrorChannel: + return relayError + default: + } + if err != nil && !errors.Is(err, grpc.ErrServerStopped) { + return err + } + return nil +} diff --git a/experimental/boxdd/cmd_worker_unix.go b/experimental/boxdd/cmd_worker_unix.go new file mode 100644 index 000000000..55abc76ac --- /dev/null +++ b/experimental/boxdd/cmd_worker_unix.go @@ -0,0 +1,48 @@ +//go:build !windows + +package main + +import ( + "io" + "net" + "os" + + E "github.com/sagernet/sing/common/exceptions" +) + +type unixWorkerParent struct{} + +func (unixWorkerParent) Close() error { + return nil +} + +func prepareWorkerParent(parentProcessID uint32) (workerParent, error) { + if uint32(os.Getppid()) != parentProcessID { + return nil, E.New("worker was not started by the expected application process") + } + return unixWorkerParent{}, nil +} + +func listenWorkerEndpoint(path string, parent workerParent) (net.Listener, error) { + err := os.Remove(path) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + listener, err := net.Listen("unix", path) + if err != nil { + return nil, err + } + err = os.Chmod(path, 0o600) + if err != nil { + listener.Close() + return nil, err + } + return listener, nil +} + +func startWorkerDaemonRelay(path string, parent workerParent, onFailure func(error)) (io.Closer, error) { + if path != "" { + return nil, E.New("daemon relay is only supported on Windows") + } + return nil, nil +} diff --git a/experimental/boxdd/cmd_worker_windows.go b/experimental/boxdd/cmd_worker_windows.go new file mode 100644 index 000000000..fddec6f4f --- /dev/null +++ b/experimental/boxdd/cmd_worker_windows.go @@ -0,0 +1,500 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "io" + "net" + "os" + "strings" + "sync" + "sync/atomic" + + E "github.com/sagernet/sing/common/exceptions" + + "github.com/tailscale/go-winio" + winioProcess "github.com/tailscale/go-winio/pkg/process" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) + +type windowsWorkerParent struct { + process windows.Handle + processImage windows.Handle + executable windows.Handle + executablePath string + signer []byte + userID string + sessionID uint32 + pid uint32 + exited chan struct{} + close sync.Once + closeError error +} + +type authenticatedWorkerListener struct { + net.Listener + parent *windowsWorkerParent +} + +type windowsWorkerDaemonRelay struct { + listener net.Listener + parent *windowsWorkerParent + onFailure func(error) + connections map[net.Conn]struct{} + connectionAccess sync.Mutex + connectionWaitGroup sync.WaitGroup + closing atomic.Bool + close sync.Once + closeError error +} + +type windowsAuthenticatedDaemonConnection struct { + net.Conn + process windows.Handle + processImage windows.Handle + close sync.Once + closeError error +} + +func prepareWorkerParent(parentProcessID uint32) (workerParent, error) { + if os.Getppid() != int(parentProcessID) { + return nil, E.New("worker was not started by the expected application process") + } + parentProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.SYNCHRONIZE, false, parentProcessID) + if err != nil { + return nil, err + } + keepProcess := false + defer func() { + if !keepProcess { + windows.CloseHandle(parentProcess) + } + }() + identity, err := processIdentity(parentProcess, parentProcessID) + if err != nil { + return nil, err + } + parentImagePath, err := winioProcess.QueryFullProcessImageName(parentProcess, winioProcess.ImageNameFormatWin32Path) + if err != nil { + return nil, E.Cause(err, "query worker parent executable") + } + parentImage, err := openLockedExecutable(parentImagePath) + if err != nil { + return nil, err + } + keepParentImage := false + defer func() { + if !keepParentImage { + windows.CloseHandle(parentImage) + } + }() + workerExecutablePath, err := os.Executable() + if err != nil { + return nil, err + } + workerExecutable, err := openLockedExecutable(workerExecutablePath) + if err != nil { + return nil, err + } + keepWorkerExecutable := false + defer func() { + if !keepWorkerExecutable { + windows.CloseHandle(workerExecutable) + } + }() + workerFinalPath, err := finalWindowsPath(workerExecutable) + if err != nil { + return nil, err + } + _, expectedApplicationPath, err := installedApplicationPath(workerFinalPath) + if err != nil { + return nil, err + } + expectedApplication, err := openLockedExecutable(expectedApplicationPath) + if err != nil { + return nil, err + } + defer windows.CloseHandle(expectedApplication) + parentFinalPath, err := finalWindowsPath(parentImage) + if err != nil { + return nil, err + } + expectedApplicationFinalPath, err := finalWindowsPath(expectedApplication) + if err != nil { + return nil, err + } + if !strings.EqualFold(parentFinalPath, expectedApplicationFinalPath) { + return nil, E.New("worker parent is not the installed sing-box application") + } + sameApplication, err := sameWindowsFile(parentImage, expectedApplication) + if err != nil { + return nil, err + } + if !sameApplication { + return nil, E.New("worker parent executable was replaced") + } + err = validateApplicationProcessRole(parentProcess, expectedApplication) + if err != nil { + return nil, err + } + workerSigner, err := authenticodeSigner(workerFinalPath, workerExecutable) + if err != nil { + return nil, err + } + parentSigner, err := authenticodeSigner(parentFinalPath, parentImage) + if err != nil { + return nil, err + } + if !bytes.Equal(workerSigner, parentSigner) { + return nil, E.New("worker and application have different signing certificates") + } + parentCreationTime, err := processCreationTime(parentProcess) + if err != nil { + return nil, err + } + workerCreationTime, err := processCreationTime(windows.CurrentProcess()) + if err != nil { + return nil, err + } + if parentCreationTime >= workerCreationTime { + return nil, E.New("worker parent was created after the worker process") + } + waitResult, err := windows.WaitForSingleObject(parentProcess, 0) + if err != nil { + return nil, err + } + if waitResult != uint32(windows.WAIT_TIMEOUT) { + return nil, E.New("worker application parent exited during authentication") + } + parent := &windowsWorkerParent{ + process: parentProcess, + processImage: parentImage, + executable: workerExecutable, + executablePath: workerFinalPath, + signer: workerSigner, + userID: identity.UserID, + sessionID: identity.SessionID, + pid: parentProcessID, + exited: make(chan struct{}), + } + go func() { + _, _ = windows.WaitForSingleObject(parent.process, windows.INFINITE) + close(parent.exited) + }() + keepProcess = true + keepParentImage = true + keepWorkerExecutable = true + return parent, nil +} + +func listenWorkerEndpoint(path string, parent workerParent) (net.Listener, error) { + windowsParent := parent.(*windowsWorkerParent) + securityDescriptor := fmt.Sprintf( + "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;%s)", + windowsParent.userID, + ) + listener, err := winio.ListenPipe(path, &winio.PipeConfig{ + SecurityDescriptor: securityDescriptor, + InputBufferSize: pipeBufferSize, + OutputBufferSize: pipeBufferSize, + }) + if err != nil { + return nil, err + } + authenticatedListener := &authenticatedWorkerListener{Listener: listener, parent: windowsParent} + go func() { + <-windowsParent.exited + authenticatedListener.Close() + }() + return authenticatedListener, nil +} + +func (l *authenticatedWorkerListener) Accept() (net.Conn, error) { + for { + waitResult, err := windows.WaitForSingleObject(l.parent.process, 0) + if err != nil { + return nil, err + } + if waitResult != uint32(windows.WAIT_TIMEOUT) { + return nil, E.New("worker application parent exited") + } + connection, err := l.Listener.Accept() + if err != nil { + return nil, err + } + descriptorConnection, loaded := connection.(fileDescriptorConnection) + if !loaded { + connection.Close() + continue + } + var clientProcessID uint32 + err = windows.GetNamedPipeClientProcessId(windows.Handle(descriptorConnection.Fd()), &clientProcessID) + if err != nil || clientProcessID != l.parent.pid { + connection.Close() + continue + } + waitResult, err = windows.WaitForSingleObject(l.parent.process, 0) + if err != nil || waitResult != uint32(windows.WAIT_TIMEOUT) { + connection.Close() + return nil, E.New("worker application parent exited") + } + return connection, nil + } +} + +func (p *windowsWorkerParent) Close() error { + p.close.Do(func() { + p.closeError = E.Errors( + windows.CloseHandle(p.executable), + windows.CloseHandle(p.processImage), + windows.CloseHandle(p.process), + ) + }) + return p.closeError +} + +func startWorkerDaemonRelay(path string, parent workerParent, onFailure func(error)) (io.Closer, error) { + if path == "" { + return nil, E.New("missing --daemon-relay-socket") + } + windowsParent := parent.(*windowsWorkerParent) + listener, err := listenWorkerEndpoint(path, parent) + if err != nil { + return nil, err + } + relay := &windowsWorkerDaemonRelay{ + listener: listener, + parent: windowsParent, + onFailure: onFailure, + connections: make(map[net.Conn]struct{}), + } + go relay.serve() + return relay, nil +} + +func (r *windowsWorkerDaemonRelay) serve() { + for { + connection, err := r.listener.Accept() + if err != nil { + if !r.closing.Load() && !errors.Is(err, net.ErrClosed) { + r.onFailure(E.Cause(err, "accept daemon relay connection")) + } + return + } + r.connectionAccess.Lock() + if r.closing.Load() { + r.connectionAccess.Unlock() + connection.Close() + return + } + r.connections[connection] = struct{}{} + r.connectionWaitGroup.Add(1) + r.connectionAccess.Unlock() + go func() { + r.relay(connection) + r.connectionAccess.Lock() + delete(r.connections, connection) + r.connectionAccess.Unlock() + r.connectionWaitGroup.Done() + }() + } +} + +func (r *windowsWorkerDaemonRelay) relay(applicationConnection net.Conn) { + daemonConnection, err := r.connectDaemon() + if err != nil { + applicationConnection.Close() + return + } + copyCompleted := make(chan struct{}, 2) + firstCopyCompleted := make(chan struct{}) + var firstCopy sync.Once + copyConnection := func(destination io.Writer, source io.Reader) { + _, _ = io.Copy(destination, source) + firstCopy.Do(func() { + close(firstCopyCompleted) + }) + copyCompleted <- struct{}{} + } + go copyConnection(daemonConnection, applicationConnection) + go copyConnection(applicationConnection, daemonConnection) + select { + case <-firstCopyCompleted: + case <-r.parent.exited: + } + applicationConnection.Close() + daemonConnection.Close() + <-copyCompleted + <-copyCompleted +} + +func (r *windowsWorkerDaemonRelay) connectDaemon() (net.Conn, error) { + connection, err := winio.DialPipe(daemonPipePath, nil) + if err != nil { + return nil, err + } + keepConnection := false + defer func() { + if !keepConnection { + connection.Close() + } + }() + descriptorConnection, loaded := connection.(fileDescriptorConnection) + if !loaded { + return nil, E.New("daemon endpoint is not a Windows named pipe") + } + var processID uint32 + err = windows.GetNamedPipeServerProcessId(windows.Handle(descriptorConnection.Fd()), &processID) + if err != nil { + return nil, E.Cause(err, "identify daemon named pipe server") + } + process, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.SYNCHRONIZE, false, processID) + if err != nil { + return nil, E.Cause(err, "open daemon named pipe server process") + } + keepProcess := false + defer func() { + if !keepProcess { + windows.CloseHandle(process) + } + }() + err = validateDaemonProcessIdentity(processID) + if err != nil { + return nil, err + } + processImagePath, err := winioProcess.QueryFullProcessImageName(process, winioProcess.ImageNameFormatWin32Path) + if err != nil { + return nil, E.Cause(err, "query daemon named pipe server executable") + } + processImage, err := openLockedExecutable(processImagePath) + if err != nil { + return nil, E.Cause(err, "open daemon named pipe server executable") + } + keepProcessImage := false + defer func() { + if !keepProcessImage { + windows.CloseHandle(processImage) + } + }() + processImageFinalPath, err := finalWindowsPath(processImage) + if err != nil { + return nil, err + } + if !strings.EqualFold(processImageFinalPath, r.parent.executablePath) { + return nil, E.New("named pipe server is not the installed daemon") + } + sameExecutable, err := sameWindowsFile(processImage, r.parent.executable) + if err != nil { + return nil, err + } + if !sameExecutable { + return nil, E.New("named pipe server daemon executable was replaced") + } + signer, err := authenticodeSigner(processImageFinalPath, processImage) + if err != nil { + return nil, E.Cause(err, "authenticate daemon named pipe server") + } + if !bytes.Equal(signer, r.parent.signer) { + return nil, E.New("daemon server and worker have different signing certificates") + } + waitResult, err := windows.WaitForSingleObject(process, 0) + if err != nil { + return nil, err + } + if waitResult != uint32(windows.WAIT_TIMEOUT) { + return nil, E.New("daemon named pipe server exited during authentication") + } + keepConnection = true + keepProcess = true + keepProcessImage = true + return &windowsAuthenticatedDaemonConnection{ + Conn: connection, + process: process, + processImage: processImage, + }, nil +} + +func (r *windowsWorkerDaemonRelay) Close() error { + r.close.Do(func() { + r.closing.Store(true) + r.closeError = r.listener.Close() + r.connectionAccess.Lock() + for connection := range r.connections { + connection.Close() + } + r.connectionAccess.Unlock() + r.connectionWaitGroup.Wait() + }) + return r.closeError +} + +func (c *windowsAuthenticatedDaemonConnection) Close() error { + c.close.Do(func() { + c.closeError = E.Errors( + c.Conn.Close(), + windows.CloseHandle(c.processImage), + windows.CloseHandle(c.process), + ) + }) + return c.closeError +} + +func validateDaemonProcessIdentity(processID uint32) error { + var sessionID uint32 + err := windows.ProcessIdToSessionId(processID, &sessionID) + if err != nil { + return E.Cause(err, "query daemon named pipe server session") + } + if sessionID != 0 { + return E.New("daemon named pipe server is not in session zero") + } + managerHandle, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT) + if err != nil { + return E.Cause(err, "connect to service manager") + } + defer windows.CloseServiceHandle(managerHandle) + serviceNamePointer, err := windows.UTF16PtrFromString(serviceName) + if err != nil { + return err + } + serviceHandle, err := windows.OpenService( + managerHandle, + serviceNamePointer, + windows.SERVICE_QUERY_STATUS|windows.SERVICE_QUERY_CONFIG, + ) + if err != nil { + return E.Cause(err, "open daemon service") + } + service := &mgr.Service{Name: serviceName, Handle: serviceHandle} + defer service.Close() + status, err := service.Query() + if err != nil { + return E.Cause(err, "query daemon service status") + } + if status.State != svc.Running || status.ProcessId != processID { + return E.New("named pipe server is not the running daemon service") + } + configuration, err := service.Config() + if err != nil { + return E.Cause(err, "query daemon service configuration") + } + if !strings.EqualFold(configuration.ServiceStartName, "LocalSystem") { + return E.New("daemon service does not run as LocalSystem") + } + return nil +} + +func processCreationTime(process windows.Handle) (int64, error) { + var creationTime windows.Filetime + var exitTime windows.Filetime + var kernelTime windows.Filetime + var userTime windows.Filetime + err := windows.GetProcessTimes(process, &creationTime, &exitTime, &kernelTime, &userTime) + if err != nil { + return 0, err + } + return creationTime.Nanoseconds(), nil +} diff --git a/experimental/boxdd/crash_report.go b/experimental/boxdd/crash_report.go new file mode 100644 index 000000000..eb128b1d3 --- /dev/null +++ b/experimental/boxdd/crash_report.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "sort" + + "google.golang.org/protobuf/types/known/emptypb" +) + +const crashReportsDirectoryName = "crash_reports" + +var crashReportFileOrder = []string{metadataFileName, nativeLogFileName, goLogFileName, configSnapshotFileName} + +func (s *desktopService) ListCrashReports(ctx context.Context, empty *emptypb.Empty) (*CrashReportList, error) { + reportsDirectory := filepath.Join(workingDirectory, crashReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + entries, err := os.ReadDir(reportsDirectory) + if err != nil { + if os.IsNotExist(err) { + return &CrashReportList{}, nil + } + return nil, err + } + reports := make([]*CrashReportEntry, 0, len(entries)) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + fullPath := filepath.Join(reportsDirectory, entry.Name()) + if !reportOwnedBy(fullPath, userID) { + continue + } + reports = append(reports, &CrashReportEntry{ + Name: entry.Name(), + CrashedAt: reportTime(fullPath, "crashedAt").UnixMilli(), + IsRead: reportIsRead(fullPath), + }) + } + sort.Slice(reports, func(i, j int) bool { + return reports[i].CrashedAt > reports[j].CrashedAt + }) + return &CrashReportList{Reports: reports}, nil +} + +func (s *desktopService) ReadCrashReport(ctx context.Context, request *CrashReportRequest) (*CrashReportContent, error) { + reportsDirectory := filepath.Join(workingDirectory, crashReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) + if err != nil { + return nil, err + } + files := make([]*CrashReportFile, 0, len(crashReportFileOrder)) + for _, fileName := range crashReportFileOrder { + content, readError := os.ReadFile(filepath.Join(fullPath, fileName)) + if readError != nil { + if os.IsNotExist(readError) { + continue + } + return nil, readError + } + files = append(files, &CrashReportFile{ + Name: fileName, + Content: string(content), + }) + } + return &CrashReportContent{Files: files}, nil +} + +func (s *desktopService) MarkCrashReportRead(ctx context.Context, request *CrashReportRequest) (*emptypb.Empty, error) { + reportsDirectory := filepath.Join(workingDirectory, crashReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) + if err != nil { + return nil, err + } + err = os.WriteFile(filepath.Join(fullPath, readMarkerFileName), nil, 0o600) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *desktopService) ExportCrashReport(ctx context.Context, request *CrashReportExportRequest) (*CrashReportArchive, error) { + reportsDirectory := filepath.Join(workingDirectory, crashReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + return exportReportArchive(reportsDirectory, request.Name, userID, request.WithConfiguration, request.WithLog, request.Encrypt) +} + +func (s *desktopService) DeleteCrashReport(ctx context.Context, request *CrashReportRequest) (*emptypb.Empty, error) { + reportsDirectory := filepath.Join(workingDirectory, crashReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) + if err != nil { + return nil, err + } + err = os.RemoveAll(fullPath) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *desktopService) DeleteAllCrashReports(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + reportsDirectory := filepath.Join(workingDirectory, crashReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + err = deleteReportsForUser(reportsDirectory, userID) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} diff --git a/experimental/boxdd/debug.go b/experimental/boxdd/debug.go new file mode 100644 index 000000000..e98a49432 --- /dev/null +++ b/experimental/boxdd/debug.go @@ -0,0 +1,5 @@ +//go:build debug + +package main + +const debugEnabled = true diff --git a/experimental/boxdd/debug_stub.go b/experimental/boxdd/debug_stub.go new file mode 100644 index 000000000..f011fd9b4 --- /dev/null +++ b/experimental/boxdd/debug_stub.go @@ -0,0 +1,5 @@ +//go:build !debug + +package main + +const debugEnabled = false diff --git a/experimental/boxdd/desktop_service.go b/experimental/boxdd/desktop_service.go new file mode 100644 index 000000000..3e1a9c546 --- /dev/null +++ b/experimental/boxdd/desktop_service.go @@ -0,0 +1,225 @@ +package main + +import ( + "context" + "io/fs" + "os" + "path/filepath" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/tailscale/atomicfile" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +var _ DesktopServiceServer = (*desktopService)(nil) + +type desktopService struct { + UnimplementedDesktopServiceServer + daemon *Daemon +} + +func (s *desktopService) GetDaemonInfo(ctx context.Context, empty *emptypb.Empty) (*DaemonInfo, error) { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + ownership := DaemonOwnership_DAEMON_OWNERSHIP_AVAILABLE + options, err := loadStartOptions() + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if options.OwnerUserID == identity.UserID { + ownership = DaemonOwnership_DAEMON_OWNERSHIP_CALLER + } else if options.OwnerUserID != "" { + ownership = DaemonOwnership_DAEMON_OWNERSHIP_OTHER + } + return &DaemonInfo{ + Version: C.Version, + Ownership: ownership, + }, nil +} + +func (s *desktopService) StartService(ctx context.Context, request *StartServiceRequest) (*emptypb.Empty, error) { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + s.daemon.lifecycleAccess.Lock() + defer s.daemon.lifecycleAccess.Unlock() + if s.daemon.closed { + return nil, os.ErrClosed + } + currentOptions, err := loadStartOptions() + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if currentOptions.OwnerUserID != "" && currentOptions.OwnerUserID != identity.UserID { + return nil, status.Error(codes.PermissionDenied, "the service is owned by another user") + } + mergedOptions := currentOptions + mergedOptions.WasRunning = true + mergedOptions.OwnerUserID = identity.UserID + if request.Options != nil { + mergedOptions.OOMKillerEnabled = request.Options.OomKillerEnabled + mergedOptions.OOMKillerDisabled = request.Options.OomKillerDisabled + mergedOptions.OOMMemoryLimit = request.Options.OomMemoryLimit + } + err = s.daemon.startService(request.ConfigContent, mergedOptions) + if err != nil { + return nil, s.daemon.cleanFailedStartLocked(identity.UserID, err) + } + configError := atomicfile.WriteFile(filepath.Join(workingDirectory, serviceConfigFileName), []byte(request.ConfigContent), 0o600) + optionsError := saveStartOptions(mergedOptions) + if configError != nil || optionsError != nil { + return nil, s.daemon.cleanFailedStartLocked(identity.UserID, E.Errors(configError, optionsError)) + } + return &emptypb.Empty{}, nil +} + +func (s *desktopService) ClaimService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + s.daemon.lifecycleAccess.Lock() + defer s.daemon.lifecycleAccess.Unlock() + if s.daemon.closed { + return nil, os.ErrClosed + } + options, err := loadStartOptions() + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if options.OwnerUserID == identity.UserID { + return &emptypb.Empty{}, nil + } + if options.OwnerUserID != "" { + return nil, status.Error(codes.Aborted, "the service was claimed by another user") + } + err = s.daemon.resetRuntimeOwnerLocked(identity.UserID) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *desktopService) TakeOverService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + s.daemon.lifecycleAccess.Lock() + defer s.daemon.lifecycleAccess.Unlock() + if s.daemon.closed { + return nil, os.ErrClosed + } + options, err := loadStartOptions() + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if options.OwnerUserID == identity.UserID { + return &emptypb.Empty{}, nil + } + err = s.daemon.stopServiceLocked(identity.UserID) + if err != nil { + return nil, err + } + s.daemon.disconnectPeerConnectionsExcept(identity.UserID) + return &emptypb.Empty{}, nil +} + +func (d *Daemon) cleanFailedStartLocked(ownerUserID string, startError error) error { + closeError := d.startedService.CloseService() + crashReportError := tagUnownedReports(filepath.Join(workingDirectory, crashReportsDirectoryName), ownerUserID) + oomReportError := tagUnownedReports(filepath.Join(workingDirectory, oomReportsDirectoryName), ownerUserID) + resetError := d.resetRuntimeOwnerLocked(ownerUserID) + return E.Errors(startError, closeError, crashReportError, oomReportError, resetError) +} + +func (s *desktopService) GetWorkingDirectory(ctx context.Context, empty *emptypb.Empty) (*WorkingDirectoryInfo, error) { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + s.daemon.lifecycleAccess.Lock() + defer s.daemon.lifecycleAccess.Unlock() + options, err := loadStartOptions() + if err != nil { + return nil, err + } + if options.OwnerUserID != identity.UserID { + return nil, status.Error(codes.PermissionDenied, "the service is owned by another user") + } + size, err := directorySize(workingDirectory) + if err != nil { + return nil, err + } + return &WorkingDirectoryInfo{ + Path: workingDirectory, + Size: size, + }, nil +} + +func (s *desktopService) DestroyWorkingDirectory(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + s.daemon.lifecycleAccess.Lock() + defer s.daemon.lifecycleAccess.Unlock() + if s.daemon.closed { + return nil, os.ErrClosed + } + if s.daemon.startedService.Instance() != nil { + return nil, status.Error(codes.FailedPrecondition, "the service must be stopped before destroying the working directory") + } + options, err := loadStartOptions() + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if options.OwnerUserID != "" && options.OwnerUserID != identity.UserID { + return nil, status.Error(codes.PermissionDenied, "the service is owned by another user") + } + err = s.daemon.resetRuntimeOwnerLocked(identity.UserID) + if err != nil { + return nil, err + } + err = deleteReportsForUser(filepath.Join(workingDirectory, crashReportsDirectoryName), identity.UserID) + if err != nil { + return nil, err + } + err = deleteReportsForUser(filepath.Join(workingDirectory, oomReportsDirectoryName), identity.UserID) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func directorySize(root string) (int64, error) { + var size int64 + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + info, err := entry.Info() + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + size += info.Size() + return nil + }) + if err != nil { + return 0, err + } + return size, nil +} diff --git a/experimental/boxdd/desktop_service.pb.go b/experimental/boxdd/desktop_service.pb.go new file mode 100644 index 000000000..07093aa23 --- /dev/null +++ b/experimental/boxdd/desktop_service.pb.go @@ -0,0 +1,1639 @@ +package main + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + daemon "github.com/sagernet/sing-box/daemon" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DaemonOwnership int32 + +const ( + DaemonOwnership_DAEMON_OWNERSHIP_UNSPECIFIED DaemonOwnership = 0 + DaemonOwnership_DAEMON_OWNERSHIP_AVAILABLE DaemonOwnership = 1 + DaemonOwnership_DAEMON_OWNERSHIP_CALLER DaemonOwnership = 2 + DaemonOwnership_DAEMON_OWNERSHIP_OTHER DaemonOwnership = 3 +) + +// Enum value maps for DaemonOwnership. +var ( + DaemonOwnership_name = map[int32]string{ + 0: "DAEMON_OWNERSHIP_UNSPECIFIED", + 1: "DAEMON_OWNERSHIP_AVAILABLE", + 2: "DAEMON_OWNERSHIP_CALLER", + 3: "DAEMON_OWNERSHIP_OTHER", + } + DaemonOwnership_value = map[string]int32{ + "DAEMON_OWNERSHIP_UNSPECIFIED": 0, + "DAEMON_OWNERSHIP_AVAILABLE": 1, + "DAEMON_OWNERSHIP_CALLER": 2, + "DAEMON_OWNERSHIP_OTHER": 3, + } +) + +func (x DaemonOwnership) Enum() *DaemonOwnership { + p := new(DaemonOwnership) + *p = x + return p +} + +func (x DaemonOwnership) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DaemonOwnership) Descriptor() protoreflect.EnumDescriptor { + return file_experimental_boxdd_desktop_service_proto_enumTypes[0].Descriptor() +} + +func (DaemonOwnership) Type() protoreflect.EnumType { + return &file_experimental_boxdd_desktop_service_proto_enumTypes[0] +} + +func (x DaemonOwnership) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DaemonOwnership.Descriptor instead. +func (DaemonOwnership) EnumDescriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{0} +} + +type ProfileContent_Type int32 + +const ( + ProfileContent_LOCAL ProfileContent_Type = 0 + ProfileContent_ICLOUD ProfileContent_Type = 1 + ProfileContent_REMOTE ProfileContent_Type = 2 +) + +// Enum value maps for ProfileContent_Type. +var ( + ProfileContent_Type_name = map[int32]string{ + 0: "LOCAL", + 1: "ICLOUD", + 2: "REMOTE", + } + ProfileContent_Type_value = map[string]int32{ + "LOCAL": 0, + "ICLOUD": 1, + "REMOTE": 2, + } +) + +func (x ProfileContent_Type) Enum() *ProfileContent_Type { + p := new(ProfileContent_Type) + *p = x + return p +} + +func (x ProfileContent_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProfileContent_Type) Descriptor() protoreflect.EnumDescriptor { + return file_experimental_boxdd_desktop_service_proto_enumTypes[1].Descriptor() +} + +func (ProfileContent_Type) Type() protoreflect.EnumType { + return &file_experimental_boxdd_desktop_service_proto_enumTypes[1] +} + +func (x ProfileContent_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProfileContent_Type.Descriptor instead. +func (ProfileContent_Type) EnumDescriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{7, 0} +} + +type ArchiveReportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourcePath string `protobuf:"bytes,1,opt,name=source_path,json=sourcePath,proto3" json:"source_path,omitempty"` + DestinationPath string `protobuf:"bytes,2,opt,name=destination_path,json=destinationPath,proto3" json:"destination_path,omitempty"` + Encrypt bool `protobuf:"varint,3,opt,name=encrypt,proto3" json:"encrypt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ArchiveReportRequest) Reset() { + *x = ArchiveReportRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ArchiveReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ArchiveReportRequest) ProtoMessage() {} + +func (x *ArchiveReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[0] + 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 ArchiveReportRequest.ProtoReflect.Descriptor instead. +func (*ArchiveReportRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{0} +} + +func (x *ArchiveReportRequest) GetSourcePath() string { + if x != nil { + return x.SourcePath + } + return "" +} + +func (x *ArchiveReportRequest) GetDestinationPath() string { + if x != nil { + return x.DestinationPath + } + return "" +} + +func (x *ArchiveReportRequest) GetEncrypt() bool { + if x != nil { + return x.Encrypt + } + return false +} + +type StandaloneNetworkQualityTestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConfigUrl string `protobuf:"bytes,1,opt,name=config_url,json=configUrl,proto3" json:"config_url,omitempty"` + Serial bool `protobuf:"varint,2,opt,name=serial,proto3" json:"serial,omitempty"` + MaxRuntimeSeconds int32 `protobuf:"varint,3,opt,name=max_runtime_seconds,json=maxRuntimeSeconds,proto3" json:"max_runtime_seconds,omitempty"` + Http3 bool `protobuf:"varint,4,opt,name=http3,proto3" json:"http3,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StandaloneNetworkQualityTestRequest) Reset() { + *x = StandaloneNetworkQualityTestRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StandaloneNetworkQualityTestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StandaloneNetworkQualityTestRequest) ProtoMessage() {} + +func (x *StandaloneNetworkQualityTestRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[1] + 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 StandaloneNetworkQualityTestRequest.ProtoReflect.Descriptor instead. +func (*StandaloneNetworkQualityTestRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{1} +} + +func (x *StandaloneNetworkQualityTestRequest) GetConfigUrl() string { + if x != nil { + return x.ConfigUrl + } + return "" +} + +func (x *StandaloneNetworkQualityTestRequest) GetSerial() bool { + if x != nil { + return x.Serial + } + return false +} + +func (x *StandaloneNetworkQualityTestRequest) GetMaxRuntimeSeconds() int32 { + if x != nil { + return x.MaxRuntimeSeconds + } + return 0 +} + +func (x *StandaloneNetworkQualityTestRequest) GetHttp3() bool { + if x != nil { + return x.Http3 + } + return false +} + +type StandaloneSTUNTestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StandaloneSTUNTestRequest) Reset() { + *x = StandaloneSTUNTestRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StandaloneSTUNTestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StandaloneSTUNTestRequest) ProtoMessage() {} + +func (x *StandaloneSTUNTestRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[2] + 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 StandaloneSTUNTestRequest.ProtoReflect.Descriptor instead. +func (*StandaloneSTUNTestRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{2} +} + +func (x *StandaloneSTUNTestRequest) GetServer() string { + if x != nil { + return x.Server + } + return "" +} + +type DaemonInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Ownership DaemonOwnership `protobuf:"varint,2,opt,name=ownership,proto3,enum=desktop.DaemonOwnership" json:"ownership,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DaemonInfo) Reset() { + *x = DaemonInfo{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DaemonInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DaemonInfo) ProtoMessage() {} + +func (x *DaemonInfo) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[3] + 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 DaemonInfo.ProtoReflect.Descriptor instead. +func (*DaemonInfo) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{3} +} + +func (x *DaemonInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *DaemonInfo) GetOwnership() DaemonOwnership { + if x != nil { + return x.Ownership + } + return DaemonOwnership_DAEMON_OWNERSHIP_UNSPECIFIED +} + +type StartServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConfigContent string `protobuf:"bytes,1,opt,name=config_content,json=configContent,proto3" json:"config_content,omitempty"` + Options *StartOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartServiceRequest) Reset() { + *x = StartServiceRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartServiceRequest) ProtoMessage() {} + +func (x *StartServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[4] + 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 StartServiceRequest.ProtoReflect.Descriptor instead. +func (*StartServiceRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{4} +} + +func (x *StartServiceRequest) GetConfigContent() string { + if x != nil { + return x.ConfigContent + } + return "" +} + +func (x *StartServiceRequest) GetOptions() *StartOptions { + if x != nil { + return x.Options + } + return nil +} + +type StartOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + OomKillerEnabled bool `protobuf:"varint,1,opt,name=oom_killer_enabled,json=oomKillerEnabled,proto3" json:"oom_killer_enabled,omitempty"` + OomKillerDisabled bool `protobuf:"varint,2,opt,name=oom_killer_disabled,json=oomKillerDisabled,proto3" json:"oom_killer_disabled,omitempty"` + OomMemoryLimit int64 `protobuf:"varint,3,opt,name=oom_memory_limit,json=oomMemoryLimit,proto3" json:"oom_memory_limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartOptions) Reset() { + *x = StartOptions{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartOptions) ProtoMessage() {} + +func (x *StartOptions) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[5] + 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 StartOptions.ProtoReflect.Descriptor instead. +func (*StartOptions) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{5} +} + +func (x *StartOptions) GetOomKillerEnabled() bool { + if x != nil { + return x.OomKillerEnabled + } + return false +} + +func (x *StartOptions) GetOomKillerDisabled() bool { + if x != nil { + return x.OomKillerDisabled + } + return false +} + +func (x *StartOptions) GetOomMemoryLimit() int64 { + if x != nil { + return x.OomMemoryLimit + } + return 0 +} + +type ConfigContent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigContent) Reset() { + *x = ConfigContent{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigContent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigContent) ProtoMessage() {} + +func (x *ConfigContent) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[6] + 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 ConfigContent.ProtoReflect.Descriptor instead. +func (*ConfigContent) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{6} +} + +func (x *ConfigContent) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type ProfileContent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type ProfileContent_Type `protobuf:"varint,1,opt,name=type,proto3,enum=desktop.ProfileContent_Type" json:"type,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Config string `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + RemotePath string `protobuf:"bytes,4,opt,name=remote_path,json=remotePath,proto3" json:"remote_path,omitempty"` + AutoUpdate bool `protobuf:"varint,5,opt,name=auto_update,json=autoUpdate,proto3" json:"auto_update,omitempty"` + AutoUpdateInterval int32 `protobuf:"varint,6,opt,name=auto_update_interval,json=autoUpdateInterval,proto3" json:"auto_update_interval,omitempty"` + LastUpdated int64 `protobuf:"varint,7,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProfileContent) Reset() { + *x = ProfileContent{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProfileContent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProfileContent) ProtoMessage() {} + +func (x *ProfileContent) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[7] + 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 ProfileContent.ProtoReflect.Descriptor instead. +func (*ProfileContent) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{7} +} + +func (x *ProfileContent) GetType() ProfileContent_Type { + if x != nil { + return x.Type + } + return ProfileContent_LOCAL +} + +func (x *ProfileContent) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ProfileContent) GetConfig() string { + if x != nil { + return x.Config + } + return "" +} + +func (x *ProfileContent) GetRemotePath() string { + if x != nil { + return x.RemotePath + } + return "" +} + +func (x *ProfileContent) GetAutoUpdate() bool { + if x != nil { + return x.AutoUpdate + } + return false +} + +func (x *ProfileContent) GetAutoUpdateInterval() int32 { + if x != nil { + return x.AutoUpdateInterval + } + return 0 +} + +func (x *ProfileContent) GetLastUpdated() int64 { + if x != nil { + return x.LastUpdated + } + return 0 +} + +type ProfileData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProfileData) Reset() { + *x = ProfileData{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProfileData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProfileData) ProtoMessage() {} + +func (x *ProfileData) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[8] + 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 ProfileData.ProtoReflect.Descriptor instead. +func (*ProfileData) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{8} +} + +func (x *ProfileData) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type WorkingDirectoryInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Size int64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkingDirectoryInfo) Reset() { + *x = WorkingDirectoryInfo{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkingDirectoryInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkingDirectoryInfo) ProtoMessage() {} + +func (x *WorkingDirectoryInfo) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[9] + 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 WorkingDirectoryInfo.ProtoReflect.Descriptor instead. +func (*WorkingDirectoryInfo) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{9} +} + +func (x *WorkingDirectoryInfo) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *WorkingDirectoryInfo) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type CrashReportList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Reports []*CrashReportEntry `protobuf:"bytes,1,rep,name=reports,proto3" json:"reports,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CrashReportList) Reset() { + *x = CrashReportList{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CrashReportList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CrashReportList) ProtoMessage() {} + +func (x *CrashReportList) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[10] + 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 CrashReportList.ProtoReflect.Descriptor instead. +func (*CrashReportList) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{10} +} + +func (x *CrashReportList) GetReports() []*CrashReportEntry { + if x != nil { + return x.Reports + } + return nil +} + +type CrashReportEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + CrashedAt int64 `protobuf:"varint,2,opt,name=crashed_at,json=crashedAt,proto3" json:"crashed_at,omitempty"` + IsRead bool `protobuf:"varint,3,opt,name=is_read,json=isRead,proto3" json:"is_read,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CrashReportEntry) Reset() { + *x = CrashReportEntry{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CrashReportEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CrashReportEntry) ProtoMessage() {} + +func (x *CrashReportEntry) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[11] + 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 CrashReportEntry.ProtoReflect.Descriptor instead. +func (*CrashReportEntry) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{11} +} + +func (x *CrashReportEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CrashReportEntry) GetCrashedAt() int64 { + if x != nil { + return x.CrashedAt + } + return 0 +} + +func (x *CrashReportEntry) GetIsRead() bool { + if x != nil { + return x.IsRead + } + return false +} + +type CrashReportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CrashReportRequest) Reset() { + *x = CrashReportRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CrashReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CrashReportRequest) ProtoMessage() {} + +func (x *CrashReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[12] + 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 CrashReportRequest.ProtoReflect.Descriptor instead. +func (*CrashReportRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{12} +} + +func (x *CrashReportRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type CrashReportExportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + WithConfiguration bool `protobuf:"varint,2,opt,name=with_configuration,json=withConfiguration,proto3" json:"with_configuration,omitempty"` + WithLog bool `protobuf:"varint,3,opt,name=with_log,json=withLog,proto3" json:"with_log,omitempty"` + Encrypt bool `protobuf:"varint,4,opt,name=encrypt,proto3" json:"encrypt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CrashReportExportRequest) Reset() { + *x = CrashReportExportRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CrashReportExportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CrashReportExportRequest) ProtoMessage() {} + +func (x *CrashReportExportRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[13] + 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 CrashReportExportRequest.ProtoReflect.Descriptor instead. +func (*CrashReportExportRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{13} +} + +func (x *CrashReportExportRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CrashReportExportRequest) GetWithConfiguration() bool { + if x != nil { + return x.WithConfiguration + } + return false +} + +func (x *CrashReportExportRequest) GetWithLog() bool { + if x != nil { + return x.WithLog + } + return false +} + +func (x *CrashReportExportRequest) GetEncrypt() bool { + if x != nil { + return x.Encrypt + } + return false +} + +type CrashReportContent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Files []*CrashReportFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CrashReportContent) Reset() { + *x = CrashReportContent{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CrashReportContent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CrashReportContent) ProtoMessage() {} + +func (x *CrashReportContent) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[14] + 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 CrashReportContent.ProtoReflect.Descriptor instead. +func (*CrashReportContent) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{14} +} + +func (x *CrashReportContent) GetFiles() []*CrashReportFile { + if x != nil { + return x.Files + } + return nil +} + +type CrashReportFile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CrashReportFile) Reset() { + *x = CrashReportFile{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CrashReportFile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CrashReportFile) ProtoMessage() {} + +func (x *CrashReportFile) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[15] + 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 CrashReportFile.ProtoReflect.Descriptor instead. +func (*CrashReportFile) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{15} +} + +func (x *CrashReportFile) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CrashReportFile) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type CrashReportArchive struct { + state protoimpl.MessageState `protogen:"open.v1"` + FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CrashReportArchive) Reset() { + *x = CrashReportArchive{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CrashReportArchive) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CrashReportArchive) ProtoMessage() {} + +func (x *CrashReportArchive) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[16] + 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 CrashReportArchive.ProtoReflect.Descriptor instead. +func (*CrashReportArchive) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{16} +} + +func (x *CrashReportArchive) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *CrashReportArchive) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type OOMReportList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Reports []*OOMReportEntry `protobuf:"bytes,1,rep,name=reports,proto3" json:"reports,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OOMReportList) Reset() { + *x = OOMReportList{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OOMReportList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OOMReportList) ProtoMessage() {} + +func (x *OOMReportList) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[17] + 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 OOMReportList.ProtoReflect.Descriptor instead. +func (*OOMReportList) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{17} +} + +func (x *OOMReportList) GetReports() []*OOMReportEntry { + if x != nil { + return x.Reports + } + return nil +} + +type OOMReportEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + RecordedAt int64 `protobuf:"varint,2,opt,name=recorded_at,json=recordedAt,proto3" json:"recorded_at,omitempty"` + IsRead bool `protobuf:"varint,3,opt,name=is_read,json=isRead,proto3" json:"is_read,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OOMReportEntry) Reset() { + *x = OOMReportEntry{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OOMReportEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OOMReportEntry) ProtoMessage() {} + +func (x *OOMReportEntry) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[18] + 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 OOMReportEntry.ProtoReflect.Descriptor instead. +func (*OOMReportEntry) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{18} +} + +func (x *OOMReportEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OOMReportEntry) GetRecordedAt() int64 { + if x != nil { + return x.RecordedAt + } + return 0 +} + +func (x *OOMReportEntry) GetIsRead() bool { + if x != nil { + return x.IsRead + } + return false +} + +type OOMReportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OOMReportRequest) Reset() { + *x = OOMReportRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OOMReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OOMReportRequest) ProtoMessage() {} + +func (x *OOMReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[19] + 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 OOMReportRequest.ProtoReflect.Descriptor instead. +func (*OOMReportRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{19} +} + +func (x *OOMReportRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type OOMReportExportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + WithConfiguration bool `protobuf:"varint,2,opt,name=with_configuration,json=withConfiguration,proto3" json:"with_configuration,omitempty"` + WithLog bool `protobuf:"varint,3,opt,name=with_log,json=withLog,proto3" json:"with_log,omitempty"` + Encrypt bool `protobuf:"varint,4,opt,name=encrypt,proto3" json:"encrypt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OOMReportExportRequest) Reset() { + *x = OOMReportExportRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OOMReportExportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OOMReportExportRequest) ProtoMessage() {} + +func (x *OOMReportExportRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[20] + 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 OOMReportExportRequest.ProtoReflect.Descriptor instead. +func (*OOMReportExportRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{20} +} + +func (x *OOMReportExportRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OOMReportExportRequest) GetWithConfiguration() bool { + if x != nil { + return x.WithConfiguration + } + return false +} + +func (x *OOMReportExportRequest) GetWithLog() bool { + if x != nil { + return x.WithLog + } + return false +} + +func (x *OOMReportExportRequest) GetEncrypt() bool { + if x != nil { + return x.Encrypt + } + return false +} + +type OOMReportContent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Files []*OOMReportFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OOMReportContent) Reset() { + *x = OOMReportContent{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OOMReportContent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OOMReportContent) ProtoMessage() {} + +func (x *OOMReportContent) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[21] + 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 OOMReportContent.ProtoReflect.Descriptor instead. +func (*OOMReportContent) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{21} +} + +func (x *OOMReportContent) GetFiles() []*OOMReportFile { + if x != nil { + return x.Files + } + return nil +} + +type OOMReportFile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + IsProfile bool `protobuf:"varint,3,opt,name=is_profile,json=isProfile,proto3" json:"is_profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OOMReportFile) Reset() { + *x = OOMReportFile{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OOMReportFile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OOMReportFile) ProtoMessage() {} + +func (x *OOMReportFile) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[22] + 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 OOMReportFile.ProtoReflect.Descriptor instead. +func (*OOMReportFile) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{22} +} + +func (x *OOMReportFile) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OOMReportFile) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +func (x *OOMReportFile) GetIsProfile() bool { + if x != nil { + return x.IsProfile + } + return false +} + +var File_experimental_boxdd_desktop_service_proto protoreflect.FileDescriptor + +const file_experimental_boxdd_desktop_service_proto_rawDesc = "" + + "\n" + + "(experimental/boxdd/desktop_service.proto\x12\adesktop\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cdaemon/started_service.proto\"|\n" + + "\x14ArchiveReportRequest\x12\x1f\n" + + "\vsource_path\x18\x01 \x01(\tR\n" + + "sourcePath\x12)\n" + + "\x10destination_path\x18\x02 \x01(\tR\x0fdestinationPath\x12\x18\n" + + "\aencrypt\x18\x03 \x01(\bR\aencrypt\"\xa2\x01\n" + + "#StandaloneNetworkQualityTestRequest\x12\x1d\n" + + "\n" + + "config_url\x18\x01 \x01(\tR\tconfigUrl\x12\x16\n" + + "\x06serial\x18\x02 \x01(\bR\x06serial\x12.\n" + + "\x13max_runtime_seconds\x18\x03 \x01(\x05R\x11maxRuntimeSeconds\x12\x14\n" + + "\x05http3\x18\x04 \x01(\bR\x05http3\"3\n" + + "\x19StandaloneSTUNTestRequest\x12\x16\n" + + "\x06server\x18\x01 \x01(\tR\x06server\"^\n" + + "\n" + + "DaemonInfo\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x126\n" + + "\townership\x18\x02 \x01(\x0e2\x18.desktop.DaemonOwnershipR\townership\"m\n" + + "\x13StartServiceRequest\x12%\n" + + "\x0econfig_content\x18\x01 \x01(\tR\rconfigContent\x12/\n" + + "\aoptions\x18\x02 \x01(\v2\x15.desktop.StartOptionsR\aoptions\"\x96\x01\n" + + "\fStartOptions\x12,\n" + + "\x12oom_killer_enabled\x18\x01 \x01(\bR\x10oomKillerEnabled\x12.\n" + + "\x13oom_killer_disabled\x18\x02 \x01(\bR\x11oomKillerDisabled\x12(\n" + + "\x10oom_memory_limit\x18\x03 \x01(\x03R\x0eoomMemoryLimit\")\n" + + "\rConfigContent\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"\xb0\x02\n" + + "\x0eProfileContent\x120\n" + + "\x04type\x18\x01 \x01(\x0e2\x1c.desktop.ProfileContent.TypeR\x04type\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + + "\x06config\x18\x03 \x01(\tR\x06config\x12\x1f\n" + + "\vremote_path\x18\x04 \x01(\tR\n" + + "remotePath\x12\x1f\n" + + "\vauto_update\x18\x05 \x01(\bR\n" + + "autoUpdate\x120\n" + + "\x14auto_update_interval\x18\x06 \x01(\x05R\x12autoUpdateInterval\x12!\n" + + "\flast_updated\x18\a \x01(\x03R\vlastUpdated\")\n" + + "\x04Type\x12\t\n" + + "\x05LOCAL\x10\x00\x12\n" + + "\n" + + "\x06ICLOUD\x10\x01\x12\n" + + "\n" + + "\x06REMOTE\x10\x02\"!\n" + + "\vProfileData\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\">\n" + + "\x14WorkingDirectoryInfo\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + + "\x04size\x18\x02 \x01(\x03R\x04size\"F\n" + + "\x0fCrashReportList\x123\n" + + "\areports\x18\x01 \x03(\v2\x19.desktop.CrashReportEntryR\areports\"^\n" + + "\x10CrashReportEntry\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + + "\n" + + "crashed_at\x18\x02 \x01(\x03R\tcrashedAt\x12\x17\n" + + "\ais_read\x18\x03 \x01(\bR\x06isRead\"(\n" + + "\x12CrashReportRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x92\x01\n" + + "\x18CrashReportExportRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12-\n" + + "\x12with_configuration\x18\x02 \x01(\bR\x11withConfiguration\x12\x19\n" + + "\bwith_log\x18\x03 \x01(\bR\awithLog\x12\x18\n" + + "\aencrypt\x18\x04 \x01(\bR\aencrypt\"D\n" + + "\x12CrashReportContent\x12.\n" + + "\x05files\x18\x01 \x03(\v2\x18.desktop.CrashReportFileR\x05files\"?\n" + + "\x0fCrashReportFile\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\"E\n" + + "\x12CrashReportArchive\x12\x1b\n" + + "\tfile_name\x18\x01 \x01(\tR\bfileName\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data\"B\n" + + "\rOOMReportList\x121\n" + + "\areports\x18\x01 \x03(\v2\x17.desktop.OOMReportEntryR\areports\"^\n" + + "\x0eOOMReportEntry\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n" + + "\vrecorded_at\x18\x02 \x01(\x03R\n" + + "recordedAt\x12\x17\n" + + "\ais_read\x18\x03 \x01(\bR\x06isRead\"&\n" + + "\x10OOMReportRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x90\x01\n" + + "\x16OOMReportExportRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12-\n" + + "\x12with_configuration\x18\x02 \x01(\bR\x11withConfiguration\x12\x19\n" + + "\bwith_log\x18\x03 \x01(\bR\awithLog\x12\x18\n" + + "\aencrypt\x18\x04 \x01(\bR\aencrypt\"@\n" + + "\x10OOMReportContent\x12,\n" + + "\x05files\x18\x01 \x03(\v2\x16.desktop.OOMReportFileR\x05files\"\\\n" + + "\rOOMReportFile\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\acontent\x18\x02 \x01(\fR\acontent\x12\x1d\n" + + "\n" + + "is_profile\x18\x03 \x01(\bR\tisProfile*\x8c\x01\n" + + "\x0fDaemonOwnership\x12 \n" + + "\x1cDAEMON_OWNERSHIP_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aDAEMON_OWNERSHIP_AVAILABLE\x10\x01\x12\x1b\n" + + "\x17DAEMON_OWNERSHIP_CALLER\x10\x02\x12\x1a\n" + + "\x16DAEMON_OWNERSHIP_OTHER\x10\x032\xca\n" + + "\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" + + "\x0fTakeOverService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12F\n" + + "\fStartService\x12\x1c.desktop.StartServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12N\n" + + "\x13GetWorkingDirectory\x12\x16.google.protobuf.Empty\x1a\x1d.desktop.WorkingDirectoryInfo\"\x00\x12K\n" + + "\x17DestroyWorkingDirectory\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12F\n" + + "\x10ListCrashReports\x12\x16.google.protobuf.Empty\x1a\x18.desktop.CrashReportList\"\x00\x12M\n" + + "\x0fReadCrashReport\x12\x1b.desktop.CrashReportRequest\x1a\x1b.desktop.CrashReportContent\"\x00\x12L\n" + + "\x13MarkCrashReportRead\x12\x1b.desktop.CrashReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n" + + "\x11ExportCrashReport\x12!.desktop.CrashReportExportRequest\x1a\x1b.desktop.CrashReportArchive\"\x00\x12J\n" + + "\x11DeleteCrashReport\x12\x1b.desktop.CrashReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" + + "\x15DeleteAllCrashReports\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12B\n" + + "\x0eListOOMReports\x12\x16.google.protobuf.Empty\x1a\x16.desktop.OOMReportList\"\x00\x12G\n" + + "\rReadOOMReport\x12\x19.desktop.OOMReportRequest\x1a\x19.desktop.OOMReportContent\"\x00\x12H\n" + + "\x11MarkOOMReportRead\x12\x19.desktop.OOMReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Q\n" + + "\x0fExportOOMReport\x12\x1f.desktop.OOMReportExportRequest\x1a\x1b.desktop.CrashReportArchive\"\x00\x12F\n" + + "\x0fDeleteOOMReport\x12\x19.desktop.OOMReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" + + "\x13DeleteAllOOMReports\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x002\xbd\x04\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\x12@\n" + + "\rEncodeProfile\x12\x17.desktop.ProfileContent\x1a\x14.desktop.ProfileData\"\x00\x12@\n" + + "\rDecodeProfile\x12\x14.desktop.ProfileData\x1a\x17.desktop.ProfileContent\"\x00\x12H\n" + + "\rArchiveReport\x12\x1d.desktop.ArchiveReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12y\n" + + "!StartStandaloneNetworkQualityTest\x12,.desktop.StandaloneNetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12[\n" + + "\x17StartStandaloneSTUNTest\x12\".desktop.StandaloneSTUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01B6Z4github.com/sagernet/sing-box/experimental/boxdd;mainb\x06proto3" + +var ( + file_experimental_boxdd_desktop_service_proto_rawDescOnce sync.Once + file_experimental_boxdd_desktop_service_proto_rawDescData []byte +) + +func file_experimental_boxdd_desktop_service_proto_rawDescGZIP() []byte { + file_experimental_boxdd_desktop_service_proto_rawDescOnce.Do(func() { + file_experimental_boxdd_desktop_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_experimental_boxdd_desktop_service_proto_rawDesc), len(file_experimental_boxdd_desktop_service_proto_rawDesc))) + }) + return file_experimental_boxdd_desktop_service_proto_rawDescData +} + +var ( + file_experimental_boxdd_desktop_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) + file_experimental_boxdd_desktop_service_proto_msgTypes = make([]protoimpl.MessageInfo, 23) + file_experimental_boxdd_desktop_service_proto_goTypes = []any{ + (DaemonOwnership)(0), // 0: desktop.DaemonOwnership + (ProfileContent_Type)(0), // 1: desktop.ProfileContent.Type + (*ArchiveReportRequest)(nil), // 2: desktop.ArchiveReportRequest + (*StandaloneNetworkQualityTestRequest)(nil), // 3: desktop.StandaloneNetworkQualityTestRequest + (*StandaloneSTUNTestRequest)(nil), // 4: desktop.StandaloneSTUNTestRequest + (*DaemonInfo)(nil), // 5: desktop.DaemonInfo + (*StartServiceRequest)(nil), // 6: desktop.StartServiceRequest + (*StartOptions)(nil), // 7: desktop.StartOptions + (*ConfigContent)(nil), // 8: desktop.ConfigContent + (*ProfileContent)(nil), // 9: desktop.ProfileContent + (*ProfileData)(nil), // 10: desktop.ProfileData + (*WorkingDirectoryInfo)(nil), // 11: desktop.WorkingDirectoryInfo + (*CrashReportList)(nil), // 12: desktop.CrashReportList + (*CrashReportEntry)(nil), // 13: desktop.CrashReportEntry + (*CrashReportRequest)(nil), // 14: desktop.CrashReportRequest + (*CrashReportExportRequest)(nil), // 15: desktop.CrashReportExportRequest + (*CrashReportContent)(nil), // 16: desktop.CrashReportContent + (*CrashReportFile)(nil), // 17: desktop.CrashReportFile + (*CrashReportArchive)(nil), // 18: desktop.CrashReportArchive + (*OOMReportList)(nil), // 19: desktop.OOMReportList + (*OOMReportEntry)(nil), // 20: desktop.OOMReportEntry + (*OOMReportRequest)(nil), // 21: desktop.OOMReportRequest + (*OOMReportExportRequest)(nil), // 22: desktop.OOMReportExportRequest + (*OOMReportContent)(nil), // 23: desktop.OOMReportContent + (*OOMReportFile)(nil), // 24: desktop.OOMReportFile + (*emptypb.Empty)(nil), // 25: google.protobuf.Empty + (*daemon.NetworkQualityTestProgress)(nil), // 26: daemon.NetworkQualityTestProgress + (*daemon.STUNTestProgress)(nil), // 27: daemon.STUNTestProgress + } +) + +var file_experimental_boxdd_desktop_service_proto_depIdxs = []int32{ + 0, // 0: desktop.DaemonInfo.ownership:type_name -> desktop.DaemonOwnership + 7, // 1: desktop.StartServiceRequest.options:type_name -> desktop.StartOptions + 1, // 2: desktop.ProfileContent.type:type_name -> desktop.ProfileContent.Type + 13, // 3: desktop.CrashReportList.reports:type_name -> desktop.CrashReportEntry + 17, // 4: desktop.CrashReportContent.files:type_name -> desktop.CrashReportFile + 20, // 5: desktop.OOMReportList.reports:type_name -> desktop.OOMReportEntry + 24, // 6: desktop.OOMReportContent.files:type_name -> desktop.OOMReportFile + 25, // 7: desktop.DesktopService.GetDaemonInfo:input_type -> google.protobuf.Empty + 25, // 8: desktop.DesktopService.ClaimService:input_type -> google.protobuf.Empty + 25, // 9: desktop.DesktopService.TakeOverService:input_type -> google.protobuf.Empty + 6, // 10: desktop.DesktopService.StartService:input_type -> desktop.StartServiceRequest + 25, // 11: desktop.DesktopService.GetWorkingDirectory:input_type -> google.protobuf.Empty + 25, // 12: desktop.DesktopService.DestroyWorkingDirectory:input_type -> google.protobuf.Empty + 25, // 13: desktop.DesktopService.ListCrashReports:input_type -> google.protobuf.Empty + 14, // 14: desktop.DesktopService.ReadCrashReport:input_type -> desktop.CrashReportRequest + 14, // 15: desktop.DesktopService.MarkCrashReportRead:input_type -> desktop.CrashReportRequest + 15, // 16: desktop.DesktopService.ExportCrashReport:input_type -> desktop.CrashReportExportRequest + 14, // 17: desktop.DesktopService.DeleteCrashReport:input_type -> desktop.CrashReportRequest + 25, // 18: desktop.DesktopService.DeleteAllCrashReports:input_type -> google.protobuf.Empty + 25, // 19: desktop.DesktopService.ListOOMReports:input_type -> google.protobuf.Empty + 21, // 20: desktop.DesktopService.ReadOOMReport:input_type -> desktop.OOMReportRequest + 21, // 21: desktop.DesktopService.MarkOOMReportRead:input_type -> desktop.OOMReportRequest + 22, // 22: desktop.DesktopService.ExportOOMReport:input_type -> desktop.OOMReportExportRequest + 21, // 23: desktop.DesktopService.DeleteOOMReport:input_type -> desktop.OOMReportRequest + 25, // 24: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty + 8, // 25: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent + 8, // 26: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent + 9, // 27: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent + 10, // 28: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData + 2, // 29: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest + 3, // 30: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest + 4, // 31: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest + 5, // 32: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo + 25, // 33: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty + 25, // 34: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty + 25, // 35: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty + 11, // 36: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo + 25, // 37: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty + 12, // 38: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList + 16, // 39: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent + 25, // 40: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty + 18, // 41: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive + 25, // 42: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty + 25, // 43: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty + 19, // 44: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList + 23, // 45: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent + 25, // 46: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty + 18, // 47: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive + 25, // 48: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty + 25, // 49: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty + 25, // 50: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty + 8, // 51: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent + 10, // 52: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData + 9, // 53: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent + 25, // 54: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty + 26, // 55: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 27, // 56: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress + 32, // [32:57] is the sub-list for method output_type + 7, // [7:32] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_experimental_boxdd_desktop_service_proto_init() } +func file_experimental_boxdd_desktop_service_proto_init() { + if File_experimental_boxdd_desktop_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + 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: 2, + NumMessages: 23, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_experimental_boxdd_desktop_service_proto_goTypes, + DependencyIndexes: file_experimental_boxdd_desktop_service_proto_depIdxs, + EnumInfos: file_experimental_boxdd_desktop_service_proto_enumTypes, + MessageInfos: file_experimental_boxdd_desktop_service_proto_msgTypes, + }.Build() + File_experimental_boxdd_desktop_service_proto = out.File + file_experimental_boxdd_desktop_service_proto_goTypes = nil + file_experimental_boxdd_desktop_service_proto_depIdxs = nil +} diff --git a/experimental/boxdd/desktop_service.proto b/experimental/boxdd/desktop_service.proto new file mode 100644 index 000000000..8139f4e79 --- /dev/null +++ b/experimental/boxdd/desktop_service.proto @@ -0,0 +1,173 @@ +syntax = "proto3"; + +package desktop; +option go_package = "github.com/sagernet/sing-box/experimental/boxdd;main"; + +import "google/protobuf/empty.proto"; +import "daemon/started_service.proto"; + +service DesktopService { + rpc GetDaemonInfo(google.protobuf.Empty) returns (DaemonInfo) {} + rpc ClaimService(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc TakeOverService(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc StartService(StartServiceRequest) returns (google.protobuf.Empty) {} + rpc GetWorkingDirectory(google.protobuf.Empty) returns (WorkingDirectoryInfo) {} + rpc DestroyWorkingDirectory(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc ListCrashReports(google.protobuf.Empty) returns (CrashReportList) {} + rpc ReadCrashReport(CrashReportRequest) returns (CrashReportContent) {} + rpc MarkCrashReportRead(CrashReportRequest) returns (google.protobuf.Empty) {} + rpc ExportCrashReport(CrashReportExportRequest) returns (CrashReportArchive) {} + rpc DeleteCrashReport(CrashReportRequest) returns (google.protobuf.Empty) {} + rpc DeleteAllCrashReports(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc ListOOMReports(google.protobuf.Empty) returns (OOMReportList) {} + rpc ReadOOMReport(OOMReportRequest) returns (OOMReportContent) {} + rpc MarkOOMReportRead(OOMReportRequest) returns (google.protobuf.Empty) {} + rpc ExportOOMReport(OOMReportExportRequest) returns (CrashReportArchive) {} + rpc DeleteOOMReport(OOMReportRequest) returns (google.protobuf.Empty) {} + rpc DeleteAllOOMReports(google.protobuf.Empty) returns (google.protobuf.Empty) {} +} + +service ApplicationService { + rpc CheckConfig(ConfigContent) returns (google.protobuf.Empty) {} + rpc FormatConfig(ConfigContent) returns (ConfigContent) {} + rpc EncodeProfile(ProfileContent) returns (ProfileData) {} + rpc DecodeProfile(ProfileData) returns (ProfileContent) {} + rpc ArchiveReport(ArchiveReportRequest) returns (google.protobuf.Empty) {} + rpc StartStandaloneNetworkQualityTest(StandaloneNetworkQualityTestRequest) returns (stream daemon.NetworkQualityTestProgress) {} + rpc StartStandaloneSTUNTest(StandaloneSTUNTestRequest) returns (stream daemon.STUNTestProgress) {} +} + +message ArchiveReportRequest { + string source_path = 1; + string destination_path = 2; + bool encrypt = 3; +} + +message StandaloneNetworkQualityTestRequest { + string config_url = 1; + bool serial = 2; + int32 max_runtime_seconds = 3; + bool http3 = 4; +} + +message StandaloneSTUNTestRequest { + string server = 1; +} + +message DaemonInfo { + string version = 1; + DaemonOwnership ownership = 2; +} + +enum DaemonOwnership { + DAEMON_OWNERSHIP_UNSPECIFIED = 0; + DAEMON_OWNERSHIP_AVAILABLE = 1; + DAEMON_OWNERSHIP_CALLER = 2; + DAEMON_OWNERSHIP_OTHER = 3; +} + +message StartServiceRequest { + string config_content = 1; + StartOptions options = 2; +} + +message StartOptions { + bool oom_killer_enabled = 1; + bool oom_killer_disabled = 2; + int64 oom_memory_limit = 3; +} + +message ConfigContent { + string content = 1; +} + +message ProfileContent { + enum Type { + LOCAL = 0; + ICLOUD = 1; + REMOTE = 2; + } + + Type type = 1; + string name = 2; + string config = 3; + string remote_path = 4; + bool auto_update = 5; + int32 auto_update_interval = 6; + int64 last_updated = 7; +} + +message ProfileData { + bytes data = 1; +} + +message WorkingDirectoryInfo { + string path = 1; + int64 size = 2; +} + +message CrashReportList { + repeated CrashReportEntry reports = 1; +} + +message CrashReportEntry { + string name = 1; + int64 crashed_at = 2; + bool is_read = 3; +} + +message CrashReportRequest { + string name = 1; +} + +message CrashReportExportRequest { + string name = 1; + bool with_configuration = 2; + bool with_log = 3; + bool encrypt = 4; +} + +message CrashReportContent { + repeated CrashReportFile files = 1; +} + +message CrashReportFile { + string name = 1; + string content = 2; +} + +message CrashReportArchive { + string file_name = 1; + bytes data = 2; +} + +message OOMReportList { + repeated OOMReportEntry reports = 1; +} + +message OOMReportEntry { + string name = 1; + int64 recorded_at = 2; + bool is_read = 3; +} + +message OOMReportRequest { + string name = 1; +} + +message OOMReportExportRequest { + string name = 1; + bool with_configuration = 2; + bool with_log = 3; + bool encrypt = 4; +} + +message OOMReportContent { + repeated OOMReportFile files = 1; +} + +message OOMReportFile { + string name = 1; + bytes content = 2; + bool is_profile = 3; +} diff --git a/experimental/boxdd/desktop_service_grpc.pb.go b/experimental/boxdd/desktop_service_grpc.pb.go new file mode 100644 index 000000000..a9308332c --- /dev/null +++ b/experimental/boxdd/desktop_service_grpc.pb.go @@ -0,0 +1,1125 @@ +package main + +import ( + context "context" + + daemon "github.com/sagernet/sing-box/daemon" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DesktopService_GetDaemonInfo_FullMethodName = "/desktop.DesktopService/GetDaemonInfo" + DesktopService_ClaimService_FullMethodName = "/desktop.DesktopService/ClaimService" + DesktopService_TakeOverService_FullMethodName = "/desktop.DesktopService/TakeOverService" + DesktopService_StartService_FullMethodName = "/desktop.DesktopService/StartService" + DesktopService_GetWorkingDirectory_FullMethodName = "/desktop.DesktopService/GetWorkingDirectory" + DesktopService_DestroyWorkingDirectory_FullMethodName = "/desktop.DesktopService/DestroyWorkingDirectory" + DesktopService_ListCrashReports_FullMethodName = "/desktop.DesktopService/ListCrashReports" + DesktopService_ReadCrashReport_FullMethodName = "/desktop.DesktopService/ReadCrashReport" + DesktopService_MarkCrashReportRead_FullMethodName = "/desktop.DesktopService/MarkCrashReportRead" + DesktopService_ExportCrashReport_FullMethodName = "/desktop.DesktopService/ExportCrashReport" + DesktopService_DeleteCrashReport_FullMethodName = "/desktop.DesktopService/DeleteCrashReport" + DesktopService_DeleteAllCrashReports_FullMethodName = "/desktop.DesktopService/DeleteAllCrashReports" + DesktopService_ListOOMReports_FullMethodName = "/desktop.DesktopService/ListOOMReports" + DesktopService_ReadOOMReport_FullMethodName = "/desktop.DesktopService/ReadOOMReport" + DesktopService_MarkOOMReportRead_FullMethodName = "/desktop.DesktopService/MarkOOMReportRead" + DesktopService_ExportOOMReport_FullMethodName = "/desktop.DesktopService/ExportOOMReport" + DesktopService_DeleteOOMReport_FullMethodName = "/desktop.DesktopService/DeleteOOMReport" + DesktopService_DeleteAllOOMReports_FullMethodName = "/desktop.DesktopService/DeleteAllOOMReports" +) + +// DesktopServiceClient is the client API for DesktopService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DesktopServiceClient interface { + GetDaemonInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DaemonInfo, error) + ClaimService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + TakeOverService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + StartService(ctx context.Context, in *StartServiceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetWorkingDirectory(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*WorkingDirectoryInfo, error) + DestroyWorkingDirectory(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + ListCrashReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CrashReportList, error) + ReadCrashReport(ctx context.Context, in *CrashReportRequest, opts ...grpc.CallOption) (*CrashReportContent, error) + MarkCrashReportRead(ctx context.Context, in *CrashReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ExportCrashReport(ctx context.Context, in *CrashReportExportRequest, opts ...grpc.CallOption) (*CrashReportArchive, error) + DeleteCrashReport(ctx context.Context, in *CrashReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteAllCrashReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + ListOOMReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OOMReportList, error) + ReadOOMReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*OOMReportContent, error) + MarkOOMReportRead(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ExportOOMReport(ctx context.Context, in *OOMReportExportRequest, opts ...grpc.CallOption) (*CrashReportArchive, error) + DeleteOOMReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteAllOOMReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type desktopServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDesktopServiceClient(cc grpc.ClientConnInterface) DesktopServiceClient { + return &desktopServiceClient{cc} +} + +func (c *desktopServiceClient) GetDaemonInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DaemonInfo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DaemonInfo) + err := c.cc.Invoke(ctx, DesktopService_GetDaemonInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) ClaimService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_ClaimService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) TakeOverService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_TakeOverService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) StartService(ctx context.Context, in *StartServiceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_StartService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) GetWorkingDirectory(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*WorkingDirectoryInfo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WorkingDirectoryInfo) + err := c.cc.Invoke(ctx, DesktopService_GetWorkingDirectory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) DestroyWorkingDirectory(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_DestroyWorkingDirectory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) ListCrashReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CrashReportList, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CrashReportList) + err := c.cc.Invoke(ctx, DesktopService_ListCrashReports_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) ReadCrashReport(ctx context.Context, in *CrashReportRequest, opts ...grpc.CallOption) (*CrashReportContent, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CrashReportContent) + err := c.cc.Invoke(ctx, DesktopService_ReadCrashReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) MarkCrashReportRead(ctx context.Context, in *CrashReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_MarkCrashReportRead_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) ExportCrashReport(ctx context.Context, in *CrashReportExportRequest, opts ...grpc.CallOption) (*CrashReportArchive, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CrashReportArchive) + err := c.cc.Invoke(ctx, DesktopService_ExportCrashReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) DeleteCrashReport(ctx context.Context, in *CrashReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_DeleteCrashReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) DeleteAllCrashReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_DeleteAllCrashReports_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) ListOOMReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OOMReportList, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OOMReportList) + err := c.cc.Invoke(ctx, DesktopService_ListOOMReports_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) ReadOOMReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*OOMReportContent, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OOMReportContent) + err := c.cc.Invoke(ctx, DesktopService_ReadOOMReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) MarkOOMReportRead(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_MarkOOMReportRead_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) ExportOOMReport(ctx context.Context, in *OOMReportExportRequest, opts ...grpc.CallOption) (*CrashReportArchive, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CrashReportArchive) + err := c.cc.Invoke(ctx, DesktopService_ExportOOMReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) DeleteOOMReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_DeleteOOMReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) DeleteAllOOMReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_DeleteAllOOMReports_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. +type DesktopServiceServer interface { + GetDaemonInfo(context.Context, *emptypb.Empty) (*DaemonInfo, error) + ClaimService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + TakeOverService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + StartService(context.Context, *StartServiceRequest) (*emptypb.Empty, error) + GetWorkingDirectory(context.Context, *emptypb.Empty) (*WorkingDirectoryInfo, error) + DestroyWorkingDirectory(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + ListCrashReports(context.Context, *emptypb.Empty) (*CrashReportList, error) + ReadCrashReport(context.Context, *CrashReportRequest) (*CrashReportContent, error) + MarkCrashReportRead(context.Context, *CrashReportRequest) (*emptypb.Empty, error) + ExportCrashReport(context.Context, *CrashReportExportRequest) (*CrashReportArchive, error) + DeleteCrashReport(context.Context, *CrashReportRequest) (*emptypb.Empty, error) + DeleteAllCrashReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + ListOOMReports(context.Context, *emptypb.Empty) (*OOMReportList, error) + ReadOOMReport(context.Context, *OOMReportRequest) (*OOMReportContent, error) + MarkOOMReportRead(context.Context, *OOMReportRequest) (*emptypb.Empty, error) + ExportOOMReport(context.Context, *OOMReportExportRequest) (*CrashReportArchive, error) + DeleteOOMReport(context.Context, *OOMReportRequest) (*emptypb.Empty, error) + DeleteAllOOMReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + mustEmbedUnimplementedDesktopServiceServer() +} + +// UnimplementedDesktopServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDesktopServiceServer struct{} + +func (UnimplementedDesktopServiceServer) GetDaemonInfo(context.Context, *emptypb.Empty) (*DaemonInfo, error) { + return nil, status.Error(codes.Unimplemented, "method GetDaemonInfo not implemented") +} + +func (UnimplementedDesktopServiceServer) ClaimService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method ClaimService not implemented") +} + +func (UnimplementedDesktopServiceServer) TakeOverService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method TakeOverService not implemented") +} + +func (UnimplementedDesktopServiceServer) StartService(context.Context, *StartServiceRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method StartService not implemented") +} + +func (UnimplementedDesktopServiceServer) GetWorkingDirectory(context.Context, *emptypb.Empty) (*WorkingDirectoryInfo, error) { + return nil, status.Error(codes.Unimplemented, "method GetWorkingDirectory not implemented") +} + +func (UnimplementedDesktopServiceServer) DestroyWorkingDirectory(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DestroyWorkingDirectory not implemented") +} + +func (UnimplementedDesktopServiceServer) ListCrashReports(context.Context, *emptypb.Empty) (*CrashReportList, error) { + return nil, status.Error(codes.Unimplemented, "method ListCrashReports not implemented") +} + +func (UnimplementedDesktopServiceServer) ReadCrashReport(context.Context, *CrashReportRequest) (*CrashReportContent, error) { + return nil, status.Error(codes.Unimplemented, "method ReadCrashReport not implemented") +} + +func (UnimplementedDesktopServiceServer) MarkCrashReportRead(context.Context, *CrashReportRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method MarkCrashReportRead not implemented") +} + +func (UnimplementedDesktopServiceServer) ExportCrashReport(context.Context, *CrashReportExportRequest) (*CrashReportArchive, error) { + return nil, status.Error(codes.Unimplemented, "method ExportCrashReport not implemented") +} + +func (UnimplementedDesktopServiceServer) DeleteCrashReport(context.Context, *CrashReportRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteCrashReport not implemented") +} + +func (UnimplementedDesktopServiceServer) DeleteAllCrashReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteAllCrashReports not implemented") +} + +func (UnimplementedDesktopServiceServer) ListOOMReports(context.Context, *emptypb.Empty) (*OOMReportList, error) { + return nil, status.Error(codes.Unimplemented, "method ListOOMReports not implemented") +} + +func (UnimplementedDesktopServiceServer) ReadOOMReport(context.Context, *OOMReportRequest) (*OOMReportContent, error) { + return nil, status.Error(codes.Unimplemented, "method ReadOOMReport not implemented") +} + +func (UnimplementedDesktopServiceServer) MarkOOMReportRead(context.Context, *OOMReportRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method MarkOOMReportRead not implemented") +} + +func (UnimplementedDesktopServiceServer) ExportOOMReport(context.Context, *OOMReportExportRequest) (*CrashReportArchive, error) { + return nil, status.Error(codes.Unimplemented, "method ExportOOMReport not implemented") +} + +func (UnimplementedDesktopServiceServer) DeleteOOMReport(context.Context, *OOMReportRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteOOMReport not implemented") +} + +func (UnimplementedDesktopServiceServer) DeleteAllOOMReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteAllOOMReports not implemented") +} +func (UnimplementedDesktopServiceServer) mustEmbedUnimplementedDesktopServiceServer() {} +func (UnimplementedDesktopServiceServer) testEmbeddedByValue() {} + +// UnsafeDesktopServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DesktopServiceServer will +// result in compilation errors. +type UnsafeDesktopServiceServer interface { + mustEmbedUnimplementedDesktopServiceServer() +} + +func RegisterDesktopServiceServer(s grpc.ServiceRegistrar, srv DesktopServiceServer) { + // If the following call panics, it indicates UnimplementedDesktopServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DesktopService_ServiceDesc, srv) +} + +func _DesktopService_GetDaemonInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).GetDaemonInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_GetDaemonInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).GetDaemonInfo(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_ClaimService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).ClaimService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_ClaimService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).ClaimService(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_TakeOverService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).TakeOverService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_TakeOverService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).TakeOverService(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_StartService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StartServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).StartService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_StartService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).StartService(ctx, req.(*StartServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_GetWorkingDirectory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).GetWorkingDirectory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_GetWorkingDirectory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).GetWorkingDirectory(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_DestroyWorkingDirectory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).DestroyWorkingDirectory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_DestroyWorkingDirectory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).DestroyWorkingDirectory(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_ListCrashReports_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).ListCrashReports(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_ListCrashReports_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).ListCrashReports(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_ReadCrashReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CrashReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).ReadCrashReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_ReadCrashReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).ReadCrashReport(ctx, req.(*CrashReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_MarkCrashReportRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CrashReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).MarkCrashReportRead(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_MarkCrashReportRead_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).MarkCrashReportRead(ctx, req.(*CrashReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_ExportCrashReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CrashReportExportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).ExportCrashReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_ExportCrashReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).ExportCrashReport(ctx, req.(*CrashReportExportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_DeleteCrashReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CrashReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).DeleteCrashReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_DeleteCrashReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).DeleteCrashReport(ctx, req.(*CrashReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_DeleteAllCrashReports_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).DeleteAllCrashReports(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_DeleteAllCrashReports_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).DeleteAllCrashReports(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_ListOOMReports_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).ListOOMReports(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_ListOOMReports_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).ListOOMReports(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_ReadOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OOMReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).ReadOOMReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_ReadOOMReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).ReadOOMReport(ctx, req.(*OOMReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_MarkOOMReportRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OOMReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).MarkOOMReportRead(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_MarkOOMReportRead_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).MarkOOMReportRead(ctx, req.(*OOMReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_ExportOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OOMReportExportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).ExportOOMReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_ExportOOMReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).ExportOOMReport(ctx, req.(*OOMReportExportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_DeleteOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OOMReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).DeleteOOMReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_DeleteOOMReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).DeleteOOMReport(ctx, req.(*OOMReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_DeleteAllOOMReports_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).DeleteAllOOMReports(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_DeleteAllOOMReports_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).DeleteAllOOMReports(ctx, req.(*emptypb.Empty)) + } + 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) +var DesktopService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "desktop.DesktopService", + HandlerType: (*DesktopServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetDaemonInfo", + Handler: _DesktopService_GetDaemonInfo_Handler, + }, + { + MethodName: "ClaimService", + Handler: _DesktopService_ClaimService_Handler, + }, + { + MethodName: "TakeOverService", + Handler: _DesktopService_TakeOverService_Handler, + }, + { + MethodName: "StartService", + Handler: _DesktopService_StartService_Handler, + }, + { + MethodName: "GetWorkingDirectory", + Handler: _DesktopService_GetWorkingDirectory_Handler, + }, + { + MethodName: "DestroyWorkingDirectory", + Handler: _DesktopService_DestroyWorkingDirectory_Handler, + }, + { + MethodName: "ListCrashReports", + Handler: _DesktopService_ListCrashReports_Handler, + }, + { + MethodName: "ReadCrashReport", + Handler: _DesktopService_ReadCrashReport_Handler, + }, + { + MethodName: "MarkCrashReportRead", + Handler: _DesktopService_MarkCrashReportRead_Handler, + }, + { + MethodName: "ExportCrashReport", + Handler: _DesktopService_ExportCrashReport_Handler, + }, + { + MethodName: "DeleteCrashReport", + Handler: _DesktopService_DeleteCrashReport_Handler, + }, + { + MethodName: "DeleteAllCrashReports", + Handler: _DesktopService_DeleteAllCrashReports_Handler, + }, + { + MethodName: "ListOOMReports", + Handler: _DesktopService_ListOOMReports_Handler, + }, + { + MethodName: "ReadOOMReport", + Handler: _DesktopService_ReadOOMReport_Handler, + }, + { + MethodName: "MarkOOMReportRead", + Handler: _DesktopService_MarkOOMReportRead_Handler, + }, + { + MethodName: "ExportOOMReport", + Handler: _DesktopService_ExportOOMReport_Handler, + }, + { + MethodName: "DeleteOOMReport", + Handler: _DesktopService_DeleteOOMReport_Handler, + }, + { + MethodName: "DeleteAllOOMReports", + Handler: _DesktopService_DeleteAllOOMReports_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "experimental/boxdd/desktop_service.proto", +} + +const ( + ApplicationService_CheckConfig_FullMethodName = "/desktop.ApplicationService/CheckConfig" + ApplicationService_FormatConfig_FullMethodName = "/desktop.ApplicationService/FormatConfig" + ApplicationService_EncodeProfile_FullMethodName = "/desktop.ApplicationService/EncodeProfile" + ApplicationService_DecodeProfile_FullMethodName = "/desktop.ApplicationService/DecodeProfile" + ApplicationService_ArchiveReport_FullMethodName = "/desktop.ApplicationService/ArchiveReport" + ApplicationService_StartStandaloneNetworkQualityTest_FullMethodName = "/desktop.ApplicationService/StartStandaloneNetworkQualityTest" + ApplicationService_StartStandaloneSTUNTest_FullMethodName = "/desktop.ApplicationService/StartStandaloneSTUNTest" +) + +// ApplicationServiceClient is the client API for ApplicationService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ApplicationServiceClient interface { + CheckConfig(ctx context.Context, in *ConfigContent, opts ...grpc.CallOption) (*emptypb.Empty, error) + FormatConfig(ctx context.Context, in *ConfigContent, opts ...grpc.CallOption) (*ConfigContent, error) + EncodeProfile(ctx context.Context, in *ProfileContent, opts ...grpc.CallOption) (*ProfileData, error) + DecodeProfile(ctx context.Context, in *ProfileData, opts ...grpc.CallOption) (*ProfileContent, error) + ArchiveReport(ctx context.Context, in *ArchiveReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + StartStandaloneNetworkQualityTest(ctx context.Context, in *StandaloneNetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[daemon.NetworkQualityTestProgress], error) + StartStandaloneSTUNTest(ctx context.Context, in *StandaloneSTUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[daemon.STUNTestProgress], error) +} + +type applicationServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewApplicationServiceClient(cc grpc.ClientConnInterface) ApplicationServiceClient { + return &applicationServiceClient{cc} +} + +func (c *applicationServiceClient) CheckConfig(ctx context.Context, in *ConfigContent, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ApplicationService_CheckConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *applicationServiceClient) FormatConfig(ctx context.Context, in *ConfigContent, opts ...grpc.CallOption) (*ConfigContent, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ConfigContent) + err := c.cc.Invoke(ctx, ApplicationService_FormatConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *applicationServiceClient) EncodeProfile(ctx context.Context, in *ProfileContent, opts ...grpc.CallOption) (*ProfileData, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProfileData) + err := c.cc.Invoke(ctx, ApplicationService_EncodeProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *applicationServiceClient) DecodeProfile(ctx context.Context, in *ProfileData, opts ...grpc.CallOption) (*ProfileContent, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProfileContent) + err := c.cc.Invoke(ctx, ApplicationService_DecodeProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *applicationServiceClient) ArchiveReport(ctx context.Context, in *ArchiveReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ApplicationService_ArchiveReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *applicationServiceClient) StartStandaloneNetworkQualityTest(ctx context.Context, in *StandaloneNetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[daemon.NetworkQualityTestProgress], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ApplicationService_ServiceDesc.Streams[0], ApplicationService_StartStandaloneNetworkQualityTest_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[StandaloneNetworkQualityTestRequest, daemon.NetworkQualityTestProgress]{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 ApplicationService_StartStandaloneNetworkQualityTestClient = grpc.ServerStreamingClient[daemon.NetworkQualityTestProgress] + +func (c *applicationServiceClient) StartStandaloneSTUNTest(ctx context.Context, in *StandaloneSTUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[daemon.STUNTestProgress], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ApplicationService_ServiceDesc.Streams[1], ApplicationService_StartStandaloneSTUNTest_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[StandaloneSTUNTestRequest, daemon.STUNTestProgress]{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 ApplicationService_StartStandaloneSTUNTestClient = grpc.ServerStreamingClient[daemon.STUNTestProgress] + +// ApplicationServiceServer is the server API for ApplicationService service. +// All implementations must embed UnimplementedApplicationServiceServer +// for forward compatibility. +type ApplicationServiceServer interface { + CheckConfig(context.Context, *ConfigContent) (*emptypb.Empty, error) + FormatConfig(context.Context, *ConfigContent) (*ConfigContent, error) + EncodeProfile(context.Context, *ProfileContent) (*ProfileData, error) + DecodeProfile(context.Context, *ProfileData) (*ProfileContent, error) + ArchiveReport(context.Context, *ArchiveReportRequest) (*emptypb.Empty, error) + StartStandaloneNetworkQualityTest(*StandaloneNetworkQualityTestRequest, grpc.ServerStreamingServer[daemon.NetworkQualityTestProgress]) error + StartStandaloneSTUNTest(*StandaloneSTUNTestRequest, grpc.ServerStreamingServer[daemon.STUNTestProgress]) error + mustEmbedUnimplementedApplicationServiceServer() +} + +// UnimplementedApplicationServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedApplicationServiceServer struct{} + +func (UnimplementedApplicationServiceServer) CheckConfig(context.Context, *ConfigContent) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method CheckConfig not implemented") +} + +func (UnimplementedApplicationServiceServer) FormatConfig(context.Context, *ConfigContent) (*ConfigContent, error) { + return nil, status.Error(codes.Unimplemented, "method FormatConfig not implemented") +} + +func (UnimplementedApplicationServiceServer) EncodeProfile(context.Context, *ProfileContent) (*ProfileData, error) { + return nil, status.Error(codes.Unimplemented, "method EncodeProfile not implemented") +} + +func (UnimplementedApplicationServiceServer) DecodeProfile(context.Context, *ProfileData) (*ProfileContent, error) { + return nil, status.Error(codes.Unimplemented, "method DecodeProfile not implemented") +} + +func (UnimplementedApplicationServiceServer) ArchiveReport(context.Context, *ArchiveReportRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method ArchiveReport not implemented") +} + +func (UnimplementedApplicationServiceServer) StartStandaloneNetworkQualityTest(*StandaloneNetworkQualityTestRequest, grpc.ServerStreamingServer[daemon.NetworkQualityTestProgress]) error { + return status.Error(codes.Unimplemented, "method StartStandaloneNetworkQualityTest not implemented") +} + +func (UnimplementedApplicationServiceServer) StartStandaloneSTUNTest(*StandaloneSTUNTestRequest, grpc.ServerStreamingServer[daemon.STUNTestProgress]) error { + return status.Error(codes.Unimplemented, "method StartStandaloneSTUNTest not implemented") +} +func (UnimplementedApplicationServiceServer) mustEmbedUnimplementedApplicationServiceServer() {} +func (UnimplementedApplicationServiceServer) testEmbeddedByValue() {} + +// UnsafeApplicationServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ApplicationServiceServer will +// result in compilation errors. +type UnsafeApplicationServiceServer interface { + mustEmbedUnimplementedApplicationServiceServer() +} + +func RegisterApplicationServiceServer(s grpc.ServiceRegistrar, srv ApplicationServiceServer) { + // If the following call panics, it indicates UnimplementedApplicationServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ApplicationService_ServiceDesc, srv) +} + +func _ApplicationService_CheckConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConfigContent) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApplicationServiceServer).CheckConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ApplicationService_CheckConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApplicationServiceServer).CheckConfig(ctx, req.(*ConfigContent)) + } + return interceptor(ctx, in, info, handler) +} + +func _ApplicationService_FormatConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConfigContent) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApplicationServiceServer).FormatConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ApplicationService_FormatConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApplicationServiceServer).FormatConfig(ctx, req.(*ConfigContent)) + } + return interceptor(ctx, in, info, handler) +} + +func _ApplicationService_EncodeProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProfileContent) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApplicationServiceServer).EncodeProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ApplicationService_EncodeProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApplicationServiceServer).EncodeProfile(ctx, req.(*ProfileContent)) + } + return interceptor(ctx, in, info, handler) +} + +func _ApplicationService_DecodeProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProfileData) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApplicationServiceServer).DecodeProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ApplicationService_DecodeProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApplicationServiceServer).DecodeProfile(ctx, req.(*ProfileData)) + } + return interceptor(ctx, in, info, handler) +} + +func _ApplicationService_ArchiveReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ArchiveReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApplicationServiceServer).ArchiveReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ApplicationService_ArchiveReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApplicationServiceServer).ArchiveReport(ctx, req.(*ArchiveReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ApplicationService_StartStandaloneNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(StandaloneNetworkQualityTestRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ApplicationServiceServer).StartStandaloneNetworkQualityTest(m, &grpc.GenericServerStream[StandaloneNetworkQualityTestRequest, daemon.NetworkQualityTestProgress]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ApplicationService_StartStandaloneNetworkQualityTestServer = grpc.ServerStreamingServer[daemon.NetworkQualityTestProgress] + +func _ApplicationService_StartStandaloneSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(StandaloneSTUNTestRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ApplicationServiceServer).StartStandaloneSTUNTest(m, &grpc.GenericServerStream[StandaloneSTUNTestRequest, daemon.STUNTestProgress]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ApplicationService_StartStandaloneSTUNTestServer = grpc.ServerStreamingServer[daemon.STUNTestProgress] + +// ApplicationService_ServiceDesc is the grpc.ServiceDesc for ApplicationService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ApplicationService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "desktop.ApplicationService", + HandlerType: (*ApplicationServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CheckConfig", + Handler: _ApplicationService_CheckConfig_Handler, + }, + { + MethodName: "FormatConfig", + Handler: _ApplicationService_FormatConfig_Handler, + }, + { + MethodName: "EncodeProfile", + Handler: _ApplicationService_EncodeProfile_Handler, + }, + { + MethodName: "DecodeProfile", + Handler: _ApplicationService_DecodeProfile_Handler, + }, + { + MethodName: "ArchiveReport", + Handler: _ApplicationService_ArchiveReport_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "StartStandaloneNetworkQualityTest", + Handler: _ApplicationService_StartStandaloneNetworkQualityTest_Handler, + ServerStreams: true, + }, + { + StreamName: "StartStandaloneSTUNTest", + Handler: _ApplicationService_StartStandaloneSTUNTest_Handler, + ServerStreams: true, + }, + }, + Metadata: "experimental/boxdd/desktop_service.proto", +} diff --git a/experimental/boxdd/main.go b/experimental/boxdd/main.go new file mode 100644 index 000000000..7f79209f1 --- /dev/null +++ b/experimental/boxdd/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + "os" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +const serviceName = "sing-box-daemon" + +var mainCommand = &cobra.Command{ + Use: serviceName, + Version: C.Version, +} + +var commandVersion = &cobra.Command{ + Use: "version", + Short: "Print the daemon version", + Args: cobra.NoArgs, + Run: func(command *cobra.Command, args []string) { + fmt.Println("sing-box-daemon version", C.Version) + fmt.Println("core api version", daemon.APIVersion) + }, +} + +func init() { + mainCommand.AddCommand(commandVersion) +} + +func main() { + log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{ + BaseTime: time.Now(), + DisableColors: true, + }, os.Stderr, "", nil, false).Logger()) + err := mainCommand.Execute() + if err != nil { + log.Fatal(err) + } +} diff --git a/experimental/boxdd/managed.go b/experimental/boxdd/managed.go new file mode 100644 index 000000000..811bf07cb --- /dev/null +++ b/experimental/boxdd/managed.go @@ -0,0 +1,63 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/daemon" + E "github.com/sagernet/sing/common/exceptions" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var _ daemon.ManagedHandler = (*managedHandler)(nil) + +type managedHandler struct { + daemon *Daemon +} + +func (h *managedHandler) ServiceStop() error { + if h.daemon.closed { + return os.ErrClosed + } + options, err := loadStartOptions() + if err != nil { + return err + } + return h.daemon.stopServiceLocked(options.OwnerUserID) +} + +func (h *managedHandler) ServiceReload() error { + if h.daemon.closed { + return os.ErrClosed + } + configContent, err := loadServiceConfig() + if err != nil { + return err + } + options, err := loadStartOptions() + if err != nil { + return err + } + err = h.daemon.startService(configContent, options) + if err != nil { + return err + } + options.WasRunning = true + return saveStartOptions(options) +} + +func (h *managedHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) { + return &daemon.SystemProxyStatus{}, nil +} + +func (h *managedHandler) SetSystemProxyEnabled(enabled bool) error { + if !enabled { + return nil + } + return status.Error(codes.FailedPrecondition, "the system proxy is not available") +} + +func (h *managedHandler) TriggerNativeCrash() error { + return E.New("native crash is not supported") +} diff --git a/experimental/boxdd/oom_report.go b/experimental/boxdd/oom_report.go new file mode 100644 index 000000000..57365288b --- /dev/null +++ b/experimental/boxdd/oom_report.go @@ -0,0 +1,158 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "slices" + "sort" + "strings" + + "google.golang.org/protobuf/types/known/emptypb" +) + +const oomReportsDirectoryName = "oom_reports" + +// File order and the profile classification follow the client convention: +// sing-box-for-apple Library/Shared/OOMReportManager.swift (availableFiles) +// and Library/Shared/OOMReportArchive.swift (profileFiles). +var oomReportLeadingFileOrder = []string{metadataFileName, configSnapshotFileName, goLogFileName} + +func (s *desktopService) ListOOMReports(ctx context.Context, empty *emptypb.Empty) (*OOMReportList, error) { + reportsDirectory := filepath.Join(workingDirectory, oomReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + entries, err := os.ReadDir(reportsDirectory) + if err != nil { + if os.IsNotExist(err) { + return &OOMReportList{}, nil + } + return nil, err + } + reports := make([]*OOMReportEntry, 0, len(entries)) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + fullPath := filepath.Join(reportsDirectory, entry.Name()) + if !reportOwnedBy(fullPath, userID) { + continue + } + reports = append(reports, &OOMReportEntry{ + Name: entry.Name(), + RecordedAt: reportTime(fullPath, "recordedAt").UnixMilli(), + IsRead: reportIsRead(fullPath), + }) + } + sort.Slice(reports, func(i, j int) bool { + return reports[i].RecordedAt > reports[j].RecordedAt + }) + return &OOMReportList{Reports: reports}, nil +} + +func (s *desktopService) ReadOOMReport(ctx context.Context, request *OOMReportRequest) (*OOMReportContent, error) { + reportsDirectory := filepath.Join(workingDirectory, oomReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) + if err != nil { + return nil, err + } + files := make([]*OOMReportFile, 0, len(oomReportLeadingFileOrder)) + for _, fileName := range oomReportLeadingFileOrder { + content, readError := os.ReadFile(filepath.Join(fullPath, fileName)) + if readError != nil { + if os.IsNotExist(readError) { + continue + } + return nil, readError + } + files = append(files, &OOMReportFile{ + Name: fileName, + Content: content, + }) + } + entries, err := os.ReadDir(fullPath) + if err != nil { + return nil, err + } + profileNames := make([]string, 0, len(entries)) + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || strings.HasPrefix(name, ".") { + continue + } + if slices.Contains(oomReportLeadingFileOrder, name) { + continue + } + profileNames = append(profileNames, name) + } + sort.Strings(profileNames) + for _, name := range profileNames { + files = append(files, &OOMReportFile{ + Name: name, + IsProfile: true, + }) + } + return &OOMReportContent{Files: files}, nil +} + +func (s *desktopService) MarkOOMReportRead(ctx context.Context, request *OOMReportRequest) (*emptypb.Empty, error) { + reportsDirectory := filepath.Join(workingDirectory, oomReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) + if err != nil { + return nil, err + } + err = os.WriteFile(filepath.Join(fullPath, readMarkerFileName), nil, 0o600) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *desktopService) ExportOOMReport(ctx context.Context, request *OOMReportExportRequest) (*CrashReportArchive, error) { + reportsDirectory := filepath.Join(workingDirectory, oomReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + return exportReportArchive(reportsDirectory, request.Name, userID, request.WithConfiguration, request.WithLog, request.Encrypt) +} + +func (s *desktopService) DeleteOOMReport(ctx context.Context, request *OOMReportRequest) (*emptypb.Empty, error) { + reportsDirectory := filepath.Join(workingDirectory, oomReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID) + if err != nil { + return nil, err + } + err = os.RemoveAll(fullPath) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (s *desktopService) DeleteAllOOMReports(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { + reportsDirectory := filepath.Join(workingDirectory, oomReportsDirectoryName) + userID, err := s.daemon.reportCaller(ctx, reportsDirectory) + if err != nil { + return nil, err + } + err = deleteReportsForUser(reportsDirectory, userID) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} diff --git a/experimental/boxdd/peer.go b/experimental/boxdd/peer.go new file mode 100644 index 000000000..e771c4cba --- /dev/null +++ b/experimental/boxdd/peer.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "net" + + E "github.com/sagernet/sing/common/exceptions" + + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +type peerIdentity struct { + UserID string + ProcessID uint32 + SessionID uint32 +} + +type peerAuthInfo struct { + credentials.CommonAuthInfo + identity peerIdentity +} + +func (i *peerAuthInfo) AuthType() string { + return "local-process" +} + +func peerIdentityFromContext(ctx context.Context) (peerIdentity, error) { + peerInfo, loaded := peer.FromContext(ctx) + if !loaded || peerInfo.AuthInfo == nil { + return platformFallbackPeerIdentity(ctx) + } + authInfo, loaded := peerInfo.AuthInfo.(*peerAuthInfo) + if !loaded { + return peerIdentity{}, E.New("unexpected peer authentication type") + } + return authInfo.identity, nil +} + +var _ credentials.AuthInfo = (*peerAuthInfo)(nil) + +type peerConnection interface { + net.Conn + peerConnectionIdentity() peerIdentity +} diff --git a/experimental/boxdd/peer_stub.go b/experimental/boxdd/peer_stub.go new file mode 100644 index 000000000..c25f96ed7 --- /dev/null +++ b/experimental/boxdd/peer_stub.go @@ -0,0 +1,17 @@ +//go:build !windows + +package main + +import ( + "context" + + "google.golang.org/grpc" +) + +func platformServerOptions(daemon *Daemon) ([]grpc.ServerOption, error) { + return nil, nil +} + +func platformFallbackPeerIdentity(ctx context.Context) (peerIdentity, error) { + return peerIdentity{UserID: "local"}, nil +} diff --git a/experimental/boxdd/peer_windows.go b/experimental/boxdd/peer_windows.go new file mode 100644 index 000000000..eefff9724 --- /dev/null +++ b/experimental/boxdd/peer_windows.go @@ -0,0 +1,555 @@ +//go:build windows + +package main + +import ( + "bytes" + "context" + "errors" + "net" + "os" + "strconv" + "strings" + "sync" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" + + winioProcess "github.com/tailscale/go-winio/pkg/process" + "golang.org/x/sys/windows" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +const ( + daemonExecutableName = "sing-box-daemon.exe" + applicationExecutableName = "sing-box.exe" + workerPipePrefix = `\\.\pipe\sing-box-worker.` +) + +type windowsTransportCredentials struct { + daemon *Daemon + daemonSigner []byte + daemonExecutable windows.Handle + expectedWorkerPath string + expectedApplicationPath string +} + +type windowsAuthenticatedConnection struct { + net.Conn + daemon *Daemon + identity peerIdentity + process windows.Handle + processImage windows.Handle + parentProcess windows.Handle + parentProcessImage windows.Handle + close sync.Once + closeError error +} + +type fileDescriptorConnection interface { + Fd() uintptr +} + +func platformServerOptions(daemon *Daemon) ([]grpc.ServerOption, error) { + if listenAddress != "" { + return nil, nil + } + transportCredentials := &windowsTransportCredentials{daemon: daemon} + err := transportCredentials.initializeServerIdentity() + if err != nil { + return nil, err + } + return []grpc.ServerOption{grpc.Creds(transportCredentials)}, nil +} + +func platformFallbackPeerIdentity(ctx context.Context) (peerIdentity, error) { + return peerIdentity{}, E.New("missing Windows peer authentication") +} + +func (c *windowsTransportCredentials) ClientHandshake(ctx context.Context, authority string, rawConnection net.Conn) (net.Conn, credentials.AuthInfo, error) { + return nil, nil, E.New("Windows local process credentials do not support client handshakes") +} + +func (c *windowsTransportCredentials) ServerHandshake(rawConnection net.Conn) (net.Conn, credentials.AuthInfo, error) { + connection, authenticationInformation, err := c.serverHandshake(rawConnection) + if err != nil { + serviceLogError(E.Cause(err, "reject Windows daemon connection")) + } + return connection, authenticationInformation, err +} + +func (c *windowsTransportCredentials) serverHandshake(rawConnection net.Conn) (net.Conn, credentials.AuthInfo, error) { + descriptorConnection, loaded := rawConnection.(fileDescriptorConnection) + if !loaded { + return nil, nil, E.New("daemon endpoint is not a Windows named pipe") + } + var processID uint32 + err := windows.GetNamedPipeClientProcessId(windows.Handle(descriptorConnection.Fd()), &processID) + if err != nil { + return nil, nil, E.Cause(err, "identify named pipe client") + } + process, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.SYNCHRONIZE, false, processID) + if err != nil { + return nil, nil, E.Cause(err, "open named pipe client process") + } + keepProcess := false + defer func() { + if !keepProcess { + windows.CloseHandle(process) + } + }() + identity, err := processIdentity(process, processID) + if err != nil { + return nil, nil, err + } + workerImagePath, err := winioProcess.QueryFullProcessImageName(process, winioProcess.ImageNameFormatWin32Path) + if err != nil { + return nil, nil, E.Cause(err, "query named pipe client executable") + } + processImage, err := openLockedExecutable(workerImagePath) + if err != nil { + return nil, nil, E.Cause(err, "open named pipe client executable") + } + keepProcessImage := false + defer func() { + if !keepProcessImage { + windows.CloseHandle(processImage) + } + }() + processImageFinalPath, err := finalWindowsPath(processImage) + if err != nil { + return nil, nil, E.Cause(err, "resolve named pipe client executable") + } + if !strings.EqualFold(processImageFinalPath, c.expectedWorkerPath) { + return nil, nil, E.New("named pipe client is not the installed sing-box worker") + } + sameExecutable, err := sameWindowsFile(processImage, c.daemonExecutable) + if err != nil { + return nil, nil, err + } + if !sameExecutable { + return nil, nil, E.New("named pipe client worker executable was replaced") + } + workerSigner, err := authenticodeSigner(processImageFinalPath, processImage) + if err != nil { + return nil, nil, E.Cause(err, "authenticate sing-box worker") + } + if !bytes.Equal(workerSigner, c.daemonSigner) { + return nil, nil, E.New("sing-box worker and daemon have different signing certificates") + } + parentProcessID, err := processParentID(process) + if err != nil { + return nil, nil, err + } + err = validateWorkerProcessRole(process, parentProcessID) + if err != nil { + return nil, nil, err + } + parentProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.SYNCHRONIZE, false, parentProcessID) + if err != nil { + return nil, nil, E.Cause(err, "open sing-box worker parent process") + } + keepParentProcess := false + defer func() { + if !keepParentProcess { + windows.CloseHandle(parentProcess) + } + }() + parentIdentity, err := processIdentity(parentProcess, parentProcessID) + if err != nil { + return nil, nil, err + } + if parentIdentity.UserID != identity.UserID || parentIdentity.SessionID != identity.SessionID { + return nil, nil, E.New("sing-box worker and application have different process identities") + } + parentCreationTime, err := processCreationTime(parentProcess) + if err != nil { + return nil, nil, err + } + workerCreationTime, err := processCreationTime(process) + if err != nil { + return nil, nil, err + } + if parentCreationTime >= workerCreationTime { + return nil, nil, E.New("sing-box worker parent was created after the worker") + } + parentImagePath, err := winioProcess.QueryFullProcessImageName(parentProcess, winioProcess.ImageNameFormatWin32Path) + if err != nil { + return nil, nil, E.Cause(err, "query sing-box worker parent executable") + } + parentProcessImage, err := openLockedExecutable(parentImagePath) + if err != nil { + return nil, nil, E.Cause(err, "open sing-box worker parent executable") + } + keepParentProcessImage := false + defer func() { + if !keepParentProcessImage { + windows.CloseHandle(parentProcessImage) + } + }() + expectedApplication, err := openLockedExecutable(c.expectedApplicationPath) + if err != nil { + return nil, nil, E.Cause(err, "open installed application executable") + } + defer windows.CloseHandle(expectedApplication) + parentImageFinalPath, err := finalWindowsPath(parentProcessImage) + if err != nil { + return nil, nil, E.Cause(err, "resolve sing-box worker parent executable") + } + expectedApplicationFinalPath, err := finalWindowsPath(expectedApplication) + if err != nil { + return nil, nil, E.Cause(err, "resolve installed application executable") + } + if !strings.EqualFold(parentImageFinalPath, expectedApplicationFinalPath) { + return nil, nil, E.New("sing-box worker parent is not the installed application") + } + sameApplication, err := sameWindowsFile(parentProcessImage, expectedApplication) + if err != nil { + return nil, nil, err + } + if !sameApplication { + return nil, nil, E.New("sing-box worker parent executable was replaced") + } + err = validateApplicationProcessRole(parentProcess, expectedApplication) + if err != nil { + return nil, nil, err + } + applicationSigner, err := authenticodeSigner(parentImageFinalPath, parentProcessImage) + if err != nil { + return nil, nil, E.Cause(err, "authenticate sing-box application") + } + if !bytes.Equal(applicationSigner, c.daemonSigner) { + return nil, nil, E.New("sing-box application and daemon have different signing certificates") + } + workerWaitResult, err := windows.WaitForSingleObject(process, 0) + if err != nil { + return nil, nil, err + } + parentWaitResult, err := windows.WaitForSingleObject(parentProcess, 0) + if err != nil { + return nil, nil, err + } + if workerWaitResult != uint32(windows.WAIT_TIMEOUT) || parentWaitResult != uint32(windows.WAIT_TIMEOUT) { + return nil, nil, E.New("sing-box worker or application exited during authentication") + } + parentIdentity.ProcessID = parentProcessID + connection := &windowsAuthenticatedConnection{ + Conn: rawConnection, + daemon: c.daemon, + identity: parentIdentity, + process: process, + processImage: processImage, + parentProcess: parentProcess, + parentProcessImage: parentProcessImage, + } + keepProcess = true + keepProcessImage = true + keepParentProcess = true + keepParentProcessImage = true + c.daemon.registerPeerConnection(connection) + authenticationInformation := &peerAuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}, + identity: parentIdentity, + } + return connection, authenticationInformation, nil +} + +func (c *windowsTransportCredentials) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{ + SecurityProtocol: "windows-local-process", + SecurityVersion: "1", + } +} + +func (c *windowsTransportCredentials) Clone() credentials.TransportCredentials { + return &windowsTransportCredentials{ + daemon: c.daemon, + daemonSigner: c.daemonSigner, + daemonExecutable: c.daemonExecutable, + expectedWorkerPath: c.expectedWorkerPath, + expectedApplicationPath: c.expectedApplicationPath, + } +} + +func (c *windowsTransportCredentials) OverrideServerName(serverNameOverride string) error { + return nil +} + +func (c *windowsTransportCredentials) initializeServerIdentity() error { + executablePath, err := os.Executable() + if err != nil { + return E.Cause(err, "locate daemon executable") + } + executable, err := openLockedExecutable(executablePath) + if err != nil { + return E.Cause(err, "open daemon executable") + } + keepExecutable := false + defer func() { + if !keepExecutable { + windows.CloseHandle(executable) + } + }() + finalPath, err := finalWindowsPath(executable) + if err != nil { + return E.Cause(err, "resolve daemon executable") + } + _, applicationPath, err := installedApplicationPath(finalPath) + if err != nil { + return err + } + c.daemonExecutable = executable + c.expectedWorkerPath = finalPath + c.expectedApplicationPath = applicationPath + c.daemonSigner, err = authenticodeSigner(finalPath, executable) + if err != nil { + return E.Cause(err, "authenticate daemon executable") + } + keepExecutable = true + return nil +} + +func processIdentity(process windows.Handle, processID uint32) (peerIdentity, error) { + var token windows.Token + err := windows.OpenProcessToken(process, windows.TOKEN_QUERY, &token) + if err != nil { + return peerIdentity{}, E.Cause(err, "open named pipe client token") + } + defer token.Close() + user, err := token.GetTokenUser() + if err != nil { + return peerIdentity{}, E.Cause(err, "query named pipe client user") + } + userID := user.User.Sid.String() + if userID == "" { + return peerIdentity{}, E.New("named pipe client has an invalid user SID") + } + var sessionID uint32 + err = windows.ProcessIdToSessionId(processID, &sessionID) + if err != nil { + return peerIdentity{}, E.Cause(err, "query named pipe client session") + } + if sessionID == 0 { + return peerIdentity{}, E.New("named pipe client is not in an interactive session") + } + return peerIdentity{UserID: userID, ProcessID: processID, SessionID: sessionID}, nil +} + +func validateApplicationProcessRole(process windows.Handle, expectedApplication windows.Handle) error { + arguments, err := processCommandLine(process) + if err != nil { + return E.Cause(err, "query named pipe client command line") + } + for _, argument := range arguments[1:] { + normalizedArgument := strings.ToLower(argument) + if normalizedArgument == "--type" || strings.HasPrefix(normalizedArgument, "--type=") { + return E.New("named pipe client is an Electron child process") + } + } + applicationParent, err := processParentIsApplication(process, expectedApplication) + if err != nil { + return err + } + if applicationParent { + return E.New("named pipe client is a child of the sing-box application") + } + return nil +} + +func validateWorkerProcessRole(process windows.Handle, parentProcessID uint32) error { + arguments, err := processCommandLine(process) + if err != nil { + return E.Cause(err, "query sing-box worker command line") + } + if len(arguments) != 8 || + arguments[1] != "worker" || + arguments[2] != "--socket" || + arguments[4] != "--parent-pid" || + arguments[6] != "--daemon-relay-socket" { + return E.New("named pipe client is not a sing-box worker process") + } + if !strings.HasPrefix(strings.ToLower(arguments[3]), strings.ToLower(workerPipePrefix)) || + !strings.HasPrefix(strings.ToLower(arguments[7]), strings.ToLower(workerPipePrefix)) || + strings.EqualFold(arguments[3], arguments[7]) { + return E.New("sing-box worker has invalid private pipe paths") + } + commandParentProcessID, err := strconv.ParseUint(arguments[5], 10, 32) + if err != nil || uint32(commandParentProcessID) != parentProcessID { + return E.New("sing-box worker has an invalid parent process ID") + } + return nil +} + +func processCommandLine(process windows.Handle) ([]string, error) { + var bufferLength uint32 + queryError := windows.NtQueryInformationProcess( + process, + windows.ProcessCommandLineInformation, + nil, + 0, + &bufferLength, + ) + if bufferLength == 0 { + if queryError != nil { + return nil, queryError + } + return nil, E.New("named pipe client has an empty command line buffer") + } + buffer := make([]byte, bufferLength) + queryError = windows.NtQueryInformationProcess( + process, + windows.ProcessCommandLineInformation, + unsafe.Pointer(&buffer[0]), + uint32(len(buffer)), + &bufferLength, + ) + if queryError != nil { + return nil, queryError + } + commandLine := (*windows.NTUnicodeString)(unsafe.Pointer(&buffer[0])) + if commandLine.Buffer == nil || commandLine.Length == 0 || commandLine.Length%2 != 0 { + return nil, E.New("named pipe client has an invalid command line") + } + commandLineString := windows.UTF16ToString(unsafe.Slice(commandLine.Buffer, int(commandLine.Length/2))) + return windows.DecomposeCommandLine(commandLineString) +} + +func processParentIsApplication(process windows.Handle, expectedApplication windows.Handle) (bool, error) { + parentProcessID, err := processParentID(process) + if err != nil { + return false, err + } + if parentProcessID == 0 { + return false, nil + } + parentProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, parentProcessID) + if err != nil { + if errors.Is(err, windows.ERROR_INVALID_PARAMETER) { + return false, nil + } + return false, E.Cause(err, "open named pipe client parent") + } + defer windows.CloseHandle(parentProcess) + parentImagePath, err := winioProcess.QueryFullProcessImageName(parentProcess, winioProcess.ImageNameFormatWin32Path) + if err != nil { + return false, E.Cause(err, "query named pipe client parent executable") + } + parentImage, err := openLockedExecutable(parentImagePath) + if err != nil { + return false, err + } + defer windows.CloseHandle(parentImage) + return sameWindowsFile(parentImage, expectedApplication) +} + +func processParentID(process windows.Handle) (uint32, error) { + var processInformation windows.PROCESS_BASIC_INFORMATION + processInformationLength := uint32(unsafe.Sizeof(processInformation)) + err := windows.NtQueryInformationProcess( + process, + windows.ProcessBasicInformation, + unsafe.Pointer(&processInformation), + processInformationLength, + &processInformationLength, + ) + if err != nil { + return 0, E.Cause(err, "query named pipe client parent") + } + parentProcessID := uint32(processInformation.InheritedFromUniqueProcessId) + if parentProcessID == 0 || uintptr(parentProcessID) != processInformation.InheritedFromUniqueProcessId { + return 0, nil + } + return parentProcessID, nil +} + +func openLockedExecutable(path string) (windows.Handle, error) { + pathPointer, err := windows.UTF16PtrFromString(path) + if err != nil { + return 0, err + } + return windows.CreateFile( + pathPointer, + windows.GENERIC_READ, + windows.FILE_SHARE_READ, + nil, + windows.OPEN_EXISTING, + windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_SEQUENTIAL_SCAN, + 0, + ) +} + +func finalWindowsPath(file windows.Handle) (string, error) { + buffer := make([]uint16, windows.MAX_LONG_PATH) + for { + length, err := windows.GetFinalPathNameByHandle(file, &buffer[0], uint32(len(buffer)), 0) + if err != nil { + return "", err + } + if length < uint32(len(buffer)) { + return normalizeWindowsPath(windows.UTF16ToString(buffer[:length])), nil + } + buffer = make([]uint16, length+1) + } +} + +func normalizeWindowsPath(path string) string { + if strings.HasPrefix(path, `\\?\UNC\`) { + return `\\` + path[len(`\\?\UNC\`):] + } + return strings.TrimPrefix(path, `\\?\`) +} + +func sameWindowsFile(first windows.Handle, second windows.Handle) (bool, error) { + var firstInformation windows.ByHandleFileInformation + err := windows.GetFileInformationByHandle(first, &firstInformation) + if err != nil { + return false, E.Cause(err, "query named pipe client executable identity") + } + var secondInformation windows.ByHandleFileInformation + err = windows.GetFileInformationByHandle(second, &secondInformation) + if err != nil { + return false, E.Cause(err, "query installed application executable identity") + } + return firstInformation.VolumeSerialNumber == secondInformation.VolumeSerialNumber && + firstInformation.FileIndexHigh == secondInformation.FileIndexHigh && + firstInformation.FileIndexLow == secondInformation.FileIndexLow, nil +} + +func (c *windowsAuthenticatedConnection) peerConnectionIdentity() peerIdentity { + return c.identity +} + +func (d *Daemon) registerPeerConnection(connection peerConnection) { + d.peerAccess.Lock() + defer d.peerAccess.Unlock() + if d.peerConnections == nil { + d.peerConnections = make(map[peerConnection]peerIdentity) + } + d.peerConnections[connection] = connection.peerConnectionIdentity() +} + +func (d *Daemon) unregisterPeerConnection(connection peerConnection) { + d.peerAccess.Lock() + defer d.peerAccess.Unlock() + delete(d.peerConnections, connection) +} + +func (c *windowsAuthenticatedConnection) Close() error { + c.close.Do(func() { + c.daemon.unregisterPeerConnection(c) + c.closeError = E.Errors( + c.Conn.Close(), + windows.CloseHandle(c.parentProcessImage), + windows.CloseHandle(c.parentProcess), + windows.CloseHandle(c.processImage), + windows.CloseHandle(c.process), + ) + }) + return c.closeError +} + +var ( + _ credentials.TransportCredentials = (*windowsTransportCredentials)(nil) + _ peerConnection = (*windowsAuthenticatedConnection)(nil) +) diff --git a/experimental/boxdd/report.go b/experimental/boxdd/report.go new file mode 100644 index 000000000..5d5b5b961 --- /dev/null +++ b/experimental/boxdd/report.go @@ -0,0 +1,243 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "time" + + "github.com/sagernet/sing-box/experimental/libbox" + "github.com/sagernet/sing/common/rw" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + // File names within a report directory follow the client convention: + // sing-box-for-apple Library/Shared/CrashReportArchive.swift (ReportArchive) + // and sing-box experimental/libbox/report.go. + readMarkerFileName = ".read" + ownerMarkerFileName = ".owner" + metadataFileName = "metadata.json" + configSnapshotFileName = "configuration.json" + goLogFileName = "go.log" + nativeLogFileName = "native.log" +) + +func reportPath(reportsDirectory string, name string) (string, error) { + if !filepath.IsLocal(name) || name == "." || name != filepath.Base(name) { + return "", status.Error(codes.InvalidArgument, "invalid report name") + } + fullPath := filepath.Join(reportsDirectory, name) + info, err := os.Stat(fullPath) + if err != nil { + if os.IsNotExist(err) { + return "", status.Error(codes.NotFound, "report not found") + } + return "", err + } + if !info.IsDir() { + return "", status.Error(codes.NotFound, "report not found") + } + return fullPath, nil +} + +func reportPathForUser(reportsDirectory string, name string, userID string) (string, error) { + fullPath, err := reportPath(reportsDirectory, name) + if err != nil { + return "", err + } + if !reportOwnedBy(fullPath, userID) { + return "", status.Error(codes.NotFound, "report not found") + } + return fullPath, nil +} + +func reportOwnedBy(fullPath string, userID string) bool { + ownerContent, err := os.ReadFile(filepath.Join(fullPath, ownerMarkerFileName)) + return err == nil && strings.TrimSpace(string(ownerContent)) == userID +} + +func tagUnownedReports(reportsDirectory string, userID string) error { + if userID == "" { + return nil + } + entries, err := os.ReadDir(reportsDirectory) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + ownerPath := filepath.Join(reportsDirectory, entry.Name(), ownerMarkerFileName) + _, err = os.Stat(ownerPath) + if err == nil { + continue + } + if !os.IsNotExist(err) { + return err + } + err = os.WriteFile(ownerPath, []byte(userID+"\n"), 0o600) + if err != nil { + return err + } + } + return nil +} + +func (d *Daemon) reportCaller(ctx context.Context, reportsDirectory string) (string, error) { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return "", err + } + d.lifecycleAccess.Lock() + defer d.lifecycleAccess.Unlock() + options, err := loadStartOptions() + if err != nil && !os.IsNotExist(err) { + return "", err + } + err = tagUnownedReports(reportsDirectory, options.OwnerUserID) + if err != nil { + return "", err + } + return identity.UserID, nil +} + +func deleteReportsForUser(reportsDirectory string, userID string) error { + entries, err := os.ReadDir(reportsDirectory) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + fullPath := filepath.Join(reportsDirectory, entry.Name()) + if !reportOwnedBy(fullPath, userID) { + continue + } + err = os.RemoveAll(fullPath) + if err != nil { + return err + } + } + return nil +} + +func reportTime(fullPath string, timestampKey string) time.Time { + metadataContent, err := os.ReadFile(filepath.Join(fullPath, metadataFileName)) + if err == nil { + var metadata map[string]any + err = json.Unmarshal(metadataContent, &metadata) + if err == nil { + if timestamp, isString := metadata[timestampKey].(string); isString && timestamp != "" { + parsedTime, parseError := time.Parse(time.RFC3339, timestamp) + if parseError == nil { + return parsedTime + } + } + } + } + info, err := os.Stat(fullPath) + if err != nil { + return time.Time{} + } + return info.ModTime() +} + +func reportIsRead(fullPath string) bool { + _, err := os.Stat(filepath.Join(fullPath, readMarkerFileName)) + return err == nil +} + +func exportReportArchive(reportsDirectory string, name string, userID string, withConfiguration bool, withLog bool, encrypt bool) (*CrashReportArchive, error) { + fullPath, err := reportPathForUser(reportsDirectory, name, userID) + if err != nil { + return nil, err + } + tempRoot := filepath.Join(workingDirectory, "temp") + err = os.MkdirAll(tempRoot, 0o700) + if err != nil { + return nil, err + } + tempDirectory, err := os.MkdirTemp(tempRoot, "report-") + if err != nil { + return nil, err + } + defer os.RemoveAll(tempDirectory) + strippedPath := filepath.Join(tempDirectory, name) + err = copyDirectory(fullPath, strippedPath) + if err != nil { + return nil, err + } + os.Remove(filepath.Join(strippedPath, readMarkerFileName)) + os.Remove(filepath.Join(strippedPath, ownerMarkerFileName)) + if !withConfiguration { + os.Remove(filepath.Join(strippedPath, configSnapshotFileName)) + } + if !withLog { + os.Remove(filepath.Join(strippedPath, goLogFileName)) + os.Remove(filepath.Join(strippedPath, nativeLogFileName)) + } + fileName := name + ".zip" + if encrypt { + fileName += ".age" + } + archivePath := filepath.Join(tempDirectory, fileName) + err = libbox.CreateZipArchive(strippedPath, archivePath, encrypt) + if err != nil { + return nil, err + } + data, err := os.ReadFile(archivePath) + if err != nil { + return nil, err + } + return &CrashReportArchive{ + FileName: fileName, + Data: data, + }, nil +} + +func copyDirectory(sourcePath string, destinationPath string) error { + err := os.MkdirAll(destinationPath, 0o700) + if err != nil { + return err + } + entries, err := os.ReadDir(sourcePath) + if err != nil { + return err + } + for _, entry := range entries { + sourceEntryPath := filepath.Join(sourcePath, entry.Name()) + destinationEntryPath := filepath.Join(destinationPath, entry.Name()) + if entry.IsDir() { + err = copyDirectory(sourceEntryPath, destinationEntryPath) + if err != nil { + return err + } + continue + } + info, err := entry.Info() + if err != nil { + return err + } + if !info.Mode().IsRegular() { + return status.Error(codes.FailedPrecondition, "report contains a non-regular file") + } + err = rw.CopyFile(sourceEntryPath, destinationEntryPath) + if err != nil { + return err + } + } + return nil +} diff --git a/experimental/boxdd/security_windows.go b/experimental/boxdd/security_windows.go new file mode 100644 index 000000000..40cd92e2f --- /dev/null +++ b/experimental/boxdd/security_windows.go @@ -0,0 +1,515 @@ +//go:build windows + +package main + +import ( + "bytes" + "crypto/sha1" + "encoding/binary" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "unicode/utf16" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" + + "github.com/tailscale/go-winio" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/mgr" +) + +const ( + trustedInstallerUserID = "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464" + fileDeleteChildAccess = 0x00000040 +) + +func secureWindowsInstallation(executablePath string, allowUnsafeInstallation bool) (string, error) { + daemonExecutable, err := openLockedExecutable(executablePath) + if err != nil { + return "", err + } + defer windows.CloseHandle(daemonExecutable) + daemonPath, err := finalWindowsPath(daemonExecutable) + if err != nil { + return "", err + } + installationDirectory, applicationPath, err := installedApplicationPath(daemonPath) + if err != nil { + return "", err + } + applicationExecutable, err := openLockedExecutable(applicationPath) + if err != nil { + return "", E.Cause(err, "open installed application") + } + defer windows.CloseHandle(applicationExecutable) + daemonSigner, err := authenticodeSigner(daemonPath, daemonExecutable) + if err != nil { + return "", E.Cause(err, "authenticate installed daemon") + } + applicationFinalPath, err := finalWindowsPath(applicationExecutable) + if err != nil { + return "", err + } + applicationSigner, err := authenticodeSigner(applicationFinalPath, applicationExecutable) + if err != nil { + return "", E.Cause(err, "authenticate installed application") + } + if !bytes.Equal(daemonSigner, applicationSigner) { + return "", E.New("installed application and daemon have different signing certificates") + } + volumeRoot, err := validateFixedNTFSVolume(installationDirectory) + if err != nil { + return "", err + } + err = validateInstallationAncestors(filepath.Dir(installationDirectory), volumeRoot, !allowUnsafeInstallation) + if err != nil { + return "", err + } + err = validateTreeHasNoReparsePoints(installationDirectory) + if err != nil { + return "", err + } + err = applyProtectedTree( + installationDirectory, + "O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;GRGX;;;AU)", + "O:SYG:SYD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;GRGX;;;AU)", + ) + if err != nil { + return "", err + } + return daemonPath, nil +} + +func installedApplicationPath(daemonPath string) (string, string, error) { + daemonDirectory := filepath.Dir(daemonPath) + resourcesDirectory := filepath.Dir(daemonDirectory) + installationDirectory := filepath.Dir(resourcesDirectory) + if !strings.EqualFold(filepath.Base(daemonPath), daemonExecutableName) || + !strings.EqualFold(filepath.Base(daemonDirectory), "daemon") || + !strings.EqualFold(filepath.Base(resourcesDirectory), "resources") { + return "", "", E.New("daemon executable is outside the installed sing-box layout") + } + return installationDirectory, filepath.Join(installationDirectory, applicationExecutableName), nil +} + +func secureWindowsWorkingDirectory(path string) error { + serviceUserID, err := windowsServiceSID() + if err != nil { + return E.Cause(err, "create daemon service SID") + } + serviceUserIDString := serviceUserID.String() + if serviceUserIDString == "" { + return E.New("daemon service has an invalid SID") + } + err = validateTreeHasNoReparsePoints(path) + if err != nil { + return err + } + directoryDescriptor := fmt.Sprintf( + "O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;%s)", + serviceUserIDString, + ) + fileDescriptor := fmt.Sprintf( + "O:SYG:SYD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;%s)", + serviceUserIDString, + ) + return applyProtectedTree(path, directoryDescriptor, fileDescriptor) +} + +func windowsServiceSID() (*windows.SID, error) { + serviceNameUTF16 := utf16.Encode([]rune(strings.ToUpper(serviceName))) + serviceNameContent := make([]byte, len(serviceNameUTF16)*2) + for index, codeUnit := range serviceNameUTF16 { + binary.LittleEndian.PutUint16(serviceNameContent[index*2:], codeUnit) + } + serviceNameHash := sha1.Sum(serviceNameContent) + return windows.StringToSid(fmt.Sprintf( + "S-1-5-80-%d-%d-%d-%d-%d", + binary.LittleEndian.Uint32(serviceNameHash[0:4]), + binary.LittleEndian.Uint32(serviceNameHash[4:8]), + binary.LittleEndian.Uint32(serviceNameHash[8:12]), + binary.LittleEndian.Uint32(serviceNameHash[12:16]), + binary.LittleEndian.Uint32(serviceNameHash[16:20]), + )) +} + +func validateProtectedWindowsWorkingDirectory(path string, serviceUserID *windows.SID) error { + attributes, err := windowsFileAttributes(path) + if err != nil { + return err + } + if attributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 { + return E.New("daemon working directory path is not a directory") + } + if attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return E.New("daemon working directory is a reparse point") + } + descriptor, err := windows.GetNamedSecurityInfo( + path, + windows.SE_FILE_OBJECT, + windows.OWNER_SECURITY_INFORMATION|windows.GROUP_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION, + ) + if err != nil { + return err + } + owner, _, err := descriptor.Owner() + if err != nil { + return err + } + systemUserID, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid) + if err != nil { + return err + } + if !windows.EqualSid(owner, systemUserID) { + return E.New("daemon working directory is not owned by SYSTEM") + } + control, _, err := descriptor.Control() + if err != nil { + return err + } + if control&windows.SE_DACL_PROTECTED == 0 { + return E.New("daemon working directory access control is inherited") + } + discretionaryAccessControlList, _, err := descriptor.DACL() + if err != nil { + return err + } + if discretionaryAccessControlList == nil || discretionaryAccessControlList.AceCount != 3 { + return E.New("daemon working directory has unexpected access control entries") + } + administratorsUserID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) + if err != nil { + return err + } + expectedUsers := map[string]bool{ + systemUserID.String(): false, + administratorsUserID.String(): false, + serviceUserID.String(): false, + } + for index := uint32(0); index < uint32(discretionaryAccessControlList.AceCount); index++ { + var accessControlEntry *windows.ACCESS_ALLOWED_ACE + err = windows.GetAce(discretionaryAccessControlList, index, &accessControlEntry) + if err != nil { + return err + } + if accessControlEntry.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE || + accessControlEntry.Header.AceFlags != windows.OBJECT_INHERIT_ACE|windows.CONTAINER_INHERIT_ACE || + uint32(accessControlEntry.Mask) != 0x001F01FF { + return E.New("daemon working directory has an unsafe access control entry") + } + userID := (*windows.SID)(unsafe.Pointer(&accessControlEntry.SidStart)).String() + seen, exists := expectedUsers[userID] + if !exists || seen { + return E.New("daemon working directory grants access to an unexpected principal") + } + expectedUsers[userID] = true + } + for _, seen := range expectedUsers { + if !seen { + return E.New("daemon working directory is missing a required access control entry") + } + } + return nil +} + +func applyProtectedServiceSecurity(service *mgr.Service) error { + descriptor, err := windows.SecurityDescriptorFromString( + "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x2008d;;;AU)", + ) + if err != nil { + return err + } + discretionaryAccessControlList, _, err := descriptor.DACL() + if err != nil { + return err + } + return windows.SetSecurityInfo( + service.Handle, + windows.SE_SERVICE, + windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, + nil, + nil, + discretionaryAccessControlList, + nil, + ) +} + +func allowAuthenticatedUsersToQueryCurrentProcess() error { + descriptor, err := windows.SecurityDescriptorFromString( + "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x101000;;;AU)", + ) + if err != nil { + return err + } + discretionaryAccessControlList, _, err := descriptor.DACL() + if err != nil { + return err + } + return windows.SetSecurityInfo( + windows.CurrentProcess(), + windows.SE_KERNEL_OBJECT, + windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, + nil, + nil, + discretionaryAccessControlList, + nil, + ) +} + +func validateFixedNTFSVolume(path string) (string, error) { + pathPointer, err := windows.UTF16PtrFromString(path) + if err != nil { + return "", err + } + volumePathBuffer := make([]uint16, windows.MAX_LONG_PATH) + err = windows.GetVolumePathName(pathPointer, &volumePathBuffer[0], uint32(len(volumePathBuffer))) + if err != nil { + return "", E.Cause(err, "resolve installation volume") + } + volumePath := windows.UTF16ToString(volumePathBuffer) + volumePathPointer, err := windows.UTF16PtrFromString(volumePath) + if err != nil { + return "", err + } + if windows.GetDriveType(volumePathPointer) != windows.DRIVE_FIXED { + return "", E.New("sing-box must be installed on a fixed local drive") + } + fileSystemNameBuffer := make([]uint16, 32) + err = windows.GetVolumeInformation( + volumePathPointer, + nil, + 0, + nil, + nil, + nil, + &fileSystemNameBuffer[0], + uint32(len(fileSystemNameBuffer)), + ) + if err != nil { + return "", E.Cause(err, "query installation file system") + } + if !strings.EqualFold(windows.UTF16ToString(fileSystemNameBuffer), "NTFS") { + return "", E.New("sing-box must be installed on NTFS") + } + return filepath.Clean(volumePath), nil +} + +func validateInstallationAncestors(path string, volumeRoot string, validatePermissions bool) error { + currentPath := filepath.Clean(path) + cleanVolumeRoot := filepath.Clean(volumeRoot) + for { + err := validateInstallationAncestor(currentPath, validatePermissions) + if err != nil { + return err + } + if strings.EqualFold(currentPath, cleanVolumeRoot) { + return nil + } + parentPath := filepath.Dir(currentPath) + if parentPath == currentPath { + return E.New("installation path is outside its resolved volume") + } + currentPath = parentPath + } +} + +func validateInstallationAncestor(path string, validatePermissions bool) error { + attributes, err := windowsFileAttributes(path) + if err != nil { + return err + } + if attributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 { + return E.New("installation ancestor is not a directory: ", path) + } + if attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return E.New("installation ancestor is a reparse point: ", path) + } + if !validatePermissions { + return nil + } + descriptor, err := windows.GetNamedSecurityInfo( + path, + windows.SE_FILE_OBJECT, + windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION, + ) + if err != nil { + return E.Cause(err, "query installation ancestor security") + } + owner, _, err := descriptor.Owner() + if err != nil { + return err + } + if !trustedAdministrativeUser(owner) { + return E.New("installation ancestor is owned by an unprivileged principal: ", path) + } + discretionaryAccessControlList, _, err := descriptor.DACL() + if err != nil { + return err + } + if discretionaryAccessControlList == nil { + return E.New("installation ancestor has an empty access control list: ", path) + } + for index := uint32(0); index < uint32(discretionaryAccessControlList.AceCount); index++ { + var accessControlEntry *windows.ACCESS_ALLOWED_ACE + err = windows.GetAce(discretionaryAccessControlList, index, &accessControlEntry) + if err != nil { + return err + } + if accessControlEntry.Header.AceFlags&windows.INHERIT_ONLY_ACE != 0 { + continue + } + if accessControlEntry.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE { + continue + } + mask := uint32(accessControlEntry.Mask) + dangerousAccess := uint32(windows.DELETE | windows.WRITE_DAC | windows.WRITE_OWNER | windows.GENERIC_WRITE | windows.GENERIC_ALL | fileDeleteChildAccess) + if mask&dangerousAccess == 0 { + continue + } + principal := (*windows.SID)(unsafe.Pointer(&accessControlEntry.SidStart)) + if !trustedAdministrativeUser(principal) { + return E.New("installation ancestor is replaceable by an unprivileged principal: ", path) + } + } + return nil +} + +func trustedAdministrativeUser(userID *windows.SID) bool { + if userID == nil { + return false + } + return userID.IsWellKnown(windows.WinLocalSystemSid) || + userID.IsWellKnown(windows.WinBuiltinAdministratorsSid) || + userID.String() == trustedInstallerUserID +} + +func validateTreeHasNoReparsePoints(root string) error { + return filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkError error) error { + if walkError != nil { + return walkError + } + attributes, err := windowsFileAttributes(path) + if err != nil { + return err + } + if attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return E.New("protected tree contains a reparse point: ", path) + } + return nil + }) +} + +func applyProtectedTree(root string, directorySecurityDescriptor string, fileSecurityDescriptor string) error { + return winio.RunWithPrivilege(winio.SeRestorePrivilege, func() error { + directoryDescriptor, err := windows.SecurityDescriptorFromString(directorySecurityDescriptor) + if err != nil { + return err + } + fileDescriptor, err := windows.SecurityDescriptorFromString(fileSecurityDescriptor) + if err != nil { + return err + } + return filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkError error) error { + if walkError != nil { + return walkError + } + descriptor := fileDescriptor + if entry.IsDir() { + descriptor = directoryDescriptor + } + err = applyProtectedFileSecurity(path, descriptor) + if err != nil { + return E.Cause(err, "secure ", path) + } + return nil + }) + }) +} + +func applyProtectedFileSecurity(path string, descriptor *windows.SECURITY_DESCRIPTOR) error { + owner, _, err := descriptor.Owner() + if err != nil { + return err + } + group, _, err := descriptor.Group() + if err != nil { + return err + } + discretionaryAccessControlList, _, err := descriptor.DACL() + if err != nil { + return err + } + return windows.SetNamedSecurityInfo( + path, + windows.SE_FILE_OBJECT, + windows.OWNER_SECURITY_INFORMATION| + windows.GROUP_SECURITY_INFORMATION| + windows.DACL_SECURITY_INFORMATION| + windows.PROTECTED_DACL_SECURITY_INFORMATION, + owner, + group, + discretionaryAccessControlList, + nil, + ) +} + +func windowsFileAttributes(path string) (uint32, error) { + pathPointer, err := windows.UTF16PtrFromString(path) + if err != nil { + return 0, err + } + return windows.GetFileAttributes(pathPointer) +} + +func ensureWindowsWorkingDirectory(path string) error { + serviceUserID, err := windowsServiceSID() + if err != nil { + return E.Cause(err, "create daemon service SID") + } + created := false + _, err = os.Lstat(path) + if os.IsNotExist(err) { + serviceUserIDString := serviceUserID.String() + if serviceUserIDString == "" { + return E.New("daemon service has an invalid SID") + } + descriptor, descriptorError := windows.SecurityDescriptorFromString( + fmt.Sprintf("D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;%s)", serviceUserIDString), + ) + if descriptorError != nil { + return descriptorError + } + pathPointer, pathError := windows.UTF16PtrFromString(path) + if pathError != nil { + return pathError + } + securityAttributes := &windows.SecurityAttributes{ + Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), + SecurityDescriptor: descriptor, + } + err = windows.CreateDirectory(pathPointer, securityAttributes) + if err != nil { + return E.Cause(err, "create protected daemon working directory") + } + created = true + } else if err != nil { + return err + } + if created { + err = secureWindowsWorkingDirectory(path) + if err != nil { + return err + } + } + err = validateProtectedWindowsWorkingDirectory(path, serviceUserID) + if err != nil { + return err + } + if created { + return nil + } + return secureWindowsWorkingDirectory(path) +} diff --git a/experimental/boxdd/server.go b/experimental/boxdd/server.go new file mode 100644 index 000000000..32cb4d414 --- /dev/null +++ b/experimental/boxdd/server.go @@ -0,0 +1,343 @@ +package main + +import ( + "context" + "errors" + "net" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing-box/experimental/libbox" + "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/service/oomkiller" + "github.com/sagernet/sing/service" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/status" +) + +const daemonCrashOutputFileName = "CrashReport-Daemon.log" + +type Daemon struct { + logger log.ContextLogger + startedService *daemon.StartedService + server *grpc.Server + listenerPath string + lifecycleAccess sync.Mutex + closed bool + peerAccess sync.Mutex + peerConnections map[peerConnection]peerIdentity +} + +func newDaemon() (*Daemon, error) { + ctx := include.Context(context.Background()) + d := &Daemon{ + logger: log.StdLogger(), + } + d.startedService = daemon.NewStartedService(daemon.ServiceOptions{ + Context: ctx, + LogMaxLines: 3000, + }) + reporter := libbox.NewOOMReporter(d.startedService) + service.MustRegister[oomkiller.OOMReporter](ctx, reporter) + managedService := daemon.NewManagedService(daemon.ManagedServiceOptions{ + Handler: &managedHandler{d}, + Debug: debugEnabled, + OOMReporter: reporter, + }) + authorizer := newAuthorizer(d) + serverOptions := []grpc.ServerOption{ + grpc.ChainUnaryInterceptor(newUnaryAuthorizeInterceptor(authorizer), daemon.UnaryErrorInterceptor), + grpc.ChainStreamInterceptor(newStreamAuthorizeInterceptor(authorizer), daemon.StreamErrorInterceptor), + } + platformOptions, err := platformServerOptions(d) + if err != nil { + return nil, err + } + serverOptions = append(serverOptions, platformOptions...) + d.server = grpc.NewServer(serverOptions...) + daemon.RegisterStartedServiceServer(d.server, d.startedService) + daemon.RegisterManagedServiceServer(d.server, managedService) + RegisterDesktopServiceServer(d.server, &desktopService{daemon: d}) + healthServer := health.NewServer() + healthServer.SetServingStatus(daemon.StartedService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + healthServer.SetServingStatus(daemon.ManagedService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + healthServer.SetServingStatus(DesktopService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer(d.server, healthServer) + if listenAddress != "" { + reflection.Register(d.server) + } + return d, nil +} + +func (d *Daemon) listen() (net.Listener, error) { + if listenAddress != "" { + d.logger.Warn("listening on TCP address ", listenAddress, ": development only, no access control") + return net.Listen("tcp", listenAddress) + } + return listenEndpoint() +} + +func (d *Daemon) Start() error { + listener, err := d.listen() + if err != nil { + return err + } + if listener.Addr().Network() == "unix" { + d.listenerPath, err = filepath.Abs(listener.Addr().String()) + if err != nil { + listener.Close() + return err + } + } + d.logger.Info("daemon listening at ", listener.Addr()) + go func() { + serveError := d.server.Serve(listener) + if serveError != nil && !errors.Is(serveError, grpc.ErrServerStopped) { + d.logger.Error("serve: ", serveError) + } + }() + go d.restore() + return nil +} + +func (d *Daemon) restore() { + d.lifecycleAccess.Lock() + defer d.lifecycleAccess.Unlock() + if d.closed { + return + } + options, err := loadStartOptions() + if err != nil { + if !os.IsNotExist(err) { + d.logger.Warn("load start options: ", err) + } + return + } + err = tagUnownedReports(filepath.Join(workingDirectory, crashReportsDirectoryName), options.OwnerUserID) + if err != nil { + d.logger.Warn("tag crash reports: ", err) + } + err = tagUnownedReports(filepath.Join(workingDirectory, oomReportsDirectoryName), options.OwnerUserID) + if err != nil { + d.logger.Warn("tag OOM reports: ", err) + } + if !options.WasRunning { + return + } + configContent, err := loadServiceConfig() + if err != nil { + d.logger.Error("restore service: ", err) + return + } + d.logger.Info("restoring service") + err = d.startService(configContent, options) + if err != nil { + d.logger.Error("restore service: ", err) + } +} + +func (d *Daemon) startService(configContent string, options startOptions) error { + _ = os.WriteFile(filepath.Join(workingDirectory, configSnapshotFileName), []byte(configContent), 0o600) + libbox.ReloadSetupOptions(&libbox.SetupOptions{ + OomKillerEnabled: options.OOMKillerEnabled, + OomKillerDisabled: options.OOMKillerDisabled, + OomMemoryLimit: options.OOMMemoryLimit, + }) + d.startedService.SetOOMKillerOptions(options.OOMKillerEnabled, options.OOMKillerDisabled, uint64(options.OOMMemoryLimit)) + return d.startedService.StartOrReloadService(configContent, nil) +} + +func (d *Daemon) clearRuntimeData() error { + entries, err := os.ReadDir(workingDirectory) + if err != nil { + return err + } + for _, entry := range entries { + if entry.Name() == crashReportsDirectoryName || + entry.Name() == oomReportsDirectoryName || + entry.Name() == daemonCrashOutputFileName { + continue + } + entryPath := filepath.Join(workingDirectory, entry.Name()) + if d.listenerPath != "" && entryPath == d.listenerPath { + continue + } + err = os.RemoveAll(entryPath) + if err != nil { + return err + } + } + return nil +} + +func (d *Daemon) resetRuntimeOwnerLocked(ownerUserID string) error { + err := d.clearRuntimeData() + if err != nil { + return err + } + return saveStartOptions(startOptions{OwnerUserID: ownerUserID}) +} + +func (d *Daemon) stopServiceLocked(nextOwnerUserID string) error { + options, err := loadStartOptions() + if err != nil && !os.IsNotExist(err) { + return err + } + if d.startedService.Instance() != nil { + err = d.startedService.CloseService() + if err != nil { + return err + } + } + crashReportError := tagUnownedReports(filepath.Join(workingDirectory, crashReportsDirectoryName), options.OwnerUserID) + if crashReportError != nil { + return crashReportError + } + oomReportError := tagUnownedReports(filepath.Join(workingDirectory, oomReportsDirectoryName), options.OwnerUserID) + if oomReportError != nil { + return oomReportError + } + return d.resetRuntimeOwnerLocked(nextOwnerUserID) +} + +func (d *Daemon) Close() { + d.lifecycleAccess.Lock() + d.closed = true + d.lifecycleAccess.Unlock() + d.server.Stop() + d.lifecycleAccess.Lock() + _ = d.startedService.CloseService() + d.startedService.Close() + d.lifecycleAccess.Unlock() +} + +func (d *Daemon) disconnectPeerConnectionsExcept(userID string) { + d.peerAccess.Lock() + var connections []peerConnection + for connection, identity := range d.peerConnections { + if identity.UserID != userID { + connections = append(connections, connection) + } + } + d.peerAccess.Unlock() + for _, connection := range connections { + connection.Close() + } +} + +type Authorizer interface { + Authorize(ctx context.Context, method string) error + InvokeUnary(ctx context.Context, method string, handler func() (any, error)) (any, error) +} + +func newAuthorizer(daemon *Daemon) Authorizer { + if listenAddress != "" { + return &allowAllAuthorizer{daemon: daemon} + } + return &daemonAuthorizer{daemon: daemon} +} + +type allowAllAuthorizer struct { + daemon *Daemon +} + +func (a *allowAllAuthorizer) Authorize(ctx context.Context, method string) error { + return nil +} + +func (a *allowAllAuthorizer) InvokeUnary(ctx context.Context, method string, handler func() (any, error)) (any, error) { + if ownerProtectedMethod(method) { + a.daemon.lifecycleAccess.Lock() + defer a.daemon.lifecycleAccess.Unlock() + } + return handler() +} + +type daemonAuthorizer struct { + daemon *Daemon +} + +func (a *daemonAuthorizer) Authorize(ctx context.Context, method string) error { + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return status.Error(codes.Unauthenticated, err.Error()) + } + desktopPrefix := "/" + DesktopService_ServiceDesc.ServiceName + "/" + if strings.HasPrefix(method, desktopPrefix) { + return nil + } + if ownerProtectedMethod(method) { + a.daemon.lifecycleAccess.Lock() + defer a.daemon.lifecycleAccess.Unlock() + return a.daemon.authorizeOwnerLocked(identity.UserID) + } + return status.Error(codes.PermissionDenied, "the service is not available") +} + +func (a *daemonAuthorizer) InvokeUnary(ctx context.Context, method string, handler func() (any, error)) (any, error) { + if !ownerProtectedMethod(method) { + err := a.Authorize(ctx, method) + if err != nil { + return nil, err + } + return handler() + } + identity, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Unauthenticated, err.Error()) + } + a.daemon.lifecycleAccess.Lock() + defer a.daemon.lifecycleAccess.Unlock() + err = a.daemon.authorizeOwnerLocked(identity.UserID) + if err != nil { + return nil, err + } + return handler() +} + +func ownerProtectedMethod(method string) bool { + startedPrefix := "/" + daemon.StartedService_ServiceDesc.ServiceName + "/" + managedPrefix := "/" + daemon.ManagedService_ServiceDesc.ServiceName + "/" + return strings.HasPrefix(method, startedPrefix) || strings.HasPrefix(method, managedPrefix) +} + +func (d *Daemon) authorizeOwnerLocked(userID string) error { + options, err := loadStartOptions() + if err != nil { + if os.IsNotExist(err) { + return status.Error(codes.PermissionDenied, "the service has no owner") + } + return err + } + if options.OwnerUserID == "" || options.OwnerUserID != userID { + return status.Error(codes.PermissionDenied, "the service is owned by another user") + } + return nil +} + +func newUnaryAuthorizeInterceptor(authorizer Authorizer) grpc.UnaryServerInterceptor { + return func(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return authorizer.InvokeUnary(ctx, info.FullMethod, func() (any, error) { + return handler(ctx, request) + }) + } +} + +func newStreamAuthorizeInterceptor(authorizer Authorizer) grpc.StreamServerInterceptor { + return func(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + err := authorizer.Authorize(stream.Context(), info.FullMethod) + if err != nil { + return err + } + return handler(server, stream) + } +} diff --git a/experimental/boxdd/server_unix.go b/experimental/boxdd/server_unix.go new file mode 100644 index 000000000..72bff9649 --- /dev/null +++ b/experimental/boxdd/server_unix.go @@ -0,0 +1,30 @@ +//go:build !windows + +package main + +import ( + "net" + "os" + "path/filepath" +) + +func listenEndpoint() (net.Listener, error) { + path := socketPath + if path == "" { + path = filepath.Join(workingDirectory, serviceName+".sock") + } + err := os.Remove(path) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + listener, err := net.Listen("unix", path) + if err != nil { + return nil, err + } + err = os.Chmod(path, 0o666) + if err != nil { + listener.Close() + return nil, err + } + return listener, nil +} diff --git a/experimental/boxdd/server_windows.go b/experimental/boxdd/server_windows.go new file mode 100644 index 000000000..0adfdca24 --- /dev/null +++ b/experimental/boxdd/server_windows.go @@ -0,0 +1,38 @@ +package main + +import ( + "net" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + + "github.com/tailscale/go-winio" +) + +// libuv (Node net.connect) opens the client end with GENERIC_READ|GENERIC_WRITE and +// falls back to read-only/write-only opens on ERROR_ACCESS_DENIED (src/win/pipe.c, +// open_named_pipe), so the client principal needs the full GRGW grant. +const pipeSecurityDescriptor = `D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)` + +// winio's PipeConfig defaults to zero-quota pipe instances, where NPFS pends every +// WriteFile until the peer posts a consuming ReadFile. Node's http2 client never +// posts one when it opens a pipe immediately after destroying a previous pipe +// socket (the renderer host's deadline-abort-then-retry pattern), which deadlocks +// the gRPC handshake on the server's initial SETTINGS write and surfaces in the +// app as a connect timeout. Nonzero quotas let the handshake complete into the +// pipe buffer. +const ( + pipeBufferSize = 65536 + daemonPipePath = `\\.\pipe\ProtectedPrefix\Administrators\sing-box` +) + +func listenEndpoint() (net.Listener, error) { + if socketPath != "" && !strings.EqualFold(socketPath, daemonPipePath) { + return nil, E.New("custom Windows daemon pipe paths are not supported") + } + return winio.ListenPipe(daemonPipePath, &winio.PipeConfig{ + SecurityDescriptor: pipeSecurityDescriptor, + InputBufferSize: pipeBufferSize, + OutputBufferSize: pipeBufferSize, + }) +} diff --git a/experimental/boxdd/snapshot.go b/experimental/boxdd/snapshot.go new file mode 100644 index 000000000..b48eb9bbb --- /dev/null +++ b/experimental/boxdd/snapshot.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "path/filepath" + + "github.com/sagernet/sing/common/json" + "github.com/sagernet/tailscale/atomicfile" +) + +const ( + serviceConfigFileName = "config.json" + startOptionsFileName = "start_options.json" +) + +type startOptions struct { + WasRunning bool `json:"was_running"` + OwnerUserID string `json:"owner_user_id"` + OOMKillerEnabled bool `json:"oom_killer_enabled"` + OOMKillerDisabled bool `json:"oom_killer_disabled"` + OOMMemoryLimit int64 `json:"oom_memory_limit"` +} + +func loadServiceConfig() (string, error) { + content, err := os.ReadFile(filepath.Join(workingDirectory, serviceConfigFileName)) + if err != nil { + return "", err + } + return string(content), nil +} + +func loadStartOptions() (startOptions, error) { + content, err := os.ReadFile(filepath.Join(workingDirectory, startOptionsFileName)) + if err != nil { + return startOptions{}, err + } + options, err := json.UnmarshalExtended[startOptions](content) + if err != nil { + return startOptions{}, err + } + return options, nil +} + +func saveStartOptions(options startOptions) error { + content, err := json.Marshal(options) + if err != nil { + return err + } + return atomicfile.WriteFile(filepath.Join(workingDirectory, startOptionsFileName), content, 0o600) +}