• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.51
/src/mod_s2s_dialback.erl
1
%%%-------------------------------------------------------------------
2
%%% Created : 16 Dec 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
3
%%%
4
%%%
5
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
6
%%%
7
%%% This program is free software; you can redistribute it and/or
8
%%% modify it under the terms of the GNU General Public License as
9
%%% published by the Free Software Foundation; either version 2 of the
10
%%% License, or (at your option) any later version.
11
%%%
12
%%% This program is distributed in the hope that it will be useful,
13
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
14
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15
%%% General Public License for more details.
16
%%%
17
%%% You should have received a copy of the GNU General Public License along
18
%%% with this program; if not, write to the Free Software Foundation, Inc.,
19
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
%%%
21
%%%-------------------------------------------------------------------
22
-module(mod_s2s_dialback).
23
-behaviour(gen_mod).
24
-protocol({xep, 220, '1.1.1', '17.03', "complete", ""}).
25
-protocol({xep, 185, '1.0', '17.03', "complete", ""}).
26

27
%% gen_mod API
28
-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]).
29
-export([mod_doc/0]).
30
%% Hooks
31
-export([s2s_out_auth_result/2, s2s_out_downgraded/2,
32
         s2s_in_packet/2, s2s_out_packet/2, s2s_in_recv/3,
33
         s2s_in_features/2, s2s_out_init/2, s2s_out_closed/2,
34
         s2s_out_tls_verify/2]).
35

36
-include_lib("xmpp/include/xmpp.hrl").
37
-include("logger.hrl").
38
-include("translate.hrl").
39

40
%%%===================================================================
41
%%% API
42
%%%===================================================================
43
start(_Host, _Opts) ->
44
    {ok, [{hook, s2s_out_init, s2s_out_init, 50},
100✔
45
          {hook, s2s_out_closed, s2s_out_closed, 50},
46
          {hook, s2s_in_pre_auth_features, s2s_in_features, 50},
47
          {hook, s2s_in_post_auth_features, s2s_in_features, 50},
48
          {hook, s2s_in_handle_recv, s2s_in_recv, 50},
49
          {hook, s2s_in_unauthenticated_packet, s2s_in_packet, 50},
50
          {hook, s2s_in_authenticated_packet, s2s_in_packet, 50},
51
          {hook, s2s_out_packet, s2s_out_packet, 50},
52
          {hook, s2s_out_downgraded, s2s_out_downgraded, 50},
53
          {hook, s2s_out_auth_result, s2s_out_auth_result, 50},
54
          {hook, s2s_out_tls_verify, s2s_out_tls_verify, 50}]}.
55

56
stop(_Host) ->
57
    ok.
100✔
58

59
reload(_Host, _NewOpts, _OldOpts) ->
60
    ok.
×
61

62
depends(_Host, _Opts) ->
63
    [].
116✔
64

65
mod_opt_type(access) ->
66
    econf:acl().
116✔
67

68
mod_options(_Host) ->
69
    [{access, all}].
116✔
70

71
mod_doc() ->
72
    #{desc =>
×
73
          [?T("The module adds support for "
74
              "https://xmpp.org/extensions/xep-0220.html"
75
              "[XEP-0220: Server Dialback] to provide server identity "
76
              "verification based on DNS."), "",
77
           ?T("WARNING: DNS-based verification is vulnerable to "
78
              "https://en.wikipedia.org/wiki/DNS_spoofing"
79
              "[DNS cache poisoning], so modern servers rely on "
80
              "verification based on PKIX certificates. Thus this module "
81
              "is only recommended for backward compatibility "
82
              "with servers running outdated software or non-TLS servers, "
83
              "or those with invalid certificates (as long as you accept "
84
              "the risks, e.g. you assume that the remote server has "
85
              "an invalid certificate due to poor administration and "
86
              "not because it's compromised).")],
87
      opts =>
88
          [{access,
89
            #{value => ?T("AccessName"),
90
              desc =>
91
                  ?T("An access rule that can be used to restrict "
92
                     "dialback for some servers. The default value "
93
                     "is 'all'.")}}],
94
      example =>
95
          ["modules:",
96
           "  mod_s2s_dialback:",
97
           "    access:",
98
           "      allow:",
99
           "        server: legacy.domain.tld",
100
           "        server: invalid-cert.example.org",
101
           "      deny: all"]}.
