aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/mod_s2s.lua
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/mod_s2s.lua')
-rw-r--r--plugins/mod_s2s.lua167
1 files changed, 118 insertions, 49 deletions
diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua
index ee65ba70..45684f9a 100644
--- a/plugins/mod_s2s.lua
+++ b/plugins/mod_s2s.lua
@@ -16,32 +16,38 @@ local tostring, type = tostring, type;
local t_insert = table.insert;
local traceback = debug.traceback;
-local add_task = require "util.timer".add_task;
-local stop_timer = require "util.timer".stop;
-local st = require "util.stanza";
-local initialize_filters = require "util.filters".initialize;
-local nameprep = require "util.encodings".stringprep.nameprep;
-local new_xmpp_stream = require "util.xmppstream".new;
-local s2s_new_incoming = require "core.s2smanager".new_incoming;
-local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
-local s2s_destroy_session = require "core.s2smanager".destroy_session;
-local uuid_gen = require "util.uuid".generate;
-local async = require "util.async";
+local add_task = require "prosody.util.timer".add_task;
+local stop_timer = require "prosody.util.timer".stop;
+local st = require "prosody.util.stanza";
+local initialize_filters = require "prosody.util.filters".initialize;
+local nameprep = require "prosody.util.encodings".stringprep.nameprep;
+local new_xmpp_stream = require "prosody.util.xmppstream".new;
+local s2s_new_incoming = require "prosody.core.s2smanager".new_incoming;
+local s2s_new_outgoing = require "prosody.core.s2smanager".new_outgoing;
+local s2s_destroy_session = require "prosody.core.s2smanager".destroy_session;
+local uuid_gen = require "prosody.util.uuid".generate;
+local async = require "prosody.util.async";
local runner = async.runner;
-local connect = require "net.connect".connect;
-local service = require "net.resolvers.service";
-local resolver_chain = require "net.resolvers.chain";
-local errors = require "util.error";
-local set = require "util.set";
-
-local connect_timeout = module:get_option_number("s2s_timeout", 90);
-local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5);
+local connect = require "prosody.net.connect".connect;
+local service = require "prosody.net.resolvers.service";
+local resolver_chain = require "prosody.net.resolvers.chain";
+local errors = require "prosody.util.error";
+local set = require "prosody.util.set";
+
+local connect_timeout = module:get_option_period("s2s_timeout", 90);
+local stream_close_timeout = module:get_option_period("s2s_close_timeout", 5);
local opt_keepalives = module:get_option_boolean("s2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true));
local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One day...
local secure_domains, insecure_domains =
module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items;
local require_encryption = module:get_option_boolean("s2s_require_encryption", true);
-local stanza_size_limit = module:get_option_number("s2s_stanza_size_limit", 1024*512);
+local stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000);
+
+local advertised_idle_timeout = 14*60; -- default in all net.server implementations
+local network_settings = module:get_option("network_settings");
+if type(network_settings) == "table" and type(network_settings.read_timeout) == "number" then
+ advertised_idle_timeout = network_settings.read_timeout;
+end
local measure_connections_inbound = module:metric(
"gauge", "connections_inbound", "",
@@ -95,6 +101,12 @@ local s2s_service_options = {
};
local s2s_service_options_mt = { __index = s2s_service_options }
+if module:get_option_boolean("use_dane", false) then
+ -- DANE is supported in net.connect but only for outgoing connections,
+ -- to authenticate incoming connections with DANE we need
+ module:depends("s2s_auth_dane_in");
+end
+
module:hook("stats-update", function ()
measure_connections_inbound:clear()
measure_connections_outbound:clear()
@@ -141,22 +153,22 @@ local function bounce_sendq(session, reason)
if session.had_stream then -- set when a stream is opened by the remote
error_type, condition = "wait", "remote-server-timeout";
end
- if errors.is_err(reason) then
+ if errors.is_error(reason) then
error_type, condition, reason_text = reason.type, reason.condition, reason.text;
elseif type(reason) == "string" then
reason_text = reason;
end
- for i, data in ipairs(sendq) do
- local reply = data[2];
- if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then
- reply.attr.type = "error";
- reply:tag("error", {type = error_type, by = session.from_host})
- :tag(condition, {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
- if reason_text then
- reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})
- :text("Server-to-server connection failed: "..reason_text):up();
- end
+ for i, stanza in ipairs(sendq) do
+ if not stanza.attr.xmlns and bouncy_stanzas[stanza.name] and stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+ local reply = st.error_reply(
+ stanza,
+ error_type,
+ condition,
+ reason_text and ("Server-to-server connection failed: "..reason_text) or nil
+ );
core_process_stanza(dummy, reply);
+ else
+ (session.log or log)("debug", "Not eligible for bouncing, discarding %s", stanza:top_tag());
end
sendq[i] = nil;
end
@@ -182,15 +194,11 @@ function route_to_existing_session(event)
(host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
-- Queue stanza until we are able to send it
- local queued_item = {
- tostring(stanza),
- stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza);
- };
if host.sendq then
- t_insert(host.sendq, queued_item);
+ t_insert(host.sendq, st.clone(stanza));
else
-- luacheck: ignore 122
- host.sendq = { queued_item };
+ host.sendq = { st.clone(stanza) };
end
host.log("debug", "stanza [%s] queued ", stanza.name);
return true;
@@ -215,7 +223,7 @@ function route_to_new_session(event)
-- Store in buffer
host_session.bounce_sendq = bounce_sendq;
- host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
+ host_session.sendq = { st.clone(stanza) };
log("debug", "stanza [%s] queued until connection complete", stanza.name);
-- FIXME Cleaner solution to passing extra data from resolvers to net.server
-- This mt-clone allows resolvers to add extra data, currently used for DANE TLSA records
@@ -255,9 +263,37 @@ function module.add_host(module)
end
module:hook("route/remote", route_to_existing_session, -1);
module:hook("route/remote", route_to_new_session, -10);
+ module:hook("s2sout-stream-features", function (event)
+ if not (stanza_size_limit or advertised_idle_timeout) then return end
+ local limits = event.features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" })
+ if stanza_size_limit then
+ limits:text_tag("max-bytes", string.format("%d", stanza_size_limit));
+ end
+ if advertised_idle_timeout then
+ limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout));
+ end
+ limits:up();
+ end);
+ module:hook_tag("urn:xmpp:bidi", "bidi", function(session, stanza)
+ -- Advertising features on bidi connections where no <stream:features> is sent in the other direction
+ local limits = stanza:get_child("limits", "urn:xmpp:stream-limits:0");
+ if limits then
+ session.outgoing_stanza_size_limit = tonumber(limits:get_child_text("max-bytes"));
+ end
+ end, 100);
module:hook("s2s-authenticated", make_authenticated, -1);
module:hook("s2s-read-timeout", keepalive, -1);
+ module:hook("smacks-ack-delayed", function (event)
+ if event.origin.type == "s2sin" or event.origin.type == "s2sout" then
+ event.origin:close("connection-timeout");
+ return true;
+ end
+ end, -1);
module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) -- luacheck: ignore 212/stanza
+ local limits = stanza:get_child("limits", "urn:xmpp:stream-limits:0");
+ if limits then
+ session.outgoing_stanza_size_limit = tonumber(limits:get_child_text("max-bytes"));
+ end
if session.type == "s2sout" then
-- Stream is authenticated and we are seem to be done with feature negotiation,
-- so the stream is ready for stanzas. RFC 6120 Section 4.3
@@ -283,7 +319,7 @@ function module.add_host(module)
function module.unload()
if module.reloading then return end
for _, session in pairs(sessions) do
- if session.to_host == module.host or session.from_host == module.host then
+ if session.host == module.host then
session:close("host-gone");
end
end
@@ -328,8 +364,8 @@ function mark_connected(session)
if sendq then
session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
local send = session.sends2s;
- for i, data in ipairs(sendq) do
- send(data[1]);
+ for i, stanza in ipairs(sendq) do
+ send(stanza);
sendq[i] = nil;
end
session.sendq = nil;
@@ -393,10 +429,10 @@ end
--- Helper to check that a session peer's certificate is valid
local function check_cert_status(session)
local host = session.direction == "outgoing" and session.to_host or session.from_host
- local conn = session.conn:socket()
+ local conn = session.conn
local cert
- if conn.getpeercertificate then
- cert = conn:getpeercertificate()
+ if conn.ssl_peercertificate then
+ cert = conn:ssl_peercertificate()
end
return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
@@ -408,8 +444,7 @@ local function session_secure(session)
session.secure = true;
session.encrypted = true;
- local sock = session.conn:socket();
- local info = sock.info and sock:info();
+ local info = session.conn:ssl_info();
if type(info) == "table" then
(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
session.compressed = info.compression;
@@ -438,7 +473,8 @@ function stream_callbacks._streamopened(session, attr)
session.had_stream = true; -- Had a stream opened at least once
-- TODO: Rename session.secure to session.encrypted
- if session.secure == false then
+ if session.secure == false then -- Set by mod_tls during STARTTLS handshake
+ session.starttls = "completed";
session_secure(session);
end
@@ -526,6 +562,18 @@ function stream_callbacks._streamopened(session, attr)
end
if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then
+ if stanza_size_limit or advertised_idle_timeout then
+ features:reset();
+ local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" });
+ if stanza_size_limit then
+ limits:text_tag("max-bytes", string.format("%d", stanza_size_limit));
+ end
+ if advertised_idle_timeout then
+ limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout));
+ end
+ features:reset();
+ end
+
log("debug", "Sending stream features: %s", features);
session.sends2s(features);
else
@@ -760,6 +808,7 @@ local function initialize_session(session)
local w = conn.write;
if conn:ssl() then
+ -- Direct TLS was used
session_secure(session);
end
@@ -770,6 +819,11 @@ local function initialize_session(session)
end
if t then
t = filter("bytes/out", tostring(t));
+ if session.outgoing_stanza_size_limit and #t > session.outgoing_stanza_size_limit then
+ log("warn", "Attempt to send a stanza exceeding session limit of %dB (%dB)!", session.outgoing_stanza_size_limit, #t);
+ -- TODO Pass identifiable error condition back to allow appropriate handling
+ return false
+ end
if t then
return w(conn, t);
end
@@ -932,14 +986,27 @@ end
-- Complete the sentence "Your certificate " with what's wrong
local function friendly_cert_error(session) --> string
if session.cert_chain_status == "invalid" then
- if session.cert_chain_errors then
+ if type(session.cert_chain_errors) == "table" then
local cert_errors = set.new(session.cert_chain_errors[1]);
if cert_errors:contains("certificate has expired") then
return "has expired";
elseif cert_errors:contains("self signed certificate") then
return "is self-signed";
+ elseif cert_errors:contains("no matching DANE TLSA records") then
+ return "does not match any DANE TLSA records";
+ end
+
+ local chain_errors = set.new(session.cert_chain_errors[2]);
+ for i, e in pairs(session.cert_chain_errors) do
+ if i > 2 then chain_errors:add_list(e); end
+ end
+ if chain_errors:contains("certificate has expired") then
+ return "has an expired certificate chain";
+ elseif chain_errors:contains("no matching DANE TLSA records") then
+ return "does not match any DANE TLSA records";
end
end
+ -- TODO cert_chain_errors can be a string, handle that
return "is not trusted"; -- for some other reason
elseif session.cert_identity_status == "invalid" then
return "is not valid for this name";
@@ -966,6 +1033,8 @@ function check_auth_policy(event)
-- In practice most cases are configuration mistakes or forgotten
-- certificate renewals. We think it's better to let the other party
-- know about the problem so that they can fix it.
+ --
+ -- Note: Bounce message must not include name of server, as it may leak half your JID in semi-anon MUCs.
session:close({ condition = "not-authorized", text = "Your server's certificate "..reason },
nil, "Remote server's certificate "..reason);
return false;
@@ -976,7 +1045,7 @@ module:hook("s2s-check-certificate", check_auth_policy, -1);
module:hook("server-stopping", function(event)
-- Close ports
- local pm = require "core.portmanager";
+ local pm = require "prosody.core.portmanager";
for _, netservice in pairs(module.items["net-provider"]) do
pm.unregister_service(netservice.name, netservice);
end