Commit 6941b083 authored by jan.koester's avatar jan.koester
Browse files

test

parent 91ee215c
Loading
Loading
Loading
Loading
+1 −0
Changes for CMakeLists.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ set(PROXYPLUS_CORE_SOURCES
    src/plugin/PluginManager.cpp
    src/http/HttpProxyPath.cpp
    src/http/HttpProxyServer.cpp
    src/http/UpstreamConnectionPool.cpp
    src/http/WebSocketProxy.cpp
    src/smtp/SmtpProxyServer.cpp
    src/api/JsonApiServer.cpp
+6 −0
Changes for config/proxyplus.yaml: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -71,6 +71,12 @@ proxyplus:
    # fd-exhaustion incident max_open_files guards against). 0 = never time out idle
    # connections (the old, pre-fix behavior).
    idle_connection_timeout_seconds: 120
    # Upstream connection pooling: idle already-open connections to a backend are reused
    # instead of paying a fresh TCP/TLS(/HTTP-3 probe) handshake per proxied request.
    # max_idle_per_origin caps how many idle connections stay open per upstream origin at
    # once; idle_timeout_seconds discards (rather than reuses) one that's sat idle longer.
    upstream_pool_max_idle_per_origin: 8
    upstream_pool_idle_timeout_seconds: 60

  pid_file: "/run/proxyplus.pid"
  recovery_mode: false
+6 −0
Changes for src/config/BootstrapConfig.cpp: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -50,6 +50,8 @@ const std::set<std::string> &knownLeafPatterns() {
        "/proxyplus/process/group",
        "/proxyplus/process/max_open_files",
        "/proxyplus/process/idle_connection_timeout_seconds",
        "/proxyplus/process/upstream_pool_max_idle_per_origin",
        "/proxyplus/process/upstream_pool_idle_timeout_seconds",
        "/proxyplus/pid_file",
        "/proxyplus/recovery_mode",
        "/proxyplus/maintenance_mode",
@@ -226,6 +228,10 @@ void BootstrapConfig::loadAndValidate() {
    cfg.maxOpenFiles = readInt(conf, "/proxyplus/process/max_open_files", 65536);
    cfg.idleConnectionTimeoutSeconds =
        readInt(conf, "/proxyplus/process/idle_connection_timeout_seconds", 120);
    cfg.upstreamPoolMaxIdlePerOrigin =
        readInt(conf, "/proxyplus/process/upstream_pool_max_idle_per_origin", 8);
    cfg.upstreamPoolIdleTimeoutSeconds =
        readInt(conf, "/proxyplus/process/upstream_pool_idle_timeout_seconds", 60);
    cfg.pidFile = readValue(conf, "/proxyplus/pid_file", "");

    cfg.recoveryMode = readBool(conf, "/proxyplus/recovery_mode", false) ? RecoveryMode::Recover
+11 −0
Changes for src/config/BootstrapConfig.h: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -99,6 +99,17 @@ struct AppConfig {
    // "disabled" for anyone who explicitly wants the old always-open behavior.
    int idleConnectionTimeoutSeconds = 120;

    // UpstreamConnectionPool sizing (see src/http/UpstreamConnectionPool.h) -- reusing already-
    // open upstream connections instead of a fresh TCP/TLS(/HTTP-3 probe) handshake per proxied
    // request. Positive by default for the same reason idleConnectionTimeoutSeconds above is:
    // this fixes a real production overload (bursty real traffic spiking upstream connect() to
    // 100+/sec against a steady handful of client connections/sec, starving whatever request --
    // most visibly a wss:// upgrade -- queued up behind the burst) out of the box.
    // maxIdlePerOrigin caps how many idle connections stay open per upstream origin at once;
    // idleTimeoutSeconds discards (rather than reuses) one that's sat idle longer than that.
    int upstreamPoolMaxIdlePerOrigin = 8;
    int upstreamPoolIdleTimeoutSeconds = 60;

    RecoveryMode recoveryMode = RecoveryMode::Normal;
    MaintenanceMode maintenanceMode = MaintenanceMode::Off;
};
+51 −5
Changes for src/http/HttpProxyServer.cpp: 51 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -113,6 +113,7 @@ HttpProxyController::HttpProxyController(std::vector<netplus::socket *> serverSo
                                          auth::IAuthService &authService,
                                          plugin::PluginManager &pluginManager,
                                          lb::LoadBalancer &loadBalancer,
                                          http::UpstreamConnectionPool &connectionPool,
                                          tls::AcmeChallengeStore *acmeChallenges,
                                          int idleTimeoutSeconds)
    : libhttppp::HttpEvent(std::move(serverSockets), /*timeout=*/1000, /*h2OffloadThreads=*/0,
@@ -123,6 +124,7 @@ HttpProxyController::HttpProxyController(std::vector<netplus::socket *> serverSo
    , _authService(authService)
    , _pluginManager(pluginManager)
    , _loadBalancer(loadBalancer)
    , _connectionPool(connectionPool)
    , _acmeChallenges(acmeChallenges) {}

bool HttpProxyController::tryServeAcmeChallenge(libhttppp::HttpRequest &curreq,
@@ -177,6 +179,32 @@ struct ScopedLbRelease {
        if (lb) lb->release(pool, target, ok);
    }
};

// Mirrors ScopedLbRelease above: inert until arm() is called (once acquire() below has
// actually produced a connection to give back), then guarantees the checked-out HttpClient is
// either returned to the pool (ok=true, set only once the whole request/response cycle
// completed without throwing) or simply destroyed (ok=false, e.g. an exception unwound past
// this point) -- never both and never neither, regardless of how RequestEvent exits.
struct ScopedPoolRelease {
    proxyplus::http::UpstreamConnectionPool *pool = nullptr;
    libhttppp::HttpUrl origin;
    std::unique_ptr<libhttppp::HttpClient> client;
    bool ok = false;

    void arm(proxyplus::http::UpstreamConnectionPool &connectionPool,
             const libhttppp::HttpUrl &upstreamOrigin,
             std::unique_ptr<libhttppp::HttpClient> checkedOut) {
        pool = &connectionPool;
        origin = upstreamOrigin;
        client = std::move(checkedOut);
    }

    ~ScopedPoolRelease() {
        if (pool && client && ok) pool->release(std::move(client), origin);
        // else: client's own destructor runs here, closing the connection -- exactly what
        // happened before pooling existed.
    }
};
} // namespace

void HttpProxyController::applyResponseCompression(libhttppp::HttpRequest &curreq,
@@ -426,12 +454,19 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
        }

        bool upstreamOk = false;
        ScopedPoolRelease poolGuard; // inert unless acquire() below actually hands out a connection
        try {
            libhttppp::HttpUrl upstream(upstreamBase + forwardPath);
            // vers=3: try HTTP/3 first, falling back to HTTP/2 then HTTP/1.1 over
            // TLS (or straight to H1 for a cleartext upstream) -- see HttpClient
            // Reuse an already-open connection to this origin if the pool has an idle one
            // (see UpstreamConnectionPool -- this is what avoids paying a fresh TCP/TLS(/HTTP-3
            // probe) handshake on every single proxied request); vers=3: try HTTP/3 first,
            // falling back to HTTP/2 then HTTP/1.1 over TLS (or straight to H1 for a cleartext
            // upstream) when a new connection actually needs constructing -- see HttpClient
            // ctor in libhttppp/src/http.cpp for the fallback ladder.
            libhttppp::HttpClient client(upstream, /*vers=*/3, /*timeoutSec=*/60, trustPolicyFor(matched));
            auto clientPtr = _connectionPool.acquire(upstream, /*vers=*/3, /*timeoutSec=*/60,
                                                      trustPolicyFor(matched));
            libhttppp::HttpClient &client = *clientPtr;
            poolGuard.arm(_connectionPool, upstream, std::move(clientPtr));
            // A reverse proxy must hand 3xx responses straight back to the real client
            // (which owns cookie storage/navigation), not chase them itself -- HttpClient
            // otherwise follows redirects like a browser and silently swallows the
@@ -461,7 +496,12 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
                }
            }
            outreq.setHeaderData("host")->push_back(upstream.getHost());
            outreq.setHeaderData("connection")->push_back("close");
            // Deliberately no "Connection: close" here (unlike before pooling existed) --
            // forcing that on every request is exactly what defeated reuse; the upstream is now
            // allowed to keep the connection alive so UpstreamConnectionPool can hand it back
            // out for a later request. If the upstream itself sends "Connection: close" (or an
            // H2 GOAWAY) in its response, HttpClient::wantsClose() picks that up and the pool
            // won't reuse that instance regardless (see ScopedPoolRelease below).

            // Standard reverse-proxy hints for the upstream: X-Forwarded-For chains (append to
            // whatever a prior proxy already put there, via the copy the loop above already
@@ -555,6 +595,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
        }

        lbGuard.ok = upstreamOk;
        poolGuard.ok = upstreamOk;
    } catch (const libhttppp::HTTPException &e) {
        std::cerr << "[http] request handling failed: " << e.what() << std::endl;
    }
@@ -565,6 +606,7 @@ HttpProxyServer::HttpProxyServer(config::ListenerConfig listener, std::string ce
                                  auth::IAuthService &authService,
                                  plugin::PluginManager &pluginManager,
                                  lb::LoadBalancer &loadBalancer,
                                  http::UpstreamConnectionPool &connectionPool,
                                  tls::AcmeChallengeStore *acmeChallenges,
                                  int idleTimeoutSeconds)
    : _listener(listener)
@@ -575,6 +617,7 @@ HttpProxyServer::HttpProxyServer(config::ListenerConfig listener, std::string ce
    , _authService(authService)
    , _pluginManager(pluginManager)
    , _loadBalancer(loadBalancer)
    , _connectionPool(connectionPool)
    , _acmeChallenges(acmeChallenges)
    , _idleTimeoutSeconds(idleTimeoutSeconds) {
    bindAndListen();
@@ -584,6 +627,7 @@ HttpProxyServer::HttpProxyServer(config::ListenerConfig listener,
                                  const std::map<std::string, netplus::ssl::CertificateBundle> &certsByHostname,
                                  store::ConfigStore &configStore, auth::IAuthService &authService,
                                  plugin::PluginManager &pluginManager, lb::LoadBalancer &loadBalancer,
                                  http::UpstreamConnectionPool &connectionPool,
                                  tls::AcmeChallengeStore *acmeChallenges,
                                  int idleTimeoutSeconds)
    : _listener(listener)
@@ -593,6 +637,7 @@ HttpProxyServer::HttpProxyServer(config::ListenerConfig listener,
    , _authService(authService)
    , _pluginManager(pluginManager)
    , _loadBalancer(loadBalancer)
    , _connectionPool(connectionPool)
    , _acmeChallenges(acmeChallenges)
    , _idleTimeoutSeconds(idleTimeoutSeconds) {
    bindAndListen();
@@ -613,7 +658,8 @@ void HttpProxyServer::run() {
    HttpProxyController controller(_httpd->getServerSockets(),
                                    _listener.address + ":" + std::to_string(_listener.port),
                                    _tlsActive, _configStore, _authService, _pluginManager,
                                    _loadBalancer, _acmeChallenges, _idleTimeoutSeconds);
                                    _loadBalancer, _connectionPool, _acmeChallenges,
                                    _idleTimeoutSeconds);
    controller.runEventloop();
}

Loading