102

103
s2s_in_features(Acc, _) ->
104
    [#db_feature{errors = true}|Acc].
20✔
105

106
s2s_out_init({ok, State}, Opts) ->
107
    case proplists:get_value(db_verify, Opts) of
8✔
108
        {StreamID, Key, Pid} ->
109
            %% This is an outbound s2s connection created at step 1.
110
            %% The purpose of this connection is to verify dialback key ONLY.
111
            %% The connection is not registered in s2s table and thus is not
112
            %% seen by anyone.
113
            %% The connection will be closed immediately after receiving the
114
            %% verification response (at step 3)
115
            {ok, State#{db_verify => {StreamID, Key, Pid}}};
2✔
116
        undefined ->
117
            {ok, State#{db_enabled => true}}
6✔
118
    end;
119
s2s_out_init(Acc, _Opts) ->
120
    Acc.
×
121

122
s2s_out_closed(#{server := LServer,
123
                 remote_server := RServer,
124
                 lang := Lang,
125
                 db_verify := {StreamID, _Key, _Pid}} = State, Reason) ->
126
    %% Outbound s2s verificating connection (created at step 1) is
127
    %% closed suddenly without receiving the response.
128
    %% Building a response on our own
129
    Response = #db_verify{from = RServer, to = LServer,
×
130
                          id = StreamID, type = error,
131
                          sub_els = [mk_error(Reason, Lang)]},
132
    s2s_out_packet(State, Response);
×
133
s2s_out_closed(State, _Reason) ->
134
    State.
4✔
135

136
s2s_out_auth_result(#{db_verify := _} = State, _) ->
137
    %% The temporary outbound s2s connect (intended for verification)
138
    %% has passed authentication state (either successfully or not, no matter)
139
    %% and at this point we can send verification request as described
140
    %% in section 2.1.2, step 2
141
    {stop, send_verify_request(State)};
2✔
142
s2s_out_auth_result(#{db_enabled := true,
143
                      socket := Socket, ip := IP,
144
                      server := LServer,
145
                      remote_server := RServer} = State, {false, _}) ->
146
    %% SASL authentication has failed, retrying with dialback
147
    %% Sending dialback request, section 2.1.1, step 1
148
    ?INFO_MSG("(~ts) Retrying with s2s dialback authentication: ~ts -> ~ts (~ts)",
2✔
149
              [xmpp_socket:pp(Socket), LServer, RServer,
150
               ejabberd_config:may_hide_data(misc:ip_to_list(IP))]),
2✔
151
    State1 = maps:remove(stop_reason, State#{on_route => queue}),
2✔
152
    {stop, send_db_request(State1)};
2✔
153
s2s_out_auth_result(State, _) ->
154
    State.
2✔
155

156
s2s_out_downgraded(#{db_verify := _} = State, _) ->
157
    %% The verifying outbound s2s connection detected non-RFC compliant
158
    %% server, send verification request immediately without auth phase,
159
    %% section 2.1.2, step 2
160
    {stop, send_verify_request(State)};
×
161
s2s_out_downgraded(#{db_enabled := true,
162
                     socket := Socket, ip := IP,
163
                     server := LServer,
164
                     remote_server := RServer} = State, _) ->
165
    %% non-RFC compliant server detected, send dialback request instantly,
166
    %% section 2.1.1, step 1
167
    ?INFO_MSG("(~ts) Trying s2s dialback authentication with "
×
168
              "non-RFC compliant server: ~ts -> ~ts (~ts)",
169
              [xmpp_socket:pp(Socket), LServer, RServer,
170
               ejabberd_config:may_hide_data(misc:ip_to_list(IP))]),
×
171
    {stop, send_db_request(State)};
×
172
s2s_out_downgraded(State, _) ->
173
    State.
×
174

175
s2s_in_packet(#{stream_id := StreamID, lang := Lang} = State,
176
              #db_result{from = From, to = To, key = Key, type = undefined}) ->
177
    %% Received dialback request, section 2.2.1, step 1
178
    try
2✔
179
        ok = check_from_to(From, To),
2✔
180
        %% We're creating a temporary outbound s2s connection to
181
        %% send verification request and to receive verification response
182
        {ok, Pid} = ejabberd_s2s_out:start(
2✔
183
                      To, From, [{db_verify, {StreamID, Key, self()}}]),
184
        ejabberd_s2s_out:connect(Pid),
2✔
185
        {stop, State}
2✔
186
    catch _:{badmatch, {error, Reason}} ->
187
            {stop,
×
188
             send_db_result(State,
189
                            #db_verify{from = From, to = To, type = error,
190
                                       sub_els = [mk_error(Reason, Lang)]})}
191
    end;
192
s2s_in_packet(State, #db_verify{to = To, from = From, key = Key,
193
                                id = StreamID, type = undefined}) ->
194
    %% Received verification request, section 2.2.2, step 2
195
    Type = case make_key(To, From, StreamID) of
2✔
196
               Key -> valid;
2✔
197
               _ -> invalid
×
198
           end,
199
    Response = #db_verify{from = To, to = From, id = StreamID, type = Type},
2✔
200
    {stop, ejabberd_s2s_in:send(State, Response)};
2✔
201
s2s_in_packet(State, Pkt) when is_record(Pkt, db_result);
202
                               is_record(Pkt, db_verify) ->
203
    ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]),
