#ifndef __TCPServer_h__ # define __TCPServer_h__ # include "Command.h" # include "CommandList.h" # include "IPAddressList.h" # include "Socket.h" # include "SubscriptionManager.h" # include "TCPSession.h" # include "TCPSocket.h" # include "TLSInfo.h" # include # include namespace core { /// /// TCPServer /// /// Manage a socket connection as a TCP server type. Connections to the socket are processed through /// the accept functionality. /// /// A list of connections is maintained in a vector object. /// /// This object extends the Command object as well so it can be added to a Console object and /// process commands to display status information. /// class TCPServer : public TCPSocket, public Command { public: /// /// The constructor for the TCPServer object. /// /// @param ePoll the EPoll instance that manages the socket. /// @param url the IP address for the socket to receive connection requests. /// @param port the port number that the socket will listen on. /// @param commandName the name of the command used to invoke the status display for this object. /// TCPServer(EPoll &ePoll, IPAddress address, TLSInfo *tlsInfo, std::string delimiter = " ", int depth = 10, std::string text = ""); /// /// The destructor for this object. /// virtual ~TCPServer(); virtual void sessionErrorHandler(std::string errorString, std::stringstream &out); /// /// getSocketAccept is designed to allow a polymorphic extension of this object to /// return a type of object that extends the definition of the server socket. /// Returning the appropriate session object that extends from Session provides /// the mechanism where the server can select the protocol dialog for the desired /// service. /// virtual TCPSession *getSocketAccept(EPoll &epoll); /// /// The list of sessions that are currently open and being maintained by this object. /// /// This is the one real, strong-owning reference per live session - /// see TCPSession::pin()'s own doc comment for the full reasoning. /// Every other place that keeps a TCPSession* around (Subscription:: /// subscribers, a Zephora-specific sessionsByPlayerName map, etc.) /// is just a non-owning observer and must pin() before touching the /// object. A session stops existing, for real, the instant its /// entry here is erased and every other outstanding pin() (if any /// thread happened to be mid-call) has released its own reference - /// removeFromSessionList() is the only place that ever removes an /// entry, and Thread::run() calls it instead of ever calling /// `delete` on a TCPSession directly (see its own comment). /// std::vector> sessions; /// /// The commands object is a CommandList and is used to store Command objects to be /// parsed and run as data comes into the session. /// CommandList commands; /// /// If not NULL the blacklist object can be assigned to this server socket and the server /// IP addresses connecting to the server attempting to accept a socket are contained in /// this list then the connection is rejected and no accept is granted. /// IPAddressList *blackList; /// /// If not NULL the blacklist object can be assigned to this server socket and the server /// IP addresses connecting to the server attempting to accept a socket are contained in /// this list then the connection is rejected and no accept is granted. /// IPAddressList *whiteList; void removeFromSessionList(TCPSession *session); void output(std::stringstream &out); ///< Output the consoles array to the console. /// /// /// void sendToAll(std::stringstream &out); /// /// Use this sendToAll method to output the contents of the out stream /// to all the connections on the server excluding the sender session. /// void sendToAll(std::stringstream &out, TCPSession &sender, SessionFilter filter); /// /// Use this sendToAll method to output the contents of the out stream /// to all the connections on the server excluding the sender session /// and the entries identified by the passed in filter object. /// void sendToAll(std::stringstream &out, TCPSession &sender); /// /// The Subscription Manager tracks all subscriptions on the server. /// SubscriptionManager subscriptions; /// /// Use the getSessionByAlias to retrieve a session pointer by the value /// of the alias pointer. Returns an already-pin()'d shared_ptr (null /// if not found) rather than a raw pointer, so callers can't /// accidentally hold onto - or use - a reference that isn't actually /// keeping the session alive. /// std::shared_ptr getSessionByAlias(void *alias); protected: /// /// This method is called by the lower socket when a connection request comes into /// the listening and bound server port. /// void connectionRequest() override; /// /// Drops any session that completed a TCP accept() but has never /// had a single byte of real data pass through onDataReceived() - /// covers both a TLS handshake that never finishes and a plain /// connection that never sends anything - once it's been open /// longer than a short grace period. Nothing else in this class /// ever revisits an already-accepted session once it stops /// generating epoll events (Thread::run()'s dispatch loop only /// acts on a session when a real event for it arrives), so a /// connection that completes the handshake and then goes /// permanently silent - exactly what a port scan/prober looks /// like from here, as opposed to a normal scan that just connects /// and immediately closes again, which the existing read-hangup /// path already cleans up fine - would otherwise sit in `sessions` /// forever, one TCPSession + one fd each. Called from /// connectionRequest() (see its own call site) so the sweep keeps /// pace with however fast new connections are actually arriving, /// without needing a dedicated timer/thread of its own; a /// legitimate, already-talking client is never at risk, since /// hasReceivedData latches true permanently on its first real byte /// and is never consulted again after that. /// void sweepStaleSessions(); /// /// The listening socket itself is never a TLS peer - accepting a /// connection on it surfaces as a plain read() failing with ENOTCONN /// (see Socket::receiveData(), which is what actually detects and /// dispatches to connectionRequest() above). When this server is /// TLS-configured, TCPSocket::receiveData()'s normal ctx-based /// dispatch would instead route that same event through /// TLS::receiveData()/SSL_read() against a listening fd that was /// never SSL_accept()-ed at all (real per-connection sessions are /// the only sockets TLS applies to) - confirmed via a real crash /// (SIGSEGV inside SSL_read, called with `this` pointing at this /// server object, not a session). Always using the plain path here, /// regardless of whether tlsInfo is set, keeps connection-detection /// working the same way with or without TLS configured. /// void receiveData(coreutils::ZString &buffer) override; /// /// This method is called when the Command associated with this object is requested /// because a user has typed in the associated command name on a command entry line. /// /// @param the session object to write the output to. /// int processCommand(coreutils::ZString &request, TCPSession &session) override; protected: // Was private - ConsoleServer::logSend() (and any other subclass // that iterates `sessions`, itself public further up) needs to be // able to synchronize against the exact same mutex // connectionRequest()/removeFromSessionList() already use, rather // than reading a shared_ptr vector with no locking of its own. // // Was a plain std::mutex - self-deadlocked the accepting thread on // EVERY single connection to ConsoleServer (confirmed live via a // gdb thread dump caught mid-stall): connectionRequest() locks // `lock`, then calls accept(), which constructs the new session - // whose base Socket constructor logs its own creation via // coreutils::Log, whose destructor calls back into // ConsoleServer::logSend() (exactly the synchronization this was // designed for, per the comment above) - which tries to lock this // SAME mutex again, on the SAME thread that already holds it. // std::mutex is not reentrant, so that thread deadlocked // permanently, every time, without ever completing the accept - // this is why the admin console never responded to anything. // Since all of a server's listening sockets share one fixed-size // EPoll worker-thread pool, each such deadlocked thread is gone // for good - enough accumulated connection attempts (even // sporadic ones, e.g. internet-wide port scanners probing every // port) eventually exhausts every worker thread, at which point // the WHOLE server - every listening socket, not just the // console - stops accepting anything new. This is the actual // root cause of BARANTMail's own multi-day-recurring "all ports // simultaneously stop accepting connections" incidents - nothing // to do with DNS/SPF lookup timing, which was a real, separate, // worthwhile fix but not this bug. recursive_mutex lets the same // thread re-enter safely (identical behavior to std::mutex for // every other thread) without changing the locking design at all. std::recursive_mutex lock; public: /// /// A listening TCPServer/ConsoleServer (as opposed to an accepted /// TCPSession) is constructed once, directly by main(), and lives /// for the whole process - never through the shared_ptr-based /// ownership TCPSession::registerLive()/pin() provide (see their /// own comments), since nothing ever calls `new` on one of these. /// Thread::run()'s epoll dispatch still needs a SAFE way to tell /// "this raw pointer is one of those known-immortal listening /// sockets, fine to call methods on directly" apart from "this is /// an unrecognized/stale pointer that must not be touched at all" /// (see TCPSession::tryGet()'s own comment on why dynamic_cast /// can't make that distinction safely). Registering `this` once, /// in the constructor, into this tiny set is that signal - it's /// checked by address only, never dereferenced for the check /// itself. /// static bool isKnownListeningSocket(void *ptr) { std::lock_guard guard(listeningSocketsMutex()); return listeningSockets().count(ptr) > 0; } private: TCPSession *accept(); static std::mutex &listeningSocketsMutex() { static std::mutex m; return m; } static std::set &listeningSockets() { static std::set s; return s; } }; } #endif