Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic PROXY protocol support for Squid-to-peer connections #281

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e2130d1
Initial implementation
eduard-bagdasaryan Jan 16, 2025
41a71bb
Revert tunnel.cc changes until FwdState changes become more stable
rousskov Jan 16, 2025
8c367fb
Re-implement ad-hoc ProxyProtocol::Header packing using new BinaryPacker
rousskov Jan 16, 2025
464bbc5
fixup: Removed failed attempt at (hopefully premature) optimization
rousskov Jan 16, 2025
0392fde
fixup: Report packing state _before_ assertions
rousskov Jan 16, 2025
bf3f105
Generate a PROXY protocol header before opening a connection
eduard-bagdasaryan Jan 20, 2025
e3c4dab
Moved Magic() functions to Elements.cc
eduard-bagdasaryan Jan 20, 2025
267ba2a
Made FwdState::proxyProtocolHeader optional<>
eduard-bagdasaryan Jan 21, 2025
9fb2bb5
Added missing headers
eduard-bagdasaryan Jan 21, 2025
42c5ef5
Detailed decision making time in directive docs
rousskov Jan 21, 2025
fb8efa2
Added pseudo header and TLV configuration documentation
rousskov Jan 21, 2025
f0ceb1f
Fixed the condition passed to cs->allowPersistent()
eduard-bagdasaryan Jan 21, 2025
8434e72
Autoformatted
eduard-bagdasaryan Jan 21, 2025
9ac8bfc
Basic options support for http_outgoing_proxy_protocol
eduard-bagdasaryan Jan 23, 2025
5aa51b3
Refactored with AddrOption, PortOption and TlvOption classes
eduard-bagdasaryan Jan 27, 2025
dfe7934
Refactored, added some descriptions and performed some tests
eduard-bagdasaryan Jan 29, 2025
3d6262f
Added RefCount for Options
eduard-bagdasaryan Jan 29, 2025
1a90e3b
Autoformatted
eduard-bagdasaryan Jan 29, 2025
49e50c0
Do not dereference a nil valueFormat
eduard-bagdasaryan Jan 29, 2025
b848cb7
Simplified and reduced duplication
eduard-bagdasaryan Jan 29, 2025
0a1cf1f
Minor polishing
eduard-bagdasaryan Jan 29, 2025
32742cb
Implemented OutgoingHttpConfig::dump()
eduard-bagdasaryan Jan 29, 2025
58963f2
Added a missing header
eduard-bagdasaryan Jan 29, 2025
0f209f3
Added a negative configuration test
eduard-bagdasaryan Jan 30, 2025
c856b67
Improved with format/forward.h
eduard-bagdasaryan Feb 3, 2025
b460c57
Addressing code review comments
eduard-bagdasaryan Feb 3, 2025
fa64314
Simplified by moving the parsing into the Option constructor
eduard-bagdasaryan Feb 3, 2025
50096e6
Covered other exception paths in OutgoingHttpConfig::adjustAddresses()
eduard-bagdasaryan Feb 3, 2025
2968fcd
Introduced special handling for "-" header fields
rousskov Feb 3, 2025
d3835c6
Introduced Token::print(ostream)
eduard-bagdasaryan Feb 4, 2025
0f26c31
Remove Option::theValue
eduard-bagdasaryan Feb 4, 2025
46f04ea
OutgoingHttpConfig::adjustAddresses()
eduard-bagdasaryan Feb 4, 2025
97c059f
Merged from head
eduard-bagdasaryan Feb 4, 2025
208b71b
Removed and unused method
eduard-bagdasaryan Feb 5, 2025
72303fb
Format::needsAle() should cover all codes that does not need ALE
eduard-bagdasaryan Feb 5, 2025
3717a74
Configure an unspecified port/address as a single dash
eduard-bagdasaryan Feb 5, 2025
b1aebf3
Let the adjustAddresses() decide whether the problem is fatal
eduard-bagdasaryan Feb 5, 2025
04142e8
Added a couple of tlvs to the negative configuration test
eduard-bagdasaryan Feb 5, 2025
9d069ee
Reject tlvs with a length greater than 65535
eduard-bagdasaryan Feb 5, 2025
ed07241
Code cleanup
eduard-bagdasaryan Feb 5, 2025
2b537ce
Autoformatted
eduard-bagdasaryan Feb 5, 2025
e045f61
Renamed to Format::isConstant(), addressing a request
eduard-bagdasaryan Feb 5, 2025
93cb024
Reject port options with leftovers
eduard-bagdasaryan Feb 5, 2025
c2338c5
Avoid duplicate tlvs
eduard-bagdasaryan Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 90 additions & 5 deletions src/FwdState.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
#include "neighbors.h"
#include "pconn.h"
#include "PeerPoolMgr.h"
#include "parser/BinaryPacker.h"
#include "proxyp/Elements.h"
#include "proxyp/Header.h"
#include "proxyp/OutgoingHttpConfig.h"
#include "ResolvedPeers.h"
#include "security/BlindPeerConnector.h"
#include "SquidConfig.h"
Expand Down Expand Up @@ -881,8 +885,14 @@ FwdState::noteConnection(HappyConnOpener::Answer &answer)
return dispatch();
}

