diff --git a/adapter/security.go b/adapter/security.go new file mode 100644 index 000000000..2ef7966cd --- /dev/null +++ b/adapter/security.go @@ -0,0 +1,19 @@ +package adapter + +import ( + "context" + + "github.com/sagernet/sing/service" +) + +type SecurityPolicy interface { + CheckFeature(feature string) error +} + +func CheckSecurityFeature(ctx context.Context, feature string) error { + policy := service.FromContext[SecurityPolicy](ctx) + if policy == nil { + return nil + } + return policy.CheckFeature(feature) +} diff --git a/box.go b/box.go index fcd753d9c..608b436df 100644 --- a/box.go +++ b/box.go @@ -189,7 +189,7 @@ func New(options Options) (*Box, error) { len(certificateOptions.Certificate) > 0 || len(certificateOptions.CertificatePath) > 0 || len(certificateOptions.CertificateDirectoryPath) > 0 { - certificateStore, err := certificate.NewStore(logFactory.NewLogger("certificate"), certificateOptions) + certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions) if err != nil { return nil, err } @@ -436,6 +436,12 @@ func New(options Options) (*Box, error) { } } if ntpOptions.Enabled { + if ntpOptions.WriteToSystem { + err = adapter.CheckSecurityFeature(ctx, "NTP `write_to_system`") + if err != nil { + return nil, err + } + } ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain()) if err != nil { return nil, E.Cause(err, "create NTP service") diff --git a/common/certificate/store.go b/common/certificate/store.go index 790e12fe4..34871a6b2 100644 --- a/common/certificate/store.go +++ b/common/certificate/store.go @@ -2,6 +2,7 @@ package certificate import ( "bytes" + "context" "crypto/x509" "io/fs" "os" @@ -15,11 +16,13 @@ import ( "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service/filemanager" ) var _ adapter.CertificateStore = (*Store)(nil) type Store struct { + ctx context.Context access sync.RWMutex storeType string systemPool *x509.CertPool @@ -32,7 +35,7 @@ type Store struct { platform storePlatform } -func NewStore(logger logger.Logger, options option.CertificateOptions) (*Store, error) { +func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) { storeType := options.Store if storeType == "" { storeType = C.CertificateStoreSystem @@ -59,6 +62,7 @@ func NewStore(logger logger.Logger, options option.CertificateOptions) (*Store, return nil, E.New("unknown certificate store: ", options.Store) } store := &Store{ + ctx: ctx, storeType: storeType, systemPool: systemPool, certificate: strings.Join(options.Certificate, "\n"), @@ -165,7 +169,7 @@ func (s *Store) update() error { appendPEMBlock(pemBuffer, s.certificate) } for _, path := range s.certificatePaths { - pemContent, err := os.ReadFile(path) + pemContent, err := filemanager.ReadFile(s.ctx, path) if err != nil { return err } @@ -176,7 +180,7 @@ func (s *Store) update() error { } var firstErr error for _, directoryPath := range s.certificateDirectoryPaths { - directoryEntries, err := readUniqueDirectoryEntries(directoryPath) + directoryEntries, err := readUniqueDirectoryEntries(s.ctx, directoryPath) if err != nil { if firstErr == nil && !os.IsNotExist(err) { firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath) @@ -184,7 +188,7 @@ func (s *Store) update() error { continue } for _, directoryEntry := range directoryEntries { - pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name())) + pemContent, err := filemanager.ReadFile(s.ctx, filepath.Join(directoryPath, directoryEntry.Name())) if err == nil && currentPool.AppendCertsFromPEM(pemContent) { appendPEMBlock(pemBuffer, string(pemContent)) } @@ -223,8 +227,8 @@ func (s *Store) newBasePool() (*x509.CertPool, error) { } } -func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) { - files, err := os.ReadDir(dir) +func readUniqueDirectoryEntries(ctx context.Context, dir string) ([]fs.DirEntry, error) { + files, err := filemanager.ReadDir(ctx, dir) if err != nil { return nil, err } diff --git a/common/tls/acme.go b/common/tls/acme.go index 93f293f6f..efccdeced 100644 --- a/common/tls/acme.go +++ b/common/tls/acme.go @@ -5,6 +5,7 @@ package tls import ( "context" "crypto/tls" + "os" "slices" "strings" @@ -13,6 +14,7 @@ import ( "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service/filemanager" "github.com/caddyserver/certmagic" "github.com/libdns/acmedns" @@ -54,8 +56,13 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound } var storage certmagic.Storage if options.DataDirectory != "" { + dataDirectory := filemanager.BasePath(ctx, os.ExpandEnv(options.DataDirectory)) + err := filemanager.MkdirAll(ctx, dataDirectory, 0o700) + if err != nil { + return nil, nil, E.Cause(err, "create ACME data directory") + } storage = &certmagic.FileStorage{ - Path: options.DataDirectory, + Path: dataDirectory, } } else { storage = certmagic.Default.Storage diff --git a/common/tls/ech.go b/common/tls/ech.go index 8c884cab6..46ab50a23 100644 --- a/common/tls/ech.go +++ b/common/tls/ech.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "encoding/pem" "net" - "os" "strings" "sync" "time" @@ -19,6 +18,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" aTLS "github.com/sagernet/sing/common/tls" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" mDNS "github.com/miekg/dns" "golang.org/x/crypto/cryptobyte" @@ -29,7 +29,7 @@ func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, op if len(options.ECH.Config) > 0 { echConfig = []byte(strings.Join(options.ECH.Config, "\n")) } else if options.ECH.ConfigPath != "" { - content, err := os.ReadFile(options.ECH.ConfigPath) + content, err := filemanager.ReadFile(ctx, options.ECH.ConfigPath) if err != nil { return nil, E.Cause(err, "read ECH config") } @@ -60,7 +60,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, if len(options.ECH.Key) > 0 { echKey = []byte(strings.Join(options.ECH.Key, "\n")) } else if options.ECH.KeyPath != "" { - content, err := os.ReadFile(options.ECH.KeyPath) + content, err := filemanager.ReadFile(ctx, options.ECH.KeyPath) if err != nil { return E.Cause(err, "read ECH keys") } diff --git a/common/tls/std_client.go b/common/tls/std_client.go index deb0ed925..80fa6cb28 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -8,7 +8,6 @@ import ( "crypto/x509" "encoding/base64" "net" - "os" "strings" "time" @@ -20,6 +19,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service/filemanager" ) type STDClientConfig struct { @@ -179,7 +179,7 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } else if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) + content, err := filemanager.ReadFile(ctx, options.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } @@ -196,7 +196,7 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres if len(options.ClientCertificate) > 0 { clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n")) } else if options.ClientCertificatePath != "" { - content, err := os.ReadFile(options.ClientCertificatePath) + content, err := filemanager.ReadFile(ctx, options.ClientCertificatePath) if err != nil { return nil, E.Cause(err, "read client certificate") } @@ -206,7 +206,7 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres if len(options.ClientKey) > 0 { clientKey = []byte(strings.Join(options.ClientKey, "\n")) } else if options.ClientKeyPath != "" { - content, err := os.ReadFile(options.ClientKeyPath) + content, err := filemanager.ReadFile(ctx, options.ClientKeyPath) if err != nil { return nil, E.Cause(err, "read client key") } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index b801d2de6..ff4ee1a4c 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "crypto/x509" "net" - "os" "strings" "sync" "time" @@ -20,6 +19,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" ) var errInsecureUnused = E.New("tls: insecure unused") @@ -90,6 +90,7 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string { } type STDServerConfig struct { + ctx context.Context access sync.RWMutex config *tls.Config handshakeTimeout time.Duration @@ -260,13 +261,13 @@ func (c *STDServerConfig) certificateUpdated(path string) error { if path == c.certificatePath || path == c.keyPath { switch path { case c.certificatePath: - certificate, err := os.ReadFile(c.certificatePath) + certificate, err := filemanager.ReadFile(c.ctx, c.certificatePath) if err != nil { return E.Cause(err, "reload certificate from ", c.certificatePath) } c.certificate = certificate case c.keyPath: - key, err := os.ReadFile(c.keyPath) + key, err := filemanager.ReadFile(c.ctx, c.keyPath) if err != nil { return E.Cause(err, "reload key from ", c.keyPath) } @@ -286,7 +287,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error { clientCertificateCA := x509.NewCertPool() var reloaded bool for _, certPath := range c.clientCertificatePath { - content, err := os.ReadFile(certPath) + content, err := filemanager.ReadFile(c.ctx, certPath) if err != nil { c.logger.Error(E.Cause(err, "reload certificate from ", certPath)) continue @@ -307,7 +308,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error { c.access.Unlock() c.logger.Info("reloaded client certificates") } else if path == c.echKeyPath { - echKey, err := os.ReadFile(c.echKeyPath) + echKey, err := filemanager.ReadFile(c.ctx, c.echKeyPath) if err != nil { return E.Cause(err, "reload ECH keys from ", c.echKeyPath) } @@ -405,7 +406,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } else if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) + content, err := filemanager.ReadFile(ctx, options.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } @@ -414,7 +415,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. if len(options.Key) > 0 { key = []byte(strings.Join(options.Key, "\n")) } else if options.KeyPath != "" { - content, err := os.ReadFile(options.KeyPath) + content, err := filemanager.ReadFile(ctx, options.KeyPath) if err != nil { return nil, E.Cause(err, "read key") } @@ -457,7 +458,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. } else if len(options.ClientCertificatePath) > 0 { clientCertificateCA := x509.NewCertPool() for _, path := range options.ClientCertificatePath { - content, err := os.ReadFile(path) + content, err := filemanager.ReadFile(ctx, path) if err != nil { return nil, E.Cause(err, "read client certificate from ", path) } @@ -494,6 +495,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. handshakeTimeout = C.TCPTimeout } serverConfig := &STDServerConfig{ + ctx: ctx, config: tlsConfig, handshakeTimeout: handshakeTimeout, logger: logger, diff --git a/common/tls/system_client.go b/common/tls/system_client.go index ade2bd363..06f16352b 100644 --- a/common/tls/system_client.go +++ b/common/tls/system_client.go @@ -3,7 +3,6 @@ package tls import ( "context" "crypto/x509" - "os" "strings" "time" @@ -11,6 +10,7 @@ import ( "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" ) type SystemTLSValidated struct { @@ -89,7 +89,7 @@ func resolveSystemAnchors(ctx context.Context, options option.OutboundTLSOptions return []byte(strings.Join(options.Certificate, "\n")), true, nil, nil } if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) + content, err := filemanager.ReadFile(ctx, options.CertificatePath) if err != nil { return nil, false, nil, E.Cause(err, "read certificate") } diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index 3fb3acfda..16d0352d0 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -8,7 +8,6 @@ import ( "crypto/x509" "math/rand" "net" - "os" "strings" "time" @@ -21,6 +20,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service/filemanager" utls "github.com/metacubex/utls" "golang.org/x/net/http2" @@ -251,7 +251,7 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } else if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) + content, err := filemanager.ReadFile(ctx, options.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } @@ -268,7 +268,7 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre if len(options.ClientCertificate) > 0 { clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n")) } else if options.ClientCertificatePath != "" { - content, err := os.ReadFile(options.ClientCertificatePath) + content, err := filemanager.ReadFile(ctx, options.ClientCertificatePath) if err != nil { return nil, E.Cause(err, "read client certificate") } @@ -278,7 +278,7 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre if len(options.ClientKey) > 0 { clientKey = []byte(strings.Join(options.ClientKey, "\n")) } else if options.ClientKeyPath != "" { - content, err := os.ReadFile(options.ClientKeyPath) + content, err := filemanager.ReadFile(ctx, options.ClientKeyPath) if err != nil { return nil, E.Cause(err, "read client key") } diff --git a/daemon/errors.go b/daemon/errors.go deleted file mode 100644 index 17a4e8006..000000000 --- a/daemon/errors.go +++ /dev/null @@ -1,42 +0,0 @@ -package daemon - -import ( - "context" - "errors" - "os" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func UnaryErrorInterceptor(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - response, err := handler(ctx, request) - if err != nil { - return nil, mapStatusError(err) - } - return response, nil -} - -func StreamErrorInterceptor(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - err := handler(server, stream) - if err != nil { - return mapStatusError(err) - } - return nil -} - -func mapStatusError(err error) error { - if _, loaded := status.FromError(err); loaded { - return err - } - switch { - case errors.Is(err, os.ErrInvalid): - return status.Error(codes.FailedPrecondition, "service not started") - case errors.Is(err, os.ErrClosed): - return status.Error(codes.Unavailable, "service is closing") - case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded): - return status.FromContextError(err).Err() - } - return err -} diff --git a/daemon/server.go b/daemon/server.go index b5e401621..2c6ab4f93 100644 --- a/daemon/server.go +++ b/daemon/server.go @@ -15,8 +15,8 @@ import ( func NewServer(startedService *StartedService, secret string) *grpc.Server { server := grpc.NewServer( - grpc.ChainUnaryInterceptor(newUnaryAuthInterceptor(secret), UnaryErrorInterceptor), - grpc.ChainStreamInterceptor(newStreamAuthInterceptor(secret), StreamErrorInterceptor), + grpc.ChainUnaryInterceptor(newUnaryAuthInterceptor(secret)), + grpc.ChainStreamInterceptor(newStreamAuthInterceptor(secret)), ) healthServer := health.NewServer() RegisterStartedServiceServer(server, startedService) diff --git a/daemon/started_service.go b/daemon/started_service.go index c48a7faed..73fc80b03 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -196,6 +196,7 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov } oldInstance := s.instance if oldInstance != nil { + s.instance = nil s.updateStatus(ServiceStatus_STOPPING) s.serviceAccess.Unlock() _ = oldInstance.Close() @@ -221,6 +222,8 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov return nil } if err != nil { + s.instance = nil + _ = instance.Close() return s.updateStatusError(err) } s.startedAt = time.Now() @@ -243,16 +246,13 @@ func (s *StartedService) CloseService() error { case ServiceStatus_STARTING, ServiceStatus_STARTED: default: s.serviceAccess.Unlock() - return os.ErrInvalid + return nil } s.updateStatus(ServiceStatus_STOPPING) instance := s.instance s.instance = nil if instance != nil { - err := instance.Close() - if err != nil { - return s.updateStatusError(err) - } + _ = instance.Close() } s.startedAt = time.Time{} s.updateStatus(ServiceStatus_IDLE) diff --git a/daemon/started_service_usbip.go b/daemon/started_service_usbip.go index 3e771451b..132336fcb 100644 --- a/daemon/started_service_usbip.go +++ b/daemon/started_service_usbip.go @@ -29,7 +29,7 @@ func (s *StartedService) ProvideUSBDevices(server grpc.BidiStreamingServer[USBPr instance := s.instance s.serviceAccess.RUnlock() if instance == nil { - return E.New("service not started") + return nil } serviceManager := service.FromContext[adapter.ServiceManager](instance.ctx) if serviceManager == nil { @@ -122,7 +122,7 @@ func (s *StartedService) SubscribeUSBIPServerStatus( instance := s.instance s.serviceAccess.RUnlock() if instance == nil { - return E.New("service not started") + return nil } serviceManager := service.FromContext[adapter.ServiceManager](instance.ctx) if serviceManager == nil { diff --git a/dns/transport/hosts/hosts.go b/dns/transport/hosts/hosts.go index 074c07c95..4db7988cb 100644 --- a/dns/transport/hosts/hosts.go +++ b/dns/transport/hosts/hosts.go @@ -43,7 +43,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt files = append(files, defaultFile) } else { for _, path := range options.Path { - files = append(files, NewFile(filemanager.BasePath(ctx, os.ExpandEnv(path)))) + files = append(files, NewFile(ctx, filemanager.BasePath(ctx, os.ExpandEnv(path)))) } } if options.Predefined != nil { diff --git a/dns/transport/hosts/hosts_file.go b/dns/transport/hosts/hosts_file.go index af507f012..5d5605b19 100644 --- a/dns/transport/hosts/hosts_file.go +++ b/dns/transport/hosts/hosts_file.go @@ -2,15 +2,16 @@ package hosts import ( "bufio" + "context" "errors" "io" "net/netip" - "os" "strings" "sync" "time" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service/filemanager" "github.com/miekg/dns" ) @@ -18,6 +19,7 @@ import ( const cacheMaxAge = 5 * time.Second type File struct { + ctx context.Context path string access sync.Mutex byName map[string][]netip.Addr @@ -26,8 +28,9 @@ type File struct { size int64 } -func NewFile(path string) *File { +func NewFile(ctx context.Context, path string) *File { return &File{ + ctx: ctx, path: path, } } @@ -37,7 +40,7 @@ func NewDefault() (*File, error) { if err != nil { return nil, E.Cause(err, "resolve default hosts path") } - return NewFile(defaultPathResolved), nil + return NewFile(context.Background(), defaultPathResolved), nil } func (f *File) Lookup(name string) []netip.Addr { @@ -52,7 +55,7 @@ func (f *File) update() { if now.Before(f.expire) && len(f.byName) > 0 { return } - stat, err := os.Stat(f.path) + stat, err := filemanager.Stat(f.ctx, f.path) if err != nil { return } @@ -61,7 +64,7 @@ func (f *File) update() { return } byName := make(map[string][]netip.Addr) - file, err := os.Open(f.path) + file, err := filemanager.Open(f.ctx, f.path) if err != nil { return } diff --git a/dns/transport/hosts/hosts_test.go b/dns/transport/hosts/hosts_test.go index 61d20e0c6..5c9628bfd 100644 --- a/dns/transport/hosts/hosts_test.go +++ b/dns/transport/hosts/hosts_test.go @@ -1,6 +1,7 @@ package hosts import ( + "context" "net/netip" "os" "runtime" @@ -13,7 +14,7 @@ import ( func TestHosts(t *testing.T) { t.Parallel() - require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, NewFile("testdata/hosts").Lookup("localhost")) + require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, NewFile(context.Background(), "testdata/hosts").Lookup("localhost")) if runtime.GOOS != "windows" { defaultPathResolved, err := defaultPath() if err != nil { @@ -21,7 +22,7 @@ func TestHosts(t *testing.T) { } content, readErr := os.ReadFile(defaultPathResolved) require.NoError(t, readErr) - hFile := NewFile(defaultPathResolved) + hFile := NewFile(context.Background(), defaultPathResolved) if len(hFile.Lookup("localhost")) == 0 { t.Fatal("failed to resolve localhost: ", defaultPathResolved, ": \n", string(content)) } diff --git a/experimental/boxdd/cmd_service_windows.go b/experimental/boxdd/cmd_service_windows.go index 42a580f03..6db36c01c 100644 --- a/experimental/boxdd/cmd_service_windows.go +++ b/experimental/boxdd/cmd_service_windows.go @@ -3,6 +3,7 @@ package main import ( "errors" "os" + "strconv" "strings" "time" @@ -48,6 +49,18 @@ var commandServiceUninstall = &cobra.Command{ }, } +var commandServiceSetInsecureMode = &cobra.Command{ + Use: "set-insecure-mode ", + Short: "Set whether configurations may use privileges unrelated to networking", + Args: cobra.ExactArgs(1), + Run: func(command *cobra.Command, args []string) { + err := serviceSetInsecureMode(args[0]) + if err != nil { + log.Fatal(E.Cause(err, "set insecure mode")) + } + }, +} + func addPlatformServiceCommands() { commandServiceInstall.Flags().BoolVar( &commandServiceFlagAllowUnsafeInstallation, @@ -57,6 +70,57 @@ func addPlatformServiceCommands() { ) commandService.AddCommand(commandServiceInstall) commandService.AddCommand(commandServiceUninstall) + commandService.AddCommand(commandServiceSetInsecureMode) +} + +func serviceSetInsecureMode(value string) error { + enabled, err := strconv.ParseBool(value) + if err != nil { + return E.Cause(err, "parse value") + } + if !windows.GetCurrentProcessToken().IsElevated() { + return E.New("setting insecure mode requires an elevated process") + } + directory, err := installedServiceWorkingDirectory() + if err != nil { + return err + } + serviceUserID, err := windowsServiceSID() + if err != nil { + return E.Cause(err, "create daemon service SID") + } + err = validateProtectedWindowsWorkingDirectory(directory, serviceUserID) + if err != nil { + return E.Cause(err, "validate working directory") + } + return saveSecuritySettings(directory, securitySettings{InsecureModeEnabled: enabled}) +} + +func installedServiceWorkingDirectory() (string, 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() + config, err := service.Config() + if err != nil { + return "", E.Cause(err, "query service config") + } + arguments, err := windows.DecomposeCommandLine(config.BinaryPathName) + if err != nil { + return "", E.Cause(err, "parse service command line") + } + for index, argument := range arguments { + if argument == "--working-directory" && index+1 < len(arguments) { + return arguments[index+1], nil + } + } + return "", E.New("missing working directory in the service configuration") } func serviceInstall() error { diff --git a/experimental/boxdd/cmd_worker.go b/experimental/boxdd/cmd_worker.go index 38ccbef5d..734b8b3b8 100644 --- a/experimental/boxdd/cmd_worker.go +++ b/experimental/boxdd/cmd_worker.go @@ -63,8 +63,8 @@ func runWorker() error { } defer listener.Close() server := grpc.NewServer( - grpc.ChainUnaryInterceptor(daemon.UnaryErrorInterceptor), - grpc.ChainStreamInterceptor(daemon.StreamErrorInterceptor), + grpc.ChainUnaryInterceptor(unaryLocaleInterceptor), + grpc.ChainStreamInterceptor(streamLocaleInterceptor), ) RegisterApplicationServiceServer(server, &applicationService{ startedService: daemon.NewStartedService(daemon.ServiceOptions{Context: include.Context(context.Background())}), diff --git a/experimental/boxdd/desktop_service.go b/experimental/boxdd/desktop_service.go index 53af22b2e..0d9086b9c 100644 --- a/experimental/boxdd/desktop_service.go +++ b/experimental/boxdd/desktop_service.go @@ -196,18 +196,69 @@ func (s *desktopService) TakeOverService(ctx context.Context, empty *emptypb.Emp return &emptypb.Empty{}, nil } +func (s *desktopService) GetSecuritySettings(ctx context.Context, empty *emptypb.Empty) (*SecuritySettings, error) { + _, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + if !insecureModeAvailable() { + return &SecuritySettings{}, nil + } + return &SecuritySettings{ + Available: true, + InsecureModeEnabled: s.daemon.insecureModeEnabled(), + }, nil +} + +func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) { + _, err := peerIdentityFromContext(ctx) + if err != nil { + return nil, err + } + if !insecureModeAvailable() { + return nil, status.Error(codes.FailedPrecondition, "insecure mode is not available on this platform") + } + if request.Enabled { + return nil, status.Error(codes.PermissionDenied, "enabling insecure mode requires an elevated service command") + } + s.daemon.lifecycleAccess.Lock() + defer s.daemon.lifecycleAccess.Unlock() + if s.daemon.closed { + return nil, os.ErrClosed + } + wasEnabled := s.daemon.insecureModeEnabled() + err = saveSecuritySettings(workingDirectory, securitySettings{InsecureModeEnabled: false}) + if err != nil { + return nil, err + } + if wasEnabled && s.daemon.startedService.Instance() != nil { + var ownerUserID string + ownerUserID, err = loadOwner() + if err != nil { + return nil, err + } + err = s.daemon.stopServiceLocked(ownerUserID) + if err != nil { + return nil, err + } + } + return &emptypb.Empty{}, nil +} + func (d *Daemon) cleanFailedStartLocked(ownerUserID string, options startOptions, startError error) error { var platformError error if d.platform != nil { platformError = d.platform.ResetPlatformOptions() } - closeError := d.startedService.CloseService() + if d.startedService.Instance() != nil { + _ = d.startedService.CloseService() + } directory := userWorkingDirectory(ownerUserID) crashReportError := tagUnownedReports(filepath.Join(directory, crashReportsDirectoryName), ownerUserID) oomReportError := tagUnownedReports(filepath.Join(directory, oomReportsDirectoryName), ownerUserID) options.WasRunning = false snapshotError := saveStartOptions(ownerUserID, options) - return E.Errors(startError, platformError, closeError, crashReportError, oomReportError, snapshotError) + return E.Errors(startError, platformError, crashReportError, oomReportError, snapshotError) } func (s *desktopService) GetWorkingDirectory(ctx context.Context, empty *emptypb.Empty) (*WorkingDirectoryInfo, error) { diff --git a/experimental/boxdd/desktop_service.pb.go b/experimental/boxdd/desktop_service.pb.go index 0cc4c212d..94bbeb0fc 100644 --- a/experimental/boxdd/desktop_service.pb.go +++ b/experimental/boxdd/desktop_service.pb.go @@ -1424,6 +1424,102 @@ func (x *OOMReportFile) GetIsProfile() bool { return false } +type SecuritySettings struct { + state protoimpl.MessageState `protogen:"open.v1"` + Available bool `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"` + InsecureModeEnabled bool `protobuf:"varint,2,opt,name=insecure_mode_enabled,json=insecureModeEnabled,proto3" json:"insecure_mode_enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecuritySettings) Reset() { + *x = SecuritySettings{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecuritySettings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecuritySettings) ProtoMessage() {} + +func (x *SecuritySettings) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[23] + 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 SecuritySettings.ProtoReflect.Descriptor instead. +func (*SecuritySettings) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{23} +} + +func (x *SecuritySettings) GetAvailable() bool { + if x != nil { + return x.Available + } + return false +} + +func (x *SecuritySettings) GetInsecureModeEnabled() bool { + if x != nil { + return x.InsecureModeEnabled + } + return false +} + +type SetInsecureModeEnabledRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetInsecureModeEnabledRequest) Reset() { + *x = SetInsecureModeEnabledRequest{} + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetInsecureModeEnabledRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetInsecureModeEnabledRequest) ProtoMessage() {} + +func (x *SetInsecureModeEnabledRequest) ProtoReflect() protoreflect.Message { + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[24] + 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 SetInsecureModeEnabledRequest.ProtoReflect.Descriptor instead. +func (*SetInsecureModeEnabledRequest) Descriptor() ([]byte, []int) { + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{24} +} + +func (x *SetInsecureModeEnabledRequest) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + type InstallUpdateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` InstallerPath string `protobuf:"bytes,1,opt,name=installer_path,json=installerPath,proto3" json:"installer_path,omitempty"` @@ -1433,7 +1529,7 @@ type InstallUpdateRequest struct { func (x *InstallUpdateRequest) Reset() { *x = InstallUpdateRequest{} - mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[23] + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1445,7 +1541,7 @@ func (x *InstallUpdateRequest) String() string { func (*InstallUpdateRequest) ProtoMessage() {} func (x *InstallUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[23] + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1458,7 +1554,7 @@ func (x *InstallUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallUpdateRequest.ProtoReflect.Descriptor instead. func (*InstallUpdateRequest) Descriptor() ([]byte, []int) { - return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{23} + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{25} } func (x *InstallUpdateRequest) GetInstallerPath() string { @@ -1477,7 +1573,7 @@ type InstallUpdateResponse struct { func (x *InstallUpdateResponse) Reset() { *x = InstallUpdateResponse{} - mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[24] + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1489,7 +1585,7 @@ func (x *InstallUpdateResponse) String() string { func (*InstallUpdateResponse) ProtoMessage() {} func (x *InstallUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[24] + mi := &file_experimental_boxdd_desktop_service_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1502,7 +1598,7 @@ func (x *InstallUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallUpdateResponse.ProtoReflect.Descriptor instead. func (*InstallUpdateResponse) Descriptor() ([]byte, []int) { - return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{24} + return file_experimental_boxdd_desktop_service_proto_rawDescGZIP(), []int{26} } func (x *InstallUpdateResponse) GetResult() InstallUpdateResult { @@ -1606,7 +1702,12 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" + "\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\"=\n" + + "is_profile\x18\x03 \x01(\bR\tisProfile\"d\n" + + "\x10SecuritySettings\x12\x1c\n" + + "\tavailable\x18\x01 \x01(\bR\tavailable\x122\n" + + "\x15insecure_mode_enabled\x18\x02 \x01(\bR\x13insecureModeEnabled\"9\n" + + "\x1dSetInsecureModeEnabledRequest\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\"=\n" + "\x14InstallUpdateRequest\x12%\n" + "\x0einstaller_path\x18\x01 \x01(\tR\rinstallerPath\"M\n" + "\x15InstallUpdateResponse\x124\n" + @@ -1620,7 +1721,7 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" + "!INSTALL_UPDATE_RESULT_UNSPECIFIED\x10\x00\x12!\n" + "\x1dINSTALL_UPDATE_RESULT_STARTED\x10\x01\x12)\n" + "%INSTALL_UPDATE_RESULT_SIGNER_MISMATCH\x10\x02\x12#\n" + - "\x1fINSTALL_UPDATE_RESULT_NOT_NEWER\x10\x032\x9c\v\n" + + "\x1fINSTALL_UPDATE_RESULT_NOT_NEWER\x10\x032\xc4\f\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" + @@ -1640,7 +1741,9 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" + "\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\"\x00\x12P\n" + - "\rInstallUpdate\x12\x1d.desktop.InstallUpdateRequest\x1a\x1e.desktop.InstallUpdateResponse\"\x002\xbd\x04\n" + + "\rInstallUpdate\x12\x1d.desktop.InstallUpdateRequest\x1a\x1e.desktop.InstallUpdateResponse\"\x00\x12J\n" + + "\x13GetSecuritySettings\x12\x16.google.protobuf.Empty\x1a\x19.desktop.SecuritySettings\"\x00\x12Z\n" + + "\x16SetInsecureModeEnabled\x12&.desktop.SetInsecureModeEnabledRequest\x1a\x16.google.protobuf.Empty\"\x002\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" + @@ -1664,7 +1767,7 @@ func file_experimental_boxdd_desktop_service_proto_rawDescGZIP() []byte { var ( file_experimental_boxdd_desktop_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) - file_experimental_boxdd_desktop_service_proto_msgTypes = make([]protoimpl.MessageInfo, 25) + file_experimental_boxdd_desktop_service_proto_msgTypes = make([]protoimpl.MessageInfo, 27) file_experimental_boxdd_desktop_service_proto_goTypes = []any{ (DaemonOwnership)(0), // 0: desktop.DaemonOwnership (InstallUpdateResult)(0), // 1: desktop.InstallUpdateResult @@ -1692,11 +1795,13 @@ var ( (*OOMReportExportRequest)(nil), // 23: desktop.OOMReportExportRequest (*OOMReportContent)(nil), // 24: desktop.OOMReportContent (*OOMReportFile)(nil), // 25: desktop.OOMReportFile - (*InstallUpdateRequest)(nil), // 26: desktop.InstallUpdateRequest - (*InstallUpdateResponse)(nil), // 27: desktop.InstallUpdateResponse - (*emptypb.Empty)(nil), // 28: google.protobuf.Empty - (*daemon.NetworkQualityTestProgress)(nil), // 29: daemon.NetworkQualityTestProgress - (*daemon.STUNTestProgress)(nil), // 30: daemon.STUNTestProgress + (*SecuritySettings)(nil), // 26: desktop.SecuritySettings + (*SetInsecureModeEnabledRequest)(nil), // 27: desktop.SetInsecureModeEnabledRequest + (*InstallUpdateRequest)(nil), // 28: desktop.InstallUpdateRequest + (*InstallUpdateResponse)(nil), // 29: desktop.InstallUpdateResponse + (*emptypb.Empty)(nil), // 30: google.protobuf.Empty + (*daemon.NetworkQualityTestProgress)(nil), // 31: daemon.NetworkQualityTestProgress + (*daemon.STUNTestProgress)(nil), // 32: daemon.STUNTestProgress } ) @@ -1709,60 +1814,64 @@ var file_experimental_boxdd_desktop_service_proto_depIdxs = []int32{ 21, // 5: desktop.OOMReportList.reports:type_name -> desktop.OOMReportEntry 25, // 6: desktop.OOMReportContent.files:type_name -> desktop.OOMReportFile 1, // 7: desktop.InstallUpdateResponse.result:type_name -> desktop.InstallUpdateResult - 28, // 8: desktop.DesktopService.GetDaemonInfo:input_type -> google.protobuf.Empty - 28, // 9: desktop.DesktopService.ClaimService:input_type -> google.protobuf.Empty - 28, // 10: desktop.DesktopService.TakeOverService:input_type -> google.protobuf.Empty + 30, // 8: desktop.DesktopService.GetDaemonInfo:input_type -> google.protobuf.Empty + 30, // 9: desktop.DesktopService.ClaimService:input_type -> google.protobuf.Empty + 30, // 10: desktop.DesktopService.TakeOverService:input_type -> google.protobuf.Empty 7, // 11: desktop.DesktopService.StartService:input_type -> desktop.StartServiceRequest - 28, // 12: desktop.DesktopService.GetWorkingDirectory:input_type -> google.protobuf.Empty - 28, // 13: desktop.DesktopService.DestroyWorkingDirectory:input_type -> google.protobuf.Empty - 28, // 14: desktop.DesktopService.ListCrashReports:input_type -> google.protobuf.Empty + 30, // 12: desktop.DesktopService.GetWorkingDirectory:input_type -> google.protobuf.Empty + 30, // 13: desktop.DesktopService.DestroyWorkingDirectory:input_type -> google.protobuf.Empty + 30, // 14: desktop.DesktopService.ListCrashReports:input_type -> google.protobuf.Empty 15, // 15: desktop.DesktopService.ReadCrashReport:input_type -> desktop.CrashReportRequest 15, // 16: desktop.DesktopService.MarkCrashReportRead:input_type -> desktop.CrashReportRequest 16, // 17: desktop.DesktopService.ExportCrashReport:input_type -> desktop.CrashReportExportRequest 15, // 18: desktop.DesktopService.DeleteCrashReport:input_type -> desktop.CrashReportRequest - 28, // 19: desktop.DesktopService.DeleteAllCrashReports:input_type -> google.protobuf.Empty - 28, // 20: desktop.DesktopService.ListOOMReports:input_type -> google.protobuf.Empty + 30, // 19: desktop.DesktopService.DeleteAllCrashReports:input_type -> google.protobuf.Empty + 30, // 20: desktop.DesktopService.ListOOMReports:input_type -> google.protobuf.Empty 22, // 21: desktop.DesktopService.ReadOOMReport:input_type -> desktop.OOMReportRequest 22, // 22: desktop.DesktopService.MarkOOMReportRead:input_type -> desktop.OOMReportRequest 23, // 23: desktop.DesktopService.ExportOOMReport:input_type -> desktop.OOMReportExportRequest 22, // 24: desktop.DesktopService.DeleteOOMReport:input_type -> desktop.OOMReportRequest - 28, // 25: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty - 26, // 26: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest - 9, // 27: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent - 9, // 28: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent - 10, // 29: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent - 11, // 30: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData - 3, // 31: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest - 4, // 32: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest - 5, // 33: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest - 6, // 34: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo - 28, // 35: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty - 28, // 36: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty - 28, // 37: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty - 12, // 38: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo - 28, // 39: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty - 13, // 40: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList - 17, // 41: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent - 28, // 42: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty - 19, // 43: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive - 28, // 44: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty - 28, // 45: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty - 20, // 46: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList - 24, // 47: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent - 28, // 48: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty - 19, // 49: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive - 28, // 50: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty - 28, // 51: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty - 27, // 52: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse - 28, // 53: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty - 9, // 54: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent - 11, // 55: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData - 10, // 56: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent - 28, // 57: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty - 29, // 58: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress - 30, // 59: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress - 34, // [34:60] is the sub-list for method output_type - 8, // [8:34] is the sub-list for method input_type + 30, // 25: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty + 28, // 26: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest + 30, // 27: desktop.DesktopService.GetSecuritySettings:input_type -> google.protobuf.Empty + 27, // 28: desktop.DesktopService.SetInsecureModeEnabled:input_type -> desktop.SetInsecureModeEnabledRequest + 9, // 29: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent + 9, // 30: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent + 10, // 31: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent + 11, // 32: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData + 3, // 33: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest + 4, // 34: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest + 5, // 35: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest + 6, // 36: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo + 30, // 37: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty + 30, // 38: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty + 30, // 39: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty + 12, // 40: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo + 30, // 41: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty + 13, // 42: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList + 17, // 43: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent + 30, // 44: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty + 19, // 45: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive + 30, // 46: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty + 30, // 47: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty + 20, // 48: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList + 24, // 49: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent + 30, // 50: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty + 19, // 51: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive + 30, // 52: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty + 30, // 53: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty + 29, // 54: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse + 26, // 55: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings + 30, // 56: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty + 30, // 57: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty + 9, // 58: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent + 11, // 59: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData + 10, // 60: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent + 30, // 61: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty + 31, // 62: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 32, // 63: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress + 36, // [36:64] is the sub-list for method output_type + 8, // [8:36] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name 8, // [8:8] is the sub-list for extension extendee 0, // [0:8] is the sub-list for field type_name @@ -1779,7 +1888,7 @@ func file_experimental_boxdd_desktop_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_experimental_boxdd_desktop_service_proto_rawDesc), len(file_experimental_boxdd_desktop_service_proto_rawDesc)), NumEnums: 3, - NumMessages: 25, + NumMessages: 27, NumExtensions: 0, NumServices: 2, }, diff --git a/experimental/boxdd/desktop_service.proto b/experimental/boxdd/desktop_service.proto index dc52b1ac6..22d212c0a 100644 --- a/experimental/boxdd/desktop_service.proto +++ b/experimental/boxdd/desktop_service.proto @@ -26,6 +26,8 @@ service DesktopService { rpc DeleteOOMReport(OOMReportRequest) returns (google.protobuf.Empty) {} rpc DeleteAllOOMReports(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc InstallUpdate(InstallUpdateRequest) returns (InstallUpdateResponse) {} + rpc GetSecuritySettings(google.protobuf.Empty) returns (SecuritySettings) {} + rpc SetInsecureModeEnabled(SetInsecureModeEnabledRequest) returns (google.protobuf.Empty) {} } service ApplicationService { @@ -173,6 +175,15 @@ message OOMReportFile { bool is_profile = 3; } +message SecuritySettings { + bool available = 1; + bool insecure_mode_enabled = 2; +} + +message SetInsecureModeEnabledRequest { + bool enabled = 1; +} + message InstallUpdateRequest { string installer_path = 1; } diff --git a/experimental/boxdd/desktop_service_grpc.pb.go b/experimental/boxdd/desktop_service_grpc.pb.go index 8bb230945..034f5f109 100644 --- a/experimental/boxdd/desktop_service_grpc.pb.go +++ b/experimental/boxdd/desktop_service_grpc.pb.go @@ -36,6 +36,8 @@ const ( DesktopService_DeleteOOMReport_FullMethodName = "/desktop.DesktopService/DeleteOOMReport" DesktopService_DeleteAllOOMReports_FullMethodName = "/desktop.DesktopService/DeleteAllOOMReports" DesktopService_InstallUpdate_FullMethodName = "/desktop.DesktopService/InstallUpdate" + DesktopService_GetSecuritySettings_FullMethodName = "/desktop.DesktopService/GetSecuritySettings" + DesktopService_SetInsecureModeEnabled_FullMethodName = "/desktop.DesktopService/SetInsecureModeEnabled" ) // DesktopServiceClient is the client API for DesktopService service. @@ -61,6 +63,8 @@ type DesktopServiceClient interface { 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) InstallUpdate(ctx context.Context, in *InstallUpdateRequest, opts ...grpc.CallOption) (*InstallUpdateResponse, error) + GetSecuritySettings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SecuritySettings, error) + SetInsecureModeEnabled(ctx context.Context, in *SetInsecureModeEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type desktopServiceClient struct { @@ -261,6 +265,26 @@ func (c *desktopServiceClient) InstallUpdate(ctx context.Context, in *InstallUpd return out, nil } +func (c *desktopServiceClient) GetSecuritySettings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SecuritySettings, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SecuritySettings) + err := c.cc.Invoke(ctx, DesktopService_GetSecuritySettings_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *desktopServiceClient) SetInsecureModeEnabled(ctx context.Context, in *SetInsecureModeEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DesktopService_SetInsecureModeEnabled_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. @@ -284,6 +308,8 @@ type DesktopServiceServer interface { DeleteOOMReport(context.Context, *OOMReportRequest) (*emptypb.Empty, error) DeleteAllOOMReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error) InstallUpdate(context.Context, *InstallUpdateRequest) (*InstallUpdateResponse, error) + GetSecuritySettings(context.Context, *emptypb.Empty) (*SecuritySettings, error) + SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) mustEmbedUnimplementedDesktopServiceServer() } @@ -369,6 +395,14 @@ func (UnimplementedDesktopServiceServer) DeleteAllOOMReports(context.Context, *e func (UnimplementedDesktopServiceServer) InstallUpdate(context.Context, *InstallUpdateRequest) (*InstallUpdateResponse, error) { return nil, status.Error(codes.Unimplemented, "method InstallUpdate not implemented") } + +func (UnimplementedDesktopServiceServer) GetSecuritySettings(context.Context, *emptypb.Empty) (*SecuritySettings, error) { + return nil, status.Error(codes.Unimplemented, "method GetSecuritySettings not implemented") +} + +func (UnimplementedDesktopServiceServer) SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SetInsecureModeEnabled not implemented") +} func (UnimplementedDesktopServiceServer) mustEmbedUnimplementedDesktopServiceServer() {} func (UnimplementedDesktopServiceServer) testEmbeddedByValue() {} @@ -732,6 +766,42 @@ func _DesktopService_InstallUpdate_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _DesktopService_GetSecuritySettings_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).GetSecuritySettings(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_GetSecuritySettings_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).GetSecuritySettings(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DesktopService_SetInsecureModeEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetInsecureModeEnabledRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DesktopServiceServer).SetInsecureModeEnabled(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DesktopService_SetInsecureModeEnabled_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DesktopServiceServer).SetInsecureModeEnabled(ctx, req.(*SetInsecureModeEnabledRequest)) + } + 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) @@ -815,6 +885,14 @@ var DesktopService_ServiceDesc = grpc.ServiceDesc{ MethodName: "InstallUpdate", Handler: _DesktopService_InstallUpdate_Handler, }, + { + MethodName: "GetSecuritySettings", + Handler: _DesktopService_GetSecuritySettings_Handler, + }, + { + MethodName: "SetInsecureModeEnabled", + Handler: _DesktopService_SetInsecureModeEnabled_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "experimental/boxdd/desktop_service.proto", diff --git a/experimental/boxdd/insecure_mode.go b/experimental/boxdd/insecure_mode.go new file mode 100644 index 000000000..77965ce09 --- /dev/null +++ b/experimental/boxdd/insecure_mode.go @@ -0,0 +1,22 @@ +package main + +import ( + "path/filepath" + + "github.com/sagernet/sing/common/json" + "github.com/sagernet/tailscale/atomicfile" +) + +const securitySettingsFileName = "security.json" + +type securitySettings struct { + InsecureModeEnabled bool `json:"insecure_mode_enabled"` +} + +func saveSecuritySettings(directory string, settings securitySettings) error { + content, err := json.Marshal(settings) + if err != nil { + return err + } + return atomicfile.WriteFile(filepath.Join(directory, securitySettingsFileName), content, 0o600) +} diff --git a/experimental/boxdd/insecure_mode_stub.go b/experimental/boxdd/insecure_mode_stub.go new file mode 100644 index 000000000..e20cabf92 --- /dev/null +++ b/experimental/boxdd/insecure_mode_stub.go @@ -0,0 +1,16 @@ +//go:build !windows + +package main + +import "context" + +func registerSecurityPolicy(ctx context.Context, daemon *Daemon) { +} + +func insecureModeAvailable() bool { + return false +} + +func (d *Daemon) insecureModeEnabled() bool { + return false +} diff --git a/experimental/boxdd/insecure_mode_windows.go b/experimental/boxdd/insecure_mode_windows.go new file mode 100644 index 000000000..3f12e822f --- /dev/null +++ b/experimental/boxdd/insecure_mode_windows.go @@ -0,0 +1,210 @@ +//go:build windows + +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental/locale" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" +) + +func registerSecurityPolicy(ctx context.Context, daemon *Daemon) { + service.MustRegister[adapter.SecurityPolicy](ctx, &daemonSecurityPolicy{daemon}) + service.MustRegister[filemanager.Manager](ctx, &restrictedFileManager{daemon}) +} + +func insecureModeAvailable() bool { + return true +} + +func loadSecuritySettings(directory string) (securitySettings, error) { + content, err := os.ReadFile(filepath.Join(directory, securitySettingsFileName)) + if err != nil { + return securitySettings{}, err + } + settings, err := json.UnmarshalExtended[securitySettings](content) + if err != nil { + return securitySettings{}, err + } + return settings, nil +} + +func (d *Daemon) insecureModeEnabled() bool { + settings, err := loadSecuritySettings(workingDirectory) + if err != nil { + return false + } + return settings.InsecureModeEnabled +} + +func insecureFeatureError(feature string) error { + return E.New(fmt.Sprintf(locale.Current().InsecureFeatureMessage, feature)) +} + +type daemonSecurityPolicy struct { + daemon *Daemon +} + +func (p *daemonSecurityPolicy) CheckFeature(feature string) error { + if p.daemon.insecureModeEnabled() { + return nil + } + return insecureFeatureError(feature) +} + +type restrictedFileManager struct { + daemon *Daemon +} + +func (m *restrictedFileManager) BasePath(name string) string { + if filepath.IsAbs(name) { + return name + } + currentDirectory, err := os.Getwd() + if err != nil { + return name + } + return filepath.Join(currentDirectory, name) +} + +func (m *restrictedFileManager) TempPath() string { + currentDirectory, err := os.Getwd() + if err != nil { + return "." + } + return currentDirectory +} + +func (m *restrictedFileManager) checkPath(name string) (string, error) { + path, err := filepath.Abs(m.BasePath(name)) + if err != nil { + return "", err + } + if m.daemon.insecureModeEnabled() { + return path, nil + } + currentDirectory, err := os.Getwd() + if err != nil { + return "", err + } + normalizedRoot := strings.ToLower(filepath.Clean(currentDirectory)) + normalizedPath := strings.ToLower(filepath.Clean(path)) + if normalizedPath != normalizedRoot && !strings.HasPrefix(normalizedPath, normalizedRoot+string(filepath.Separator)) { + return "", E.New(fmt.Sprintf(locale.Current().ExternalPathFeature, path)) + } + existingPath := path + for { + _, err = os.Lstat(existingPath) + if err == nil { + break + } + if !os.IsNotExist(err) { + return "", err + } + parentPath := filepath.Dir(existingPath) + if parentPath == existingPath { + return "", err + } + existingPath = parentPath + } + resolvedRoot, err := filepath.EvalSymlinks(currentDirectory) + if err != nil { + return "", err + } + resolvedExistingPath, err := filepath.EvalSymlinks(existingPath) + if err != nil { + return "", err + } + remainingPath, err := filepath.Rel(existingPath, path) + if err != nil { + return "", err + } + resolvedPath := filepath.Join(resolvedExistingPath, remainingPath) + normalizedResolvedRoot := strings.ToLower(filepath.Clean(resolvedRoot)) + normalizedResolvedPath := strings.ToLower(filepath.Clean(resolvedPath)) + if normalizedResolvedPath != normalizedResolvedRoot && !strings.HasPrefix(normalizedResolvedPath, normalizedResolvedRoot+string(filepath.Separator)) { + return "", E.New(fmt.Sprintf(locale.Current().ExternalPathFeature, path)) + } + return path, nil +} + +func (m *restrictedFileManager) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { + path, err := m.checkPath(name) + if err != nil { + return nil, err + } + return os.OpenFile(path, flag, perm) +} + +func (m *restrictedFileManager) Create(name string) (*os.File, error) { + path, err := m.checkPath(name) + if err != nil { + return nil, err + } + return os.Create(path) +} + +func (m *restrictedFileManager) CreateTemp(pattern string) (*os.File, error) { + currentDirectory, err := os.Getwd() + if err != nil { + return nil, err + } + return os.CreateTemp(currentDirectory, pattern) +} + +func (m *restrictedFileManager) Chown(path string) error { + return nil +} + +func (m *restrictedFileManager) Mkdir(path string, perm os.FileMode) error { + checkedPath, err := m.checkPath(path) + if err != nil { + return err + } + return os.Mkdir(checkedPath, perm) +} + +func (m *restrictedFileManager) MkdirAll(path string, perm os.FileMode) error { + checkedPath, err := m.checkPath(path) + if err != nil { + return err + } + return os.MkdirAll(checkedPath, perm) +} + +func (m *restrictedFileManager) Remove(path string) error { + checkedPath, err := m.checkPath(path) + if err != nil { + return err + } + return os.Remove(checkedPath) +} + +func (m *restrictedFileManager) RemoveAll(path string) error { + checkedPath, err := m.checkPath(path) + if err != nil { + return err + } + return os.RemoveAll(checkedPath) +} + +func (m *restrictedFileManager) Rename(oldPath string, newPath string) error { + checkedOldPath, err := m.checkPath(oldPath) + if err != nil { + return err + } + checkedNewPath, err := m.checkPath(newPath) + if err != nil { + return err + } + return os.Rename(checkedOldPath, checkedNewPath) +} diff --git a/experimental/boxdd/locale.go b/experimental/boxdd/locale.go new file mode 100644 index 000000000..d666984d8 --- /dev/null +++ b/experimental/boxdd/locale.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + + "github.com/sagernet/sing-box/experimental/locale" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func setLocaleFromContext(ctx context.Context) { + requestMetadata, loaded := metadata.FromIncomingContext(ctx) + if !loaded { + return + } + for _, localeID := range requestMetadata.Get("accept-language") { + if locale.Set(localeID) { + return + } + } +} + +func unaryLocaleInterceptor(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + setLocaleFromContext(ctx) + return handler(ctx, request) +} + +func streamLocaleInterceptor(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + setLocaleFromContext(stream.Context()) + return handler(server, stream) +} diff --git a/experimental/boxdd/server.go b/experimental/boxdd/server.go index 87871b31a..4f70e0e38 100644 --- a/experimental/boxdd/server.go +++ b/experimental/boxdd/server.go @@ -52,6 +52,7 @@ func newDaemon() (*Daemon, error) { if platformInterface != nil { service.MustRegister[adapter.PlatformInterface](ctx, platformInterface) } + registerSecurityPolicy(ctx, d) d.startedService = daemon.NewStartedService(daemon.ServiceOptions{ Context: ctx, LogMaxLines: 3000, @@ -65,8 +66,8 @@ func newDaemon() (*Daemon, error) { }) authorizer := newAuthorizer(d) serverOptions := []grpc.ServerOption{ - grpc.ChainUnaryInterceptor(newUnaryAuthorizeInterceptor(authorizer), daemon.UnaryErrorInterceptor), - grpc.ChainStreamInterceptor(newStreamAuthorizeInterceptor(authorizer), daemon.StreamErrorInterceptor), + grpc.ChainUnaryInterceptor(newUnaryAuthorizeInterceptor(authorizer), unaryLocaleInterceptor), + grpc.ChainStreamInterceptor(newStreamAuthorizeInterceptor(authorizer), streamLocaleInterceptor), } platformOptions, err := platformServerOptions(d) if err != nil { diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 372e3938e..3ca556757 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -163,11 +163,13 @@ func (c *CacheFile) startCacheCleanup() { func (c *CacheFile) start() error { const fileMode = 0o666 + cacheFile, err := filemanager.OpenFile(c.ctx, c.path, os.O_RDWR|os.O_CREATE, fileMode) + if err != nil { + return err + } + cacheFile.Close() options := bbolt.Options{Timeout: time.Second} - var ( - db *bbolt.DB - err error - ) + var db *bbolt.DB for range 10 { db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { @@ -177,7 +179,7 @@ func (c *CacheFile) start() error { continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(c.path) + rmErr := filemanager.Remove(c.ctx, c.path) if rmErr != nil { return err } @@ -260,7 +262,7 @@ func (c *CacheFile) resetDB() { c.resetAccess.Lock() defer c.resetAccess.Unlock() c.DB.Close() - os.Remove(c.path) + filemanager.Remove(c.ctx, c.path) db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second}) if err == nil { _ = filemanager.Chown(c.ctx, c.path) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index c04875366..8de33d6a6 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -140,6 +140,10 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op }) if options.ExternalUI != "" { s.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) + _, err := filemanager.ReadDir(ctx, s.externalUI) + if err != nil && !os.IsNotExist(err) { + return nil, E.Cause(err, "read external UI directory") + } chiRouter.Group(func(r chi.Router) { r.Get("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently).ServeHTTP) r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(Dir(s.externalUI)))) diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad9fff536..a5997ebea 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -7,7 +7,6 @@ import ( "io" "net" "net/http" - "os" "path/filepath" "strings" @@ -24,9 +23,9 @@ func (s *Server) checkAndDownloadExternalUI() { if s.externalUI == "" { return } - entries, err := os.ReadDir(s.externalUI) + entries, err := filemanager.ReadDir(s.ctx, s.externalUI) if err != nil { - os.MkdirAll(s.externalUI, 0o755) + filemanager.MkdirAll(s.ctx, s.externalUI, 0o755) } if len(entries) == 0 { err = s.downloadExternalUI() @@ -79,7 +78,7 @@ func (s *Server) downloadExternalUI() error { } err = s.downloadZIP(response.Body, s.externalUI) if err != nil { - removeAllInDirectory(s.externalUI) + removeAllInDirectory(s.ctx, s.externalUI) } return err } @@ -89,7 +88,7 @@ func (s *Server) downloadZIP(body io.Reader, output string) error { if err != nil { return err } - defer os.Remove(tempFile.Name()) + defer filemanager.Remove(s.ctx, tempFile.Name()) _, err = io.Copy(tempFile, body) tempFile.Close() if err != nil { @@ -113,7 +112,7 @@ func (s *Server) downloadZIP(body io.Reader, output string) error { if len(pathElements) > 1 { saveDirectory = filepath.Join(saveDirectory, filepath.Join(pathElements[:len(pathElements)-1]...)) } - err = os.MkdirAll(saveDirectory, 0o755) + err = filemanager.MkdirAll(s.ctx, saveDirectory, 0o755) if err != nil { return err } @@ -140,13 +139,13 @@ func downloadZIPEntry(ctx context.Context, zipFile *zip.File, savePath string) e return common.Error(io.Copy(saveFile, reader)) } -func removeAllInDirectory(directory string) { - dirEntries, err := os.ReadDir(directory) +func removeAllInDirectory(ctx context.Context, directory string) { + dirEntries, err := filemanager.ReadDir(ctx, directory) if err != nil { return } for _, dirEntry := range dirEntries { - os.RemoveAll(filepath.Join(directory, dirEntry.Name())) + filemanager.RemoveAll(ctx, filepath.Join(directory, dirEntry.Name())) } } diff --git a/experimental/libbox/http.go b/experimental/libbox/http.go index 25cb778c2..c9ee37878 100644 --- a/experimental/libbox/http.go +++ b/experimental/libbox/http.go @@ -83,7 +83,7 @@ func NewHTTPClient() HTTPClient { client.transport.TLSClientConfig = &client.tls client.transport.DisableKeepAlives = true if C.IsAndroid { - store, err := certificate.NewStore(logger.NOP(), option.CertificateOptions{}) + store, err := certificate.NewStore(context.Background(), logger.NOP(), option.CertificateOptions{}) if err != nil { panic(E.Cause(err, "initialize certificate store")) } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 9f8aa03cf..8c2761302 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -6,7 +6,6 @@ import ( "path/filepath" "runtime" "runtime/debug" - "strings" "time" "github.com/sagernet/sing-box/common/networkquality" @@ -102,12 +101,9 @@ func Setup(options *SetupOptions) error { return redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log")) } -func SetLocale(localeId string) error { - if strings.Contains(localeId, "@") { - localeId = strings.Split(localeId, "@")[0] - } - if !locale.Set(localeId) { - return E.New("unsupported locale: ", localeId) +func SetLocale(localeID string) error { + if !locale.Set(localeID) { + return E.New("unsupported locale: ", localeID) } return nil } diff --git a/experimental/locale/locale.go b/experimental/locale/locale.go index e5575af4e..122fbfd64 100644 --- a/experimental/locale/locale.go +++ b/experimental/locale/locale.go @@ -1,32 +1,103 @@ package locale +import ( + "strings" + "sync/atomic" + + "golang.org/x/text/language" +) + var ( - localeRegistry = make(map[string]*Locale) - current = defaultLocal + localeRegistry = map[string]*Locale{ + "en": defaultLocale, + } + localeMatcher = language.NewMatcher( + []language.Tag{ + language.English, + language.SimplifiedChinese, + language.TraditionalChinese, + language.Persian, + language.Russian, + }, + language.PreferSameScript(true), + ) + localeNames = []string{"en", "zh-Hans", "zh-Hant", "fa", "ru"} + current atomic.Pointer[Locale] ) type Locale struct { - // deprecated messages for graphical clients Locale string DeprecatedMessage string DeprecatedMessageNoLink string + InsecureFeatureMessage string + ExternalPathFeature string } -var defaultLocal = &Locale{ - Locale: "en_US", - DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s please checkout documentation for migration.", +var defaultLocale = &Locale{ + Locale: "en", + DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s. Please check the documentation for migration.", DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.", + InsecureFeatureMessage: "%s is considered insecure in the graphical client for sing-box on Windows. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.", + ExternalPathFeature: "Access to %s (outside of the working directory) is considered insecure in the graphical client for sing-box on Windows. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.", +} + +func init() { + current.Store(defaultLocale) } func Current() *Locale { - return current + return current.Load() } -func Set(localeId string) bool { - locale, loaded := localeRegistry[localeId] +func Set(localeID string) bool { + localeEntries := strings.Split(localeID, ",") + for i, localeEntry := range localeEntries { + languageID, options, hasOptions := strings.Cut(localeEntry, ";") + languageID, _, _ = strings.Cut(strings.TrimSpace(languageID), "@") + languageID = strings.ReplaceAll(languageID, "_", "-") + if !hasOptions { + languageID, _, _ = strings.Cut(languageID, ".") + } + switch { + case strings.EqualFold(languageID, "C"), strings.EqualFold(languageID, "POSIX"): + languageID = "en" + case strings.EqualFold(languageID, "zh-CHS"): + languageID = "zh-Hans" + case strings.EqualFold(languageID, "zh-CHT"): + languageID = "zh-Hant" + } + localeEntries[i] = languageID + if hasOptions { + localeEntries[i] += ";" + options + } + } + localeID = strings.Join(localeEntries, ",") + tags, _, err := language.ParseAcceptLanguage(localeID) + if err != nil || len(tags) == 0 { + return false + } + for i, tag := range tags { + base, script, region := tag.Raw() + if base.String() != "zh" && base.String() != "cmn" { + continue + } + if script.String() == "Hans" || script.String() == "Hant" { + continue + } + languageID := "zh-Hans" + if region.String() == "TW" || region.String() == "HK" || region.String() == "MO" { + languageID = "zh-Hant" + } + if region.String() != "ZZ" { + languageID += "-" + region.String() + } + tags[i] = language.MustParse(languageID) + } + _, localeIndex, _ := localeMatcher.Match(tags...) + selectedLocale, loaded := localeRegistry[localeNames[localeIndex]] if !loaded { return false } - current = locale + current.Store(selectedLocale) return true } diff --git a/experimental/locale/locale_fa.go b/experimental/locale/locale_fa.go new file mode 100644 index 000000000..0e9211d64 --- /dev/null +++ b/experimental/locale/locale_fa.go @@ -0,0 +1,11 @@ +package locale + +func init() { + localeRegistry["fa"] = &Locale{ + Locale: "fa", + DeprecatedMessage: "%s از sing-box %s منسوخ شده است و در sing-box %s حذف خواهد شد؛ لطفاً راهنمای مهاجرت را ببینید.", + DeprecatedMessageNoLink: "%s از sing-box %s منسوخ شده است و در sing-box %s حذف خواهد شد.", + InsecureFeatureMessage: "%s در کلاینت گرافیکی sing-box برای Windows ناامن تلقی می‌شود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.", + ExternalPathFeature: "دسترسی به %s (خارج از پوشهٔ کاری) در کلاینت گرافیکی sing-box برای Windows ناامن تلقی می‌شود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.", + } +} diff --git a/experimental/locale/locale_ru.go b/experimental/locale/locale_ru.go new file mode 100644 index 000000000..e7c7f7e80 --- /dev/null +++ b/experimental/locale/locale_ru.go @@ -0,0 +1,11 @@ +package locale + +func init() { + localeRegistry["ru"] = &Locale{ + Locale: "ru", + DeprecatedMessage: "Использование %s устарело в sing-box %s, и эта возможность будет удалена в sing-box %s. Ознакомьтесь с руководством по миграции.", + DeprecatedMessageNoLink: "Использование %s устарело в sing-box %s, и эта возможность будет удалена в sing-box %s.", + InsecureFeatureMessage: "%s считается небезопасным в графическом клиенте sing-box для Windows. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.", + ExternalPathFeature: "Доступ к %s (за пределами рабочего каталога) считается небезопасным в графическом клиенте sing-box для Windows. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.", + } +} diff --git a/experimental/locale/locale_zh_CN.go b/experimental/locale/locale_zh_CN.go index f5605d9d1..a63450101 100644 --- a/experimental/locale/locale_zh_CN.go +++ b/experimental/locale/locale_zh_CN.go @@ -3,9 +3,11 @@ package locale var warningMessageForEndUsers = "\n\n如果您不明白此消息意味着什么:您的配置文件已过时,且将很快不可用。请联系您的配置提供者以更新配置。" func init() { - localeRegistry["zh_CN"] = &Locale{ - Locale: "zh_CN", + localeRegistry["zh-Hans"] = &Locale{ + Locale: "zh-Hans", DeprecatedMessage: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除,请参阅迁移指南。" + warningMessageForEndUsers, DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers, + InsecureFeatureMessage: "%s 在 sing-box 的 Windows 图形客户端中被视为不安全。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。", + ExternalPathFeature: "访问 %s(位于工作目录之外)在 sing-box 的 Windows 图形客户端中是不安全的。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。", } } diff --git a/experimental/locale/locale_zh_TW.go b/experimental/locale/locale_zh_TW.go new file mode 100644 index 000000000..3c5202eb6 --- /dev/null +++ b/experimental/locale/locale_zh_TW.go @@ -0,0 +1,11 @@ +package locale + +func init() { + localeRegistry["zh-Hant"] = &Locale{ + Locale: "zh-Hant", + DeprecatedMessage: "%s 已在 sing-box %s 中棄用,且將在 sing-box %s 中移除,請參閱遷移指南。", + DeprecatedMessageNoLink: "%s 已在 sing-box %s 中棄用,且將在 sing-box %s 中移除。", + InsecureFeatureMessage: "%s 在 sing-box 的 Windows 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。", + ExternalPathFeature: "存取 %s(位於工作目錄之外)在 sing-box 的 Windows 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。", + } +} diff --git a/go.mod b/go.mod index e32e607a6..8930942fc 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( golang.org/x/net v0.50.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.41.0 + golang.org/x/text v0.34.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 @@ -176,7 +177,6 @@ require ( go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index cd7839bb3..88d1b8f13 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -7,6 +7,7 @@ import ( "net/http/httputil" "net/netip" "net/url" + "os" "time" "github.com/sagernet/sing-box/adapter" @@ -26,6 +27,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" ) func RegisterInbound(registry *inbound.Registry) { @@ -73,7 +75,12 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if options.Masquerade != nil && options.Masquerade.Type != "" { switch options.Masquerade.Type { case C.Hysterai2MasqueradeTypeFile: - masqueradeHandler = http.FileServer(http.Dir(options.Masquerade.FileOptions.Directory)) + masqueradeDirectory := filemanager.BasePath(ctx, os.ExpandEnv(options.Masquerade.FileOptions.Directory)) + _, err = filemanager.ReadDir(ctx, masqueradeDirectory) + if err != nil && !os.IsNotExist(err) { + return nil, E.Cause(err, "read masquerade directory") + } + masqueradeHandler = http.FileServer(http.Dir(masqueradeDirectory)) case C.Hysterai2MasqueradeTypeProxy: masqueradeURL, err := url.Parse(options.Masquerade.ProxyOptions.URL) if err != nil { diff --git a/protocol/naive/outbound.go b/protocol/naive/outbound.go index 8249a1fef..1e753c33d 100644 --- a/protocol/naive/outbound.go +++ b/protocol/naive/outbound.go @@ -6,7 +6,6 @@ import ( "context" "encoding/pem" "net" - "os" "strings" "github.com/sagernet/cronet-go" @@ -25,6 +24,7 @@ import ( N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" mDNS "github.com/miekg/dns" ) @@ -109,7 +109,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if len(options.TLS.Certificate) > 0 { trustedRootCertificates = strings.Join(options.TLS.Certificate, "\n") } else if options.TLS.CertificatePath != "" { - content, err := os.ReadFile(options.TLS.CertificatePath) + content, err := filemanager.ReadFile(ctx, options.TLS.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } @@ -146,7 +146,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if len(options.TLS.ECH.Config) > 0 { echConfig = []byte(strings.Join(options.TLS.ECH.Config, "\n")) } else if options.TLS.ECH.ConfigPath != "" { - content, err := os.ReadFile(options.TLS.ECH.ConfigPath) + content, err := filemanager.ReadFile(ctx, options.TLS.ECH.ConfigPath) if err != nil { return nil, E.Cause(err, "read ECH config") } diff --git a/protocol/ssh/outbound.go b/protocol/ssh/outbound.go index f79d6460d..d045f1896 100644 --- a/protocol/ssh/outbound.go +++ b/protocol/ssh/outbound.go @@ -23,6 +23,7 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service/filemanager" "golang.org/x/crypto/ssh" ) @@ -88,7 +89,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL privateKey = []byte(strings.Join(options.PrivateKey, "\n")) } else { var err error - privateKey, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)) + privateKey, err = filemanager.ReadFile(ctx, os.ExpandEnv(options.PrivateKeyPath)) if err != nil { return nil, E.Cause(err, "read private key") } diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 44042236d..279c06f16 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -151,6 +151,16 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL } stateDirectory = filemanager.BasePath(ctx, os.ExpandEnv(stateDirectory)) stateDirectory, _ = filepath.Abs(stateDirectory) + mkdirErr := filemanager.MkdirAll(ctx, stateDirectory, 0o700) + if mkdirErr != nil { + return nil, E.Cause(mkdirErr, "create state directory") + } + if options.SSHServer != nil && options.SSHServer.Enabled { + err := adapter.CheckSecurityFeature(ctx, "Tailscale `ssh_server`") + if err != nil { + return nil, err + } + } for _, advertiseRoute := range options.AdvertiseRoutes { if advertiseRoute.Addr().IsUnspecified() && advertiseRoute.Bits() == 0 { return nil, E.New("`advertise_routes` cannot be default, use `advertise_exit_node` instead.") @@ -424,7 +434,7 @@ func (t *Endpoint) postStart() error { } t.filter = localBackend.ExportFilter() if sshEnabled { - sshServer, err := tailssh.New(t.server, t.platformInterface, t.sshServerOptions, t.logger) + sshServer, err := tailssh.New(t.ctx, t.server, t.platformInterface, t.sshServerOptions, t.logger) if err != nil { return E.Cause(err, "create SSH server") } diff --git a/protocol/tailscale/tailssh/server.go b/protocol/tailscale/tailssh/server.go index f39163716..7fa93ae84 100644 --- a/protocol/tailscale/tailssh/server.go +++ b/protocol/tailscale/tailssh/server.go @@ -15,7 +15,6 @@ import ( "net/http" "net/netip" "net/url" - "os" "path" "path/filepath" "strings" @@ -28,6 +27,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/service/filemanager" tsDNS "github.com/sagernet/tailscale/net/dns" "github.com/sagernet/tailscale/tailcfg" "github.com/sagernet/tailscale/tsnet" @@ -93,7 +93,7 @@ type activeSession struct { cancel context.CancelFunc } -func New(tsnetServer *tsnet.Server, platformInterface adapter.PlatformInterface, options *option.TailscaleSSHServerOptions, logger logger.ContextLogger) (*Server, error) { +func New(ctx context.Context, tsnetServer *tsnet.Server, platformInterface adapter.PlatformInterface, options *option.TailscaleSSHServerOptions, logger logger.ContextLogger) (*Server, error) { s := &Server{ tsnetServer: tsnetServer, platformInterface: platformInterface, @@ -104,7 +104,7 @@ func New(tsnetServer *tsnet.Server, platformInterface adapter.PlatformInterface, done: make(chan struct{}), activeConns: make(map[*activeSession]struct{}), } - s.serverCtx, s.serverCancel = context.WithCancel(context.Background()) + s.serverCtx, s.serverCancel = context.WithCancel(ctx) hostSigner, err := s.loadOrGenerateHostKey() if err != nil { return nil, err @@ -132,7 +132,7 @@ func (s *Server) loadOrGenerateHostKey() (gossh.Signer, error) { if isPrivilegedUser() { systemKey := systemHostKeyPath() if systemKey != "" { - keyData, err := os.ReadFile(systemKey) + keyData, err := filemanager.ReadFile(s.serverCtx, systemKey) if err == nil { signer, parseErr := gossh.ParsePrivateKey(keyData) if parseErr == nil { @@ -144,7 +144,7 @@ func (s *Server) loadOrGenerateHostKey() (gossh.Signer, error) { } } keyPath := filepath.Join(s.tsnetServer.Dir, "ssh_host_ed25519_key") - keyData, err := os.ReadFile(keyPath) + keyData, err := filemanager.ReadFile(s.serverCtx, keyPath) if err == nil { signer, parseErr := gossh.ParsePrivateKey(keyData) if parseErr == nil { @@ -163,11 +163,11 @@ func (s *Server) loadOrGenerateHostKey() (gossh.Signer, error) { } pemData := pem.EncodeToMemory(keyBytes) dir := filepath.Dir(keyPath) - err = os.MkdirAll(dir, 0o700) + err = filemanager.MkdirAll(s.serverCtx, dir, 0o700) if err != nil { return nil, err } - err = os.WriteFile(keyPath, pemData, 0o600) + err = filemanager.WriteFile(s.serverCtx, keyPath, pemData, 0o600) if err != nil { return nil, err } diff --git a/protocol/tor/outbound.go b/protocol/tor/outbound.go index 6f0c3fd6d..0a10f2ad0 100644 --- a/protocol/tor/outbound.go +++ b/protocol/tor/outbound.go @@ -20,8 +20,8 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/service/filemanager" "github.com/cretz/bine/control" "github.com/cretz/bine/tor" @@ -46,36 +46,50 @@ type Outbound struct { func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (adapter.Outbound, error) { var startConf tor.StartConf startConf.DataDir = os.ExpandEnv(options.DataDirectory) - startConf.TempDataDirBase = os.TempDir() - startConf.ExtraArgs = options.ExtraArgs - if options.DataDirectory != "" { - dataDirAbs, _ := filepath.Abs(startConf.DataDir) - if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.IsFile(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { - options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath) - } - if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.IsFile(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { - options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path) - } - } - if options.ExecutablePath != "" { - startConf.ExePath = options.ExecutablePath - startConf.ProcessCreator = nil - startConf.UseEmbeddedControlConn = false - } if startConf.DataDir != "" { - torrcFile := filepath.Join(startConf.DataDir, "torrc") - err := rw.MkdirParent(torrcFile) + startConf.DataDir = filemanager.BasePath(ctx, startConf.DataDir) + } + startConf.TempDataDirBase = filemanager.TempPath(ctx) + if startConf.DataDir != "" { + err := filemanager.MkdirAll(ctx, startConf.DataDir, 0o755) if err != nil { return nil, err } - if !rw.IsFile(torrcFile) { - err := os.WriteFile(torrcFile, []byte(""), 0o600) + dataDirAbs, _ := filepath.Abs(startConf.DataDir) + geoIPPath := filepath.Join(dataDirAbs, "geoip") + geoIPInfo, err := filemanager.Stat(ctx, geoIPPath) + if err == nil && !geoIPInfo.IsDir() && !common.Contains(options.ExtraArgs, "--GeoIPFile") { + options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath) + } + geoIP6Path := filepath.Join(dataDirAbs, "geoip6") + geoIP6Info, err := filemanager.Stat(ctx, geoIP6Path) + if err == nil && !geoIP6Info.IsDir() && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { + options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path) + } + torrcFile := filepath.Join(startConf.DataDir, "torrc") + torrcInfo, err := filemanager.Stat(ctx, torrcFile) + if os.IsNotExist(err) { + err = filemanager.WriteFile(ctx, torrcFile, []byte(""), 0o600) if err != nil { return nil, err } + } else if err != nil { + return nil, err + } else if torrcInfo.IsDir() { + return nil, E.New("Tor configuration path is a directory: ", torrcFile) } startConf.TorrcFile = torrcFile } + startConf.ExtraArgs = options.ExtraArgs + if options.ExecutablePath != "" { + err := adapter.CheckSecurityFeature(ctx, "Tor `executable_path`") + if err != nil { + return nil, err + } + startConf.ExePath = options.ExecutablePath + startConf.ProcessCreator = nil + startConf.UseEmbeddedControlConn = false + } outboundDialer, err := dialer.New(ctx, options.DialerOptions, false) if err != nil { return nil, err diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index 427c95709..513b8bf90 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -2,7 +2,6 @@ package rule import ( "context" - "os" "path/filepath" "strings" "sync" @@ -100,7 +99,7 @@ func (s *LocalRuleSet) reloadFile(path string) error { var ruleSet option.PlainRuleSetCompat switch s.fileFormat { case C.RuleSetFormatSource, "": - content, err := os.ReadFile(path) + content, err := filemanager.ReadFile(s.ctx, path) if err != nil { return err } @@ -110,7 +109,7 @@ func (s *LocalRuleSet) reloadFile(path string) error { } case C.RuleSetFormatBinary: - setFile, err := os.Open(path) + setFile, err := filemanager.Open(s.ctx, path) if err != nil { return err } diff --git a/service/acme/service.go b/service/acme/service.go index 1b9677099..fe2d516ea 100644 --- a/service/acme/service.go +++ b/service/acme/service.go @@ -9,6 +9,7 @@ import ( "encoding/json" "net/http" "net/url" + "os" "reflect" "slices" "strings" @@ -24,6 +25,7 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" "github.com/caddyserver/certmagic" "github.com/caddyserver/zerossl" @@ -78,7 +80,12 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s var storage certmagic.Storage if options.DataDirectory != "" { - storage = &certmagic.FileStorage{Path: options.DataDirectory} + dataDirectory := filemanager.BasePath(ctx, os.ExpandEnv(options.DataDirectory)) + err := filemanager.MkdirAll(ctx, dataDirectory, 0o700) + if err != nil { + return nil, E.Cause(err, "create ACME data directory") + } + storage = &certmagic.FileStorage{Path: dataDirectory} } else { storage = certmagic.Default.Storage } diff --git a/service/api/dashboard.go b/service/api/dashboard.go index a1085dae8..a522335f2 100644 --- a/service/api/dashboard.go +++ b/service/api/dashboard.go @@ -74,6 +74,10 @@ func newDashboard(ctx context.Context, logger log.ContextLogger, options option. } func (d *dashboard) start() error { + _, err := filemanager.ReadDir(d.ctx, d.path) + if err != nil && !os.IsNotExist(err) { + return E.Cause(err, "read dashboard directory") + } transport, err := d.resolveTransport() if err != nil { return E.Cause(err, "create dashboard http client") @@ -147,7 +151,7 @@ func (d *dashboard) loopUpdate() { } func (d *dashboard) loadState() dashboardStatus { - entries, err := os.ReadDir(d.path) + entries, err := filemanager.ReadDir(d.ctx, d.path) if err != nil { return dashboardEmpty } @@ -155,12 +159,12 @@ func (d *dashboard) loadState() dashboardStatus { return dashboardEmpty } etagPath := filepath.Join(d.path, dashboardEtagFileName) - etagBytes, err := os.ReadFile(etagPath) + etagBytes, err := filemanager.ReadFile(d.ctx, etagPath) if err != nil { return dashboardUserProvided } d.lastEtag = strings.TrimSpace(string(etagBytes)) - info, err := os.Stat(etagPath) + info, err := filemanager.Stat(d.ctx, etagPath) if err == nil { d.lastUpdated = info.ModTime() } @@ -212,7 +216,7 @@ func (d *dashboard) extract(body io.Reader, etag string) error { return err } tempZipPath := tempFile.Name() - defer os.Remove(tempZipPath) + defer filemanager.Remove(d.ctx, tempZipPath) _, err = io.Copy(tempFile, body) tempFile.Close() if err != nil { @@ -271,7 +275,7 @@ func (d *dashboard) extract(body io.Reader, etag string) error { if err != nil { return err } - return os.Rename(tempDir, d.path) + return filemanager.Rename(d.ctx, tempDir, d.path) } func extractZipEntry(ctx context.Context, file *zip.File, savePath string) error { diff --git a/service/ccm/credential.go b/service/ccm/credential.go index 695efc7ae..8766a66e4 100644 --- a/service/ccm/credential.go +++ b/service/ccm/credential.go @@ -2,6 +2,7 @@ package ccm import ( "bytes" + "context" "encoding/json" "io" "net/http" @@ -11,6 +12,7 @@ import ( "time" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service/filemanager" ) const ( @@ -42,8 +44,8 @@ func getDefaultCredentialsPath() (string, error) { return filepath.Join(userInfo.HomeDir, ".claude", ".credentials.json"), nil } -func readCredentialsFromFile(path string) (*oauthCredentials, error) { - data, err := os.ReadFile(path) +func readCredentialsFromFile(ctx context.Context, path string) (*oauthCredentials, error) { + data, err := filemanager.ReadFile(ctx, path) if err != nil { return nil, err } @@ -60,14 +62,14 @@ func readCredentialsFromFile(path string) (*oauthCredentials, error) { return credentialsContainer.ClaudeAIAuth, nil } -func writeCredentialsToFile(oauthCredentials *oauthCredentials, path string) error { +func writeCredentialsToFile(ctx context.Context, oauthCredentials *oauthCredentials, path string) error { data, err := json.MarshalIndent(map[string]any{ "claudeAiOauth": oauthCredentials, }, "", " ") if err != nil { return err } - return os.WriteFile(path, data, 0o600) + return filemanager.WriteFile(ctx, path, data, 0o600) } type oauthCredentials struct { diff --git a/service/ccm/credential_darwin.go b/service/ccm/credential_darwin.go index 24047b858..ab779f7eb 100644 --- a/service/ccm/credential_darwin.go +++ b/service/ccm/credential_darwin.go @@ -3,6 +3,7 @@ package ccm import ( + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -33,9 +34,9 @@ func getKeychainServiceName() string { return "Claude Code-credentials-" + hex.EncodeToString(hash[:])[:8] } -func platformReadCredentials(customPath string) (*oauthCredentials, error) { +func platformReadCredentials(ctx context.Context, customPath string) (*oauthCredentials, error) { if customPath != "" { - return readCredentialsFromFile(customPath) + return readCredentialsFromFile(ctx, customPath) } userInfo, err := getRealUser() @@ -66,12 +67,12 @@ func platformReadCredentials(customPath string) (*oauthCredentials, error) { if err != nil { return nil, err } - return readCredentialsFromFile(defaultPath) + return readCredentialsFromFile(ctx, defaultPath) } -func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath string) error { +func platformWriteCredentials(ctx context.Context, oauthCredentials *oauthCredentials, customPath string) error { if customPath != "" { - return writeCredentialsToFile(oauthCredentials, customPath) + return writeCredentialsToFile(ctx, oauthCredentials, customPath) } userInfo, err := getRealUser() @@ -112,5 +113,5 @@ func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath str if err != nil { return err } - return writeCredentialsToFile(oauthCredentials, defaultPath) + return writeCredentialsToFile(ctx, oauthCredentials, defaultPath) } diff --git a/service/ccm/credential_other.go b/service/ccm/credential_other.go index 828c78c07..2989367da 100644 --- a/service/ccm/credential_other.go +++ b/service/ccm/credential_other.go @@ -2,7 +2,9 @@ package ccm -func platformReadCredentials(customPath string) (*oauthCredentials, error) { +import "context" + +func platformReadCredentials(ctx context.Context, customPath string) (*oauthCredentials, error) { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() @@ -10,10 +12,10 @@ func platformReadCredentials(customPath string) (*oauthCredentials, error) { return nil, err } } - return readCredentialsFromFile(customPath) + return readCredentialsFromFile(ctx, customPath) } -func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath string) error { +func platformWriteCredentials(ctx context.Context, oauthCredentials *oauthCredentials, customPath string) error { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() @@ -21,5 +23,5 @@ func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath str return err } } - return writeCredentialsToFile(oauthCredentials, customPath) + return writeCredentialsToFile(ctx, oauthCredentials, customPath) } diff --git a/service/ccm/service.go b/service/ccm/service.go index 3aca535df..f143c6939 100644 --- a/service/ccm/service.go +++ b/service/ccm/service.go @@ -160,6 +160,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio usageTracker = &AggregatedUsage{ LastUpdated: time.Now(), Combinations: make([]CostCombination, 0), + ctx: ctx, filePath: options.UsagesPath, logger: logger, } @@ -201,7 +202,7 @@ func (s *Service) Start(stage adapter.StartStage) error { s.userManager.UpdateUsers(s.users) - credentials, err := platformReadCredentials(s.credentialPath) + credentials, err := platformReadCredentials(s.ctx, s.credentialPath) if err != nil { return E.Cause(err, "read credentials") } @@ -271,7 +272,7 @@ func (s *Service) getAccessToken() (string, error) { s.credentials = newCredentials - err = platformWriteCredentials(newCredentials, s.credentialPath) + err = platformWriteCredentials(s.ctx, newCredentials, s.credentialPath) if err != nil { s.logger.Warn("persist refreshed token: ", err) } diff --git a/service/ccm/service_usage.go b/service/ccm/service_usage.go index 36e9ee65d..a6d285c43 100644 --- a/service/ccm/service_usage.go +++ b/service/ccm/service_usage.go @@ -1,6 +1,7 @@ package ccm import ( + "context" "encoding/json" "fmt" "math" @@ -11,6 +12,7 @@ import ( "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service/filemanager" ) type UsageStats struct { @@ -36,6 +38,7 @@ type AggregatedUsage struct { LastUpdated time.Time `json:"last_updated"` Combinations []CostCombination `json:"combinations"` mutex sync.Mutex + ctx context.Context filePath string logger log.ContextLogger lastSaveTime time.Time @@ -567,7 +570,7 @@ func (u *AggregatedUsage) Load() error { u.LastUpdated = time.Time{} u.Combinations = nil - data, err := os.ReadFile(u.filePath) + data, err := filemanager.ReadFile(u.ctx, u.filePath) if err != nil { if os.IsNotExist(err) { return nil @@ -601,12 +604,12 @@ func (u *AggregatedUsage) Save() error { } tmpFile := u.filePath + ".tmp" - err = os.WriteFile(tmpFile, data, 0o644) + err = filemanager.WriteFile(u.ctx, tmpFile, data, 0o644) if err != nil { return err } - defer os.Remove(tmpFile) - err = os.Rename(tmpFile, u.filePath) + defer filemanager.Remove(u.ctx, tmpFile) + err = filemanager.Rename(u.ctx, tmpFile, u.filePath) if err == nil { u.saveMutex.Lock() u.lastSaveTime = time.Now() diff --git a/service/derp/service.go b/service/derp/service.go index ee91e3a16..a754580c0 100644 --- a/service/derp/service.go +++ b/service/derp/service.go @@ -137,7 +137,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio func (d *Service) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateStart: - config, err := readDERPConfig(filemanager.BasePath(d.ctx, d.configPath)) + config, err := readDERPConfig(d.ctx, filemanager.BasePath(d.ctx, d.configPath)) if err != nil { return err } @@ -166,7 +166,7 @@ func (d *Service) Start(stage adapter.StartStage) error { server.SetMeshKey(d.meshKey) } else if d.meshKeyPath != "" { var meshKeyContent []byte - meshKeyContent, err = os.ReadFile(d.meshKeyPath) + meshKeyContent, err = filemanager.ReadFile(d.ctx, d.meshKeyPath) if err != nil { return err } @@ -447,11 +447,11 @@ type derpConfig struct { PrivateKey key.NodePrivate } -func readDERPConfig(path string) (*derpConfig, error) { - content, err := os.ReadFile(path) +func readDERPConfig(ctx context.Context, path string) (*derpConfig, error) { + content, err := filemanager.ReadFile(ctx, path) if err != nil { if os.IsNotExist(err) { - return writeNewDERPConfig(path) + return writeNewDERPConfig(ctx, path) } return nil, err } @@ -463,9 +463,9 @@ func readDERPConfig(path string) (*derpConfig, error) { return &config, nil } -func writeNewDERPConfig(path string) (*derpConfig, error) { +func writeNewDERPConfig(ctx context.Context, path string) (*derpConfig, error) { newKey := key.NewNode() - err := os.MkdirAll(filepath.Dir(path), 0o777) + err := filemanager.MkdirAll(ctx, filepath.Dir(path), 0o777) if err != nil { return nil, err } @@ -476,7 +476,7 @@ func writeNewDERPConfig(path string) (*derpConfig, error) { if err != nil { return nil, err } - err = os.WriteFile(path, content, 0o644) + err = filemanager.WriteFile(ctx, path, content, 0o644) if err != nil { return nil, err } diff --git a/service/ocm/credential.go b/service/ocm/credential.go index 76651a8e1..1524d4859 100644 --- a/service/ocm/credential.go +++ b/service/ocm/credential.go @@ -2,6 +2,7 @@ package ocm import ( "bytes" + "context" "encoding/json" "io" "net/http" @@ -11,6 +12,7 @@ import ( "time" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service/filemanager" ) const ( @@ -42,8 +44,8 @@ func getDefaultCredentialsPath() (string, error) { return filepath.Join(userInfo.HomeDir, ".codex", "auth.json"), nil } -func readCredentialsFromFile(path string) (*oauthCredentials, error) { - data, err := os.ReadFile(path) +func readCredentialsFromFile(ctx context.Context, path string) (*oauthCredentials, error) { + data, err := filemanager.ReadFile(ctx, path) if err != nil { return nil, err } @@ -55,12 +57,12 @@ func readCredentialsFromFile(path string) (*oauthCredentials, error) { return &credentials, nil } -func writeCredentialsToFile(credentials *oauthCredentials, path string) error { +func writeCredentialsToFile(ctx context.Context, credentials *oauthCredentials, path string) error { data, err := json.MarshalIndent(credentials, "", " ") if err != nil { return err } - return os.WriteFile(path, data, 0o600) + return filemanager.WriteFile(ctx, path, data, 0o600) } type oauthCredentials struct { diff --git a/service/ocm/credential_darwin.go b/service/ocm/credential_darwin.go index f3da2a63e..bd49f6148 100644 --- a/service/ocm/credential_darwin.go +++ b/service/ocm/credential_darwin.go @@ -2,7 +2,9 @@ package ocm -func platformReadCredentials(customPath string) (*oauthCredentials, error) { +import "context" + +func platformReadCredentials(ctx context.Context, customPath string) (*oauthCredentials, error) { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() @@ -10,10 +12,10 @@ func platformReadCredentials(customPath string) (*oauthCredentials, error) { return nil, err } } - return readCredentialsFromFile(customPath) + return readCredentialsFromFile(ctx, customPath) } -func platformWriteCredentials(credentials *oauthCredentials, customPath string) error { +func platformWriteCredentials(ctx context.Context, credentials *oauthCredentials, customPath string) error { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() @@ -21,5 +23,5 @@ func platformWriteCredentials(credentials *oauthCredentials, customPath string) return err } } - return writeCredentialsToFile(credentials, customPath) + return writeCredentialsToFile(ctx, credentials, customPath) } diff --git a/service/ocm/credential_other.go b/service/ocm/credential_other.go index 22dfd0337..64544f608 100644 --- a/service/ocm/credential_other.go +++ b/service/ocm/credential_other.go @@ -2,7 +2,9 @@ package ocm -func platformReadCredentials(customPath string) (*oauthCredentials, error) { +import "context" + +func platformReadCredentials(ctx context.Context, customPath string) (*oauthCredentials, error) { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() @@ -10,10 +12,10 @@ func platformReadCredentials(customPath string) (*oauthCredentials, error) { return nil, err } } - return readCredentialsFromFile(customPath) + return readCredentialsFromFile(ctx, customPath) } -func platformWriteCredentials(credentials *oauthCredentials, customPath string) error { +func platformWriteCredentials(ctx context.Context, credentials *oauthCredentials, customPath string) error { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() @@ -21,5 +23,5 @@ func platformWriteCredentials(credentials *oauthCredentials, customPath string) return err } } - return writeCredentialsToFile(credentials, customPath) + return writeCredentialsToFile(ctx, credentials, customPath) } diff --git a/service/ocm/service.go b/service/ocm/service.go index 18bae4573..d02e923e3 100644 --- a/service/ocm/service.go +++ b/service/ocm/service.go @@ -179,6 +179,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio usageTracker = &AggregatedUsage{ LastUpdated: time.Now(), Combinations: make([]CostCombination, 0), + ctx: ctx, filePath: options.UsagesPath, logger: logger, } @@ -222,7 +223,7 @@ func (s *Service) Start(stage adapter.StartStage) error { s.userManager.UpdateUsers(s.users) - credentials, err := platformReadCredentials(s.credentialPath) + credentials, err := platformReadCredentials(s.ctx, s.credentialPath) if err != nil { return E.Cause(err, "read credentials") } @@ -292,7 +293,7 @@ func (s *Service) getAccessToken() (string, error) { s.credentials = newCredentials - err = platformWriteCredentials(newCredentials, s.credentialPath) + err = platformWriteCredentials(s.ctx, newCredentials, s.credentialPath) if err != nil { s.logger.Warn("persist refreshed token: ", err) } diff --git a/service/ocm/service_usage.go b/service/ocm/service_usage.go index 18696f3b4..d16f3f43d 100644 --- a/service/ocm/service_usage.go +++ b/service/ocm/service_usage.go @@ -1,6 +1,7 @@ package ocm import ( + "context" "encoding/json" "fmt" "math" @@ -12,6 +13,7 @@ import ( "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service/filemanager" ) type UsageStats struct { @@ -56,6 +58,7 @@ type AggregatedUsage struct { LastUpdated time.Time `json:"last_updated"` Combinations []CostCombination `json:"combinations"` mutex sync.Mutex + ctx context.Context filePath string logger log.ContextLogger lastSaveTime time.Time @@ -1072,7 +1075,7 @@ func (u *AggregatedUsage) Load() error { u.LastUpdated = time.Time{} u.Combinations = nil - data, err := os.ReadFile(u.filePath) + data, err := filemanager.ReadFile(u.ctx, u.filePath) if err != nil { if os.IsNotExist(err) { return nil @@ -1106,12 +1109,12 @@ func (u *AggregatedUsage) Save() error { } tmpFile := u.filePath + ".tmp" - err = os.WriteFile(tmpFile, data, 0o644) + err = filemanager.WriteFile(u.ctx, tmpFile, data, 0o644) if err != nil { return err } - defer os.Remove(tmpFile) - err = os.Rename(tmpFile, u.filePath) + defer filemanager.Remove(u.ctx, tmpFile) + err = filemanager.Rename(u.ctx, tmpFile, u.filePath) if err == nil { u.saveMutex.Lock() u.lastSaveTime = time.Now() diff --git a/service/origin_ca/service.go b/service/origin_ca/service.go index e9fca42c6..d7bd235a1 100644 --- a/service/origin_ca/service.go +++ b/service/origin_ca/service.go @@ -18,6 +18,7 @@ import ( "io/fs" "net" "net/http" + "os" "slices" "strconv" "strings" @@ -33,6 +34,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" "github.com/caddyserver/certmagic" ) @@ -109,7 +111,13 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s } var storage certmagic.Storage if options.DataDirectory != "" { - storage = &certmagic.FileStorage{Path: options.DataDirectory} + dataDirectory := filemanager.BasePath(ctx, os.ExpandEnv(options.DataDirectory)) + mkdirErr := filemanager.MkdirAll(ctx, dataDirectory, 0o700) + if mkdirErr != nil { + cancel() + return nil, E.Cause(mkdirErr, "create data directory") + } + storage = &certmagic.FileStorage{Path: dataDirectory} } else { storage = certmagic.Default.Storage } diff --git a/service/ssmapi/cache.go b/service/ssmapi/cache.go index f942265d4..38552f1c8 100644 --- a/service/ssmapi/cache.go +++ b/service/ssmapi/cache.go @@ -37,7 +37,7 @@ func (s *Service) loadCache() error { return nil } basePath := filemanager.BasePath(s.ctx, s.cachePath) - cacheBinary, err := os.ReadFile(basePath) + cacheBinary, err := filemanager.ReadFile(s.ctx, basePath) if err != nil { if os.IsNotExist(err) { return nil @@ -46,7 +46,7 @@ func (s *Service) loadCache() error { } err = s.decodeCache(cacheBinary) if err != nil { - os.RemoveAll(basePath) + filemanager.RemoveAll(s.ctx, basePath) return err } s.cacheMutex.Lock() @@ -73,11 +73,11 @@ func (s *Service) saveCache() error { func (s *Service) writeCache(cacheBinary []byte) error { basePath := filemanager.BasePath(s.ctx, s.cachePath) - err := os.MkdirAll(filepath.Dir(basePath), 0o777) + err := filemanager.MkdirAll(s.ctx, filepath.Dir(basePath), 0o777) if err != nil { return err } - err = os.WriteFile(basePath, cacheBinary, 0o644) + err = filemanager.WriteFile(s.ctx, basePath, cacheBinary, 0o644) if err != nil { return err }