Commit cc665827 authored by jan.koester's avatar jan.koester
Browse files

test

parent e79f7fa7
Loading
Loading
Loading
Loading
+4 −5
Changes for src/http/HttpProxyServer.cpp: 4 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -393,7 +393,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
        }

        std::string upstreamBase;
        std::string pickedTarget; // only set (and only release()d) when a pool picked it
        ScopedLbRelease lbGuard; // inert unless a pool actually picks a target below
        if (!matched.upstreamPool.empty()) {
            auto picked = _loadBalancer.pick(matched.upstreamPool, sourceIp);
            if (!picked) {
@@ -401,7 +401,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
                          "upstream pool '" + matched.upstreamPool + "' has no available targets");
                return;
            }
            pickedTarget = *picked;
            lbGuard.arm(_loadBalancer, matched.upstreamPool, *picked);
            upstreamBase = *picked;
        } else {
            upstreamBase = matched.upstream;
@@ -418,9 +418,8 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
        // WebSocketProxy.h for why this always speaks H1.1 to the upstream
        // regardless of what protocol curreq itself arrived over.
        if (method == "GET" && isWebSocketUpgrade(curreq)) {
            bool ok = handleWebSocketUpgrade(curreq, sourceIp, upstreamBase, forwardPath,
            lbGuard.ok = handleWebSocketUpgrade(curreq, sourceIp, upstreamBase, forwardPath,
                                                 trustPolicyFor(matched));
            if (!pickedTarget.empty()) _loadBalancer.release(matched.upstreamPool, pickedTarget, ok);
            return;
        }

@@ -553,7 +552,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int
            sendError(curreq, HTTP502, std::string("upstream error: ") + e.what());
        }

        if (!pickedTarget.empty()) _loadBalancer.release(matched.upstreamPool, pickedTarget, upstreamOk);
        lbGuard.ok = upstreamOk;
    } catch (const libhttppp::HTTPException &e) {
        std::cerr << "[http] request handling failed: " << e.what() << std::endl;
    }
+59 −8
Changes for src/http/WebSocketProxy.cpp: 59 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@
#include <atomic>
#include <cctype>
#include <cstring>
#include <ctime>
#include <iostream>
#include <map>
#include <set>
@@ -11,6 +12,8 @@
#include <thread>
#include <vector>

#include <sys/socket.h>

#include <netplus/connection.h>
#include <netplus/eventapi.h>
#include <netplus/exception.h>
@@ -104,6 +107,36 @@ std::unique_ptr<netplus::socket> connectUpstream(const libhttppp::HttpUrl &upstr
    return tcpsock;
}

// Bounds every blocking send/recv this tunnel's sockets ever do -- both during the handshake
// (sendAll()/readUntilHeadersEnd() below, which run inline on a shared event-loop worker
// thread, *before* ScopedTunnelSlot even applies) and for the tunnel's entire relayed lifetime
// (relayLoop(), once it's off on its own dedicated thread). Without this, a peer that accepts a
// connection but stops draining/filling its side -- TCP window closes, or the path is silently
// blackholed by a NAT/middlebox without RST/FIN -- hangs the blocking call forever, and during
// the handshake phase that risks stalling the shared worker pool, not just leaking fds (the
// same failure shape as project_proxyplus_h3_thread_starvation). SO_KEEPALIVE additionally lets
// the OS notice a genuinely vanished peer even while both sides are legitimately idle -- WS
// pings alone don't cover a network path that goes quiet without a clean close (phone screen
// lock, laptop sleep, NAT rebind).
//
// Known gap: this only protects the *read* side end-to-end. netplus::ssl's send path
// (tls::flush_out) currently swallows a timeout-driven EAGAIN as if the write had fully
// succeeded rather than propagating it (see project_proxyplus_fd_limit_exhaustion in memory) --
// so for a wss:// client or https:// upstream specifically, a stalled *write* still won't be
// caught by this timeout alone. relayLoop()'s own idle-activity check below is what actually
// catches that case, just later (after the idle window elapses) rather than immediately.
constexpr int kWsSocketTimeoutSeconds = 30;

// See relayLoop()'s idle-tracking comment for what this actually guards against (a
// connection that's alive-but-silent forever, as opposed to a stalled send/recv).
constexpr int kWsIdleTimeoutSeconds = 300;