×
204
    State;
×
205
s2s_in_packet(State, _) ->
206
    State.
4✔
207

208
s2s_in_recv(#{lang := Lang} = State, El, {error, Why}) ->
209
    case xmpp:get_name(El) of
2✔
210
        Tag when Tag == <<"db:result">>;
211
                 Tag == <<"db:verify">> ->
212
            case xmpp:get_type(El) of
×
213
                T when T /= <<"valid">>,
214
                       T /= <<"invalid">>,
215
                       T /= <<"error">> ->
216
                    Err = xmpp:make_error(El, mk_error({codec_error, Why}, Lang)),
×
217
                    {stop, ejabberd_s2s_in:send(State, Err)};
×
218
                _ ->
219
                    State
×
220
            end;
221
        _ ->
222
            State
2✔
223
    end;
224
s2s_in_recv(State, _El, _Pkt) ->
225
    State.
30✔
226

227
s2s_out_packet(#{server := LServer,
228
                 remote_server := RServer,
229
                 db_verify := {StreamID, _Key, Pid}} = State,
230
               #db_verify{from = RServer, to = LServer,
231
                          id = StreamID, type = Type} = Response)
232
  when Type /= undefined ->
233
    %% Received verification response, section 2.1.2, step 3
234
    %% This is a response for the request sent at step 2
235
    ejabberd_s2s_in:update_state(
2✔
236
      Pid, fun(S) -> send_db_result(S, Response) end),
2✔
237
    %% At this point the connection is no longer needed and we can terminate it
238
    ejabberd_s2s_out:stop_async(self()),
2✔
239
    State;
2✔
240
s2s_out_packet(#{server := LServer, remote_server := RServer} = State,
241
               #db_result{to = LServer, from = RServer,
242
                          type = Type} = Result) when Type /= undefined ->
243
    %% Received dialback response, section 2.1.1, step 4
244
    %% This is a response to the request sent at step 1
245
    State1 = maps:remove(db_enabled, State),
2✔
246
    case Type of
2✔
247
        valid ->
248
            State2 = ejabberd_s2s_out:handle_auth_success(<<"dialback">>, State1),
2✔
249
            ejabberd_s2s_out:establish(State2);
2✔
250
        _ ->
251
            Reason = str:format("Peer responded with error: ~s",
×
252
                                [format_error(Result)]),
253
            ejabberd_s2s_out:handle_auth_failure(
×
254
              <<"dialback">>, {auth, Reason}, State1)
255
    end;
256
s2s_out_packet(State, Pkt) when is_record(Pkt, db_result);
257
                                is_record(Pkt, db_verify) ->
258
    ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]),
×
259
    State;
×
260
s2s_out_packet(State, _) ->
261
    State.
×
262

