aboutsummaryrefslogtreecommitdiff
/*
 * InspIRCd -- Internet Relay Chat Daemon
 *
 *   Copyright (C) 2026 Atlas
 *   Copyright (C) 2021 Dominic Hamon
 *   Copyright (C) 2019 linuxdaemon <linuxdaemon.irc@gmail.com>
 *   Copyright (C) 2018-2019, 2021-2023 Sadie Powell <sadie@sadiepowell.dev>
 *
 * 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 identifier for a TLS certificate fingerprint TLV entry.
	PP2_TYPE_CERTFP = 0xE0,

	// 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 fingerprint forwarded by the proxy for the client certificate.
	std::string certificate_fingerprint;

	// The fake certificate we attach for this connection, if any.
	reference<ssl_cert> certificate;

	// 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 (static_cast<uint8_t>(recvq[start_index]))
		{
			case PP2_TYPE_SSL:
				if (!ReadProxyTLVSSL(sock, start_index + PP2_TLV_LENGTH, length))
					return 0;
				break;

			case PP2_TYPE_CERTFP:
				if (!ReadProxyTLVCertFP(sock, start_index + PP2_TLV_LENGTH, length))
					return 0;
				break;
		}

		return PP2_TLV_LENGTH + length;
	}

	bool ReadProxyTLVCertFP(StreamSocket* sock, size_t start_index, uint16_t buffer_length)
	{
		// If the socket is not a user socket we don't have to do
		// anything with this TLV's information.
		if (sock->type != StreamSocket::SS_USER)
			return true;

		// The fingerprint must be a non-empty hex-encoded digest of a plausible size.
		// A hex string must have an even length and at most 128 chars (64-byte digest).
		if (buffer_length == 0 || buffer_length % 2 != 0 || buffer_length > 128)
		{
			ServerInstance->Logs.Debug(MODNAME, "Ignoring PP2_TYPE_CERTFP TLV with unexpected length {}", buffer_length);
			return true;
		}

		std::string& recvq = GetRecvQ();
		certificate_fingerprint.assign(&recvq[start_index], buffer_length);

		// Verify every character is a valid hexadecimal digit.
		if (!std::all_of(certificate_fingerprint.begin(), certificate_fingerprint.end(), [](unsigned char c) { return std::isxdigit(c); }))
		{
			ServerInstance->Logs.Debug(MODNAME, "Ignoring PP2_TYPE_CERTFP TLV with non-hex content");
			return true;
		}

		ServerInstance->Logs.Debug(MODNAME, "Received certificate fingerprint from HAProxy: {}", certificate_fingerprint);

		// If the SSL TLV was already processed, patch the existing certificate
		// with the fingerprint now that we have it.
		if (certificate)
		{
			SetCertificateFingerprint(certificate);
		}
		return true;
	}

	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. If the proxy already forwarded a
		// certificate fingerprint via PP2_TYPE_CERTFP we use it; otherwise we only
		// know the client was connected over TLS.
		if (!certificate)
		{
			auto* cert = new ssl_cert();
			cert->dn = "(unknown)";
			cert->issuer = "(unknown)";

			if (!certificate_fingerprint.empty())
			{
				SetCertificateFingerprint(cert);
			}
			else
			{
				cert->error = "HAProxy does not forward client TLS certificates";
				cert->invalid = true;
				cert->revoked = true;
				cert->trusted = false;
				cert->unknownsigner = true;
			}

			LocalUser* luser = static_cast<UserIOHandler*>(sock)->user;
			sslapi->SetCertificate(luser, cert);
			certificate = 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);
	}

	void SetCertificateFingerprint(ssl_cert* cert)
	{
		cert->fingerprints.push_back(std::move(certificate_fingerprint));
		cert->invalid = false;
		cert->trusted = true;
		cert->unknownsigner = false;
		cert->revoked = false;
		cert->error.clear();
	}

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)