package daemon import ( "errors" "fmt" "log/slog" "net" "net/http" "net/http/pprof" "strings" "time" ) // isLoopback reports whether the address binds to a loopback interface // (127.0.0.1, ::1, localhost). REQ-123: pprof must be loopback-only. // // An empty host (e.g. ":6060") binds ALL interfaces and is therefore // treated as NON-loopback (F2: loopback-bypass fix). Only an explicit // loopback IP or the "localhost" name is accepted. func isLoopback(addr string) bool { host, _, err := net.SplitHostPort(addr) if err != nil { host = addr } host = strings.TrimSpace(host) // F2: empty host (":6060") binds all interfaces — reject. if host == "" { return false } if host == "localhost" { return true } ip := net.ParseIP(host) if ip != nil { return ip.IsLoopback() } return false } // StartPprof starts the pprof HTTP server on addr. REQ-123: pprof is // unauthenticated and MUST bind to a loopback interface only; this is a // hard invariant (F2: the --pprof-allow-public override was a phantom flag // that was never implemented and has been removed — non-loopback binds are // always refused). func StartPprof(addr string, log *slog.Logger) (*http.Server, error) { if addr == "" { return nil, nil } // REQ-123: pprof must bind to loopback only. This is a hard // invariant; there is no public-bind override. if !isLoopback(addr) { log.Error("pprof refuses non-loopback bind", slog.String("addr", addr), slog.String("reason", "REQ-123: pprof is unauthenticated; loopback-only is a hard invariant")) return nil, fmt.Errorf("pprof: refusing non-loopback bind %s (REQ-123; unauthenticated; loopback-only is a hard invariant)", addr) } mux := http.NewServeMux() mux.HandleFunc("/debug/pprof/", pprof.Index) mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) mux.HandleFunc("/debug/pprof/profile", pprof.Profile) mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) mux.HandleFunc("/debug/pprof/trace", pprof.Trace) mux.Handle("/debug/pprof/heap", pprof.Handler("heap")) mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) mux.Handle("/debug/pprof/block", pprof.Handler("block")) mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex")) server := &http.Server{ Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, } log.Warn("pprof endpoint exposed", slog.String("addr", addr), slog.String("warning", "unauthenticated, operator-only — do not expose publicly")) go func() { err := server.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Error("pprof server stopped", slog.String("addr", addr), slog.Any("err", err)) } }() return server, nil }