263
-spec s2s_out_tls_verify(boolean(), ejabberd_s2s_out:state()) -> boolean().
264
s2s_out_tls_verify(_, #{server_host := ServerHost, remote_server := RServer}) ->
265
    Access = mod_s2s_dialback_opt:access(ServerHost),
2✔
266
    case acl:match_rule(ServerHost, Access, jid:make(RServer)) of
2✔
267
        allow -> false;
2✔
268
        deny -> true
×
269
    end.
270

271
%%%===================================================================
272
%%% Internal functions
273
%%%===================================================================
274
-spec make_key(binary(), binary(), binary()) -> binary().
275
make_key(From, To, StreamID) ->
276
    Secret = ejabberd_config:get_shared_key(),
4✔
277
    str:to_hexlist(
4✔
278
      crypto:mac(hmac, sha256, str:to_hexlist(crypto:hash(sha256, Secret)),
279
                  [To, " ", From, " ", StreamID])).
280

281
-spec send_verify_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state().
282
send_verify_request(#{server := LServer,
283
                      remote_server := RServer,
284
                      db_verify := {StreamID, Key, _Pid}} = State) ->
285
    Request = #db_verify{from = LServer, to = RServer,
2✔
286
                         key = Key, id = StreamID},
287
    ejabberd_s2s_out:send(State, Request).
2✔
288

289
-spec send_db_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state().
290
send_db_request(#{server := LServer,
291
                  remote_server := RServer,
292
                  stream_remote_id := StreamID} = State) ->
293
    Key = make_key(LServer, RServer, StreamID),
2✔
294
    ejabberd_s2s_out:send(State, #db_result{from = LServer,
2✔
295
                                            to = RServer,
296
                                            key = Key}).
297

298
-spec send_db_result(ejabberd_s2s_in:state(), db_verify()) -> ejabberd_s2s_in:state().
299
send_db_result(State, #db_verify{from = From, to = To,
300
                                 type = Type, sub_els = Els}) ->
301
    %% Sending dialback response, section 2.2.1, step 4
302
    %% This is a response to the request received at step 1
303
    Response = #db_result{from = To, to = From, type = Type, sub_els = Els},
2✔
304
    State1 = ejabberd_s2s_in:send(State, Response),
2✔
305
    case Type of
2✔
306
        valid ->
307
            State2 = ejabberd_s2s_in:handle_auth_success(
2✔
308
                       From, <<"dialback">>, undefined, State1),
309
            ejabberd_s2s_in:establish(State2);
2✔
310
        _ ->
311
            Reason = str:format("Verification failed: ~s",
×
312
                                [format_error(Response)]),
313
            ejabberd_s2s_in:handle_auth_failure(
×
314
              From, <<"dialback">>, Reason, State1)
315
    end.
316

317
-spec check_from_to(binary(), binary()) -> ok | {error, forbidden | host_unknown}.
318
check_from_to(From, To) ->
319
    case ejabberd_router:is_my_route(To) of
2✔
320
            false -> {error, host_unknown};
×
321
            true ->
322
            LServer = ejabberd_router:host_of_route(To),
2✔
323
                case ejabberd_s2s:allow_host(LServer, From) of
2✔
324
                    true -> ok;
2✔
325
                    false -> {error, forbidden}
×
326
                end
327
    end.
328

329
-spec mk_error(term(), binary()) -> stanza_error().
330
mk_error(forbidden, Lang) ->
331
    xmpp:err_forbidden(?T("Access denied by service policy"), Lang);
×
332
mk_error(host_unknown, Lang) ->
333
    xmpp:err_not_allowed(?T("Host unknown"), Lang);
×
334
mk_error({codec_error, Why}, Lang) ->
335
    xmpp:err_bad_request(xmpp:io_format_error(Why), Lang);
×
336
mk_error({_Class, _Reason} = Why, Lang) ->
337
    Txt = xmpp_stream_out:format_error(Why),
×
338
    xmpp:err_remote_server_not_found(Txt, Lang);
×
339
mk_error(_, _) ->
340
    xmpp:err_internal_server_error().
×
341

342
-spec format_error(db_result()) -> binary().
343
format_error(#db_result{type = invalid}) ->
344
    <<"invalid dialback key">>;
×
345
format_error(#db_result{type = error} = Result) ->
346
    case xmpp:get_error(Result) of
×
347
        #stanza_error{} = Err ->
348
            xmpp:format_stanza_error(Err);
×
349
        undefined ->
350
            <<"unrecognized error">>
×
351
    end;
352
format_error(_) ->
353
    <<"unexpected dialback result">>.
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc