]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Revised Connection cloning during ConnOpener creation
authorAlex Rousskov <rousskov@measurement-factory.com>
Thu, 5 Aug 2021 21:56:12 +0000 (17:56 -0400)
committerAlex Rousskov <rousskov@measurement-factory.com>
Thu, 5 Aug 2021 22:57:35 +0000 (18:57 -0400)
Item 1 in recent branch commit 21f1082 correctly identified the
Connection sharing problem, but its solution (i.e. let ConnOpener clone
the passed Connection) was not only somewhat expensive but buggy:
ConnOpener assumed that cloneDestinationDetails() is the right cloning
method, but that method missed Connection properties like tos and nfmark
(at least). Another method -- cloneIdentDetails() would copy those, but
calling that method in non-IDENT contexts would look weird, and even
that method could have missed something (now or in the future).

The core problem here was not in the correct cloning method selection,
but that ConnOpener is not in a position to select at all! ConnOpener
should be using/forwarding all the provided Connection details, not
guessing which ones are relevant to the _caller_. After that became
clear, we had two choices:

A. ConnOpener clones everything. This is a simple, safe choice. However,
   it means double-cloning: First, the caller would need to clone to
   copy just the right set of Connection members (or effectively clear
   the irrelevant ones). And then ConnOpener would clone again to break
   the Connection ownership link with poorly implemented callers. In
   some cases, that cloning was already following ResolvedPeers cloning,
   resulting in triple cloning of each used destination on the common
   forwarding path! Such cloning is both expensive and complicates
   triage by creating long chains of Connection IDs.

B. ConnOpener clones nothing; ConnOpener creator is responsible for
   supplying ConnOpener with a non-owned Connection object (that does
   not need to be cloned by ConnOpener). This is a clean, efficient,
   forward-looking solution but it is a bit less safe because the
   compiler will not identify buggy creators that share a Connection
   object they own with ConnOpener.

After weighing all these impossible-to-compare factors, we went with B.

Implementing B required adjusting several callers that were still
passing the Connections they owned to ConnOpener.

