From 9afba36e547c29faf520bc252675e0c59047b146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:11:50 +0100 Subject: [PATCH 01/11] Put all s2s options in one host-type map - All options can be specified globally and per host type - Host-type s2s section completely overrides the global one - Options have defaults as specified in the docs - Certfile path is checked now - Nested maps are always included as they contain defaults --- src/config/mongoose_config_spec.erl | 80 +++++++++++------------------ 1 file changed, 30 insertions(+), 50 deletions(-) diff --git a/src/config/mongoose_config_spec.erl b/src/config/mongoose_config_spec.erl index 249b503cb3..4bd0eaec76 100644 --- a/src/config/mongoose_config_spec.erl +++ b/src/config/mongoose_config_spec.erl @@ -98,6 +98,7 @@ root() -> Listen = listen(), Auth = auth(), Modules = modules(), + S2S = s2s(), #section{ items = #{<<"general">> => General#section{required = [<<"default_server_domain">>], process = fun ?MODULE:process_general/1, @@ -110,7 +111,7 @@ root() -> <<"shaper">> => shaper(), <<"acl">> => acl(), <<"access">> => access(), - <<"s2s">> => s2s(), + <<"s2s">> => S2S#section{include = always}, <<"host_config">> => #list{items = host_config(), wrap = none} }, @@ -148,7 +149,7 @@ host_config() -> <<"modules">> => modules(), <<"acl">> => acl(), <<"access">> => access(), - <<"s2s">> => host_s2s() + <<"s2s">> => s2s() }, wrap = none }. @@ -873,54 +874,35 @@ access_rule_item() -> format_items = map }. -%% path: s2s +%% path: (host_config[].)s2s s2s() -> #section{ - items = maps:merge(s2s_global_items(), s2s_host_items()), - defaults = #{<<"address">> => #{}}, - include = always, - wrap = none - }. - -%% path: host_config[].s2s -host_s2s() -> - #section{ - items = s2s_host_items(), - wrap = none + items = #{<<"default_policy">> => #option{type = atom, + validate = {enum, [allow, deny]}}, + <<"host_policy">> => #list{items = s2s_host_policy(), + format_items = map}, + <<"use_starttls">> => #option{type = atom, + validate = {enum, [false, optional, required, + required_trusted]}}, + <<"certfile">> => #option{type = string, + validate = filename}, + <<"shared">> => #option{type = binary, + validate = non_empty}, + <<"address">> => #list{items = s2s_address(), + format_items = map}, + <<"ciphers">> => #option{type = string}, + <<"max_retry_delay">> => #option{type = integer, + validate = positive}, + <<"outgoing">> => s2s_outgoing(), + <<"dns">> => s2s_dns()}, + defaults = #{<<"default_policy">> => allow, + <<"use_starttls">> => false, + <<"ciphers">> => ejabberd_tls:default_ciphers(), + <<"max_retry_delay">> => 300}, + format_items = map, + wrap = host_config }. -s2s_host_items() -> - #{<<"default_policy">> => #option{type = atom, - validate = {enum, [allow, deny]}, - wrap = {host_config, s2s_default_policy}}, - <<"host_policy">> => #list{items = s2s_host_policy(), - format_items = map, - wrap = {host_config, s2s_host_policy}}, - <<"shared">> => #option{type = binary, - validate = non_empty, - wrap = {host_config, s2s_shared}}, - <<"max_retry_delay">> => #option{type = integer, - validate = positive, - wrap = {host_config, s2s_max_retry_delay}} - }. - -s2s_global_items() -> - #{<<"dns">> => s2s_dns(), - <<"outgoing">> => s2s_outgoing(), - <<"use_starttls">> => #option{type = atom, - validate = {enum, [false, optional, required, - required_trusted]}, - wrap = {global_config, s2s_use_starttls}}, - <<"certfile">> => #option{type = string, - validate = non_empty, - wrap = {global_config, s2s_certfile}}, - <<"address">> => #list{items = s2s_address(), - format_items = map, - wrap = {global_config, s2s_address}}, - <<"ciphers">> => #option{type = string, - wrap = {global_config, s2s_ciphers}} - }. - %% path: (host_config[].)s2s.dns s2s_dns() -> #section{ @@ -931,8 +913,7 @@ s2s_dns() -> format_items = map, include = always, defaults = #{<<"timeout">> => 10, - <<"retries">> => 2}, - wrap = {global_config, s2s_dns} + <<"retries">> => 2} }. %% path: (host_config[].)s2s.outgoing @@ -951,8 +932,7 @@ s2s_outgoing() -> include = always, defaults = #{<<"port">> => 5269, <<"ip_versions">> => [4, 6], - <<"connection_timeout">> => 10000}, - wrap = {global_config, s2s_outgoing} + <<"connection_timeout">> => 10000} }. %% path: (host_config[].)s2s.host_policy[] From b6a7d59ffa485cfa7bc80ff048064a3db3023398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:15:15 +0100 Subject: [PATCH 02/11] Handle the new host-type options in s2s helpers There is now a separate s2s shared secret for each host type --- src/ejabberd_s2s.erl | 69 ++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 537040522c..cc153d6282 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -36,7 +36,7 @@ filter/4, route/4, have_connection/1, - key/2, + key/3, get_connections_pids/1, try_register/1, remove_connection/2, @@ -46,7 +46,8 @@ incoming_s2s_number/0, outgoing_s2s_number/0, domain_utf8_to_ascii/1, - timeout/0 + timeout/0, + lookup_certfile/1 ]). %% Hooks callbacks @@ -79,7 +80,7 @@ pid :: pid() }. -record(s2s_shared, { - scope = global :: global, % this might become per host type + host_type :: mongooseim:host_type(), secret :: binary() }). -record(state, {}). @@ -178,10 +179,10 @@ node_cleanup(Acc, Node) -> Res = mnesia:async_dirty(F), maps:put(?MODULE, Res, Acc). --spec key({jid:lserver(), jid:lserver()}, binary()) -> +-spec key(mongooseim:host_type(), {jid:lserver(), jid:lserver()}, binary()) -> binary(). -key({From, To}, StreamID) -> - Secret = get_shared_secret(), +key(HostType, {From, To}, StreamID) -> + Secret = get_shared_secret(HostType), SecretHashed = base16:encode(crypto:hash(sha256, Secret)), HMac = crypto:mac(hmac, sha256, SecretHashed, [From, " ", To, " ", StreamID]), base16:encode(HMac). @@ -537,29 +538,18 @@ outgoing_s2s_number() -> %% Check if host is in blacklist or white list allow_host(MyServer, S2SHost) -> - Hosts = ?MYHOSTS, - case lists:dropwhile( - fun(ParentDomain) -> - not lists:member(ParentDomain, Hosts) - end, parent_domains(MyServer)) of - [MyHost|_] -> - allow_host1(MyHost, S2SHost); - [] -> - allow_host1(MyServer, S2SHost) - end. - -allow_host1(MyHost, S2SHost) -> - case mongoose_config:lookup_opt([{s2s_host_policy, MyHost}, S2SHost]) of - {ok, deny} -> - false; - {ok, allow} -> - true; + case mongoose_domain_api:get_host_type(MyServer) of {error, not_found} -> - case mongoose_config:lookup_opt({s2s_default_policy, MyHost}) of + false; + {ok, HostType} -> + case mongoose_config:lookup_opt([{s2s, HostType}, host_policy, S2SHost]) of + {ok, allow} -> + true; {ok, deny} -> false; - _ -> - mongoose_hooks:s2s_allow_host(MyHost, S2SHost) /= deny + {error, not_found} -> + mongoose_config:get_opt([{s2s, HostType}, default_policy]) =:= allow + andalso mongoose_hooks:s2s_allow_host(MyServer, S2SHost) =:= allow end end. @@ -601,18 +591,33 @@ get_s2s_state(S2sPid)-> end, [{s2s_pid, S2sPid} | Infos]. --spec get_shared_secret() -> binary(). -get_shared_secret() -> - %% Currently there is only one key, in the future there might be one key per host type - [#s2s_shared{secret = Secret}] = ets:lookup(s2s_shared, global), +-spec get_shared_secret(mongooseim:host_type()) -> binary(). +get_shared_secret(HostType) -> + [#s2s_shared{secret = Secret}] = ets:lookup(s2s_shared, HostType), Secret. -spec set_shared_secret() -> mnesia:t_result(ok). set_shared_secret() -> - Secret = case mongoose_config:lookup_opt(s2s_shared) of + mnesia:transaction(fun() -> + [set_shared_secret_t(HostType) || HostType <- ?ALL_HOST_TYPES], + ok + end). + +-spec set_shared_secret_t(mongooseim:host_type()) -> ok. +set_shared_secret_t(HostType) -> + Secret = case mongoose_config:lookup_opt([{s2s, HostType}, shared]) of {ok, SecretFromConfig} -> SecretFromConfig; {error, not_found} -> base16:encode(crypto:strong_rand_bytes(10)) end, - mnesia:transaction(fun() -> mnesia:write(#s2s_shared{secret = Secret}) end). + mnesia:write(#s2s_shared{host_type = HostType, secret = Secret}). + +-spec lookup_certfile(mongooseim:host_type()) -> {ok, string()} | {error, not_found}. +lookup_certfile(HostType) -> + case mongoose_config:lookup_opt({domain_certfile, HostType}) of + {ok, CertFile} -> + CertFile; + {error, not_found} -> + mongoose_config:lookup_opt([{s2s, HostType}, certfile]) + end. From 65914c6f64a1903f8fcbb2c2925234c146738e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:16:35 +0100 Subject: [PATCH 03/11] Rework s2s_in logic to read options per host type Main changes: - Server is always checked as host type is needed - Error handling is more consistent and covers more cases --- src/ejabberd_s2s_in.erl | 176 +++++++++++++++++++++------------------- 1 file changed, 92 insertions(+), 84 deletions(-) diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 79a7c9bce8..0044f83b85 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -60,6 +60,7 @@ tls_cert_verify = false :: boolean(), tls_options = [] :: [{_, _}], server :: jid:server() | undefined, + host_type :: mongooseim:host_type() | undefined, authenticated = false :: boolean(), auth_domain :: binary() | undefined, connections = dict:new(), @@ -136,31 +137,19 @@ init([{SockMod, Socket}, Opts]) -> {value, {_, S}} -> S; _ -> none end, - UseTLS = mongoose_config:get_opt(s2s_use_starttls, false), - {StartTLS, TLSRequired, TLSCertVerify} = get_tls_params(UseTLS), - TLSOpts1 = case mongoose_config:lookup_opt(s2s_certfile) of - {error, not_found} -> - []; - {ok, CertFile} -> - [{certfile, CertFile}] - end, - TLSOpts2 = lists:filter(fun({protocol_options, _}) -> true; + TLSOpts = lists:filter(fun({protocol_options, _}) -> true; ({dhfile, _}) -> true; ({cafile, _}) -> true; ({ciphers, _}) -> true; (_) -> false end, Opts), - TLSOpts = lists:append(TLSOpts1, TLSOpts2), Timer = erlang:start_timer(ejabberd_s2s:timeout(), self(), []), {ok, wait_for_stream, #state{socket = Socket, sockmod = SockMod, streamid = new_id(), shaper = Shaper, - tls = StartTLS, tls_enabled = false, - tls_required = TLSRequired, - tls_cert_verify = TLSCertVerify, tls_options = TLSOpts, timer = Timer}}. @@ -173,71 +162,92 @@ init([{SockMod, Socket}, Opts]) -> -spec wait_for_stream(ejabberd:xml_stream_item(), state()) -> fsm_return(). wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> - case {xml:get_attr_s(<<"xmlns">>, Attrs), - xml:get_attr_s(<<"xmlns:db">>, Attrs), - xml:get_attr_s(<<"to">>, Attrs), - xml:get_attr_s(<<"version">>, Attrs) == <<"1.0">>} of - {<<"jabber:server">>, _, Server, true} when - StateData#state.tls and (not StateData#state.authenticated) -> - send_text(StateData, ?STREAM_HEADER(<<" version='1.0'">>)), - SASL = - case StateData#state.tls_enabled of - true -> - verify_cert_and_get_sasl(StateData#state.sockmod, - StateData#state.socket, - StateData#state.tls_cert_verify); - _Else -> - [] - end, - StartTLS = get_tls_xmlel(StateData), - case SASL of - {error_cert_verif, CertError} -> - RemoteServer = xml:get_attr_s(<<"from">>, Attrs), - ?LOG_INFO(#{what => s2s_connection_closing, - text => <<"Closing s2s connection">>, - server => StateData#state.server, - remote_server => RemoteServer, - reason => cert_error, - cert_error => CertError}), - send_text(StateData, exml:to_binary( - mongoose_xmpp_errors:policy_violation(<<"en">>, CertError))), - {atomic, Pid} = ejabberd_s2s:find_connection( - jid:make(<<"">>, Server, <<"">>), - jid:make(<<"">>, RemoteServer, <<"">>)), - ejabberd_s2s_out:stop_connection(Pid), - - {stop, normal, StateData}; - _ -> - send_element(StateData, - #xmlel{name = <<"stream:features">>, - children = SASL ++ StartTLS ++ stream_features(Server)}), - {next_state, wait_for_feature_request, StateData#state{server = Server}} + case maps:from_list(Attrs) of + AttrMap = #{<<"xmlns">> := <<"jabber:server">>, <<"to">> := Server} -> + case StateData#state.server of + undefined -> + case mongoose_domain_api:get_host_type(Server) of + {error, not_found} -> + stream_start_error(StateData, mongoose_xmpp_errors:host_unknown()); + {ok, HostType} -> + UseTLS = mongoose_config:get_opt([{s2s, HostType}, use_starttls]), + {StartTLS, TLSRequired, TLSCertVerify} = get_tls_params(UseTLS), + start_stream(AttrMap, StateData#state{server = Server, + host_type = HostType, + tls = StartTLS, + tls_required = TLSRequired, + tls_cert_verify = TLSCertVerify}) + end; + Server -> + start_stream(AttrMap, StateData); + _Other -> + Msg = <<"The 'to' attribute differs from the originally provided one">>, + stream_start_error(StateData, mongoose_xmpp_errors:host_unknown(?MYLANG, Msg)) end; - {<<"jabber:server">>, _, Server, true} when - StateData#state.authenticated -> - send_text(StateData, ?STREAM_HEADER(<<" version='1.0'">>)), - send_element(StateData, - #xmlel{name = <<"stream:features">>, - children = stream_features(Server)}), - {next_state, stream_established, StateData}; - {<<"jabber:server">>, <<"jabber:server:dialback">>, _Server, _} -> - send_text(StateData, ?STREAM_HEADER(<<"">>)), - {next_state, stream_established, StateData}; + #{<<"xmlns">> := <<"jabber:server">>} -> + Msg = <<"The 'to' attribute is missing">>, + stream_start_error(StateData, mongoose_xmpp_errors:improper_addressing(?MYLANG, Msg)); _ -> - send_text(StateData, exml:to_binary(mongoose_xmpp_errors:invalid_namespace())), - {stop, normal, StateData} + stream_start_error(StateData, mongoose_xmpp_errors:invalid_namespace()) end; wait_for_stream({xmlstreamerror, _}, StateData) -> - send_text(StateData, - <<(?STREAM_HEADER(<<"">>))/binary, - (mongoose_xmpp_errors:xml_not_well_formed_bin())/binary, - (?STREAM_TRAILER)/binary>>), - {stop, normal, StateData}; + stream_start_error(StateData, mongoose_xmpp_errors:xml_not_well_formed()); wait_for_stream(timeout, StateData) -> {stop, normal, StateData}; wait_for_stream(closed, StateData) -> {stop, normal, StateData}. +start_stream(#{<<"version">> := <<"1.0">>, <<"from">> := RemoteServer}, + StateData = #state{tls = true, authenticated = false, server = Server, + host_type = HostType}) -> + SASL = case StateData#state.tls_enabled of + true -> + verify_cert_and_get_sasl(StateData#state.sockmod, + StateData#state.socket, + StateData#state.tls_cert_verify); + _Else -> + [] + end, + StartTLS = get_tls_xmlel(StateData), + case SASL of + {error_cert_verif, CertError} -> + ?LOG_INFO(#{what => s2s_connection_closing, + text => <<"Closing s2s connection">>, + server => StateData#state.server, + remote_server => RemoteServer, + reason => cert_error, + cert_error => CertError}), + Res = stream_start_error(StateData, + mongoose_xmpp_errors:policy_violation(?MYLANG, CertError)), + {atomic, Pid} = ejabberd_s2s:find_connection(jid:make(<<"">>, Server, <<"">>), + jid:make(<<"">>, RemoteServer, <<"">>)), + ejabberd_s2s_out:stop_connection(Pid), + Res; + _ -> + send_text(StateData, ?STREAM_HEADER(<<" version='1.0'">>)), + send_element(StateData, + #xmlel{name = <<"stream:features">>, + children = SASL ++ StartTLS ++ stream_features(HostType, Server)}), + {next_state, wait_for_feature_request, StateData} + end; +start_stream(#{<<"version">> := <<"1.0">>}, + StateData = #state{authenticated = true, host_type = HostType, server = Server}) -> + send_text(StateData, ?STREAM_HEADER(<<" version='1.0'">>)), + send_element(StateData, #xmlel{name = <<"stream:features">>, + children = stream_features(HostType, Server)}), + {next_state, stream_established, StateData}; +start_stream(#{<<"xmlns:db">> := <<"jabber:server:dialback">>}, StateData) -> + send_text(StateData, ?STREAM_HEADER(<<>>)), + {next_state, stream_established, StateData}; +start_stream(_, StateData) -> + stream_start_error(StateData, mongoose_xmpp_errors:invalid_xml()). + +stream_start_error(StateData, Error) -> + send_text(StateData, ?STREAM_HEADER(<<>>)), + send_element(StateData, Error), + send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}. + -spec wait_for_feature_request(ejabberd:xml_stream_item(), state() ) -> fsm_return(). wait_for_feature_request({xmlstreamelement, El}, StateData) -> @@ -250,15 +260,9 @@ wait_for_feature_request({xmlstreamelement, El}, StateData) -> TLSEnabled == false, SockMod == gen_tcp -> ?LOG_DEBUG(#{what => s2s_starttls}), - #state{socket = Socket, server = Server} = StateData, - TLSOpts = case mongoose_config:lookup_opt([domain_certfile, Server]) of - {error, not_found} -> - StateData#state.tls_options; - {ok, CertFile} -> - lists:keystore(certfile, 1, StateData#state.tls_options, {certfile, CertFile}) - end, + TLSOpts = tls_options(StateData), TLSSocket = (StateData#state.sockmod):starttls( - Socket, TLSOpts, + StateData#state.socket, TLSOpts, exml:to_binary( #xmlel{name = <<"proceed">>, attrs = [{<<"xmlns">>, ?NS_TLS}]})), @@ -297,6 +301,13 @@ wait_for_feature_request({xmlstreamerror, _}, StateData) -> wait_for_feature_request(closed, StateData) -> {stop, normal, StateData}. +tls_options(#state{host_type = HostType, tls_options = TLSOptions}) -> + case ejabberd_s2s:lookup_certfile(HostType) of + {ok, CertFile} -> + [{certfile, CertFile} | TLSOptions]; + {error, not_found} -> + TLSOptions + end. -spec stream_established(ejabberd:xml_stream_item(), state()) -> fsm_return(). stream_established({xmlstreamelement, El}, StateData) -> @@ -337,7 +348,7 @@ stream_established({xmlstreamelement, El}, StateData) -> to => To, from => From, message_id => Id, key => Key}), LTo = jid:nameprep(To), LFrom = jid:nameprep(From), - Type = case ejabberd_s2s:key({LTo, LFrom}, Id) of + Type = case ejabberd_s2s:key(StateData#state.host_type, {LTo, LFrom}, Id) of Key -> <<"valid">>; _ -> <<"invalid">> end, @@ -581,12 +592,9 @@ send_text(StateData, Text) -> send_element(StateData, El) -> send_text(StateData, exml:to_binary(El)). --spec stream_features(binary()) -> [exml:element()]. -stream_features(Domain) -> - case mongoose_domain_api:get_domain_host_type(Domain) of - {ok, HostType} -> mongoose_hooks:s2s_stream_features(HostType, Domain); - {error, not_found} -> [] - end. +-spec stream_features(mongooseim:host_type(), binary()) -> [exml:element()]. +stream_features(HostType, Domain) -> + mongoose_hooks:s2s_stream_features(HostType, Domain). -spec change_shaper(state(), jid:lserver(), jid:jid()) -> any(). change_shaper(StateData, Host, JID) -> From 2b675e31d0c3ba8b7fa51e0967252b2d56219c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:21:46 +0100 Subject: [PATCH 04/11] Rework s2s_out logic to read options per host type Also: unify getting TLS options --- src/ejabberd_s2s_out.erl | 150 ++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 87 deletions(-) diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index dd929ba5be..53d519c0ff 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -74,6 +74,7 @@ db_enabled = true :: boolean(), try_auth = true :: boolean(), myname, server, queue, + host_type :: mongooseim:host_type(), delay_to_retry = undefined_delay, new = false :: boolean(), verify = false :: false | {pid(), Key :: binary(), SID :: binary()}, @@ -122,10 +123,6 @@ %% We do not block on send anymore. -define(TCP_SEND_TIMEOUT, 15000). -%% Maximum delay to wait before retrying to connect after a failed attempt. -%% Specified in seconds. Default value is 5 minutes. --define(MAX_RETRY_DELAY, 300). - -define(STREAM_HEADER(From, To, Other), <<"", " ?LOG_DEBUG(#{what => s2s_out_started, text => <<"New outgoing s2s connection">>, from => From, server => Server, type => Type}), - {TLS, TLSRequired} = case mongoose_config:get_opt(s2s_use_starttls, false) of + {ok, HostType} = mongoose_domain_api:get_host_type(From), + {TLS, TLSRequired} = case mongoose_config:get_opt([{s2s, HostType}, use_starttls]) of UseTls when (UseTls==false) -> {false, false}; UseTls when (UseTls==true) or (UseTls==optional) -> @@ -199,18 +197,6 @@ init([From, Server, Type]) -> {true, true} end, UseV10 = TLS, - TLSOpts = case mongoose_config:lookup_opt(s2s_certfile) of - {error, not_found} -> - [connect]; - {ok, CertFile} -> - [{certfile, CertFile}, connect] - end, - TLSOpts2 = case mongoose_config:lookup_opt(s2s_ciphers) of - {error, not_found} -> - TLSOpts; - {ok, Ciphers} -> - [{ciphers, Ciphers} | TLSOpts] - end, {New, Verify} = case Type of new -> {true, false}; @@ -222,9 +208,10 @@ init([From, Server, Type]) -> {ok, open_socket, #state{use_v10 = UseV10, tls = TLS, tls_required = TLSRequired, - tls_options = TLSOpts2, + tls_options = tls_options(HostType), queue = queue:new(), myname = From, + host_type = HostType, server = Server, new = New, verify = Verify, @@ -237,7 +224,7 @@ init([From, Server, Type]) -> %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- -spec open_socket(_, state()) -> fsm_return(). -open_socket(init, StateData) -> +open_socket(init, StateData = #state{host_type = HostType}) -> log_s2s_out(StateData#state.new, StateData#state.myname, StateData#state.server, @@ -247,11 +234,11 @@ open_socket(init, StateData) -> server => StateData#state.server, new => StateData#state.new, verify => StateData#state.verify}), - AddrList = get_addr_list(StateData#state.server), + AddrList = get_addr_list(HostType, StateData#state.server), case lists:foldl(fun(_, {ok, Socket}) -> {ok, Socket}; (#{ip_tuple := Addr, port := Port, type := Type}, _) -> - open_socket2(Type, Addr, Port) + open_socket2(HostType, Type, Addr, Port) end, ?SOCKET_DEFAULT_RESULT, AddrList) of {ok, Socket} -> Version = case StateData#state.use_v10 of @@ -288,13 +275,12 @@ open_socket(timeout, StateData) -> open_socket(_, StateData) -> {next_state, open_socket, StateData}. --spec open_socket2(Type :: inet | inet6, - Addr :: inet:ip_address(), - Port :: inet:port_number()) -> {'error', _} | {'ok', _}. -open_socket2(Type, Addr, Port) -> +-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(), + Timeout = outgoing_s2s_timeout(HostType), SockOpts = [binary, {packet, 0}, {send_timeout, ?TCP_SEND_TIMEOUT}, @@ -528,14 +514,10 @@ wait_for_starttls_proceed({xmlstreamelement, El}, StateData) -> myname => StateData#state.myname, server => StateData#state.server}), Socket = StateData#state.socket, - TLSOpts = get_tls_opts_with_certfile(StateData), - TLSOpts2 = get_tls_opts_with_ciphers(TLSOpts), - TLSSocket = ejabberd_socket:starttls(Socket, TLSOpts2), + TLSSocket = ejabberd_socket:starttls(Socket, StateData#state.tls_options), NewStateData = StateData#state{socket = TLSSocket, streamid = new_id(), - tls_enabled = true, - tls_options = TLSOpts2 - }, + tls_enabled = true}, send_text(NewStateData, ?STREAM_HEADER(StateData#state.myname, StateData#state.server, <<" version='1.0'">>)), @@ -899,6 +881,7 @@ send_db_request(StateData) -> ok; true -> Key1 = ejabberd_s2s:key( + StateData#state.host_type, {StateData#state.myname, Server}, StateData#state.remote_streamid), send_element(StateData, @@ -952,16 +935,16 @@ is_verify_res(_) -> -include_lib("kernel/include/inet.hrl"). --spec lookup_services(jid:server()) -> [addr()]. -lookup_services(Server) -> +-spec lookup_services(mongooseim:host_type(), jid:lserver()) -> [addr()]. +lookup_services(HostType, Server) -> case ejabberd_s2s:domain_utf8_to_ascii(Server) of false -> []; - ASCIIAddr -> do_lookup_services(ASCIIAddr) + ASCIIAddr -> do_lookup_services(HostType, ASCIIAddr) end. --spec do_lookup_services(jid:server()) -> [addr()]. -do_lookup_services(Server) -> - Res = srv_lookup(Server), +-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, @@ -983,9 +966,10 @@ do_lookup_services(Server) -> end. --spec srv_lookup(jid:server()) -> {'error', atom()} | {'ok', inet:hostent()}. -srv_lookup(Server) -> - #{timeout := TimeoutSec, retries := Retries} = mongoose_config:get_opt(s2s_dns), +-spec srv_lookup(mongooseim:host_type(), jid:lserver()) -> + {'error', atom()} | {'ok', inet:hostent()}. +srv_lookup(HostType, Server) -> + #{timeout := TimeoutSec, retries := Retries} = mongoose_config:get_opt([{s2s, HostType}, dns]), srv_lookup(Server, timer:seconds(TimeoutSec), Retries). @@ -1015,18 +999,18 @@ srv_lookup(Server, Timeout, Retries) -> {ok, _HEnt} = R -> R end. --spec lookup_addrs(jid:server()) -> [addr()]. -lookup_addrs(Server) -> - Port = outgoing_s2s_port(), +-spec lookup_addrs(mongooseim:host_type(), jid:server()) -> [addr()]. +lookup_addrs(HostType, Server) -> + Port = outgoing_s2s_port(HostType), lists:foldl(fun(Type, []) -> [#{ip_tuple => Addr, port => Port, type => Type} - || Addr <- lookup_addrs(Server, Type)]; + || Addr <- lookup_addrs_for_type(Server, Type)]; (_Type, Addrs) -> Addrs - end, [], outgoing_s2s_types()). + end, [], outgoing_s2s_types(HostType)). --spec lookup_addrs(jid:lserver(), inet | inet6) -> [inet:ip_address()]. -lookup_addrs(Server, Type) -> +-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, @@ -1039,13 +1023,13 @@ lookup_addrs(Server, Type) -> end. --spec outgoing_s2s_port() -> inet:port_number(). -outgoing_s2s_port() -> - mongoose_config:get_opt([s2s_outgoing, port]). +-spec outgoing_s2s_port(mongooseim:host_type()) -> inet:port_number(). +outgoing_s2s_port(HostType) -> + mongoose_config:get_opt([{s2s, HostType}, outgoing, port]). --spec outgoing_s2s_types() -> [inet | inet6, ...]. -outgoing_s2s_types() -> +-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 @@ -1057,14 +1041,14 @@ outgoing_s2s_types() -> %% 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_outgoing, ip_versions])]. + [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() -> non_neg_integer() | infinity. -outgoing_s2s_timeout() -> - mongoose_config:get_opt([s2s_outgoing, connection_timeout], 10000). +-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 @@ -1109,7 +1093,7 @@ wait_before_reconnect(StateData) -> D1 -> %% Duplicate the delay with each successive failed %% reconnection attempt, but don't exceed the max - lists:min([D1 * 2, get_max_retry_delay()]) + 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, @@ -1120,8 +1104,8 @@ wait_before_reconnect(StateData) -> %% @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() -> - mongoose_config:get_opt(s2s_max_retry_delay, ?MAX_RETRY_DELAY) * 1000. +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 @@ -1144,28 +1128,28 @@ fsm_limit_opts() -> [] end. --spec get_addr_list(jid:server()) -> [addr()]. -get_addr_list(Server) -> - lists:foldl(fun(F, []) -> F(Server); +-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/1, - fun lookup_services/1, - fun lookup_addrs/1]). + 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(jid:server()) -> [addr()]. -get_predefined_addresses(Server) -> - case mongoose_config:lookup_opt([s2s_address, Server]) of +-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(M), + Port = get_predefined_port(HostType, M), [#{ip_tuple => IPTuple, port => Port, type => addr_type(IPTuple)}]; {error, not_found} -> [] end. -get_predefined_port(#{port := Port}) -> Port; -get_predefined_port(_) -> outgoing_s2s_port(). +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. @@ -1205,21 +1189,13 @@ get_acc_with_new_tls(?NS_TLS, El1, {SEXT, _STLS, _STLSReq}) -> get_acc_with_new_tls(_, _, Acc) -> Acc. -get_tls_opts_with_certfile(StateData) -> - case mongoose_config:lookup_opt([domain_certfile, StateData#state.myname]) of - {error, not_found} -> - StateData#state.tls_options; - {ok, CertFile} -> - lists:keystore(certfile, 1, StateData#state.tls_options, {certfile, CertFile}) - end. - -get_tls_opts_with_ciphers(TLSOpts) -> - case mongoose_config:lookup_opt(s2s_ciphers) of - {error, not_found} -> - TLSOpts; - {ok, Ciphers} -> - [{ciphers, Ciphers} | TLSOpts] - end. +tls_options(HostType) -> + CipherOpt = {ciphers, mongoose_config:get_opt([{s2s, HostType}, ciphers])}, + CertFileOpts = case ejabberd_s2s:lookup_certfile(HostType) of + {ok, CertFile} -> [{certfile, CertFile}]; + {error, not_found} -> [] + end, + [connect, CipherOpt | CertFileOpts]. calc_addr_index({Priority, Weight, Port, Host}) -> N = case Weight of From 901ffae96afe7cf9c889e6dfab0a074a23eef0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:23:05 +0100 Subject: [PATCH 05/11] Make default ciphers reusable in config specs --- src/ejabberd_tls.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ejabberd_tls.erl b/src/ejabberd_tls.erl index 67e279a1bb..e2e9cb21f1 100644 --- a/src/ejabberd_tls.erl +++ b/src/ejabberd_tls.erl @@ -13,6 +13,7 @@ %% tls interfaces required by ejabberd_socket & ejabberd_receiver modules. -export([tcp_to_tls/2, + default_ciphers/0, send/2, recv_data/2, controlling_process/2, @@ -76,7 +77,7 @@ tcp_to_tls(TCPSocket, Opts) -> Module = proplists:get_value(tls_module, Opts, fast_tls), NewOpts1 = proplists:delete(tls_module, Opts), NewOpts2 = case proplists:get_value(ciphers, NewOpts1) of - undefined -> [{ciphers, "TLSv1.2:TLSv1.3"} | NewOpts1]; + undefined -> [{ciphers, default_ciphers()} | NewOpts1]; _ -> NewOpts1 end, case Module:tcp_to_tls(TCPSocket, NewOpts2) of @@ -90,6 +91,8 @@ tcp_to_tls(TCPSocket, Opts) -> Error -> Error end. +default_ciphers() -> + "TLSv1.2:TLSv1.3". -spec send(socket(), binary()) -> ok | {error, any()}. send(#ejabberd_tls_socket{tls_module = M, tls_socket = S}, B) -> M:send(S, B). From 721aec27ccc4623e0f0d8457e4e99665dafecf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:23:58 +0100 Subject: [PATCH 06/11] Rework small tests with the new s2s config format - Test options per host type - Ensure that host-type options override global ones - Put existing files as certificates --- test/config_parser_SUITE.erl | 119 ++++++++++-------- .../mongooseim-pgsql.toml | 2 +- test/config_parser_SUITE_data/s2s_only.toml | 2 +- test/config_parser_helper.erl | 87 ++++++------- test/mongoose_config_SUITE.erl | 6 +- 5 files changed, 115 insertions(+), 101 deletions(-) diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index 6dcaf00c55..5126791031 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -176,7 +176,8 @@ groups() -> acl_merge_host_and_global, access, access_merge_host_and_global]}, - {s2s, [parallel], [s2s_dns_timeout, + {s2s, [parallel], [s2s_host_config, + s2s_dns_timeout, s2s_dns_retries, s2s_outgoing_port, s2s_outgoing_ip_versions, @@ -1472,55 +1473,67 @@ access_merge_host_and_global(_Config) -> %% tests: s2s +s2s_host_config(_Config) -> + DefaultS2S = config_parser_helper:default_s2s(), + EmptyHostConfig = host_config(#{<<"s2s">> => #{}}), + ?cfg(host_key(s2s), DefaultS2S, + EmptyHostConfig#{<<"s2s">> => #{<<"dns">> => #{<<"timeout">> => 5}}}), + StartTLSHostConfig = host_config(#{<<"s2s">> => #{<<"use_starttls">> => <<"required">>}}), + ?cfg(host_key(s2s), DefaultS2S#{use_starttls => required}, + StartTLSHostConfig#{<<"s2s">> => #{<<"dns">> => #{<<"timeout">> => 5}}}). + s2s_dns_timeout(_Config) -> - ?cfg([s2s_dns, timeout], 10, #{}), % default - ?cfg([s2s_dns, timeout], 5, #{<<"s2s">> => #{<<"dns">> => #{<<"timeout">> => 5}}}), - ?err(#{<<"s2s">> => #{<<"dns">> => #{<<"timeout">> => 0}}}). + ?cfgh([s2s, dns, timeout], 10, #{}), % default + ?cfgh([s2s, dns, timeout], 5, #{<<"s2s">> => #{<<"dns">> => #{<<"timeout">> => 5}}}), + ?errh(#{<<"s2s">> => #{<<"dns">> => #{<<"timeout">> => 0}}}). s2s_dns_retries(_Config) -> - ?cfg([s2s_dns, retries], 2, #{}), % default - ?cfg([s2s_dns, retries], 1, #{<<"s2s">> => #{<<"dns">> => #{<<"retries">> => 1}}}), - ?err(#{<<"s2s">> => #{<<"dns">> => #{<<"retries">> => 0}}}). + ?cfgh([s2s, dns, retries], 2, #{}), % default + ?cfgh([s2s, dns, retries], 1, #{<<"s2s">> => #{<<"dns">> => #{<<"retries">> => 1}}}), + ?errh(#{<<"s2s">> => #{<<"dns">> => #{<<"retries">> => 0}}}). s2s_outgoing_port(_Config) -> - ?cfg([s2s_outgoing, port], 5269, #{}), % default - ?cfg([s2s_outgoing, port], 5270, #{<<"s2s">> => #{<<"outgoing">> => #{<<"port">> => 5270}}}), - ?err(#{<<"s2s">> => #{<<"outgoing">> => #{<<"port">> => <<"http">>}}}). + ?cfgh([s2s, outgoing, port], 5269, #{}), % default + ?cfgh([s2s, outgoing, port], 5270, #{<<"s2s">> => #{<<"outgoing">> => #{<<"port">> => 5270}}}), + ?errh(#{<<"s2s">> => #{<<"outgoing">> => #{<<"port">> => <<"http">>}}}). s2s_outgoing_ip_versions(_Config) -> - ?cfg([s2s_outgoing, ip_versions], [4, 6], #{}), % default - ?cfg([s2s_outgoing, ip_versions], [6, 4], + ?cfgh([s2s, outgoing, ip_versions], [4, 6], #{}), % default + ?cfgh([s2s, outgoing, ip_versions], [6, 4], #{<<"s2s">> => #{<<"outgoing">> => #{<<"ip_versions">> => [6, 4]}}}), - ?err(#{<<"s2s">> => #{<<"outgoing">> => #{<<"ip_versions">> => []}}}), - ?err(#{<<"s2s">> => #{<<"outgoing">> => #{<<"ip_versions">> => [<<"http">>]}}}). + ?errh(#{<<"s2s">> => #{<<"outgoing">> => #{<<"ip_versions">> => []}}}), + ?errh(#{<<"s2s">> => #{<<"outgoing">> => #{<<"ip_versions">> => [<<"http">>]}}}). s2s_outgoing_timeout(_Config) -> - ?cfg([s2s_outgoing, connection_timeout], 10000, #{}), % default - ?cfg([s2s_outgoing, connection_timeout], 5000, - #{<<"s2s">> => #{<<"outgoing">> => #{<<"connection_timeout">> => 5000}}}), - ?cfg([s2s_outgoing, connection_timeout], infinity, - #{<<"s2s">> => #{<<"outgoing">> => #{<<"connection_timeout">> => <<"infinity">>}}}), - ?err(#{<<"s2s">> => #{<<"outgoing">> => #{<<"connection_timeout">> => 0}}}). + ?cfgh([s2s, outgoing, connection_timeout], 10000, #{}), % default + ?cfgh([s2s, outgoing, connection_timeout], 5000, + #{<<"s2s">> => #{<<"outgoing">> => #{<<"connection_timeout">> => 5000}}}), + ?cfgh([s2s, outgoing, connection_timeout], infinity, + #{<<"s2s">> => #{<<"outgoing">> => #{<<"connection_timeout">> => <<"infinity">>}}}), + ?errh(#{<<"s2s">> => #{<<"outgoing">> => #{<<"connection_timeout">> => 0}}}). s2s_use_starttls(_Config) -> - ?cfg(s2s_use_starttls, required, #{<<"s2s">> => #{<<"use_starttls">> => <<"required">>}}), - ?err(#{<<"s2s">> => #{<<"use_starttls">> => <<"unnecessary">>}}). + ?cfgh([s2s, use_starttls], false, #{}), % default + ?cfgh([s2s, use_starttls], required, #{<<"s2s">> => #{<<"use_starttls">> => <<"required">>}}), + ?errh(#{<<"s2s">> => #{<<"use_starttls">> => <<"unnecessary">>}}). s2s_certfile(_Config) -> - ?cfg(s2s_certfile, "cert.pem", #{<<"s2s">> => #{<<"certfile">> => <<"cert.pem">>}}), - ?err(#{<<"s2s">> => #{<<"certfile">> => []}}). + ?cfgh([s2s, certfile], "priv/server.pem", #{<<"s2s">> => #{<<"certfile">> => <<"priv/server.pem">>}}), + ?errh([#{reason := invalid_filename}], #{<<"s2s">> => #{<<"certfile">> => <<"nofile.pem">>}}), + ?errh(#{<<"s2s">> => #{<<"certfile">> => []}}). s2s_default_policy(_Config) -> - ?cfgh(s2s_default_policy, deny, #{<<"s2s">> => #{<<"default_policy">> => <<"deny">>}}), + ?cfgh([s2s, default_policy], allow, #{}), % default + ?cfgh([s2s, default_policy], deny, #{<<"s2s">> => #{<<"default_policy">> => <<"deny">>}}), ?errh(#{<<"s2s">> => #{<<"default_policy">> => <<"ask">>}}). s2s_host_policy(_Config) -> Policy = #{<<"host">> => <<"host1">>, <<"policy">> => <<"allow">>}, - ?cfgh(s2s_host_policy, #{<<"host1">> => allow}, + ?cfgh([s2s, host_policy], #{<<"host1">> => allow}, #{<<"s2s">> => #{<<"host_policy">> => [Policy]}}), - ?cfgh(s2s_host_policy, #{<<"host1">> => allow, - <<"host2">> => deny}, + ?cfgh([s2s, host_policy], #{<<"host1">> => allow, + <<"host2">> => deny}, #{<<"s2s">> => #{<<"host_policy">> => [Policy, #{<<"host">> => <<"host2">>, <<"policy">> => <<"deny">>}]}}), ?errh(#{<<"s2s">> => #{<<"host_policy">> => [maps:without([<<"host">>], Policy)]}}), @@ -1534,29 +1547,29 @@ s2s_address(_Config) -> Addr = #{<<"host">> => <<"host1">>, <<"ip_address">> => <<"192.168.1.2">>, <<"port">> => 5321}, - ?cfg(s2s_address, #{}, #{}),% default - ?cfg(s2s_address, #{<<"host1">> => #{ip_address => "192.168.1.2", port => 5321}}, - #{<<"s2s">> => #{<<"address">> => [Addr]}}), - ?cfg(s2s_address, #{<<"host1">> => #{ip_address => "192.168.1.2"}}, - #{<<"s2s">> => #{<<"address">> => [maps:without([<<"port">>], Addr)]}}), - ?err(#{<<"s2s">> => #{<<"address">> => [maps:without([<<"host">>], Addr)]}}), - ?err(#{<<"s2s">> => #{<<"address">> => [maps:without([<<"ip_address">>], Addr)]}}), - ?err(#{<<"s2s">> => #{<<"address">> => [Addr#{<<"host">> => <<>>}]}}), - ?err(#{<<"s2s">> => #{<<"address">> => [Addr#{<<"ip_address">> => <<"host2">>}]}}), - ?err(#{<<"s2s">> => #{<<"address">> => [Addr#{<<"port">> => <<"seaport">>}]}}), - ?err(#{<<"s2s">> => #{<<"address">> => [Addr, maps:remove(<<"port">>, Addr)]}}). + ?cfgh([s2s, address], #{<<"host1">> => #{ip_address => "192.168.1.2", port => 5321}}, + #{<<"s2s">> => #{<<"address">> => [Addr]}}), + ?cfgh([s2s, address], #{<<"host1">> => #{ip_address => "192.168.1.2"}}, + #{<<"s2s">> => #{<<"address">> => [maps:without([<<"port">>], Addr)]}}), + ?errh(#{<<"s2s">> => #{<<"address">> => [maps:without([<<"host">>], Addr)]}}), + ?errh(#{<<"s2s">> => #{<<"address">> => [maps:without([<<"ip_address">>], Addr)]}}), + ?errh(#{<<"s2s">> => #{<<"address">> => [Addr#{<<"host">> => <<>>}]}}), + ?errh(#{<<"s2s">> => #{<<"address">> => [Addr#{<<"ip_address">> => <<"host2">>}]}}), + ?errh(#{<<"s2s">> => #{<<"address">> => [Addr#{<<"port">> => <<"seaport">>}]}}), + ?errh(#{<<"s2s">> => #{<<"address">> => [Addr, maps:remove(<<"port">>, Addr)]}}). s2s_ciphers(_Config) -> - ?cfg(s2s_ciphers, "TLSv1.2:TLSv1.3", - #{<<"s2s">> => #{<<"ciphers">> => <<"TLSv1.2:TLSv1.3">>}}), - ?err(#{<<"s2s">> => #{<<"ciphers">> => [<<"cipher1">>, <<"cipher2">>]}}). + ?cfgh([s2s, ciphers], ejabberd_tls:default_ciphers(), #{}), % default + ?cfgh([s2s, ciphers], "TLSv1.2", + #{<<"s2s">> => #{<<"ciphers">> => <<"TLSv1.2">>}}), + ?errh(#{<<"s2s">> => #{<<"ciphers">> => [<<"cipher1">>, <<"cipher2">>]}}). s2s_shared(_Config) -> - ?cfgh(s2s_shared, <<"secret">>, #{<<"s2s">> => #{<<"shared">> => <<"secret">>}}), + ?cfgh([s2s, shared], <<"secret">>, #{<<"s2s">> => #{<<"shared">> => <<"secret">>}}), ?errh(#{<<"s2s">> => #{<<"shared">> => 536837}}). s2s_max_retry_delay(_Config) -> - ?cfgh(s2s_max_retry_delay, 120, #{<<"s2s">> => #{<<"max_retry_delay">> => 120}}), + ?cfgh([s2s, max_retry_delay], 120, #{<<"s2s">> => #{<<"max_retry_delay">> => 120}}), ?errh(#{<<"s2s">> => #{<<"max_retry_delay">> => 0}}). %% modules @@ -3377,17 +3390,10 @@ create_files(Config) -> %% The files must exist for validation to pass Root = small_path_helper:repo_dir(Config), file:make_dir("priv"), - PrivkeyPath = filename:join(Root, "tools/ssl/mongooseim/privkey.pem"), - CertPath = filename:join(Root, "tools/ssl/mongooseim/cert.pem"), - CaPath = filename:join(Root, "tools/ssl/ca/cacert.pem"), - DHPath = filename:join(Root, "tools/ssl/mongooseim/dh_server.pem"), + [ensure_copied(filename:join(Root, From), To) || {From, To} <- files_to_copy()], ok = file:write_file("priv/access_psk", ""), ok = file:write_file("priv/provision_psk", ""), - ok = filelib:ensure_dir("www/muc/dummy"), - ensure_copied(CaPath, "priv/ca.pem"), - ensure_copied(CertPath, "priv/cert.pem"), - ensure_copied(PrivkeyPath, "priv/dc1.pem"), - ensure_copied(DHPath, "priv/dh.pem"). + ok = filelib:ensure_dir("www/muc/dummy"). ensure_copied(From, To) -> case file:copy(From, To) of @@ -3397,3 +3403,10 @@ ensure_copied(From, To) -> error(#{what => ensure_copied_failed, from => From, to => To, reason => Other}) end. + +files_to_copy() -> + [{"tools/ssl/mongooseim/privkey.pem", "priv/dc1.pem"}, + {"tools/ssl/mongooseim/cert.pem", "priv/cert.pem"}, + {"tools/ssl/mongooseim/dh_server.pem", "priv/dh.pem"}, + {"tools/ssl/mongooseim/server.pem", "priv/server.pem"}, + {"tools/ssl/ca/cacert.pem", "priv/ca.pem"}]. diff --git a/test/config_parser_SUITE_data/mongooseim-pgsql.toml b/test/config_parser_SUITE_data/mongooseim-pgsql.toml index 11f2bc7731..eb25fcdd49 100644 --- a/test/config_parser_SUITE_data/mongooseim-pgsql.toml +++ b/test/config_parser_SUITE_data/mongooseim-pgsql.toml @@ -364,7 +364,7 @@ [s2s] use_starttls = "optional" - certfile = "tools/ssl/mongooseim/server.pem" + certfile = "priv/server.pem" default_policy = "allow" outgoing.port = 5299 diff --git a/test/config_parser_SUITE_data/s2s_only.toml b/test/config_parser_SUITE_data/s2s_only.toml index 8631250a73..c051891c9d 100644 --- a/test/config_parser_SUITE_data/s2s_only.toml +++ b/test/config_parser_SUITE_data/s2s_only.toml @@ -7,7 +7,7 @@ [s2s] use_starttls = "optional" - certfile = "tools/ssl/mongooseim/server.pem" + certfile = "priv/server.pem" default_policy = "allow" ciphers = "TLSv1.2:TLSv1.3" outgoing.port = 5299 diff --git a/test/config_parser_helper.erl b/test/config_parser_helper.erl index 6a94c88256..f563ae36a2 100644 --- a/test/config_parser_helper.erl +++ b/test/config_parser_helper.erl @@ -22,9 +22,11 @@ options("host_types") -> {registration_timeout, 600}, {routing_modules, mongoose_router:default_routing_modules()}, {sm_backend, {mnesia, []}}, - {s2s_address, #{}}, - {s2s_dns, default_s2s_dns()}, - {s2s_outgoing, default_s2s_outgoing()}, + {{s2s, <<"another host type">>}, default_s2s()}, + {{s2s, <<"localhost">>}, default_s2s()}, + {{s2s, <<"some host type">>}, default_s2s()}, + {{s2s, <<"this is host type">>}, default_s2s()}, + {{s2s, <<"yet another host type">>}, default_s2s()}, {{auth, <<"another host type">>}, auth_with_methods(#{})}, {{auth, <<"localhost">>}, auth_with_methods(#{rdbms => #{users_number_estimate => false}})}, @@ -82,9 +84,8 @@ options("miscellaneous") -> {periodic_report, 10800000}, report, {tracking_id, "UA-123456789"}]}]}, - {s2s_address, #{}}, - {s2s_dns, default_s2s_dns()}, - {s2s_outgoing, default_s2s_outgoing()}, + {{s2s, <<"anonymous.localhost">>}, default_s2s()}, + {{s2s, <<"localhost">>}, default_s2s()}, {sm_backend, {mnesia, []}}, {{auth, <<"anonymous.localhost">>}, custom_auth()}, {{auth, <<"localhost">>}, custom_auth()}, @@ -107,9 +108,8 @@ options("modules") -> {rdbms_server_type, generic}, {registration_timeout, 600}, {routing_modules, mongoose_router:default_routing_modules()}, - {s2s_address, #{}}, - {s2s_dns, default_s2s_dns()}, - {s2s_outgoing, default_s2s_outgoing()}, + {{s2s, <<"dummy_host">>}, default_s2s()}, + {{s2s, <<"localhost">>}, default_s2s()}, {sm_backend, {mnesia, []}}, {{auth, <<"dummy_host">>}, default_auth()}, {{auth, <<"localhost">>}, default_auth()}, @@ -241,11 +241,6 @@ options("mongooseim-pgsql") -> {rdbms_server_type, generic}, {registration_timeout, infinity}, {routing_modules, mongoose_router:default_routing_modules()}, - {s2s_address, #{<<"fed1">> => #{ip_address => "127.0.0.1"}}}, - {s2s_certfile, "tools/ssl/mongooseim/server.pem"}, - {s2s_dns, default_s2s_dns()}, - {s2s_outgoing, (default_s2s_outgoing())#{port => 5299}}, - {s2s_use_starttls, optional}, {services, [{service_admin_extra, [{submods, @@ -276,9 +271,9 @@ options("mongooseim-pgsql") -> {{replaced_wait_timeout, <<"anonymous.localhost">>}, 2000}, {{replaced_wait_timeout, <<"localhost">>}, 2000}, {{replaced_wait_timeout, <<"localhost.bis">>}, 2000}, - {{s2s_default_policy, <<"anonymous.localhost">>}, allow}, - {{s2s_default_policy, <<"localhost">>}, allow}, - {{s2s_default_policy, <<"localhost.bis">>}, allow}, + {{s2s, <<"anonymous.localhost">>}, pgsql_s2s()}, + {{s2s, <<"localhost">>}, pgsql_s2s()}, + {{s2s, <<"localhost.bis">>}, pgsql_s2s()}, {{access, global}, pgsql_access()}, {{access, <<"anonymous.localhost">>}, pgsql_access()}, {{access, <<"localhost">>}, pgsql_access()}, @@ -355,9 +350,9 @@ options("outgoing_pools") -> {rdbms_server_type, generic}, {registration_timeout, 600}, {routing_modules, mongoose_router:default_routing_modules()}, - {s2s_address, #{}}, - {s2s_dns, default_s2s_dns()}, - {s2s_outgoing, default_s2s_outgoing()}, + {{s2s, <<"anonymous.localhost">>}, default_s2s()}, + {{s2s, <<"localhost">>}, default_s2s()}, + {{s2s, <<"localhost.bis">>}, default_s2s()}, {sm_backend, {mnesia, []}}, {{auth, <<"anonymous.localhost">>}, default_auth()}, {{auth, <<"localhost">>}, default_auth()}, @@ -381,9 +376,6 @@ options("s2s_only") -> {rdbms_server_type, generic}, {registration_timeout, 600}, {routing_modules, mongoose_router:default_routing_modules()}, - {s2s_certfile, "tools/ssl/mongooseim/server.pem"}, - {s2s_ciphers, "TLSv1.2:TLSv1.3"}, - {s2s_use_starttls, optional}, {sm_backend, {mnesia, []}}, {{auth, <<"dummy_host">>}, default_auth()}, {{auth, <<"localhost">>}, default_auth()}, @@ -391,22 +383,8 @@ options("s2s_only") -> {{modules, <<"localhost">>}, #{}}, {{replaced_wait_timeout, <<"dummy_host">>}, 2000}, {{replaced_wait_timeout, <<"localhost">>}, 2000}, - {s2s_address, #{<<"fed1">> => #{ip_address => "127.0.0.1"}, - <<"fed2">> => #{ip_address => "127.0.0.1", port => 8765}}}, - {s2s_dns, #{retries => 1, timeout => 30}}, - {s2s_outgoing, #{connection_timeout => 4000, - ip_versions => [6, 4], - port => 5299}}, - {{s2s_default_policy, <<"dummy_host">>}, allow}, - {{s2s_default_policy, <<"localhost">>}, allow}, - {{s2s_max_retry_delay, <<"dummy_host">>}, 30}, - {{s2s_max_retry_delay, <<"localhost">>}, 30}, - {{s2s_shared, <<"dummy_host">>}, <<"shared secret">>}, - {{s2s_shared, <<"localhost">>}, <<"shared secret">>}, - {{s2s_host_policy, <<"dummy_host">>}, #{<<"fed1">> => allow, - <<"reg1">> => deny}}, - {{s2s_host_policy, <<"localhost">>}, #{<<"fed1">> => allow, - <<"reg1">> => deny}}]. + {{s2s, <<"dummy_host">>}, custom_s2s()}, + {{s2s, <<"localhost">>}, custom_s2s()}]. all_modules() -> #{mod_mam_rdbms_user => [{muc, true}, {pm, true}], @@ -718,9 +696,34 @@ default_auth() -> sasl_external => [standard], sasl_mechanisms => cyrsasl:default_modules()}. -default_s2s_dns() -> - #{retries => 2, - timeout => 10}. +pgsql_s2s() -> + Outgoing = (default_s2s_outgoing())#{port => 5299}, + (default_s2s())#{address => #{<<"fed1">> => #{ip_address => "127.0.0.1"}}, + certfile => "priv/server.pem", + outgoing => Outgoing, + use_starttls => optional}. + +custom_s2s() -> + #{address => + #{<<"fed1">> => #{ip_address => "127.0.0.1"}, + <<"fed2">> => #{ip_address => "127.0.0.1", port => 8765}}, + certfile => "priv/server.pem", + ciphers => ejabberd_tls:default_ciphers(), + default_policy => allow, + dns => #{retries => 1, timeout => 30}, + host_policy => #{<<"fed1">> => allow, <<"reg1">> => deny}, + max_retry_delay => 30, + outgoing => #{connection_timeout => 4000, ip_versions => [6, 4], port => 5299}, + shared => <<"shared secret">>, + use_starttls => optional}. + +default_s2s() -> + #{ciphers => ejabberd_tls:default_ciphers(), + default_policy => allow, + dns => #{retries => 2, timeout => 10}, + max_retry_delay => 300, + outgoing => default_s2s_outgoing(), + use_starttls => false}. default_s2s_outgoing() -> #{connection_timeout => 10000, diff --git a/test/mongoose_config_SUITE.erl b/test/mongoose_config_SUITE.erl index eb8f0b4d41..38afc2076d 100644 --- a/test/mongoose_config_SUITE.erl +++ b/test/mongoose_config_SUITE.erl @@ -184,13 +184,11 @@ minimal_config_opts() -> {rdbms_server_type, generic}, {registration_timeout, 600}, {routing_modules, mongoose_router:default_routing_modules()}, - {s2s_address, #{}}, - {s2s_dns, config_parser_helper:default_s2s_dns()}, - {s2s_outgoing, config_parser_helper:default_s2s_outgoing()}, {sm_backend, {mnesia, []}}, {{auth, <<"localhost">>}, config_parser_helper:default_auth()}, {{modules, <<"localhost">>}, #{}}, - {{replaced_wait_timeout, <<"localhost">>}, 2000}]. + {{replaced_wait_timeout, <<"localhost">>}, 2000}, + {{s2s, <<"localhost">>}, config_parser_helper:default_s2s()}]. start_slave_node(Config) -> SlaveNode = do_start_slave_node(), From 85492b43f7cf7ff69bd264ac2e3da5c541dabc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:25:43 +0100 Subject: [PATCH 07/11] Add a generic getter for RPC specs to improve code reuse --- big_tests/tests/distributed_helper.erl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/big_tests/tests/distributed_helper.erl b/big_tests/tests/distributed_helper.erl index 5ae57d6d28..a71f4b4181 100644 --- a/big_tests/tests/distributed_helper.erl +++ b/big_tests/tests/distributed_helper.erl @@ -143,20 +143,23 @@ require_rpc_nodes(Nodes) -> %% @doc Shorthand for hosts->mim->node from `test.config'. -spec mim() -> rpc_spec(). mim() -> - #{node => get_or_fail({hosts, mim, node})}. + rpc_spec(mim). -spec mim2() -> rpc_spec(). mim2() -> - #{node => get_or_fail({hosts, mim2, node})}. + rpc_spec(mim2). -spec mim3() -> rpc_spec(). mim3() -> - #{node => get_or_fail({hosts, mim3, node})}. + rpc_spec(mim3). %% @doc Shorthand for hosts->fed->node from `test.config'. -spec fed() -> rpc_spec(). fed() -> - #{node => get_or_fail({hosts, fed, node})}. + rpc_spec(fed). + +rpc_spec(NodeKey) -> + #{node => get_or_fail({hosts, NodeKey, node})}. get_or_fail(Key) -> Val = ct:get_config(Key), From 5749038ffe751a3786f173c432ef706f1ce6cf2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:26:43 +0100 Subject: [PATCH 08/11] Rework s2s helper to utilize the fact that tls options are in a map --- big_tests/tests/s2s_helper.erl | 169 ++++++++++++--------------------- 1 file changed, 59 insertions(+), 110 deletions(-) diff --git a/big_tests/tests/s2s_helper.erl b/big_tests/tests/s2s_helper.erl index d21753b789..a968fbb34d 100644 --- a/big_tests/tests/s2s_helper.erl +++ b/big_tests/tests/s2s_helper.erl @@ -4,131 +4,80 @@ -export([end_s2s/1]). -export([configure_s2s/2]). --import(distributed_helper, [fed/0, - mim/0, - require_rpc_nodes/1, - rpc/4]). - --include_lib("common_test/include/ct.hrl"). - --record(s2s_opts, { - node1_s2s_certfile = undefined, - node1_s2s_use_starttls = undefined, - node1_s2s_listener = [], - node2_s2s_certfile = undefined, - node2_s2s_use_starttls = undefined, - node2_s2s_listener = [] - }). +-import(distributed_helper, [fed/0, mim/0, rpc_spec/1, require_rpc_nodes/1, rpc/4]). +-import(domain_helper, [host_type/1]). suite(Config) -> - require_rpc_nodes([mim, fed]) ++ Config. + require_rpc_nodes(node_keys()) ++ Config. init_s2s(Config) -> - Node1S2SCertfile = rpc(mim(), mongoose_config, get_opt, [s2s_certfile, undefined]), - Node1S2SUseStartTLS = rpc(mim(), mongoose_config, get_opt, [s2s_use_starttls, undefined]), - Node1S2SPort = ct:get_config({hosts, mim, incoming_s2s_port}), - [Node1S2SListener] = mongoose_helper:get_listeners(mim(), #{port => Node1S2SPort, - module => ejabberd_s2s_in}), + [{{s2s, NodeKey}, get_s2s_opts(NodeKey)} || NodeKey <- node_keys()] ++ + [{escalus_user_db, xmpp} | Config]. - Node2S2SCertfile = rpc(fed(), mongoose_config, get_opt, [s2s_certfile, undefined]), - Node2S2SUseStartTLS = rpc(fed(), mongoose_config, get_opt, [s2s_use_starttls, undefined]), - Node2S2SPort = ct:get_config({hosts, fed, incoming_s2s_port}), - [Node2S2SListener] = mongoose_helper:get_listeners(fed(), #{port => Node2S2SPort, - module => ejabberd_s2s_in}), - S2S = #s2s_opts{node1_s2s_certfile = Node1S2SCertfile, - node1_s2s_use_starttls = Node1S2SUseStartTLS, - node1_s2s_listener = Node1S2SListener, - node2_s2s_certfile = Node2S2SCertfile, - node2_s2s_use_starttls = Node2S2SUseStartTLS, - node2_s2s_listener = Node2S2SListener}, +node_keys() -> + [mim, fed]. - [{s2s_opts, S2S}, - {escalus_user_db, xmpp} | Config]. +get_s2s_opts(NodeKey) -> + RPCSpec = rpc_spec(NodeKey), + S2SOpts = rpc(RPCSpec, mongoose_config, get_opt, [{s2s, host_type(NodeKey)}]), + S2SPort = ct:get_config({hosts, NodeKey, incoming_s2s_port}), + [S2SListener] = mongoose_helper:get_listeners(RPCSpec, #{port => S2SPort, + module => ejabberd_s2s_in}), + #{opts => S2SOpts, listener => S2SListener}. end_s2s(Config) -> - S2SOrig = ?config(s2s_opts, Config), - configure_s2s(S2SOrig), + [configure_and_restart_s2s(NodeKey, S2SOrig) || {{s2s, NodeKey}, S2SOrig} <- Config], ok. -configure_s2s(both_plain, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_certfile = undefined, - node1_s2s_use_starttls = undefined, - node2_s2s_certfile = undefined, - node2_s2s_use_starttls = undefined}), - Config; -configure_s2s(both_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), %The initial config assumes that both nodes are configured to use encrypted s2s - configure_s2s(S2S), - Config; -configure_s2s(both_tls_required, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required, - node2_s2s_use_starttls = required}), - Config; -configure_s2s(node1_tls_optional_node2_tls_required, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node2_s2s_use_starttls = required}), - Config; -configure_s2s(node1_tls_required_node2_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required}), - Config; -configure_s2s(node1_tls_required_trusted_node2_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required_trusted}), - Config; -configure_s2s(node1_tls_false_node2_tls_optional, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = false}), - Config; -configure_s2s(node1_tls_optional_node2_tls_false, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node2_s2s_use_starttls = false}), - Config; -configure_s2s(node1_tls_false_node2_tls_required, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = false, - node2_s2s_use_starttls = required}), - Config; -configure_s2s(node1_tls_required_node2_tls_false, Config) -> - S2S = ?config(s2s_opts, Config), - configure_s2s(S2S#s2s_opts{node1_s2s_use_starttls = required, - node2_s2s_use_starttls = false}), - Config; -configure_s2s(node1_tls_optional_node2_tls_required_trusted_with_cachain, Config) -> - S2S = ?config(s2s_opts, Config), - S2SListener = #{tls := TLSOpts} = S2S#s2s_opts.node2_s2s_listener, +configure_s2s(Group, Config) -> + TLSPreset = tls_preset(Group), + [configure_and_restart_s2s(NodeKey, s2s_config(maps:get(NodeKey, TLSPreset), S2SOrig, Config)) + || {{s2s, NodeKey}, S2SOrig} <- Config], + Config. + +s2s_config(plain, S2S = #{opts := Opts}, _) -> + S2S#{opts := maps:remove(certfile, Opts#{use_starttls := false})}; +s2s_config(required_trusted_with_cachain, S2S = #{opts := Opts, listener := Listener}, Config) -> + #{tls := TLSOpts} = Listener, CACertFile = filename:join([path_helper:repo_dir(Config), "tools", "ssl", "ca", "cacert.pem"]), NewTLSOpts = lists:keystore(cafile, 1, TLSOpts, {cafile, CACertFile}), - configure_s2s(S2S#s2s_opts{node2_s2s_use_starttls = required_trusted, - node2_s2s_listener = S2SListener#{tls => NewTLSOpts} - }), - Config. - -configure_s2s(#s2s_opts{node1_s2s_certfile = Certfile1, - node1_s2s_use_starttls = StartTLS1, - node2_s2s_certfile = Certfile2, - node2_s2s_use_starttls = StartTLS2} = S2SOpts) -> - configure_s2s(mim(), Certfile1, StartTLS1), - configure_s2s(fed(), Certfile2, StartTLS2), - restart_s2s(S2SOpts). - -configure_s2s(#{} = Spec, Certfile, StartTLS) -> - set_or_unset_opt(Spec, s2s_certfile, Certfile), - set_or_unset_opt(Spec, s2s_use_starttls, StartTLS). - -set_or_unset_opt(Spec, Opt, undefined) -> - rpc(Spec, mongoose_config, unset_opt, [Opt]); -set_or_unset_opt(Spec, Opt, Value) -> + S2S#{opts := Opts#{use_starttls := required_trusted}, + listener := Listener#{tls := NewTLSOpts}}; +s2s_config(StartTLS, S2S = #{opts := Opts}, _) -> + S2S#{opts := Opts#{use_starttls := StartTLS}}. + +tls_preset(both_plain) -> + #{mim => plain, fed => plain}; +tls_preset(both_tls_optional) -> + #{mim => optional, fed => optional}; +tls_preset(both_tls_required) -> + #{mim => required, fed => required}; +tls_preset(node1_tls_optional_node2_tls_required) -> + #{mim => optional, fed => required}; +tls_preset(node1_tls_required_node2_tls_optional) -> + #{mim => required, fed => optional}; +tls_preset(node1_tls_required_trusted_node2_tls_optional) -> + #{mim => required_trusted, fed => optional}; +tls_preset(node1_tls_false_node2_tls_optional) -> + #{mim => false, fed => optional}; +tls_preset(node1_tls_optional_node2_tls_false) -> + #{mim => optional, fed => false}; +tls_preset(node1_tls_false_node2_tls_required) -> + #{mim => false, fed => required}; +tls_preset(node1_tls_required_node2_tls_false) -> + #{mim => required, fed => false}; +tls_preset(node1_tls_optional_node2_tls_required_trusted_with_cachain) -> + #{mim => optional, fed => required_trusted_with_cachain}. + +configure_and_restart_s2s(NodeKey, #{opts := Opts, listener := Listener}) -> + HostType = host_type(NodeKey), + set_opt(rpc_spec(NodeKey), [{s2s, HostType}], Opts), + restart_s2s(rpc_spec(NodeKey), Listener). + +set_opt(Spec, Opt, Value) -> rpc(Spec, mongoose_config, set_opt, [Opt, Value]). -restart_s2s(#s2s_opts{node1_s2s_listener = Node1S2SListener, - node2_s2s_listener = Node2S2SListener}) -> - restart_s2s(mim(), Node1S2SListener), - restart_s2s(fed(), Node2S2SListener). - restart_s2s(#{} = Spec, S2SListener) -> Children = rpc(Spec, supervisor, which_children, [ejabberd_s2s_out_sup]), [rpc(Spec, ejabberd_s2s_out, stop_connection, [Pid]) || From 58179a25b9c12096301986d5b93685246294df7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:28:12 +0100 Subject: [PATCH 09/11] Add s2s tests for error conditions Also: parallelize, remove repetition --- big_tests/tests/pubsub_s2s_SUITE.erl | 5 +- big_tests/tests/s2s_SUITE.erl | 183 ++++++++++++++++----------- 2 files changed, 113 insertions(+), 75 deletions(-) diff --git a/big_tests/tests/pubsub_s2s_SUITE.erl b/big_tests/tests/pubsub_s2s_SUITE.erl index e9497eee1b..c9b5c5306e 100644 --- a/big_tests/tests/pubsub_s2s_SUITE.erl +++ b/big_tests/tests/pubsub_s2s_SUITE.erl @@ -44,10 +44,7 @@ groups() -> NodeTree <- [<<"dag">>, <<"tree">>]]. base_groups() -> - G = [ - {basic, [parallel], basic_tests()} - ], - ct_helper:repeat_all_until_all_ok(G). + [{basic, [parallel], basic_tests()}]. basic_tests() -> [ diff --git a/big_tests/tests/s2s_SUITE.erl b/big_tests/tests/s2s_SUITE.erl index 1221b5ddd7..4908e62c17 100644 --- a/big_tests/tests/s2s_SUITE.erl +++ b/big_tests/tests/s2s_SUITE.erl @@ -10,7 +10,6 @@ -include_lib("escalus/include/escalus.hrl"). -include_lib("exml/include/exml.hrl"). -include_lib("exml/include/exml_stream.hrl"). --include_lib("common_test/include/ct.hrl"). %% Module aliases -define(dh, distributed_helper). @@ -40,26 +39,25 @@ all() -> ]. groups() -> - G = [{both_plain, [sequence], all_tests()}, - {both_tls_optional, [], essentials()}, - {both_tls_required, [], essentials()}, + [{both_plain, [sequence], all_tests()}, + {both_tls_optional, [], essentials()}, + {both_tls_required, [], essentials()}, - {node1_tls_optional_node2_tls_required, [], essentials()}, - {node1_tls_required_node2_tls_optional, [], essentials()}, + {node1_tls_optional_node2_tls_required, [], essentials()}, + {node1_tls_required_node2_tls_optional, [], essentials()}, - %% Node1 closes connection from nodes with invalid certs - {node1_tls_required_trusted_node2_tls_optional, [], negative()}, + %% Node1 closes connection from nodes with invalid certs + {node1_tls_required_trusted_node2_tls_optional, [], negative()}, - %% Node1 accepts connection provided the cert can be verified - {node1_tls_optional_node2_tls_required_trusted_with_cachain, [], - essentials() ++ connection_cases()}, + %% Node1 accepts connection provided the cert can be verified + {node1_tls_optional_node2_tls_required_trusted_with_cachain, [parallel], + essentials() ++ connection_cases()}, - {node1_tls_false_node2_tls_optional, [], essentials()}, - {node1_tls_optional_node2_tls_false, [], essentials()}, + {node1_tls_false_node2_tls_optional, [], essentials()}, + {node1_tls_optional_node2_tls_false, [], essentials()}, - {node1_tls_false_node2_tls_required, [], negative()}, - {node1_tls_required_node2_tls_false, [], negative()}], - ct_helper:repeat_all_until_all_ok(G). + {node1_tls_false_node2_tls_required, [], negative()}, + {node1_tls_required_node2_tls_false, [], negative()}]. essentials() -> [simple_message]. @@ -72,6 +70,12 @@ negative() -> connection_cases() -> [successful_external_auth_with_valid_cert, + start_stream_fails_for_wrong_namespace, + start_stream_fails_for_wrong_version, + start_stream_fails_without_version, + start_stream_fails_without_host, + start_stream_fails_for_unknown_host, + starttls_fails_for_unknown_host, only_messages_from_authenticated_domain_users_are_accepted, auth_with_valid_cert_fails_when_requested_name_is_not_in_the_cert, auth_with_valid_cert_fails_for_other_mechanism_than_external]. @@ -220,33 +224,50 @@ nonascii_addr(Config) -> end). successful_external_auth_with_valid_cert(Config) -> - {KeyFile, CertFile} = get_main_key_and_cert_files(Config), - ConnectionArgs = [{host, "localhost"}, - {to_server, "fed1"}, - {from_server, "localhost_bis"}, - {requested_name, <<"localhost">>}, - {starttls, required}, - {port, ct:get_config({hosts, fed, incoming_s2s_port})}, - {ssl_opts, [{certfile, CertFile}, - {keyfile, KeyFile}]} - ], + ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config), {ok, Client, _Features} = escalus_connection:start(ConnectionArgs, [fun s2s_start_stream/2, fun s2s_starttls/2, fun s2s_external_auth/2]), escalus_connection:stop(Client). +start_stream_fails_for_wrong_namespace(Config) -> + start_stream_fails(Config, <<"invalid-namespace">>, + [fun s2s_start_stream_with_wrong_namespace/2]). + +start_stream_fails_for_wrong_version(Config) -> + %% TLS authentication requires version 1.0 + start_stream_fails(Config, <<"invalid-xml">>, + [fun s2s_start_stream_with_wrong_version/2]). + +start_stream_fails_without_version(Config) -> + %% TLS authentication requires version 1.0 + start_stream_fails(Config, <<"invalid-xml">>, + [fun s2s_start_stream_without_version/2]). + +start_stream_fails_without_host(Config) -> + start_stream_fails(Config, <<"improper-addressing">>, + [fun s2s_start_stream_without_host/2]). + +start_stream_fails_for_unknown_host(Config) -> + start_stream_fails(Config, <<"host-unknown">>, + [fun s2s_start_stream_to_wrong_host/2]). + +starttls_fails_for_unknown_host(Config) -> + start_stream_fails(Config, <<"host-unknown">>, + [fun s2s_start_stream/2, + fun s2s_starttls_to_wrong_host/2]). + +start_stream_fails(Config, ErrorType, ConnectionSteps) -> + ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config), + {ok, Client, _} = escalus_connection:start(ConnectionArgs, ConnectionSteps), + [Start, Error, End] = escalus:wait_for_stanzas(Client, 3), + escalus:assert(is_stream_start, Start), + escalus:assert(is_stream_error, [ErrorType, <<>>], Error), + escalus:assert(is_stream_end, End). + only_messages_from_authenticated_domain_users_are_accepted(Config) -> - {KeyFile, CertFile} = get_main_key_and_cert_files(Config), - ConnectionArgs = [{host, "localhost"}, - {to_server, "fed1"}, - {from_server, "localhost_bis"}, - {requested_name, <<"localhost">>}, - {starttls, required}, - {port, ct:get_config({hosts, fed, incoming_s2s_port})}, - {ssl_opts, [{certfile, CertFile}, - {keyfile, KeyFile}]} - ], + ConnectionArgs = connection_args("localhost.bis", <<"localhost">>, Config), {ok, Client, _Features} = escalus_connection:start(ConnectionArgs, [fun s2s_start_stream/2, fun s2s_starttls/2, @@ -270,16 +291,7 @@ only_messages_from_authenticated_domain_users_are_accepted(Config) -> escalus_connection:stop(Client). auth_with_valid_cert_fails_when_requested_name_is_not_in_the_cert(Config) -> - {KeyFile, CertFile} = get_main_key_and_cert_files(Config), - ConnectionArgs = [{host, "localhost"}, - {to_server, "fed1"}, - {from_server, "some_not_in_cert_domain"}, - {requested_name, <<"some_not_in_cert_domain">>}, - {starttls, required}, - {port, ct:get_config({hosts, fed, incoming_s2s_port})}, - {ssl_opts, [{certfile, CertFile}, - {keyfile, KeyFile}]} - ], + ConnectionArgs = connection_args("not_in_cert_domain", <<"not_in_cert_domain">>, Config), {ok, Client, _Features} = escalus_connection:start(ConnectionArgs, [fun s2s_start_stream/2, fun s2s_starttls/2]), @@ -292,16 +304,7 @@ auth_with_valid_cert_fails_when_requested_name_is_not_in_the_cert(Config) -> end. auth_with_valid_cert_fails_for_other_mechanism_than_external(Config) -> - {KeyFile, CertFile} = get_main_key_and_cert_files(Config), - ConnectionArgs = [{host, "localhost"}, - {to_server, "fed1"}, - {from_server, "localhost"}, - {requested_name, <<"localhost">>}, - {starttls, required}, - {port, ct:get_config({hosts, fed, incoming_s2s_port})}, - {ssl_opts, [{certfile, CertFile}, - {keyfile, KeyFile}]} - ], + ConnectionArgs = connection_args("localhost", <<"localhost">>, Config), {ok, Client, _Features} = escalus_connection:start(ConnectionArgs, [fun s2s_start_stream/2, fun s2s_starttls/2 @@ -313,6 +316,41 @@ auth_with_valid_cert_fails_for_other_mechanism_than_external(Config) -> escalus_connection:wait_for_close(Client, timer:seconds(5)). +connection_args(FromServer, RequestedName, Config) -> + {KeyFile, CertFile} = get_main_key_and_cert_files(Config), + [{host, "localhost"}, + {to_server, "fed1"}, + {from_server, FromServer}, + {requested_name, RequestedName}, + {starttls, required}, + {port, ct:get_config({hosts, fed, incoming_s2s_port})}, + {ssl_opts, [{certfile, CertFile}, {keyfile, KeyFile}]}]. + +s2s_start_stream_with_wrong_namespace(Conn = #client{props = Props}, Features) -> + Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"xmlns">> => <<"42">>} end), + ok = escalus_connection:send(Conn, Start), + {Conn, Features}. + +s2s_start_stream_with_wrong_version(Conn = #client{props = Props}, Features) -> + Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"version">> => <<"42">>} end), + ok = escalus_connection:send(Conn, Start), + {Conn, Features}. + +s2s_start_stream_without_version(Conn = #client{props = Props}, Features) -> + Start = s2s_stream_start_stanza(Props, fun(Attrs) -> maps:remove(<<"version">>, Attrs) end), + ok = escalus_connection:send(Conn, Start), + {Conn, Features}. + +s2s_start_stream_without_host(Conn = #client{props = Props}, Features) -> + Start = s2s_stream_start_stanza(Props, fun(Attrs) -> maps:remove(<<"to">>, Attrs) end), + ok = escalus_connection:send(Conn, Start), + {Conn, Features}. + +s2s_start_stream_to_wrong_host(Conn = #client{props = Props}, Features) -> + Start = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs#{<<"to">> => <<"42">>} end), + ok = escalus_connection:send(Conn, Start), + {Conn, Features}. + s2s_start_stream(Conn = #client{props = Props}, []) -> StreamStartRep = s2s_start_stream_and_wait_for_response(Conn), @@ -322,23 +360,21 @@ s2s_start_stream(Conn = #client{props = Props}, []) -> escalus_session:stream_features(Conn#client{props = [{sid, Id} | Props]}, []). s2s_start_stream_and_wait_for_response(Conn = #client{props = Props}) -> - {to_server, To} = lists:keyfind(to_server, 1, Props), - {from_server, From} = lists:keyfind(from_server, 1, Props), - - StreamStart = s2s_stream_start_stanza(To, From), + StreamStart = s2s_stream_start_stanza(Props, fun(Attrs) -> Attrs end), ok = escalus_connection:send(Conn, StreamStart), escalus_connection:get_stanza(Conn, wait_for_stream). -s2s_stream_start_stanza(To, From) -> - Attrs = [{<<"to">>, To}, - {<<"from">>, From}, - {<<"xmlns">>, <<"jabber:server">>}, - {<<"xmlns:stream">>, - <<"http://etherx.jabber.org/streams">>}, - {<<"version">>, <<"1.0">>}], - #xmlstreamstart{name = <<"stream:stream">>, attrs = Attrs}. +s2s_stream_start_stanza(Props, F) -> + Attrs = (stream_start_attrs())#{<<"to">> => proplists:get_value(to_server, Props), + <<"from">> => proplists:get_value(from_server, Props)}, + #xmlstreamstart{name = <<"stream:stream">>, attrs = maps:to_list(F(Attrs))}. -s2s_starttls(Client, Features) -> +stream_start_attrs() -> + #{<<"xmlns">> => <<"jabber:server">>, + <<"xmlns:stream">> => <<"http://etherx.jabber.org/streams">>, + <<"version">> => <<"1.0">>}. + +s2s_starttls(Client, Features, StartStreamF) -> case proplists:get_value(starttls, Features) of false -> ct:fail("The server does not offer STARTTLS"); @@ -349,7 +385,13 @@ s2s_starttls(Client, Features) -> escalus_connection:send(Client, escalus_stanza:starttls()), escalus_connection:get_stanza(Client, proceed), escalus_connection:upgrade_to_tls(Client), - s2s_start_stream(Client, []). + StartStreamF(Client, []). + +s2s_starttls(Client, Features) -> + s2s_starttls(Client, Features, fun s2s_start_stream/2). + +s2s_starttls_to_wrong_host(Client, Features) -> + s2s_starttls(Client, Features, fun s2s_start_stream_to_wrong_host/2). s2s_external_auth(Client = #client{props = Props}, Features) -> case proplists:get_value(sasl_mechanisms, Features) of @@ -369,4 +411,3 @@ get_main_key_and_cert_files(Config) -> get_main_file_path(Config, File) -> filename:join([path_helper:repo_dir(Config), "tools", "ssl", "mongooseim", File]). - From 6babcd73bcf3f8bb13be361c92558f2aa06dea77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 2 Feb 2022 09:29:05 +0100 Subject: [PATCH 10/11] Update docs for s2s options - All options are allowed per host type - s2s section overrides the whole global section now --- doc/configuration/host_config.md | 10 ++-------- doc/configuration/s2s.md | 2 +- doc/migrations/5.0.0_5.1.0.md | 4 +++- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/doc/configuration/host_config.md b/doc/configuration/host_config.md index c69e76c5df..8d07a2755b 100644 --- a/doc/configuration/host_config.md +++ b/doc/configuration/host_config.md @@ -193,13 +193,7 @@ The `register` rule is defined only for `domain2.com`. ### `host_config.s2s` -The options defined here override the ones defined in the top-level [`s2s`](s2s.md) section. -The following options are allowed: - -* [`default_policy`](s2s.md#s2sdefault_policy) -* [`host_policy`](s2s.md#s2shost_policy) -* [`shared`](s2s.md#s2sshared) -* [`max_retry_delay`](s2s.md#s2smax_retry_delay) +This section completely overrides the top-level [`s2s`](s2s.md) section, all options are allowed. #### Example @@ -227,4 +221,4 @@ The `host_policy` option is changed for `domain2.com`: ] ``` -The `default_policy` is still `deny`. +Note that `default_policy` for `domain2.com` has the default value `allow`, because `host_config.s2s` completely overrides the top-level `s2s` section, and all options are reset to the respective default values, unless they are explicitly changed. diff --git a/doc/configuration/s2s.md b/doc/configuration/s2s.md index 5fe5e624b6..02d587e0a9 100644 --- a/doc/configuration/s2s.md +++ b/doc/configuration/s2s.md @@ -21,7 +21,7 @@ Default policy for opening new S2S connections to/from remote servers. * **Syntax:** array of TOML tables with the following mandatory content: * `host` - string, host name * `policy` - string, `"allow"` or `"deny"` -* **Default:** `"allow"` +* **Default:** not set, `default_policy` is used * **Example:** ```toml diff --git a/doc/migrations/5.0.0_5.1.0.md b/doc/migrations/5.0.0_5.1.0.md index f6581f8a96..5f38062df3 100644 --- a/doc/migrations/5.0.0_5.1.0.md +++ b/doc/migrations/5.0.0_5.1.0.md @@ -16,7 +16,9 @@ See the [auth configuration](../configuration/auth.md) for details. ### Section `s2s` -The `domain_certfile` option has been moved to the `general` section because it affects `c2s` connections as well. +* All options can be set globally or inside `host_config`. +* The `host_config.s2s` section overrides the whole global section now. Previously only the specified options were overridden. +* The `domain_certfile` option has been moved to the `general` section because it affects `c2s` connections as well. ### Section `host_config` From db060abeec62877f47edd1fde8793816420d70e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 3 Feb 2022 08:01:51 +0100 Subject: [PATCH 11/11] Clean up binary syntax --- src/ejabberd_s2s_in.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 0044f83b85..2c058bd523 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -219,8 +219,8 @@ start_stream(#{<<"version">> := <<"1.0">>, <<"from">> := RemoteServer}, cert_error => CertError}), Res = stream_start_error(StateData, mongoose_xmpp_errors:policy_violation(?MYLANG, CertError)), - {atomic, Pid} = ejabberd_s2s:find_connection(jid:make(<<"">>, Server, <<"">>), - jid:make(<<"">>, RemoteServer, <<"">>)), + {atomic, Pid} = ejabberd_s2s:find_connection(jid:make(<<>>, Server, <<>>), + jid:make(<<>>, RemoteServer, <<>>)), ejabberd_s2s_out:stop_connection(Pid), Res; _ -> @@ -331,7 +331,7 @@ stream_established({xmlstreamelement, El}, StateData) -> Key, StateData#state.streamid}), Conns = dict:store({LFrom, LTo}, wait_for_verification, StateData#state.connections), - change_shaper(StateData, LTo, jid:make(<<"">>, LFrom, <<"">>)), + change_shaper(StateData, LTo, jid:make(<<>>, LFrom, <<>>)), {next_state, stream_established, StateData#state{connections = Conns,