Loading config/proxyplus.yaml +5 −0 Changes for config/proxyplus.yaml: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -61,6 +61,11 @@ proxyplus: process: user: "proxy" group: "proxy" # Open-file ceiling, raised via setrlimit() before privileges are dropped (needs to run # while still root). Covers every long-lived fd: HTTP/1.1+H2+H3 client sockets, # load-balanced upstream connections, and each WebSocket tunnel's socket pair + its own # kqueue/epoll fd. 0 = leave the OS/login-class default alone. max_open_files: 65536 pid_file: "/run/proxyplus.pid" recovery_mode: false Loading src/config/BootstrapConfig.cpp +2 −0 Changes for src/config/BootstrapConfig.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ const std::set<std::string> &knownLeafPatterns() { "/proxyplus/logging/targets", "/proxyplus/process/user", "/proxyplus/process/group", "/proxyplus/process/max_open_files", "/proxyplus/pid_file", "/proxyplus/recovery_mode", "/proxyplus/maintenance_mode", Loading Loading @@ -221,6 +222,7 @@ void BootstrapConfig::loadAndValidate() { cfg.processUser = readValue(conf, "/proxyplus/process/user", ""); cfg.processGroup = readValue(conf, "/proxyplus/process/group", ""); cfg.maxOpenFiles = readInt(conf, "/proxyplus/process/max_open_files", 65536); cfg.pidFile = readValue(conf, "/proxyplus/pid_file", ""); cfg.recoveryMode = readBool(conf, "/proxyplus/recovery_mode", false) ? RecoveryMode::Recover Loading src/config/BootstrapConfig.h +10 −0 Changes for src/config/BootstrapConfig.h: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -78,6 +78,16 @@ struct AppConfig { std::string processGroup; std::string pidFile; // Raised via setrlimit(RLIMIT_NOFILE, ...) before privileges are dropped (see main.cpp's // raiseFileDescriptorLimit() -- only root/CAP_SYS_RESOURCE can raise the hard limit past // whatever the OS/login-class default is). Needs to cover every long-lived fd this process // holds at once: per-client sockets across HTTP/1.1, H2 and H3/QUIC, load-balanced upstream // connections, and each WebSocket tunnel's socket *pair* plus its own dedicated kqueue/epoll // fd (see WebSocketProxy.cpp's kMaxConcurrentWsTunnels) -- exactly the ceiling that produced // "socketwait: kqueue() failed: Too many open files" in production. 0 = leave the OS default // alone (opt-out escape hatch for a platform where this is managed externally). int maxOpenFiles = 65536; RecoveryMode recoveryMode = RecoveryMode::Normal; MaintenanceMode maintenanceMode = MaintenanceMode::Off; }; Loading src/main.cpp +29 −0 Changes for src/main.cpp: 29 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -8,9 +8,11 @@ #include <thread> #include <vector> #include <cstring> #include <fcntl.h> #include <grp.h> #include <pwd.h> #include <sys/resource.h> #include <sys/stat.h> #include <unistd.h> Loading Loading @@ -141,6 +143,31 @@ std::pair<std::string, std::string> readServiceCredential(const std::string &dat return {line.substr(0, sep), line.substr(sep + 1)}; } // Raises this process's open-file ceiling while it can still do so -- only root/ // CAP_SYS_RESOURCE may raise rlim_max past its current value, and dropPrivileges() gives that // up for good, so this must run before it (and as early as possible generally, since // ConfigStore/AuthDBAdapter/CertificateManager/every listener bind below all consume fds // against whatever ceiling is in effect at the time they open one). Left at the OS/login-class // default otherwise, which is easy to exhaust once long-lived connections pile up -- see // AppConfig::maxOpenFiles's comment for exactly which fds count against it. Never fatal: a // process that can't raise its own ceiling should still try to run at whatever it's got, not // refuse to start. void raiseFileDescriptorLimit(const proxyplus::config::AppConfig &cfg) { if (cfg.maxOpenFiles <= 0) return; struct rlimit rl; if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) { std::cerr << "[main] WARNING: getrlimit(RLIMIT_NOFILE) failed: " << strerror(errno) << std::endl; return; } rlim_t want = static_cast<rlim_t>(cfg.maxOpenFiles); if (::geteuid() == 0 && rl.rlim_max < want) rl.rlim_max = want; rl.rlim_cur = std::min(want, rl.rlim_max); if (::setrlimit(RLIMIT_NOFILE, &rl) != 0) { std::cerr << "[main] WARNING: setrlimit(RLIMIT_NOFILE, " << rl.rlim_cur << ") failed: " << strerror(errno) << std::endl; } } void dropPrivileges(const proxyplus::config::AppConfig &cfg) { if (::geteuid() != 0) return; if (cfg.processGroup.empty() || cfg.processUser.empty()) { Loading Loading @@ -233,6 +260,8 @@ int main(int argc, char **argv) { proxyplus::config::BootstrapConfig bootstrap(argv[1]); auto cfg = bootstrap.get(); raiseFileDescriptorLimit(cfg); if (cfg.maintenanceMode == proxyplus::config::MaintenanceMode::On) { std::cerr << "[main] maintenance mode: only the admin API will be started" << std::endl; } Loading Loading
config/proxyplus.yaml +5 −0 Changes for config/proxyplus.yaml: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -61,6 +61,11 @@ proxyplus: process: user: "proxy" group: "proxy" # Open-file ceiling, raised via setrlimit() before privileges are dropped (needs to run # while still root). Covers every long-lived fd: HTTP/1.1+H2+H3 client sockets, # load-balanced upstream connections, and each WebSocket tunnel's socket pair + its own # kqueue/epoll fd. 0 = leave the OS/login-class default alone. max_open_files: 65536 pid_file: "/run/proxyplus.pid" recovery_mode: false Loading
src/config/BootstrapConfig.cpp +2 −0 Changes for src/config/BootstrapConfig.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ const std::set<std::string> &knownLeafPatterns() { "/proxyplus/logging/targets", "/proxyplus/process/user", "/proxyplus/process/group", "/proxyplus/process/max_open_files", "/proxyplus/pid_file", "/proxyplus/recovery_mode", "/proxyplus/maintenance_mode", Loading Loading @@ -221,6 +222,7 @@ void BootstrapConfig::loadAndValidate() { cfg.processUser = readValue(conf, "/proxyplus/process/user", ""); cfg.processGroup = readValue(conf, "/proxyplus/process/group", ""); cfg.maxOpenFiles = readInt(conf, "/proxyplus/process/max_open_files", 65536); cfg.pidFile = readValue(conf, "/proxyplus/pid_file", ""); cfg.recoveryMode = readBool(conf, "/proxyplus/recovery_mode", false) ? RecoveryMode::Recover Loading
src/config/BootstrapConfig.h +10 −0 Changes for src/config/BootstrapConfig.h: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -78,6 +78,16 @@ struct AppConfig { std::string processGroup; std::string pidFile; // Raised via setrlimit(RLIMIT_NOFILE, ...) before privileges are dropped (see main.cpp's // raiseFileDescriptorLimit() -- only root/CAP_SYS_RESOURCE can raise the hard limit past // whatever the OS/login-class default is). Needs to cover every long-lived fd this process // holds at once: per-client sockets across HTTP/1.1, H2 and H3/QUIC, load-balanced upstream // connections, and each WebSocket tunnel's socket *pair* plus its own dedicated kqueue/epoll // fd (see WebSocketProxy.cpp's kMaxConcurrentWsTunnels) -- exactly the ceiling that produced // "socketwait: kqueue() failed: Too many open files" in production. 0 = leave the OS default // alone (opt-out escape hatch for a platform where this is managed externally). int maxOpenFiles = 65536; RecoveryMode recoveryMode = RecoveryMode::Normal; MaintenanceMode maintenanceMode = MaintenanceMode::Off; }; Loading
src/main.cpp +29 −0 Changes for src/main.cpp: 29 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -8,9 +8,11 @@ #include <thread> #include <vector> #include <cstring> #include <fcntl.h> #include <grp.h> #include <pwd.h> #include <sys/resource.h> #include <sys/stat.h> #include <unistd.h> Loading Loading @@ -141,6 +143,31 @@ std::pair<std::string, std::string> readServiceCredential(const std::string &dat return {line.substr(0, sep), line.substr(sep + 1)}; } // Raises this process's open-file ceiling while it can still do so -- only root/ // CAP_SYS_RESOURCE may raise rlim_max past its current value, and dropPrivileges() gives that // up for good, so this must run before it (and as early as possible generally, since // ConfigStore/AuthDBAdapter/CertificateManager/every listener bind below all consume fds // against whatever ceiling is in effect at the time they open one). Left at the OS/login-class // default otherwise, which is easy to exhaust once long-lived connections pile up -- see // AppConfig::maxOpenFiles's comment for exactly which fds count against it. Never fatal: a // process that can't raise its own ceiling should still try to run at whatever it's got, not // refuse to start. void raiseFileDescriptorLimit(const proxyplus::config::AppConfig &cfg) { if (cfg.maxOpenFiles <= 0) return; struct rlimit rl; if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) { std::cerr << "[main] WARNING: getrlimit(RLIMIT_NOFILE) failed: " << strerror(errno) << std::endl; return; } rlim_t want = static_cast<rlim_t>(cfg.maxOpenFiles); if (::geteuid() == 0 && rl.rlim_max < want) rl.rlim_max = want; rl.rlim_cur = std::min(want, rl.rlim_max); if (::setrlimit(RLIMIT_NOFILE, &rl) != 0) { std::cerr << "[main] WARNING: setrlimit(RLIMIT_NOFILE, " << rl.rlim_cur << ") failed: " << strerror(errno) << std::endl; } } void dropPrivileges(const proxyplus::config::AppConfig &cfg) { if (::geteuid() != 0) return; if (cfg.processGroup.empty() || cfg.processUser.empty()) { Loading Loading @@ -233,6 +260,8 @@ int main(int argc, char **argv) { proxyplus::config::BootstrapConfig bootstrap(argv[1]); auto cfg = bootstrap.get(); raiseFileDescriptorLimit(cfg); if (cfg.maintenanceMode == proxyplus::config::MaintenanceMode::On) { std::cerr << "[main] maintenance mode: only the admin API will be started" << std::endl; } Loading