/* * Copyright (C) 1996-2023 The Squid Software Foundation and contributors * * Squid software is distributed under GPLv2+ license and includes * contributions from numerous individuals and organizations. * Please see the COPYING and CONTRIBUTORS files for details. */ /* DEBUG: section 05 Socket Connection Opener */ #include "squid.h" #include "CachePeer.h" #include "comm.h" #include "comm/Connection.h" #include "comm/ConnOpener.h" #include "comm/Loops.h" #include "fd.h" #include "fde.h" #include "globals.h" #include "icmp/net_db.h" #include "ip/QosConfig.h" #include "ip/tools.h" #include "ipcache.h" #include "SquidConfig.h" #include class CachePeer; CBDATA_NAMESPACED_CLASS_INIT(Comm, ConnOpener); Comm::ConnOpener::ConnOpener(const Comm::ConnectionPointer &c, const AsyncCall::Pointer &handler, time_t ctimeout) : AsyncJob("Comm::ConnOpener"), host_(nullptr), temporaryFd_(-1), conn_(c), callback_(handler), totalTries_(0), failRetries_(0), deadline_(squid_curtime + static_cast(ctimeout)) { debugs(5, 3, "will connect to " << c << " with " << ctimeout << " timeout"); assert(conn_); // we know where to go // Sharing a being-modified Connection object with the caller is dangerous, // but we cannot ban (or even check for) that using existing APIs. We do not // want to clone "just in case" because cloning is a bit expensive, and most // callers already have a non-owned Connection object to give us. Until the // APIs improve, we can only check that the connection is not open. assert(!conn_->isOpen()); } Comm::ConnOpener::~ConnOpener() { safe_free(host_); } bool Comm::ConnOpener::doneAll() const { // is the conn_ to be opened still waiting? if (conn_ == nullptr) { return AsyncJob::doneAll(); } // is the callback still to be called? if (callback_ == nullptr || callback_->canceled()) { return AsyncJob::doneAll(); } // otherwise, we must be waiting for something Must(temporaryFd_ >= 0 || calls_.sleep_); return false; } void Comm::ConnOpener::swanSong() { if (callback_ != nullptr) { // inform the still-waiting caller we are dying sendAnswer(Comm::ERR_CONNECT, 0, "Comm::ConnOpener::swanSong"); } // did we abort with a temporary FD assigned? if (temporaryFd_ >= 0) closeFd(); // did we abort while owning an open connection? if (conn_ && conn_->isOpen()) conn_->close(); // did we abort while waiting between retries? if (calls_.sleep_) cancelSleep(); AsyncJob::swanSong(); } void Comm::ConnOpener::setHost(const char * new_host) { // unset and erase if already set. if (host_ != nullptr) safe_free(host_); // set the new one if given. if (new_host != nullptr) host_ = xstrdup(new_host); } const char * Comm::ConnOpener::getHost() const { return host_; } /** * Connection attempt are completed. One way or the other. * Pass the results back to the external handler. */ void Comm::ConnOpener::sendAnswer(Comm::Flag errFlag, int xerrno, const char *why) { // only mark the address good/bad AFTER connect is finished. if (host_ != nullptr) { if (xerrno == 0) // XXX: should not we use errFlag instead? ipcacheMarkGoodAddr(host_, conn_->remote); else { ipcacheMarkBadAddr(host_, conn_->remote); #if USE_ICMP if (Config.onoff.test_reachability) netdbDeleteAddrNetwork(conn_->remote); #endif } } if (callback_ != nullptr) { // avoid scheduling cancelled callbacks, assuming they are common // enough to make this extra check an optimization if (callback_->canceled()) { debugs(5, 4, conn_ << " not calling canceled " << *callback_ << " [" << callback_->id << ']' ); // TODO save the pconn to the pconnPool ? } else { assert(conn_); // free resources earlier and simplify recipients if (errFlag != Comm::OK) conn_->close(); // may not be opened else assert(conn_->isOpen()); typedef CommConnectCbParams Params; Params ¶ms = GetCommParams(callback_); params.conn = conn_; conn_ = nullptr; // release ownership; prevent closure by us params.flag = errFlag; params.xerrno = xerrno; ScheduleCallHere(callback_); } callback_ = nullptr; } // The job will stop without this call because nil callback_ makes // doneAll() true, but this explicit call creates nicer debugging. mustStop(why); } /// cleans up this job I/O state without closing temporaryFd /// required before closing temporaryFd or keeping it in conn_ /// leaves FD bare so must only be called via closeFd() or keepFd() void Comm::ConnOpener::cleanFd() { debugs(5, 4, conn_ << "; temp FD " << temporaryFd_); Must(temporaryFd_ >= 0); fde &f = fd_table[temporaryFd_]; // Our write_handler was set without using Comm::Write API, so we cannot // use a cancellable Pointer-free job callback and simply cancel it here. if (f.write_handler) { /* XXX: We are about to remove write_handler, which was responsible * for deleting write_data, so we have to delete write_data * ourselves. Comm currently calls SetSelect handlers synchronously * so if write_handler is set, we know it has not been called yet. * ConnOpener converts that sync call into an async one, but only * after deleting ptr, so that is not a problem. */ delete static_cast(f.write_data); f.write_data = nullptr; f.write_handler = nullptr; } // Comm::DoSelect does not do this when calling and resetting write_handler // (because it expects more writes to come?). We could mimic that // optimization by resetting Comm "Select" state only when the FD is // actually closed. Comm::SetSelect(temporaryFd_, COMM_SELECT_WRITE, nullptr, nullptr, 0); if (calls_.timeout_ != nullptr) { calls_.timeout_->cancel("Comm::ConnOpener::cleanFd"); calls_.timeout_ = nullptr; } // Comm checkTimeouts() and commCloseAllSockets() do not clear .timeout // when calling timeoutHandler (XXX fix them), so we clear unconditionally. f.timeoutHandler = nullptr; f.timeout = 0; if (calls_.earlyAbort_ != nullptr) { comm_remove_close_handler(temporaryFd_, calls_.earlyAbort_); calls_.earlyAbort_ = nullptr; } } /// cleans I/O state and ends I/O for temporaryFd_ void Comm::ConnOpener::closeFd() { if (temporaryFd_ < 0) return; cleanFd(); // comm_close() below uses COMMIO_FD_WRITECB(fd)->active() to clear Comm // "Select" state. It will not clear ours. XXX: It should always clear // because a callback may have been active but was called before comm_close // Update: we now do this in cleanFd() // Comm::SetSelect(temporaryFd_, COMM_SELECT_WRITE, nullptr, nullptr, 0); comm_close(temporaryFd_); temporaryFd_ = -1; } /// cleans I/O state and moves temporaryFd_ to the conn_ for long-term use void Comm::ConnOpener::keepFd() { Must(conn_ != nullptr); Must(temporaryFd_ >= 0); cleanFd(); conn_->fd = temporaryFd_; temporaryFd_ = -1; } void Comm::ConnOpener::start() { Must(conn_ != nullptr); /* outbound sockets have no need to be protocol agnostic. */ if (!(Ip::EnableIpv6&IPV6_SPECIAL_V4MAPPING) && conn_->remote.isIPv4()) { conn_->local.setIPv4(); } conn_->noteStart(); if (createFd()) doConnect(); } /// called at the end of Comm::ConnOpener::DelayedConnectRetry event void Comm::ConnOpener::restart() { debugs(5, 5, conn_ << " restarting after sleep"); calls_.sleep_ = false; if (createFd()) doConnect(); } /// Create a socket for the future connection or return false. /// If false is returned, done() is guaranteed to return true and end the job. bool Comm::ConnOpener::createFd() { Must(temporaryFd_ < 0); assert(conn_); // our initiators signal abort by cancelling their callbacks if (callback_ == nullptr || callback_->canceled()) return false; temporaryFd_ = comm_open(SOCK_STREAM, IPPROTO_TCP, conn_->local, conn_->flags, host_); if (temporaryFd_ < 0) { sendAnswer(Comm::ERR_CONNECT, 0, "Comm::ConnOpener::createFd"); return false; } // Set TOS if needed. if (conn_->tos && Ip::Qos::setSockTos(temporaryFd_, conn_->tos, conn_->remote.isIPv4() ? AF_INET : AF_INET6) < 0) conn_->tos = 0; #if SO_MARK if (conn_->nfmark && Ip::Qos::setSockNfmark(temporaryFd_, conn_->nfmark) < 0) conn_->nfmark = 0; #endif fd_table[temporaryFd_].tosToServer = conn_->tos; fd_table[temporaryFd_].nfmarkToServer = conn_->nfmark; typedef CommCbMemFunT abortDialer; calls_.earlyAbort_ = JobCallback(5, 4, abortDialer, this, Comm::ConnOpener::earlyAbort); comm_add_close_handler(temporaryFd_, calls_.earlyAbort_); typedef CommCbMemFunT timeoutDialer; calls_.timeout_ = JobCallback(5, 4, timeoutDialer, this, Comm::ConnOpener::timeout); debugs(5, 3, conn_ << " will timeout in " << (deadline_ - squid_curtime)); // Update the fd_table directly because commSetConnTimeout() needs open conn_ assert(temporaryFd_ < Squid_MaxFD); assert(fd_table[temporaryFd_].flags.open); typedef CommTimeoutCbParams Params; Params ¶ms = GetCommParams(calls_.timeout_); params.conn = conn_; fd_table[temporaryFd_].timeoutHandler = calls_.timeout_; fd_table[temporaryFd_].timeout = deadline_; return true; } void Comm::ConnOpener::connected() { Must(temporaryFd_ >= 0); keepFd(); /* * stats.conn_open is used to account for the number of * connections that we have open to the CachePeer, so we can limit * based on the max-conn option. We need to increment here, * even if the connection may fail. */ if (CachePeer *peer=(conn_->getPeer())) ++peer->stats.conn_open; lookupLocalAddress(); /* TODO: remove these fd_table accesses. But old code still depends on fd_table flags to * indicate the state of a raw fd object being passed around. * Also, legacy code still depends on comm_local_port() with no access to Comm::Connection * when those are done comm_local_port can become one of our member functions to do the below. */ Must(fd_table[conn_->fd].flags.open); fd_table[conn_->fd].local_addr = conn_->local; sendAnswer(Comm::OK, 0, "Comm::ConnOpener::connected"); } /// Make an FD connection attempt. void Comm::ConnOpener::doConnect() { Must(conn_ != nullptr); Must(temporaryFd_ >= 0); ++ totalTries_; switch (comm_connect_addr(temporaryFd_, conn_->remote) ) { case Comm::INPROGRESS: debugs(5, 5, conn_ << ": Comm::INPROGRESS"); Comm::SetSelect(temporaryFd_, COMM_SELECT_WRITE, Comm::ConnOpener::InProgressConnectRetry, new Pointer(this), 0); break; case Comm::OK: debugs(5, 5, conn_ << ": Comm::OK - connected"); connected(); break; default: { const int xerrno = errno; ++failRetries_; debugs(5, 7, conn_ << ": failure #" << failRetries_ << " <= " << Config.connect_retries << ": " << xstrerr(xerrno)); if (failRetries_ < Config.connect_retries) { debugs(5, 5, conn_ << ": * - try again"); retrySleep(); return; } else { // send ERROR back to the upper layer. debugs(5, 5, conn_ << ": * - ERR tried too many times already."); sendAnswer(Comm::ERR_CONNECT, xerrno, "Comm::ConnOpener::doConnect"); } } } } /// Close and wait a little before trying to open and connect again. void Comm::ConnOpener::retrySleep() { Must(!calls_.sleep_); closeFd(); calls_.sleep_ = true; eventAdd("Comm::ConnOpener::DelayedConnectRetry", Comm::ConnOpener::DelayedConnectRetry, new Pointer(this), 0.05, 0, false); } /// cleans up this job sleep state void Comm::ConnOpener::cancelSleep() { if (calls_.sleep_) { // It would be nice to delete the sleep event, but it might be out of // the event queue and in the async queue already, so (a) we do not know // whether we can safely delete the call ptr here and (b) eventDelete() // will assert if the event went async. Thus, we let the event run so // that it deletes the call ptr [after this job is gone]. Note that we // are called only when the job ends so this "hanging event" will do // nothing but deleting the call ptr. TODO: Revise eventDelete() API. // eventDelete(Comm::ConnOpener::DelayedConnectRetry, calls_.sleep); calls_.sleep_ = false; debugs(5, 9, conn_ << " stops sleeping"); } } /** * Lookup local-end address and port of the TCP link just opened. * This ensure the connection local details are set correctly */ void Comm::ConnOpener::lookupLocalAddress() { struct sockaddr_storage addr = {}; socklen_t len = sizeof(addr); if (getsockname(conn_->fd, reinterpret_cast(&addr), &len) != 0) { int xerrno = errno; debugs(50, DBG_IMPORTANT, "ERROR: Failed to retrieve TCP/UDP details for socket: " << conn_ << ": " << xstrerr(xerrno)); return; } conn_->local = addr; debugs(5, 6, conn_); } /** Abort connection attempt. * Handles the case(s) when a partially setup connection gets closed early. */ void Comm::ConnOpener::earlyAbort(const CommCloseCbParams &io) { debugs(5, 3, io.conn); calls_.earlyAbort_ = nullptr; // NP: is closing or shutdown better? sendAnswer(Comm::ERR_CLOSING, io.xerrno, "Comm::ConnOpener::earlyAbort"); } /** * Handles the case(s) when a partially setup connection gets timed out. * NP: When commSetConnTimeout accepts generic CommCommonCbParams this can die. */ void Comm::ConnOpener::timeout(const CommTimeoutCbParams &) { debugs(5, 5, conn_ << ": * - ERR took too long to receive response."); calls_.timeout_ = nullptr; sendAnswer(Comm::TIMEOUT, ETIMEDOUT, "Comm::ConnOpener::timeout"); } /* Legacy Wrapper for the retry event after Comm::INPROGRESS * XXX: As soon as Comm::SetSelect() accepts Async calls we can use a ConnOpener::doConnect call */ void Comm::ConnOpener::InProgressConnectRetry(int, void *data) { Pointer *ptr = static_cast(data); assert(ptr); if (ConnOpener *cs = ptr->valid()) { // Ew. we are now outside the all AsyncJob protections. // get back inside by scheduling another call... typedef NullaryMemFunT Dialer; AsyncCall::Pointer call = JobCallback(5, 4, Dialer, cs, Comm::ConnOpener::doConnect); ScheduleCallHere(call); } delete ptr; } /* Legacy Wrapper for the retry event with small delay after errors. * XXX: As soon as eventAdd() accepts Async calls we can use a ConnOpener::restart call */ void Comm::ConnOpener::DelayedConnectRetry(void *data) { Pointer *ptr = static_cast(data); assert(ptr); if (ConnOpener *cs = ptr->valid()) { // Ew. we are now outside the all AsyncJob protections. // get back inside by scheduling another call... typedef NullaryMemFunT Dialer; AsyncCall::Pointer call = JobCallback(5, 4, Dialer, cs, Comm::ConnOpener::restart); ScheduleCallHere(call); } delete ptr; }