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>
73 lines
2.2 KiB
Go
73 lines
2.2 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
// lx:begin sec-consttime
|
|
"crypto/subtle"
|
|
// lx:end sec-consttime
|
|
"strings"
|
|
|
|
"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/metadata"
|
|
"google.golang.org/grpc/reflection"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func NewServer(startedService *StartedService, secret string) *grpc.Server {
|
|
server := grpc.NewServer(
|
|
grpc.ChainUnaryInterceptor(newUnaryAuthInterceptor(secret), UnaryErrorInterceptor),
|
|
grpc.ChainStreamInterceptor(newStreamAuthInterceptor(secret), StreamErrorInterceptor),
|
|
)
|
|
healthServer := health.NewServer()
|
|
RegisterStartedServiceServer(server, startedService)
|
|
healthServer.SetServingStatus(StartedService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
|
|
grpc_health_v1.RegisterHealthServer(server, healthServer)
|
|
reflection.Register(server)
|
|
return server
|
|
}
|
|
|
|
func newUnaryAuthInterceptor(secret string) grpc.UnaryServerInterceptor {
|
|
return func(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
|
err := authenticate(ctx, secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return handler(ctx, request)
|
|
}
|
|
}
|
|
|
|
func newStreamAuthInterceptor(secret string) grpc.StreamServerInterceptor {
|
|
return func(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
err := authenticate(stream.Context(), secret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return handler(server, stream)
|
|
}
|
|
}
|
|
|
|
func authenticate(ctx context.Context, secret string) error {
|
|
if secret == "" {
|
|
return nil
|
|
}
|
|
md, loaded := metadata.FromIncomingContext(ctx)
|
|
if !loaded {
|
|
return status.Error(codes.Unauthenticated, "missing metadata")
|
|
}
|
|
values := md.Get("authorization")
|
|
if len(values) == 0 {
|
|
return status.Error(codes.Unauthenticated, "missing authorization")
|
|
}
|
|
token, isBearer := strings.CutPrefix(values[0], "Bearer ")
|
|
// lx:begin sec-consttime
|
|
// Constant-time compare: a plain != leaks the secret via response timing.
|
|
if !isBearer || subtle.ConstantTimeCompare([]byte(token), []byte(secret)) != 1 {
|
|
return status.Error(codes.Unauthenticated, "invalid authorization")
|
|
}
|
|
// lx:end sec-consttime
|
|
return nil
|
|
}
|