Implement support for the HAProxy proxy protocol
authorJanne Mareike Koschinski <janne@kuschku.de>
Fri, 6 Dec 2019 09:44:50 +0000 (10:44 +0100)
committerManuel Nickschas <sputnick@quassel-irc.org>
Sun, 15 Mar 2020 16:22:51 +0000 (17:22 +0100)
14 files changed:
src/common/CMakeLists.txt
src/common/authhandler.h
src/common/ircdecoder.cpp
src/common/ircdecoder.h
src/common/protocol.h
src/common/protocols/legacy/legacypeer.cpp
src/common/proxyline.cpp [new file with mode: 0644]
src/common/proxyline.h [new file with mode: 0644]
src/common/quassel.cpp
src/common/remotepeer.cpp
src/common/remotepeer.h
src/core/core.cpp
src/core/coreauthhandler.cpp
src/core/coreauthhandler.h

index 000b772..d6dbf77 100644 (file)
@@ -41,6 +41,7 @@ target_sources(${TARGET} PRIVATE
     peerfactory.cpp
     presetnetworks.cpp
     quassel.cpp
+    proxyline.cpp
     remotepeer.cpp
     settings.cpp
     signalproxy.cpp
index c2ac3ff..b808442 100644 (file)
@@ -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(); }
index e642ef1..cadefbf 100644 (file)
@@ -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++;
index 20fda49..fcafcfe 100644 (file)
@@ -39,6 +39,24 @@ public:
      * @param parameters[out] Parsed list of parameters
      */
     static void parseMessage(const std::function<QString(const QByteArray&)>& decode, const QByteArray& raw, QHash<IrcTagKey, QString>& tags, QString& prefix, QString& command, QList<QByteArray>& 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
index 01f6ffb..ad17e17 100644 (file)
@@ -32,6 +32,8 @@ namespace Protocol {
 
 const quint32 magic = 0x42b33f00;
 
+const quint32 proxyMagic = 0x50524f58;
+
 enum Type
 {
     InternalProtocol = 0x00,
index ddec56b..9b1c56f 100644 (file)
@@ -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 (file)
index 0000000..7bd61c6
--- /dev/null
@@ -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 (file)
index 0000000..4fb2e9e
--- /dev/null
@@ -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 <QAbstractSocket>
+#include <QByteArray>
+#include <QHostAddress>
+
+#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);
+};
index b705a02..00600c0 100644 (file)
@@ -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("<address>[,...]"), "::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("<address>[,...]"), "::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"},
index 9b02ed2..d234a73 100644 (file)
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
+#include <utility>
+
+#include <QtEndian>
+
 #include <QHostAddress>
 #include <QTimer>
-#include <QtEndian>
 
 #ifdef HAVE_SSL
 #    include <QSslSocket>
@@ -28,6 +31,7 @@
 #    include <QTcpSocket>
 #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<quint32>(_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<QString> subnets = Quassel::optionValue("proxy-cidr").split(",");
+            for (const QString& subnet : subnets) {
+                if (socket()->peerAddress().isInSubnet(QHostAddress::parseSubnet(subnet))) {
+                    _useProxyLine = true;
+                    return;
+                }
+            }
+        }
+    }
+    _useProxyLine = false;
+}
index f18e91c..4a82492 100644 (file)
@@ -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;
index e63e967..a2b7283 100644 (file)
@@ -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<CoreAuthHandler*>(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();
 
index 1ea6305..8d39d06 100644 (file)
@@ -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<quint32>(magic);
+
+        if (magic == Protocol::proxyMagic) {
+            if (!socket()->canReadLine()) {
+                return;
+            }
+            QByteArray line = socket()->readLine(108);
+            _proxyLine = ProxyLine::parseProxyLine(line);
+            if (_proxyLine.protocol != QAbstractSocket::UnknownNetworkLayerProtocol) {
+                QList<QString> 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("<b>Client not initialized!</b><br>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("<b>SSL is required!</b><br>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("<b>Attempted to login before core was configured!</b><br>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(
             "<b>Invalid username or password!</b><br>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()
index 82c2c2c..db54bec 100644 (file)
@@ -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;