From: Janne Mareike Koschinski Date: Fri, 6 Dec 2019 09:44:50 +0000 (+0100) Subject: Implement support for the HAProxy proxy protocol X-Git-Tag: test-travis-01~7 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=71253052c7152711f7f1b96f274d6f06d97f2cdb Implement support for the HAProxy proxy protocol --- diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 000b772a..d6dbf779 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -41,6 +41,7 @@ target_sources(${TARGET} PRIVATE peerfactory.cpp presetnetworks.cpp quassel.cpp + proxyline.cpp remotepeer.cpp settings.cpp signalproxy.cpp diff --git a/src/common/authhandler.h b/src/common/authhandler.h index c2ac3ffe..b808442b 100644 --- a/src/common/authhandler.h +++ b/src/common/authhandler.h @@ -37,7 +37,7 @@ public: QTcpSocket* socket() const; - bool isLocal() const; + virtual bool isLocal() const; virtual void handle(const Protocol::RegisterClient&) { invalidMessage(); } virtual void handle(const Protocol::ClientDenied&) { invalidMessage(); } diff --git a/src/common/ircdecoder.cpp b/src/common/ircdecoder.cpp index e642ef1b..cadefbf7 100644 --- a/src/common/ircdecoder.cpp +++ b/src/common/ircdecoder.cpp @@ -63,16 +63,7 @@ QString IrcDecoder::parseTagValue(const QString& value) return result; } -/** - * Extracts a space-delimited fragment from an IRC message - * @param raw Raw Message - * @param start Current index into the message, will be advanced automatically - * @param end End of fragment, if already known. Default is -1, in which case it will be set to the next whitespace - * character or the end of the string - * @param prefix Required prefix. Default is 0. If set, this only parses a fragment if it starts with the given prefix. - * @return Fragment - */ -QByteArray extractFragment(const QByteArray& raw, int& start, int end = -1, char prefix = 0) +QByteArray IrcDecoder::extractFragment(const QByteArray& raw, int& start, int end, char prefix) { // Try to set find the end of the space-delimited fragment if (end == -1) { @@ -100,12 +91,7 @@ QByteArray extractFragment(const QByteArray& raw, int& start, int end = -1, char return fragment; } -/** - * Skips empty parts in the message - * @param raw Raw Message - * @param start Current index into the message, will be advanced automatically - */ -void skipEmptyParts(const QByteArray& raw, int& start) +void IrcDecoder::skipEmptyParts(const QByteArray& raw, int& start) { while (start < raw.length() && raw[start] == ' ') { start++; diff --git a/src/common/ircdecoder.h b/src/common/ircdecoder.h index 20fda497..fcafcfea 100644 --- a/src/common/ircdecoder.h +++ b/src/common/ircdecoder.h @@ -39,6 +39,24 @@ public: * @param parameters[out] Parsed list of parameters */ static void parseMessage(const std::function& decode, const QByteArray& raw, QHash& tags, QString& prefix, QString& command, QList& parameters); + + /** + * Extracts a space-delimited fragment from an IRC message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + * @param end End of fragment, if already known. Default is -1, in which case it will be set to the next whitespace + * character or the end of the string + * @param prefix Required prefix. Default is 0. If set, this only parses a fragment if it starts with the given prefix. + * @return Fragment + */ + static QByteArray extractFragment(const QByteArray& raw, int& start, int end = -1, char prefix = 0); + + /** + * Skips empty parts in the message + * @param raw Raw Message + * @param start Current index into the message, will be advanced automatically + */ + static void skipEmptyParts(const QByteArray& raw, int& start); private: /** * Parses an encoded IRCv3 message tag value diff --git a/src/common/protocol.h b/src/common/protocol.h index 01f6ffbe..ad17e179 100644 --- a/src/common/protocol.h +++ b/src/common/protocol.h @@ -32,6 +32,8 @@ namespace Protocol { const quint32 magic = 0x42b33f00; +const quint32 proxyMagic = 0x50524f58; + enum Type { InternalProtocol = 0x00, diff --git a/src/common/protocols/legacy/legacypeer.cpp b/src/common/protocols/legacy/legacypeer.cpp index ddec56be..9b1c56f4 100644 --- a/src/common/protocols/legacy/legacypeer.cpp +++ b/src/common/protocols/legacy/legacypeer.cpp @@ -49,7 +49,7 @@ void LegacyPeer::setSignalProxy(::SignalProxy* proxy) // enable compression now if requested - the initial handshake is uncompressed in the legacy protocol! _useCompression = socket()->property("UseCompression").toBool(); if (_useCompression) - qDebug() << "Using compression for peer:" << qPrintable(socket()->peerAddress().toString()); + qDebug() << "Using compression for peer:" << qPrintable(address()); } } diff --git a/src/common/proxyline.cpp b/src/common/proxyline.cpp new file mode 100644 index 00000000..7bd61c6f --- /dev/null +++ b/src/common/proxyline.cpp @@ -0,0 +1,70 @@ +/*************************************************************************** + * Copyright (C) 2005-2020 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "proxyline.h" + +#include "ircdecoder.h" + +ProxyLine ProxyLine::parseProxyLine(const QByteArray& line) +{ + ProxyLine result; + + int start = 0; + if (line.startsWith("PROXY")) { + start = 5; + } + IrcDecoder::skipEmptyParts(line, start); + QByteArray protocol = IrcDecoder::extractFragment(line, start); + if (protocol == "TCP4") { + result.protocol = QAbstractSocket::IPv4Protocol; + } else if (protocol == "TCP6") { + result.protocol = QAbstractSocket::IPv6Protocol; + } else { + result.protocol = QAbstractSocket::UnknownNetworkLayerProtocol; + } + + if (result.protocol != QAbstractSocket::UnknownNetworkLayerProtocol) { + bool ok; + IrcDecoder::skipEmptyParts(line, start); + result.sourceHost = QHostAddress(QString::fromLatin1(IrcDecoder::extractFragment(line, start))); + IrcDecoder::skipEmptyParts(line, start); + result.sourcePort = QString::fromLatin1(IrcDecoder::extractFragment(line, start)).toUShort(&ok); + if (!ok) result.sourcePort = 0; + IrcDecoder::skipEmptyParts(line, start); + result.targetHost = QHostAddress(QString::fromLatin1(IrcDecoder::extractFragment(line, start))); + IrcDecoder::skipEmptyParts(line, start); + result.targetPort = QString::fromLatin1(IrcDecoder::extractFragment(line, start)).toUShort(&ok); + if (!ok) result.targetPort = 0; + } + + return result; +} + + +QDebug operator<<(QDebug dbg, const ProxyLine& p) { + dbg.nospace(); + dbg << "(protocol = " << p.protocol; + if (p.protocol == QAbstractSocket::UnknownNetworkLayerProtocol) { + dbg << ")"; + } else { + dbg << ", sourceHost = " << p.sourceHost << ", sourcePort = " << p.sourcePort << ", targetHost = " << p.targetHost << ", targetPort = " << p.targetPort << ")"; + } + return dbg.space(); +} diff --git a/src/common/proxyline.h b/src/common/proxyline.h new file mode 100644 index 00000000..4fb2e9e7 --- /dev/null +++ b/src/common/proxyline.h @@ -0,0 +1,39 @@ +/*************************************************************************** + * Copyright (C) 2005-2020 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#pragma once + +#include +#include +#include + +#include "common-export.h" + +struct COMMON_EXPORT ProxyLine +{ + QAbstractSocket::NetworkLayerProtocol protocol = QAbstractSocket::UnknownNetworkLayerProtocol; + QHostAddress sourceHost; + uint16_t sourcePort; + QHostAddress targetHost; + uint16_t targetPort; + + static ProxyLine parseProxyLine(const QByteArray& line); + friend COMMON_EXPORT QDebug operator<<(QDebug dbg, const ProxyLine& p); +}; diff --git a/src/common/quassel.cpp b/src/common/quassel.cpp index b705a02c..00600c05 100644 --- a/src/common/quassel.cpp +++ b/src/common/quassel.cpp @@ -362,6 +362,7 @@ void Quassel::setupCliParser() {"ident-listen", tr("The address(es) quasselcore will listen on for ident requests. Same format as --listen."), tr("
[,...]"), "::1,127.0.0.1"}, {"oidentd", tr("Enable oidentd integration. In most cases you should also enable --strict-ident.")}, {"oidentd-conffile", tr("Set path to oidentd configuration file."), tr("file")}, + {"proxy-cidr", tr("Set IP range from which proxy protocol definitions are allowed"), tr("
[,...]"), "::1,127.0.0.1"}, #ifdef HAVE_SSL {"require-ssl", tr("Require SSL for remote (non-loopback) client connections.")}, {"ssl-cert", tr("Specify the path to the SSL certificate."), tr("path"), "configdir/quasselCert.pem"}, diff --git a/src/common/remotepeer.cpp b/src/common/remotepeer.cpp index 9b02ed22..d234a733 100644 --- a/src/common/remotepeer.cpp +++ b/src/common/remotepeer.cpp @@ -18,9 +18,12 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ +#include + +#include + #include #include -#include #ifdef HAVE_SSL # include @@ -28,6 +31,7 @@ # include #endif +#include "proxyline.h" #include "remotepeer.h" #include "util.h" @@ -41,6 +45,8 @@ RemotePeer::RemotePeer(::AuthHandler* authHandler, QTcpSocket* socket, Compresso , _socket(socket) , _compressor(new Compressor(socket, level, this)) , _signalProxy(nullptr) + , _proxyLine({}) + , _useProxyLine(false) , _heartBeatTimer(new QTimer(this)) , _heartBeatCount(0) , _lag(0) @@ -83,24 +89,40 @@ void RemotePeer::onCompressionError(Compressor::Error error) QString RemotePeer::description() const { - if (socket()) - return socket()->peerAddress().toString(); + return address(); +} - return QString(); +QHostAddress RemotePeer::hostAddress() const +{ + if (_useProxyLine) { + return _proxyLine.sourceHost; + } + else if (socket()) { + return socket()->peerAddress(); + } + + return {}; } QString RemotePeer::address() const { - if (socket()) - return socket()->peerAddress().toString(); - - return QString(); + QHostAddress address = hostAddress(); + if (address.isNull()) { + return {}; + } + else { + return address.toString(); + } } quint16 RemotePeer::port() const { - if (socket()) + if (_useProxyLine) { + return _proxyLine.sourcePort; + } + else if (socket()) { return socket()->peerPort(); + } return 0; } @@ -170,11 +192,8 @@ bool RemotePeer::isSecure() const bool RemotePeer::isLocal() const { - if (socket()) { - if (socket()->peerAddress() == QHostAddress::LocalHost || socket()->peerAddress() == QHostAddress::LocalHostIPv6) - return true; - } - return false; + return hostAddress() == QHostAddress::LocalHost || + hostAddress() == QHostAddress::LocalHostIPv6; } bool RemotePeer::isOpen() const @@ -212,7 +231,7 @@ bool RemotePeer::readMessage(QByteArray& msg) if (_msgSize == 0) { if (_compressor->bytesAvailable() < 4) return false; - _compressor->read((char*)&_msgSize, 4); + _compressor->read((char*) &_msgSize, 4); _msgSize = qFromBigEndian(_msgSize); if (_msgSize > maxMessageSize) { @@ -280,3 +299,21 @@ void RemotePeer::sendHeartBeat() dispatch(HeartBeat(QDateTime::currentDateTime().toUTC())); ++_heartBeatCount; } + +void RemotePeer::setProxyLine(ProxyLine proxyLine) +{ + _proxyLine = std::move(proxyLine); + + if (socket()) { + if (_proxyLine.protocol != QAbstractSocket::UnknownNetworkLayerProtocol) { + QList subnets = Quassel::optionValue("proxy-cidr").split(","); + for (const QString& subnet : subnets) { + if (socket()->peerAddress().isInSubnet(QHostAddress::parseSubnet(subnet))) { + _useProxyLine = true; + return; + } + } + } + } + _useProxyLine = false; +} diff --git a/src/common/remotepeer.h b/src/common/remotepeer.h index f18e91cd..4a824923 100644 --- a/src/common/remotepeer.h +++ b/src/common/remotepeer.h @@ -27,6 +27,7 @@ #include "compressor.h" #include "peer.h" #include "protocol.h" +#include "proxyline.h" #include "signalproxy.h" class QTimer; @@ -46,11 +47,14 @@ public: void setSignalProxy(SignalProxy* proxy) override; + void setProxyLine(ProxyLine proxyLine); + virtual QString protocolName() const = 0; QString description() const override; virtual quint16 enabledFeatures() const { return 0; } QString address() const override; + QHostAddress hostAddress() const; quint16 port() const override; bool isOpen() const override; @@ -105,6 +109,8 @@ private: QTcpSocket* _socket; Compressor* _compressor; SignalProxy* _signalProxy; + ProxyLine _proxyLine; + bool _useProxyLine; QTimer* _heartBeatTimer; int _heartBeatCount; int _lag; diff --git a/src/core/core.cpp b/src/core/core.cpp index e63e9671..a2b7283e 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -730,7 +730,7 @@ void Core::incomingConnection() connect(handler, &AuthHandler::socketError, this, &Core::socketError); connect(handler, &CoreAuthHandler::handshakeComplete, this, &Core::setupClientSession); - qInfo() << qPrintable(tr("Client connected from")) << qPrintable(socket->peerAddress().toString()); + qInfo() << qPrintable(tr("Client connected from")) << qPrintable(handler->hostAddress().toString()); if (!_configured) { stopListening(tr("Closing server for basic setup.")); @@ -744,7 +744,7 @@ void Core::clientDisconnected() auto* handler = qobject_cast(sender()); Q_ASSERT(handler); - qInfo() << qPrintable(tr("Non-authed client disconnected:")) << qPrintable(handler->socket()->peerAddress().toString()); + qInfo() << qPrintable(tr("Non-authed client disconnected:")) << qPrintable(handler->hostAddress().toString()); _connectingClients.remove(handler); handler->deleteLater(); diff --git a/src/core/coreauthhandler.cpp b/src/core/coreauthhandler.cpp index 1ea6305e..8d39d06d 100644 --- a/src/core/coreauthhandler.cpp +++ b/src/core/coreauthhandler.cpp @@ -32,6 +32,9 @@ CoreAuthHandler::CoreAuthHandler(QTcpSocket* socket, QObject* parent) : AuthHandler(parent) , _peer(nullptr) , _metricsServer(Core::instance()->metricsServer()) + , _proxyReceived(false) + , _proxyLine({}) + , _useProxyLine(false) , _magicReceived(false) , _legacy(false) , _clientRegistered(false) @@ -45,13 +48,37 @@ CoreAuthHandler::CoreAuthHandler(QTcpSocket* socket, QObject* parent) void CoreAuthHandler::onReadyRead() { - if (socket()->bytesAvailable() < 4) - return; - // once we have selected a peer, we certainly don't want to read more data! if (_peer) return; + if (!_proxyReceived) { + quint32 magic; + socket()->peek((char*) &magic, 4); + magic = qFromBigEndian(magic); + + if (magic == Protocol::proxyMagic) { + if (!socket()->canReadLine()) { + return; + } + QByteArray line = socket()->readLine(108); + _proxyLine = ProxyLine::parseProxyLine(line); + if (_proxyLine.protocol != QAbstractSocket::UnknownNetworkLayerProtocol) { + QList subnets = Quassel::optionValue("proxy-cidr").split(","); + for (const QString& subnet : subnets) { + if (socket()->peerAddress().isInSubnet(QHostAddress::parseSubnet(subnet))) { + _useProxyLine = true; + break; + } + } + } + } + _proxyReceived = true; + } + + if (socket()->bytesAvailable() < 4) + return; + if (!_magicReceived) { quint32 magic; socket()->peek((char*)&magic, 4); @@ -101,7 +128,7 @@ void CoreAuthHandler::onReadyRead() RemotePeer* peer = PeerFactory::createPeer(_supportedProtos, this, socket(), level, this); if (!peer) { - qWarning() << "Received invalid handshake data from client" << socket()->peerAddress().toString(); + qWarning() << "Received invalid handshake data from client" << hostAddress().toString(); close(); return; } @@ -130,6 +157,9 @@ void CoreAuthHandler::setPeer(RemotePeer* peer) qDebug().nospace() << "Using " << qPrintable(peer->protocolName()) << "..."; _peer = peer; + if (_proxyLine.protocol != QAbstractSocket::UnknownNetworkLayerProtocol) { + _peer->setProxyLine(_proxyLine); + } disconnect(socket(), &QIODevice::readyRead, this, &CoreAuthHandler::onReadyRead); } @@ -148,7 +178,7 @@ void CoreAuthHandler::onProtocolVersionMismatch(int actual, int expected) bool CoreAuthHandler::checkClientRegistered() { if (!_clientRegistered) { - qWarning() << qPrintable(tr("Client")) << qPrintable(socket()->peerAddress().toString()) + qWarning() << qPrintable(tr("Client")) << qPrintable(hostAddress().toString()) << qPrintable(tr("did not send a registration message before trying to login, rejecting.")); _peer->dispatch( Protocol::ClientDenied(tr("Client not initialized!
You need to send a registration message before trying to login."))); @@ -167,7 +197,7 @@ void CoreAuthHandler::handle(const Protocol::RegisterClient& msg) useSsl = _connectionFeatures & Protocol::Encryption; if (Quassel::isOptionSet("require-ssl") && !useSsl && !_peer->isLocal()) { - qInfo() << qPrintable(tr("SSL required but non-SSL connection attempt from %1").arg(socket()->peerAddress().toString())); + qInfo() << qPrintable(tr("SSL required but non-SSL connection attempt from %1").arg(hostAddress().toString())); _peer->dispatch(Protocol::ClientDenied(tr("SSL is required!
You need to use SSL in order to connect to this core."))); _peer->close(); return; @@ -222,7 +252,7 @@ void CoreAuthHandler::handle(const Protocol::Login& msg) return; if (!Core::isConfigured()) { - qWarning() << qPrintable(tr("Client")) << qPrintable(socket()->peerAddress().toString()) + qWarning() << qPrintable(tr("Client")) << qPrintable(hostAddress().toString()) << qPrintable(tr("attempted to login before the core was configured, rejecting.")); _peer->dispatch(Protocol::ClientDenied( tr("Attempted to login before core was configured!
The core must be configured before attempting to login."))); @@ -247,7 +277,7 @@ void CoreAuthHandler::handle(const Protocol::Login& msg) } if (uid == 0) { - qInfo() << qPrintable(tr("Invalid login attempt from %1 as \"%2\"").arg(socket()->peerAddress().toString(), msg.user)); + qInfo() << qPrintable(tr("Invalid login attempt from %1 as \"%2\"").arg(hostAddress().toString(), msg.user)); _peer->dispatch(Protocol::LoginFailed(tr( "Invalid username or password!
The username/password combination you supplied could not be found in the database."))); if (_metricsServer) { @@ -261,7 +291,7 @@ void CoreAuthHandler::handle(const Protocol::Login& msg) } qInfo() << qPrintable(tr("Client %1 initialized and authenticated successfully as \"%2\" (UserId: %3).") - .arg(socket()->peerAddress().toString(), msg.user, QString::number(uid.toInt()))); + .arg(_peer->address(), msg.user, QString::number(uid.toInt()))); const auto& clientFeatures = _peer->features(); auto unsupported = clientFeatures.toStringList(false); @@ -284,6 +314,24 @@ void CoreAuthHandler::handle(const Protocol::Login& msg) emit handshakeComplete(_peer, uid); } +QHostAddress CoreAuthHandler::hostAddress() const +{ + if (_useProxyLine) { + return _proxyLine.sourceHost; + } + else if (socket()) { + return socket()->peerAddress(); + } + + return {}; +} + +bool CoreAuthHandler::isLocal() const +{ + return hostAddress() == QHostAddress::LocalHost || + hostAddress() == QHostAddress::LocalHostIPv6; +} + /*** SSL Stuff ***/ void CoreAuthHandler::startSsl() diff --git a/src/core/coreauthhandler.h b/src/core/coreauthhandler.h index 82c2c2c3..db54bec1 100644 --- a/src/core/coreauthhandler.h +++ b/src/core/coreauthhandler.h @@ -24,6 +24,7 @@ #include "authhandler.h" #include "metricsserver.h" #include "peerfactory.h" +#include "proxyline.h" #include "remotepeer.h" #include "types.h" @@ -34,6 +35,9 @@ class CoreAuthHandler : public AuthHandler public: CoreAuthHandler(QTcpSocket* socket, QObject* parent = nullptr); + QHostAddress hostAddress() const; + bool isLocal() const override; + signals: void handshakeComplete(RemotePeer* peer, UserId uid); @@ -63,6 +67,9 @@ private: RemotePeer* _peer; MetricsServer* _metricsServer; + bool _proxyReceived; + ProxyLine _proxyLine; + bool _useProxyLine; bool _magicReceived; bool _legacy; bool _clientRegistered;