File

plugins/mod_proxy65.lua @ 13801:a5d5fefb8b68 13.0

mod_tls: Enable Prosody's certificate checking for incoming s2s connections (fixes #1916) (thanks Damian, Zash) Various options in Prosody allow control over the behaviour of the certificate verification process For example, some deployments choose to allow falling back to traditional "dialback" authentication (XEP-0220), while others verify via DANE, hard-coded fingerprints, or other custom plugins. Implementing this flexibility requires us to override OpenSSL's default certificate verification, to allow Prosody to verify the certificate itself, apply custom policies and make decisions based on the outcome. To enable our custom logic, we have to suppress OpenSSL's default behaviour of aborting the connection with a TLS alert message. With LuaSec, this can be achieved by using the verifyext "lsec_continue" flag. We also need to use the lsec_ignore_purpose flag, because XMPP s2s uses server certificates as "client" certificates (for mutual TLS verification in outgoing s2s connections). Commit 99d2100d2918 moved these settings out of the defaults and into mod_s2s, because we only really need these changes for s2s, and they should be opt-in, rather than automatically applied to all TLS services we offer. That commit was incomplete, because it only added the flags for incoming direct TLS connections. StartTLS connections are handled by mod_tls, which was not applying the lsec_* flags. It previously worked because they were already in the defaults. This resulted in incoming s2s connections with "invalid" certificates being aborted early by OpenSSL, even if settings such as `s2s_secure_auth = false` or DANE were present in the config. Outgoing s2s connections inherit verify "none" from the defaults, which means OpenSSL will receive the cert but will not terminate the connection when it is deemed invalid. This means we don't need lsec_continue there, and we also don't need lsec_ignore_purpose (because the remote peer is a "server"). Wondering why we can't just use verify "none" for incoming s2s? It's because in that mode, OpenSSL won't request a certificate from the peer for incoming connections. Setting verify "peer" is how you ask OpenSSL to request a certificate from the client, but also what triggers its built-in verification.
author Matthew Wild <mwild1@gmail.com>
date Tue, 01 Apr 2025 17:26:56 +0100
parent 12977:74b9e05af71e
line wrap: on
line source

-- Prosody IM
-- Copyright (C) 2008-2011 Matthew Wild
-- Copyright (C) 2008-2011 Waqas Hussain
-- Copyright (C) 2009 Thilo Cestonaro
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--

module:set_global();

local jid_compare, jid_prep = require "prosody.util.jid".compare, require "prosody.util.jid".prep;
local st = require "prosody.util.stanza";
local sha1 = require "prosody.util.hashes".sha1;
local server = require "prosody.net.server";
local portmanager = require "prosody.core.portmanager";

local sessions = module:shared("sessions");
local transfers = module:shared("transfers");
local max_buffer_size = 4096;

local listener = {};

