diff options
| author | 2024-07-17 00:06:50 +0100 | |
|---|---|---|
| committer | 2024-07-17 00:06:50 +0100 | |
| commit | 889d521e05f2e922683d48c74eb00290e7a2f548 (patch) | |
| tree | b4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /modules/haproxy.cpp | |
| parent | Use std::endian from C++20 in the sha1 module. (diff) | |
Shuffle the modules about a bit.
Diffstat (limited to 'modules/haproxy.cpp')
| -rw-r--r-- | modules/haproxy.cpp | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/modules/haproxy.cpp b/modules/haproxy.cpp new file mode 100644 index 000000000..101204328 --- /dev/null +++ b/modules/haproxy.cpp @@ -0,0 +1,437 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2021 Dominic Hamon + * Copyright (C) 2019 linuxdaemon <linuxdaemon.irc@gmail.com> + * Copyright (C) 2018-2019, 2021-2023 Sadie Powell <sadie@witchery.services> + * + * This file is part of InspIRCd. InspIRCd 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, version 2. + * + * 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, see <http://www.gnu.org/licenses/>. + */ + + +#include "inspircd.h" +#include "iohook.h" +#include "modules/ssl.h" + +enum +{ + // The TLV flag for a client being connected over TLS. + PP2_CLIENT_SSL = 0x01, + + // The family for TCP over IPv4. + PP2_FAMILY_IPV4 = 0x11, + + // The length of the PP2_FAMILY_IPV4 endpoints. + PP2_FAMILY_IPV4_LENGTH = 12, + + // The family for TCP over IPv6. + PP2_FAMILY_IPV6 = 0x21, + + // The length of the PP2_FAMILY_IPV6 endpoints. + PP2_FAMILY_IPV6_LENGTH = 36, + + // The family for UNIX sockets. + PP2_FAMILY_UNIX = 0x31, + + // The length of the PP2_FAMILY_UNIX endpoints. + PP2_FAMILY_UNIX_LENGTH = 216, + + // The bitmask we apply to extract the command. + PP2_COMMAND_MASK = 0x0F, + + // The length of the PROXY protocol header. + PP2_HEADER_LENGTH = 16, + + // The minimum length of a Type-Length-Value entry. + PP2_TLV_LENGTH = 3, + + // The identifier for a TLS TLV entry. + PP2_TYPE_SSL = 0x20, + + // The minimum length of a PP2_TYPE_SSL TLV entry. + PP2_TYPE_SSL_LENGTH = 5, + + // The length of the PROXY protocol signature. + PP2_SIGNATURE_LENGTH = 12, + + // The PROXY protocol version we support. + PP2_VERSION = 0x20, + + // The bitmask we apply to extract the protocol version. + PP2_VERSION_MASK = 0xF0 +}; + +enum HAProxyState +{ + // We are waiting for the PROXY header section. + HPS_WAITING_FOR_HEADER, + + // We are waiting for the PROXY address section. + HPS_WAITING_FOR_ADDRESS, + + // The client is fully connected. + HPS_CONNECTED +}; + +enum HAProxyCommand +{ + // LOCAL command. + HPC_LOCAL = 0x00, + + // PROXY command. + HPC_PROXY = 0x01 +}; + +struct HAProxyHeader final +{ + // The signature used to identify the HAProxy protocol. + uint8_t signature[PP2_SIGNATURE_LENGTH]; + + // The version of the PROXY protocol and command being sent. + uint8_t version_command; + + // The family for the address. + uint8_t family; + + // The length of the address section. + uint16_t length; +}; + +class HAProxyHookProvider final + : public IOHookProvider +{ +private: + UserCertificateAPI sslapi; + +public: + HAProxyHookProvider(Module* mod) + : IOHookProvider(mod, "haproxy", IOHookProvider::IOH_UNKNOWN, true) + , sslapi(mod) + { + } + + void OnAccept(StreamSocket* sock, const irc::sockets::sockaddrs& client, const irc::sockets::sockaddrs& server) override; + + void OnConnect(StreamSocket* sock) override + { + // We don't need to implement this. + } +}; + +// The signature for a HAProxy PROXY protocol header. +static constexpr char proxy_signature[13] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; + +class HAProxyHook final + : public IOHookMiddle +{ +private: + // The length of the address section. + uint16_t address_length = 0; + + // The endpoint the client is connecting from. + irc::sockets::sockaddrs client; + + // The command sent by the proxy server. + HAProxyCommand command; + + // The endpoint the client is connected to. + irc::sockets::sockaddrs server; + + // The API for interacting with user TLS internals. + UserCertificateAPI& sslapi; + + // The current state of the PROXY parser. + HAProxyState state = HPS_WAITING_FOR_HEADER; + + size_t ReadProxyTLV(StreamSocket* sock, size_t start_index, uint16_t buffer_length) + { + // A TLV must at least consist of a type (uint8_t) and a length (uint16_t). + if (buffer_length < PP2_TLV_LENGTH) + { + sock->SetError("Truncated HAProxy PROXY TLV type and/or length"); + return 0; + } + + // Check that the length can actually contain the TLV value. + std::string& recvq = GetRecvQ(); + uint16_t length = ntohs(recvq[start_index + 1] | (recvq[start_index + 2] << 8)); + if (buffer_length < PP2_TLV_LENGTH + length) + { + sock->SetError("Truncated HAProxy PROXY TLV value"); + return 0; + } + + // What type of TLV are we parsing? + switch (recvq[start_index]) + { + case PP2_TYPE_SSL: + if (!ReadProxyTLVSSL(sock, start_index + PP2_TLV_LENGTH, length)) + return 0; + break; + } + + return PP2_TLV_LENGTH + length; + } + + bool ReadProxyTLVSSL(StreamSocket* sock, size_t start_index, uint16_t buffer_length) + { + // A TLS TLV must at least consist of client info (uint8_t) and verification info (uint32_t). + if (buffer_length < PP2_TYPE_SSL_LENGTH) + { + sock->SetError("Truncated HAProxy PROXY TLS TLV"); + return false; + } + + // If the socket is not a user socket we don't have to do + // anything with this TLVs information. + if (sock->type != StreamSocket::SS_USER) + return true; + + // If the sslinfo module is not loaded we can't + // do anything with this TLV. + if (!sslapi) + return true; + + // If the client is not connecting via TLS the rest of this TLV is irrelevant. + std::string& recvq = GetRecvQ(); + if ((recvq[start_index] & PP2_CLIENT_SSL) == 0) + return true; + + // Create a fake ssl_cert for the user. Ideally we should use the user's + // TLS client certificate here but as of 2018-10-16 this is not forwarded + // by HAProxy. + auto* cert = new ssl_cert(); + cert->error = "HAProxy does not forward client TLS certificates"; + cert->invalid = true; + cert->revoked = true; + cert->trusted = false; + cert->unknownsigner = true; + + // Extract the user for this socket and set their certificate. + LocalUser* luser = static_cast<UserIOHandler*>(sock)->user; + sslapi->SetCertificate(luser, cert); + return true; + } + + int ReadData(std::string& destrecvq) + { + // Once connected we handle no special data. + std::string& recvq = GetRecvQ(); + destrecvq.append(recvq); + recvq.clear(); + return 1; + } + + int ReadProxyAddress(StreamSocket* sock, std::string& destrecvq) + { + // Block until we have the entire address. + std::string& recvq = GetRecvQ(); + if (recvq.length() < address_length) + return 0; + + switch (command) + { + case HPC_LOCAL: + // Skip the address completely. + recvq.erase(0, address_length); + break; + + case HPC_PROXY: + // Store the endpoint information. + size_t tlv_index = 0; + switch (client.family()) + { + case AF_INET: + memcpy(&client.in4.sin_addr.s_addr, &recvq[0], 4); + memcpy(&server.in4.sin_addr.s_addr, &recvq[4], 4); + memcpy(&client.in4.sin_port, &recvq[8], 2); + memcpy(&server.in4.sin_port, &recvq[10], 2); + tlv_index = 12; + break; + + case AF_INET6: + memcpy(client.in6.sin6_addr.s6_addr, &recvq[0], 16); + memcpy(server.in6.sin6_addr.s6_addr, &recvq[16], 16); + memcpy(&client.in6.sin6_port, &recvq[32], 2); + memcpy(&server.in6.sin6_port, &recvq[34], 2); + tlv_index = 36; + break; + + case AF_UNIX: + memcpy(client.un.sun_path, &recvq[0], 108); + memcpy(server.un.sun_path, &recvq[108], 108); + tlv_index = 216; + break; + } + + if (!sock->OnChangeLocalSocketAddress(server) || !sock->OnChangeRemoteSocketAddress(client)) + return -1; + + // Parse any available TLVs. + while (tlv_index < address_length) + { + size_t length = ReadProxyTLV(sock, tlv_index, address_length - tlv_index); + if (!length) + return -1; + + tlv_index += length; + } + + // Erase the processed proxy information from the receive queue. + recvq.erase(0, address_length); + break; + } + + // We're done! + state = HPS_CONNECTED; + return ReadData(destrecvq); + } + + int ReadProxyHeader(StreamSocket* sock, std::string& destrecvq) + { + // Block until we have a header. + std::string& recvq = GetRecvQ(); + if (recvq.length() < PP2_HEADER_LENGTH) + return 0; + + // Read the header. + HAProxyHeader header; + memcpy(&header, recvq.c_str(), PP2_HEADER_LENGTH); + recvq.erase(0, PP2_HEADER_LENGTH); + + // Check we are actually parsing a HAProxy header. + if (memcmp(&header.signature, proxy_signature, PP2_SIGNATURE_LENGTH) != 0) + { + // If we've reached this point the proxy server did not send a proxy information. + sock->SetError("Invalid HAProxy PROXY signature"); + return -1; + } + + // We only support this version of the protocol. + const uint8_t version = (header.version_command & PP2_VERSION_MASK); + if (version != PP2_VERSION) + { + sock->SetError("Unsupported HAProxy PROXY protocol version"); + return -1; + } + + // We only support the LOCAL and PROXY commands. + command = static_cast<HAProxyCommand>(header.version_command & PP2_COMMAND_MASK); + switch (command) + { + case HPC_LOCAL: + // Intentionally left blank. + break; + + case HPC_PROXY: + // Check the protocol support and initialise the sockaddrs. + uint16_t shortest_length; + switch (header.family) + { + case PP2_FAMILY_IPV4: // TCP over IPv4. + client.sa.sa_family = server.sa.sa_family = AF_INET; + shortest_length = PP2_FAMILY_IPV4_LENGTH; + break; + + case PP2_FAMILY_IPV6: // TCP over IPv6. + client.sa.sa_family = server.sa.sa_family = AF_INET6; + shortest_length = PP2_FAMILY_IPV6_LENGTH; + break; + + case PP2_FAMILY_UNIX: // UNIX stream. + client.sa.sa_family = server.sa.sa_family = AF_UNIX; + shortest_length = PP2_FAMILY_UNIX_LENGTH; + break; + + default: // Unknown protocol. + sock->SetError("Invalid HAProxy PROXY protocol type"); + return -1; + } + + // Check that the length can actually contain the addresses. + address_length = ntohs(header.length); + if (address_length < shortest_length) + { + sock->SetError("Truncated HAProxy PROXY address section"); + return -1; + } + break; + + default: + sock->SetError("Unsupported HAProxy PROXY command"); + return -1; + } + + state = HPS_WAITING_FOR_ADDRESS; + return ReadProxyAddress(sock, destrecvq); + } + +public: + HAProxyHook(const std::shared_ptr<IOHookProvider>& Prov, StreamSocket* sock, UserCertificateAPI& api) + : IOHookMiddle(Prov) + , sslapi(api) + { + sock->AddIOHook(this); + } + + bool IsHookReady() const override + { + return state == HPS_CONNECTED; + } + + ssize_t OnStreamSocketWrite(StreamSocket* sock, StreamSocket::SendQueue& uppersendq) override + { + // We don't need to implement this. + GetSendQ().moveall(uppersendq); + return 1; + } + + ssize_t OnStreamSocketRead(StreamSocket* sock, std::string& destrecvq) override + { + switch (state) + { + case HPS_WAITING_FOR_HEADER: + return ReadProxyHeader(sock, destrecvq); + + case HPS_WAITING_FOR_ADDRESS: + return ReadProxyAddress(sock, destrecvq); + + case HPS_CONNECTED: + return ReadData(destrecvq); + } + + // We should never reach this point. + return -1; + } +}; + +void HAProxyHookProvider::OnAccept(StreamSocket* sock, const irc::sockets::sockaddrs& client, const irc::sockets::sockaddrs& server) +{ + new HAProxyHook(shared_from_this(), sock, sslapi); +} + +class ModuleHAProxy final + : public Module +{ +private: + std::shared_ptr<HAProxyHookProvider> hookprov; + +public: + ModuleHAProxy() + : Module(VF_VENDOR, "Allows IRC connections to be made using reverse proxies that implement version 2 of the HAProxy PROXY protocol.") + , hookprov(std::make_shared<HAProxyHookProvider>(this)) + { + } +}; + +MODULE_INIT(ModuleHAProxy) |
