From: Eduard Bagdasaryan Date: Fri, 15 Feb 2019 00:17:41 +0000 (+0000) Subject: Log PROXY protocol v2 TLVs; fix PROXY protocol parsing bugs (#342) X-Git-Tag: M-staged-PR342 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=36c774f;p=thirdparty%2Fsquid.git Log PROXY protocol v2 TLVs; fix PROXY protocol parsing bugs (#342) Added %proxy_protocol::>h logformat code for logging received PROXY protocol TLV values and passing them to adaptation services and helpers. For simplicity, this implementation treats TLV values as text and reuses the existing HTTP header field logging interfaces. Support for binary TLV value logging can be added later if needed. Also support logging of metadata extracted from the fixed portion of a PROXY protocol header and referenced as "pseudo headers": :command, :version, :src_addr, :dst_addr, :src_port, and :dst_port. Also fixed several bugs in the old PROXY protocol v1/v2 parsing code: * Buffer overrun in ConnStateData::parseProxy2p0(). The available local SBuf could be less than sizeof(pax), resulting in copying excessive bytes with SBuf::rawContent(). * Incorrect processing of malformed v1 headers lacking CRLF in ConnStateData::parseProxy1p0(), which waited for more data even if the buffer size already exceeded the maximum v1 header size. * Incorrect processing of partial-buffered v1 headers, when only the initial (magic) part of the header has been received. The old code resulted with an error instead of waiting for more data in this case. * Incorrect v1 header framing for proto=UNKNOWN headers. The code used only LF while the protocol requires CRLF for it. * Do not use address information from v2 header if the header proto is UNSPEC. * Incorrect v1 magic expectations (a 6-character `PROXY ` instead of the proper 5-character `PROXY` sequence) leading to mishandling of non-PROXY input. For example, receiving `PROXY\r\n` would result in "need more data" outcome (and long timeout) instead of an immediate error. Also eliminated code duplication in HttpHeader::getByNameListMember() and HttpHeader::getListMember(), moving the common part into a separate getListMember() method. Also eliminated code duplication and probably fixed a bug with applying client_netmask parameter in ConnStateData constructor. The mask should not be applied to localhost and IPv6 addresses but was. Also parse PROXY protocol v2 LOCAL addresses (for logging purposes). In compliance with the PROXY protocol specs, LOCAL addresses are still unused for connection routing. --- diff --git a/configure.ac b/configure.ac index c3854c2747..f46110d86c 100644 --- a/configure.ac +++ b/configure.ac @@ -3860,6 +3860,7 @@ AC_CONFIG_FILES([ src/mem/Makefile src/mgr/Makefile src/parser/Makefile + src/proxyp/Makefile src/repl/Makefile src/sbuf/Makefile src/security/Makefile diff --git a/src/AccessLogEntry.cc b/src/AccessLogEntry.cc index d54b00eee7..09de0a22c6 100644 --- a/src/AccessLogEntry.cc +++ b/src/AccessLogEntry.cc @@ -10,6 +10,7 @@ #include "AccessLogEntry.h" #include "HttpReply.h" #include "HttpRequest.h" +#include "proxyp/Header.h" #include "SquidConfig.h" #include "ssl/support.h" @@ -39,8 +40,7 @@ AccessLogEntry::getLogClientIp(char *buf, size_t bufsz) const // - IPv4 clients masked with client_netmask // - IPv6 clients use 'privacy addressing' instead. - if (!log_ip.isLocalhost() && log_ip.isIPv4()) - log_ip.applyMask(Config.Addrs.client_netmask); + log_ip.applyClientMask(Config.Addrs.client_netmask); log_ip.toStr(buf, bufsz); } @@ -94,6 +94,8 @@ AccessLogEntry::getExtUser() const return nullptr; } +AccessLogEntry::AccessLogEntry() {} + AccessLogEntry::~AccessLogEntry() { safe_free(headers.request); diff --git a/src/AccessLogEntry.h b/src/AccessLogEntry.h index 2079b31f54..a33450bf5e 100644 --- a/src/AccessLogEntry.h +++ b/src/AccessLogEntry.h @@ -21,6 +21,7 @@ #include "LogTags.h" #include "MessageSizes.h" #include "Notes.h" +#include "proxyp/forward.h" #include "sbuf/SBuf.h" #if ICAP_CLIENT #include "adaptation/icap/Elements.h" @@ -41,7 +42,7 @@ class AccessLogEntry: public RefCountable public: typedef RefCount Pointer; - AccessLogEntry() {} + AccessLogEntry(); ~AccessLogEntry(); /// Fetch the client IP log string into the given buffer. @@ -184,6 +185,9 @@ public: /// key=value pairs returned from URL rewrite/redirect helper NotePairs::Pointer notes; + /// see ConnStateData::proxyProtocolHeader_ + ProxyProtocol::HeaderPointer proxyProtocolHeader; + #if ICAP_CLIENT /** \brief This subclass holds log info for ICAP part of request * \todo Inner class declarations should be moved outside diff --git a/src/HttpHeader.cc b/src/HttpHeader.cc index 8850f52d5e..930b4a5263 100644 --- a/src/HttpHeader.cc +++ b/src/HttpHeader.cc @@ -953,57 +953,23 @@ HttpHeader::hasNamed(const char *name, unsigned int namelen, String *result) con /* * Returns a the value of the specified list member, if any. */ -String +SBuf HttpHeader::getByNameListMember(const char *name, const char *member, const char separator) const { - String header; - const char *pos = NULL; - const char *item; - int ilen; - int mlen = strlen(member); - assert(name); - - header = getByName(name); - - String result; - - while (strListGetItem(&header, separator, &item, &ilen, &pos)) { - if (strncmp(item, member, mlen) == 0 && item[mlen] == '=') { - result.append(item + mlen + 1, ilen - mlen - 1); - break; - } - } - - return result; + const auto header = getByName(name); + return ::getListMember(header, member, separator); } /* * returns a the value of the specified list member, if any. */ -String +SBuf HttpHeader::getListMember(Http::HdrType id, const char *member, const char separator) const { - String header; - const char *pos = NULL; - const char *item; - int ilen; - int mlen = strlen(member); - assert(any_registered_header(id)); - - header = getStrOrList(id); - String result; - - while (strListGetItem(&header, separator, &item, &ilen, &pos)) { - if (strncmp(item, member, mlen) == 0 && item[mlen] == '=') { - result.append(item + mlen + 1, ilen - mlen - 1); - break; - } - } - - header.clean(); - return result; + const auto header = getStrOrList(id); + return ::getListMember(header, member, separator); } /* test if a field is present */ diff --git a/src/HttpHeader.h b/src/HttpHeader.h index fa94fe2fd5..24e98c5f36 100644 --- a/src/HttpHeader.h +++ b/src/HttpHeader.h @@ -118,8 +118,12 @@ public: bool hasNamed(const SBuf &s, String *value = 0) const; /// \deprecated use SBuf method instead. bool hasNamed(const char *name, unsigned int namelen, String *value = 0) const; - String getByNameListMember(const char *name, const char *member, const char separator) const; - String getListMember(Http::HdrType id, const char *member, const char separator) const; + /// searches for the first matching key=value pair within the name-identified field + /// \returns the value of the found pair or an empty string + SBuf getByNameListMember(const char *name, const char *member, const char separator) const; + /// searches for the first matching key=value pair within the field + /// \returns the value of the found pair or an empty string + SBuf getListMember(Http::HdrType id, const char *member, const char separator) const; int has(Http::HdrType id) const; /// Appends "this cache" information to VIA header field. /// Takes the initial VIA value from "from" parameter, if provided. diff --git a/src/Makefile.am b/src/Makefile.am index 0113a3ba1b..3fefaf9706 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -31,8 +31,8 @@ LOADABLE_MODULES_SOURCES = \ LoadableModules.h \ LoadableModules.cc -SUBDIRS = mem base anyp helper dns ftp parser comm eui acl format clients sbuf servers fs repl store DiskIO -DIST_SUBDIRS = mem base anyp helper dns ftp parser comm eui acl format clients sbuf servers fs repl store DiskIO +SUBDIRS = mem base anyp helper dns ftp parser comm eui acl format clients sbuf servers fs repl store DiskIO proxyp +DIST_SUBDIRS = mem base anyp helper dns ftp parser comm eui acl format clients sbuf servers fs repl store DiskIO proxyp if ENABLE_AUTH SUBDIRS += auth @@ -551,6 +551,7 @@ squid_LDADD = \ $(SSL_LIBS) \ ipc/libipc.la \ mgr/libmgr.la \ + proxyp/libproxyp.la \ parser/libparser.la \ eui/libeui.la \ icmp/libicmp.la \ @@ -1005,6 +1006,7 @@ nodist_tests_testURL_SOURCES = \ tests/stub_StatHist.cc tests_testURL_LDADD = \ libsquid.la \ + proxyp/libproxyp.la \ anyp/libanyp.la \ base/libbase.la \ ip/libip.la \ @@ -2040,6 +2042,7 @@ tests_test_http_range_LDADD = \ acl/libacls.la \ acl/libstate.la \ acl/libapi.la \ + proxyp/libproxyp.la \ parser/libparser.la \ ip/libip.la \ fs/libfs.la \ @@ -2426,6 +2429,7 @@ tests_testHttpRequest_LDADD = \ fs/libfs.la \ $(SSL_LIBS) \ ipc/libipc.la \ + proxyp/libproxyp.la \ parser/libparser.la \ dns/libdns.la \ base/libbase.la \ @@ -2714,6 +2718,7 @@ tests_testCacheManager_LDADD = \ ftp/libftp.la \ helper/libhelper.la \ http/libhttp.la \ + proxyp/libproxyp.la \ parser/libparser.la \ ident/libident.la \ acl/libacls.la \ @@ -3030,6 +3035,7 @@ tests_testEvent_LDADD = \ ftp/libftp.la \ helper/libhelper.la \ http/libhttp.la \ + proxyp/libproxyp.la \ parser/libparser.la \ ident/libident.la \ acl/libacls.la \ @@ -3262,6 +3268,7 @@ tests_testEventLoop_LDADD = \ ftp/libftp.la \ helper/libhelper.la \ http/libhttp.la \ + proxyp/libproxyp.la \ parser/libparser.la \ ident/libident.la \ acl/libacls.la \ diff --git a/src/StrList.cc b/src/StrList.cc index bc7c86bd46..980ac5bcb1 100644 --- a/src/StrList.cc +++ b/src/StrList.cc @@ -131,3 +131,17 @@ strListGetItem(const String * str, char del, const char **item, int *ilen, const return len > 0; } +SBuf +getListMember(const String &list, const char *key, const char delimiter) +{ + const char *pos = nullptr; + const char *item = nullptr; + int ilen = 0; + const auto keyLen = strlen(key); + while (strListGetItem(&list, delimiter, &item, &ilen, &pos)) { + if (static_cast(ilen) > keyLen && strncmp(item, key, keyLen) == 0 && item[keyLen] == '=') + return SBuf(item + keyLen + 1, ilen - keyLen - 1); + } + return SBuf(); +} + diff --git a/src/StrList.h b/src/StrList.h index 9212080a84..475fac8887 100644 --- a/src/StrList.h +++ b/src/StrList.h @@ -19,6 +19,10 @@ void strListAdd(String * str, const char *item, char del); int strListIsMember(const String * str, const SBuf &item, char del); int strListIsSubstr(const String * list, const char *s, char del); int strListGetItem(const String * str, char del, const char **item, int *ilen, const char **pos); +/// Searches for the first matching key=value pair +/// within a delimiter-separated list of items. +/// \returns the value of the found pair or an empty string. +SBuf getListMember(const String &list, const char *key, const char delimiter); #endif /* SQUID_STRLIST_H_ */ diff --git a/src/adaptation/icap/ModXact.cc b/src/adaptation/icap/ModXact.cc index d541da4c96..3a095a0bb2 100644 --- a/src/adaptation/icap/ModXact.cc +++ b/src/adaptation/icap/ModXact.cc @@ -1810,8 +1810,8 @@ void Adaptation::Icap::ModXact::fillDoneStatus(MemBuf &buf) const bool Adaptation::Icap::ModXact::gotEncapsulated(const char *section) const { - return icapReply->header.getByNameListMember("Encapsulated", - section, ',').size() > 0; + return !icapReply->header.getByNameListMember("Encapsulated", + section, ',').isEmpty(); } // calculate whether there is a virgin HTTP body and diff --git a/src/adaptation/icap/OptXact.cc b/src/adaptation/icap/OptXact.cc index 1dfea87324..a2233e15d1 100644 --- a/src/adaptation/icap/OptXact.cc +++ b/src/adaptation/icap/OptXact.cc @@ -87,8 +87,8 @@ void Adaptation::Icap::OptXact::handleCommRead(size_t) // We read everything if there is no response body. If there is a body, // we cannot parse it because we do not support any opt-body-types, so // we leave readAll false which forces connection closure. - readAll = !icapReply->header.getByNameListMember("Encapsulated", - "opt-body", ',').size(); + readAll = icapReply->header.getByNameListMember("Encapsulated", + "opt-body", ',').isEmpty(); debugs(93, 7, HERE << "readAll=" << readAll); icap_tio_finish = current_time; setOutcome(xoOpt); diff --git a/src/cf.data.pre b/src/cf.data.pre index ed1ced4bab..908e824f78 100644 --- a/src/cf.data.pre +++ b/src/cf.data.pre @@ -4884,6 +4884,34 @@ DOC_START service name in curly braces to record response time(s) specific to that service. For example: %{my_service}adapt::sum_trs + Format codes related to the PROXY protocol: + + proxy_protocol::>h PROXY protocol header, including optional TLVs. + + Supports the same field and element reporting/extraction logic + as %http::>h. For configuration and reporting purposes, Squid + maps each PROXY TLV to an HTTP header field: the TLV type + (configured as a decimal integer) is the field name, and the + TLV value is the field value. All TLVs of "LOCAL" connections + (in PROXY protocol terminology) are currently skipped/ignored. + + Squid also maps the following standard PROXY protocol header + blocks to pseudo HTTP headers (their names use PROXY + terminology and start with a colon, following HTTP tradition + for pseudo headers): :command, :version, :src_addr, :dst_addr, + :src_port, and :dst_port. + + Without optional parameters, this logformat code logs + pseudo headers and TLVs. + + This format code uses pass-through URL encoding by default. + + Example: + # relay custom PROXY TLV #224 to adaptation services + adaptation_meta Client-Foo "%proxy_protocol::>h{224}" + + See also: %http::>h + The default formats available (which do not need re-defining) are: logformat squid %ts.%03tu %6tr %>a %Ss/%03>Hs %= Proxy2p0magic.length()) { - // PROXY/1.0 magic is shorter, so we know that - // the input does not start with any PROXY magic - return proxyProtocolError("PROXY protocol error: invalid header"); - } - - // TODO: detect short non-magic prefixes earlier to avoid - // waiting for more data which may never come - - // not enough bytes to parse yet. - return false; -} - -/// parse the PROXY/1.0 protocol header from the connection read buffer -bool -ConnStateData::parseProxy1p0() -{ - ::Parser::Tokenizer tok(inBuf); - tok.skip(Proxy1p0magic); - - // skip to first LF (assumes it is part of CRLF) - static const CharacterSet lineContent = CharacterSet::LF.complement("non-LF"); - SBuf line; - if (tok.prefix(line, lineContent, 107-Proxy1p0magic.length())) { - if (tok.skip('\n')) { - // found valid header - inBuf = tok.remaining(); - needProxyProtocolHeader_ = false; - // reset the tokenizer to work on found line only. - tok.reset(line); - } else - return false; // no LF yet - - } else // protocol error only if there are more than 107 bytes prefix header - return proxyProtocolError(inBuf.length() > 107? "PROXY/1.0 error: missing CRLF" : NULL); - - static const SBuf unknown("UNKNOWN"), tcpName("TCP"); - if (tok.skip(tcpName)) { - - // skip TCP/IP version number - static const CharacterSet tcpVersions("TCP-version","46"); - if (!tok.skipOne(tcpVersions)) - return proxyProtocolError("PROXY/1.0 error: missing TCP version"); - - // skip SP after protocol version - if (!tok.skip(' ')) - return proxyProtocolError("PROXY/1.0 error: missing SP"); - - SBuf ipa, ipb; - int64_t porta, portb; - static const CharacterSet ipChars = CharacterSet("IP Address",".:") + CharacterSet::HEXDIG; - - // parse: src-IP SP dst-IP SP src-port SP dst-port CR - // leave the LF until later. - const bool correct = tok.prefix(ipa, ipChars) && tok.skip(' ') && - tok.prefix(ipb, ipChars) && tok.skip(' ') && - tok.int64(porta) && tok.skip(' ') && - tok.int64(portb) && - tok.skip('\r'); - if (!correct) - return proxyProtocolError("PROXY/1.0 error: invalid syntax"); - - // parse IP and port strings - Ip::Address originalClient, originalDest; - - if (!originalClient.GetHostByName(ipa.c_str())) - return proxyProtocolError("PROXY/1.0 error: invalid src-IP address"); - - if (!originalDest.GetHostByName(ipb.c_str())) - return proxyProtocolError("PROXY/1.0 error: invalid dst-IP address"); - - if (porta > 0 && porta <= 0xFFFF) // max uint16_t - originalClient.port(static_cast(porta)); - else - return proxyProtocolError("PROXY/1.0 error: invalid src port"); - - if (portb > 0 && portb <= 0xFFFF) // max uint16_t - originalDest.port(static_cast(portb)); - else - return proxyProtocolError("PROXY/1.0 error: invalid dst port"); - - // we have original client and destination details now - // replace the client connection values - debugs(33, 5, "PROXY/1.0 protocol on connection " << clientConnection); - clientConnection->local = originalDest; - clientConnection->remote = originalClient; - if ((clientConnection->flags & COMM_TRANSPARENT)) - clientConnection->flags ^= COMM_TRANSPARENT; // prevent TPROXY spoofing of this new IP. - debugs(33, 5, "PROXY/1.0 upgrade: " << clientConnection); - return true; - - } else if (tok.skip(unknown)) { - // found valid but unusable header - return true; - - } else - return proxyProtocolError("PROXY/1.0 error: invalid protocol family"); - - return false; -} - -/// parse the PROXY/2.0 protocol header from the connection read buffer -bool -ConnStateData::parseProxy2p0() -{ - static const SBuf::size_type prefixLen = Proxy2p0magic.length(); - if (inBuf.length() < prefixLen + 4) - return false; // need more bytes - - if ((inBuf[prefixLen] & 0xF0) != 0x20) // version == 2 is mandatory - return proxyProtocolError("PROXY/2.0 error: invalid version"); - - const char command = (inBuf[prefixLen] & 0x0F); - if ((command & 0xFE) != 0x00) // values other than 0x0-0x1 are invalid - return proxyProtocolError("PROXY/2.0 error: invalid command"); - - const char family = (inBuf[prefixLen+1] & 0xF0) >>4; - if (family > 0x3) // values other than 0x0-0x3 are invalid - return proxyProtocolError("PROXY/2.0 error: invalid family"); - - const char proto = (inBuf[prefixLen+1] & 0x0F); - if (proto > 0x2) // values other than 0x0-0x2 are invalid - return proxyProtocolError("PROXY/2.0 error: invalid protocol type"); - - const char *clen = inBuf.rawContent() + prefixLen + 2; - uint16_t len; - memcpy(&len, clen, sizeof(len)); - len = ntohs(len); - - if (inBuf.length() < prefixLen + 4 + len) - return false; // need more bytes - - inBuf.consume(prefixLen + 4); // 4 being the extra bytes - const SBuf extra = inBuf.consume(len); - needProxyProtocolHeader_ = false; // found successfully - - // LOCAL connections do nothing with the extras - if (command == 0x00/* LOCAL*/) - return true; - - union pax { - struct { /* for TCP/UDP over IPv4, len = 12 */ - struct in_addr src_addr; - struct in_addr dst_addr; - uint16_t src_port; - uint16_t dst_port; - } ipv4_addr; - struct { /* for TCP/UDP over IPv6, len = 36 */ - struct in6_addr src_addr; - struct in6_addr dst_addr; - uint16_t src_port; - uint16_t dst_port; - } ipv6_addr; -#if NOT_SUPPORTED - struct { /* for AF_UNIX sockets, len = 216 */ - uint8_t src_addr[108]; - uint8_t dst_addr[108]; - } unix_addr; -#endif - }; - - pax ipu; - memcpy(&ipu, extra.rawContent(), sizeof(pax)); - - // replace the client connection values - debugs(33, 5, "PROXY/2.0 protocol on connection " << clientConnection); - switch (family) { - case 0x1: // IPv4 - clientConnection->local = ipu.ipv4_addr.dst_addr; - clientConnection->local.port(ntohs(ipu.ipv4_addr.dst_port)); - clientConnection->remote = ipu.ipv4_addr.src_addr; - clientConnection->remote.port(ntohs(ipu.ipv4_addr.src_port)); - if ((clientConnection->flags & COMM_TRANSPARENT)) - clientConnection->flags ^= COMM_TRANSPARENT; // prevent TPROXY spoofing of this new IP. - break; - case 0x2: // IPv6 - clientConnection->local = ipu.ipv6_addr.dst_addr; - clientConnection->local.port(ntohs(ipu.ipv6_addr.dst_port)); - clientConnection->remote = ipu.ipv6_addr.src_addr; - clientConnection->remote.port(ntohs(ipu.ipv6_addr.src_port)); - if ((clientConnection->flags & COMM_TRANSPARENT)) - clientConnection->flags ^= COMM_TRANSPARENT; // prevent TPROXY spoofing of this new IP. - break; - default: // do nothing - break; + try { + const auto parsed = ProxyProtocol::Parse(inBuf); + proxyProtocolHeader_ = parsed.header; + assert(bool(proxyProtocolHeader_)); + inBuf.consume(parsed.size); + needProxyProtocolHeader_ = false; + if (proxyProtocolHeader_->hasForwardedAddresses()) { + clientConnection->local = proxyProtocolHeader_->destinationAddress; + clientConnection->remote = proxyProtocolHeader_->sourceAddress; + if ((clientConnection->flags & COMM_TRANSPARENT)) + clientConnection->flags ^= COMM_TRANSPARENT; // prevent TPROXY spoofing of this new IP. + debugs(33, 5, "PROXY/" << proxyProtocolHeader_->version() << " upgrade: " << clientConnection); + } + } catch (const Parser::BinaryTokenizer::InsufficientInput &) { + debugs(33, 3, "PROXY protocol: waiting for more than " << inBuf.length() << " bytes"); + return false; + } catch (const std::exception &e) { + return proxyProtocolError(e.what()); } - debugs(33, 5, "PROXY/2.0 upgrade: " << clientConnection); return true; } @@ -2383,7 +2200,7 @@ ConnStateData::ConnStateData(const MasterXaction::Pointer &xact) : // store the details required for creating more MasterXaction objects as new requests come in log_addr = xact->tcpClient->remote; - log_addr.applyMask(Config.Addrs.client_netmask); + log_addr.applyClientMask(Config.Addrs.client_netmask); // register to receive notice of Squid signal events // which may affect long persisting client connections @@ -2812,6 +2629,7 @@ ConnStateData::postHttpsAccept() acl_checklist->al->tcpClient = clientConnection; acl_checklist->al->cache.port = port; acl_checklist->al->cache.caddr = log_addr; + acl_checklist->al->proxyProtocolHeader = proxyProtocolHeader_; HTTPMSGUNLOCK(acl_checklist->al->request); acl_checklist->al->request = request; HTTPMSGLOCK(acl_checklist->al->request); diff --git a/src/client_side.h b/src/client_side.h index 07d2b8d504..19530e72c8 100644 --- a/src/client_side.h +++ b/src/client_side.h @@ -18,6 +18,7 @@ #include "http/forward.h" #include "HttpControlMsg.h" #include "ipc/FdNotes.h" +#include "proxyp/forward.h" #include "sbuf/SBuf.h" #include "servers/Server.h" #if USE_AUTH @@ -320,6 +321,8 @@ public: NotePairs::Pointer notes(); bool hasNotes() const { return bool(theNotes) && !theNotes->empty(); } + const ProxyProtocol::HeaderPointer &proxyProtocolHeader() const { return proxyProtocolHeader_; } + protected: void startDechunkingRequest(); void finishDechunkingRequest(bool withSuccess); @@ -368,8 +371,6 @@ private: /* PROXY protocol functionality */ bool proxyProtocolValidateClient(); bool parseProxyProtocolHeader(); - bool parseProxy1p0(); - bool parseProxy2p0(); bool proxyProtocolError(const char *reason); #if USE_OPENSSL @@ -384,6 +385,9 @@ private: /// whether PROXY protocol header is still expected bool needProxyProtocolHeader_; + /// the parsed PROXY protocol header + ProxyProtocol::HeaderPointer proxyProtocolHeader_; + #if USE_AUTH /// some user details that can be used to perform authentication on this connection Auth::UserRequest::Pointer auth_; diff --git a/src/client_side_request.cc b/src/client_side_request.cc index b509b29419..07f0db61d8 100644 --- a/src/client_side_request.cc +++ b/src/client_side_request.cc @@ -48,6 +48,7 @@ #include "MemObject.h" #include "Parsing.h" #include "profiler/Profiler.h" +#include "proxyp/Header.h" #include "redirect.h" #include "rfc1738.h" #include "SquidConfig.h" @@ -172,6 +173,7 @@ ClientHttpRequest::ClientHttpRequest(ConnStateData * aConn) : al->tcpClient = clientConnection = aConn->clientConnection; al->cache.port = aConn->port; al->cache.caddr = aConn->log_addr; + al->proxyProtocolHeader = aConn->proxyProtocolHeader(); #if USE_OPENSSL if (aConn->clientConnection != NULL && aConn->clientConnection->isOpen()) { diff --git a/src/esi/VarState.cc b/src/esi/VarState.cc index c35742cd12..b5daf6cde3 100644 --- a/src/esi/VarState.cc +++ b/src/esi/VarState.cc @@ -380,10 +380,10 @@ ESIVariableCookie::eval (ESIVarState &state, char const *subref, char const *fou if (!subref) s = state.header().getStr (Http::HdrType::COOKIE); else { - String S = state.header().getListMember (Http::HdrType::COOKIE, subref, ';'); + const auto subCookie = state.header().getListMember(Http::HdrType::COOKIE, subref, ';'); - if (S.size()) - ESISegment::ListAppend (state.getOutput(), S.rawBuf(), S.size()); + if (subCookie.length()) + ESISegment::ListAppend(state.getOutput(), subCookie.rawContent(), subCookie.length()); else if (found_default) ESISegment::ListAppend (state.getOutput(), found_default, strlen (found_default)); } diff --git a/src/format/ByteCode.h b/src/format/ByteCode.h index afceb7b537..f12c9fd020 100644 --- a/src/format/ByteCode.h +++ b/src/format/ByteCode.h @@ -244,8 +244,12 @@ typedef enum { LFT_EXT_ACL_CLIENT_EUI48, LFT_EXT_ACL_CLIENT_EUI64, LFT_EXT_ACL_NAME, - LFT_EXT_ACL_DATA + LFT_EXT_ACL_DATA, + /* PROXY protocol details */ + LFT_PROXY_PROTOCOL_RECEIVED_HEADER, + LFT_PROXY_PROTOCOL_RECEIVED_HEADER_ELEM, + LFT_PROXY_PROTOCOL_RECEIVED_ALL_HEADERS } ByteCode_t; /// Quoting style for a format output. diff --git a/src/format/Format.cc b/src/format/Format.cc index b4596ee43b..b0e79639c7 100644 --- a/src/format/Format.cc +++ b/src/format/Format.cc @@ -21,6 +21,7 @@ #include "http/Stream.h" #include "HttpRequest.h" #include "MemBuf.h" +#include "proxyp/Header.h" #include "rfc1738.h" #include "sbuf/StringConvert.h" #include "security/CertError.h" @@ -683,7 +684,7 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS if (al->request) { const Adaptation::History::Pointer ah = al->request->adaptHistory(); if (ah) { // XXX: add adapt::allMeta.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator)); + sb = ah->allMeta.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator); out = sb.c_str(); quote = 1; } @@ -742,7 +743,7 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS case LFT_ICAP_REQ_HEADER_ELEM: if (al->icap.request) { - sb = StringToSBuf(al->icap.request->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator)); + sb = al->icap.request->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator); out = sb.c_str(); quote = 1; } @@ -772,7 +773,7 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS case LFT_ICAP_REP_HEADER_ELEM: if (al->icap.reply) { - sb = StringToSBuf(al->icap.reply->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator)); + sb = al->icap.reply->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator); out = sb.c_str(); quote = 1; } @@ -818,7 +819,31 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS #endif case LFT_REQUEST_HEADER_ELEM: if (const Http::Message *msg = actualRequestHeader(al)) { - sb = StringToSBuf(msg->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator)); + sb = msg->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator); + out = sb.c_str(); + quote = 1; + } + break; + + case LFT_PROXY_PROTOCOL_RECEIVED_HEADER: + if (al->proxyProtocolHeader) { + sb = al->proxyProtocolHeader->getValues(fmt->data.headerId, fmt->data.header.separator); + out = sb.c_str(); + quote = 1; + } + break; + + case LFT_PROXY_PROTOCOL_RECEIVED_ALL_HEADERS: + if (al->proxyProtocolHeader) { + sb = al->proxyProtocolHeader->toMime(); + out = sb.c_str(); + quote = 1; + } + break; + + case LFT_PROXY_PROTOCOL_RECEIVED_HEADER_ELEM: + if (al->proxyProtocolHeader) { + sb = al->proxyProtocolHeader->getElem(fmt->data.headerId, fmt->data.header.element, fmt->data.header.separator); out = sb.c_str(); quote = 1; } @@ -826,7 +851,7 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS case LFT_ADAPTED_REQUEST_HEADER_ELEM: if (al->adapted_request) { - sb = StringToSBuf(al->adapted_request->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator)); + sb = al->adapted_request->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator); out = sb.c_str(); quote = 1; } @@ -834,7 +859,7 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS case LFT_REPLY_HEADER_ELEM: if (const Http::Message *msg = actualReplyHeader(al)) { - sb = StringToSBuf(msg->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator)); + sb = msg->header.getByNameListMember(fmt->data.header.header, fmt->data.header.element, fmt->data.header.separator); out = sb.c_str(); quote = 1; } diff --git a/src/format/Token.cc b/src/format/Token.cc index 658c898923..f973d4a456 100644 --- a/src/format/Token.cc +++ b/src/format/Token.cc @@ -11,6 +11,8 @@ #include "format/Token.h" #include "format/TokenTableEntry.h" #include "globals.h" +#include "proxyp/Elements.h" +#include "sbuf/Stream.h" #include "SquidConfig.h" #include "Store.h" @@ -176,6 +178,10 @@ static TokenTableEntry TokenTableMisc[] = { TokenTableEntry(NULL, LFT_NONE) /* this must be last */ }; +static TokenTableEntry TokenTableProxyProtocol[] = { + TokenTableEntry(">h", LFT_PROXY_PROTOCOL_RECEIVED_HEADER), +}; + #if USE_ADAPTATION static TokenTableEntry TokenTableAdapt[] = { TokenTableEntry("all_trs", LFT_ADAPTATION_ALL_XACT_TIMES), @@ -251,6 +257,7 @@ Format::Token::Init() TheConfig.registerTokens(SBuf("tls"),::Format::TokenTableSsl); TheConfig.registerTokens(SBuf("ssl"),::Format::TokenTableSsl); #endif + TheConfig.registerTokens(SBuf("proxy_protocol"), ::Format::TokenTableProxyProtocol); } /// Scans a token table to see if the next token exists there @@ -480,9 +487,14 @@ Format::Token::parse(const char *def, Quoting *quoting) case LFT_NOTE: + case LFT_PROXY_PROTOCOL_RECEIVED_HEADER: + if (data.string) { char *header = data.string; - char *cp = strchr(header, ':'); + const auto initialType = type; + + const auto pseudoHeader = header[0] == ':'; + char *cp = strchr(pseudoHeader ? header+1 : header, ':'); if (cp) { *cp = '\0'; @@ -522,11 +534,22 @@ Format::Token::parse(const char *def, Quoting *quoting) type = LFT_ICAP_REP_HEADER_ELEM; break; #endif + case LFT_PROXY_PROTOCOL_RECEIVED_HEADER: + type = LFT_PROXY_PROTOCOL_RECEIVED_HEADER_ELEM; + break; default: break; } } + if (!*header) + throw TexcHere(ToSBuf("Can't parse configuration token: '", def, "': missing header name")); + + if (initialType == LFT_PROXY_PROTOCOL_RECEIVED_HEADER) + data.headerId = ProxyProtocol::FieldNameToFieldType(SBuf(header)); + else if (pseudoHeader) + throw TexcHere(ToSBuf("Pseudo headers are not supported in this context; got: '", def, "'")); + data.header.header = header; } else { switch (type) { @@ -554,6 +577,9 @@ Format::Token::parse(const char *def, Quoting *quoting) type = LFT_ICAP_REP_ALL_HEADERS; break; #endif + case LFT_PROXY_PROTOCOL_RECEIVED_HEADER: + type = LFT_PROXY_PROTOCOL_RECEIVED_ALL_HEADERS; + break; default: break; } diff --git a/src/format/Token.h b/src/format/Token.h index 858ffb9096..9d9cc674be 100644 --- a/src/format/Token.h +++ b/src/format/Token.h @@ -10,6 +10,7 @@ #define _SQUID_FORMAT_TOKEN_H #include "format/ByteCode.h" +#include "proxyp/Elements.h" /* * Squid configuration allows users to define custom formats in @@ -50,6 +51,9 @@ public: const char *label; struct { char *string; + // TODO: Add ID caching for protocols other than PROXY protocol. + /// the cached ID of the parsed header or zero + ProxyProtocol::Two::FieldType headerId; struct { char *header; diff --git a/src/ip/Address.cc b/src/ip/Address.cc index 0c34696f6a..6761e94624 100644 --- a/src/ip/Address.cc +++ b/src/ip/Address.cc @@ -101,6 +101,13 @@ Ip::Address::applyMask(Ip::Address const &mask_addr) return changes; } +void +Ip::Address::applyClientMask(const Address &mask) +{ + if (!isLocalhost() && isIPv4()) + (void)applyMask(mask); +} + bool Ip::Address::applyMask(const unsigned int cidrMask, int mtype) { diff --git a/src/ip/Address.h b/src/ip/Address.h index 2698dbca60..138fe3c65c 100644 --- a/src/ip/Address.h +++ b/src/ip/Address.h @@ -190,6 +190,11 @@ public: */ bool applyMask(const unsigned int cidr, int mtype); + /// Apply so-called 'privacy masking' to IPv4 addresses, + /// except localhost IP. + /// IPv6 clients use 'privacy addressing' instead. + void applyClientMask(const Address &mask); + /** Return the ASCII equivalent of the address * Semantically equivalent to the IPv4 inet_ntoa() * eg. 127.0.0.1 (IPv4) or ::1 (IPv6) diff --git a/src/parser/BinaryTokenizer.cc b/src/parser/BinaryTokenizer.cc index fd196fae5d..e2f937ab0a 100644 --- a/src/parser/BinaryTokenizer.cc +++ b/src/parser/BinaryTokenizer.cc @@ -9,6 +9,7 @@ /* DEBUG: section 24 SBuf */ #include "squid.h" +#include "ip/Address.h" #include "parser/BinaryTokenizer.h" Parser::BinaryTokenizer::BinaryTokenizer(): BinaryTokenizer(SBuf()) @@ -75,6 +76,14 @@ Parser::BinaryTokenizer::got(const SBuf &value, uint64_t size, const char *descr } +/// debugging helper for parsed addresses +void +Parser::BinaryTokenizer::got(const Ip::Address &value, uint64_t size, const char *description) const +{ + debugs(24, 7, context << description << '=' << value << + BinaryTokenizer_tail(size, parsed_ - size)); +} + /// debugging helper for skipped fields void Parser::BinaryTokenizer::skipped(uint64_t size, const char *description) const @@ -164,6 +173,32 @@ Parser::BinaryTokenizer::area(uint64_t size, const char *description) return result; } +template +Ip::Address +Parser::BinaryTokenizer::inetAny(const char *description) +{ + InAddr addr; + const auto size = sizeof(addr); + want(size, description); + memcpy(&addr, data_.rawContent() + parsed_, size); + parsed_ += size; + const Ip::Address result(addr); + got(result, size, description); + return result; +} + +Ip::Address +Parser::BinaryTokenizer::inet4(const char *description) +{ + return inetAny(description); +} + +Ip::Address +Parser::BinaryTokenizer::inet6(const char *description) +{ + return inetAny(description); +} + void Parser::BinaryTokenizer::skip(uint64_t size, const char *description) { diff --git a/src/parser/BinaryTokenizer.h b/src/parser/BinaryTokenizer.h index 29ac815a1d..de8369e2ae 100644 --- a/src/parser/BinaryTokenizer.h +++ b/src/parser/BinaryTokenizer.h @@ -9,6 +9,7 @@ #ifndef SQUID_SRC_PARSER_BINARYTOKENIZER_H #define SQUID_SRC_PARSER_BINARYTOKENIZER_H +#include "ip/forward.h" #include "sbuf/SBuf.h" namespace Parser @@ -82,6 +83,12 @@ public: /// parse size consecutive bytes as an opaque blob SBuf area(uint64_t size, const char *description); + /// interpret the next 4 bytes as a raw in_addr structure + Ip::Address inet4(const char *description); + + /// interpret the next 16 bytes as a raw in6_addr structure + Ip::Address inet6(const char *description); + /* * Variable-length arrays (a.k.a. Pascal or prefix strings). * pstringN() extracts and returns N-bit length followed by length bytes @@ -109,9 +116,13 @@ protected: void want(uint64_t size, const char *description) const; void got(uint32_t value, uint64_t size, const char *description) const; void got(const SBuf &value, uint64_t size, const char *description) const; + void got(const Ip::Address &value, uint64_t size, const char *description) const; void skipped(uint64_t size, const char *description) const; private: + template + Ip::Address inetAny(const char *description); + SBuf data_; uint64_t parsed_; ///< number of data bytes parsed or skipped uint64_t syncPoint_; ///< where to re-start the next parsing attempt diff --git a/src/proxyp/Elements.cc b/src/proxyp/Elements.cc new file mode 100644 index 0000000000..60daea5321 --- /dev/null +++ b/src/proxyp/Elements.cc @@ -0,0 +1,85 @@ +/* + * 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. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#include "squid.h" +#include "parser/Tokenizer.h" +#include "proxyp/Elements.h" +#include "sbuf/Stream.h" + +#include + +const ProxyProtocol::FieldMap ProxyProtocol::PseudoHeaderFields = { + { SBuf(":version"), ProxyProtocol::Two::htPseudoVersion }, + { SBuf(":command"), ProxyProtocol::Two::htPseudoCommand }, + { SBuf(":src_addr"), ProxyProtocol::Two::htPseudoSrcAddr }, + { SBuf(":dst_addr"), ProxyProtocol::Two::htPseudoDstAddr }, + { SBuf(":src_port"), ProxyProtocol::Two::htPseudoSrcPort }, + { SBuf(":dst_port"), ProxyProtocol::Two::htPseudoDstPort } +}; + +namespace ProxyProtocol { +static Two::FieldType NameToFieldType(const SBuf &); +static Two::FieldType IntegerToFieldType(const SBuf &); +} // namespace ProxyProtocol + +/// FieldNameToFieldType() helper that handles pseudo headers +ProxyProtocol::Two::FieldType +ProxyProtocol::NameToFieldType(const SBuf &name) +{ + const auto it = PseudoHeaderFields.find(name); + if (it != PseudoHeaderFields.end()) + return it->second; + + static const SBuf pseudoMark(":"); + if (name.startsWith(pseudoMark)) + throw TexcHere(ToSBuf("Unsupported PROXY protocol pseudo header: ", name)); + + throw TexcHere(ToSBuf("Invalid PROXY protocol pseudo header or TLV type name. ", + "Expected a pseudo header like :src_addr but got '", name, "'")); +} + +/// FieldNameToFieldType() helper that handles integer TLV types +ProxyProtocol::Two::FieldType +ProxyProtocol::IntegerToFieldType(const SBuf &rawInteger) +{ + int64_t tlvType = 0; + + Parser::Tokenizer ptok(rawInteger); + if (ptok.skip('0')) { + if (!ptok.atEnd()) + throw TexcHere(ToSBuf("Invalid PROXY protocol TLV type value. ", + "Expected a decimal integer without leading zeros but got '", + rawInteger, "'")); // e.g., 077, 0xFF, or 0b101 + // tlvType stays zero + } else { + Must(ptok.int64(tlvType, 10, false)); // the first character is a DIGIT + if (!ptok.atEnd()) + throw TexcHere(ToSBuf("Invalid PROXY protocol TLV type value. ", + "Trailing garbage after ", tlvType, " in '", + rawInteger, "'")); // e.g., 1.0 or 5e0 + } + + const auto limit = std::numeric_limits::max(); + if (tlvType > limit) + throw TexcHere(ToSBuf("Invalid PROXY protocol TLV type value. ", + "Expected an integer less than ", limit, + " but got '", tlvType, "'")); + + return Two::FieldType(tlvType); +} + +ProxyProtocol::Two::FieldType +ProxyProtocol::FieldNameToFieldType(const SBuf &tlvTypeRaw) +{ + // we could branch on ":" instead of DIGIT but then field names that lack a + // leading ":" (like "version") would get a less accurate error message + return Parser::Tokenizer(tlvTypeRaw).skipOne(CharacterSet::DIGIT) ? + IntegerToFieldType(tlvTypeRaw): + NameToFieldType(tlvTypeRaw); +} + diff --git a/src/proxyp/Elements.h b/src/proxyp/Elements.h new file mode 100644 index 0000000000..ffd7b47b5f --- /dev/null +++ b/src/proxyp/Elements.h @@ -0,0 +1,90 @@ +/* + * 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. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#ifndef SQUID_PROXYP_ELEMENTS_H +#define SQUID_PROXYP_ELEMENTS_H + +#include "sbuf/SBuf.h" + +// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt +namespace ProxyProtocol { +namespace Two { + +/// numeric IDs of registered PROXY protocol TLV types and pseudo headers +typedef enum { + htUnknown = 0x00, + + // The PROXY protocol specs list these TLV types as already registered. + htAlpn = 0x01, // PP2_TYPE_ALPN + htAuthority = 0x02, // PP2_TYPE_AUTHORITY + htCrc32c = 0x03, // PP2_TYPE_CRC32C + htNoop = 0x04, // PP2_TYPE_NOOP + htSsl = 0x20, // PP2_TYPE_SSL + htSslVersion = 0x21, // PP2_SUBTYPE_SSL_VERSION + htSslCn = 0x22, // PP2_SUBTYPE_SSL_CN + htSslCipher = 0x23, // PP2_SUBTYPE_SSL_CIPHER + htSslSigAlg = 0x24, // PP2_SUBTYPE_SSL_SIG_ALG + htSslKeyAlg = 0x25, // PP2_SUBTYPE_SSL_KEY_ALG + htNetns = 0x30, // PP2_TYPE_NETNS + + // IDs for PROXY protocol header pseudo-headers. + // Larger than 255 to avoid clashes with possible TLV type IDs. + htPseudoVersion = 0x101, + htPseudoCommand = 0x102, + htPseudoSrcAddr = 0x103, + htPseudoDstAddr = 0x104, + htPseudoSrcPort = 0x105, + htPseudoDstPort = 0x106 +} FieldType; + +/// PROXY protocol 'command' field value +typedef enum { + cmdLocal = 0x00, + cmdProxy = 0x01 +} Command; + +typedef enum { + /// corresponds to a local connection or an unsupported protocol family + afUnspecified = 0x00, + afInet = 0x1, + afInet6 = 0x2, + afUnix = 0x3 +} AddressFamily; + +typedef enum { + tpUnspecified = 0x00, + tpStream = 0x1, + tpDgram = 0x2 +} TransportProtocol; + +/// a single Type-Length-Value (TLV) block from PROXY protocol specs +class Tlv +{ +public: + typedef uint8_t value_type; + + Tlv(const value_type t, const SBuf &val): value(val), type(t) {} + + SBuf value; + value_type type; +}; + +} // namespace Two + +typedef std::map FieldMap; +/// a mapping between pseudo header names and ids +extern const FieldMap PseudoHeaderFields; + +/// Parses human-friendly PROXY protocol field type representation. +/// Only pseudo headers can (and should) be represented by their names. +Two::FieldType FieldNameToFieldType(const SBuf &nameOrId); + +} // namespace ProxyProtocol + +#endif + diff --git a/src/proxyp/Header.cc b/src/proxyp/Header.cc new file mode 100644 index 0000000000..468e0cef9c --- /dev/null +++ b/src/proxyp/Header.cc @@ -0,0 +1,106 @@ +/* + * 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. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#include "squid.h" +#include "proxyp/Elements.h" +#include "proxyp/Header.h" +#include "sbuf/Stream.h" +#include "sbuf/StringConvert.h" +#include "SquidConfig.h" +#include "StrList.h" + +ProxyProtocol::Header::Header(const SBuf &ver, const Two::Command cmd): + version_(ver), + command_(cmd), + ignoreAddresses_(false) +{} + +SBuf +ProxyProtocol::Header::toMime() const +{ + SBufStream result; + for (const auto &p: PseudoHeaderFields) { + const auto value = getValues(p.second); + if (!value.isEmpty()) + result << p.first << ": " << value << "\r\n"; + } + // cannot reuse Header::getValues(): need the original TLVs layout + for (const auto &tlv: tlvs) + result << tlv.type << ": " << tlv.value << "\r\n"; + return result.buf(); +} + +SBuf +ProxyProtocol::Header::getValues(const uint32_t headerType, const char sep) const +{ + switch (headerType) { + + case Two::htPseudoVersion: + return version_; + + case Two::htPseudoCommand: + return ToSBuf(command_); + + case Two::htPseudoSrcAddr: { + if (!hasAddresses()) + return SBuf(); + auto logAddr = sourceAddress; + logAddr.applyClientMask(Config.Addrs.client_netmask); + char ipBuf[MAX_IPSTRLEN]; + return SBuf(logAddr.toStr(ipBuf, sizeof(ipBuf))); + } + + case Two::htPseudoDstAddr: { + if (!hasAddresses()) + return SBuf(); + char ipBuf[MAX_IPSTRLEN]; + return SBuf(destinationAddress.toStr(ipBuf, sizeof(ipBuf))); + } + + case Two::htPseudoSrcPort: { + return hasAddresses() ? ToSBuf(sourceAddress.port()) : SBuf(); + } + + case Two::htPseudoDstPort: { + return hasAddresses() ? ToSBuf(destinationAddress.port()) : SBuf(); + } + + default: { + SBufStream result; + for (const auto &m: tlvs) { + if (m.type == headerType) { + // XXX: result.tellp() always returns -1 + if (!result.buf().isEmpty()) + result << sep; + result << m.value; + } + } + return result.buf(); + } + } +} + +SBuf +ProxyProtocol::Header::getElem(const uint32_t headerType, const char *member, const char sep) const +{ + const auto whole = SBufToString(getValues(headerType, sep)); + return getListMember(whole, member, sep); +} + +const SBuf & +ProxyProtocol::Header::addressFamily() const +{ + static const SBuf v4("4"); + static const SBuf v6("6"); + static const SBuf vMix("mix"); + return + (sourceAddress.isIPv6() && destinationAddress.isIPv6()) ? v6 : + (sourceAddress.isIPv4() && destinationAddress.isIPv4()) ? v4 : + vMix; +} + diff --git a/src/proxyp/Header.h b/src/proxyp/Header.h new file mode 100644 index 0000000000..8b01fd0816 --- /dev/null +++ b/src/proxyp/Header.h @@ -0,0 +1,85 @@ +/* + * 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. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#ifndef SQUID_PROXYP_HEADER_H +#define SQUID_PROXYP_HEADER_H + +#include "base/RefCount.h" +#include "ip/Address.h" +#include "proxyp/Elements.h" +#include "sbuf/SBuf.h" + +namespace ProxyProtocol { + +/// PROXY protocol v1 or v2 header +class Header: public RefCountable +{ +public: + typedef RefCount
Pointer; + typedef std::vector Tlvs; + + Header(const SBuf &ver, const Two::Command cmd); + + /// HTTP header-like string representation of the header. + /// The returned string has one line per pseudo header and + /// one line per TLV (if any). + SBuf toMime() const; + + /// \returns a delimiter-separated list of values of TLVs of the given type + SBuf getValues(const uint32_t headerType, const char delimiter = ',') const; + + /// Searches for the first key=value pair occurrence within the + /// value for the provided TLV type. Assumes that the TLV value + /// is a delimiter-separated list. + /// \returns the value of the found pair or the empty string. + SBuf getElem(const uint32_t headerType, const char *member, const char delimiter) const; + + /// PROXY protocol version + const SBuf &version() const { return version_; } + + /// whether source and destination addresses are valid addresses of the original "client" connection + bool hasForwardedAddresses() const { return !localConnection() && hasAddresses(); } + + /// marks the header as lacking address information + void ignoreAddresses() { ignoreAddresses_ = true; } + + /// whether the header relays address information (including LOCAL connections) + bool hasAddresses() const { return !ignoreAddresses_; } + + /// \returns "4" or "6" if both source and destination addresses are IPv4 or IPv6 + /// \returns "mix" otherwise + const SBuf &addressFamily() const; + + /// source address of the client connection + Ip::Address sourceAddress; + /// intended destination address of the client connection + Ip::Address destinationAddress; + /// empty in v1 headers and when ignored in v2 headers + Tlvs tlvs; + +private: + /// Whether the connection over PROXY protocol is 'cmdLocal'. + /// Such connections are established without being relayed. + /// Received addresses and TLVs are discarded in this mode. + bool localConnection() const { return command_ == Two::cmdLocal; } + + /// PROXY protocol version + SBuf version_; + + /// for v2 headers: the command field + /// for v1 headers: Two::cmdProxy + Two::Command command_; + + /// true if the header relays no address information + bool ignoreAddresses_; +}; + +} // namespace ProxyProtocol + +#endif + diff --git a/src/proxyp/Makefile.am b/src/proxyp/Makefile.am new file mode 100644 index 0000000000..0632aab7c5 --- /dev/null +++ b/src/proxyp/Makefile.am @@ -0,0 +1,21 @@ +## 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. +## Please see the COPYING and CONTRIBUTORS files for details. +## + +include $(top_srcdir)/src/Common.am +include $(top_srcdir)/src/TestHeaders.am + +noinst_LTLIBRARIES = libproxyp.la + +libproxyp_la_SOURCES = \ + forward.h \ + Elements.h \ + Elements.cc \ + Header.cc \ + Header.h \ + Parser.cc \ + Parser.h + diff --git a/src/proxyp/Parser.cc b/src/proxyp/Parser.cc new file mode 100644 index 0000000000..ef1f282311 --- /dev/null +++ b/src/proxyp/Parser.cc @@ -0,0 +1,275 @@ +/* + * 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. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#include "squid.h" +#include "parser/BinaryTokenizer.h" +#include "parser/Tokenizer.h" +#include "proxyp/Elements.h" +#include "proxyp/Header.h" +#include "proxyp/Parser.h" +#include "sbuf/Stream.h" + +#include + +#if HAVE_SYS_SOCKET_H +#include +#endif +#if HAVE_NETINET_IN_H +#include +#endif +#if HAVE_NETINET_IP_H +#include +#endif + +namespace ProxyProtocol { +namespace One { +/// magic octet prefix for PROXY protocol version 1 +static const SBuf Magic("PROXY", 5); +/// extracts PROXY protocol v1 header from the given buffer +static Parsed Parse(const SBuf &buf); + +static void ExtractIp(Parser::Tokenizer &tok, Ip::Address &addr); +static void ExtractPort(Parser::Tokenizer &tok, Ip::Address &addr, const bool trailingSpace); +static void ParseAddresses(Parser::Tokenizer &tok, Header::Pointer &header); +} + +namespace Two { +/// magic octet prefix for PROXY protocol version 2 +static const SBuf Magic("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); +/// extracts PROXY protocol v2 header from the given buffer +static Parsed Parse(const SBuf &buf); + +static void ParseAddresses(const uint8_t family, Parser::BinaryTokenizer &tok, Header::Pointer &header); +static void ParseTLVs(Parser::BinaryTokenizer &tok, Header::Pointer &header); +} +} + +void +ProxyProtocol::One::ExtractIp(Parser::Tokenizer &tok, Ip::Address &addr) +{ + static const auto ipChars = CharacterSet("IP Address",".:") + CharacterSet::HEXDIG; + + SBuf ip; + + if (!tok.prefix(ip, ipChars)) + throw TexcHere("PROXY/1.0 error: malformed IP address"); + + if (!tok.skip(' ')) + throw TexcHere("PROXY/1.0 error: garbage after IP address"); + + if (!addr.GetHostByName(ip.c_str())) + throw TexcHere("PROXY/1.0 error: invalid IP address"); + +} + +void +ProxyProtocol::One::ExtractPort(Parser::Tokenizer &tok, Ip::Address &addr, const bool trailingSpace) +{ + int64_t port = -1; + + if (!tok.int64(port, 10, false)) + throw TexcHere("PROXY/1.0 error: malformed port"); + + if (trailingSpace && !tok.skip(' ')) + throw TexcHere("PROXY/1.0 error: garbage after port"); + + if (port > std::numeric_limits::max()) + throw TexcHere("PROXY/1.0 error: invalid port"); + + addr.port(static_cast(port)); +} + +void +ProxyProtocol::One::ParseAddresses(Parser::Tokenizer &tok, Header::Pointer &header) +{ + static const CharacterSet addressFamilies("Address family", "46"); + SBuf parsedAddressFamily; + + if (!tok.prefix(parsedAddressFamily, addressFamilies, 1)) + throw TexcHere("PROXY/1.0 error: missing or invalid IP address family"); + + if (!tok.skip(' ')) + throw TexcHere("PROXY/1.0 error: missing SP after the IP address family"); + + // parse: src-IP SP dst-IP SP src-port SP dst-port + ExtractIp(tok, header->sourceAddress); + ExtractIp(tok, header->destinationAddress); + + if (header->addressFamily() != parsedAddressFamily) + throw TexcHere("PROXY/1.0 error: declared and/or actual IP address families mismatch"); + + ExtractPort(tok, header->sourceAddress, true); + ExtractPort(tok, header->destinationAddress, false); +} + +/// parses PROXY protocol v1 header from the buffer +ProxyProtocol::Parsed +ProxyProtocol::One::Parse(const SBuf &buf) +{ + Parser::Tokenizer tok(buf); + + static const SBuf::size_type maxHeaderLength = 107; // including CRLF + static const auto maxInteriorLength = maxHeaderLength - Magic.length() - 2; + static const auto interiorChars = CharacterSet::CR.complement().rename("non-CR"); + SBuf interior; + + if (!(tok.prefix(interior, interiorChars, maxInteriorLength) && + tok.skip('\r') && + tok.skip('\n'))) { + if (tok.atEnd()) + throw Parser::BinaryTokenizer::InsufficientInput(); + // "empty interior", "too-long interior", or "missing LF after CR" + throw TexcHere("PROXY/1.0 error: malformed header"); + } + // extracted all PROXY protocol bytes + + static const SBuf v1("1.0"); + Header::Pointer header = new Header(v1, Two::cmdProxy); + + Parser::Tokenizer interiorTok(interior); + + if (!interiorTok.skip(' ')) + throw TexcHere("PROXY/1.0 error: missing SP after the magic sequence"); + + static const SBuf protoUnknown("UNKNOWN"); + static const SBuf protoTcp("TCP"); + + if (interiorTok.skip(protoTcp)) + ParseAddresses(interiorTok, header); + else if (interiorTok.skip(protoUnknown)) + header->ignoreAddresses(); + else + throw TexcHere("PROXY/1.0 error: invalid INET protocol or family"); + + return Parsed(header, tok.parsedSize()); +} + +void +ProxyProtocol::Two::ParseAddresses(const uint8_t family, Parser::BinaryTokenizer &tok, Header::Pointer &header) +{ + switch (family) { + + case afInet: { + header->sourceAddress = tok.inet4("src_addr IPv4"); + header->destinationAddress = tok.inet4("dst_addr IPv4"); + header->sourceAddress.port(tok.uint16("src_port")); + header->destinationAddress.port(tok.uint16("dst_port")); + break; + } + + case afInet6: { + header->sourceAddress = tok.inet6("src_addr IPv6"); + header->destinationAddress = tok.inet6("dst_addr IPv6"); + header->sourceAddress.port(tok.uint16("src_port")); + header->destinationAddress.port(tok.uint16("dst_port")); + break; + } + + case afUnix: { // TODO: add support + // the address block length is 216 bytes + tok.skip(216, "unix_addr"); + break; + } + + default: { + // unreachable code: we have checked family validity already + Must(false); + break; + } + } +} + +void +ProxyProtocol::Two::ParseTLVs(Parser::BinaryTokenizer &tok, Header::Pointer &header) { + while (!tok.atEnd()) { + const auto type = tok.uint8("pp2_tlv::type"); + header->tlvs.emplace_back(type, tok.pstring16("pp2_tlv::value")); + } +} + +ProxyProtocol::Parsed +ProxyProtocol::Two::Parse(const SBuf &buf) +{ + Parser::BinaryTokenizer tokHeader(buf, true); + + const auto versionAndCommand = tokHeader.uint8("version and command"); + + const auto version = (versionAndCommand & 0xF0) >> 4; + if (version != 2) // version == 2 is mandatory + throw TexcHere(ToSBuf("PROXY/2.0 error: invalid version ", version)); + + const auto command = (versionAndCommand & 0x0F); + if (command > cmdProxy) + throw TexcHere(ToSBuf("PROXY/2.0 error: invalid command ", command)); + + const auto familyAndProto = tokHeader.uint8("family and proto"); + + const auto family = (familyAndProto & 0xF0) >> 4; + if (family > afUnix) + throw TexcHere(ToSBuf("PROXY/2.0 error: invalid address family ", family)); + + const auto proto = (familyAndProto & 0x0F); + if (proto > tpDgram) + throw TexcHere(ToSBuf("PROXY/2.0 error: invalid transport protocol ", proto)); + + const auto rawHeader = tokHeader.pstring16("header"); + + static const SBuf v2("2.0"); + Header::Pointer header = new Header(v2, Two::Command(command)); + + if (proto == tpUnspecified || family == afUnspecified) { + header->ignoreAddresses(); + // discard address block and TLVs because we cannot tell + // how to parse such addresses and where the TLVs start. + } else { + Parser::BinaryTokenizer leftoverTok(rawHeader); + ParseAddresses(family, leftoverTok, header); + // TODO: parse TLVs for LOCAL connections + if (header->hasForwardedAddresses()) + ParseTLVs(leftoverTok, header); + } + + return Parsed(header, tokHeader.parsed()); +} + +ProxyProtocol::Parsed +ProxyProtocol::Parse(const SBuf &buf) +{ + Parser::Tokenizer magicTok(buf); + + const auto parser = + magicTok.skip(Two::Magic) ? &Two::Parse : + magicTok.skip(One::Magic) ? &One::Parse : + nullptr; + + if (parser) { + const auto parsed = (parser)(magicTok.remaining()); + return Parsed(parsed.header, magicTok.parsedSize() + parsed.size); + } + + // detect and terminate other protocols + if (buf.length() >= Two::Magic.length()) { + // PROXY/1.0 magic is shorter, so we know that + // the input does not start with any PROXY magic + throw TexcHere("PROXY protocol error: invalid magic"); + } + + // TODO: detect short non-magic prefixes earlier to avoid + // waiting for more data which may never come + + // not enough bytes to parse magic yet + throw Parser::BinaryTokenizer::InsufficientInput(); +} + +ProxyProtocol::Parsed::Parsed(const Header::Pointer &parsedHeader, const size_t parsedSize): + header(parsedHeader), + size(parsedSize) +{ + assert(bool(parsedHeader)); +} + diff --git a/src/proxyp/Parser.h b/src/proxyp/Parser.h new file mode 100644 index 0000000000..06d4c40a96 --- /dev/null +++ b/src/proxyp/Parser.h @@ -0,0 +1,36 @@ +/* + * 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. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#ifndef SQUID_PROXYP_PARSER_H +#define SQUID_PROXYP_PARSER_H + +#include "proxyp/forward.h" +#include "sbuf/forward.h" + +namespace ProxyProtocol { + +/// successful parsing result +class Parsed +{ +public: + Parsed(const HeaderPointer &parsedHeader, const size_t parsedSize); + + HeaderPointer header; ///< successfully parsed header; not nil + size_t size; ///< raw bytes parsed, including any magic/delimiters +}; + +/// Parses a PROXY protocol header from the buffer, determining +/// the protocol version (v1 or v2) by the leading magic string. +/// \throws Parser::BinaryTokenizer::InsufficientInput to ask for more data +/// \returns the successfully parsed header +Parsed Parse(const SBuf &); + +} // namespace ProxyProtocol + +#endif + diff --git a/src/proxyp/forward.h b/src/proxyp/forward.h new file mode 100644 index 0000000000..7c69999378 --- /dev/null +++ b/src/proxyp/forward.h @@ -0,0 +1,24 @@ +/* + * 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. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#ifndef _SQUID_SRC_PROXYP_FORWARD_H +#define _SQUID_SRC_PROXYP_FORWARD_H + +#include "base/RefCount.h" + +namespace ProxyProtocol +{ + +class Header; + +typedef RefCount
HeaderPointer; + +} + +#endif /* _SQUID_SRC_PROXYP_FORWARD_H */ + diff --git a/src/tests/stub_HttpHeader.cc b/src/tests/stub_HttpHeader.cc index f85c466da3..e898fa8e04 100644 --- a/src/tests/stub_HttpHeader.cc +++ b/src/tests/stub_HttpHeader.cc @@ -50,8 +50,8 @@ String HttpHeader::getById(Http::HdrType) const STUB_RETVAL(String()) bool HttpHeader::getByIdIfPresent(Http::HdrType, String *) const STUB_RETVAL(false) bool HttpHeader::hasNamed(const SBuf &, String *) const STUB_RETVAL(false) bool HttpHeader::hasNamed(const char *, unsigned int, String *) const STUB_RETVAL(false) -String HttpHeader::getByNameListMember(const char *, const char *, const char) const STUB_RETVAL(String()) -String HttpHeader::getListMember(Http::HdrType, const char *, const char) const STUB_RETVAL(String()) +SBuf HttpHeader::getByNameListMember(const char *, const char *, const char) const STUB_RETVAL(SBuf()) +SBuf HttpHeader::getListMember(Http::HdrType, const char *, const char) const STUB_RETVAL(SBuf()) int HttpHeader::has(Http::HdrType) const STUB_RETVAL(0) void HttpHeader::addVia(const AnyP::ProtocolVersion &, const HttpHeader *) STUB void HttpHeader::putInt(Http::HdrType, int) STUB