File

core/hostmanager.lua @ 13652:a08065207ef0

net.server_epoll: Call :shutdown() on TLS sockets when supported Comment from Matthew: This fixes a potential issue where the Prosody process gets blocked on sockets waiting for them to close. Unlike non-TLS sockets, closing a TLS socket sends layer 7 data, and this can cause problems for sockets which are in the process of being cleaned up. This depends on LuaSec changes which are not yet upstream. From Martijn's original email: So first my analysis of luasec. in ssl.c the socket is put into blocking mode right before calling SSL_shutdown() inside meth_destroy(). My best guess to why this is is because meth_destroy is linked to the __close and __gc methods, which can't exactly be called multiple times and luasec does want to make sure that a tls session is shutdown as clean as possible. I can't say I disagree with this reasoning and don't want to change this behaviour. My solution to this without changing the current behaviour is to introduce a shutdown() method. I am aware that this overlaps in a conflicting way with tcp's shutdown method, but it stays close to the OpenSSL name. This method calls SSL_shutdown() in the current (non)blocking mode of the underlying socket and returns a boolean whether or not the shutdown is completed (matching SSL_shutdown()'s 0 or 1 return values), and returns the familiar ssl_ioerror() strings on error with a false for completion. This error can then be used to determine if we have wantread/wantwrite to finalize things. Once meth_shutdown() has been called once a shutdown flag will be set, which indicates to meth_destroy() that the SSL_shutdown() has been handled by the application and it shouldn't be needed to set the socket to blocking mode. I've left the SSL_shutdown() call in the LSEC_STATE_CONNECTED to prevent TOCTOU if the application reaches a timeout for the shutdown code, which might allow SSL_shutdown() to clean up anyway at the last possible moment. Another thing I've changed to luasec is the call to socket_setblocking() right before calling close(2) in socket_destroy() in usocket.c. According to the latest POSIX[0]: Note that the requirement for close() on a socket to block for up to the current linger interval is not conditional on the O_NONBLOCK setting. Which I read to mean that removing O_NONBLOCK on the socket before close doesn't impact the behaviour and only causes noise in system call tracers. I didn't touch the windows bits of this, since I don't do windows. For the prosody side of things I've made the TLS shutdown bits resemble interface:onwritable(), and put it under a combined guard of self._tls and self.conn.shutdown. The self._tls bit is there to prevent getting stuck on this condition, and self.conn.shutdown is there to prevent the code being called by instances where the patched luasec isn't deployed. The destroy() method can be called from various places and is read by me as the "we give up" error path. To accommodate for these unexpected entrypoints I've added a single call to self.conn:shutdown() to prevent the socket being put into blocking mode. I have no expectations that there is any other use here. Same as previous, the self.conn.shutdown check is there to make sure it's not called on unpatched luasec deployments and self._tls is there to make sure we don't call shutdown() on tcp sockets. I wouldn't recommend logging of the conn:shutdown() error inside close(), since a lot of clients simply close the connection before SSL_shutdown() is done.
author Martijn van Duren <martijn@openbsd.org>
date Thu, 06 Feb 2025 15:04:38 +0000
parent 12972:ead41e25ebc0
line wrap: on
line source

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

local configmanager = require "prosody.core.configmanager";
local modulemanager = require "prosody.core.modulemanager";
local events_new = require "prosody.util.events".new;
local disco_items = require "prosody.util.multitable".new();
local NULL = {};

local log = require "prosody.util.logger".init("hostmanager");

local hosts = prosody.hosts;
local prosody_events = prosody.events;
if not _G.prosody.incoming_s2s then
	require "prosody.core.s2smanager";
end
local incoming_s2s = _G.prosody.incoming_s2s;
local core_route_stanza = _G.prosody.core_route_stanza;

local pairs, rawget = pairs, rawget;
local tostring, type = tostring, type;
local setmetatable = setmetatable;

local _ENV = nil;
-- luacheck: std none