sendProxyProtoHeaderIfNeeded(answer.conn);
}

void
FwdState::tunnelIfNeeded(const Comm::ConnectionPointer &conn)
{
// Check if we need to TLS before use
if (const auto *peer = answer.conn->getPeer()) {
if (const auto *peer = conn->getPeer()) {
// Assume that it is only possible for the client-first from the
// bumping modes to try connect to a remote server. The bumped
// requests with other modes are using pinned connections or fails.
Expand All @@ -896,12 +906,87 @@ FwdState::noteConnection(HappyConnOpener::Answer &answer)
if (originWantsEncryptedTraffic && // the "encrypted traffic" part
!peer->options.originserver && // the "through a proxy" part
!peer->secure.encryptTransport) // the "exclude HTTPS proxies" part
return advanceDestination("establish tunnel through proxy", answer.conn, [this,&answer] {
establishTunnelThruProxy(answer.conn);
return advanceDestination("establish tunnel through proxy", conn, [this,&conn] {
establishTunnelThruProxy(conn);
});
}

secureConnectionToPeerIfNeeded(answer.conn);
secureConnectionToPeerIfNeeded(conn);
}

/// whether it is required to send Proxy protocol header
bool
FwdState::needProxyProtoHeader() const
{
if (!Config.outgoingProxyProtocolHttp)
return false;

if (!Config.outgoingProxyProtocolHttp->aclList)
return true;

ACLFilledChecklist ch(Config.outgoingProxyProtocolHttp->aclList, request);
ch.al = al;
ch.syncAle(request, nullptr);
return ch.fastCheck().allowed();
}

void
FwdState::sendProxyProtoHeaderIfNeeded(const Comm::ConnectionPointer &conn)
{
if (needProxyProtoHeader()) {
static const SBuf v2("2.0");
ProxyProtocol::Header header(v2, ProxyProtocol::Two::cmdProxy);
header.sourceAddress = conn->local; // TODO: must be original source
header.destinationAddress = conn->remote;

BinaryPacker packer;
header.pack(packer);
const auto packed = packer.packed();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you update this code to generate the PROXY protocol header before opening a connection, please store it as SBuf. Convert to MemBuf only when it is time to write it (using the freshly opened connection).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


// XXX: Avoid this copying by adding an SBuf-friendly Comm::Write()!
MemBuf mb;
mb.init();
mb.append(packed.rawContent(), packed.length());

AsyncCall::Pointer call = commCbCall(17, 5, "FwdState::proxyHeaderSent",
CommIoCbPtrFun(&FwdState::proxyHeaderSent, this));
Comm::Write(conn, &mb, call);
return;
}

tunnelIfNeeded(conn);
}

/// resumes operations after the (possibly failed) Proxy protocol header sending
void
FwdState::proxyHeaderSent(const Comm::ConnectionPointer &conn, char *, size_t size, Comm::Flag errflag, int, void *data)
{
auto fwd = reinterpret_cast<FwdState *>(data);

ErrorState *error = nullptr;

if (errflag == Comm::ERR_CLOSING)
return;

if (!Comm::IsConnOpen(conn) || fd_table[conn->fd].closing()) {
// The socket could get closed while our callback was queued. Sync
// Connection. XXX: Connection::fd may already be stale/invalid here.
fwd->closePendingConnection(conn, "conn was closed while waiting for FwdState::proxyHeaderSent");
error = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, fwd->request, fwd->al);
}

if (errflag != Comm::OK || size <= 0) {
conn->close();
return;
}

if (error) {
fwd->fail(error);
fwd->retryOrBail();
return;
}

fwd->tunnelIfNeeded(conn);
}

void
Expand Down Expand Up @@ -1139,7 +1224,7 @@ FwdState::connectStart()
retriable = ch.fastCheck().allowed();
}
cs->setRetriable(retriable);
cs->allowPersistent(pconnRace != raceHappened);
cs->allowPersistent((pconnRace != raceHappened) && !needProxyProtoHeader());
destinations->notificationPending = true; // start() is async
transportWait.start(cs, callback);
}
Expand Down
6 changes: 6 additions & 0 deletions src/FwdState.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ class FwdState: public RefCountable, public PeerSelectionInitiator

