From ce1b6bee2ce62fe31b87e4aff9afcb616a1f6a89 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 27 Jan 2025 14:29:29 +0100 Subject: [PATCH 01/15] Improve xmpp_socket send_xml spec --- src/listeners/mongoose_xmpp_socket.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/listeners/mongoose_xmpp_socket.erl b/src/listeners/mongoose_xmpp_socket.erl index a8f5553236..1e7d69f109 100644 --- a/src/listeners/mongoose_xmpp_socket.erl +++ b/src/listeners/mongoose_xmpp_socket.erl @@ -28,7 +28,7 @@ -callback activate(state()) -> ok. -callback close(state()) -> ok. -callback send_xml(state(), iodata() | exml_stream:element() | [exml_stream:element()]) -> - ok | {error, term()}. + ok | {error, atom()}. -callback get_peer_certificate(state()) -> peercert_return(). -callback has_peer_cert(state(), mongoose_listener:options()) -> boolean(). -callback is_channel_binding_supported(state()) -> boolean(). @@ -148,7 +148,7 @@ close(#ranch_ssl{socket = Socket}) -> close(#xmpp_socket{module = Module, state = State}) -> Module:close(State). --spec send_xml(socket(), exml_stream:element() | [exml_stream:element()]) -> ok | {error, term()}. +-spec send_xml(socket(), exml_stream:element() | [exml_stream:element()]) -> ok | {error, atom()}. send_xml(#ranch_tcp{socket = Socket, connection_type = Type}, XML) -> Data = exml:to_iolist(XML), mongoose_instrument:execute(tcp_data_out, #{connection_type => Type}, #{byte_size => iolist_size(Data)}), From 4e65d1caa2a720d290173aff421e713825896f52 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 4 Feb 2025 20:14:41 +0100 Subject: [PATCH 02/15] Refine just_tls specs --- src/just_tls.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/just_tls.erl b/src/just_tls.erl index c43996ddfa..bec05f8cfe 100644 --- a/src/just_tls.erl +++ b/src/just_tls.erl @@ -57,12 +57,12 @@ tcp_to_tls(Socket, Opts, server) -> ssl:handshake(Socket, TlsOpts, 5000). %% @doc Prepare SSL options for direct use of ssl:connect/2 (client side) --spec make_client_opts(options()) -> [ssl:tls_option()]. +-spec make_client_opts(options()) -> [ssl:tls_client_option()]. make_client_opts(Opts) -> format_opts(Opts, client). %% @doc Prepare SSL options for direct use of ssl:handshake/2 (server side) --spec make_server_opts(options()) -> [ssl:tls_option()]. +-spec make_server_opts(options()) -> [ssl:tls_server_option()]. make_server_opts(Opts) -> format_opts(Opts, server). From a12cc76d6a9a422ddac4b3b4c0d682dcac8927ac Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 1 Feb 2025 13:54:51 +0100 Subject: [PATCH 03/15] Rename socket:new to socket:accept --- src/c2s/mongoose_c2s.erl | 2 +- src/component/mongoose_component_connection.erl | 2 +- src/listeners/mongoose_xmpp_socket.erl | 13 ++++++++----- src/s2s/mongoose_s2s_in.erl | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/c2s/mongoose_c2s.erl b/src/c2s/mongoose_c2s.erl index d08f6664c6..fbffb2ba2b 100644 --- a/src/c2s/mongoose_c2s.erl +++ b/src/c2s/mongoose_c2s.erl @@ -81,7 +81,7 @@ init({Transport, Ref, LOpts}) -> -spec handle_event(gen_statem:event_type(), term(), state(), data()) -> fsm_res(). handle_event(internal, {connect, {Transport, Ref, LOpts}}, connect, _) -> #{shaper := ShaperName, max_stanza_size := MaxStanzaSize} = LOpts, - C2SSocket = mongoose_xmpp_socket:new(Transport, c2s, Ref, LOpts), + C2SSocket = mongoose_xmpp_socket:accept(Transport, c2s, Ref, LOpts), verify_ip_is_not_blacklisted(C2SSocket), {ok, Parser} = exml_stream:new_parser([{max_element_size, MaxStanzaSize}]), Shaper = mongoose_shaper:new(ShaperName), diff --git a/src/component/mongoose_component_connection.erl b/src/component/mongoose_component_connection.erl index e9970600e6..be7aca9284 100644 --- a/src/component/mongoose_component_connection.erl +++ b/src/component/mongoose_component_connection.erl @@ -75,7 +75,7 @@ handle_event(internal, {connect, {Transport, Ref, LOpts}}, connect, _) -> #{shaper := ShaperName, max_stanza_size := MaxStanzaSize} = LOpts, {ok, Parser} = exml_stream:new_parser([{max_element_size, MaxStanzaSize}]), Shaper = mongoose_shaper:new(ShaperName), - Socket = mongoose_xmpp_socket:new(Transport, component, Ref, LOpts), + Socket = mongoose_xmpp_socket:accept(Transport, component, Ref, LOpts), StateData = #component_data{socket = Socket, parser = Parser, shaper = Shaper, listener_opts = LOpts}, {next_state, wait_for_stream, StateData, state_timeout(LOpts)}; handle_event(internal, #xmlstreamstart{attrs = Attrs}, wait_for_stream, StateData) -> diff --git a/src/listeners/mongoose_xmpp_socket.erl b/src/listeners/mongoose_xmpp_socket.erl index 1e7d69f109..36db6f293d 100644 --- a/src/listeners/mongoose_xmpp_socket.erl +++ b/src/listeners/mongoose_xmpp_socket.erl @@ -2,7 +2,7 @@ -include_lib("public_key/include/public_key.hrl"). --export([new/4, +-export([accept/4, handle_data/2, activate/1, close/1, @@ -72,22 +72,25 @@ -type peercert_return() :: no_peer_cert | {bad_cert, term()} | {ok, #'Certificate'{}}. -export_type([socket/0, state/0, conn_type/0, peercert_return/0]). --spec new(mongoose_listener:transport_module(), mongoose_listener:connection_type(), ranch:ref(), mongoose_listener:options()) -> socket(). -new(ranch_tcp, Type, Ref, LOpts) -> +-spec accept(mongoose_listener:transport_module(), + mongoose_listener:connection_type(), + ranch:ref(), + mongoose_listener:options()) -> socket(). +accept(ranch_tcp, Type, Ref, LOpts) -> {ok, Socket, ConnDetails} = mongoose_listener:read_connection_details(Ref, ranch_tcp, LOpts), #{src_address := PeerIp, src_port := PeerPort} = ConnDetails, SocketState = #ranch_tcp{socket = Socket, connection_type = Type, ranch_ref = Ref, ip = {PeerIp, PeerPort}}, activate(SocketState), SocketState; -new(ranch_ssl, Type, Ref, LOpts) -> +accept(ranch_ssl, Type, Ref, LOpts) -> {ok, Socket, ConnDetails} = mongoose_listener:read_connection_details(Ref, ranch_ssl, LOpts), #{src_address := PeerIp, src_port := PeerPort} = ConnDetails, SocketState = #ranch_ssl{socket = Socket, connection_type = Type, ranch_ref = Ref, ip = {PeerIp, PeerPort}}, activate(SocketState), SocketState; -new(Module, Type, Ref, LOpts) -> +accept(Module, Type, Ref, LOpts) -> {State, NewType} = Module:new(Ref, Type, LOpts), PeerIp = Module:peername(State), SocketState = #xmpp_socket{module = Module, state = State, diff --git a/src/s2s/mongoose_s2s_in.erl b/src/s2s/mongoose_s2s_in.erl index fe228eae20..ad38c19dff 100644 --- a/src/s2s/mongoose_s2s_in.erl +++ b/src/s2s/mongoose_s2s_in.erl @@ -84,7 +84,7 @@ handle_event(internal, {connect, {Transport, Ref, LOpts}}, connect, _) when is_a #{shaper := ShaperName, max_stanza_size := MaxStanzaSize} = LOpts, {ok, Parser} = exml_stream:new_parser([{max_element_size, MaxStanzaSize}]), Shaper = mongoose_shaper:new(ShaperName), - Socket = mongoose_xmpp_socket:new(Transport, s2s, Ref, LOpts), + Socket = mongoose_xmpp_socket:accept(Transport, s2s, Ref, LOpts), ?LOG_DEBUG(#{what => s2s_in_started, text => "New incoming S2S connection", socket => Socket}), Data = #s2s_data{socket = Socket, parser = Parser, shaper = Shaper, listener_opts = LOpts}, {next_state, {wait_for_stream, stream_start}, Data, state_timeout(LOpts)}; From 6b69370cdc1783966b1576e783fe849c6f0cb27f Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 1 Feb 2025 13:51:12 +0100 Subject: [PATCH 04/15] Relax xmpp_socket:tcp_to_tls specs --- src/listeners/mongoose_xmpp_socket.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/listeners/mongoose_xmpp_socket.erl b/src/listeners/mongoose_xmpp_socket.erl index 36db6f293d..2e791b3224 100644 --- a/src/listeners/mongoose_xmpp_socket.erl +++ b/src/listeners/mongoose_xmpp_socket.erl @@ -70,6 +70,7 @@ -type state() :: term(). -type conn_type() :: tcp | tls. -type peercert_return() :: no_peer_cert | {bad_cert, term()} | {ok, #'Certificate'{}}. +-type with_tls_opts() :: #{tls := just_tls:options(), _ => _}. -export_type([socket/0, state/0, conn_type/0, peercert_return/0]). -spec accept(mongoose_listener:transport_module(), @@ -106,7 +107,7 @@ activate(#ranch_ssl{socket = Socket}) -> activate(#xmpp_socket{module = Module, state = State}) -> Module:activate(State). --spec tcp_to_tls(socket(), mongoose_listener:options()) -> {ok, socket()} | {error, term()}. +-spec tcp_to_tls(socket(), with_tls_opts()) -> {ok, socket()} | {error, term()}. tcp_to_tls(#ranch_tcp{socket = TcpSocket, connection_type = Type, ranch_ref = Ref, ip = Ip}, #{tls := TlsConfig}) -> inet:setopts(TcpSocket, [{active, false}]), From 90d7a04e916bfe6938afb176557cf23ec9c23743 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 4 Feb 2025 09:10:27 +0100 Subject: [PATCH 05/15] Improve typing information for ejabberd_s2s --- src/s2s/ejabberd_s2s.erl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/s2s/ejabberd_s2s.erl b/src/s2s/ejabberd_s2s.erl index 9c80372b42..5bd9e35c20 100644 --- a/src/s2s/ejabberd_s2s.erl +++ b/src/s2s/ejabberd_s2s.erl @@ -48,7 +48,6 @@ -ignore_xref([start_link/0]). -include("mongoose.hrl"). --include("jlib.hrl"). %% Pair of hosts {FromServer, ToServer}. %% FromServer is the local server. @@ -56,14 +55,14 @@ %% Used in a lot of API and backend functions. -type fromto() :: {jid:lserver(), jid:lserver()}. -%% Pids for ejabberd_s2s_out servers +%% Pids for mongoose_s2s_out servers -type s2s_pids() :: [pid()]. -record(state, {}). --type base16_secret() :: binary(). -type stream_id() :: binary(). --type s2s_dialback_key() :: binary(). +-type base16_secret() :: <<_:16, _:_*16>>. %% Hex encoded +-type s2s_dialback_key() :: <<_:16, _:_*16>>. %% Hex encoded -export_type([fromto/0, s2s_pids/0, base16_secret/0, stream_id/0, s2s_dialback_key/0]). @@ -74,9 +73,13 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +-spec filter(jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()) -> + drop | xmpp_router:filter(). filter(From, To, Acc, Packet) -> {From, To, Acc, Packet}. +-spec route(jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()) -> + {done, mongoose_acc:t()}. % this is the 'last resort' router, it always returns 'done'. route(From, To, Acc, Packet) -> do_route(From, To, Acc, Packet). From 256f34bb5eea646816685f06e81c0f934e020275 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 1 Feb 2025 13:53:05 +0100 Subject: [PATCH 06/15] Extend s2s outgoing config --- src/config/mongoose_config_spec.erl | 32 ++++++++++++++++++++++------ test/common/config_parser_helper.erl | 24 ++++++++++++++++++--- test/config_parser_SUITE.erl | 14 +++++++++--- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/config/mongoose_config_spec.erl b/src/config/mongoose_config_spec.erl index 6cb97669e9..e82b0e6659 100644 --- a/src/config/mongoose_config_spec.erl +++ b/src/config/mongoose_config_spec.erl @@ -821,15 +821,28 @@ s2s() -> format_items = map}, <<"shared">> => #option{type = binary, validate = non_empty}, + <<"shaper">> => #option{type = atom, + validate = non_empty}, + <<"state_timeout">> => #option{type = int_or_infinity, + validate = non_negative}, + <<"stream_timeout">> => #option{type = int_or_infinity, + validate = non_negative}, <<"address">> => #list{items = s2s_address(), format_items = map}, <<"max_retry_delay">> => #option{type = integer, validate = positive}, + <<"max_stanza_size">> => #option{type = int_or_infinity, + validate = positive, + process = fun ?MODULE:process_infinity_as_zero/1}, <<"outgoing">> => s2s_outgoing(), <<"dns">> => s2s_dns(), <<"tls">> => tls([client, xmpp])}, defaults = #{<<"default_policy">> => allow, - <<"max_retry_delay">> => 300}, + <<"shaper">> => none, + <<"max_stanza_size">> => 0, + <<"max_retry_delay">> => 300, + <<"state_timeout">> => timer:seconds(5), + <<"stream_timeout">> => timer:minutes(10)}, wrap = host_config }. @@ -859,7 +872,7 @@ s2s_outgoing() -> }, include = always, defaults = #{<<"port">> => 5269, - <<"ip_versions">> => [4, 6], + <<"ip_versions">> => [4, 6], %% NOTE: we still prefer IPv4 first <<"connection_timeout">> => 10000} }. @@ -883,10 +896,12 @@ s2s_address() -> <<"ip_address">> => #option{type = string, validate = ip_address}, <<"port">> => #option{type = integer, - validate = port} + validate = port}, + <<"tls">> => #option{type = boolean} }, required = [<<"host">>, <<"ip_address">>], - process = fun ?MODULE:process_s2s_address/1 + process = fun ?MODULE:process_s2s_address/1, + defaults = #{<<"tls">> => false} }. %% Callbacks for 'process' @@ -1047,8 +1062,13 @@ process_acl_condition(Value) -> process_s2s_host_policy(#{host := S2SHost, policy := Policy}) -> {S2SHost, Policy}. -process_s2s_address(M) -> - maps:take(host, M). +process_s2s_address(#{ip_address := IPAddress} = M0) -> + {ok, IPTuple} = inet:parse_address(IPAddress), + M1 = M0#{ip_tuple => IPTuple, ip_version => ip_version(IPTuple)}, + maps:take(host, M1). + +ip_version(T) when tuple_size(T) =:= 4 -> inet; +ip_version(T) when tuple_size(T) =:= 8 -> inet6. process_infinity_as_zero(infinity) -> 0; process_infinity_as_zero(Num) -> Num. diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index b444ecc7f5..d638f738b3 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -744,26 +744,44 @@ default_auth() -> pgsql_s2s() -> Outgoing = (default_s2s_outgoing())#{port => 5299}, - (default_s2s())#{address => #{<<"fed1">> => #{ip_address => "127.0.0.1"}}, + (default_s2s())#{address => #{<<"fed1">> => #{ip_address => "127.0.0.1", + ip_tuple => {127, 0, 0, 1}, + ip_version => inet, + tls => false}}, outgoing => Outgoing}. custom_s2s() -> Tls0 = #{cacertfile => "priv/ca.pem", server_name_indication => default_sni()}, Tls = maps:merge(default_xmpp_tls(), Tls0), #{address => - #{<<"fed1">> => #{ip_address => "127.0.0.1"}, - <<"fed2">> => #{ip_address => "127.0.0.1", port => 8765}}, + #{<<"fed1">> => #{ip_address => "127.0.0.1", + ip_tuple => {127, 0, 0, 1}, + ip_version => inet, + tls => false}, + <<"fed2">> => #{ip_address => "127.0.0.1", + ip_tuple => {127, 0, 0, 1}, + ip_version => inet, + port => 8765, + tls => false}}, tls => Tls, default_policy => allow, dns => #{retries => 1, timeout => 30}, host_policy => #{<<"fed1">> => allow, <<"reg1">> => deny}, + shaper => none, + max_stanza_size => 0, max_retry_delay => 30, + state_timeout => timer:seconds(5), + stream_timeout => timer:minutes(10), outgoing => #{connection_timeout => 4000, ip_versions => [6, 4], port => 5299}, shared => <<"shared secret">>}. default_s2s() -> #{default_policy => allow, + shaper => none, + max_stanza_size => 0, max_retry_delay => 300, + state_timeout => timer:seconds(5), + stream_timeout => timer:minutes(10), outgoing => default_s2s_outgoing(), dns => #{retries => 2, timeout => 10} }. diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index ea2da1892a..0f77df1775 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -1349,10 +1349,18 @@ s2s_host_policy(_Config) -> s2s_address(_Config) -> Addr = #{<<"host">> => <<"host1">>, <<"ip_address">> => <<"192.168.1.2">>, - <<"port">> => 5321}, - ?cfgh([s2s, address], #{<<"host1">> => #{ip_address => "192.168.1.2", port => 5321}}, + <<"port">> => 5321, + <<"tls">> => false}, + ?cfgh([s2s, address], #{<<"host1">> => #{ip_address => "192.168.1.2", + ip_tuple => {192, 168, 1, 2}, + ip_version => inet, + port => 5321, + tls => false}}, #{<<"s2s">> => #{<<"address">> => [Addr]}}), - ?cfgh([s2s, address], #{<<"host1">> => #{ip_address => "192.168.1.2"}}, + ?cfgh([s2s, address], #{<<"host1">> => #{ip_address => "192.168.1.2", + ip_tuple => {192, 168, 1, 2}, + ip_version => inet, + tls => false}}, #{<<"s2s">> => #{<<"address">> => [maps:without([<<"port">>], Addr)]}}), ?errh(#{<<"s2s">> => #{<<"address">> => [maps:without([<<"host">>], Addr)]}}), ?errh(#{<<"s2s">> => #{<<"address">> => [maps:without([<<"ip_address">>], Addr)]}}), From e858109b2430ff487b58c93403acc92f28d65424 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 5 Feb 2025 11:25:40 +0100 Subject: [PATCH 07/15] Implement module with RFC6120 and XEP-0368 DNS discovery rules --- src/mongoose_addr_list.erl | 268 +++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 src/mongoose_addr_list.erl diff --git a/src/mongoose_addr_list.erl b/src/mongoose_addr_list.erl new file mode 100644 index 0000000000..78a01caad0 --- /dev/null +++ b/src/mongoose_addr_list.erl @@ -0,0 +1,268 @@ +-module(mongoose_addr_list). + +-feature(maybe_expr, enable). + +-compile({inline, [inet_to_dns_type/1, dns_to_inet_type/1]}). + +-xep([{xep, 368}, {version, "1.1.0"}]). + +-include("mongoose_logger.hrl"). + +-export([get_addr_list/3]). + +-type hostname() :: string(). +-type dns_ip_type() :: a | aaaa. +-type dns_ip_types() :: [a | aaaa, ...]. +-type with_tls() :: boolean(). +-type srv() :: {Prio :: integer(), + Weight :: integer(), + Port :: inet:port_number(), + DnsName :: hostname()}. +-type srv_tls() :: {Prio :: integer(), + Weight :: integer(), + Port :: inet:port_number(), + DnsName :: hostname(), + Tls :: with_tls()}. +-type pre_addr() :: #{ip_address := string(), + ip_tuple := inet:ip_address(), + ip_version := inet | inet6, + tls := with_tls(), + port => inet:port_number()}. +-type addr() :: #{ip_address => string(), + ip_tuple := inet:ip_address(), + ip_version := inet | inet6, + port := inet:port_number(), + tls := with_tls()}. +-export_type([addr/0]). + +-spec get_addr_list(HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + EnforcedTls :: with_tls()) -> [addr()]. +get_addr_list(HostType, LServer, EnforceTls) -> + maybe + [] ?= get_predefined_addresses(HostType, LServer, EnforceTls), + Domain = binary_to_list(LServer), + [] ?= lookup_services(HostType, Domain, EnforceTls), + lookup_addrs(HostType, Domain, EnforceTls) + else + [_|_] = Res -> + Res + end. + +%% @doc Get IPs predefined for a given s2s domain in the configuration +-spec get_predefined_addresses(HostType :: mongooseim:host_type(), + LServer :: jid:lserver(), + EnforceTls :: with_tls()) -> [addr()]. +get_predefined_addresses(HostType, LServer, EnforceTls) -> + case lookup_predefined_addresses(HostType, LServer) of + {ok, M} -> + ensure_tls_and_port(HostType, M, EnforceTls); + _ -> + [] + end. + +%% https://datatracker.ietf.org/doc/html/rfc6120#section-3.2.1 Preferred Process: SRV Lookup +%% The preferred process for FQDN resolution is to use [DNS-SRV] records +-spec lookup_services(HostType :: mongooseim:host_type(), + Domain :: hostname(), + EnforceTls :: with_tls()) -> [addr()]. +lookup_services(HostType, Domain, EnforceTls) -> + case mongoose_s2s_lib:domain_utf8_to_ascii(Domain) of + false -> []; + ASCIIAddr -> do_lookup_services(HostType, ASCIIAddr, EnforceTls) + end. + +%% https://datatracker.ietf.org/doc/html/rfc6120#section-3.2.2 Fallback Process +%% The fallback process SHOULD be a normal "A" or "AAAA" address record resolution +%% to determine the IPv4 or IPv6 address of the origin domain. +-spec lookup_addrs(HostType :: mongooseim:host_type(), + Domain :: hostname(), + EnforceTls :: with_tls()) -> [addr()]. +lookup_addrs(HostType, Domain, EnforceTls) -> + Port = outgoing_s2s_port(HostType), + Types = outgoing_s2s_types(HostType), + Fun = fun(Type) -> + MaybeHostEnt = dns_lookup(HostType, Domain, Type), + prepare_addr(MaybeHostEnt, Port, EnforceTls, Domain, Type) + end, + Expanded = lists:map(Fun, Types), + lists:flatten(Expanded). + +-spec ensure_tls_and_port(mongooseim:host_type(), pre_addr(), with_tls()) -> [addr()]. +ensure_tls_and_port(_, #{tls := false}, true) -> + []; +ensure_tls_and_port(_, #{port := _} = M, _) -> + [M]; +ensure_tls_and_port(HostType, #{} = M, _) -> + Port = outgoing_s2s_port(HostType), + [M#{port => Port}]. + +-spec do_lookup_services(HostType :: mongooseim:host_type(), + Domain :: hostname(), + EnforceTls :: with_tls()) -> [addr()]. +do_lookup_services(HostType, Domain, EnforceTls) -> + case srv_lookups(HostType, Domain, EnforceTls) of + {error, Reason} -> + ?LOG_ERROR(#{what => s2s_srv_lookup_failed, + text => <<"The DNS servers failed to lookup a SRV record." + " You should check your DNS configuration.">>, + nameserver => inet_db:res_option(nameserver), + service => build_service_name(EnforceTls, Domain), + dns_rr_type => srv, reason => Reason}), + []; + TlsTaggedSrvAddrLists -> + Addrs = order_and_prepare_addrs(HostType, TlsTaggedSrvAddrLists), + ?LOG_DEBUG(#{what => s2s_srv_lookup_success, addresses => Addrs, server => Domain}), + Addrs + end. + +%% https://xmpp.org/extensions/xep-0368.html SRV records for XMPP over TLS +%% This XEP extends that to include new xmpps-client/xmpps-server SRV records pointing +%% to direct TLS ports and combine priorities and weights as if they were a single SRV +%% record similar to RFC-6186. +%% Both 'xmpp-' and 'xmpps-' records SHOULD be treated as the same record with regard to +%% connection order as specified by RFC-2782, in that all priorities and weights are mixed. +%% This enables the server operator to decide if they would rather clients connect +%% with STARTTLS or direct TLS. However, clients MAY choose to prefer one type of +%% connection over the other. +-spec order_and_prepare_addrs(mongooseim:host_type(), [srv_tls()]) -> [addr()]. +order_and_prepare_addrs(HostType, TlsTaggedSrvAddrLists) -> + OrderedByPriority = order_by_priority_and_weight(TlsTaggedSrvAddrLists), + WithIpAddresses = for_each_tagged_srv_get_ip_addresses(HostType, OrderedByPriority), + lists:flatten(WithIpAddresses). + +%% Probabilities are not exactly proportional to weights +%% for simplicity (higher weights are overvalued) +%% https://datatracker.ietf.org/doc/html/rfc2782 +%% Priority: +%% A client MUST attempt to contact the target host with +%% the lowest-numbered priority it can reach; +%% Weight: +%% The weight field specifies a relative weight for entries with the same priority. +%% Larger weights SHOULD be given a proportionately higher probability of being selected. +-spec order_by_priority_and_weight([srv_tls()]) -> [srv_tls()]. +order_by_priority_and_weight(TlsTaggedSrvAddrLists) -> + Fun = fun({P1, W1, _, _, _}, {P2, W2, _, _, _}) -> P1 =< P2 andalso W2 =< W1 end, + lists:sort(Fun, TlsTaggedSrvAddrLists). + +-spec for_each_tagged_srv_get_ip_addresses(mongooseim:host_type(), [srv_tls()]) -> [[[addr()]]]. +for_each_tagged_srv_get_ip_addresses(HostType, TlsTaggedSrvAddrLists) -> + MapFun = fun({_, _, Port, Host, Tls}) -> + build_with_ip_address_list_for_host(HostType, Host, Port, Tls) + end, + lists:map(MapFun, TlsTaggedSrvAddrLists). + +-spec build_with_ip_address_list_for_host( + mongooseim:host_type(), hostname(), inet:port_number(), with_tls()) -> + [[addr()]]. +build_with_ip_address_list_for_host(HostType, Host, Port, Tls) -> + Types = outgoing_s2s_types(HostType), + FoldFun = fun(Type) -> build_with_typed_dns_lookup(HostType, Host, Port, Tls, Type) end, + lists:map(FoldFun, Types). + +-spec build_with_typed_dns_lookup( + mongooseim:host_type(), hostname(), inet:port_number(), with_tls(), dns_ip_type()) -> + [addr()]. +build_with_typed_dns_lookup(HostType, Host, Port, Tls, Type) -> + MaybeHostEnt = dns_lookup(HostType, Host, Type), + prepare_addr(MaybeHostEnt, Port, Tls, Host, Type). + +%% Config lookup functions +-spec outgoing_s2s_types(mongooseim:host_type()) -> dns_ip_types(). +outgoing_s2s_types(HostType) -> + [inet_to_dns_type(V) || V <- ip_versions(HostType)]. + +-spec inet_to_dns_type(4 | 6) -> dns_ip_type(). +inet_to_dns_type(4) -> a; +inet_to_dns_type(6) -> aaaa. + +-spec dns_to_inet_type(a | aaaa) -> inet | inet6. +dns_to_inet_type(a) -> inet; +dns_to_inet_type(aaaa) -> inet6. + +-spec ip_versions(mongooseim:host_type()) -> [4 | 6]. +ip_versions(HostType) -> + mongoose_config:get_opt([{s2s, HostType}, outgoing, ip_versions]). + +-spec lookup_predefined_addresses(mongooseim:host_type(), jid:lserver()) -> + {ok, pre_addr()} | {error, atom()}. +lookup_predefined_addresses(HostType, LServer) -> + mongoose_config:lookup_opt([{s2s, HostType}, address, LServer]). + +-spec outgoing_s2s_port(mongooseim:host_type()) -> inet:port_number(). +outgoing_s2s_port(HostType) -> + mongoose_config:get_opt([{s2s, HostType}, outgoing, port]). + +-spec get_dns(mongooseim:host_type()) -> + #{timeout := non_neg_integer(), retries := pos_integer()}. +get_dns(HostType) -> + mongoose_config:get_opt([{s2s, HostType}, dns]). + +-spec prepare_addr + ([inet:ip4_address()], inet:port_number(), with_tls(), hostname(), a) -> [addr()]; + ([inet:ip6_address()], inet:port_number(), with_tls(), hostname(), aaaa) -> [addr()]. +prepare_addr([_|_] = Addrs, Port, Tls, _, Type) -> + MapFun = fun(Addr) -> #{ip_tuple => Addr, ip_version => dns_to_inet_type(Type), + port => Port, tls => Tls} end, + lists:map(MapFun, Addrs); +prepare_addr([], _, _, Domain, Type) -> + ?LOG_ERROR(#{what => s2s_dns_lookup_failed, + text => <<"The DNS servers failed to lookup an A|AAAA record." + " You should check your DNS configuration.">>, + nameserver => inet_db:res_option(nameserver), + server => Domain, dns_rr_type => Type}), + []. + +-spec srv_lookups(HostType :: mongooseim:host_type(), + Domain :: hostname(), + EnforceTls :: with_tls()) -> [srv_tls()] | {error, atom()}. +srv_lookups(HostType, Domain, true) -> + case dns_lookup(HostType, build_service_name(true, Domain), srv) of + [_|_] = TlsAddrList -> + tag_tls(TlsAddrList, true); + [] -> + {error, timeout} + end; +srv_lookups(HostType, Domain, _) -> + TlsHostEnt = dns_lookup(HostType, build_service_name(true, Domain), srv), + HostEnt = dns_lookup(HostType, build_service_name(false, Domain), srv), + case {TlsHostEnt, HostEnt} of + {[_|_] = TlsAddrList, [_|_] = AddrList} -> + tag_tls(TlsAddrList, true) ++ tag_tls(AddrList, false); + {[_|_] = TlsAddrList, []} -> + tag_tls(TlsAddrList, true); + {[], [_|_] = AddrList} -> + tag_tls(AddrList, false); + {[], []} -> + {error, timeout} + end. + +-spec build_service_name(with_tls(), hostname()) -> hostname(). +build_service_name(true, Domain) -> + "_xmpps-server._tcp." ++ Domain; +build_service_name(false, Domain) -> + "_xmpp-server._tcp." ++ Domain. + +tag_tls(AddrList, Tls) -> + lists:map(fun({Prio, Weight, Port, Host}) -> {Prio, Weight, Port, Host, Tls} end, AddrList). + +-spec dns_lookup(mongooseim:host_type(), hostname(), a) -> [inet:ip4_address()]; + (mongooseim:host_type(), hostname(), aaaa) -> [inet:ip6_address()]; + (mongooseim:host_type(), hostname(), srv) -> [srv()]. +dns_lookup(HostType, Domain, DnsRrType) -> + #{timeout := TimeoutSec, retries := Retries} = get_dns(HostType), + Timeout = timer:seconds(TimeoutSec), + dns_lookup(Domain, DnsRrType, Timeout, Retries). + +-spec dns_lookup(hostname(), a, timeout(), non_neg_integer()) -> [inet:ip4_address()]; + (hostname(), aaaa, timeout(), non_neg_integer()) -> [inet:ip6_address()]; + (hostname(), srv, timeout(), non_neg_integer()) -> [srv()]. +dns_lookup(_Domain, _DnsRrType, _, 0) -> + []; +dns_lookup(Domain, DnsRrType, Timeout, Retries) -> + case inet_res:lookup(Domain, in, DnsRrType, [], Timeout) of + [_|_] = List -> + List; + _ -> + dns_lookup(Domain, DnsRrType, Timeout, Retries - 1) + end. From 08dc9b65888b1cf2a6603277f8c8aa6aa1cde9f7 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 4 Feb 2025 20:04:00 +0100 Subject: [PATCH 08/15] Introduce xmpp_socket:tcp_to_tls for the client side --- src/c2s/mongoose_c2s.erl | 2 +- src/just_tls.erl | 2 +- src/listeners/mongoose_xmpp_socket.erl | 33 +++++++++++++++++++------- src/mod_bosh_socket.erl | 6 ++--- src/mod_websockets.erl | 6 ++--- src/s2s/mongoose_s2s_in.erl | 2 +- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/c2s/mongoose_c2s.erl b/src/c2s/mongoose_c2s.erl index fbffb2ba2b..fd8486ce37 100644 --- a/src/c2s/mongoose_c2s.erl +++ b/src/c2s/mongoose_c2s.erl @@ -400,7 +400,7 @@ handle_starttls(StateData = #c2s_data{socket = TcpSocket, parser = Parser, listener_opts = LOpts = #{tls := _}}, El, SaslAcc, Retries) -> send_xml(StateData, mongoose_c2s_stanzas:tls_proceed()), %% send last negotiation chunk via tcp - case mongoose_xmpp_socket:tcp_to_tls(TcpSocket, LOpts) of + case mongoose_xmpp_socket:tcp_to_tls(TcpSocket, LOpts, server) of {ok, TlsSocket} -> {ok, NewParser} = exml_stream:reset_parser(Parser), NewStateData = StateData#c2s_data{socket = TlsSocket, diff --git a/src/just_tls.erl b/src/just_tls.erl index bec05f8cfe..7224e74ac8 100644 --- a/src/just_tls.erl +++ b/src/just_tls.erl @@ -45,7 +45,7 @@ %% APIs %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec tcp_to_tls(inet:socket(), options(), client | server) -> +-spec tcp_to_tls(inet:socket(), options(), mongoose_xmpp_socket:side()) -> {ok, ssl:sslsocket()} | {error, any()}. tcp_to_tls(Socket, Opts, client) -> TlsOpts = format_opts(Opts, client), diff --git a/src/listeners/mongoose_xmpp_socket.erl b/src/listeners/mongoose_xmpp_socket.erl index 2e791b3224..7330c6a825 100644 --- a/src/listeners/mongoose_xmpp_socket.erl +++ b/src/listeners/mongoose_xmpp_socket.erl @@ -10,7 +10,7 @@ export_key_materials/5, get_peer_certificate/1, has_peer_cert/2, - tcp_to_tls/2, + tcp_to_tls/3, is_ssl/1, send_xml/2]). @@ -21,7 +21,7 @@ -callback new(ranch:ref(), mongoose_listener:connection_type(), mongoose_listener:options()) -> {state(), mongoose_listener:connection_type()}. -callback peername(state()) -> mongoose_transport:peer(). --callback tcp_to_tls(state(), mongoose_listener:options()) -> +-callback tcp_to_tls(state(), mongoose_listener:options(), side()) -> {ok, state()} | {error, term()}. -callback handle_data(state(), {tcp | ssl, term(), binary()}) -> binary() | {raw, [exml:element()]} | {error, term()}. @@ -68,10 +68,11 @@ -type socket() :: #ranch_tcp{} | #ranch_ssl{} | #xmpp_socket{}. -type state() :: term(). +-type side() :: client | server. -type conn_type() :: tcp | tls. -type peercert_return() :: no_peer_cert | {bad_cert, term()} | {ok, #'Certificate'{}}. -type with_tls_opts() :: #{tls := just_tls:options(), _ => _}. --export_type([socket/0, state/0, conn_type/0, peercert_return/0]). +-export_type([socket/0, state/0, side/0, conn_type/0, peercert_return/0]). -spec accept(mongoose_listener:transport_module(), mongoose_listener:connection_type(), @@ -107,25 +108,39 @@ activate(#ranch_ssl{socket = Socket}) -> activate(#xmpp_socket{module = Module, state = State}) -> Module:activate(State). --spec tcp_to_tls(socket(), with_tls_opts()) -> {ok, socket()} | {error, term()}. +-spec tcp_to_tls(socket(), with_tls_opts(), side()) -> {ok, socket()} | {error, term()}. tcp_to_tls(#ranch_tcp{socket = TcpSocket, connection_type = Type, ranch_ref = Ref, ip = Ip}, - #{tls := TlsConfig}) -> + #{tls := TlsConfig}, client) -> + inet:setopts(TcpSocket, [{active, false}]), + SslOpts = just_tls:make_client_opts(TlsConfig), + Ret = ssl:connect(TcpSocket, SslOpts, 5000), + VerifyResults = just_tls:receive_verify_results(), + case Ret of + {ok, SslSocket} -> + ssl:setopts(SslSocket, [{active, once}]), + {ok, #ranch_ssl{socket = SslSocket, connection_type = Type, + ranch_ref = Ref, ip = Ip, verify_results = VerifyResults}}; + {error, Reason} -> + {error, Reason} + end; +tcp_to_tls(#ranch_tcp{socket = TcpSocket, connection_type = Type, ranch_ref = Ref, ip = Ip}, + #{tls := TlsConfig}, server) -> inet:setopts(TcpSocket, [{active, false}]), SslOpts = just_tls:make_server_opts(TlsConfig), Ret = ssl:handshake(TcpSocket, SslOpts, 5000), VerifyResults = just_tls:receive_verify_results(), case Ret of {ok, SslSocket} -> - ranch_ssl:setopts(SslSocket, [{active, once}]), + ssl:setopts(SslSocket, [{active, once}]), {ok, #ranch_ssl{socket = SslSocket, connection_type = Type, ranch_ref = Ref, ip = Ip, verify_results = VerifyResults}}; {error, Reason} -> {error, Reason} end; -tcp_to_tls(#ranch_ssl{}, _) -> +tcp_to_tls(#ranch_ssl{}, _, _) -> {error, already_tls_connection}; -tcp_to_tls(#xmpp_socket{module = Module, state = State} = C2SSocket, LOpts) -> - case Module:tcp_to_tls(State, LOpts) of +tcp_to_tls(#xmpp_socket{module = Module, state = State} = C2SSocket, LOpts, Mode) -> + case Module:tcp_to_tls(State, LOpts, Mode) of {ok, NewState} -> {ok, C2SSocket#xmpp_socket{state = NewState}}; Error -> diff --git a/src/mod_bosh_socket.erl b/src/mod_bosh_socket.erl index 21ef7ffde5..491a74a4fc 100644 --- a/src/mod_bosh_socket.erl +++ b/src/mod_bosh_socket.erl @@ -22,7 +22,7 @@ %% mongoose_xmpp_socket callbacks -export([new/3, peername/1, - tcp_to_tls/2, + tcp_to_tls/3, handle_data/2, activate/1, send_xml/2, @@ -1060,9 +1060,9 @@ new(Socket, _, _LOpts) -> peername(#bosh_socket{peer = Peer}) -> Peer. --spec tcp_to_tls(mod_bosh:socket(), mongoose_listener:options()) -> +-spec tcp_to_tls(mod_bosh:socket(), mongoose_listener:options(), mongoose_xmpp_socket:side()) -> {ok, mod_bosh:socket()} | {error, term()}. -tcp_to_tls(_Socket, _LOpts) -> +tcp_to_tls(_Socket, _LOpts, _Mode) -> {error, tls_not_allowed_on_bosh}. -spec handle_data(mod_bosh:socket(), {tcp | ssl, term(), iodata()}) -> diff --git a/src/mod_websockets.erl b/src/mod_websockets.erl index 1d818763a1..b40c85be7e 100644 --- a/src/mod_websockets.erl +++ b/src/mod_websockets.erl @@ -23,7 +23,7 @@ %% mongoose_xmpp_socket callbacks -export([new/3, peername/1, - tcp_to_tls/2, + tcp_to_tls/3, handle_data/2, activate/1, close/1, @@ -376,9 +376,9 @@ new(Socket, _, _LOpts) -> peername(#websocket{peername = PeerName}) -> PeerName. --spec tcp_to_tls(socket(), mongoose_listener:options()) -> +-spec tcp_to_tls(socket(), mongoose_listener:options(), mongoose_xmpp_socket:side()) -> {ok, socket()} | {error, term()}. -tcp_to_tls(_Socket, _LOpts) -> +tcp_to_tls(_Socket, _LOpts, _Mode) -> {error, tls_not_allowed_on_websockets}. -spec handle_data(socket(), {tcp | ssl, term(), term()}) -> diff --git a/src/s2s/mongoose_s2s_in.erl b/src/s2s/mongoose_s2s_in.erl index ad38c19dff..2c35fd9966 100644 --- a/src/s2s/mongoose_s2s_in.erl +++ b/src/s2s/mongoose_s2s_in.erl @@ -208,7 +208,7 @@ handle_starttls(#s2s_data{socket = TcpSocket, listener_opts = LOpts = #{tls := _}} = Data, #xmlel{attrs = #{<<"xmlns">> := ?NS_TLS}} = El) -> send_xml(Data, tls_proceed()), %% send last negotiation chunk via tcp - case mongoose_xmpp_socket:tcp_to_tls(TcpSocket, LOpts) of + case mongoose_xmpp_socket:tcp_to_tls(TcpSocket, LOpts, server) of {ok, TlsSocket} -> {ok, NewParser} = exml_stream:reset_parser(Parser), NewData = Data#s2s_data{socket = TlsSocket, From eb2a51bd217f7006f23dd1f9be54c487dc3c1240 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 4 Feb 2025 20:10:20 +0100 Subject: [PATCH 09/15] Introduce xmpp_socket:connect/ API --- src/listeners/mongoose_xmpp_socket.erl | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/listeners/mongoose_xmpp_socket.erl b/src/listeners/mongoose_xmpp_socket.erl index 7330c6a825..99680af904 100644 --- a/src/listeners/mongoose_xmpp_socket.erl +++ b/src/listeners/mongoose_xmpp_socket.erl @@ -2,7 +2,12 @@ -include_lib("public_key/include/public_key.hrl"). +-define(DEF_SOCKET_OPTS, + binary, {active, false}, {packet, raw}, + {send_timeout, 15000}, {send_timeout_close, true}). + -export([accept/4, + connect/4, handle_data/2, activate/1, close/1, @@ -100,6 +105,40 @@ accept(Module, Type, Ref, LOpts) -> activate(SocketState), SocketState. +-spec connect(mongoose_addr_list:addr(), + with_tls_opts(), + mongoose_listener:connection_type(), + timeout()) -> socket() | {error, timeout | inet:posix() | any()}. +connect(#{ip_tuple := Addr, ip_version := Inet, port := Port, tls := false}, Opts, Type, Timeout) -> + SockOpts = socket_options(false, Inet, Opts), + case gen_tcp:connect(Addr, Port, SockOpts, Timeout) of + {ok, Socket} -> + SocketState = #ranch_tcp{socket = Socket, connection_type = Type, + ranch_ref = {self(), Type}, ip = {Addr, Port}}, + activate(SocketState), + SocketState; + {error, Reason} -> + {error, Reason} + end; +connect(#{ip_tuple := Addr, ip_version := Inet, port := Port, tls := true}, Opts, Type, Timeout) -> + SockOpts = socket_options(true, Inet, Opts), + case ssl:connect(Addr, Port, SockOpts, Timeout) of + {ok, Socket} -> + SocketState = #ranch_ssl{socket = Socket, connection_type = Type, + ranch_ref = {self(), Type}, ip = {Addr, Port}}, + activate(SocketState), + SocketState; + {error, Reason} -> + {error, Reason} + end. + +-spec socket_options(true, inet | inet6, with_tls_opts()) -> [ssl:tls_client_option()]; + (false, inet | inet6, with_tls_opts()) -> [gen_tcp:connect_option()]. +socket_options(true, Inet, #{tls := TlsOpts}) -> + [Inet, ?DEF_SOCKET_OPTS | just_tls:make_client_opts(TlsOpts)]; +socket_options(false, Inet, _) -> + [Inet, ?DEF_SOCKET_OPTS]. + -spec activate(socket()) -> ok | {error, term()}. activate(#ranch_tcp{socket = Socket}) -> ranch_tcp:setopts(Socket, [{active, once}]); From 81849b802bfb6f44b7d0faf7b5fd861758d457d8 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 4 Feb 2025 20:20:03 +0100 Subject: [PATCH 10/15] Reorganise mongoose_s2s_dialback --- src/s2s/mongoose_s2s_dialback.erl | 79 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/src/s2s/mongoose_s2s_dialback.erl b/src/s2s/mongoose_s2s_dialback.erl index f49db0488f..5269b290dc 100644 --- a/src/s2s/mongoose_s2s_dialback.erl +++ b/src/s2s/mongoose_s2s_dialback.erl @@ -36,22 +36,19 @@ %% (db:result should've been named db:key). -module(mongoose_s2s_dialback). --export([step_1/2, - step_2/3, - step_3/3, - step_4/2]). +-export([step_1/2, step_2/3, step_3/3, step_4/2]). --export([parse_key/1, - parse_validity/1]). +-export([parse_key/1, parse_validity/1]). -export([make_key/3]). -xep([{xep, 185}, {version, "1.0"}]). %% Dialback Key Generation and Validation -xep([{xep, 220}, {version, "1.1.1"}]). %% Server Dialback --include("mongoose.hrl"). -include("jlib.hrl"). +-compile({inline, [fromto_to_attrs/1, is_valid_to_type/1]}). + %% Initiating server sends dialback key %% https://xmpp.org/extensions/xep-0220.html#example-1 -spec step_1(ejabberd_s2s:fromto(), ejabberd_s2s:s2s_dialback_key()) -> exml:element(). @@ -83,13 +80,6 @@ step_4(FromTo, IsValid) -> #xmlel{name = <<"db:result">>, attrs = Attrs#{<<"type">> => is_valid_to_type(IsValid)}}. --spec fromto_to_attrs(ejabberd_s2s:fromto()) -> exml:attrs(). -fromto_to_attrs({LocalServer, RemoteServer}) -> - #{<<"from">> => LocalServer, <<"to">> => RemoteServer}. - -is_valid_to_type(true) -> <<"valid">>; -is_valid_to_type(false) -> <<"invalid">>. - -spec parse_key(exml:element()) -> false | {Step :: step_1 | step_2, FromTo :: ejabberd_s2s:fromto(), @@ -104,12 +94,6 @@ parse_key(El = #xmlel{name = <<"db:verify">>}) -> parse_key(_) -> false. -parse_key(Step, El) -> - FromTo = parse_from_to(El), - StreamID = exml_query:attr(El, <<"id">>, <<>>), - Key = exml_query:cdata(El), - {Step, FromTo, StreamID, Key}. - %% Parse dialback verification result. %% Verification result is stored in the `type' attribute and could be `valid' or `invalid'. -spec parse_validity(exml:element()) -> false @@ -117,33 +101,50 @@ parse_key(Step, El) -> FromTo :: ejabberd_s2s:fromto(), StreamID :: ejabberd_s2s:stream_id(), IsValid :: boolean()}. -parse_validity(El = #xmlel{name = <<"db:verify">>}) -> +parse_validity(#xmlel{name = <<"db:verify">>, attrs = Attrs}) -> %% Receiving Server is Informed by Authoritative Server that Key is Valid or Invalid (Step 3) - parse_validity(step_3, El); -parse_validity(El = #xmlel{name = <<"db:result">>}) -> + parse_validity(step_3, Attrs); +parse_validity(#xmlel{name = <<"db:result">>, attrs = Attrs}) -> %% Receiving Server Sends Valid or Invalid Verification Result to Initiating Server (Step 4) - parse_validity(step_4, El); + parse_validity(step_4, Attrs); parse_validity(_) -> false. -parse_validity(Step, El) -> - FromTo = parse_from_to(El), - StreamID = exml_query:attr(El, <<"id">>, <<>>), - IsValid = exml_query:attr(El, <<"type">>) =:= <<"valid">>, - {Step, FromTo, StreamID, IsValid}. - --spec parse_from_to(exml:element()) -> ejabberd_s2s:fromto(). -parse_from_to(El) -> - RemoteJid = jid:from_binary(exml_query:attr(El, <<"from">>, <<>>)), - LocalJid = jid:from_binary(exml_query:attr(El, <<"to">>, <<>>)), - #jid{luser = <<>>, lresource = <<>>, lserver = LRemoteServer} = RemoteJid, - #jid{luser = <<>>, lresource = <<>>, lserver = LLocalServer} = LocalJid, - %% We use fromto() as seen by ejabberd_s2s_out and ejabberd_s2s - {LLocalServer, LRemoteServer}. - -spec make_key(ejabberd_s2s:fromto(), ejabberd_s2s:stream_id(), ejabberd_s2s:base16_secret()) -> ejabberd_s2s:s2s_dialback_key(). make_key({From, To}, StreamID, Secret) -> SecretHashed = binary:encode_hex(crypto:hash(sha256, Secret), lowercase), HMac = crypto:mac(hmac, sha256, SecretHashed, [From, " ", To, " ", StreamID]), binary:encode_hex(HMac, lowercase). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec parse_key(T, exml:element()) -> + {T, ejabberd_s2s:fromto(), ejabberd_s2s:stream_id(), ejabberd_s2s:s2s_dialback_key()}. +parse_key(Step, #xmlel{attrs = Attrs} = El) -> + FromTo = parse_from_to(Attrs), + StreamID = maps:get(<<"id">>, Attrs, <<>>), + Key = exml_query:cdata(El), + {Step, FromTo, StreamID, Key}. + +-spec parse_validity(T, exml:attrs()) -> {T, ejabberd_s2s:fromto(), ejabberd_s2s:stream_id(), boolean()}. +parse_validity(Step, Attrs) -> + FromTo = parse_from_to(Attrs), + StreamID = maps:get(<<"id">>, Attrs, <<>>), + IsValid = maps:get(<<"type">>, Attrs, undefined) =:= <<"valid">>, + {Step, FromTo, StreamID, IsValid}. + +-spec parse_from_to(exml:attrs()) -> ejabberd_s2s:fromto(). +parse_from_to(#{<<"from">> := Remote, <<"to">> := Local}) -> + #jid{luser = <<>>, lresource = <<>>, lserver = LRemoteServer} = jid:from_binary(Remote), + #jid{luser = <<>>, lresource = <<>>, lserver = LLocalServer} = jid:from_binary(Local), + %% We use fromto() as seen by mongoose_s2s_out and ejabberd_s2s + {LLocalServer, LRemoteServer}. + +-spec fromto_to_attrs(ejabberd_s2s:fromto()) -> exml:attrs(). +fromto_to_attrs({LocalServer, RemoteServer}) -> + #{<<"from">> => LocalServer, <<"to">> => RemoteServer}. + +-spec is_valid_to_type(boolean()) -> binary(). +is_valid_to_type(true) -> <<"valid">>; +is_valid_to_type(false) -> <<"invalid">>. From 23644bd11515c7034de09f624a7ddd0cd816e79a Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 4 Feb 2025 20:17:54 +0100 Subject: [PATCH 11/15] Rework s2s out connections --- big_tests/tests/s2s_SUITE.erl | 225 ++++-- big_tests/tests/s2s_helper.erl | 40 +- rel/files/mongooseim.toml | 3 + rel/mim1.vars-toml.config | 1 + src/ejabberd_sup.erl | 2 +- src/just_tls.erl | 2 +- src/s2s/ejabberd_s2s.erl | 69 +- src/s2s/ejabberd_s2s_out.erl | 1158 --------------------------- src/s2s/mongoose_s2s_backend.erl | 2 +- src/s2s/mongoose_s2s_in.erl | 43 +- src/s2s/mongoose_s2s_info.erl | 12 +- src/s2s/mongoose_s2s_lib.erl | 16 +- src/s2s/mongoose_s2s_out.erl | 593 ++++++++++++++ src/s2s/mongoose_s2s_socket_out.erl | 386 --------- src/stats_api.erl | 2 +- 15 files changed, 829 insertions(+), 1725 deletions(-) delete mode 100644 src/s2s/ejabberd_s2s_out.erl create mode 100644 src/s2s/mongoose_s2s_out.erl delete mode 100644 src/s2s/mongoose_s2s_socket_out.erl diff --git a/big_tests/tests/s2s_SUITE.erl b/big_tests/tests/s2s_SUITE.erl index a6c7c1e4be..45d5af8092 100644 --- a/big_tests/tests/s2s_SUITE.erl +++ b/big_tests/tests/s2s_SUITE.erl @@ -11,7 +11,6 @@ -include_lib("exml/include/exml.hrl"). -include_lib("exml/include/exml_stream.hrl"). -include_lib("eunit/include/eunit.hrl"). --include_lib("kernel/include/inet.hrl"). %% Module aliases -import(distributed_helper, [mim/0, rpc_spec/1, rpc/4]). @@ -25,6 +24,7 @@ all() -> {group, both_plain}, {group, both_tls_optional}, %% default MongooseIM config {group, both_tls_required}, + {group, both_tls_enforced}, {group, node1_tls_optional_node2_tls_required}, {group, node1_tls_required_node2_tls_optional}, @@ -42,6 +42,7 @@ groups() -> [{both_plain, [], all_tests()}, {both_tls_optional, [], essentials()}, {both_tls_required, [], essentials()}, + {both_tls_enforced, [], essentials()}, {node1_tls_optional_node2_tls_required, [], essentials()}, {node1_tls_required_node2_tls_optional, [], essentials()}, @@ -61,8 +62,14 @@ essentials() -> [simple_message]. all_tests() -> - [connections_info, dns_discovery, dns_discovery_ip_fail, nonexistent_user, - unknown_domain, malformed_jid, dialback_with_wrong_key]. + [connections_info, + dns_srv_discovery, + dns_ip_discovery, + dns_discovery_fail, + nonexistent_user, + unknown_domain, + malformed_jid, + dialback_with_wrong_key]. negative() -> [timeout_waiting_for_message]. @@ -99,46 +106,70 @@ end_per_suite(Config) -> init_per_group(dialback, Config) -> %% Tell mnesia that mim and mim2 nodes are clustered distributed_helper:add_node_to_cluster(distributed_helper:mim2(), Config); +init_per_group(both_tls_enforced, Config) -> + meck_dns_srv_lookup("fed1", srv_ssl), + Config1 = s2s_helper:configure_s2s(both_tls_enforced, Config), + [{requires_tls, group_with_tls(both_tls_enforced)}, {group, both_tls_enforced} | Config1]; init_per_group(GroupName, Config) -> Config1 = s2s_helper:configure_s2s(GroupName, Config), [{requires_tls, group_with_tls(GroupName)}, {group, GroupName} | Config1]. +end_per_group(both_tls_enforced, _Config) -> + rpc(mim(), meck, unload, []); end_per_group(_GroupName, _Config) -> ok. -init_per_testcase(dns_discovery = CaseName, Config) -> - meck_inet_res("_xmpp-server._tcp.fed2"), - ok = rpc(mim(), meck, new, [inet, [no_link, unstick, passthrough]]), - ok = rpc(mim(), meck, expect, - [inet, getaddr, - fun ("fed2", inet) -> - {ok, {127, 0, 0, 1}}; - (Address, Family) -> - meck:passthrough([Address, Family]) - end]), +init_per_testcase(dns_srv_discovery = CaseName, Config) -> + meck_dns_srv_lookup("fed2", srv), Config1 = escalus_users:update_userspec(Config, alice2, server, <<"fed2">>), escalus:init_per_testcase(CaseName, Config1); -init_per_testcase(dns_discovery_ip_fail = CaseName, Config) -> - meck_inet_res("_xmpp-server._tcp.fed3"), - ok = rpc(mim(), meck, new, [inet, [no_link, unstick, passthrough]]), +init_per_testcase(dns_ip_discovery = CaseName, Config) -> + meck_dns_srv_lookup("fed2", ip), + Config1 = escalus_users:update_userspec(Config, alice2, server, <<"fed2">>), + escalus:init_per_testcase(CaseName, Config1); +init_per_testcase(dns_discovery_fail = CaseName, Config) -> + meck_dns_srv_lookup("fed3", none), escalus:init_per_testcase(CaseName, Config); init_per_testcase(CaseName, Config) -> escalus:init_per_testcase(CaseName, Config). -meck_inet_res(Domain) -> +meck_dns_srv_lookup(Domain, Which) -> + FedPort = ct:get_config({hosts, fed, incoming_s2s_port}), ok = rpc(mim(), meck, new, [inet_res, [no_link, unstick, passthrough]]), - ok = rpc(mim(), meck, expect, - [inet_res, getbyname, - fun (Domain1, srv, _Timeout) when Domain1 == Domain -> - {ok, {hostent, Domain, [], srv, 1, - [{30, 10, 5299, "localhost"}]}}; - (Name, Type, Timeout) -> - meck:passthrough([Name, Type, Timeout]) - end]). - -end_per_testcase(CaseName, Config) when CaseName =:= dns_discovery; - CaseName =:= dns_discovery_ip_fail -> + ok = rpc(mim(), meck, expect, [inet_res, lookup, inet_res_lookup_fun(Domain, FedPort, Which)]). + +inet_res_lookup_fun(Domain, FedPort, srv_ssl) -> + fun("_xmpps-server._tcp." ++ Domain1, in, srv, _Opts, _Timeout) when Domain1 =:= Domain -> + [{30, 0, FedPort, "localhost"}]; + (Name, Class, Type, Opts, Timeout) -> + meck:passthrough([Name, Class, Type, Opts, Timeout]) + end; +inet_res_lookup_fun(Domain, FedPort, srv) -> + fun("_xmpp-server._tcp." ++ Domain1, in, srv, _Opts, _Timeout) when Domain1 =:= Domain -> + [{30, 0, FedPort, "localhost"}]; + (Name, Class, Type, Opts, Timeout) -> + meck:passthrough([Name, Class, Type, Opts, Timeout]) + end; +inet_res_lookup_fun(Domain, _FedPort, ip) -> + fun(Domain1, in, a, _Opts, _Timeout) when Domain1 =:= Domain -> + [{127, 0, 0, 1}]; + (Name, Class, Type, Opts, Timeout) -> + meck:passthrough([Name, Class, Type, Opts, Timeout]) + end; +inet_res_lookup_fun(Domain, _FedPort, none) -> + fun("_xmpp-server._tcp." ++ Domain1, in, srv, _Opts, _Timeout) when Domain1 =:= Domain -> + {error, nxdomain}; + (Domain1, in, inet, _Opts, _Timeout) when Domain1 =:= Domain -> + {error, nxdomain}; + (Name, Class, Type, Opts, Timeout) -> + meck:passthrough([Name, Class, Type, Opts, Timeout]) + end. + +end_per_testcase(CaseName, Config) when CaseName =:= dns_srv_discovery; + CaseName =:= dns_ip_discovery; + CaseName =:= dns_discovery_fail -> rpc(mim(), meck, unload, []), + s2s_helper:reset_s2s_connections(), escalus:end_per_testcase(CaseName, Config); end_per_testcase(CaseName, Config) -> escalus:end_per_testcase(CaseName, Config). @@ -186,26 +217,30 @@ connections_info(Config) -> [_ | _] = get_s2s_connections(mim(), FedDomain, out), ok. -dns_discovery(Config) -> +dns_srv_discovery(Config) -> simple_message(Config), %% Ensure that the mocked DNS discovery for connecting to the other server History = rpc(mim(), meck, history, [inet_res]), - ?assertEqual(length(History), 2), - ?assertEqual(s2s_helper:has_xmpp_server(History, "fed2"), true), + ?assert(0 < length(History), History), + ?assert(s2s_helper:has_xmpp_server(History, "_xmpp-server._tcp.fed2", srv), History), ok. +dns_ip_discovery(Config) -> + simple_message(Config), + %% Ensure that the mocked DNS discovery for connecting to the other server + History = rpc(mim(), meck, history, [inet_res]), + ?assert(0 < length(History), History), + ?assert(s2s_helper:has_xmpp_server(History, "fed2", a), History), + ok. -dns_discovery_ip_fail(Config) -> - escalus:fresh_story(Config, [{alice, 1}], fun(Alice1) -> - - escalus:send(Alice1, escalus_stanza:chat_to( - <<"alice2@fed3">>, - <<"Hello, second Alice!">>)), +dns_discovery_fail(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice1) -> + escalus:send(Alice1, escalus_stanza:chat_to(<<"alice2@fed3">>, <<"Hello, second Alice!">>)), Stanza = escalus:wait_for_stanza(Alice1, 5000), escalus:assert(is_error, [<<"cancel">>, <<"remote-server-not-found">>], Stanza), - History = rpc(mim(), meck, history, [inet]), - ?assertEqual(s2s_helper:has_inet_errors(History, "fed3"), true) + History = rpc(mim(), meck, history, [inet_res]), + ?assert(s2s_helper:has_inet_errors(History, "fed3"), History) end). get_s2s_connections(RPCSpec, Domain, Type) -> @@ -214,9 +249,9 @@ get_s2s_connections(RPCSpec, Domain, Type) -> [Connection || Connection <- AllS2SConnections, Type =/= in orelse [Domain] =:= maps:get(domains, Connection), Type =/= out orelse Domain =:= maps:get(server, Connection)], - ct:pal("Node = ~p, ConnectionType = ~p, Domain = ~s~nDomainS2SConnections(~p): ~p", + ct:pal("Node = ~p, ConnectionType = ~p, Domain = ~s~nDomainS2SConnections(~p): ~p~nAll Connections: ~p", [maps:get(node, RPCSpec), Type, Domain, length(DomainS2SConnections), - DomainS2SConnections]), + DomainS2SConnections, AllS2SConnections]), DomainS2SConnections. nonexistent_user(Config) -> @@ -269,7 +304,7 @@ dialback_with_wrong_key(_Config) -> Key = <<"123456">>, %% wrong key StreamId = <<"sdfdsferrr">>, StartType = {verify, self(), Key, StreamId}, - {ok, _} = rpc(rpc_spec(mim), ejabberd_s2s_out, start, [FromTo, StartType]), + {ok, _} = rpc(rpc_spec(mim), mongoose_s2s_out, start_connection, [FromTo, StartType]), receive %% Remote server (fed1) rejected out request {'$gen_cast', {validity_from_s2s_out, false, FromTo}} -> @@ -494,59 +529,79 @@ shared_secret(mim) -> <<"f623e54a0741269be7dd">>; %% Some random key shared_secret(mim2) -> <<"9e438f25e81cf347100b">>. assert_events(TS, Config) -> - TLS = proplists:get_value(requires_tls, Config), - instrument_helper:assert(xmpp_element_size_in, #{connection_type => s2s}, fun(#{byte_size := S}) -> S > 0 end, + TLS = proplists:get_value(requires_tls, Config, false), + Labels = #{connection_type => s2s}, + Filter = fun(#{byte_size := S}) -> S > 0 end, + + instrument_helper:assert(xmpp_element_size_in, Labels, Filter, #{expected_count => element_count(in, TLS), min_timestamp => TS}), - instrument_helper:assert(xmpp_element_size_out, #{connection_type => s2s}, fun(#{byte_size := S}) -> S > 0 end, + instrument_helper:assert(xmpp_element_size_out, Labels, Filter, #{expected_count => element_count(out, TLS), min_timestamp => TS}), - instrument_helper:assert(tcp_data_in, #{connection_type => s2s}, fun(#{byte_size := S}) -> S > 0 end, - #{min_timestamp => TS}), - instrument_helper:assert(tcp_data_out, #{connection_type => s2s}, fun(#{byte_size := S}) -> S > 0 end, - #{min_timestamp => TS}). + Value = distributed_helper:rpc(distributed_helper:mim(), mongoose_instrument_event_table, all_keys, []), + ct:pal("Value ~p~n", [Value]), + case TLS of + true -> + instrument_helper:assert(tls_data_out, Labels, Filter, #{min_timestamp => TS}), + instrument_helper:assert(tls_data_in, Labels, Filter, #{min_timestamp => TS}); + false -> + instrument_helper:assert(tcp_data_in, Labels, Filter, #{min_timestamp => TS}), + instrument_helper:assert(tcp_data_out, Labels, Filter, #{min_timestamp => TS}) + end. +% Some of these steps happen asynchronously, so the order may be different. +% Since S2S connections are unidirectional, mim1 acts both as initiating, +% and receiving (and authoritative) server in the dialback procedure. +% +% We also test that both users on each side of federation can text each other, +% hence both server will run the dialback each. +% +% When a user in mim1 writes to a user in fed1, from the perspective of mim1: +% - Open an outgoing connection from mim1 to fed1: +% 1. Outgoing stream starts +% 2. Incoming stream starts +% 3. Incoming stream features +% 4. Outgoing dialback key (step 1) +% - Open an incoming connection from fed1 to mim1: +% 5. Incoming stream starts +% 6. Outgoing stream starts +% 7. Outgoing stream features +% 8. Incoming dialback verification request (step 2, as authoritative server) +% 9. Outgoing dialback verification response (step 3, as receiving server) +% - Original outgoing connection +% 10. Incoming dialback result (step 4, as initiating server) +% 11. Outgoing message from user in mim1 to user in fed1 +% +% Likewise, when a user in fed1 writes to a user in mim1, from the perspective of mim1: +% - Open an incoming connection from fed1 to mim1: +% 1. Incoming stream starts +% 2. Outgoing stream starts +% 3. Outgoing stream features +% 4. Incoming dialback key (step 1) +% - Open an outgoing connection from mim1 to fed1: +% 5. Outgoing stream starts +% 6. Incoming stream starts +% 7. Incoming stream features +% 8. Outgoing dialback verification request (step 2, as authoritative server) +% 9. Incoming dialback verification response (step 3, as receiving server) +% - Original incoming connection +% 10. Outgoing dialback result (step 4, as initiating server) +% 11. Incoming message from user in fed1 to user in mim1 +% +% The number can be seen as the sum of all arrows from the dialback diagram, since mim +% acts as all three roles in the two dialback procedures that occur: +% https://xmpp.org/extensions/xep-0220.html#intro-howitworks +% (6 arrows) + one for stream header response + one for the actual message element_count(_Dir, true) -> % TLS tests are not checking a specific number of events, because the numbers are flaky positive; element_count(in, false) -> - % Some of these steps happen asynchronously, so the order may be different. - % Since S2S connections are unidirectional, mim1 acts both as initiating, - % and receiving (and authoritative) server in the dialback procedure. - % 1. Stream start response from fed1 (as initiating server) - % 1.b. Stream features after the response - % 2. Stream start from fed1 (as receiving server) - % 3. Dialback key (step 1, as receiving server) - % 4. Dialback verification request (step 2, as authoritative server) - % 5. Dialback result (step 4, as initiating server) - % New s2s process is started to verify fed1 as an authoritative server - % This process sends a new stream header, as it opens a new connection to fed1, - % now acting as an authoritative server. Also see comment on L360 in ejabberd_s2s. - % 6. Stream start response from fed1 - % 6.b. Stream features after the response - % 7. Dialback verification response (step 3, as receiving server) - % 8. Message from federated Alice - % The number can be seen as the sum of all arrows from the dialback diagram, since mim - % acts as all three roles in the two dialback procedures that occur: - % https://xmpp.org/extensions/xep-0220.html#intro-howitworks - % (6 arrows) + one for stream header response + one for the actual message - 10; + 11; element_count(out, false) -> - % Since S2S connections are unidirectional, mim1 acts both as initiating, - % and receiving (and authoritative) server in the dialback procedure. - % 1. Dialback key (step 1, as initiating server) - % 2. Dialback verification response (step 3, as authoritative server) - % 3. Dialback request (step 2, as receiving server) - % 4. Message from Alice - % 5. Dialback result (step 4, as receiving server) - % The number calculation corresponds to in_element, however the stream headers are not - % sent as XML elements, but straight as text, and so these three events do not appear: - % - open stream to fed1, - % - stream response for fed1->mim stream, - % -.b. Stream features after the response - % - open stream to fed1 as authoritative server. - 9. + 11. group_with_tls(both_tls_optional) -> true; group_with_tls(both_tls_required) -> true; +group_with_tls(both_tls_enforced) -> true; group_with_tls(node1_tls_optional_node2_tls_required) -> true; group_with_tls(node1_tls_required_node2_tls_optional) -> true; group_with_tls(node1_tls_required_trusted_node2_tls_optional) -> true; @@ -556,5 +611,7 @@ group_with_tls(_GN) -> false. tested_events() -> [{xmpp_element_size_in, #{connection_type => s2s}}, {xmpp_element_size_out, #{connection_type => s2s}}, + {tls_data_in, #{connection_type => s2s}}, + {tls_data_out, #{connection_type => s2s}}, {tcp_data_in, #{connection_type => s2s}}, {tcp_data_out, #{connection_type => s2s}}]. diff --git a/big_tests/tests/s2s_helper.erl b/big_tests/tests/s2s_helper.erl index e76f6e7a00..61576f72a5 100644 --- a/big_tests/tests/s2s_helper.erl +++ b/big_tests/tests/s2s_helper.erl @@ -1,6 +1,7 @@ -module(s2s_helper). --export([init_s2s/1, end_s2s/1, configure_s2s/2, has_inet_errors/2, has_xmpp_server/2]). +-export([init_s2s/1, end_s2s/1, configure_s2s/2, has_inet_errors/2, has_xmpp_server/3, + reset_s2s_connections/0]). -import(distributed_helper, [rpc_spec/1, rpc/4]). @@ -23,21 +24,21 @@ configure_s2s(Group, Config) -> has_inet_errors(History, Server) -> Inet = lists:any( - fun({_, {inet, getaddr, [Server1, inet]}, {error, nxdomain}}) - when Server1 == Server -> true; + fun({_, {inet_res, lookup, [Server1, in, a, _, _]}, []}) + when Server1 =:= Server -> true; (_) -> false end, History), Inet6 = lists:any( - fun({_, {inet, getaddr, [Server1, inet6]}, {error, nxdomain}}) - when Server1 == Server -> true; + fun({_, {inet_res, lookup, [Server1, in, aaaa, _, _]}, []}) + when Server1 =:= Server -> true; (_) -> false end, History), Inet andalso Inet6. -has_xmpp_server(History, Server) -> - lists:all( - fun({_, _, {ok, {hostent, "_xmpp-server._tcp." ++ Server1, _, srv, _, _}}}) - when Server1 == Server -> true; +has_xmpp_server(History, Server, DnsRrType) -> + lists:any( + fun({_Pid, {inet_res, lookup, [Server1, in, DnsRrType1, _, _]}, [_|_]}) + when Server1 =:= Server, DnsRrType1 =:= DnsRrType -> true; (_) -> false end, History). @@ -60,6 +61,8 @@ tls_config(required_trusted, #{tls := TlsOpts} = Opts, _) -> Opts#{tls => TlsOpts#{mode => starttls_required, verify_mode => selfsigned_peer}}; tls_config(required, #{tls := TlsOpts} = Opts, _) -> Opts#{tls => TlsOpts#{mode => starttls_required, verify_mode => none}}; +tls_config(enforced, #{tls := TlsOpts} = Opts, _) -> + Opts#{tls => TlsOpts#{mode => tls, verify_mode => none}}; tls_config(optional, #{tls := TlsOpts} = Opts, _) -> Opts#{tls => TlsOpts#{mode => starttls, verify_mode => none}}; tls_config(plain, Opts, _) -> @@ -71,6 +74,8 @@ tls_preset(both_tls_optional) -> #{mim => optional, fed => optional}; tls_preset(both_tls_required) -> #{mim => required, fed => required}; +tls_preset(both_tls_enforced) -> + #{mim => enforced, fed => enforced}; tls_preset(node1_tls_optional_node2_tls_required) -> #{mim => optional, fed => required}; tls_preset(node1_tls_required_node2_tls_optional) -> @@ -93,8 +98,8 @@ set_opt(Spec, Opt, Value) -> rpc(Spec, mongoose_config, set_opt, [Opt, Value]). restart_s2s(#{} = Spec, S2SListener) -> - Children = rpc(Spec, supervisor, which_children, [ejabberd_s2s_out_sup]), - [rpc(Spec, ejabberd_s2s_out, stop_connection, [Pid]) || + Children = rpc(Spec, supervisor, which_children, [mongoose_s2s_out_sup]), + [rpc(Spec, mongoose_s2s_out, stop_connection, [Pid, <<"closing connection">>]) || {_, Pid, _, _} <- Children], Children0 = rpc(Spec, supervisor, which_children, [mongoose_listener_sup]), @@ -103,3 +108,16 @@ restart_s2s(#{} = Spec, S2SListener) -> [rpc(Spec, erlang, exit, [Pid, kill]) || {_, Pid, _, _} <- ChildrenIn], mongoose_helper:restart_listener(Spec, S2SListener). + +reset_s2s_connections() -> + [ reset_s2s_connections(rpc_spec(NodeKey)) || NodeKey <- node_keys()]. + +reset_s2s_connections(Spec) -> + Children = rpc(Spec, supervisor, which_children, [mongoose_s2s_out_sup]), + [rpc(Spec, mongoose_s2s_out, stop_connection, [Pid, <<"closing connection">>]) || + {_, Pid, _, _} <- Children], + + Children0 = rpc(Spec, supervisor, which_children, [mongoose_listener_sup]), + Listeners = [Ref || {Ref, _, _, [mongoose_s2s_listener | _]} <- Children], + ChildrenIn = lists:flatten([ranch:procs(Ref, connections) || Ref <- Listeners]), + [rpc(Spec, erlang, exit, [Pid, kill]) || {_, Pid, _, _} <- ChildrenIn]. diff --git a/rel/files/mongooseim.toml b/rel/files/mongooseim.toml index 21f055eb45..aaa5b929b9 100644 --- a/rel/files/mongooseim.toml +++ b/rel/files/mongooseim.toml @@ -367,6 +367,9 @@ ] [s2s] + {{#max_retry_delay}} + max_retry_delay = {{{max_retry_delay}}} + {{/max_retry_delay}} {{#s2s_default_policy}} default_policy = {{{s2s_default_policy}}} {{/s2s_default_policy}} diff --git a/rel/mim1.vars-toml.config b/rel/mim1.vars-toml.config index 93c59ec4bf..f0d5f4483d 100644 --- a/rel/mim1.vars-toml.config +++ b/rel/mim1.vars-toml.config @@ -6,6 +6,7 @@ {c2s_tls_port, 5223}. {outgoing_s2s_port, 5299}. {incoming_s2s_port, 5269}. +{max_retry_delay, 1}. {http_port, 5280}. {https_port, 5285}. {component_port, 8888}. diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index 4cba86f064..fd08dd627e 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -60,7 +60,7 @@ init([]) -> C2SSupervisor = template_supervisor_spec(mongoose_c2s_sup, mongoose_c2s), S2SOutSupervisor = - template_supervisor_spec(ejabberd_s2s_out_sup, ejabberd_s2s_out), + template_supervisor_spec(mongoose_s2s_out_sup, mongoose_s2s_out), IQSupervisor = template_supervisor_spec(ejabberd_iq_sup, mongoose_iq_worker), {ok, {{one_for_one, 10, 1}, diff --git a/src/just_tls.erl b/src/just_tls.erl index 7224e74ac8..b90f86b1b5 100644 --- a/src/just_tls.erl +++ b/src/just_tls.erl @@ -17,7 +17,7 @@ %% Other options should be supported if the implementing module supports it. -type options() :: #{module => module(), verify_mode := peer | selfsigned_peer | none, - mode => tls | starttls | starttls_required, % only ejabberd_s2s_out doesn't use it (yet) + mode => tls | starttls | starttls_required, % only mongoose_s2s_out doesn't use it (yet) certfile => string(), cacertfile => string(), ciphers => string(), diff --git a/src/s2s/ejabberd_s2s.erl b/src/s2s/ejabberd_s2s.erl index 5bd9e35c20..5c527bf806 100644 --- a/src/s2s/ejabberd_s2s.erl +++ b/src/s2s/ejabberd_s2s.erl @@ -83,15 +83,15 @@ filter(From, To, Acc, Packet) -> route(From, To, Acc, Packet) -> do_route(From, To, Acc, Packet). -%% Called by ejabberd_s2s_out process. +%% Called by mongoose_s2s_out process. -spec try_register(fromto()) -> IsRegistered :: boolean(). try_register(FromTo) -> Pid = self(), IsRegistered = call_try_register(Pid, FromTo), case IsRegistered of false -> - %% This usually happens when a ejabberd_s2s_out connection is established during dialback - %% procedure to check the key. + %% This usually happens when a mongoose_s2s_out connection is established + %% during dialback procedure to check the key. %% We still are fine, we just would not use that s2s connection to route %% any stanzas to the remote server. %% Could be a sign of abuse or a bug though, so use logging here. @@ -145,42 +145,36 @@ code_change(_OldVsn, State, _Extra) -> hooks() -> [{node_cleanup, global, fun ?MODULE:node_cleanup/3, #{}, 50}]. --spec do_route(From :: jid:jid(), - To :: jid:jid(), - Acc :: mongoose_acc:t(), - Packet :: exml:element()) -> - {done, mongoose_acc:t()}. % this is the 'last resort' router, it always returns 'done'. -do_route(From, To, Acc, Packet) -> - ?LOG_DEBUG(#{what => s2s_route, acc => Acc}), +-spec do_route(jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()) -> + {done, mongoose_acc:t()}. +do_route(From, To, Acc0, Packet) -> + ?LOG_DEBUG(#{what => s2s_route, acc => Acc0}), case find_connection(From, To) of {ok, Pid} when is_pid(Pid) -> ?LOG_DEBUG(#{what => s2s_found_connection, text => <<"Send packet to s2s connection">>, - s2s_pid => Pid, acc => Acc}), + s2s_pid => Pid, acc => Acc0}), NewPacket = jlib:replace_from_to(From, To, Packet), - Acc1 = mongoose_hooks:s2s_send_packet(Acc, From, To, Packet), - send_element(Pid, Acc1, NewPacket), - {done, Acc1}; + NewStanzaParams = #{element => NewPacket, from_jid => From, to_jid => To}, + Acc1 = mongoose_acc:update_stanza(NewStanzaParams, Acc0), + Acc2 = mongoose_hooks:s2s_send_packet(Acc1, From, To, NewPacket), + mongoose_s2s_out:route(Pid, Acc2), + {done, Acc2}; {error, _Reason} -> - case mongoose_acc:stanza_type(Acc) of + case mongoose_acc:stanza_type(Acc0) of <<"error">> -> - {done, Acc}; + {done, Acc0}; <<"result">> -> - {done, Acc}; + {done, Acc0}; _ -> - ?LOG_DEBUG(#{what => s2s_connection_not_found, acc => Acc}), + ?LOG_DEBUG(#{what => s2s_connection_not_found, acc => Acc0}), {Acc1, Err} = jlib:make_error_reply( - Acc, Packet, mongoose_xmpp_errors:service_unavailable()), + Acc0, Packet, mongoose_xmpp_errors:service_unavailable()), Acc2 = ejabberd_router:route(To, From, Acc1, Err), {done, Acc2} end end. --spec send_element(pid(), mongoose_acc:t(), exml:element()) -> ok. -send_element(Pid, Acc, El) -> - Pid ! {send_element, Acc, El}, - ok. - -spec find_connection(From :: jid:jid(), To :: jid:jid()) -> {ok, pid()} | {error, not_allowed}. find_connection(From, To) -> @@ -211,30 +205,9 @@ ensure_enough_connections(FromTo, OldCons) -> OldCons end. --spec open_new_connections(N :: pos_integer(), FromTo :: fromto()) -> ok. +-spec open_new_connections(N :: pos_integer(), FromTo :: fromto()) -> any(). open_new_connections(N, FromTo) -> - [open_new_connection(FromTo) || _N <- lists:seq(1, N)], - ok. - --spec open_new_connection(FromTo :: fromto()) -> ok. -open_new_connection(FromTo) -> - %% Start a process, but do not connect to the server yet. - {ok, Pid} = ejabberd_s2s_out:start(FromTo, new), - %% Try to write the Pid into Mnesia/CETS - IsRegistered = call_try_register(Pid, FromTo), - maybe_start_connection(Pid, FromTo, IsRegistered), - ok. - -%% If registration is successful, create an actual network connection. -%% If not successful, remove the process. --spec maybe_start_connection(Pid :: pid(), FromTo :: fromto(), IsRegistered :: boolean()) -> ok. -maybe_start_connection(Pid, FromTo, true) -> - ?LOG_INFO(#{what => s2s_new_connection, - text => <<"New s2s connection started">>, - from_to => FromTo, s2s_pid => Pid}), - ejabberd_s2s_out:start_connection(Pid); -maybe_start_connection(Pid, _FromTo, false) -> - ejabberd_s2s_out:stop_connection(Pid). + [mongoose_s2s_out:start_connection(FromTo, new) || _N <- lists:seq(1, N)]. -spec set_shared_secret() -> ok. set_shared_secret() -> @@ -258,7 +231,7 @@ internal_database_init() -> Backend = mongoose_config:get_opt(s2s_backend), mongoose_s2s_backend:init(#{backend => Backend}). -%% Get ejabberd_s2s_out pids +%% Get mongoose_s2s_out pids -spec get_s2s_out_pids(FromTo :: fromto()) -> s2s_pids(). get_s2s_out_pids(FromTo) -> mongoose_s2s_backend:get_s2s_out_pids(FromTo). diff --git a/src/s2s/ejabberd_s2s_out.erl b/src/s2s/ejabberd_s2s_out.erl deleted file mode 100644 index 13af92b507..0000000000 --- a/src/s2s/ejabberd_s2s_out.erl +++ /dev/null @@ -1,1158 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_s2s_out.erl -%%% Author : Alexey Shchepin -%%% Purpose : Manage outgoing server-to-server connections -%%% Created : 6 Dec 2002 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2011 ProcessOne -%%% -%%% This program 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; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% 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, write to the Free Software -%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -%%% -%%%---------------------------------------------------------------------- - --module(ejabberd_s2s_out). --author('alexey@process-one.net'). --behaviour(p1_fsm). - -% TODO this should be in a separate module after feature/cets is merged --xep([{xep, 220}, {version, "1.1.1"}]). - -%% External exports --export([start/2, - start_link/2, - start_connection/1, - stop_connection/1, - terminate_if_waiting_delay/1]). - -%% p1_fsm callbacks (same as gen_fsm) --export([init/1, - open_socket/2, - wait_for_stream/2, - wait_for_validation/2, - wait_for_features/2, - wait_for_auth_result/2, - wait_for_starttls_proceed/2, - reopen_socket/2, - wait_before_retry/2, - stream_established/2, - handle_event/3, - handle_sync_event/4, - handle_info/3, - terminate/3, - print_state/1, - code_change/4]). - --export_type([connection_info/0]). - --ignore_xref([open_socket/2, print_state/1, - reopen_socket/2, start_link/2, stream_established/2, - wait_before_retry/2, wait_for_auth_result/2, - wait_for_features/2, wait_for_starttls_proceed/2, wait_for_stream/2, - wait_for_stream/2, wait_for_validation/2]). - --type verify_requester() :: false | {S2SIn :: pid(), Key :: ejabberd_s2s:s2s_dialback_key(), SID :: ejabberd_s2s:stream_id()}. - --include("mongoose.hrl"). --include("jlib.hrl"). --include_lib("exml/include/exml_stream.hrl"). - --record(state, {socket, - streamid :: ejabberd_s2s:stream_id() | undefined, - remote_streamid = <<>> :: ejabberd_s2s:stream_id(), - tls = false :: boolean(), - tls_required = false :: boolean(), - tls_enabled = false :: boolean(), - tls_options :: just_tls:options(), - authenticated = false :: boolean(), - dialback_enabled = true :: boolean(), - try_auth = true :: boolean(), - from_to :: ejabberd_s2s:fromto(), - myname :: jid:lserver(), - server :: jid:lserver(), - queue :: element_queue(), - host_type :: mongooseim:host_type(), - delay_to_retry :: non_neg_integer() | undefined, - is_registered = false :: boolean(), - verify = false :: verify_requester(), - timer :: reference() - }). --type state() :: #state{}. - --type connection_info() :: - #{pid => pid(), - direction => out, - statename => statename(), - addr => unknown | inet:ip_address(), - port => unknown | inet:port_number(), - streamid => ejabberd_s2s:stream_id() | undefined, - tls => boolean(), - tls_required => boolean(), - tls_enabled => boolean(), - tls_options => just_tls:options(), - authenticated => boolean(), - dialback_enabled => boolean(), - try_auth => boolean(), - myname => jid:lserver(), - server => jid:lserver(), - delay_to_retry => undefined | non_neg_integer(), - verify => verify_requester()}. - --type element_queue() :: queue:queue(#xmlel{}). --type statename() :: open_socket - | wait_for_stream - | wait_for_features - | wait_for_auth_result - | wait_for_starttls_proceed - | wait_for_validation - | wait_before_retry. - -%% FSM handler return value --type fsm_return() :: {stop, Reason :: normal, state()} - | {next_state, statename(), state()} - | {next_state, statename(), state(), Timeout :: integer()}. - --type dns_name() :: string(). - --type addr() :: #{address := inet:ip_address() | dns_name(), - port := inet:port_number(), - type := inet | inet6}. - -%%-define(DBGFSM, true). - --ifdef(DBGFSM). --define(FSMOPTS, [{debug, [trace]}]). --else. --define(FSMOPTS, []). --endif. - --define(FSMTIMEOUT, 30000). - -%% We do not block on send anymore. --define(TCP_SEND_TIMEOUT, 15000). - --define(STREAM_HEADER(From, To), - <<"", - "">> - ). - --define(SOCKET_DEFAULT_RESULT, {error, badarg}). - - --define(CLOSE_GENERIC(StateName, Reason, StateData), - ?LOG_INFO(#{what => s2s_out_closing, text => <<"Closing s2s connection">>, - state_name => StateName, reason => Reason, - myname => StateData#state.myname, server => StateData#state.server}), - {stop, normal, StateData}). - --define(CLOSE_GENERIC(StateName, Reason, El, StateData), - ?LOG_INFO(#{what => s2s_out_closing, text => <<"Closing s2s connection on stanza">>, - state_name => StateName, reason => Reason, exml_packet => El, - myname => StateData#state.myname, server => StateData#state.server}), - {stop, normal, StateData}). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- --spec start(ejabberd_s2s:fromto(), _) -> {error, _} | {ok, undefined | pid()} | {ok, undefined | pid(), _}. -start(FromTo, Type) -> - supervisor:start_child(ejabberd_s2s_out_sup, [FromTo, Type]). - --spec start_link(ejabberd_s2s:fromto(), _) -> ignore | {error, _} | {ok, pid()}. -start_link(FromTo, Type) -> - p1_fsm:start_link(ejabberd_s2s_out, [FromTo, Type], - fsm_limit_opts() ++ ?FSMOPTS). - -start_connection(Pid) -> - p1_fsm:send_event(Pid, init). - -stop_connection(Pid) -> - p1_fsm:send_event(Pid, closed). - -%%%---------------------------------------------------------------------- -%%% Callback functions from p1_fsm -%%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- --spec init(list()) -> {ok, open_socket, state()}. -init([{From, Server} = FromTo, Type]) -> - process_flag(trap_exit, true), - ?LOG_DEBUG(#{what => s2s_out_started, - text => <<"New outgoing s2s connection">>, - from => From, server => Server, type => Type}), - {ok, HostType} = mongoose_domain_api:get_host_type(From), - TlsOpts = tls_options(HostType), - {TLS, TLSRequired} = get_tls_params(TlsOpts), - {IsRegistered, Verify} = case Type of - new -> - {true, false}; - {verify, Pid, Key, SID} -> - start_connection(self()), - {false, {Pid, Key, SID}} - end, - Timer = erlang:start_timer(mongoose_s2s_lib:timeout(), self(), []), - {ok, open_socket, #state{tls = TLS, - tls_required = TLSRequired, - tls_options = TlsOpts, - queue = queue:new(), - from_to = FromTo, - myname = From, - host_type = HostType, - server = Server, - is_registered = IsRegistered, - verify = Verify, - timer = Timer}}. - -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- --spec open_socket(_, state()) -> fsm_return(). -open_socket(init, StateData = #state{host_type = HostType}) -> - log_s2s_out(StateData#state.is_registered, - StateData#state.myname, - StateData#state.server, - StateData#state.tls), - ?LOG_DEBUG(#{what => s2s_open_socket, - myname => StateData#state.myname, - server => StateData#state.server, - is_registered => StateData#state.is_registered, - verify => StateData#state.verify}), - AddrList = get_addr_list(HostType, StateData#state.server), - case lists:foldl(fun(_, {ok, Socket}) -> - {ok, Socket}; - (#{address := Addr, port := Port, type := Type}, _) -> - open_socket2(HostType, Type, Addr, Port) - end, ?SOCKET_DEFAULT_RESULT, AddrList) of - {ok, Socket} -> - NewStateData = StateData#state{socket = Socket, - tls_enabled = false, - streamid = new_id()}, - send_text(NewStateData, - ?STREAM_HEADER(StateData#state.myname, StateData#state.server)), - {next_state, wait_for_stream, NewStateData, ?FSMTIMEOUT}; - {error, Reason} -> - ?LOG_WARNING(#{what => s2s_out_failed, reason => Reason, - text => <<"Outgoing s2s connection failed (remote server not found)">>, - myname => StateData#state.myname, server => StateData#state.server}), - wait_before_reconnect(StateData) - end; -open_socket(closed, StateData) -> - ?CLOSE_GENERIC(open_socket, closed, StateData); -open_socket(timeout, StateData) -> - ?CLOSE_GENERIC(open_socket, timeout, StateData); -open_socket(_, StateData) -> - {next_state, open_socket, StateData}. - --spec open_socket2(mongooseim:host_type(), inet | inet6, inet:ip_address(), inet:port_number()) -> - {error, _} | {ok, _}. -open_socket2(HostType, Type, Addr, Port) -> - ?LOG_DEBUG(#{what => s2s_out_connecting, - address => Addr, port => Port}), - Timeout = outgoing_s2s_timeout(HostType), - SockOpts = [binary, - {packet, 0}, - {send_timeout, ?TCP_SEND_TIMEOUT}, - {send_timeout_close, true}, - {active, false}, - Type], - - case (catch mongoose_s2s_socket_out:connect(s2s, Addr, Port, SockOpts, Timeout)) of - {ok, _Socket} = R -> R; - {error, Reason} = R -> - ?LOG_DEBUG(#{what => s2s_out_failed, - address => Addr, port => Port, reason => Reason}), - R; - {'EXIT', Reason} -> - ?LOG_DEBUG(#{what => s2s_out_failed, - text => <<"Failed to open s2s socket because of crashing">>, - address => Addr, port => Port, reason => Reason}), - {error, Reason} - end. - -%%---------------------------------------------------------------------- - --spec wait_for_stream(ejabberd:xml_stream_item(), state()) -> fsm_return(). -wait_for_stream(#xmlstreamstart{attrs = Attrs}, StateData0) -> - RemoteStreamID = maps:get(<<"id">>, Attrs, <<>>), - StateData = StateData0#state{remote_streamid = RemoteStreamID}, - case {maps:get(<<"xmlns">>, Attrs, <<>>), - maps:get(<<"xmlns:db">>, Attrs, <<>>), - maps:get(<<"version">>, Attrs, <<>>) =:= <<"1.0">>} of - {<<"jabber:server">>, <<"jabber:server:dialback">>, false} -> - send_dialback_request(StateData); - {<<"jabber:server">>, <<"jabber:server:dialback">>, true} -> - {next_state, wait_for_features, StateData, ?FSMTIMEOUT}; - {<<"jabber:server">>, <<"">>, true} -> - {next_state, wait_for_features, StateData#state{dialback_enabled = false}, ?FSMTIMEOUT}; - {NSProvided, DB, _} -> - send_element(StateData, mongoose_xmpp_errors:invalid_namespace()), - ?LOG_INFO(#{what => s2s_out_closing, - text => <<"Closing s2s connection: (invalid namespace)">>, - namespace_provided => NSProvided, - namespace_expected => <<"jabber:server">>, - xmlns_dialback_provided => DB, - all_attributes => Attrs, - myname => StateData#state.myname, server => StateData#state.server}), - {stop, normal, StateData} - end; -wait_for_stream(#xmlstreamerror{}, StateData) -> - send_element(StateData, mongoose_xmpp_errors:xml_not_well_formed()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_stream, xmlstreamerror, StateData); -wait_for_stream(#xmlstreamend{}, StateData) -> - ?CLOSE_GENERIC(wait_for_stream, xmlstreamend, StateData); -wait_for_stream(timeout, StateData) -> - ?CLOSE_GENERIC(wait_for_stream, timeout, StateData); -wait_for_stream(closed, StateData) -> - ?CLOSE_GENERIC(wait_for_stream, closed, StateData). - - --spec wait_for_validation(ejabberd:xml_stream_item(), state()) -> fsm_return(). -wait_for_validation({xmlstreamelement, El}, StateData = #state{from_to = FromTo}) -> - case mongoose_s2s_dialback:parse_validity(El) of - {step_3, FromTo, StreamID, IsValid} -> - ?LOG_DEBUG(#{what => s2s_receive_verify, - from_to => FromTo, stream_id => StreamID, is_valid => IsValid}), - case StateData#state.verify of - false -> - %% This is unexpected condition. - %% We've received step_3 reply, but there is no matching outgoing connection. - %% We could close the connection here. - next_state(wait_for_validation, StateData); - {Pid, _Key, _SID} -> - mongoose_s2s_in:send_validity_from_s2s_out(Pid, IsValid, FromTo), - next_state(wait_for_validation, StateData) - end; - {step_4, FromTo, StreamID, IsValid} -> - ?LOG_DEBUG(#{what => s2s_receive_result, - from_to => FromTo, stream_id => StreamID, is_valid => IsValid}), - #state{tls_enabled = Enabled, tls_required = Required} = StateData, - case IsValid of - true when (Enabled==true) or (Required==false) -> - %% Initiating server receives valid verification result from receiving server (Step 4) - send_queue(StateData, StateData#state.queue), - ?LOG_INFO(#{what => s2s_out_connected, - text => <<"New outgoing s2s connection established">>, - tls_enabled => StateData#state.tls_enabled, - myname => StateData#state.myname, server => StateData#state.server}), - {next_state, stream_established, - StateData#state{queue = queue:new()}}; - true when (Enabled==false) and (Required==true) -> - %% TODO: bounce packets - ?CLOSE_GENERIC(wait_for_validation, tls_required_but_unavailable, El, StateData); - _ -> - %% TODO: bounce packets - ?CLOSE_GENERIC(wait_for_validation, invalid_dialback_key, El, StateData) - end; - false -> - {next_state, wait_for_validation, StateData, ?FSMTIMEOUT*3} - end; -wait_for_validation(#xmlstreamend{}, StateData) -> - ?CLOSE_GENERIC(wait_for_validation, xmlstreamend, StateData); -wait_for_validation(#xmlstreamerror{}, StateData) -> - send_element(StateData, mongoose_xmpp_errors:xml_not_well_formed()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_validation, xmlstreamerror, StateData); -wait_for_validation(timeout, #state{verify = {VPid, VKey, SID}} = StateData) - when is_pid(VPid) and is_binary(VKey) and is_binary(SID) -> - %% This is an auxiliary s2s connection for dialback. - %% This timeout is normal and doesn't represent a problem. - ?LOG_INFO(#{what => s2s_out_validation_timeout, - text => <<"Timeout in verify outgoing s2s connection. Stopping">>, - myname => StateData#state.myname, server => StateData#state.server}), - {stop, normal, StateData}; -wait_for_validation(timeout, StateData) -> - ?LOG_INFO(#{what => s2s_out_connect_timeout, - text => <<"Connection timeout in outgoing s2s connection. Stopping">>, - myname => StateData#state.myname, server => StateData#state.server}), - {stop, normal, StateData}; -wait_for_validation(closed, StateData) -> - ?LOG_INFO(#{what => s2s_out_validation_closed, - text => <<"Connection closed when waiting for validation in outgoing s2s connection. Stopping">>, - myname => StateData#state.myname, server => StateData#state.server}), - {stop, normal, StateData}. - - --spec wait_for_features(ejabberd:xml_stream_item(), state()) -> fsm_return(). -wait_for_features({xmlstreamelement, El}, StateData) -> - case El of - #xmlel{name = <<"stream:features">>, children = Els} -> - {SASLEXT, StartTLS, StartTLSRequired} = - lists:foldl( - fun(#xmlel{name = <<"mechanisms">>, children = Els1} = El1, Acc) -> - Attr = exml_query:attr(El1, <<"xmlns">>, <<>>), - get_acc_with_new_sext(Attr, Els1, Acc); - (#xmlel{name = <<"starttls">>} = El1, Acc) -> - Attr = exml_query:attr(El1, <<"xmlns">>, <<>>), - get_acc_with_new_tls(Attr, El1, Acc); - (_, Acc) -> - Acc - end, {false, false, false}, Els), - handle_parsed_features({SASLEXT, StartTLS, StartTLSRequired, StateData}); - _ -> - send_element(StateData, mongoose_xmpp_errors:bad_format()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_features, bad_format, El, StateData) - end; -wait_for_features(#xmlstreamend{}, StateData) -> - ?CLOSE_GENERIC(wait_for_features, xmlstreamend, StateData); -wait_for_features(#xmlstreamerror{}, StateData) -> - send_element(StateData, mongoose_xmpp_errors:xml_not_well_formed()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_features, xmlstreamerror, StateData); -wait_for_features(timeout, StateData) -> - ?CLOSE_GENERIC(wait_for_features, timeout, StateData); -wait_for_features(closed, StateData) -> - ?CLOSE_GENERIC(wait_for_features, closed, StateData). - - --spec wait_for_auth_result(ejabberd:xml_stream_item(), state()) -> fsm_return(). -wait_for_auth_result({xmlstreamelement, El}, StateData) -> - case El of - #xmlel{name = <<"success">>} -> - case exml_query:attr(El, <<"xmlns">>) of - ?NS_SASL -> - ?LOG_DEBUG(#{what => s2s_auth_success, - myname => StateData#state.myname, - server => StateData#state.server}), - send_text(StateData, - ?STREAM_HEADER(StateData#state.myname, StateData#state.server)), - {next_state, wait_for_stream, - StateData#state{streamid = new_id(), - authenticated = true - }, ?FSMTIMEOUT}; - _ -> - send_element(StateData, mongoose_xmpp_errors:bad_format()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_auth_result, bad_format, El, StateData) - end; - #xmlel{name = <<"failure">>} -> - case exml_query:attr(El, <<"xmlns">>) of - ?NS_SASL -> - ?LOG_WARNING(#{what => s2s_auth_failure, - text => <<"Received failure result in ejabberd_s2s_out. Restarting">>, - myname => StateData#state.myname, - server => StateData#state.server}), - mongoose_s2s_socket_out:close(StateData#state.socket), - {next_state, reopen_socket, - StateData#state{socket = undefined}, ?FSMTIMEOUT}; - _ -> - send_element(StateData, mongoose_xmpp_errors:bad_format()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_auth_result, bad_format, El, StateData) - end; - _ -> - send_element(StateData, mongoose_xmpp_errors:bad_format()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_auth_result, bad_format, El, StateData) - end; -wait_for_auth_result(#xmlstreamend{}, StateData) -> - ?CLOSE_GENERIC(wait_for_auth_result, xmlstreamend, StateData); -wait_for_auth_result(#xmlstreamerror{}, StateData) -> - send_element(StateData, mongoose_xmpp_errors:xml_not_well_formed()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_auth_result, xmlstreamerror, StateData); -wait_for_auth_result(timeout, StateData) -> - ?CLOSE_GENERIC(wait_for_auth_result, timeout, StateData); -wait_for_auth_result(closed, StateData) -> - ?CLOSE_GENERIC(wait_for_auth_result, closed, StateData). - - --spec wait_for_starttls_proceed(ejabberd:xml_stream_item(), state()) -> fsm_return(). -wait_for_starttls_proceed({xmlstreamelement, El}, StateData) -> - case El of - #xmlel{name = <<"proceed">>} -> - case exml_query:attr(El, <<"xmlns">>) of - ?NS_TLS -> - ?LOG_DEBUG(#{what => s2s_starttls, - myname => StateData#state.myname, - server => StateData#state.server}), - TLSSocket = mongoose_s2s_socket_out:connect_tls(StateData#state.socket, - StateData#state.tls_options), - NewStateData = StateData#state{socket = TLSSocket, - streamid = new_id(), - tls_enabled = true}, - send_text(NewStateData, - ?STREAM_HEADER(StateData#state.myname, StateData#state.server)), - {next_state, wait_for_stream, NewStateData, ?FSMTIMEOUT}; - _ -> - send_element(StateData, mongoose_xmpp_errors:bad_format()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_auth_result, bad_format, El, StateData) - end; - _ -> - ?CLOSE_GENERIC(wait_for_auth_result, bad_format, El, StateData) - end; -wait_for_starttls_proceed(#xmlstreamend{}, StateData) -> - ?CLOSE_GENERIC(wait_for_starttls_proceed, xmlstreamend, StateData); -wait_for_starttls_proceed(#xmlstreamerror{}, StateData) -> - send_element(StateData, mongoose_xmpp_errors:xml_not_well_formed()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(wait_for_starttls_proceed, xmlstreamerror, StateData); -wait_for_starttls_proceed(timeout, StateData) -> - ?CLOSE_GENERIC(wait_for_starttls_proceed, timeout, StateData); -wait_for_starttls_proceed(closed, StateData) -> - ?CLOSE_GENERIC(wait_for_starttls_proceed, closed, StateData). - - --spec reopen_socket(ejabberd:xml_stream_item(), state()) -> fsm_return(). -reopen_socket({xmlstreamelement, _El}, StateData) -> - {next_state, reopen_socket, StateData, ?FSMTIMEOUT}; -reopen_socket(#xmlstreamend{}, StateData) -> - {next_state, reopen_socket, StateData, ?FSMTIMEOUT}; -reopen_socket(#xmlstreamerror{}, StateData) -> - {next_state, reopen_socket, StateData, ?FSMTIMEOUT}; -reopen_socket(timeout, StateData) -> - ?CLOSE_GENERIC(reopen_socket, timeout, StateData); -reopen_socket(closed, StateData) -> - p1_fsm:send_event(self(), init), - {next_state, open_socket, StateData, ?FSMTIMEOUT}. - - -%% @doc This state is use to avoid reconnecting to often to bad sockets --spec wait_before_retry(ejabberd:xml_stream_item(), state()) -> fsm_return(). -wait_before_retry(_Event, StateData) -> - {next_state, wait_before_retry, StateData, ?FSMTIMEOUT}. - --spec stream_established(ejabberd:xml_stream_item(), state()) -> fsm_return(). -stream_established({xmlstreamelement, El}, StateData = #state{from_to = FromTo}) -> - ?LOG_DEBUG(#{what => s2s_out_stream_established, exml_packet => El, - myname => StateData#state.myname, server => StateData#state.server}), - case mongoose_s2s_dialback:parse_validity(El) of - {step_3, FromTo, StreamID, IsValid} -> - ?LOG_DEBUG(#{what => s2s_recv_verify, - from_to => FromTo, stream_id => StreamID, is_valid => IsValid, - myname => StateData#state.myname, server => StateData#state.server}), - case StateData#state.verify of - {VPid, _VKey, _SID} -> - mongoose_s2s_in:send_validity_from_s2s_out(VPid, IsValid, FromTo); - _ -> - ok - end; - {step_4, _FromTo, _StreamID, _IsValid} -> - ok; - false -> - ok - end, - {next_state, stream_established, StateData}; -stream_established(#xmlstreamend{}, StateData) -> - ?CLOSE_GENERIC(stream_established, xmlstreamend, StateData); -stream_established(#xmlstreamerror{}, StateData) -> - send_element(StateData, mongoose_xmpp_errors:xml_not_well_formed()), - send_text(StateData, ?STREAM_TRAILER), - ?CLOSE_GENERIC(stream_established, xmlstreamerror, StateData); -stream_established(timeout, StateData) -> - ?CLOSE_GENERIC(stream_established, timeout, StateData); -stream_established(closed, StateData) -> - ?CLOSE_GENERIC(stream_established, closed, StateData). - - -%%---------------------------------------------------------------------- -%% Func: StateName/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -%%state_name(Event, From, StateData) -> -%% Reply = ok, -%% {reply, Reply, state_name, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_event(_Event, StateName, StateData) -> - next_state(StateName, StateData). - -handle_sync_event(get_state_info, _From, StateName, StateData) -> - {reply, handle_get_state_info(StateName, StateData), StateName, StateData}; -handle_sync_event(_Event, _From, StateName, StateData) -> - {reply, ok, StateName, StateData, get_timeout_interval(StateName)}. - - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_info({send_element, Acc, El}, StateName, StateData) -> - case StateName of - stream_established -> - cancel_timer(StateData#state.timer), - Timer = erlang:start_timer(mongoose_s2s_lib:timeout(), self(), []), - send_element(StateData, El), - {next_state, StateName, StateData#state{timer = Timer}}; - %% In this state we bounce all message: We are waiting before - %% trying to reconnect - wait_before_retry -> - bounce_element(Acc, El, mongoose_xmpp_errors:remote_server_not_found(<<"en">>, <<"From s2s">>)), - {next_state, StateName, StateData}; - _ -> - Q = queue:in({Acc, El}, StateData#state.queue), - next_state(StateName, StateData#state{queue = Q}) - end; -handle_info({timeout, Timer, _}, wait_before_retry, - #state{timer = Timer} = StateData) -> - ?LOG_INFO(#{what => s2s_reconnect_delay_expired, - text => <<"Reconnect delay expired: Will now retry to connect when needed.">>, - myname => StateData#state.myname, - server => StateData#state.server}), - {stop, normal, StateData}; -handle_info({timeout, Timer, _}, StateName, - #state{timer = Timer} = StateData) -> - ?CLOSE_GENERIC(StateName, s2s_out_timeout, StateData); -handle_info(terminate_if_waiting_before_retry, wait_before_retry, StateData) -> - ?CLOSE_GENERIC(wait_before_retry, terminate_if_waiting_before_retry, StateData); -handle_info(terminate_if_waiting_before_retry, StateName, StateData) -> - next_state(StateName, StateData); -handle_info(_, StateName, StateData) -> - next_state(StateName, StateData). - -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- -terminate(Reason, StateName, StateData) -> - ?LOG_DEBUG(#{what => s2s_out_closed, text => <<"ejabberd_s2s_out terminated">>, - reason => Reason, state_name => StateName, - myname => StateData#state.myname, server => StateData#state.server}), - case StateData#state.is_registered of - false -> - ok; - true -> - ejabberd_s2s:remove_connection( - {StateData#state.myname, StateData#state.server}, self()) - end, - E = mongoose_xmpp_errors:remote_server_not_found(<<"en">>, <<"Bounced by s2s">>), - %% bounce queue manage by process and Erlang message queue - bounce_queue(StateData#state.queue, E), - case queue:is_empty(StateData#state.queue) of - true -> - ok; - false -> - ?LOG_WARNING(#{what => s2s_terminate_non_empty, - state_name => StateName, reason => Reason, - queue => lists:sublist(queue:to_list(StateData#state.queue), 10), - authenticated => StateData#state.authenticated}) - end, - bounce_messages(E), - case StateData#state.socket of - undefined -> - ok; - _Socket -> - mongoose_s2s_socket_out:close(StateData#state.socket) - end, - ok. - -%%---------------------------------------------------------------------- -%% Func: print_state/1 -%% Purpose: Prepare the state to be printed on error log -%% Returns: State to print -%%---------------------------------------------------------------------- -print_state(State) -> - State. - -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - --spec send_text(state(), binary()) -> ok. -send_text(StateData, Text) -> - mongoose_s2s_socket_out:send_text(StateData#state.socket, Text). - - --spec send_element(state(), exml:element()|mongoose_acc:t()) -> ok. -send_element(StateData, #xmlel{} = El) -> - mongoose_s2s_socket_out:send_element(StateData#state.socket, El). - --spec send_element(state(), mongoose_acc:t(), exml:element()) -> mongoose_acc:t(). -send_element(StateData, Acc, El) -> - mongoose_s2s_socket_out:send_element(StateData#state.socket, El), - Acc. - - --spec send_queue(state(), Q :: element_queue()) -> ok. -send_queue(StateData, Q) -> - case queue:out(Q) of - {{value, {Acc, El}}, Q1} -> - send_element(StateData, Acc, El), - send_queue(StateData, Q1); - {empty, _Q1} -> - ok - end. - - -%% @doc Bounce a single message (xmlel) --spec bounce_element(Acc :: mongoose_acc:t(), El :: exml:element(), Error :: exml:element()) -> ok. -bounce_element(Acc, El, Error) -> - case mongoose_acc:stanza_type(Acc) of - <<"error">> -> ok; - <<"result">> -> ok; - _ -> - From = mongoose_acc:from_jid(Acc), - To = mongoose_acc:to_jid(Acc), - {Acc1, Err} = jlib:make_error_reply(Acc, El, Error), - ejabberd_router:route(To, From, Acc1, Err) - end. - - --spec bounce_queue(Q :: element_queue(), Error :: exml:element()) -> ok. -bounce_queue(Q, Error) -> - case queue:out(Q) of - {{value, {Acc, El}}, Q1} -> - bounce_element(Acc, El, Error), - bounce_queue(Q1, Error); - {empty, _} -> - ok - end. - - --spec new_id() -> binary(). -new_id() -> - mongoose_bin:gen_from_crypto(). - - --spec cancel_timer(reference()) -> ok. -cancel_timer(Timer) -> - erlang:cancel_timer(Timer), - receive - {timeout, Timer, _} -> - ok - after 0 -> - ok - end. - - --spec bounce_messages(exml:element()) -> ok. -bounce_messages(Error) -> - receive - {send_element, Acc, El} -> - bounce_element(Acc, El, Error), - bounce_messages(Error) - after 0 -> - ok - end. - - --spec send_dialback_request(state()) -> fsm_return(). -send_dialback_request(StateData) -> - IsRegistered = case StateData#state.is_registered of - false -> - ejabberd_s2s:try_register(StateData#state.from_to); - true -> - true - end, - NewStateData = StateData#state{is_registered = IsRegistered}, - try - case IsRegistered of - false -> - %% Still not registered in the s2s table as an outgoing connection - ok; - true -> - Key1 = ejabberd_s2s:key( - StateData#state.host_type, - StateData#state.from_to, - StateData#state.remote_streamid), - %% Initiating server sends dialback key - send_element(StateData, mongoose_s2s_dialback:step_1(StateData#state.from_to, Key1)) - end, - case StateData#state.verify of - false -> - ok; - {_Pid, Key2, SID} -> - %% Receiving server sends verification request - send_element(StateData, mongoose_s2s_dialback:step_2(StateData#state.from_to, Key2, SID)) - end, - {next_state, wait_for_validation, NewStateData, ?FSMTIMEOUT*6} - catch - Class:Reason:Stacktrace -> - ?LOG_ERROR(#{what => s2s_out_send_dialback_request_failed, - class => Class, reason => Reason, stacktrace => Stacktrace, - myname => StateData#state.myname, server => StateData#state.server}), - {stop, normal, NewStateData} - end. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% SRV support - --include_lib("kernel/include/inet.hrl"). - --spec lookup_services(mongooseim:host_type(), jid:lserver()) -> [addr()]. -lookup_services(HostType, Server) -> - case mongoose_s2s_lib:domain_utf8_to_ascii(Server) of - false -> []; - ASCIIAddr -> do_lookup_services(HostType, ASCIIAddr) - end. - --spec do_lookup_services(mongooseim:host_type(), jid:lserver()) -> [addr()]. -do_lookup_services(HostType, Server) -> - Res = srv_lookup(HostType, Server), - case Res of - {error, Reason} -> - ?LOG_DEBUG(#{what => s2s_srv_lookup_failed, - reason => Reason, server => Server}), - []; - {AddrList, Type} -> - %% Probabilities are not exactly proportional to weights - %% for simplicity (higher weights are overvalued) - case (catch lists:map(fun calc_addr_index/1, AddrList)) of - {'EXIT', _Reason} -> - []; - IndexedAddrs -> - Addrs = [#{address => Addr, port => Port, type => Type} - || {_Index, Addr, Port} <- lists:keysort(1, IndexedAddrs)], - ?LOG_DEBUG(#{what => s2s_srv_lookup_success, - addresses => Addrs, server => Server}), - Addrs - end - end. - --spec srv_lookup(mongooseim:host_type(), jid:lserver()) -> - {error, atom()} | {list(), inet | inet6}. -srv_lookup(HostType, Server) -> - #{timeout := TimeoutSec, retries := Retries} = mongoose_config:get_opt([{s2s, HostType}, dns]), - case srv_lookup(Server, timer:seconds(TimeoutSec), Retries) of - {error, Reason} -> - {error, Reason}; - {ok, #hostent{h_addr_list = AddrList}} -> - case get_inet_protocol(Server) of - {error, Reason} -> - {error, Reason}; - Type -> - {AddrList, Type} - end - end. - --spec get_inet_protocol(jid:lserver()) -> {error, atom()} | inet | inet6. -get_inet_protocol(Server) -> - case inet:getaddr(binary_to_list(Server), inet) of - {ok, _IPv4Addr} -> - inet; - {error, _} -> - case inet:getaddr(binary_to_list(Server), inet6) of - {ok, _IPv6Addr} -> - inet6; - {error, Reason} -> - {error, Reason} - end - end. - -%% @doc XXX - this behaviour is suboptimal in the case that the domain -%% has a "_xmpp-server._tcp." but not a "_jabber._tcp." record and -%% we don't get a DNS reply for the "_xmpp-server._tcp." lookup. In this -%% case we'll give up when we get the "_jabber._tcp." nxdomain reply. --spec srv_lookup(jid:server(), - Timeout :: non_neg_integer(), - Retries :: pos_integer() - ) -> {error, atom()} | {ok, inet:hostent()}. -srv_lookup(_Server, _Timeout, Retries) when Retries < 1 -> - {error, timeout}; -srv_lookup(Server, Timeout, Retries) -> - case inet_res:getbyname("_xmpp-server._tcp." ++ binary_to_list(Server), srv, Timeout) of - {error, _Reason} -> - case inet_res:getbyname("_jabber._tcp." ++ binary_to_list(Server), srv, Timeout) of - {error, timeout} -> - ?LOG_ERROR(#{what => s2s_dns_lookup_failed, - text => <<"The DNS servers timed out on request for IN SRV." - " You should check your DNS configuration.">>, - nameserver => inet_db:res_option(nameserver), - server => Server}), - srv_lookup(Server, Timeout, Retries - 1); - R -> R - end; - {ok, _HEnt} = R -> R - end. - --spec lookup_addrs(mongooseim:host_type(), jid:server()) -> [addr()]. -lookup_addrs(HostType, Server) -> - Port = outgoing_s2s_port(HostType), - lists:foldl(fun(Type, []) -> - [#{address => Addr, port => Port, type => Type} - || Addr <- lookup_addrs_for_type(Server, Type)]; - (_Type, Addrs) -> - Addrs - end, [], outgoing_s2s_types(HostType)). - --spec lookup_addrs_for_type(jid:lserver(), inet | inet6) -> [inet:ip_address()]. -lookup_addrs_for_type(Server, Type) -> - case inet:gethostbyname(binary_to_list(Server), Type) of - {ok, #hostent{h_addr_list = Addrs}} -> - ?LOG_DEBUG(#{what => s2s_srv_resolve_success, - type => Type, server => Server, addresses => Addrs}), - Addrs; - {error, Reason} -> - ?LOG_DEBUG(#{what => s2s_srv_resolve_failed, - type => Type, server => Server, reason => Reason}), - [] - end. - - --spec outgoing_s2s_port(mongooseim:host_type()) -> inet:port_number(). -outgoing_s2s_port(HostType) -> - mongoose_config:get_opt([{s2s, HostType}, outgoing, port]). - -get_tls_params(#{mode := starttls_required}) -> - {true, true}; -get_tls_params(#{mode := starttls}) -> - {true, false}; -get_tls_params(_) -> - {false, false}. - --spec outgoing_s2s_types(mongooseim:host_type()) -> [inet | inet6, ...]. -outgoing_s2s_types(HostType) -> - %% DISCUSSION: Why prefer IPv4 first? - %% - %% IPv4 connectivity will be available for everyone for - %% many years to come. So, there's absolutely no benefit - %% in preferring IPv6 connections which are flaky at best - %% nowadays. - %% - %% On the other hand content providers hesitate putting up - %% AAAA records for their sites due to the mentioned - %% quality of current IPv6 connectivity. Making IPv6 the a - %% `fallback' may avoid these problems elegantly. - [ip_version_to_type(V) || V <- mongoose_config:get_opt([{s2s, HostType}, outgoing, ip_versions])]. - -ip_version_to_type(4) -> inet; -ip_version_to_type(6) -> inet6. - --spec outgoing_s2s_timeout(mongooseim:host_type()) -> non_neg_integer() | infinity. -outgoing_s2s_timeout(HostType) -> - mongoose_config:get_opt([{s2s, HostType}, outgoing, connection_timeout], 10000). - -%% @doc Human readable S2S logging: Log only new outgoing connections as INFO -%% Do not log dialback -log_s2s_out(false, _, _, _) -> ok; -%% Log new outgoing connections: -log_s2s_out(_, Myname, Server, Tls) -> - ?LOG_INFO(#{what => s2s_out, - text => <<"Trying to open s2s connection">>, - myname => Myname, server => Server, tls => Tls}). - -next_state(StateName, StateData) -> - {next_state, StateName, StateData, - get_timeout_interval(StateName)}. - -%% @doc Calculate timeout depending on which state we are in: -%% Can return integer > 0 | infinity --spec get_timeout_interval(statename()) -> infinity | non_neg_integer(). -get_timeout_interval(StateName) -> - case StateName of - %% Validation implies dialback: Networking can take longer: - wait_for_validation -> - ?FSMTIMEOUT*6; - %% When stream is established, we only rely on S2S Timeout timer: - stream_established -> - infinity; - _ -> - ?FSMTIMEOUT - end. - - -%% @doc This function is intended to be called at the end of a state -%% function that want to wait for a reconnect delay before stopping. --spec wait_before_reconnect(state()) -> fsm_return(). -wait_before_reconnect(StateData) -> - E = mongoose_xmpp_errors:remote_server_not_found(<<"en">>, <<"From s2s (waiting)">>), - %% bounce queue manage by process and Erlang message queue - bounce_queue(StateData#state.queue, E), - bounce_messages(E), - cancel_timer(StateData#state.timer), - Delay = case StateData#state.delay_to_retry of - undefined -> - %% The initial delay is random between 1 and 15 seconds - %% Return a random integer between 1000 and 15000 - MicroSecs = erlang:system_time(microsecond), - (MicroSecs rem 14000) + 1000; - D1 -> - %% Duplicate the delay with each successive failed - %% reconnection attempt, but don't exceed the max - lists:min([D1 * 2, get_max_retry_delay(StateData#state.host_type)]) - end, - Timer = erlang:start_timer(Delay, self(), []), - {next_state, wait_before_retry, StateData#state{timer=Timer, - delay_to_retry = Delay, - queue = queue:new()}}. - - -%% @doc Get the maximum allowed delay for retry to reconnect (in milliseconds). -%% The default value is 5 minutes. -%% The option {s2s_max_retry_delay, Seconds} can be used (in seconds). -get_max_retry_delay(HostType) -> - mongoose_config:get_opt([{s2s, HostType}, max_retry_delay]) * 1000. - - -%% @doc Terminate s2s_out connections that are in state wait_before_retry --spec terminate_if_waiting_delay(ejabberd_s2s:fromto()) -> ok. -terminate_if_waiting_delay(FromTo) -> - Pids = ejabberd_s2s:get_s2s_out_pids(FromTo), - lists:foreach( - fun(Pid) -> - Pid ! terminate_if_waiting_before_retry - end, - Pids). - - --spec fsm_limit_opts() -> [{max_queue, integer()}]. -fsm_limit_opts() -> - case mongoose_config:lookup_opt(max_fsm_queue) of - {ok, N} -> - [{max_queue, N}]; - {error, not_found} -> - [] - end. - --spec get_addr_list(mongooseim:host_type(), jid:lserver()) -> [addr()]. -get_addr_list(HostType, Server) -> - lists:foldl(fun(F, []) -> F(HostType, Server); - (_, Result) -> Result - end, [], [fun get_predefined_addresses/2, - fun lookup_services/2, - fun lookup_addrs/2]). - -%% @doc Get IPs predefined for a given s2s domain in the configuration --spec get_predefined_addresses(mongooseim:host_type(), jid:lserver()) -> [addr()]. -get_predefined_addresses(HostType, Server) -> - case mongoose_config:lookup_opt([{s2s, HostType}, address, Server]) of - {ok, #{ip_address := IPAddress} = M} -> - {ok, IPTuple} = inet:parse_address(IPAddress), - Port = get_predefined_port(HostType, M), - [#{address => IPTuple, port => Port, type => addr_type(IPTuple)}]; - {error, not_found} -> - [] - end. - -get_predefined_port(_HostType, #{port := Port}) -> Port; -get_predefined_port(HostType, _Addr) -> outgoing_s2s_port(HostType). - -addr_type(Addr) when tuple_size(Addr) =:= 4 -> inet; -addr_type(Addr) when tuple_size(Addr) =:= 8 -> inet6. - -get_acc_with_new_sext(?NS_SASL, Els1, {_SEXT, STLS, STLSReq}) -> - NewSEXT = - lists:any( - fun(El) -> - is_record(El, xmlel) - andalso <<"mechanism">> =:= El#xmlel.name - andalso <<"EXTERNAL">> =:= exml_query:cdata(El) - end, Els1), - - {NewSEXT, STLS, STLSReq}; -get_acc_with_new_sext(_, _, Acc) -> - Acc. - -get_acc_with_new_tls(?NS_TLS, El1, {SEXT, _STLS, _STLSReq}) -> - {SEXT, true, undefined =/= exml_query:subelement(El1, <<"required">>)}; -get_acc_with_new_tls(_, _, Acc) -> - Acc. - -tls_options(HostType) -> - mongoose_config:get_opt([{s2s, HostType}, tls], #{}). - -calc_addr_index({Priority, Weight, Port, Host}) -> - N = case Weight of - 0 -> 0; - _ -> (Weight + 1) * rand:uniform() - end, - {Priority * 65536 - N, Host, Port}. - -handle_parsed_features({false, false, _, StateData = #state{authenticated = true}}) -> - send_queue(StateData, StateData#state.queue), - ?LOG_INFO(#{what => s2s_out_connected, - text => <<"New outgoing s2s connection established">>, - myname => StateData#state.myname, server => StateData#state.server}), - {next_state, stream_established, - StateData#state{queue = queue:new()}}; -handle_parsed_features({true, _, _, StateData = #state{try_auth = true, is_registered = true}}) -> - send_element(StateData, - #xmlel{name = <<"auth">>, - attrs = #{<<"xmlns">> => ?NS_SASL, - <<"mechanism">> => <<"EXTERNAL">>}, - children = - [#xmlcdata{content = base64:encode( - StateData#state.myname)}]}), - {next_state, wait_for_auth_result, - StateData#state{try_auth = false}, ?FSMTIMEOUT}; -handle_parsed_features({_, true, _, StateData = #state{tls = true, tls_enabled = false}}) -> - send_element(StateData, - #xmlel{name = <<"starttls">>, - attrs = #{<<"xmlns">> => ?NS_TLS}}), - {next_state, wait_for_starttls_proceed, StateData, - ?FSMTIMEOUT}; -handle_parsed_features({_, _, true, StateData = #state{tls = false}}) -> - ?LOG_DEBUG(#{what => s2s_out_restarted, - myname => StateData#state.myname, server => StateData#state.server}), - mongoose_s2s_socket_out:close(StateData#state.socket), - {next_state, reopen_socket, - StateData#state{socket = undefined}, ?FSMTIMEOUT}; -handle_parsed_features({_, _, _, StateData = #state{dialback_enabled = true}}) -> - send_dialback_request(StateData); -handle_parsed_features({_, _, _, StateData}) -> - ?LOG_DEBUG(#{what => s2s_out_restarted, - myname => StateData#state.myname, server => StateData#state.server}), - % TODO: clear message queue - mongoose_s2s_socket_out:close(StateData#state.socket), - {next_state, reopen_socket, StateData#state{socket = undefined}, ?FSMTIMEOUT}. - -handle_get_state_info(StateName, StateData) -> - {Addr, Port} = get_peername(StateData#state.socket), - #{pid => self(), - direction => out, - statename => StateName, - addr => Addr, - port => Port, - streamid => StateData#state.streamid, - tls => StateData#state.tls, - tls_required => StateData#state.tls_required, - tls_enabled => StateData#state.tls_enabled, - tls_options => StateData#state.tls_options, - authenticated => StateData#state.authenticated, - dialback_enabled => StateData#state.dialback_enabled, - try_auth => StateData#state.try_auth, - myname => StateData#state.myname, - server => StateData#state.server, - delay_to_retry => StateData#state.delay_to_retry, - verify => StateData#state.verify}. - -get_peername(undefined) -> - {unknown, unknown}; -get_peername(Socket) -> - {ok, {Addr, Port}} = mongoose_s2s_socket_out:peername(Socket), - {Addr, Port}. diff --git a/src/s2s/mongoose_s2s_backend.erl b/src/s2s/mongoose_s2s_backend.erl index 20c3982eef..66ee2d6d1f 100644 --- a/src/s2s/mongoose_s2s_backend.erl +++ b/src/s2s/mongoose_s2s_backend.erl @@ -34,7 +34,7 @@ init(Opts) -> get_s2s_out_pids(FromTo) -> mongoose_backend:call(global, ?MAIN_MODULE, ?FUNCTION_NAME, [FromTo]). -%% Register ejabberd_s2s_out connection +%% Register mongoose_s2s_out connection -spec try_register(Pid :: pid(), FromTo :: ejabberd_s2s:fromto()) -> boolean(). try_register(Pid, FromTo) -> diff --git a/src/s2s/mongoose_s2s_in.erl b/src/s2s/mongoose_s2s_in.erl index 2c35fd9966..ecb4d383b9 100644 --- a/src/s2s/mongoose_s2s_in.erl +++ b/src/s2s/mongoose_s2s_in.erl @@ -8,7 +8,7 @@ -record(s2s_data, { host_type :: undefined | mongooseim:host_type(), - lserver = ?MYNAME :: jid:lserver(), + myname = ?MYNAME :: jid:lserver(), auth_domain :: jid:lserver() | undefined, lang = ?MYLANG :: ejabberd:lang(), streamid = mongoose_bin:gen_from_crypto() :: binary(), @@ -38,7 +38,7 @@ streamid => ejabberd_s2s:stream_id(), tls => boolean(), tls_enabled => boolean(), - tls_options => just_tls:options(), + tls_options => undefined | just_tls:options(), authenticated => boolean(), shaper => mongoose_shaper:shaper(), domains => [jid:lserver()]}. @@ -165,7 +165,7 @@ handle_stream_start(D0, #{<<"xmlns">> := ?NS_SERVER, <<"to">> := Server} = Attrs LServer = jid:nameprep(Server), case is_binary(LServer) andalso mongoose_domain_api:get_host_type(LServer) of {ok, HostType} -> - D1 = D0#s2s_data{lserver = LServer, host_type = HostType}, + D1 = D0#s2s_data{myname = LServer, host_type = HostType}, stream_start_features_before_auth(D1, Attrs); _ -> Info = #{location => ?LOCATION, last_event => {stream_start, Attrs}}, @@ -178,7 +178,7 @@ handle_stream_start(D0, #{<<"xmlns">> := ?NS_SERVER} = Attrs, stream_start) -> handle_stream_start(D0, Attrs, stream_start) -> Info = #{location => ?LOCATION, last_event => {stream_start, Attrs}}, stream_start_error(D0, Info, mongoose_xmpp_errors:invalid_namespace()); -handle_stream_start(#s2s_data{lserver = LServer} = D0, +handle_stream_start(#s2s_data{myname = LServer} = D0, #{<<"xmlns">> := ?NS_SERVER, <<"to">> := Server} = Attrs, authenticated) -> @@ -267,7 +267,7 @@ handle_auth_start(#s2s_data{} = Data, #xmlel{attrs = #{<<"xmlns">> := _}}) -> -spec handle_dialback(data(), exml:element(), state()) -> fsm_res(). handle_dialback(#s2s_data{} = Data, #xmlel{} = El, _) -> case mongoose_s2s_dialback:parse_key(El) of - %% Incoming dialback key, we have to verify it using ejabberd_s2s_out before + %% Incoming dialback key, we have to verify it using mongoose_s2s_out before %% accepting any incoming stanzas %% (we have to receive the `validity_from_s2s_out' event first). {step_1, FromTo, StreamID, Key} = Parsed -> @@ -276,11 +276,11 @@ handle_dialback(#s2s_data{} = Data, #xmlel{} = El, _) -> %% domain is handled by this server: case {mongoose_s2s_lib:allow_host(FromTo), is_local_host_known(FromTo)} of {true, true} -> - ejabberd_s2s_out:terminate_if_waiting_delay(FromTo), + mongoose_s2s_out:terminate_if_waiting_delay(FromTo), StartType = {verify, self(), Key, Data#s2s_data.streamid}, - %% Could we reuse an existing ejabberd_s2s_out connection + %% Could we reuse an existing mongoose_s2s_out connection %% instead of making a new one? - ejabberd_s2s_out:start(FromTo, StartType), + mongoose_s2s_out:start_connection(FromTo, StartType), Conns = maps:put(FromTo, wait_for_verification, Data#s2s_data.connections), NewData = Data#s2s_data{connections = Conns}, {next_state, stream_established, NewData}; @@ -405,11 +405,12 @@ handle_socket_packet(#s2s_data{parser = Parser} = Data, Packet) -> end. -spec handle_socket_elements(data(), [exml_stream:element()], non_neg_integer()) -> fsm_res(). -handle_socket_elements(#s2s_data{lserver = LServer, shaper = Shaper} = Data, Elements, Size) -> +handle_socket_elements(#s2s_data{myname = LServer, shaper = Shaper} = Data, Elements, Size) -> {NewShaper, Pause} = mongoose_shaper:update(Shaper, Size), [mongoose_instrument:execute( - xmpp_element_size_in, labels(), #{byte_size => exml:xml_size(El), lserver => LServer}) - || El <- Elements], + xmpp_element_size_in, labels(), + #{byte_size => exml:xml_size(Elem), lserver => LServer, pid => self(), module => ?MODULE}) + || Elem <- Elements], NewData = Data#s2s_data{shaper = NewShaper}, StreamEvents0 = [ {next_event, internal, XmlEl} || XmlEl <- Elements ], StreamEvents1 = maybe_add_pause(NewData, StreamEvents0, Pause), @@ -447,7 +448,7 @@ handle_get_state_info(#s2s_data{socket = Socket, listener_opts = LOpts} = Data, streamid => Data#s2s_data.streamid, tls => maps:is_key(tls, LOpts), tls_enabled => mongoose_xmpp_socket:is_ssl(Socket), - tls_options => maps:get(tls, LOpts, #{}), + tls_options => maps:get(tls, LOpts, undefined), authenticated => Data#s2s_data.authenticated, shaper => Data#s2s_data.shaper, domains => Domains}. @@ -467,7 +468,7 @@ state_timeout(#{state_timeout := Timeout}) -> -spec stream_start_features_before_auth(data(), exml:attrs()) -> fsm_res(). stream_start_features_before_auth( - #s2s_data{host_type = HostType, lserver = LServer, socket = Socket} = Data, + #s2s_data{host_type = HostType, myname = LServer, socket = Socket} = Data, #{<<"version">> := ?XMPP_VERSION}) -> IsSSL = mongoose_xmpp_socket:is_ssl(Socket), StreamFeatures0 = mongoose_hooks:s2s_stream_features(HostType, LServer), @@ -483,7 +484,7 @@ stream_start_features_before_auth(#s2s_data{} = Data, #{<<"xmlns:db">> := ?NS_SE {next_state, wait_for_feature_before_auth, Data, state_timeout(Data)}. -spec stream_start_after_auth(data(), exml:attrs()) -> fsm_res(). -stream_start_after_auth(#s2s_data{host_type = HostType, lserver = LServer} = Data, +stream_start_after_auth(#s2s_data{host_type = HostType, myname = LServer} = Data, #{<<"version">> := ?XMPP_VERSION}) -> StreamFeatures = mongoose_hooks:s2s_stream_features(HostType, LServer), Features = #xmlel{name = <<"stream:features">>, @@ -516,13 +517,14 @@ stream_error(Data, Error) -> {stop, {shutdown, stream_error}, Data}. -spec send_xml(data(), exml_stream:element()) -> maybe_ok(). -send_xml(#s2s_data{socket = Socket}, Elem) -> +send_xml(#s2s_data{myname = LServer, socket = Socket}, Elem) -> mongoose_instrument:execute( - xmpp_element_size_out, labels(), #{element => Elem, byte_size => exml:xml_size(Elem)}), + xmpp_element_size_out, labels(), + #{byte_size => exml:xml_size(Elem), lserver => LServer, pid => self(), module => ?MODULE}), mongoose_xmpp_socket:send_xml(Socket, Elem). -spec execute_element_event(data(), exml_stream:element(), mongoose_instrument:event_name()) -> ok. -execute_element_event(#s2s_data{lserver = LServer}, #xmlel{name = Name} = El, EventName) -> +execute_element_event(#s2s_data{myname = LServer}, #xmlel{name = Name} = El, EventName) -> Metrics = measure_element(Name, exml_query:attr(El, <<"type">>)), Measurements = Metrics#{element => El, lserver => LServer}, mongoose_instrument:execute(EventName, #{}, Measurements); @@ -595,14 +597,15 @@ match_labels([DL | DLabels], [PL | PLabels]) -> end. -spec stream_header(data()) -> exml_stream:start(). -stream_header(#s2s_data{lserver = LServer, streamid = Id, lang = Lang}) -> +stream_header(#s2s_data{myname = LServer, streamid = Id, lang = Lang}) -> Attrs = #{<<"xmlns:stream">> => ?NS_STREAM, <<"xmlns">> => ?NS_SERVER, <<"xmlns:db">> => ?NS_SERVER_DIALBACK, <<"version">> => ?XMPP_VERSION, + <<"xml:lang">> => Lang, <<"id">> => Id, - <<"from">> => LServer, - <<"xml:lang">> => Lang}, + <<"from">> => LServer + }, #xmlstreamstart{name = <<"stream:stream">>, attrs = Attrs}. -spec add_tls_elems(data(), boolean(), [exml:element()]) -> [exml:element()]. diff --git a/src/s2s/mongoose_s2s_info.erl b/src/s2s/mongoose_s2s_info.erl index cf832edeae..b00bbc7039 100644 --- a/src/s2s/mongoose_s2s_info.erl +++ b/src/s2s/mongoose_s2s_info.erl @@ -8,8 +8,7 @@ -include("mongoose_logger.hrl"). -type direction() :: in | out. --type supervisor_child_spec() :: { undefined, pid(), worker, [module()] }. --type connection_info() :: mongoose_s2s_in:connection_info() | ejabberd_s2s_out:connection_info(). +-type connection_info() :: mongoose_s2s_in:connection_info() | mongoose_s2s_out:connection_info(). %% @doc Get information about S2S connections of the specified type. -spec get_connections(direction()) -> [connection_info()]. @@ -19,11 +18,8 @@ get_connections(in) -> Pids = lists:flatten([ranch:procs(Ref, connections) || Ref <- Listeners]), [Conn || Pid <- Pids, Conn <- get_state_info(in, Pid)]; get_connections(out) -> - Specs = supervisor:which_children(ejabberd_s2s_out_sup), - [Conn || Spec <- Specs, Conn <- get_state_info(out, child_to_pid(Spec))]. - --spec child_to_pid(supervisor_child_spec()) -> pid(). -child_to_pid({_, Pid, _, _}) -> Pid. + Specs = supervisor:which_children(mongoose_s2s_out_sup), + [Conn || {_, Pid, _, _} <- Specs, Conn <- get_state_info(out, Pid)]. -spec get_state_info(direction(), pid()) -> [connection_info()]. get_state_info(in, Pid) when is_pid(Pid) -> @@ -35,7 +31,7 @@ get_state_info(in, Pid) when is_pid(Pid) -> [] end; get_state_info(out, Pid) when is_pid(Pid) -> - case gen_fsm_compat:sync_send_all_state_event(Pid, get_state_info) of + case mongoose_s2s_out:get_state_info(Pid) of Info when is_map(Info) -> [Info]; Other -> diff --git a/src/s2s/mongoose_s2s_lib.erl b/src/s2s/mongoose_s2s_lib.erl index 49f86cf5b4..1a09ddf035 100644 --- a/src/s2s/mongoose_s2s_lib.erl +++ b/src/s2s/mongoose_s2s_lib.erl @@ -6,7 +6,6 @@ %% (it depends on the hook handlers). -module(mongoose_s2s_lib). -export([make_from_to/2, - timeout/0, domain_utf8_to_ascii/1, check_shared_secret/2, choose_pid/2, @@ -27,17 +26,22 @@ make_from_to(#jid{lserver = FromServer}, #jid{lserver = ToServer}) -> {FromServer, ToServer}. -timeout() -> - 600000. - %% Converts a UTF-8 domain to ASCII (IDNA) --spec domain_utf8_to_ascii(jid:server()) -> jid:server() | false. -domain_utf8_to_ascii(Domain) -> +-spec domain_utf8_to_ascii(jid:lserver()) -> jid:lserver() | false; + (string()) -> string() | false. +domain_utf8_to_ascii(Domain) when is_binary(Domain) -> case catch idna:utf8_to_ascii(Domain) of {'EXIT', _} -> false; AsciiDomain -> list_to_binary(AsciiDomain) + end; +domain_utf8_to_ascii(Domain) when is_list(Domain) -> + case catch idna:utf8_to_ascii(Domain) of + {'EXIT', _} -> + false; + AsciiDomain -> + AsciiDomain end. -spec check_shared_secret(HostType, StoredSecretResult) -> ok | {update, NewSecret} when diff --git a/src/s2s/mongoose_s2s_out.erl b/src/s2s/mongoose_s2s_out.erl new file mode 100644 index 0000000000..ddd9c28d7f --- /dev/null +++ b/src/s2s/mongoose_s2s_out.erl @@ -0,0 +1,593 @@ +-module(mongoose_s2s_out). + +-behaviour(gen_statem). + +-include_lib("exml/include/exml_stream.hrl"). +-include("mongoose.hrl"). +-include("jlib.hrl"). + +-type features_before_auth() :: + #{sasl_external := boolean(), + starttls := false | optional | required, + is_ssl := boolean()}. +-type options() :: #{ + shaper := none, + max_stanza_size := non_neg_integer(), + max_retry_delay := pos_integer(), + state_timeout := timeout(), + stream_timeout := timeout(), + tls := just_tls:options()}. +-record(s2s_data, { + host_type :: mongooseim:host_type(), + myname :: jid:lserver(), + remote_server :: jid:lserver(), + socket :: mongoose_xmpp_socket:socket(), + parser :: exml_stream:parser(), + shaper :: mongoose_shaper:shaper(), + opts :: options(), %% This is very similar to listener opts + process_type :: process_type(), + streamid = mongoose_bin:gen_from_crypto() :: binary(), + remote_streamid = <<>> :: ejabberd_s2s:stream_id(), + dialback_enabled = true :: boolean() + }). +-type data() :: ejabberd_s2s:fromto() | #s2s_data{}. +-type maybe_ok() :: ok | {error, atom()}. +-type fsm_res() :: gen_statem:event_handler_result(state(), data()). +-type stream_state() :: stream_start | authenticated. +-type state() :: connect + | {wait_for_stream, stream_state()} + | {wait_for_features, stream_state()} + | wait_for_starttls_proceed + | wait_for_auth_result + | wait_for_session_establishment + | wait_for_validation + | stream_established + | bounce_and_disconnect. + +-type connection_info() :: + #{pid => pid(), + direction => out, + state_name => state(), + addr => unknown | inet:ip_address(), + port => unknown | inet:port_number(), + streamid => ejabberd_s2s:stream_id() | undefined, + shaper => mongoose_shaper:shaper(), + tls => boolean(), + tls_required => boolean(), + tls_enabled => boolean(), + tls_options => #{} | just_tls:options(), + authenticated => boolean(), + dialback_enabled => boolean(), + server => jid:lserver(), + myname => jid:lserver(), + process_type => process_type()}. + +-export_type([connection_info/0]). + +%% gen_statem callbacks +-export([callback_mode/0, init/1, handle_event/4, terminate/3]). + +-export([start_connection/2, + start_link/2, + stop_connection/2, + route/2, + get_state_info/1, + terminate_if_waiting_delay/1]). +-ignore_xref([start_link/2, stop_connection/2]). + +-type process_type() :: + new | + {verify, S2SIn :: pid(), Key :: ejabberd_s2s:s2s_dialback_key(), SID :: ejabberd_s2s:stream_id()}. +-type init_args() :: {ejabberd_s2s:fromto(), process_type()}. + +-spec start_connection(ejabberd_s2s:fromto(), process_type()) -> + supervisor:startchild_ret(). +start_connection(FromTo, PType) -> + supervisor:start_child(mongoose_s2s_out_sup, [FromTo, PType]). + +-spec start_link(ejabberd_s2s:fromto(), process_type()) -> gen_statem:start_ret(). +start_link(FromTo, PType) -> + gen_statem:start_link(?MODULE, {FromTo, PType}, []). + +-spec stop_connection(pid(), binary() | atom()) -> ok. +stop_connection(Pid, Reason) -> + gen_statem:cast(Pid, {exit, Reason}). + +-spec get_state_info(pid()) -> term(). +get_state_info(Pid) -> + gen_statem:call(Pid, get_state_info, 5000). + +-spec terminate_if_waiting_delay(ejabberd_s2s:fromto()) -> ok. +terminate_if_waiting_delay(FromTo) -> + [ gen_statem:cast(Pid, terminate_if_waiting_before_retry) + || Pid <- ejabberd_s2s:get_s2s_out_pids(FromTo) ], + ok. + +-spec route(pid(), mongoose_acc:t()) -> any(). +route(Pid, Acc) -> + Pid ! {route, Acc}. + +-spec callback_mode() -> gen_statem:callback_mode_result(). +callback_mode() -> + handle_event_function. + +-spec init(init_args()) -> gen_statem:init_result(state(), data()). +init({FromTo, new} = InitArgs) -> + case ejabberd_s2s:try_register(FromTo) of + true -> + process_flag(trap_exit, true), + ConnectEvent = {next_event, internal, {connect, InitArgs}}, + {ok, connect, FromTo, ConnectEvent}; + false -> + ignore + end; +init({FromTo, _Type} = InitArgs) -> + process_flag(trap_exit, true), + ConnectEvent = {next_event, internal, {connect, InitArgs}}, + {ok, connect, FromTo, ConnectEvent}. + +-spec handle_event(gen_statem:event_type(), term(), state(), data()) -> fsm_res(). +handle_event(internal, {connect, {{FromServer, ToServer}, PType}}, connect, _) + when is_binary(FromServer), is_binary(ToServer), + new =:= PType orelse is_tuple(PType) -> + {ok, HostType} = mongoose_domain_api:get_host_type(FromServer), + Opts = get_s2s_out_config(HostType), + #{shaper := ShaperName, max_stanza_size := MaxStanzaSize} = Opts, + {ok, Parser} = exml_stream:new_parser([{max_element_size, MaxStanzaSize}]), + Shaper = mongoose_shaper:new(ShaperName), + case open_socket(HostType, ToServer, Opts) of + {error, _Reason} -> + {keep_state_and_data, wait_before_retry_timeout(Opts)}; + Socket -> + Data = #s2s_data{host_type = HostType, + myname = FromServer, + remote_server = ToServer, + socket = Socket, + parser = Parser, + shaper = Shaper, + opts = Opts, + process_type = PType}, + send_xml(Data, stream_header(Data)), + {next_state, {wait_for_stream, stream_start}, Data, state_timeout(Data)} + end; +handle_event(internal, #xmlstreamstart{attrs = Attrs}, {wait_for_stream, StreamState}, Data) -> + handle_stream_start(Data, Attrs, StreamState); + +handle_event(internal, #xmlel{name = <<"stream:features">>} = El, + {wait_for_features, StreamState}, Data) -> + handle_features(Data, El, StreamState); +handle_event(internal, #xmlel{name = <<"success">>, attrs = #{<<"xmlns">> := ?NS_SASL}}, + wait_for_auth_result, Data) -> + NewData = Data#s2s_data{streamid = mongoose_bin:gen_from_crypto()}, + send_xml(NewData, stream_header(NewData)), + {next_state, {wait_for_stream, authenticated}, NewData, state_timeout(NewData)}; +handle_event(internal, #xmlel{name = <<"failure">>, attrs = #{<<"xmlns">> := ?NS_SASL}}, + wait_for_auth_result, _Data) -> + {stop, s2s_sasl_failure}; + +handle_event(internal, + #xmlel{name = <<"proceed">>, attrs = #{<<"xmlns">> := ?NS_TLS}} = El, + wait_for_starttls_proceed, + #s2s_data{socket = TcpSocket, parser = Parser, opts = Opts} = Data) -> + case mongoose_xmpp_socket:tcp_to_tls(TcpSocket, Opts, client) of + {ok, TlsSocket} -> + {ok, NewParser} = exml_stream:reset_parser(Parser), + NewData = Data#s2s_data{socket = TlsSocket, + parser = NewParser, + streamid = mongoose_bin:gen_from_crypto()}, + send_xml(NewData, stream_header(NewData)), + {next_state, {wait_for_stream, stream_start}, NewData, state_timeout(Opts)}; + {error, already_tls_connection} -> + ErrorStanza = mongoose_xmpp_errors:bad_request(?MYLANG, <<"bad_config">>), + send_xml(Data, jlib:make_error_reply(El, ErrorStanza)), + {stop, {shutdown, starttls_error}}; + {error, Reason} -> + {stop, {shutdown, Reason}} + end; + +handle_event(internal, #xmlel{name = <<"db:", _/binary>>} = El, wait_for_validation, + #s2s_data{myname = LServer, remote_server = RemoteServer, + socket = Socket, opts = Opts, process_type = new} = Data) -> + case mongoose_s2s_dialback:parse_validity(El) of + {step_4, {LServer, RemoteServer}, _StreamID, true} -> + case {mongoose_xmpp_socket:is_ssl(Socket), Opts} of + {false, #{tls := #{mode := starttls_required}}} -> + {stop, {shutdown, tls_required_but_unavailable}}; + _ -> + {next_state, stream_established, Data, stream_timeout(Data)} + end; + {step_4, {LServer, RemoteServer}, _StreamID, false} -> + {stop, {shutdown, invalid_dialback_key}}; + _ -> + {keep_state_and_data, state_timeout(Data)} + end; +handle_event(internal, #xmlel{name = <<"db:", _/binary>>} = El, wait_for_validation, + #s2s_data{myname = LServer, remote_server = RemoteServer, + process_type = {verify, VPid, _Key, _SID}} = Data) -> + case mongoose_s2s_dialback:parse_validity(El) of + {step_3, {LServer, RemoteServer} = FromTo, _StreamID, IsValid} -> + mongoose_s2s_in:send_validity_from_s2s_out(VPid, IsValid, FromTo), + {stop, normal}; + _ -> + {keep_state_and_data, state_timeout(Data)} + end; + +handle_event(internal, #xmlstreamend{}, _, Data) -> + send_xml(Data, ?XML_STREAM_TRAILER), + {stop, {shutdown, stream_end}}; +handle_event(internal, {disconnect, Reason}, _, _) -> + {stop, {shutdown, Reason}}; +handle_event(info, {route, Acc}, stream_established, Data) -> + send_xml(Data, mongoose_acc:element(Acc)), + {keep_state_and_data, stream_timeout(Data)}; +handle_event(info, {route, Acc}, bounce_and_disconnect, _) -> + E = mongoose_xmpp_errors:remote_server_not_found(?MYLANG, <<"From s2s (waiting)">>), + bounce_one(E, Acc), + keep_state_and_data; +handle_event(info, {route, _}, _, _) -> + {keep_state_and_data, postpone}; +handle_event(info, {Tag, _, Payload} = SocketData, _, Data) + when is_binary(Payload) andalso (Tag =:= tcp orelse Tag =:= ssl) -> + handle_socket_data(Data, SocketData); +handle_event(info, {Tag, _}, _, _) + when Tag =:= tcp_closed; Tag =:= ssl_closed -> + {stop, {shutdown, Tag}}; +handle_event(info, {Tag, _, Reason}, _, _) + when Tag =:= tcp_error; Tag =:= ssl_error -> + {stop, {shutdown, {Tag, Reason}}}; +handle_event(cast, terminate_if_waiting_before_retry, connect, _) -> + {stop, {shutdown, terminate_if_waiting_before_retry}}; +handle_event(cast, terminate_if_waiting_before_retry, _, _) -> + keep_state_and_data; +handle_event(cast, {exit, Reason}, _, {_, _}) -> + {stop, {shutdown, Reason}}; +handle_event(cast, {exit, system_shutdown}, _, #s2s_data{} = Data) -> + Error = mongoose_xmpp_errors:system_shutdown(), + send_xml(Data, Error), + send_xml(Data, ?XML_STREAM_TRAILER), + {stop, {shutdown, system_shutdown}}; +handle_event(cast, {exit, Reason}, _, #s2s_data{} = Data) + when is_binary(Reason) -> + StreamConflict = mongoose_xmpp_errors:stream_conflict(?MYLANG, Reason), + send_xml(Data, StreamConflict), + send_xml(Data, ?XML_STREAM_TRAILER), + {stop, {shutdown, Reason}}; +handle_event(cast, {stop, Reason}, _, _) -> + {stop, {shutdown, Reason}}; +handle_event({call, From}, get_state_info, State, Data) -> + Info = handle_get_state_info(Data, State), + {keep_state_and_data, [{reply, From, Info}]}; +handle_event(timeout, stream_timeout, _, _) -> + {stop, {shutdown, s2s_out_stream_timeout}}; +handle_event({timeout, activate_socket}, activate_socket, _, #s2s_data{socket = Socket}) -> + mongoose_xmpp_socket:activate(Socket), + keep_state_and_data; +handle_event({timeout, wait_before_retry}, wait_before_retry_timeout, connect, {From, To}) -> + ejabberd_s2s:remove_connection({From, To}, self()), + bounce_and_disconnect({From, To}); +handle_event(state_timeout, state_timeout_termination, bounce_and_disconnect, _) -> + {stop, {shutdown, state_timeout}}; +handle_event(state_timeout, state_timeout_termination, State, Data) -> + ?LOG_WARNING(#{what => s2s_state_timeout, state => State}), + send_xml(Data, mongoose_xmpp_errors:connection_timeout()), + send_xml(Data, ?XML_STREAM_TRAILER), + {stop, {shutdown, state_timeout}}; +handle_event(EventType, EventContent, State, Data) -> + ?LOG_WARNING(#{what => unknown_statem_event, + s2s_state => State, s2s_data => Data, + event_type => EventType, event_content => EventContent}), + keep_state_and_data. + +-spec terminate(term(), state(), data()) -> term(). +terminate(_, _, #s2s_data{myname = MyName, remote_server = ToServer, + parser = Parser, socket = Socket, process_type = new}) -> + ejabberd_s2s:remove_connection({MyName, ToServer}, self()), + bounce_messages(mongoose_xmpp_errors:remote_server_not_found(?MYLANG, <<"Bounced by s2s">>)), + exml_stream:free_parser(Parser), + mongoose_xmpp_socket:close(Socket); +terminate(_Reason, _State, #s2s_data{parser = Parser, socket = Socket}) -> + bounce_messages(mongoose_xmpp_errors:remote_server_not_found(?MYLANG, <<"Bounced by s2s">>)), + exml_stream:free_parser(Parser), + mongoose_xmpp_socket:close(Socket); +terminate(_Reason, _State, _FromTo) -> + bounce_messages(mongoose_xmpp_errors:remote_server_not_found(?MYLANG, <<"Bounced by s2s">>)). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% This change of state triggers processing again postponed events ({route, Acc}), +%% followed by a disconnection. +-spec bounce_and_disconnect(ejabberd_s2s:fromto()) -> fsm_res(). +bounce_and_disconnect({FromServer, _To} = Data) -> + {ok, HostType} = mongoose_domain_api:get_host_type(FromServer), + Opts = get_s2s_out_config(HostType), + {next_state, bounce_and_disconnect, Data, state_timeout(Opts)}. + +-spec open_socket(mongooseim:host_type(), jid:lserver(), options()) -> + mongoose_xmpp_socket:socket() | {error, atom() | inet:posix()}. +open_socket(HostType, ToServer, Opts) -> + Timeout = outgoing_s2s_timeout(HostType), + EnforceTls = is_tls_enforced(Opts), + AddrList = mongoose_addr_list:get_addr_list(HostType, ToServer, EnforceTls), + Fold = fun(Addr, {error, _}) -> + mongoose_xmpp_socket:connect(Addr, Opts, s2s, Timeout); + (_, Socket) -> + Socket + end, + case lists:foldl(Fold, {error, badarg}, AddrList) of + {error, Reason} -> + ?LOG_DEBUG(#{what => s2s_out_failed, address_list => AddrList, reason => Reason}), + {error, Reason}; + Socket -> + Socket + end. + +-spec is_tls_enforced(options()) -> boolean(). +is_tls_enforced(#{tls := #{mode := tls}}) -> + true; +is_tls_enforced(_) -> + false. + +-spec handle_stream_start(data(), exml:attrs(), stream_state()) -> fsm_res(). +handle_stream_start(#s2s_data{myname = MyName, remote_server = RemoteServer} = D0, + #{<<"xmlns">> := ?NS_SERVER, + <<"version">> := ?XMPP_VERSION, + <<"id">> := RemoteStreamID, + <<"from">> := From} = Attrs, + StreamState) -> + MaybeDialback = maps:get(<<"xmlns:db">>, Attrs, undefined), + case is_valid_from(From, RemoteServer) andalso + is_valid_to(Attrs, MyName) andalso + is_valid_dialback_ns(MaybeDialback) of + true -> + DialbackEnabled = ?NS_SERVER_DIALBACK =:= MaybeDialback, + D1 = D0#s2s_data{remote_streamid = RemoteStreamID, dialback_enabled = DialbackEnabled}, + {next_state, {wait_for_features, StreamState}, D1, state_timeout(D1)}; + false -> + send_xml(D0, mongoose_xmpp_errors:invalid_from()), + {stop, {shutdown, invalid_from_to}, D0} + end; +handle_stream_start(#s2s_data{} = Data, _, _) -> + send_xml(Data, mongoose_xmpp_errors:invalid_namespace()), + {stop, normal, Data}. + +-spec is_valid_from(jid:server(), jid:lserver()) -> boolean(). +is_valid_from(From, RemoteServer) -> + jid:nameprep(From) =:= RemoteServer. + +-spec is_valid_to(exml:attrs(), jid:lserver()) -> boolean(). +is_valid_to(Attrs, MyName) -> + case maps:get(<<"to">>, Attrs, true) of + true -> true; + To -> jid:nameprep(To) =:= MyName + end. + +-spec is_valid_dialback_ns(undefined | binary()) -> boolean(). +is_valid_dialback_ns(undefined) -> true; +is_valid_dialback_ns(?NS_SERVER_DIALBACK) -> true; +is_valid_dialback_ns(_) -> false. + +-spec handle_features(data(), exml:element(), stream_state()) -> fsm_res(). +handle_features(#s2s_data{socket = Socket} = Data, #xmlel{children = Children}, StreamState) -> + IsSsl = mongoose_xmpp_socket:is_ssl(Socket), + InitAcc = #{sasl_external => false, starttls => false, is_ssl => IsSsl}, + Input = lists:foldl(fun parse_auth_and_tls/2, InitAcc, Children), + handle_parsed_features(Data, Input, StreamState). + +-spec parse_auth_and_tls(exml:element(), features_before_auth()) -> features_before_auth(). +parse_auth_and_tls(#xmlel{name = <<"mechanisms">>, + attrs = #{<<"xmlns">> := ?NS_SASL}, + children = Els1}, Acc) -> + Pred = fun(Child) -> + #xmlel{name = <<"mechanism">>, + children = [#xmlcdata{content = <<"EXTERNAL">>}]} =:= Child + end, + Acc#{sasl_external => lists:any(Pred, Els1)}; +parse_auth_and_tls(#xmlel{name = <<"starttls">>, + attrs = #{<<"xmlns">> := ?NS_TLS}, + children = [#xmlcdata{content = <<"required">>}]}, Acc) -> + Acc#{starttls => required}; +parse_auth_and_tls(#xmlel{name = <<"starttls">>, + attrs = #{<<"xmlns">> := ?NS_TLS}}, Acc) -> + Acc#{starttls => optional}; +parse_auth_and_tls(_, Acc) -> + Acc. + +-spec handle_parsed_features(data(), features_before_auth(), stream_state()) -> fsm_res(). +handle_parsed_features(#s2s_data{opts = #{tls := _}} = Data, + #{sasl_external := _, starttls := required, is_ssl := false}, + _) -> + send_xml(Data, starttls()), + {next_state, wait_for_starttls_proceed, Data, state_timeout(Data)}; +handle_parsed_features(#s2s_data{opts = #{tls := _}} = Data, + #{sasl_external := _, starttls := optional, is_ssl := false}, + _) -> + send_xml(Data, starttls()), + {next_state, wait_for_starttls_proceed, Data, state_timeout(Data)}; +handle_parsed_features(#s2s_data{} = Data, + #{sasl_external := false, starttls := false, is_ssl := _}, + authenticated) -> + {next_state, stream_established, Data#s2s_data{}}; +handle_parsed_features(#s2s_data{process_type = new} = Data, + #{sasl_external := true, starttls := _, is_ssl := true}, + stream_start) -> + Elem = #xmlel{name = <<"auth">>, + attrs = #{<<"xmlns">> => ?NS_SASL, <<"mechanism">> => <<"EXTERNAL">>}, + children = [#xmlcdata{content = base64:encode(Data#s2s_data.myname)}]}, + send_xml(Data, Elem), + {next_state, wait_for_auth_result, Data, state_timeout(Data)}; +handle_parsed_features(#s2s_data{dialback_enabled = true} = Data, + #{sasl_external := _, starttls := _, is_ssl := _}, + _) -> + send_dialback_request(Data); +handle_parsed_features(#s2s_data{} = Data, + #{sasl_external := _, starttls := _, is_ssl := _}, + _) -> + {next_state, connect, Data, wait_before_retry_timeout(Data)}. + +-spec send_dialback_request(data()) -> fsm_res(). +send_dialback_request(#s2s_data{host_type = HostType, + myname = LServer, + remote_server = RemoteServer, + process_type = new} = Data) -> + FromTo = {LServer, RemoteServer}, + Key1 = ejabberd_s2s:key(HostType, FromTo, Data#s2s_data.remote_streamid), + %% Initiating server sends dialback key + send_xml(Data, mongoose_s2s_dialback:step_1(FromTo, Key1)), + {next_state, wait_for_validation, Data, state_timeout(Data)}; +send_dialback_request(#s2s_data{myname = LServer, + remote_server = RemoteServer, + process_type = {verify, _Pid, Key, SID}} = Data) -> + %% This is an outbound s2s connection created at step 1 in mongoose_s2s_in:handle_dialback/3 + %% Its purpose is just to verify the dialback procedure and can be closed afterwards. + send_xml(Data, mongoose_s2s_dialback:step_2({LServer, RemoteServer}, Key, SID)), + {next_state, wait_for_validation, Data, state_timeout(Data)}. + +-spec bounce_messages(exml:element()) -> ok. +bounce_messages(Error) -> + receive + {route, Acc} -> + bounce_one(Error, Acc), + bounce_messages(Error); + _ -> + bounce_messages(Error) + after 0 -> ok + end. + +%% @doc Bounce a single message (xmlel) +-spec bounce_one(exml:element(), mongoose_acc:t()) -> mongoose_acc:t(). +bounce_one(Error, Acc) -> + case mongoose_acc:stanza_type(Acc) of + <<"error">> -> Acc; + <<"result">> -> Acc; + _ -> + {From, To, Packet} = mongoose_acc:packet(Acc), + {Acc1, Err} = jlib:make_error_reply(Acc, Packet, Error), + ejabberd_router:route(To, From, Acc1, Err) + end. + +-spec send_xml(data(), exml_stream:element()) -> maybe_ok(). +send_xml(#s2s_data{myname = LServer, socket = Socket}, Elem) -> + mongoose_instrument:execute( + xmpp_element_size_out, labels(), + #{byte_size => exml:xml_size(Elem), lserver => LServer, pid => self(), module => ?MODULE}), + mongoose_xmpp_socket:send_xml(Socket, Elem). + +-spec handle_socket_data(data(), {tcp | ssl, _, binary()}) -> fsm_res(). +handle_socket_data(#s2s_data{socket = Socket} = Data, Payload) -> + case mongoose_xmpp_socket:handle_data(Socket, Payload) of + {error, _Reason} -> + {stop, {shutdown, socket_error}, Data}; + Packet when is_binary(Packet) -> + handle_socket_packet(Data, Packet) + end. + +-spec handle_socket_packet(data(), binary()) -> fsm_res(). +handle_socket_packet(#s2s_data{parser = Parser} = Data, Packet) -> + case exml_stream:parse(Parser, Packet) of + {error, Reason} -> + NextEvent = {next_event, internal, #xmlstreamerror{name = Reason}}, + {keep_state, Data, NextEvent}; + {ok, NewParser, XmlElements} -> + Size = iolist_size(Packet), + NewData = Data#s2s_data{parser = NewParser}, + handle_socket_elements(NewData, XmlElements, Size) + end. + +-spec handle_socket_elements(data(), [exml_stream:element()], non_neg_integer()) -> fsm_res(). +handle_socket_elements(#s2s_data{myname = LServer, shaper = Shaper} = Data, Elements, Size) -> + {NewShaper, Pause} = mongoose_shaper:update(Shaper, Size), + [mongoose_instrument:execute( + xmpp_element_size_in, labels(), + #{byte_size => exml:xml_size(Elem), lserver => LServer, pid => self(), module => ?MODULE}) + || Elem <- Elements], + NewData = Data#s2s_data{shaper = NewShaper}, + StreamEvents0 = [ {next_event, internal, XmlEl} || XmlEl <- Elements ], + StreamEvents1 = maybe_add_pause(NewData, StreamEvents0, Pause), + {keep_state, NewData, StreamEvents1}. + +-spec maybe_add_pause(data(), [gen_statem:action()], integer()) -> [gen_statem:action()]. +maybe_add_pause(_, StreamEvents, Pause) when Pause > 0 -> + [{{timeout, activate_socket}, Pause, activate_socket} | StreamEvents]; +maybe_add_pause(#s2s_data{socket = Socket}, StreamEvents, _) -> + mongoose_xmpp_socket:activate(Socket), + StreamEvents. + +%% This will trigger if after a while the gen_statem state has not changed. +%% Should be enabled for all transitions except into `stream_established`. +-spec state_timeout(data() | options()) -> {state_timeout, timeout(), state_timeout_termination}. +state_timeout(#s2s_data{opts = Opts}) -> + state_timeout(Opts); +state_timeout(#{state_timeout := Timeout}) -> + {state_timeout, Timeout, state_timeout_termination}. + +%% This will trigger _only_ during `stream_established`, to garbage-collect unused connections. +-spec stream_timeout(data() | options()) -> {timeout, timeout(), stream_timeout}. +stream_timeout(#s2s_data{opts = Opts}) -> + stream_timeout(Opts); +stream_timeout(#{stream_timeout := Timeout}) -> + {timeout, Timeout, stream_timeout}. + +%% Sockets failed to connect entirely +%% The initial delay is random between 1 and `max_retry_delay` seconds +-spec wait_before_retry_timeout(data() | options()) -> + {{timeout, wait_before_retry}, timeout(), wait_before_retry_timeout}. +wait_before_retry_timeout(#s2s_data{opts = Opts}) -> + wait_before_retry_timeout(Opts); +wait_before_retry_timeout(#{max_retry_delay := MaxRetryDelay}) -> + Delay = 999 + rand:uniform(timer:seconds(MaxRetryDelay) - 999), + {{timeout, wait_before_retry}, min(Delay, MaxRetryDelay), wait_before_retry_timeout}. + +-spec handle_get_state_info(data(), state()) -> connection_info(). +handle_get_state_info(#s2s_data{socket = Socket, opts = Opts} = Data, State) -> + {Addr, Port} = mongoose_xmpp_socket:get_ip(Socket), + #{pid => self(), + direction => out, + state_name => State, + addr => Addr, + port => Port, + streamid => Data#s2s_data.streamid, + tls => maps:is_key(tls, Opts), + tls_enabled => mongoose_xmpp_socket:is_ssl(Socket), + tls_options => maps:get(tls, Opts, #{}), + authenticated => stream_established =:= State, + shaper => Data#s2s_data.shaper, + dialback_enabled => Data#s2s_data.dialback_enabled, + server => Data#s2s_data.remote_server, + myname => Data#s2s_data.myname, + process_type => Data#s2s_data.process_type}. + +-spec labels() -> mongoose_instrument:labels(). +labels() -> + #{connection_type => s2s}. + +-spec outgoing_s2s_timeout(mongooseim:host_type()) -> non_neg_integer() | infinity. +outgoing_s2s_timeout(HostType) -> + mongoose_config:get_opt([{s2s, HostType}, outgoing, connection_timeout]). + +-spec get_s2s_out_config(mongooseim:host_type()) -> options(). +get_s2s_out_config(HostType) -> + mongoose_config:get_opt([{s2s, HostType}]). + +-spec stream_header(data()) -> exml_stream:start(). +stream_header(#s2s_data{myname = LServer, remote_server = Remote, streamid = Id, socket = Socket}) -> + Attrs = #{<<"xmlns:stream">> => ?NS_STREAM, + <<"xmlns">> => ?NS_SERVER, + <<"xmlns:db">> => ?NS_SERVER_DIALBACK, + <<"version">> => ?XMPP_VERSION, + <<"xml:lang">> => ?MYLANG, + <<"to">> => Remote, + <<"id">> => Id}, + %% RFC6120 ยง4.7.1: + %% Because a server is a "public entity" on the XMPP network, it MUST include + %% the 'from' attribute after the confidentiality and integrity of the stream + %% are protected via TLS or an equivalent security layer. + case mongoose_xmpp_socket:is_ssl(Socket) of + true -> + #xmlstreamstart{name = <<"stream:stream">>, attrs = Attrs#{<<"from">> => LServer}}; + false -> + #xmlstreamstart{name = <<"stream:stream">>, attrs = Attrs} + end. + +-spec starttls() -> exml:element(). +starttls() -> + #xmlel{name = <<"starttls">>, attrs = #{<<"xmlns">> => ?NS_TLS}}. diff --git a/src/s2s/mongoose_s2s_socket_out.erl b/src/s2s/mongoose_s2s_socket_out.erl deleted file mode 100644 index 674bfba625..0000000000 --- a/src/s2s/mongoose_s2s_socket_out.erl +++ /dev/null @@ -1,386 +0,0 @@ --module(mongoose_s2s_socket_out). --author('piotr.nosek@erlang-solutions.com'). - --include("mongoose.hrl"). --include("jlib.hrl"). - --behaviour(gen_server). - -%%---------------------------------------------------------------------- -%% Types -%%---------------------------------------------------------------------- - --type stanza_size() :: pos_integer() | infinity. --type connection_type() :: s2s | undefined. - --type options() :: #{max_stanza_size := stanza_size(), - hibernate_after := non_neg_integer(), - connection_type := connection_type(), - atom() => any()}. - --export_type([socket_data/0]). - --type socket_module() :: gen_tcp | ssl. --type socket() :: gen_tcp:socket() | ssl:sslsocket(). - --record(socket_data, {sockmod = gen_tcp :: socket_module(), - socket :: term(), - receiver :: pid(), - connection_type :: connection_type(), - connection_details :: mongoose_listener:connection_details() - }). - --type socket_data() :: #socket_data{}. - --record(state, {socket :: socket(), - sockmod = gen_tcp :: socket_module(), - shaper_state :: mongoose_shaper:shaper(), - dest_pid :: undefined | pid(), %% gen_fsm_compat pid - max_stanza_size :: stanza_size(), - parser :: exml_stream:parser(), - connection_type :: connection_type(), - hibernate_after = 0 :: non_neg_integer()}). --type state() :: #state{}. - -%% transport API --export([connect/5, close/1, send_text/2, send_element/2]). --export([connect_tls/2, peername/1, get_all_trasport_processes/0]). - -%% gen_server API --export([start_link/3, init/1, terminate/2, - handle_cast/2, handle_call/3, handle_info/2]). - --ignore_xref([start_link/3, get_all_trasport_processes/0]). - -%%---------------------------------------------------------------------- -%% Transport API -%%---------------------------------------------------------------------- - --spec connect(ConnectionType :: connection_type(), - Addr :: atom() | string() | inet:ip_address(), - Port :: inet:port_number(), - Opts :: [gen_tcp:connect_option()], - Timeout :: non_neg_integer() | infinity - ) -> {'error', atom()} | {'ok', socket_data()}. -connect(ConnectionType, Addr, Port, Opts, Timeout) -> - case gen_tcp:connect(Addr, Port, Opts, Timeout) of - {ok, Socket} -> - %% Receiver options are configurable only for listeners - %% It might make sense to make them configurable for - %% outgoing s2s connections as well - ReceiverOpts = #{max_stanza_size => infinity, - hibernate_after => 0, - connection_type => ConnectionType}, - Receiver = start_child(Socket, none, ReceiverOpts), - {SrcAddr, SrcPort} = case inet:sockname(Socket) of - {ok, {A, P}} -> {A, P}; - {error, _} -> {unknown, unknown} - end, - ConnectionDetails = #{dest_address => Addr, dest_port => Port, proxy => false, - src_address => SrcAddr, src_port => SrcPort}, - SocketData = #socket_data{sockmod = gen_tcp, - socket = Socket, - receiver = Receiver, - connection_type = ConnectionType, - connection_details = ConnectionDetails}, - DestPid = self(), - case gen_tcp:controlling_process(Socket, Receiver) of - ok -> - set_dest_pid(Receiver, DestPid), - {ok, SocketData}; - {error, _Reason} = Error -> - gen_tcp:close(Socket), - Error - end; - {error, _Reason} = Error -> - Error - end. - --spec close(socket_data()) -> ok. -close(#socket_data{receiver = Receiver}) -> - gen_server:cast(Receiver, close). - --spec connect_tls(socket_data(), just_tls:options()) -> socket_data(). -connect_tls(#socket_data{receiver = Receiver} = SocketData, TLSOpts) -> - tcp_to_tls(Receiver, TLSOpts), - update_socket(SocketData). - --spec send_text(socket_data(), binary()) -> ok. -send_text(SocketData, Data) -> - #socket_data{sockmod = SockMod, socket = Socket, - connection_type = s2s} = SocketData, - mongoose_instrument:execute(xmpp_element_size_out, - #{connection_type => s2s}, - #{t => out, el => Data, byte_size => byte_size(Data)}), - case catch SockMod:send(Socket, Data) of - ok -> - update_transport_metrics(Data, - #{sockmod => SockMod, direction => out}), - ok; - {error, timeout} -> - ?LOG_INFO(#{what => socket_error, reason => timeout, - socket => SockMod}), - exit(normal); - Error -> - ?LOG_INFO(#{what => socket_error, reason => Error, - socket => SockMod}), - exit(normal) - end. - - --spec send_element(socket_data(), exml:element()) -> ok. -send_element(#socket_data{connection_type = s2s} = SocketData, El) -> - BinEl = exml:to_binary(El), - send_text(SocketData, BinEl). - --spec peername(socket_data()) -> mongoose_transport:peername_return(). -peername(#socket_data{connection_details = #{src_address := SrcAddr, - src_port := SrcPort}}) -> - {ok, {SrcAddr, SrcPort}}. - -get_all_trasport_processes() -> - Connections = supervisor:which_children(mongoose_s2s_socket_out_sup), - get_transport_info(Connections). -%%---------------------------------------------------------------------- -%% gen_server interfaces -%%---------------------------------------------------------------------- --spec start_link(port(), atom(), options()) -> - ignore | {error, _} | {ok, pid()}. -start_link(Socket, Shaper, Opts) -> - gen_server:start_link(?MODULE, {Socket, Shaper, Opts}, []). - -init({Socket, Shaper, Opts}) -> - ShaperState = mongoose_shaper:new(Shaper), - #{max_stanza_size := MaxStanzaSize, - hibernate_after := HibernateAfter, - connection_type := ConnectionType} = Opts, - Parser = new_parser(MaxStanzaSize), - {ok, #state{socket = Socket, - shaper_state = ShaperState, - max_stanza_size = MaxStanzaSize, - connection_type = ConnectionType, - parser = Parser, - hibernate_after = HibernateAfter}}. - -handle_call({tcp_to_tls, TLSOpts}, _From, #state{socket = TCPSocket} = State0) -> - case just_tls:tcp_to_tls(TCPSocket, TLSOpts, client) of - {ok, TLSSocket} -> - State1 = reset_parser(State0), - State2 = State1#state{socket = TLSSocket, sockmod = ssl}, - activate_socket(State2), - {reply, ok, State2, hibernate_or_timeout(State2)}; - {error, Reason} -> - ?LOG_WARNING(#{what => tcp_to_tls_failed, reason => Reason, - dest_pid => State0#state.dest_pid}), - {stop, normal, State0} - end; -handle_call(get_socket, _From, #state{socket = Socket} = State) -> - {reply, {ok, Socket}, State, hibernate_or_timeout(State)}; -handle_call({set_dest_pid, DestPid}, _From, #state{dest_pid = undefined} = State) -> - StateAfterReset = reset_parser(State), - NewState = StateAfterReset#state{dest_pid = DestPid}, - activate_socket(NewState), - {reply, ok, NewState, hibernate_or_timeout(NewState)}; -handle_call(_Request, _From, State) -> - {reply, ok, State, hibernate_or_timeout(State)}. - -handle_cast(close, State) -> - {stop, normal, State}; -handle_cast(_Msg, State) -> - {noreply, State, hibernate_or_timeout(State)}. - -handle_info({tcp, _TCPSocket, Data}, #state{sockmod = gen_tcp} = State) -> - NewState = process_data(Data, State), - {noreply, NewState, hibernate_or_timeout(NewState)}; -handle_info({ssl, _TCPSocket, Data}, #state{sockmod = ssl} = State) -> - NewState = process_data(Data, State), - {noreply, NewState, hibernate_or_timeout(NewState)}; -handle_info({Tag, _Socket}, State) when Tag == tcp_closed; Tag == ssl_closed -> - {stop, {shutdown, Tag}, State}; -handle_info({Tag, _Socket, Reason}, State) when Tag == tcp_error; Tag == ssl_error -> - {stop, {shutdown, Tag, Reason}, State}; -handle_info({timeout, _Ref, activate}, State) -> - activate_socket(State), - {noreply, State, hibernate_or_timeout(State)}; -handle_info(timeout, State) -> - {noreply, State, hibernate()}; -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info), - {noreply, State, hibernate_or_timeout(State)}. - -terminate(_Reason, #state{parser = Parser, dest_pid = DestPid, - socket = Socket, sockmod = SockMod}) -> - free_parser(Parser), - case DestPid of - undefined -> ok; - _ -> gen_fsm_compat:send_event(DestPid, closed) - end, - catch shutdown_socket_and_wait_for_peer_to_close(Socket, SockMod), - ok. - -%%---------------------------------------------------------------------- -%% local API helpers -%%---------------------------------------------------------------------- --spec start_child(port(), atom(), options()) -> pid(). -start_child(Socket, Shaper, Opts) -> - {ok, Receiver} = supervisor:start_child(mongoose_s2s_socket_out_sup, - [Socket, Shaper, Opts]), - Receiver. - -get_transport_info(ConnectionList) when is_list(ConnectionList) -> - [get_transport_info(Pid) || {_, Pid, _, _} <- ConnectionList, is_pid(Pid)]; -get_transport_info(TransportPid) when is_pid(TransportPid) -> - State = sys:get_state(TransportPid), - maps:from_list(lists:zip(record_info(fields, state),tl(tuple_to_list(State)))). - --spec tcp_to_tls(pid(), just_tls:options()) -> ok | {error, any()}. -tcp_to_tls(Receiver, TLSOpts) -> - gen_server_call_or_noproc(Receiver, {tcp_to_tls, TLSOpts}). - --spec update_socket(socket_data()) -> socket_data(). -update_socket(#socket_data{receiver = Receiver} = SocketData) -> - case gen_server_call_or_noproc(Receiver, get_socket) of - {ok, TLSSocket} -> - SocketData#socket_data{socket = TLSSocket, sockmod = ssl}; - {error, E} -> - exit({invalid_socket_after_upgrade_to_tls, E}) - end. - --spec set_dest_pid(pid(), pid()) -> ok | {error, any()}. -set_dest_pid(Receiver, DestPid) -> - gen_server:call(Receiver, {set_dest_pid, DestPid}). - --spec gen_server_call_or_noproc(pid(), any()) -> Ret :: any() | {error, any()}. -gen_server_call_or_noproc(Pid, Message) -> - try - gen_server:call(Pid, Message) - catch - exit:{noproc, Extra} -> - {error, {noproc, Extra}}; - exit:{normal, Extra} -> - % reciver exited with normal status after the gen_server call was sent - % but before it was processed - {error, {died, Extra}} - end. - -%%-------------------------------------------------------------------- -%% internal functions -%%-------------------------------------------------------------------- - --spec activate_socket(state()) -> 'ok' | {'tcp_closed', _}. -activate_socket(#state{socket = Socket, sockmod = gen_tcp}) -> - inet:setopts(Socket, [{active, once}]), - PeerName = inet:peername(Socket), - resolve_peername(PeerName, Socket); -activate_socket(#state{socket = Socket, sockmod = ssl}) -> - ssl:setopts(Socket, [{active, once}]), - PeerName = ssl:peername(Socket), - resolve_peername(PeerName, Socket). - -resolve_peername({ok, _}, _Socket) -> - ok; -resolve_peername({error, _Reason}, Socket) -> - self() ! {tcp_closed, Socket}. - --spec process_data(binary(), state()) -> state(). -process_data(Data, #state{parser = Parser, - shaper_state = ShaperState, - dest_pid = DestPid, - sockmod = SockMod, - connection_type = ConnectionType} = State) -> - ?LOG_DEBUG(#{what => received_xml_on_stream, packet => Data, dest_pid => DestPid}), - Size = byte_size(Data), - {Events, NewParser} = - case exml_stream:parse(Parser, Data) of - {ok, NParser, Elems} -> - {[wrap_xml_elements_and_update_metrics(E, ConnectionType) || E <- Elems], NParser}; - {error, Reason} -> - {[#xmlstreamerror{name = Reason}], Parser} - end, - {NewShaperState, Pause} = mongoose_shaper:update(ShaperState, Size), - update_transport_metrics(Data, #{sockmod => SockMod, direction => in}), - [gen_fsm_compat:send_event(DestPid, Event) || Event <- Events], - maybe_pause(Pause, State), - State#state{parser = NewParser, shaper_state = NewShaperState}. - -wrap_xml_elements_and_update_metrics(El, s2s) -> - mongoose_instrument:execute(xmpp_element_size_in, - #{connection_type => s2s}, - #{t => out, el => El, byte_size => exml:xml_size(El)}), - wrap_xml(El). - -wrap_xml(#xmlel{} = E) -> - {xmlstreamelement, E}; -wrap_xml(E) -> - E. - --spec update_transport_metrics(binary(), - #{direction := in | out, - sockmod := socket_module()}) -> ok. -update_transport_metrics(Data, #{direction := in, sockmod := gen_tcp}) -> - mongoose_instrument:execute(tcp_data_in, #{connection_type => s2s}, #{byte_size => byte_size(Data)}); -update_transport_metrics(Data, #{direction := in, sockmod := ssl}) -> - mongoose_instrument:execute(tls_data_in, #{connection_type => s2s}, #{byte_size => byte_size(Data)}); -update_transport_metrics(Data, #{direction := out, sockmod := gen_tcp}) -> - mongoose_instrument:execute(tcp_data_out, #{connection_type => s2s}, #{byte_size => byte_size(Data)}); -update_transport_metrics(Data, #{direction := out, sockmod := ssl}) -> - mongoose_instrument:execute(tls_data_out, #{connection_type => s2s}, #{byte_size => byte_size(Data)}). - --spec maybe_pause(Delay :: non_neg_integer(), state()) -> any(). -maybe_pause(_, #state{dest_pid = undefined}) -> - ok; -maybe_pause(Pause, _State) when Pause > 0 -> - erlang:start_timer(Pause, self(), activate); -maybe_pause(_, State) -> - activate_socket(State). - --spec new_parser(stanza_size()) -> exml_stream:parser(). -new_parser(MaxStanzaSize) -> - MaxSize = case MaxStanzaSize of - infinity -> 0; - _ -> MaxStanzaSize - end, - {ok, NewParser} = exml_stream:new_parser([{max_element_size, MaxSize}]), - NewParser. - --spec reset_parser(state()) -> state(). -reset_parser(#state{parser = Parser} = State) -> - {ok, NewParser} = exml_stream:reset_parser(Parser), - State#state{parser = NewParser}. - --spec free_parser(exml_stream:parser()) -> ok. -free_parser(Parser) -> - exml_stream:free_parser(Parser). - --spec hibernate() -> hibernate | infinity. -hibernate() -> - case process_info(self(), message_queue_len) of - {_, 0} -> hibernate; - _ -> infinity - end. - --spec hibernate_or_timeout(state()) -> hibernate | infinity | pos_integer(). -hibernate_or_timeout(#state{hibernate_after = 0}) -> hibernate(); -hibernate_or_timeout(#state{hibernate_after = HA}) -> HA. - --spec shutdown_socket_and_wait_for_peer_to_close(socket(), socket_module() ) -> ok. -%% gen_tcp:close/2, but trying to ensure that all data is received by peer. -%% -%% This is based on tls_connection:workaround_transport_delivery_problems/2 code -%% https://github.com/erlang/otp/blob/OTP_17.0-rc2/lib/ssl/src/tls_connection.erl#L959 -%% -%% There are some more docs why we need it in http://erlang.org/doc/man/gen_tcp.html#close-1 -shutdown_socket_and_wait_for_peer_to_close(Socket, gen_tcp) -> - %% Standard trick to try to make sure all - %% data sent to the tcp port is really delivered to the - %% peer application before tcp port is closed so that the peer will - %% get the correct stream end and not only a transport close. - inet:setopts(Socket, [{active, false}]), - gen_tcp:shutdown(Socket, write), - %% Will return when other side has closed or after 30 s - %% e.g. we do not want to hang if something goes wrong - %% with the network but we want to maximise the odds that - %% peer application gets all data sent on the tcp connection. - gen_tcp:recv(Socket, 0, 30000); -shutdown_socket_and_wait_for_peer_to_close(Socket, ssl) -> - ssl:close(Socket). diff --git a/src/stats_api.erl b/src/stats_api.erl index 5ed2f8c157..f30087544b 100644 --- a/src/stats_api.erl +++ b/src/stats_api.erl @@ -13,7 +13,7 @@ incoming_s2s_number() -> -spec outgoing_s2s_number() -> {ok, non_neg_integer()}. outgoing_s2s_number() -> - {ok, length(supervisor:which_children(ejabberd_s2s_out_sup))}. + {ok, length(supervisor:which_children(mongoose_s2s_out_sup))}. -spec stats(binary()) -> {ok, integer()} | {not_found, string()}. stats(<<"uptimeseconds">>) -> From b795fcec25d4e4fad5625668c3837a9a6e86d773 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 11 Feb 2025 12:08:57 +0100 Subject: [PATCH 12/15] Return more explicit errors and failures on WS and BOSH tls callbacks --- src/mod_bosh_socket.erl | 6 +++--- src/mod_websockets.erl | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mod_bosh_socket.erl b/src/mod_bosh_socket.erl index 491a74a4fc..c3abc791ae 100644 --- a/src/mod_bosh_socket.erl +++ b/src/mod_bosh_socket.erl @@ -1062,8 +1062,8 @@ peername(#bosh_socket{peer = Peer}) -> -spec tcp_to_tls(mod_bosh:socket(), mongoose_listener:options(), mongoose_xmpp_socket:side()) -> {ok, mod_bosh:socket()} | {error, term()}. -tcp_to_tls(_Socket, _LOpts, _Mode) -> - {error, tls_not_allowed_on_bosh}. +tcp_to_tls(_Socket, _LOpts, server) -> + {error, tcp_to_tls_not_allowed_on_bosh}. -spec handle_data(mod_bosh:socket(), {tcp | ssl, term(), iodata()}) -> iodata() | {raw, [exml:element()]} | {error, term()}. @@ -1107,7 +1107,7 @@ is_channel_binding_supported(_Socket) -> -spec export_key_materials(mod_bosh:socket(), _, _, _, _) -> {error, atom()}. export_key_materials(_Socket, _, _, _, _) -> - {error, tls_not_allowed_on_bosh}. + {error, export_key_materials_not_allowed_on_bosh}. -spec is_ssl(mod_bosh:socket()) -> boolean(). is_ssl(_Socket) -> diff --git a/src/mod_websockets.erl b/src/mod_websockets.erl index b40c85be7e..97202c7882 100644 --- a/src/mod_websockets.erl +++ b/src/mod_websockets.erl @@ -378,8 +378,8 @@ peername(#websocket{peername = PeerName}) -> -spec tcp_to_tls(socket(), mongoose_listener:options(), mongoose_xmpp_socket:side()) -> {ok, socket()} | {error, term()}. -tcp_to_tls(_Socket, _LOpts, _Mode) -> - {error, tls_not_allowed_on_websockets}. +tcp_to_tls(_Socket, _LOpts, server) -> + {error, tcp_to_tls_not_supported_for_websockets}. -spec handle_data(socket(), {tcp | ssl, term(), term()}) -> iodata() | {raw, [exml:element()]} | {error, term()}. @@ -429,7 +429,7 @@ is_channel_binding_supported(_Socket) -> ConsumeSecret :: boolean(), ExportKeyMaterials :: binary() | [binary()]. export_key_materials(_Socket, _, _, _, _) -> - {error, tls_not_allowed_on_websockets}. + {error, export_key_materials_not_supported_for_websockets}. -spec is_ssl(socket()) -> boolean(). is_ssl(_Socket) -> From b2b92c969cf9c89b9a91d03f0e2b2a6f41affbb0 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 11 Feb 2025 12:10:35 +0100 Subject: [PATCH 13/15] domain_utf8_to_ascii return type based on an input parameter instead of adhoc --- src/cert_utils.erl | 4 ++-- src/mongoose_addr_list.erl | 2 +- src/s2s/mongoose_s2s_in.erl | 2 +- src/s2s/mongoose_s2s_lib.erl | 22 +++++++++++----------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cert_utils.erl b/src/cert_utils.erl index bc9724ba08..e7512b4b28 100644 --- a/src/cert_utils.erl +++ b/src/cert_utils.erl @@ -92,12 +92,12 @@ convert_to_bin(Val) when is_list(Val) -> convert_to_bin(Val) -> Val. --spec get_lserver_from_addr(bitstring() | string(), boolean()) -> [bitstring()]. +-spec get_lserver_from_addr(bitstring() | string(), boolean()) -> [binary()]. get_lserver_from_addr(V, UTF8) when is_binary(V); is_list(V) -> Val = convert_to_bin(V), case {jid:from_binary(Val), UTF8} of {#jid{luser = <<"">>, lserver = LD, lresource = <<"">>}, true} -> - case mongoose_s2s_lib:domain_utf8_to_ascii(LD) of + case mongoose_s2s_lib:domain_utf8_to_ascii(LD, binary) of false -> []; PCLD -> [PCLD] end; diff --git a/src/mongoose_addr_list.erl b/src/mongoose_addr_list.erl index 78a01caad0..3b8fb6791e 100644 --- a/src/mongoose_addr_list.erl +++ b/src/mongoose_addr_list.erl @@ -67,7 +67,7 @@ get_predefined_addresses(HostType, LServer, EnforceTls) -> Domain :: hostname(), EnforceTls :: with_tls()) -> [addr()]. lookup_services(HostType, Domain, EnforceTls) -> - case mongoose_s2s_lib:domain_utf8_to_ascii(Domain) of + case mongoose_s2s_lib:domain_utf8_to_ascii(Domain, string) of false -> []; ASCIIAddr -> do_lookup_services(HostType, ASCIIAddr, EnforceTls) end. diff --git a/src/s2s/mongoose_s2s_in.erl b/src/s2s/mongoose_s2s_in.erl index ecb4d383b9..736c9afadd 100644 --- a/src/s2s/mongoose_s2s_in.erl +++ b/src/s2s/mongoose_s2s_in.erl @@ -552,7 +552,7 @@ measure_element(_Name, _Type) -> check_auth_domain(error, _) -> false; check_auth_domain(AuthDomain, {ok, Cert}) -> - case mongoose_s2s_lib:domain_utf8_to_ascii(AuthDomain) of + case mongoose_s2s_lib:domain_utf8_to_ascii(AuthDomain, binary) of false -> false; PCAuthDomain -> diff --git a/src/s2s/mongoose_s2s_lib.erl b/src/s2s/mongoose_s2s_lib.erl index 1a09ddf035..c2de5f1090 100644 --- a/src/s2s/mongoose_s2s_lib.erl +++ b/src/s2s/mongoose_s2s_lib.erl @@ -6,7 +6,7 @@ %% (it depends on the hook handlers). -module(mongoose_s2s_lib). -export([make_from_to/2, - domain_utf8_to_ascii/1, + domain_utf8_to_ascii/2, check_shared_secret/2, choose_pid/2, need_more_connections/2, @@ -27,16 +27,16 @@ make_from_to(#jid{lserver = FromServer}, #jid{lserver = ToServer}) -> {FromServer, ToServer}. %% Converts a UTF-8 domain to ASCII (IDNA) --spec domain_utf8_to_ascii(jid:lserver()) -> jid:lserver() | false; - (string()) -> string() | false. -domain_utf8_to_ascii(Domain) when is_binary(Domain) -> - case catch idna:utf8_to_ascii(Domain) of - {'EXIT', _} -> - false; - AsciiDomain -> - list_to_binary(AsciiDomain) - end; -domain_utf8_to_ascii(Domain) when is_list(Domain) -> +-spec domain_utf8_to_ascii(string() | jid:lserver(), binary) -> jid:lserver() | false; + (string() | jid:lserver(), string) -> string() | false. +domain_utf8_to_ascii(Domain, binary) -> + Result = domain_utf8_to_ascii(Domain), + false =/= Result andalso list_to_binary(Result); +domain_utf8_to_ascii(Domain, string) -> + domain_utf8_to_ascii(Domain). + +-spec domain_utf8_to_ascii(string() | jid:lserver()) -> string() | false. +domain_utf8_to_ascii(Domain) -> case catch idna:utf8_to_ascii(Domain) of {'EXIT', _} -> false; From 0a0659b2c7edbc137c1e8df7f9750044ae35186e Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 11 Feb 2025 12:10:57 +0100 Subject: [PATCH 14/15] Unify tcp_to_tls client and server clauses --- src/listeners/mongoose_xmpp_socket.erl | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/listeners/mongoose_xmpp_socket.erl b/src/listeners/mongoose_xmpp_socket.erl index 99680af904..193d5341b1 100644 --- a/src/listeners/mongoose_xmpp_socket.erl +++ b/src/listeners/mongoose_xmpp_socket.erl @@ -149,24 +149,16 @@ activate(#xmpp_socket{module = Module, state = State}) -> -spec tcp_to_tls(socket(), with_tls_opts(), side()) -> {ok, socket()} | {error, term()}. tcp_to_tls(#ranch_tcp{socket = TcpSocket, connection_type = Type, ranch_ref = Ref, ip = Ip}, - #{tls := TlsConfig}, client) -> + #{tls := TlsConfig}, Side) -> inet:setopts(TcpSocket, [{active, false}]), - SslOpts = just_tls:make_client_opts(TlsConfig), - Ret = ssl:connect(TcpSocket, SslOpts, 5000), - VerifyResults = just_tls:receive_verify_results(), - case Ret of - {ok, SslSocket} -> - ssl:setopts(SslSocket, [{active, once}]), - {ok, #ranch_ssl{socket = SslSocket, connection_type = Type, - ranch_ref = Ref, ip = Ip, verify_results = VerifyResults}}; - {error, Reason} -> - {error, Reason} - end; -tcp_to_tls(#ranch_tcp{socket = TcpSocket, connection_type = Type, ranch_ref = Ref, ip = Ip}, - #{tls := TlsConfig}, server) -> - inet:setopts(TcpSocket, [{active, false}]), - SslOpts = just_tls:make_server_opts(TlsConfig), - Ret = ssl:handshake(TcpSocket, SslOpts, 5000), + Ret = case Side of + server -> + SslOpts = just_tls:make_server_opts(TlsConfig), + ssl:handshake(TcpSocket, SslOpts, 5000); + client -> + SslOpts = just_tls:make_client_opts(TlsConfig), + ssl:connect(TcpSocket, SslOpts, 5000) + end, VerifyResults = just_tls:receive_verify_results(), case Ret of {ok, SslSocket} -> From e70c80ac013e24f0c951c9279a242d0e7fa3e231 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 11 Feb 2025 12:11:52 +0100 Subject: [PATCH 15/15] Fix small boolean and arithmetic logic in some new functions --- src/mongoose_addr_list.erl | 4 +++- src/s2s/mongoose_s2s_out.erl | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mongoose_addr_list.erl b/src/mongoose_addr_list.erl index 3b8fb6791e..f1f13d7719 100644 --- a/src/mongoose_addr_list.erl +++ b/src/mongoose_addr_list.erl @@ -142,7 +142,9 @@ order_and_prepare_addrs(HostType, TlsTaggedSrvAddrLists) -> %% Larger weights SHOULD be given a proportionately higher probability of being selected. -spec order_by_priority_and_weight([srv_tls()]) -> [srv_tls()]. order_by_priority_and_weight(TlsTaggedSrvAddrLists) -> - Fun = fun({P1, W1, _, _, _}, {P2, W2, _, _, _}) -> P1 =< P2 andalso W2 =< W1 end, + Fun = fun({P1, _W1}, {P2, _W2}) when P1 < P2 -> true; + ({P1, _W1}, {P2, _W2}) when P1 > P2 -> false; + ({_P1, W1}, {_P2, W2}) -> W2 < W1 end, lists:sort(Fun, TlsTaggedSrvAddrLists). -spec for_each_tagged_srv_get_ip_addresses(mongooseim:host_type(), [srv_tls()]) -> [[[addr()]]]. diff --git a/src/s2s/mongoose_s2s_out.erl b/src/s2s/mongoose_s2s_out.erl index ddd9c28d7f..2b590356e4 100644 --- a/src/s2s/mongoose_s2s_out.erl +++ b/src/s2s/mongoose_s2s_out.erl @@ -535,7 +535,7 @@ wait_before_retry_timeout(#s2s_data{opts = Opts}) -> wait_before_retry_timeout(Opts); wait_before_retry_timeout(#{max_retry_delay := MaxRetryDelay}) -> Delay = 999 + rand:uniform(timer:seconds(MaxRetryDelay) - 999), - {{timeout, wait_before_retry}, min(Delay, MaxRetryDelay), wait_before_retry_timeout}. + {{timeout, wait_before_retry}, Delay, wait_before_retry_timeout}. -spec handle_get_state_info(data(), state()) -> connection_info(). handle_get_state_info(#s2s_data{socket = Socket, opts = Opts} = Data, State) ->