void hardenTunnelSocket(netplus::socket &sock) {
    sock.setTimeout(kWsSocketTimeoutSeconds * 1000);
    int keepalive = 1;
    ::setsockopt(sock.fd(), SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
}

void sendAll(netplus::socket &sock, const std::string &data) {
    size_t sent = 0;
    while (sent < data.size()) {
@@ -172,13 +205,13 @@ void relayLoop(std::unique_ptr<netplus::socket> client,
               std::string upstreamLeftover,
               ScopedTunnelSlot slot) {
    try {
        // This thread owns both sockets exclusively for its whole lifetime
        // (nothing else will ever touch client's fd again -- see
        // detachConnection()'s contract), so there's no reason to deal with
        // EAGAIN/Note here: plain blocking reads/writes are simplest and
        // correct. client arrives non-blocking (the event loop always runs
        // non-blocking sockets); upstream is fresh from connectUpstream() and
        // likely already blocking, but set explicitly rather than assume.
        // This thread owns both sockets exclusively for its whole lifetime (nothing else will
        // ever touch client's fd again -- see detachConnection()'s contract), so plain blocking
        // reads/writes are simplest and correct -- a hardenTunnelSocket()-induced EAGAIN/Note
        // from a timed-out call is just another way this loop ends, caught below like any other
        // disconnect. client arrives non-blocking (the event loop always runs non-blocking
        // sockets); upstream is fresh from connectUpstream() and likely already blocking, but
        // set explicitly rather than assume.
        client->setBlock();
        upstream->setBlock();

@@ -189,21 +222,37 @@ void relayLoop(std::unique_ptr<netplus::socket> client,
        std::vector<netplus::socket *> socks{client.get(), upstream.get()};
        std::vector<bool> ready;

        // Belt-and-suspenders alongside hardenTunnelSocket()'s SO_RCVTIMEO/SO_SNDTIMEO/
        // SO_KEEPALIVE: those bound any *single* blocking call, but a peer that's still
        // technically reachable (each individual send/recv succeeds) yet never actually sends
        // anything meaningful again -- e.g. a client that opens the tunnel and the network path
        // then goes quiet without ever tearing down the TCP connection -- would otherwise pin
        // this thread and its 3 fds (client + upstream sockets, sw's own kqueue/epoll fd)
        // forever. kWsIdleTimeoutSeconds is generous headroom above typical WS ping/pong
        // intervals (Home Assistant, browsers: 20-30s) so it only ever fires on a connection
        // that's genuinely been abandoned, not a quiet-but-alive one.
        time_t lastActivity = std::time(nullptr);

        for (;;) {
            int n = sw.waitReadMulti(socks, ready, /*timeout_ms=*/60000);
            if (n <= 0) continue; // timeout -- keep waiting, WebSocket pings keep this alive
            if (n <= 0) {
                if (std::time(nullptr) - lastActivity > kWsIdleTimeoutSeconds) break;
                continue; // timeout -- keep waiting, WebSocket pings keep this alive
            }

            if (ready[0]) {
                netplus::buffer buf(16384);
                size_t r = client->recvData(buf, 0);
                if (r == 0) break; // client closed
                sendAll(*upstream, std::string(buf.data.buf, r));
                lastActivity = std::time(nullptr);
            }
            if (ready[1]) {
                netplus::buffer buf(16384);
                size_t r = upstream->recvData(buf, 0);
                if (r == 0) break; // upstream closed
                sendAll(*client, std::string(buf.data.buf, r));
                lastActivity = std::time(nullptr);
            }
        }
    } catch (const netplus::NetException &) {
@@ -232,6 +281,7 @@ bool handleWebSocketUpgrade(libhttppp::HttpRequest &curreq,
    try {
        libhttppp::HttpUrl upstream(upstreamBase + forwardPath);
        auto upstreamSock = connectUpstream(upstream, trustPolicy, /*timeoutSec=*/30);
        hardenTunnelSocket(*upstreamSock);

        // Build the raw HTTP/1.1 request text. Unlike RequestEvent's normal
        // proxying path, Connection/Upgrade (and the Sec-WebSocket-* set)
@@ -302,6 +352,7 @@ bool handleWebSocketUpgrade(libhttppp::HttpRequest &curreq,
            upstreamSock->close();
            return true;
        }
        hardenTunnelSocket(*clientSock);

        std::vector<char> leftoverVec(curreq.RecvData.begin(), curreq.RecvData.end());
        std::string clientLeftover(leftoverVec.begin(), leftoverVec.end());