Also, after comparing the needs of existing and newly added ("clone
everything") Connection cloning method callers, we decided there is no
need to maintain three different methods. All existing callers should be
fine with a single method because none of them suffers from "extra"
copying of members that others need. Right now, the new cloneProfile()
method copies everything except FD and a few special-purpose members
(with documented reasons for not copying).

src/HappyConnOpener.cc
src/ResolvedPeers.cc
src/comm/ConnOpener.cc
src/comm/Connection.cc
src/comm/Connection.h
src/ident/Ident.cc
src/servers/FtpServer.cc
src/tests/stub_libcomm.cc

index 2ddf3f0786b067eabeebf80632e267478f4a13c7..94eef88104d4ac5b877cfb1f1510afa50efb5d4a 100644 (file)
@@ -565,21 +565,20 @@ HappyConnOpener::openFreshConnection(Attempt &attempt, PeerConnectionPointer &de
     entry->mem_obj->checkUrlChecksum();
 #endif
 
-    GetMarkingsToServer(cause.getRaw(), *dest);
+    const auto conn = dest->cloneProfile();
+    GetMarkingsToServer(cause.getRaw(), *conn);
 
-    // ConnOpener modifies its destination argument so we reset the source port
-    // in case we are reusing the destination already used by our predecessor.
-    dest->local.port(0);
     ++n_tries;
 
     typedef CommCbMemFunT<HappyConnOpener, CommConnectCbParams> Dialer;
     AsyncCall::Pointer callConnect = asyncCall(48, 5, attempt.callbackMethodName,
                                      Dialer(this, attempt.callbackMethod));
     const time_t connTimeout = dest->connectTimeout(fwdStart);
-    Comm::ConnOpener *cs = new Comm::ConnOpener(dest, callConnect, connTimeout);
-    if (!dest->getPeer())
+    Comm::ConnOpener *cs = new Comm::ConnOpener(conn, callConnect, connTimeout);
+    if (!conn->getPeer())
         cs->setHost(host_);
 
+    attempt.path = dest; // but not the being-opened conn!
     attempt.connWait.start(cs, callConnect);
 
     AsyncJob::Start(cs);
index a7accc36e5016cf0e13f079215eff7f94781382e..8079ff3b7599cdc684dcabe6a606de50c58a3763 100644 (file)
@@ -151,7 +151,7 @@ ResolvedPeers::extractFound(const char *description, const Paths::iterator &foun
         while (++pathsToSkip < paths_.size() && !paths_[pathsToSkip].available) {}
     }
 
-    const auto cleanPath = path.connection->cloneDestinationDetails();
+    const auto cleanPath = path.connection->cloneProfile();
     return PeerConnectionPointer(cleanPath, found - paths_.begin());
 }
 
index 6c7032b6ebf91991ea9ff29173b32c1672542e49..58c4c9db6b0a7be87fee724fd44d7e3d518f0d3d 100644 (file)
@@ -34,13 +34,21 @@ Comm::ConnOpener::ConnOpener(const Comm::ConnectionPointer &c, const AsyncCall::
     AsyncJob("Comm::ConnOpener"),
     host_(NULL),
     temporaryFd_(-1),
-    conn_(c->cloneDestinationDetails()),
+    conn_(c),
     callback_(handler),
     totalTries_(0),
     failRetries_(0),
     deadline_(squid_curtime + static_cast<time_t>(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()
index 72e51b84d7edcfeaf7ad1c7ba67b04ace18e9976..7e2cad2b3022e8110da4be39dbe7a6139d93c1ae 100644 (file)
@@ -61,27 +61,45 @@ Comm::Connection::~Connection()
 }
 
 Comm::ConnectionPointer
-Comm::Connection::cloneDestinationDetails() const
+Comm::Connection::cloneProfile() const
 {
-    const ConnectionPointer c = new Comm::Connection;
-    c->setAddrs(local, remote);
-    c->peerType = peerType;
-    c->flags = flags;
-    c->peer_ = cbdataReference(getPeer());
-    debugs(5, 5, this << " made " << c);
-    assert(!c->isOpen());
-    return c;
-}
+    const ConnectionPointer clone = new Comm::Connection;
+    auto &c = *clone; // optimization
+
+    /*
+     * Copy or excuse each data member. Excused members do not belong to a
+     * Connection configuration profile because their values cannot be reused
+     * across (co-existing) Connection objects and/or are tied to their own
+     * object lifetime.
+     */
+
+    c.setAddrs(local, remote);
+    c.peerType = peerType;
+    // fd excused
+    c.tos = tos;
+    c.nfmark = nfmark;
+    c.nfConnmark = nfConnmark;
+    // COMM_ORPHANED is not a part of connection opening instructions
+    c.flags = flags & ~COMM_ORPHANED;
+    if (*rfc931) // optimization
+        memcpy(c.rfc931, rfc931, sizeof(rfc931));
+
+#if USE_SQUID_EUI
+    // These are currently only set when accepting connections and never used
+    // for establishing new ones, so this copying is currently in vain, but,
+    // technically, they can be a part of connection opening instructions.
+    c.remoteEui48 = remoteEui48;
+    c.remoteEui64 = remoteEui64;
+#endif
 
-Comm::ConnectionPointer
-Comm::Connection::cloneIdentDetails() const
-{
-    auto c = cloneDestinationDetails();
-    c->tos = tos;
-    c->nfmark = nfmark;
-    c->nfConnmark = nfConnmark;
-    c->startTime_ = startTime_;
-    return c;
+    // id excused
+    c.peer_ = cbdataReference(getPeer());
+    // startTime_ excused
+    // tlsHistory excused
+
+    debugs(5, 5, this << " made " << c);
+    assert(!c.isOpen());
+    return clone;
 }
 
 void
index 202358b2c71e87136bbdefd5b41c898f0d9eb5ad..02ecb04698d1083b8a7ad852d6305b5ba0230003 100644 (file)
@@ -77,13 +77,12 @@ public:
     /** Clear the connection properties and close any open socket. */
     virtual ~Connection();
 
-    /// Create a new (closed) IDENT Connection object based on our from-Squid
-    /// connection properties.
-    ConnectionPointer cloneIdentDetails() const;
+    /// To prevent accidental copying of Connection objects that we started to
+    /// open or that are open, use cloneProfile() instead.
+    Connection(const Connection &&) = delete;
 
-    /// Create a new (closed) Connection object pointing to the same destination
-    /// as this from-Squid connection.
-    ConnectionPointer cloneDestinationDetails() const;
+    /// Create a new closed Connection with the same configuration as this one.
+    ConnectionPointer cloneProfile() const;
 
     /// close the still-open connection when its last reference is gone
     void enterOrphanage() { flags |= COMM_ORPHANED; }
@@ -140,17 +139,6 @@ public:
     virtual ScopedId codeContextGist() const override;
     virtual std::ostream &detailCodeContext(std::ostream &os) const override;
 
-private:
-    /** These objects may not be exactly duplicated. Use cloneIdentDetails() or
-     * cloneDestinationDetails() instead.
-     */
-    Connection(const Connection &c);
-
-    /** These objects may not be exactly duplicated. Use cloneIdentDetails() or
-     * cloneDestinationDetails() instead.
-     */
-    Connection & operator =(const Connection &c);
-
 public:
     /** Address/Port for the Squid end of a TCP link. */
     Ip::Address local;
index df886dd705dc3417edc96e161ee1a512d547b714..5f9e574ef5c3e7060cca2f5b3e0905d6d10ee4eb 100644 (file)
@@ -124,6 +124,7 @@ void
 Ident::Close(const CommCloseCbParams &params)
 {
     IdentStateData *state = (IdentStateData *)params.data;
+    // XXX: A Connection closure handler must update its Connection object.
     state->deleteThis("connection closed");
 }
 
@@ -141,14 +142,13 @@ Ident::ConnectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, int,
     IdentStateData *state = (IdentStateData *)data;
     state->connWait.finish();
 
-    // Finalize the details and start owning the supplied connection (so that it
-    // is not orphaned if this function bails early). As a (tiny) optimization
-    // or perhaps just diff minimization, the close handler is added later, when
-    // we know we are not bailing. This delay is safe because
-    // comm_remove_close_handler() forgives missing handlers.
+    // Start owning the supplied connection (so that it is not orphaned if this
+    // function bails early). As a (tiny) optimization or perhaps just diff
+    // minimization, the close handler is added later, when we know we are not
+    // bailing. This delay is safe because comm_remove_close_handler() forgives
+    // missing handlers.
     assert(conn); // but may be closed
-    assert(state->conn);
-    assert(!state->conn->isOpen());
+    assert(!state->conn);
     state->conn = conn;
 
     if (status != Comm::OK) {
@@ -282,10 +282,10 @@ Ident::Start(const Comm::ConnectionPointer &conn, IDCB * callback, void *data)
     state->hash.key = xstrdup(key);
 
     // copy the conn details. We do not want the original FD to be re-used by IDENT.
-    state->conn = conn->cloneIdentDetails();
+    const auto identConn = conn->cloneProfile();
     // NP: use random port for secure outbound to IDENT_PORT
-    state->conn->local.port(0);
-    state->conn->remote.port(IDENT_PORT);
+    identConn->local.port(0);
+    identConn->remote.port(IDENT_PORT);
 
     // build our query from the original connection details
     state->queryMsg.init();
@@ -295,7 +295,7 @@ Ident::Start(const Comm::ConnectionPointer &conn, IDCB * callback, void *data)
     hash_join(ident_hash, &state->hash);
 
     AsyncCall::Pointer call = commCbCall(30,3, "Ident::ConnectDone", CommConnectCbPtrFun(Ident::ConnectDone, state));
-    const auto connOpener = new Comm::ConnOpener(state->conn, call, Ident::TheConfig.timeout);
+    const auto connOpener = new Comm::ConnOpener(identConn, call, Ident::TheConfig.timeout);
     state->connWait.start(connOpener, call);
     AsyncJob::Start(connOpener);
 }
index 1ed6f9623be43f68e9d9fb49787cd6dcb38bd7aa..acafb3fbbb97086e3deea650ed674c8887eeef29 100644 (file)
@@ -1679,7 +1679,7 @@ Ftp::Server::checkDataConnPre()
     // active transfer: open a data connection from Squid to client
     typedef CommCbMemFunT<Server, CommConnectCbParams> Dialer;
     AsyncCall::Pointer callback = JobCallback(17, 3, Dialer, this, Ftp::Server::connectedForData);
-    const auto cs = new Comm::ConnOpener(dataConn, callback,
+    const auto cs = new Comm::ConnOpener(dataConn->cloneProfile(), callback,
                                          Config.Timeout.connect);
     dataConnWait.start(cs, callback);
     AsyncJob::Start(cs);
@@ -1708,6 +1708,7 @@ Ftp::Server::connectedForData(const CommConnectCbParams &params)
         Http::StreamPointer context = pipeline.front();
         Must(context->http);
         Must(context->http->storeEntry() != NULL);
+        // TODO: call closeDataConnection() to reset data conn processing?
     } else {
         // Finalize the details and start owning the supplied connection.
         assert(params.conn);
index 3d4d32743eb2d4f8bd78138ea61f1809c0916b6f..5cfeb98978f70c48c7d094a004a070b535395262 100644 (file)
@@ -22,8 +22,7 @@ void Comm::AcceptLimiter::kick() STUB
 #include "comm/Connection.h"
 Comm::Connection::Connection() STUB
 Comm::Connection::~Connection() STUB
-Comm::ConnectionPointer Comm::Connection::cloneIdentDetails() const STUB_RETVAL(nullptr)
-Comm::ConnectionPointer Comm::Connection::cloneDestinationDetails() const STUB_RETVAL(nullptr)
+Comm::ConnectionPointer Comm::Connection::cloneProfile() const STUB_RETVAL(nullptr)
 void Comm::Connection::close() STUB
 void Comm::Connection::noteClosure() STUB
 CachePeer * Comm::Connection::getPeer() const STUB_RETVAL(NULL)