Backend audit fixes (upstream-file edits wrapped in // lx: markers): - experimental/libbox oom_report.go/report.go: OOM reports + configuration.json (server secrets/keys) were written world-writable — 0o777 dirs / 0o666 files → 0o700 / 0o600. [sec-perms] - daemon/server.go + experimental/libbox/command_server.go: gRPC auth secret compared with != (timing oracle) → crypto/subtle.ConstantTimeCompare. [sec-consttime] - service/oomkiller/timer.go: network-extension cleanupTriggered logic was inverted, so FreeOSMemory was never called after a trigger; flip both assignments so a trigger schedules the deferred free and the next poll runs + clears it. [sec-oomcleanup] - transport/v2rayxhttp/client.go (lx-native file): session id used math/rand → crypto/rand, matching Xray's uuid.New() entropy and removing the spoof surface. - daemon/started_service_tailscale_ssh.go: forwardSSHAgentChannel leaked a goroutine + the ssh-agent fd on every closed session (second io.Copy blocked on an idle agent Read forever); tie both copies + the session ctx to a cancel that closes both ends. [sec-sshagent] - daemon/managed_service.go: TriggerOOMReport had no gate — rate-limit to 1/min so an authenticated client can't spin secret-bearing dumps. [sec-oomgate] - route/reachability_lx.go (lx idle-suspend file): idle tick read r.idleStop in select while stopIdleSuspend niled it after close (race + goroutine leak on Close-during-tick); pass the stop channel to the loop by value. go build ./... (default) and the D9 shaterd linux build (tags with_quic,with_wireguard,with_utls,badlinkname,tfogo_checklinkname0,with_xhttp, with_awg,with_lx_command) are green; go vet clean (2 pre-existing unsafe.Pointer warnings in TriggerDebugCrash/debug.go, untouched); go test ./route/... ./daemon/... ./service/oomkiller/... green incl. -race with with_lx_idle_suspend and v2rayxhttp with with_xhttp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
117 lines
3.3 KiB
Go
117 lines
3.3 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
// lx:begin sec-oomgate
|
|
"sync"
|
|
// lx:end sec-oomgate
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/sagernet/sing-box/service/oomkiller"
|
|
"github.com/sagernet/sing/common/memory"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
var _ ManagedServiceServer = (*ManagedService)(nil)
|
|
|
|
type ManagedService struct {
|
|
handler ManagedHandler
|
|
debug bool
|
|
oomReporter oomkiller.OOMReporter
|
|
// lx:begin sec-oomgate
|
|
oomReportMu sync.Mutex
|
|
oomReportLast time.Time
|
|
// lx:end sec-oomgate
|
|
}
|
|
|
|
type ManagedServiceOptions struct {
|
|
Handler ManagedHandler
|
|
Debug bool
|
|
OOMReporter oomkiller.OOMReporter
|
|
}
|
|
|
|
func NewManagedService(options ManagedServiceOptions) *ManagedService {
|
|
return &ManagedService{
|
|
handler: options.Handler,
|
|
debug: options.Debug,
|
|
oomReporter: options.OOMReporter,
|
|
}
|
|
}
|
|
|
|
func (s *ManagedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
|
err := s.handler.ServiceStop()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *ManagedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
|
err := s.handler.ServiceReload()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *ManagedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) {
|
|
return s.handler.SystemProxyStatus()
|
|
}
|
|
|
|
func (s *ManagedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
|
|
err := s.handler.SetSystemProxyEnabled(request.Enabled)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *ManagedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
|
|
if !s.debug {
|
|
return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable")
|
|
}
|
|
if request == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "missing debug crash request")
|
|
}
|
|
switch request.Type {
|
|
case DebugCrashRequest_GO:
|
|
time.AfterFunc(200*time.Millisecond, func() {
|
|
*(*int)(unsafe.Pointer(uintptr(0))) = 0
|
|
})
|
|
case DebugCrashRequest_NATIVE:
|
|
err := s.handler.TriggerNativeCrash()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, status.Error(codes.InvalidArgument, "unknown debug crash type")
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *ManagedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
|
if s.oomReporter == nil {
|
|
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
|
}
|
|
// lx:begin sec-oomgate
|
|
// Rate-limit operator-triggered reports to at most one per minute: each write
|
|
// dumps process state + the config snapshot (secrets) to disk, so an
|
|
// authenticated client must not be able to spin it in a tight loop.
|
|
s.oomReportMu.Lock()
|
|
if !s.oomReportLast.IsZero() && time.Since(s.oomReportLast) < time.Minute {
|
|
s.oomReportMu.Unlock()
|
|
return nil, status.Error(codes.ResourceExhausted, "OOM report rate-limited (max 1/min)")
|
|
}
|
|
s.oomReportLast = time.Now()
|
|
s.oomReportMu.Unlock()
|
|
// lx:end sec-oomgate
|
|
return &emptypb.Empty{}, s.oomReporter.WriteReport(memory.Total())
|
|
}
|
|
|
|
func (s *ManagedService) mustEmbedUnimplementedManagedServiceServer() {
|
|
}
|