void establishTunnelThruProxy(const Comm::ConnectionPointer &);
void tunnelEstablishmentDone(Http::TunnelerAnswer &answer);
bool needProxyProtoHeader() const;
void sendProxyProtoHeaderIfNeeded(const Comm::ConnectionPointer &);
void tunnelIfNeeded(const Comm::ConnectionPointer &);
void secureConnectionToPeerIfNeeded(const Comm::ConnectionPointer &);
void secureConnectionToPeer(const Comm::ConnectionPointer &);
void successfullyConnectedToPeer(const Comm::ConnectionPointer &);
Expand Down Expand Up @@ -206,6 +209,9 @@ class FwdState: public RefCountable, public PeerSelectionInitiator
/// called by Store if the entry is no longer usable
static void HandleStoreAbort(FwdState *);

/// callback for sendProxyProtoHeaderIfNeeded()
static void proxyHeaderSent(const Comm::ConnectionPointer &, char *, size_t , Comm::Flag , int, void *data);

private:
Pointer self;
ErrorState *err;
Expand Down
3 changes: 3 additions & 0 deletions src/SquidConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "MessageDelayPools.h"
#endif
#include "Notes.h"
#include "proxyp/forward.h"
#include "security/Context.h"
#include "security/forward.h"
#if USE_OPENSSL
Expand Down Expand Up @@ -547,6 +548,8 @@ class SquidConfig
int connect_gap;
int connect_timeout;
} happyEyeballs;

ProxyProtocol::OutgoingHttpConfig *outgoingProxyProtocolHttp;
};

extern SquidConfig Config;
Expand Down
1 change: 1 addition & 0 deletions src/cf.data.depend
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ sslproxy_cert_sign acl
sslproxy_cert_adapt acl
ftp_epsv acl
cache_log_message
ProxyProtocol::OutgoingHttpConfig* acl
38 changes: 38 additions & 0 deletions src/cf.data.pre
Original file line number Diff line number Diff line change
Expand Up @@ -1769,6 +1769,44 @@ DOC_START
See https://wiki.squid-cache.org/SquidFaq/SquidAcl for details.
DOC_END

NAME: http_outgoing_proxy_protocol
TYPE: ProxyProtocol::OutgoingHttpConfig*
LOC: Config.outgoingProxyProtocolHttp
DEFAULT: none
DEFAULT_DOC: Squid does not send PROXY protocol headers
DOC_START
Determines which Squid-to-peer HTTP connections start with a PROXY
protocol header. The decision is made before Squid selects the next
forwarding hop (e.g., the source and destination IP addresses of
Squid-to-peer connection are not known). The decision is made every
time Squid has to (re)forward the request.

http_outgoing_proxy_protocol [options] [if [!]<acl>...]

For now, Squid only sends PROXY protocol v2 headers.

For now, Squid disables HTTP connection reuse after sending
the PROXY protocol header on that connection.

For now, PROXY protocol header is sent unencrypted, even for
connections to TLS origin servers and cache_peers with "tls" option.
This allows some network services to extract PROXY protocol header
information before switching to forwarding of encrypted bytes. Future
enhancements may support optional encryption of PROXY protocol bytes.
For now, no options are supported. TODO: Consider adding a _required_
"encrypt=no" option _now_, so that we can later change the _default_
logic from the current "never encrypt" to "encrypt=auto" (that would
depend on whether the next hop is using TLS).

When opening a CONNECT tunnel through a cache_peer, Squid sends the
PROXY protocol header before sending CONNECT request. PROXY protocol
header bytes and CONNECT request header bytes may share the same TCP
packet.

This clause only supports fast acl types.
See https://wiki.squid-cache.org/SquidFaq/SquidAcl for details.
DOC_END

