/*
- * Copyright (C) 1996-2016 The Squid Software Foundation and contributors
+ * Copyright (C) 1996-2018 The Squid Software Foundation and contributors
*
* Squid software is distributed under GPLv2+ license and includes
* contributions from numerous individuals and organizations.
#include "ip/QosConfig.h"
#include "LogTags.h"
#include "MemBuf.h"
+#include "neighbors.h"
#include "PeerSelectState.h"
#include "sbuf/SBuf.h"
#include "security/BlindPeerConnector.h"
*
* TODO 2: then convert this into a AsyncJob, possibly a child of 'Server'
*/
-class TunnelStateData
+class TunnelStateData: public PeerSelectionInitiator
{
- CBDATA_CLASS(TunnelStateData);
+ CBDATA_CHILD(TunnelStateData);
public:
TunnelStateData(ClientHttpRequest *);
- ~TunnelStateData();
+ virtual ~TunnelStateData();
TunnelStateData(const TunnelStateData &); // do not implement
TunnelStateData &operator =(const TunnelStateData &); // do not implement
/// if it is waiting for Squid CONNECT response, closing connections.
void informUserOfPeerError(const char *errMsg, size_t);
+ /// starts connecting to the next hop, either for the first time or while
+ /// recovering from the previous connect failure
+ void startConnecting();
+
+ void noteConnectFailure(const Comm::ConnectionPointer &conn);
+
class Connection
{
bool connectReqWriting; ///< whether we are writing a CONNECT request to a peer
SBuf preReadClientData;
SBuf preReadServerData;
- time_t started; ///< when this tunnel was initiated.
+ time_t startTime; ///< object creation time, before any peer selection/connection attempts
void copyRead(Connection &from, IOCB *completion);
/// continue to set up connection to a peer, going async for SSL peers
void connectToPeer();
+ /* PeerSelectionInitiator API */
+ virtual void noteDestination(Comm::ConnectionPointer conn) override;
+ virtual void noteDestinationsEnd(ErrorState *selectionError) override;
+
+ void saveError(ErrorState *finalError);
+ void sendError(ErrorState *finalError, const char *reason);
+
private:
/// Gives Security::PeerConnector access to Answer in the TunnelStateData callback dialer.
class MyAnswerDialer: public CallDialer, public Security::PeerConnector::CbDialer
/// callback handler after connection setup (including any encryption)
void connectedToPeer(Security::EncryptorAnswer &answer);
+ /// details of the "last tunneling attempt" failure (if it failed)
+ ErrorState *savedError = nullptr;
+
public:
bool keepGoingAfterRead(size_t len, Comm::Flag errcode, int xerrno, Connection &from, Connection &to);
void copy(size_t len, Connection &from, Connection &to, IOCB *);
static CLCB tunnelServerClosed;
static CLCB tunnelClientClosed;
static CTCB tunnelTimeout;
-static PSC tunnelPeerSelectComplete;
static EVH tunnelDelayedClientRead;
static EVH tunnelDelayedServerRead;
static void tunnelConnected(const Comm::ConnectionPointer &server, void *);
TunnelStateData::TunnelStateData(ClientHttpRequest *clientRequest) :
connectRespBuf(NULL),
connectReqWriting(false),
- started(squid_curtime)
+ startTime(squid_curtime)
{
debugs(26, 3, "TunnelStateData constructed this=" << this);
client.readPendingFunc = &tunnelDelayedClientRead;
xfree(url);
serverDestinations.clear();
delete connectRespBuf;
+ delete savedError;
}
TunnelStateData::Connection::~Connection()
server.bytesIn(len);
statCounter.server.all.kbytes_in += len;
statCounter.server.other.kbytes_in += len;
+ request->hier.notePeerRead();
}
if (keepGoingAfterRead(len, errcode, xerrno, server, client))
server.bytesIn(len);
statCounter.server.all.kbytes_in += len;
statCounter.server.other.kbytes_in += len;
+ request->hier.notePeerRead();
}
if (keepGoingAfterRead(len, errcode, xerrno, server, client))
}
// if we have no reply suitable to relay, use 502 Bad Gateway
- if (!sz || sz > static_cast<size_t>(connectRespBuf->contentSize())) {
- ErrorState *err = new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request.getRaw());
- *status_ptr = Http::scBadGateway;
- err->callback = tunnelErrorComplete;
- err->callback_data = this;
- errorSend(http->getConn()->clientConnection, err);
- return;
- }
+ if (!sz || sz > static_cast<size_t>(connectRespBuf->contentSize()))
+ return sendError(new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request.getRaw()),
+ "peer error without reply");
// if we need to send back the server response. write its headers to the client
server.len = sz;
HttpReply rep;
Http::StatusCode parseErr = Http::scNone;
const bool eof = !chunkSize;
- connectRespBuf->terminate(); // HttpMsg::parse requires terminated string
+ connectRespBuf->terminate(); // Http::Message::parse requires terminated string
const bool parsed = rep.parse(connectRespBuf->content(), connectRespBuf->contentSize(), eof, &parseErr);
if (!parsed) {
if (parseErr > 0) { // unrecoverable parsing error
}
// CONNECT response was successfully parsed
- *status_ptr = rep.sline.status();
-
- // we need to relay the 401/407 responses when login=PASS(THRU)
- const char *pwd = server.conn->getPeer()->login;
- const bool relay = pwd && (strcmp(pwd, "PASS") == 0 || strcmp(pwd, "PASSTHRU") == 0) &&
- (*status_ptr == Http::scProxyAuthenticationRequired ||
- *status_ptr == Http::scUnauthorized);
+ request->hier.peer_reply_status = rep.sline.status();
// bail if we did not get an HTTP 200 (Connection Established) response
if (rep.sline.status() != Http::scOkay) {
// if we ever decide to reuse the peer connection, we must extract the error response first
- informUserOfPeerError("unsupported CONNECT response status code", (relay ? rep.hdr_sz : 0));
+ *status_ptr = rep.sline.status(); // we are relaying peer response
+ informUserOfPeerError("unsupported CONNECT response status code", rep.hdr_sz);
return;
}
{
debugs(26, 3, HERE << server.conn << ", " << len << " bytes written, flag=" << flag);
+ if (flag == Comm::ERR_CLOSING)
+ return;
+
+ request->hier.notePeerWrite();
+
/* Error? */
if (flag != Comm::OK) {
- if (flag != Comm::ERR_CLOSING) {
- debugs(26, 4, HERE << "calling TunnelStateData::server.error(" << xerrno <<")");
- server.error(xerrno); // may call comm_close
- }
+ debugs(26, 4, "to-server write failed: " << xerrno);
+ server.error(xerrno); // may call comm_close
return;
}
if (size_ptr)
*size_ptr += amount;
+
}
void
{
debugs(26, 3, HERE << client.conn << ", " << len << " bytes written, flag=" << flag);
+ if (flag == Comm::ERR_CLOSING)
+ return;
+
/* Error? */
if (flag != Comm::OK) {
- if (flag != Comm::ERR_CLOSING) {
- debugs(26, 4, HERE << "Closing client connection due to comm flags.");
- client.error(xerrno); // may call comm_close
- }
+ debugs(26, 4, "from-client read failed: " << xerrno);
+ client.error(xerrno); // may call comm_close
return;
}
* Call the tunnelStartShoveling to start the blind pump.
*/
static void
-tunnelConnectedWriteDone(const Comm::ConnectionPointer &conn, char *, size_t, Comm::Flag flag, int, void *data)
+tunnelConnectedWriteDone(const Comm::ConnectionPointer &conn, char *, size_t len, Comm::Flag flag, int, void *data)
{
TunnelStateData *tunnelState = (TunnelStateData *)data;
debugs(26, 3, HERE << conn << ", flag=" << flag);
return;
}
+ if (auto http = tunnelState->http.get()) {
+ http->out.headers_sz += len;
+ http->out.size += len;
+ }
+
tunnelStartShoveling(tunnelState);
}
/// Called when we are done writing CONNECT request to a peer.
static void
-tunnelConnectReqWriteDone(const Comm::ConnectionPointer &conn, char *, size_t, Comm::Flag flag, int, void *data)
+tunnelConnectReqWriteDone(const Comm::ConnectionPointer &conn, char *, size_t ioSize, Comm::Flag flag, int, void *data)
{
TunnelStateData *tunnelState = (TunnelStateData *)data;
debugs(26, 3, conn << ", flag=" << flag);
tunnelState->server.writer = NULL;
assert(tunnelState->waitingForConnectRequest());
+ tunnelState->request->hier.notePeerWrite();
+
if (flag != Comm::OK) {
*tunnelState->status_ptr = Http::scInternalServerError;
tunnelErrorComplete(conn->fd, data, 0);
return;
}
+ statCounter.server.all.kbytes_out += ioSize;
+ statCounter.server.other.kbytes_out += ioSize;
+
tunnelState->connectReqWriting = false;
tunnelState->connectExchangeCheckpoint();
}
if (!tunnelState->clientExpectsConnectResponse())
tunnelStartShoveling(tunnelState); // ssl-bumped connection, be quiet
else {
+ *tunnelState->status_ptr = Http::scOkay;
AsyncCall::Pointer call = commCbCall(5,5, "tunnelConnectedWriteDone",
CommIoCbPtrFun(tunnelConnectedWriteDone, tunnelState));
tunnelState->client.write(conn_established, strlen(conn_established), call, NULL);
tunnelState->server.conn->close();
}
+/// reacts to a failure to establish the given TCP connection
+void
+TunnelStateData::noteConnectFailure(const Comm::ConnectionPointer &conn)
+{
+ debugs(26, 4, "removing the failed one from " << serverDestinations.size() <<
+ " destinations: " << conn);
+
+ if (CachePeer *peer = conn->getPeer())
+ peerConnectFailed(peer);
+
+ assert(!serverDestinations.empty());
+ serverDestinations.erase(serverDestinations.begin());
+
+ // Since no TCP payload has been passed to client or server, we may
+ // TCP-connect to other destinations (including alternate IPs).
+
+ if (!FwdState::EnoughTimeToReForward(startTime))
+ return sendError(savedError, "forwarding timeout");
+
+ if (!serverDestinations.empty())
+ return startConnecting();
+
+ if (!PeerSelectionInitiator::subscribed)
+ return sendError(savedError, "tried all destinations");
+
+ debugs(26, 4, "wait for more destinations to try");
+ // expect a noteDestination*() call
+}
+
static void
tunnelConnectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, int xerrno, void *data)
{
TunnelStateData *tunnelState = (TunnelStateData *)data;
if (status != Comm::OK) {
- debugs(26, 4, HERE << conn << ", comm failure recovery.");
- /* At this point only the TCP handshake has failed. no data has been passed.
- * we are allowed to re-try the TCP-level connection to alternate IPs for CONNECT.
- */
- debugs(26, 4, "removing server 1 of " << tunnelState->serverDestinations.size() <<
- " from destinations (" << tunnelState->serverDestinations[0] << ")");
- tunnelState->serverDestinations.erase(tunnelState->serverDestinations.begin());
- time_t fwdTimeout = tunnelState->started + Config.Timeout.forward;
- if (fwdTimeout > squid_curtime && tunnelState->serverDestinations.size() > 0) {
- // find remaining forward_timeout available for this attempt
- fwdTimeout -= squid_curtime;
- if (fwdTimeout > Config.Timeout.connect)
- fwdTimeout = Config.Timeout.connect;
- /* Try another IP of this destination host */
- GetMarkingsToServer(tunnelState->request.getRaw(), *tunnelState->serverDestinations[0]);
- debugs(26, 4, HERE << "retry with : " << tunnelState->serverDestinations[0]);
- AsyncCall::Pointer call = commCbCall(26,3, "tunnelConnectDone", CommConnectCbPtrFun(tunnelConnectDone, tunnelState));
- Comm::ConnOpener *cs = new Comm::ConnOpener(tunnelState->serverDestinations[0], call, fwdTimeout);
- cs->setHost(tunnelState->url);
- AsyncJob::Start(cs);
- } else {
- debugs(26, 4, HERE << "terminate with error.");
- ErrorState *err = new ErrorState(ERR_CONNECT_FAIL, Http::scServiceUnavailable, tunnelState->request.getRaw());
- *tunnelState->status_ptr = Http::scServiceUnavailable;
- err->xerrno = xerrno;
- // on timeout is this still: err->xerrno = ETIMEDOUT;
- err->port = conn->remote.port();
- err->callback = tunnelErrorComplete;
- err->callback_data = tunnelState;
- errorSend(tunnelState->client.conn, err);
- if (tunnelState->request != NULL)
- tunnelState->request->hier.stopPeerClock(false);
- }
+ ErrorState *err = new ErrorState(ERR_CONNECT_FAIL, Http::scServiceUnavailable, tunnelState->request.getRaw());
+ err->xerrno = xerrno;
+ // on timeout is this still: err->xerrno = ETIMEDOUT;
+ err->port = conn->remote.port();
+ tunnelState->saveError(err);
+ tunnelState->noteConnectFailure(conn);
return;
}
tunnelState->server.setDelayId(DelayId());
#endif
- tunnelState->request->hier.note(conn, tunnelState->getHost());
+ tunnelState->request->hier.resetPeerNotes(conn, tunnelState->getHost());
tunnelState->server.conn = conn;
tunnelState->request->peer_host = conn->getPeer() ? conn->getPeer()->host : NULL;
* default is to allow.
*/
ACLFilledChecklist ch(Config.accessList.miss, request, NULL);
+ ch.al = http->al;
ch.src_addr = request->client_addr;
ch.my_addr = request->my_addr;
- if (ch.fastCheck() == ACCESS_DENIED) {
+ ch.syncAle(request, http->log_uri);
+ if (ch.fastCheck().denied()) {
debugs(26, 4, HERE << "MISS access forbidden.");
err = new ErrorState(ERR_FORWARDING_DENIED, Http::scForbidden, request);
http->al->http.code = Http::scForbidden;
#if USE_DELAY_POOLS
//server.setDelayId called from tunnelConnectDone after server side connection established
#endif
-
- peerSelect(&(tunnelState->serverDestinations), request, http->al,
- NULL,
- tunnelPeerSelectComplete,
- tunnelState);
+ tunnelState->startSelectingDestinations(request, http->al, nullptr);
}
void
TunnelStateData::connectedToPeer(Security::EncryptorAnswer &answer)
{
if (ErrorState *error = answer.error.get()) {
- *status_ptr = error->httpStatus;
- error->callback = tunnelErrorComplete;
- error->callback_data = this;
- errorSend(client.conn, error);
- answer.error.clear(); // preserve error for errorSendComplete()
+ answer.error.clear(); // sendError() will own the error
+ sendError(error, "TLS peer connection error");
return;
}
tunnelState->connectRespBuf = new MemBuf;
// SQUID_TCP_SO_RCVBUF: we should not accumulate more than regular I/O buffer
// can hold since any CONNECT response leftovers have to fit into server.buf.
- // 2*SQUID_TCP_SO_RCVBUF: HttpMsg::parse() zero-terminates, which uses space.
+ // 2*SQUID_TCP_SO_RCVBUF: Http::Message::parse() zero-terminates, which uses space.
tunnelState->connectRespBuf->init(SQUID_TCP_SO_RCVBUF, 2*SQUID_TCP_SO_RCVBUF);
tunnelState->readConnectResponse();
return nullptr;
}
-static void
-tunnelPeerSelectComplete(Comm::ConnectionList *peer_paths, ErrorState *err, void *data)
+void
+TunnelStateData::noteDestination(Comm::ConnectionPointer path)
{
- TunnelStateData *tunnelState = (TunnelStateData *)data;
+ const bool wasBlocked = serverDestinations.empty();
+ serverDestinations.push_back(path);
+ if (wasBlocked)
+ startConnecting();
+ // else continue to use one of the previously noted destinations;
+ // if all of them fail, we may try this path
+}
+
+void
+TunnelStateData::noteDestinationsEnd(ErrorState *selectionError)
+{
+ PeerSelectionInitiator::subscribed = false;
+ if (serverDestinations.empty()) { // was blocked, waiting for more paths
+
+ if (selectionError)
+ return sendError(selectionError, "path selection has failed");
+
+ if (savedError)
+ return sendError(savedError, "all found paths have failed");
- bool bail = false;
- if (!peer_paths || peer_paths->empty()) {
- debugs(26, 3, HERE << "No paths found. Aborting CONNECT");
- bail = true;
+ return sendError(new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request.getRaw()),
+ "path selection found no paths");
}
+ // else continue to use one of the previously noted destinations;
+ // if all of them fail, tunneling as whole will fail
+ Must(!selectionError); // finding at least one path means selection succeeded
+}
+
+/// remembers an error to be used if there will be no more connection attempts
+void
+TunnelStateData::saveError(ErrorState *error)
+{
+ debugs(26, 4, savedError << " ? " << error);
+ assert(error);
+ delete savedError; // may be nil
+ savedError = error;
+}
+
+/// Starts sending the given error message to the client, leading to the
+/// eventual transaction termination. Call with savedError to send savedError.
+void
+TunnelStateData::sendError(ErrorState *finalError, const char *reason)
+{
+ debugs(26, 3, "aborting transaction for " << reason);
- if (!bail && tunnelState->serverDestinations[0]->peerType == PINNED) {
- Comm::ConnectionPointer serverConn = borrowPinnedConnection(tunnelState->request.getRaw(), tunnelState->serverDestinations[0]);
+ if (request)
+ request->hier.stopPeerClock(false);
+
+ assert(finalError);
+
+ // get rid of any cached error unless that is what the caller is sending
+ if (savedError != finalError)
+ delete savedError; // may be nil
+ savedError = nullptr;
+
+ // we cannot try other destinations after responding with an error
+ PeerSelectionInitiator::subscribed = false; // may already be false
+
+ *status_ptr = finalError->httpStatus;
+ finalError->callback = tunnelErrorComplete;
+ finalError->callback_data = this;
+ errorSend(client.conn, finalError);
+}
+
+void
+TunnelStateData::startConnecting()
+{
+ if (request)
+ request->hier.startPeerClock();
+
+ assert(!serverDestinations.empty());
+ Comm::ConnectionPointer &dest = serverDestinations.front();
+ debugs(26, 3, "to " << dest);
+
+ if (dest->peerType == PINNED) {
+ Comm::ConnectionPointer serverConn = borrowPinnedConnection(request.getRaw(), dest);
debugs(26,7, "pinned peer connection: " << serverConn);
if (Comm::IsConnOpen(serverConn)) {
- tunnelConnectDone(serverConn, Comm::OK, 0, (void *)tunnelState);
+ tunnelConnectDone(serverConn, Comm::OK, 0, (void *)this);
return;
}
- bail = true;
- }
-
- if (bail) {
- if (!err) {
- err = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, tunnelState->request.getRaw());
- }
- *tunnelState->status_ptr = err->httpStatus;
- err->callback = tunnelErrorComplete;
- err->callback_data = tunnelState;
- errorSend(tunnelState->client.conn, err);
+ // a PINNED path failure is fatal; do not wait for more paths
+ sendError(new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request.getRaw()),
+ "pinned path failure");
return;
}
- delete err;
-
- GetMarkingsToServer(tunnelState->request.getRaw(), *tunnelState->serverDestinations[0]);
-
- if (tunnelState->request != NULL)
- tunnelState->request->hier.startPeerClock();
- debugs(26, 3, HERE << "paths=" << peer_paths->size() << ", p[0]={" << (*peer_paths)[0] << "}, serverDest[0]={" <<
- tunnelState->serverDestinations[0] << "}");
+ GetMarkingsToServer(request.getRaw(), *dest);
- AsyncCall::Pointer call = commCbCall(26,3, "tunnelConnectDone", CommConnectCbPtrFun(tunnelConnectDone, tunnelState));
- Comm::ConnOpener *cs = new Comm::ConnOpener(tunnelState->serverDestinations[0], call, Config.Timeout.connect);
- cs->setHost(tunnelState->url);
+ const time_t connectTimeout = dest->connectTimeout(startTime);
+ AsyncCall::Pointer call = commCbCall(26,3, "tunnelConnectDone", CommConnectCbPtrFun(tunnelConnectDone, this));
+ Comm::ConnOpener *cs = new Comm::ConnOpener(dest, call, connectTimeout);
+ cs->setHost(url);
AsyncJob::Start(cs);
}
fd_table[clientConn->fd].read_method = &default_read_method;
fd_table[clientConn->fd].write_method = &default_write_method;
- request->hier.note(srvConn, tunnelState->getHost());
+ request->hier.resetPeerNotes(srvConn, tunnelState->getHost());
tunnelState->server.conn = srvConn;
auto ssl = fd_table[srvConn->fd].ssl.get();
assert(ssl);
BIO *b = SSL_get_rbio(ssl);
- Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(b->ptr);
+ Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(BIO_get_data(b));
tunnelState->preReadServerData = srvBio->rBufData();
tunnelStartShoveling(tunnelState);
}