local host_mt = { }
function host_mt:__tostring()
	if self.type == "component" then
		local typ = configmanager.get(self.host, "component_module");
		if typ == "component" then
			return ("Component %q"):format(self.host);
		end
		return ("Component %q %q"):format(self.host, typ);
	elseif self.type == "local" then
		return ("VirtualHost %q"):format(self.host);
	end
end

local hosts_loaded_once;

local activate, deactivate;

local function load_enabled_hosts(config)
	local defined_hosts = config or configmanager.getconfig();
	local activated_any_host;

	for host, host_config in pairs(defined_hosts) do
		if host ~= "*" and host_config.enabled ~= false then
			if not host_config.component_module then
				activated_any_host = true;
			end
			activate(host, host_config);
		end
	end

	if not activated_any_host then
		log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded.");
	end

	prosody_events.fire_event("hosts-activated", defined_hosts);
	hosts_loaded_once = true;
end

prosody_events.add_handler("server-starting", load_enabled_hosts);

local function host_send(stanza)
	core_route_stanza(nil, stanza);
end

function activate(host, host_config)
	if rawget(hosts, host) then return nil, "The host "..host.." is already activated"; end
	host_config = host_config or configmanager.getconfig()[host];
	if not host_config then return nil, "Couldn't find the host "..tostring(host).." defined in the current config"; end
	local host_session = {
		host = host;
		s2sout = {};
		events = events_new();
		send = host_send;
		modules = {};
	};
	function host_session:close(reason)
		log("debug", "Attempt to close host session %s with reason: %s", self.host, reason);
	end
	setmetatable(host_session, host_mt);
	if not host_config.component_module then -- host
		host_session.type = "local";
		host_session.sessions = {};
	else -- component
		host_session.type = "component";
	end
	hosts[host] = host_session;
	if not host_config.disco_hidden and not host:match("[@/]") then
		disco_items:set(host:match("%.(.*)") or "*", host, host_config.name or true);
	end
	for option_name in pairs(host_config) do
		if option_name:match("_ports$") or option_name:match("_interface$") then
			log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name);
		end
	end

	log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host);
	prosody_events.fire_event("host-activated", host);
	return true;
end

function deactivate(host, reason)
	local host_session = hosts[host];
	if not host_session then return nil, "The host "..tostring(host).." is not activated"; end
	log("info", "Deactivating host: %s", host);
	prosody_events.fire_event("host-deactivating", { host = host, host_session = host_session, reason = reason });

	if type(reason) ~= "table" then
		reason = { condition = "host-gone", text = tostring(reason or "This server has stopped serving "..host) };
	end

	-- Disconnect local users, s2s connections
	-- TODO: These should move to mod_c2s and mod_s2s (how do they know they're being unloaded and not reloaded?)
	if host_session.sessions then
		for username, user in pairs(host_session.sessions) do
			for resource, session in pairs(user.sessions) do
				log("debug", "Closing connection for %s@%s/%s", username, host, resource);
				session:close(reason);
			end
		end
	end
	if host_session.s2sout then
		for remotehost, session in pairs(host_session.s2sout) do
			if session.close then
				log("debug", "Closing outgoing connection to %s", remotehost);
				session:close(reason);
			end
		end
	end
	for remote_session in pairs(incoming_s2s) do
		if remote_session.to_host == host then
			log("debug", "Closing incoming connection from %s", remote_session.from_host or "<unknown>");
			remote_session:close(reason);
		end
	end

	-- TODO: This should be done in modulemanager
	if host_session.modules then
		for module in pairs(host_session.modules) do
			modulemanager.unload(host, module);
		end
	end

	hosts[host] = nil;
	if not host:match("[@/]") then
		disco_items:remove(host:match("%.(.*)") or "*", host);
	end
	prosody_events.fire_event("host-deactivated", host);
	log("info", "Deactivated host: %s", host);
	return true;
end

local function get_children(host)
	return disco_items:get(host) or NULL;
end

return {
	activate = activate;
	deactivate = deactivate;
	get_children = get_children;
}