function listener.onincoming(conn, data)
	local session = sessions[conn] or {};

	local transfer = transfers[session.sha];
	if transfer and transfer.activated then -- copy data between initiator and target
		local initiator, target = transfer.initiator, transfer.target;
		(conn == initiator and target or initiator):write(data);
		return;
	end -- FIXME server.link should be doing this?

	if not session.greeting_done then
		local nmethods = data:byte(2) or 0;
		if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data
			if data:find("%z") then -- 0x00 = 'No authentication' is supported
				session.greeting_done = true;
				sessions[conn] = session;
				conn:write("\5\0"); -- send (SOCKS version 5, No authentication)
				module:log("debug", "SOCKS5 greeting complete");
				return;
			end
		end -- else error, unexpected input
		conn:write("\5\255"); -- send (SOCKS version 5, no acceptable method)
		conn:close();
		module:log("debug", "Invalid SOCKS5 greeting received: %q", data:sub(1, 300));
	else -- connection request
		--local head = string.char( 0x05, 0x01, 0x00, 0x03, 40 ); -- ( VER=5=SOCKS5, CMD=1=CONNECT, RSV=0=RESERVED, ATYP=3=DOMAIMNAME, SHA-1 size )
		if #data == 47 and data:sub(1,5) == "\5\1\0\3\40" and data:sub(-2) == "\0\0" then
			local sha = data:sub(6, 45);
			conn:pause();
			conn:write("\5\0\0\3\40" .. sha .. "\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
			if not transfers[sha] then
				transfers[sha] = {};
				transfers[sha].target = conn;
				session.sha = sha;
				module:log("debug", "SOCKS5 target connected for session %s", sha);
			else -- transfers[sha].target ~= nil
				transfers[sha].initiator = conn;
				session.sha = sha;
				module:log("debug", "SOCKS5 initiator connected for session %s", sha);
				server.link(conn, transfers[sha].target, max_buffer_size);
				server.link(transfers[sha].target, conn, max_buffer_size);
			end
		else -- error, unexpected input
			conn:write("\5\1\0\3\0\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
			conn:close();
			module:log("debug", "Invalid SOCKS5 negotiation received: %q", data:sub(1, 300));
		end
	end
end

function listener.ondisconnect(conn)
	local session = sessions[conn];
	if session then
		if transfers[session.sha] then
			local initiator, target = transfers[session.sha].initiator, transfers[session.sha].target;
			if initiator == conn and target ~= nil then
				target:close();
			elseif target == conn and initiator ~= nil then
				initiator:close();
			end
			transfers[session.sha] = nil;
		end
		-- Clean up any session-related stuff here
		sessions[conn] = nil;
	end
end

function module.add_host(module)
	local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service");

	local proxy_address = module:get_option_string("proxy65_address", host);
	local proxy_acl = module:get_option_array("proxy65_acl");
	local proxy_open_access = module:get_option_boolean("proxy65_open_access", false);

	-- COMPAT w/pre-0.9 where proxy65_port was specified in the components section of the config
	local legacy_config = module:get_option_number("proxy65_port");
	if legacy_config then
		module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config);
	end

	module:depends("disco");
	module:add_identity("proxy", "bytestreams", name);
	module:add_feature("http://jabber.org/protocol/bytestreams");

	module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event)
		local origin, stanza = event.origin, event.stanza;

		-- check ACL
		-- using 'while' instead of 'if' so we can break out of it
		local allow;
		if proxy_acl and #proxy_acl > 0 then
			local jid = stanza.attr.from;
			for _, acl in ipairs(proxy_acl) do
				if jid_compare(jid, acl) then
					allow = true;
					break;
				end
			end
		elseif proxy_open_access or origin.type == "c2s" then
			allow = true;
		end

		if not allow then
			module:log("warn", "Denying use of proxy for %s", stanza.attr.from);
			origin.send(st.error_reply(stanza, "auth", "forbidden"));
			return true;
		end

		local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {});
		if not proxy_port then
			module:log("warn", "Not listening on any port");
			origin.send(st.error_reply(stanza, "wait", "item-not-found", "Not listening on any port"));
			return true;
		end

		local sid = stanza.tags[1].attr.sid;
		origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid})
			:tag("streamhost", {jid=host, host=proxy_address, port=("%d"):format(proxy_port)}));
		return true;
	end);

	module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event)
		local origin, stanza = event.origin, event.stanza;

		local query = stanza.tags[1];
		local sid = query.attr.sid;
		local from = stanza.attr.from;
		local to = query:get_child_text("activate");
		local prepped_to = jid_prep(to);

		local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to);
		if prepped_to and sid then
			local sha = sha1(sid .. from .. prepped_to, true);
			if not transfers[sha] then
				module:log("debug", "Activation request has unknown session id; activation failed (%s)", info);
				origin.send(st.error_reply(stanza, "modify", "item-not-found"));
			elseif not transfers[sha].initiator then
				module:log("debug", "The sender was not connected to the proxy; activation failed (%s)", info);
				origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The sender (you) is not connected to the proxy"));
			--elseif not transfers[sha].target then -- can't happen, as target is set when a transfer object is created
			--	module:log("debug", "The recipient was not connected to the proxy; activation failed (%s)", info);
			--	origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The recipient is not connected to the proxy"));
			else -- if transfers[sha].initiator ~= nil and transfers[sha].target ~= nil then
				module:log("debug", "Transfer activated (%s)", info);
				transfers[sha].activated = true;
				transfers[sha].target:resume();
				transfers[sha].initiator:resume();
				origin.send(st.reply(stanza));
			end
		elseif to and sid then
			module:log("debug", "Malformed activation jid; activation failed (%s)", info);
			origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
		else
			module:log("debug", "Bad request; activation failed (%s)", info);
			origin.send(st.error_reply(stanza, "modify", "bad-request"));
		end
		return true;
	end);
end

module:provides("net", {
	default_port = 5000;
	listener = listener;
	multiplex = {
		pattern = "^\5";
	};
});