diff options
Diffstat (limited to 'plugins/mod_s2s.lua')
-rw-r--r-- | plugins/mod_s2s.lua | 165 |
1 files changed, 117 insertions, 48 deletions
diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index ee65ba70..04fd5bc3 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() @@ -146,17 +158,17 @@ local function bounce_sendq(session, reason) 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 |