diff options
Diffstat (limited to 'src/mod_s2s_dialback.erl')
-rw-r--r-- | src/mod_s2s_dialback.erl | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/src/mod_s2s_dialback.erl b/src/mod_s2s_dialback.erl new file mode 100644 index 000000000..dd941a3d2 --- /dev/null +++ b/src/mod_s2s_dialback.erl @@ -0,0 +1,347 @@ +%%%------------------------------------------------------------------- +%%% Created : 16 Dec 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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(mod_s2s_dialback). +-behaviour(gen_mod). +-protocol({xep, 220, '1.1.1'}). +-protocol({xep, 185, '1.0'}). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). +%% Hooks +-export([s2s_out_auth_result/2, s2s_out_downgraded/2, + s2s_in_packet/2, s2s_out_packet/2, s2s_in_recv/3, + s2s_in_features/2, s2s_out_init/2, s2s_out_closed/2, + s2s_out_tls_verify/2]). + +-include("xmpp.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, _Opts) -> + ejabberd_hooks:add(s2s_out_init, Host, ?MODULE, s2s_out_init, 50), + ejabberd_hooks:add(s2s_out_closed, Host, ?MODULE, s2s_out_closed, 50), + ejabberd_hooks:add(s2s_in_pre_auth_features, Host, ?MODULE, + s2s_in_features, 50), + ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE, + s2s_in_features, 50), + ejabberd_hooks:add(s2s_in_handle_recv, Host, ?MODULE, + s2s_in_recv, 50), + ejabberd_hooks:add(s2s_in_unauthenticated_packet, Host, ?MODULE, + s2s_in_packet, 50), + ejabberd_hooks:add(s2s_in_authenticated_packet, Host, ?MODULE, + s2s_in_packet, 50), + ejabberd_hooks:add(s2s_out_packet, Host, ?MODULE, + s2s_out_packet, 50), + ejabberd_hooks:add(s2s_out_downgraded, Host, ?MODULE, + s2s_out_downgraded, 50), + ejabberd_hooks:add(s2s_out_auth_result, Host, ?MODULE, + s2s_out_auth_result, 50), + ejabberd_hooks:add(s2s_out_tls_verify, Host, ?MODULE, + s2s_out_tls_verify, 50). + +stop(Host) -> + ejabberd_hooks:delete(s2s_out_init, Host, ?MODULE, s2s_out_init, 50), + ejabberd_hooks:delete(s2s_out_closed, Host, ?MODULE, s2s_out_closed, 50), + ejabberd_hooks:delete(s2s_in_pre_auth_features, Host, ?MODULE, + s2s_in_features, 50), + ejabberd_hooks:delete(s2s_in_post_auth_features, Host, ?MODULE, + s2s_in_features, 50), + ejabberd_hooks:delete(s2s_in_handle_recv, Host, ?MODULE, + s2s_in_recv, 50), + ejabberd_hooks:delete(s2s_in_unauthenticated_packet, Host, ?MODULE, + s2s_in_packet, 50), + ejabberd_hooks:delete(s2s_in_authenticated_packet, Host, ?MODULE, + s2s_in_packet, 50), + ejabberd_hooks:delete(s2s_out_packet, Host, ?MODULE, + s2s_out_packet, 50), + ejabberd_hooks:delete(s2s_out_downgraded, Host, ?MODULE, + s2s_out_downgraded, 50), + ejabberd_hooks:delete(s2s_out_auth_result, Host, ?MODULE, + s2s_out_auth_result, 50), + ejabberd_hooks:delete(s2s_out_tls_verify, Host, ?MODULE, + s2s_out_tls_verify, 50). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(access) -> + econf:acl(). + +mod_options(_Host) -> + [{access, all}]. + +s2s_in_features(Acc, _) -> + [#db_feature{errors = true}|Acc]. + +s2s_out_init({ok, State}, Opts) -> + case proplists:get_value(db_verify, Opts) of + {StreamID, Key, Pid} -> + %% This is an outbound s2s connection created at step 1. + %% The purpose of this connection is to verify dialback key ONLY. + %% The connection is not registered in s2s table and thus is not + %% seen by anyone. + %% The connection will be closed immediately after receiving the + %% verification response (at step 3) + {ok, State#{db_verify => {StreamID, Key, Pid}}}; + undefined -> + {ok, State#{db_enabled => true}} + end; +s2s_out_init(Acc, _Opts) -> + Acc. + +s2s_out_closed(#{server := LServer, + remote_server := RServer, + lang := Lang, + db_verify := {StreamID, _Key, _Pid}} = State, Reason) -> + %% Outbound s2s verificating connection (created at step 1) is + %% closed suddenly without receiving the response. + %% Building a response on our own + Response = #db_verify{from = RServer, to = LServer, + id = StreamID, type = error, + sub_els = [mk_error(Reason, Lang)]}, + s2s_out_packet(State, Response); +s2s_out_closed(State, _Reason) -> + State. + +s2s_out_auth_result(#{db_verify := _} = State, _) -> + %% The temporary outbound s2s connect (intended for verification) + %% has passed authentication state (either successfully or not, no matter) + %% and at this point we can send verification request as described + %% in section 2.1.2, step 2 + {stop, send_verify_request(State)}; +s2s_out_auth_result(#{db_enabled := true, + socket := Socket, ip := IP, + server := LServer, + remote_server := RServer} = State, {false, _}) -> + %% SASL authentication has failed, retrying with dialback + %% Sending dialback request, section 2.1.1, step 1 + ?INFO_MSG("(~ts) Retrying with s2s dialback authentication: ~ts -> ~ts (~ts)", + [xmpp_socket:pp(Socket), LServer, RServer, + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + State1 = maps:remove(stop_reason, State#{on_route => queue}), + {stop, send_db_request(State1)}; +s2s_out_auth_result(State, _) -> + State. + +s2s_out_downgraded(#{db_verify := _} = State, _) -> + %% The verifying outbound s2s connection detected non-RFC compliant + %% server, send verification request immediately without auth phase, + %% section 2.1.2, step 2 + {stop, send_verify_request(State)}; +s2s_out_downgraded(#{db_enabled := true, + socket := Socket, ip := IP, + server := LServer, + remote_server := RServer} = State, _) -> + %% non-RFC compliant server detected, send dialback request instantly, + %% section 2.1.1, step 1 + ?INFO_MSG("(~ts) Trying s2s dialback authentication with " + "non-RFC compliant server: ~ts -> ~ts (~ts)", + [xmpp_socket:pp(Socket), LServer, RServer, + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + {stop, send_db_request(State)}; +s2s_out_downgraded(State, _) -> + State. + +s2s_in_packet(#{stream_id := StreamID, lang := Lang} = State, + #db_result{from = From, to = To, key = Key, type = undefined}) -> + %% Received dialback request, section 2.2.1, step 1 + try + ok = check_from_to(From, To), + %% We're creating a temporary outbound s2s connection to + %% send verification request and to receive verification response + {ok, Pid} = ejabberd_s2s_out:start( + To, From, [{db_verify, {StreamID, Key, self()}}]), + ejabberd_s2s_out:connect(Pid), + {stop, State} + catch _:{badmatch, {error, Reason}} -> + {stop, + send_db_result(State, + #db_verify{from = From, to = To, type = error, + sub_els = [mk_error(Reason, Lang)]})} + end; +s2s_in_packet(State, #db_verify{to = To, from = From, key = Key, + id = StreamID, type = undefined}) -> + %% Received verification request, section 2.2.2, step 2 + Type = case make_key(To, From, StreamID) of + Key -> valid; + _ -> invalid + end, + Response = #db_verify{from = To, to = From, id = StreamID, type = Type}, + {stop, ejabberd_s2s_in:send(State, Response)}; +s2s_in_packet(State, Pkt) when is_record(Pkt, db_result); + is_record(Pkt, db_verify) -> + ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), + State; +s2s_in_packet(State, _) -> + State. + +s2s_in_recv(#{lang := Lang} = State, El, {error, Why}) -> + case xmpp:get_name(El) of + Tag when Tag == <<"db:result">>; + Tag == <<"db:verify">> -> + case xmpp:get_type(El) of + T when T /= <<"valid">>, + T /= <<"invalid">>, + T /= <<"error">> -> + Err = xmpp:make_error(El, mk_error({codec_error, Why}, Lang)), + {stop, ejabberd_s2s_in:send(State, Err)}; + _ -> + State + end; + _ -> + State + end; +s2s_in_recv(State, _El, _Pkt) -> + State. + +s2s_out_packet(#{server := LServer, + remote_server := RServer, + db_verify := {StreamID, _Key, Pid}} = State, + #db_verify{from = RServer, to = LServer, + id = StreamID, type = Type} = Response) + when Type /= undefined -> + %% Received verification response, section 2.1.2, step 3 + %% This is a response for the request sent at step 2 + ejabberd_s2s_in:update_state( + Pid, fun(S) -> send_db_result(S, Response) end), + %% At this point the connection is no longer needed and we can terminate it + ejabberd_s2s_out:stop(State); +s2s_out_packet(#{server := LServer, remote_server := RServer} = State, + #db_result{to = LServer, from = RServer, + type = Type} = Result) when Type /= undefined -> + %% Received dialback response, section 2.1.1, step 4 + %% This is a response to the request sent at step 1 + State1 = maps:remove(db_enabled, State), + case Type of + valid -> + State2 = ejabberd_s2s_out:handle_auth_success(<<"dialback">>, State1), + ejabberd_s2s_out:establish(State2); + _ -> + Reason = str:format("Peer responded with error: ~ts", + [format_error(Result)]), + ejabberd_s2s_out:handle_auth_failure( + <<"dialback">>, {auth, Reason}, State1) + end; +s2s_out_packet(State, Pkt) when is_record(Pkt, db_result); + is_record(Pkt, db_verify) -> + ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), + State; +s2s_out_packet(State, _) -> + State. + +-spec s2s_out_tls_verify(boolean(), ejabberd_s2s_out:state()) -> boolean(). +s2s_out_tls_verify(_, #{server_host := ServerHost, remote_server := RServer}) -> + Access = mod_s2s_dialback_opt:access(ServerHost), + case acl:match_rule(ServerHost, Access, jid:make(RServer)) of + allow -> false; + deny -> true + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec make_key(binary(), binary(), binary()) -> binary(). +make_key(From, To, StreamID) -> + Secret = ejabberd_config:get_shared_key(), + str:to_hexlist( + crypto:hmac(sha256, str:to_hexlist(crypto:hash(sha256, Secret)), + [To, " ", From, " ", StreamID])). + +-spec send_verify_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). +send_verify_request(#{server := LServer, + remote_server := RServer, + db_verify := {StreamID, Key, _Pid}} = State) -> + Request = #db_verify{from = LServer, to = RServer, + key = Key, id = StreamID}, + ejabberd_s2s_out:send(State, Request). + +-spec send_db_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). +send_db_request(#{server := LServer, + remote_server := RServer, + stream_remote_id := StreamID} = State) -> + Key = make_key(LServer, RServer, StreamID), + ejabberd_s2s_out:send(State, #db_result{from = LServer, + to = RServer, + key = Key}). + +-spec send_db_result(ejabberd_s2s_in:state(), db_verify()) -> ejabberd_s2s_in:state(). +send_db_result(State, #db_verify{from = From, to = To, + type = Type, sub_els = Els}) -> + %% Sending dialback response, section 2.2.1, step 4 + %% This is a response to the request received at step 1 + Response = #db_result{from = To, to = From, type = Type, sub_els = Els}, + State1 = ejabberd_s2s_in:send(State, Response), + case Type of + valid -> + State2 = ejabberd_s2s_in:handle_auth_success( + From, <<"dialback">>, undefined, State1), + ejabberd_s2s_in:establish(State2); + _ -> + Reason = str:format("Verification failed: ~ts", + [format_error(Response)]), + ejabberd_s2s_in:handle_auth_failure( + From, <<"dialback">>, Reason, State1) + end. + +-spec check_from_to(binary(), binary()) -> ok | {error, forbidden | host_unknown}. +check_from_to(From, To) -> + case ejabberd_router:is_my_route(To) of + false -> {error, host_unknown}; + true -> + LServer = ejabberd_router:host_of_route(To), + case ejabberd_s2s:allow_host(LServer, From) of + true -> ok; + false -> {error, forbidden} + end + end. + +-spec mk_error(term(), binary()) -> stanza_error(). +mk_error(forbidden, Lang) -> + xmpp:err_forbidden(?T("Access denied by service policy"), Lang); +mk_error(host_unknown, Lang) -> + xmpp:err_not_allowed(?T("Host unknown"), Lang); +mk_error({codec_error, Why}, Lang) -> + xmpp:err_bad_request(xmpp:io_format_error(Why), Lang); +mk_error({_Class, _Reason} = Why, Lang) -> + Txt = xmpp_stream_out:format_error(Why), + xmpp:err_remote_server_not_found(Txt, Lang); +mk_error(_, _) -> + xmpp:err_internal_server_error(). + +-spec format_error(db_result()) -> binary(). +format_error(#db_result{type = invalid}) -> + <<"invalid dialback key">>; +format_error(#db_result{type = error} = Result) -> + case xmpp:get_error(Result) of + #stanza_error{} = Err -> + xmpp:format_stanza_error(Err); + undefined -> + <<"unrecognized error">> + end; +format_error(_) -> + <<"unexpected dialback result">>. |