344 lines
10 KiB
Go
344 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/sagernet/sing-box/daemon"
|
|
"github.com/sagernet/sing-box/experimental/libbox"
|
|
"github.com/sagernet/sing-box/include"
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/service/oomkiller"
|
|
"github.com/sagernet/sing/service"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/health"
|
|
"google.golang.org/grpc/health/grpc_health_v1"
|
|
"google.golang.org/grpc/reflection"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const daemonCrashOutputFileName = "CrashReport-Daemon.log"
|
|
|
|
type Daemon struct {
|
|
logger log.ContextLogger
|
|
startedService *daemon.StartedService
|
|
server *grpc.Server
|
|
listenerPath string
|
|
lifecycleAccess sync.Mutex
|
|
closed bool
|
|
peerAccess sync.Mutex
|
|
peerConnections map[peerConnection]peerIdentity
|
|
}
|
|
|
|
func newDaemon() (*Daemon, error) {
|
|
ctx := include.Context(context.Background())
|
|
d := &Daemon{
|
|
logger: log.StdLogger(),
|
|
}
|
|
d.startedService = daemon.NewStartedService(daemon.ServiceOptions{
|
|
Context: ctx,
|
|
LogMaxLines: 3000,
|
|
})
|
|
reporter := libbox.NewOOMReporter(d.startedService)
|
|
service.MustRegister[oomkiller.OOMReporter](ctx, reporter)
|
|
managedService := daemon.NewManagedService(daemon.ManagedServiceOptions{
|
|
Handler: &managedHandler{d},
|
|
Debug: debugEnabled,
|
|
OOMReporter: reporter,
|
|
})
|
|
authorizer := newAuthorizer(d)
|
|
serverOptions := []grpc.ServerOption{
|
|
grpc.ChainUnaryInterceptor(newUnaryAuthorizeInterceptor(authorizer), daemon.UnaryErrorInterceptor),
|
|
grpc.ChainStreamInterceptor(newStreamAuthorizeInterceptor(authorizer), daemon.StreamErrorInterceptor),
|
|
}
|
|
platformOptions, err := platformServerOptions(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
serverOptions = append(serverOptions, platformOptions...)
|
|
d.server = grpc.NewServer(serverOptions...)
|
|
daemon.RegisterStartedServiceServer(d.server, d.startedService)
|
|
daemon.RegisterManagedServiceServer(d.server, managedService)
|
|
RegisterDesktopServiceServer(d.server, &desktopService{daemon: d})
|
|
healthServer := health.NewServer()
|
|
healthServer.SetServingStatus(daemon.StartedService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
|
|
healthServer.SetServingStatus(daemon.ManagedService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
|
|
healthServer.SetServingStatus(DesktopService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
|
|
grpc_health_v1.RegisterHealthServer(d.server, healthServer)
|
|
if listenAddress != "" {
|
|
reflection.Register(d.server)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func (d *Daemon) listen() (net.Listener, error) {
|
|
if listenAddress != "" {
|
|
d.logger.Warn("listening on TCP address ", listenAddress, ": development only, no access control")
|
|
return net.Listen("tcp", listenAddress)
|
|
}
|
|
return listenEndpoint()
|
|
}
|
|
|
|
func (d *Daemon) Start() error {
|
|
listener, err := d.listen()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if listener.Addr().Network() == "unix" {
|
|
d.listenerPath, err = filepath.Abs(listener.Addr().String())
|
|
if err != nil {
|
|
listener.Close()
|
|
return err
|
|
}
|
|
}
|
|
d.logger.Info("daemon listening at ", listener.Addr())
|
|
go func() {
|
|
serveError := d.server.Serve(listener)
|
|
if serveError != nil && !errors.Is(serveError, grpc.ErrServerStopped) {
|
|
d.logger.Error("serve: ", serveError)
|
|
}
|
|
}()
|
|
go d.restore()
|
|
return nil
|
|
}
|
|
|
|
func (d *Daemon) restore() {
|
|
d.lifecycleAccess.Lock()
|
|
defer d.lifecycleAccess.Unlock()
|
|
if d.closed {
|
|
return
|
|
}
|
|
options, err := loadStartOptions()
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
d.logger.Warn("load start options: ", err)
|
|
}
|
|
return
|
|
}
|
|
err = tagUnownedReports(filepath.Join(workingDirectory, crashReportsDirectoryName), options.OwnerUserID)
|
|
if err != nil {
|
|
d.logger.Warn("tag crash reports: ", err)
|
|
}
|
|
err = tagUnownedReports(filepath.Join(workingDirectory, oomReportsDirectoryName), options.OwnerUserID)
|
|
if err != nil {
|
|
d.logger.Warn("tag OOM reports: ", err)
|
|
}
|
|
if !options.WasRunning {
|
|
return
|
|
}
|
|
configContent, err := loadServiceConfig()
|
|
if err != nil {
|
|
d.logger.Error("restore service: ", err)
|
|
return
|
|
}
|
|
d.logger.Info("restoring service")
|
|
err = d.startService(configContent, options)
|
|
if err != nil {
|
|
d.logger.Error("restore service: ", err)
|
|
}
|
|
}
|
|
|
|
func (d *Daemon) startService(configContent string, options startOptions) error {
|
|
_ = os.WriteFile(filepath.Join(workingDirectory, configSnapshotFileName), []byte(configContent), 0o600)
|
|
libbox.ReloadSetupOptions(&libbox.SetupOptions{
|
|
OomKillerEnabled: options.OOMKillerEnabled,
|
|
OomKillerDisabled: options.OOMKillerDisabled,
|
|
OomMemoryLimit: options.OOMMemoryLimit,
|
|
})
|
|
d.startedService.SetOOMKillerOptions(options.OOMKillerEnabled, options.OOMKillerDisabled, uint64(options.OOMMemoryLimit))
|
|
return d.startedService.StartOrReloadService(configContent, nil)
|
|
}
|
|
|
|
func (d *Daemon) clearRuntimeData() error {
|
|
entries, err := os.ReadDir(workingDirectory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.Name() == crashReportsDirectoryName ||
|
|
entry.Name() == oomReportsDirectoryName ||
|
|
entry.Name() == daemonCrashOutputFileName {
|
|
continue
|
|
}
|
|
entryPath := filepath.Join(workingDirectory, entry.Name())
|
|
if d.listenerPath != "" && entryPath == d.listenerPath {
|
|
continue
|
|
}
|
|
err = os.RemoveAll(entryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Daemon) resetRuntimeOwnerLocked(ownerUserID string) error {
|
|
err := d.clearRuntimeData()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return saveStartOptions(startOptions{OwnerUserID: ownerUserID})
|
|
}
|
|
|
|
func (d *Daemon) stopServiceLocked(nextOwnerUserID string) error {
|
|
options, err := loadStartOptions()
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if d.startedService.Instance() != nil {
|
|
err = d.startedService.CloseService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
crashReportError := tagUnownedReports(filepath.Join(workingDirectory, crashReportsDirectoryName), options.OwnerUserID)
|
|
if crashReportError != nil {
|
|
return crashReportError
|
|
}
|
|
oomReportError := tagUnownedReports(filepath.Join(workingDirectory, oomReportsDirectoryName), options.OwnerUserID)
|
|
if oomReportError != nil {
|
|
return oomReportError
|
|
}
|
|
return d.resetRuntimeOwnerLocked(nextOwnerUserID)
|
|
}
|
|
|
|
func (d *Daemon) Close() {
|
|
d.lifecycleAccess.Lock()
|
|
d.closed = true
|
|
d.lifecycleAccess.Unlock()
|
|
d.server.Stop()
|
|
d.lifecycleAccess.Lock()
|
|
_ = d.startedService.CloseService()
|
|
d.startedService.Close()
|
|
d.lifecycleAccess.Unlock()
|
|
}
|
|
|
|
func (d *Daemon) disconnectPeerConnectionsExcept(userID string) {
|
|
d.peerAccess.Lock()
|
|
var connections []peerConnection
|
|
for connection, identity := range d.peerConnections {
|
|
if identity.UserID != userID {
|
|
connections = append(connections, connection)
|
|
}
|
|
}
|
|
d.peerAccess.Unlock()
|
|
for _, connection := range connections {
|
|
connection.Close()
|
|
}
|
|
}
|
|
|
|
type Authorizer interface {
|
|
Authorize(ctx context.Context, method string) error
|
|
InvokeUnary(ctx context.Context, method string, handler func() (any, error)) (any, error)
|
|
}
|
|
|
|
func newAuthorizer(daemon *Daemon) Authorizer {
|
|
if listenAddress != "" {
|
|
return &allowAllAuthorizer{daemon: daemon}
|
|
}
|
|
return &daemonAuthorizer{daemon: daemon}
|
|
}
|
|
|
|
type allowAllAuthorizer struct {
|
|
daemon *Daemon
|
|
}
|
|
|
|
func (a *allowAllAuthorizer) Authorize(ctx context.Context, method string) error {
|
|
return nil
|
|
}
|
|
|
|
func (a *allowAllAuthorizer) InvokeUnary(ctx context.Context, method string, handler func() (any, error)) (any, error) {
|
|
if ownerProtectedMethod(method) {
|
|
a.daemon.lifecycleAccess.Lock()
|
|
defer a.daemon.lifecycleAccess.Unlock()
|
|
}
|
|
return handler()
|
|
}
|
|
|
|
type daemonAuthorizer struct {
|
|
daemon *Daemon
|
|
}
|
|
|
|
func (a *daemonAuthorizer) Authorize(ctx context.Context, method string) error {
|
|
identity, err := peerIdentityFromContext(ctx)
|
|
if err != nil {
|
|
return status.Error(codes.Unauthenticated, err.Error())
|
|
}
|
|
desktopPrefix := "/" + DesktopService_ServiceDesc.ServiceName + "/"
|
|
if strings.HasPrefix(method, desktopPrefix) {
|
|
return nil
|
|
}
|
|
if ownerProtectedMethod(method) {
|
|
a.daemon.lifecycleAccess.Lock()
|
|
defer a.daemon.lifecycleAccess.Unlock()
|
|
return a.daemon.authorizeOwnerLocked(identity.UserID)
|
|
}
|
|
return status.Error(codes.PermissionDenied, "the service is not available")
|
|
}
|
|
|
|
func (a *daemonAuthorizer) InvokeUnary(ctx context.Context, method string, handler func() (any, error)) (any, error) {
|
|
if !ownerProtectedMethod(method) {
|
|
err := a.Authorize(ctx, method)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return handler()
|
|
}
|
|
identity, err := peerIdentityFromContext(ctx)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Unauthenticated, err.Error())
|
|
}
|
|
a.daemon.lifecycleAccess.Lock()
|
|
defer a.daemon.lifecycleAccess.Unlock()
|
|
err = a.daemon.authorizeOwnerLocked(identity.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return handler()
|
|
}
|
|
|
|
func ownerProtectedMethod(method string) bool {
|
|
startedPrefix := "/" + daemon.StartedService_ServiceDesc.ServiceName + "/"
|
|
managedPrefix := "/" + daemon.ManagedService_ServiceDesc.ServiceName + "/"
|
|
return strings.HasPrefix(method, startedPrefix) || strings.HasPrefix(method, managedPrefix)
|
|
}
|
|
|
|
func (d *Daemon) authorizeOwnerLocked(userID string) error {
|
|
options, err := loadStartOptions()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return status.Error(codes.PermissionDenied, "the service has no owner")
|
|
}
|
|
return err
|
|
}
|
|
if options.OwnerUserID == "" || options.OwnerUserID != userID {
|
|
return status.Error(codes.PermissionDenied, "the service is owned by another user")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newUnaryAuthorizeInterceptor(authorizer Authorizer) grpc.UnaryServerInterceptor {
|
|
return func(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
|
return authorizer.InvokeUnary(ctx, info.FullMethod, func() (any, error) {
|
|
return handler(ctx, request)
|
|
})
|
|
}
|
|
}
|
|
|
|
func newStreamAuthorizeInterceptor(authorizer Authorizer) grpc.StreamServerInterceptor {
|
|
return func(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
err := authorizer.Authorize(stream.Context(), info.FullMethod)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return handler(server, stream)
|
|
}
|
|
}
|