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