NAME: follow_x_forwarded_for
TYPE: acl_access
IFDEF: FOLLOW_X_FORWARDED_FOR
Expand Down
91 changes: 91 additions & 0 deletions src/parser/BinaryPacker.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (C) 1996-2025 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 "ip/Address.h"
#include "parser/BinaryPacker.h"

#include <limits>

/// helper for methods that need to store a single byte
void
BinaryPacker::packOctet_(const uint8_t value)
{
output_.append(static_cast<char>(value));
}

/// helper for methods that need to store a variable number of bytes
void
BinaryPacker::packOctets_(const void *value, const size_t size)
{
output_.append(static_cast<const char*>(value), size);
}

/// helper for reporting to-be-serialized field
template <typename Value>
void
BinaryPacker::packing_(const char * const description, const Value &value, const size_t size) const
{
debugs(24, 7, description << "[" << size << " bytes]: " << value);
}

void
BinaryPacker::uint8(const char * const description, const uint8_t value)
{
packing_(description, value, 1);
packOctet_(value);
}

void
BinaryPacker::uint16(const char * const description, const uint16_t value)
{
packing_(description, value, 2);
packOctet_(value >> 8);
packOctet_(value);
}

void
BinaryPacker::area(const char * const description, const SBuf &blob)
{
packing_(description, __FUNCTION__, blob.length());
packOctets_(blob.rawContent(), blob.length());
}

void
BinaryPacker::inet(const char * const description, const Ip::Address &ip)
{
if (ip.isIPv4()) {
in_addr ip4;
packing_(description, ip, sizeof(ip4));
ip.getInAddr(ip4);
packOctets_(&ip4, sizeof(ip4));
} else {
in6_addr ip6;
packing_(description, ip, sizeof(ip6));
ip.getInAddr(ip6);
packOctets_(&ip6, sizeof(ip6));
}
}

void
BinaryPacker::pstring8(const char * const description, const SBuf &area)
{
packing_(description, __FUNCTION__, area.length());
Assure(area.length() <= std::numeric_limits<uint8_t>::max());
uint8("pstring8() length", area.length());
packOctets_(area.rawContent(), area.length());
}

void
BinaryPacker::pstring16(const char * const description, const SBuf &area)
{
packing_(description, __FUNCTION__, area.length());
Assure(area.length() <= std::numeric_limits<uint16_t>::max());
uint16("pstring16() length", area.length());
packOctets_(area.rawContent(), area.length());
}
53 changes: 53 additions & 0 deletions src/parser/BinaryPacker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 1996-2025 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_PARSER_BINARYPACKER_H
#define SQUID_SRC_PARSER_BINARYPACKER_H

#include "ip/forward.h"
#include "parser/forward.h"
#include "sbuf/SBuf.h"

/// Serializes various common types using network byte order (where applicable).
/// \sa Parser::BinaryTokenizer that parses serialized fields.
class BinaryPacker
{
public:
/// packs a single-byte unsigned integer
void uint8(const char *description, uint8_t);

/// packs a two-byte unsigned integer
void uint16(const char *description, uint16_t);

/// packs all given bytes as an opaque blob
void area(const char *description, const SBuf &);

/// packs in_addr or in6_addr structure; port information is not stored
void inet(const char *description, const Ip::Address &);

/*
* Variable-length arrays (a.k.a. Pascal or prefix strings).
* pstringN() packs an N-bit length field followed by length bytes
*/
void pstring8(const char *description, const SBuf &); ///< up to 255 byte-long p-string
void pstring16(const char *description, const SBuf &); ///< up to 64 KiB-long p-string

const SBuf &packed() const { return output_; }

private:
void packOctet_(uint8_t);
void packOctets_(const void *, size_t);
template <typename Value> void packing_(const char *description, const Value &, size_t size) const;

private:
/// serialized bytes accumulated so far
SBuf output_;
};

#endif /* SQUID_SRC_PARSER_BINARYPACKER_H */

2 changes: 2 additions & 0 deletions src/parser/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ include $(top_srcdir)/src/Common.am
noinst_LTLIBRARIES = libparser.la

libparser_la_SOURCES = \
BinaryPacker.cc \
BinaryPacker.h \
BinaryTokenizer.cc \
BinaryTokenizer.h \
Tokenizer.cc \
Expand Down
2 changes: 2 additions & 0 deletions src/parser/forward.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ class BinaryTokenizer;
class InsufficientInput {};
} // namespace Parser

class BinaryPacker;

#endif /* SQUID_SRC_PARSER_FORWARD_H */

Loading
Loading