1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2010-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%
22%% Tests of traffic between seven Diameter nodes in four realms,
23%% connected as follows.
24%%
25%%                  ----- SERVER1.REALM2 -----
26%%                /                            \
27%%               /  ----- SERVER2.REALM2 -----  \
28%%              | /                            \ |
29%%   CLIENT.REALM1 ------ SERVER3.REALM2 ------ CLIENT.REALM4
30%%              | \                            / |
31%%              |  \                          /  |
32%%               \   ---- SERVER1.REALM3 -----  /
33%%                \                            /
34%%                  ----- SERVER2.REALM3 -----
35%%
36
37-module(diameter_failover_SUITE).
38
39-export([suite/0,
40         all/0]).
41
42%% testcases
43-export([start/1,
44         start_services/1,
45         connect/1,
46         send_ok/1,
47         send_nok/1,
48         send_discard_1/1,
49         send_discard_2/1,
50         stop_services/1,
51         empty/1,
52         stop/1]).
53
54%% diameter callbacks
55-export([pick_peer/4,
56         prepare_request/3,
57         prepare_retransmit/3,
58         handle_error/4,
59         handle_answer/4,
60         handle_request/3]).
61
62-include("diameter.hrl").
63-include("diameter_gen_base_rfc3588.hrl").
64
65%% ===========================================================================
66
67-define(util, diameter_util).
68
69-define(ADDR, {127,0,0,1}).
70
71-define(CLIENT1, "CLIENT.REALM1").
72-define(CLIENT2, "CLIENT.REALM4").
73-define(SERVER1, "SERVER1.REALM2").
74-define(SERVER2, "SERVER2.REALM2").
75-define(SERVER3, "SERVER3.REALM2").
76-define(SERVER4, "SERVER1.REALM3").
77-define(SERVER5, "SERVER2.REALM3").
78
79-define(IS_CLIENT(Svc), Svc == ?CLIENT1; Svc == ?CLIENT2).
80
81-define(CLIENTS, [?CLIENT1, ?CLIENT2]).
82-define(SERVERS, [?SERVER1, ?SERVER2, ?SERVER3, ?SERVER4, ?SERVER5]).
83
84-define(DICT_COMMON,  ?DIAMETER_DICT_COMMON).
85
86-define(APP_ALIAS, the_app).
87-define(APP_ID, ?DICT_COMMON:id()).
88
89%% Config for diameter:start_service/2.
90-define(SERVICE(Host),
91        [{'Origin-Host', Host},
92         {'Origin-Realm', realm(Host)},
93         {'Host-IP-Address', [?ADDR]},
94         {'Vendor-Id', 12345},
95         {'Product-Name', "OTP/diameter"},
96         {'Acct-Application-Id', [?APP_ID]},
97         {application, [{alias, ?APP_ALIAS},
98                        {dictionary, ?DICT_COMMON},
99                        {module, #diameter_callback
100                                  {peer_up = false,
101                                   peer_down = false,
102                                   default = ?MODULE}},
103                        {answer_errors, callback}]}]).
104
105-define(SUCCESS, 2001).
106
107%% Value of Termination-Cause determines client/server behaviour.
108-define(LOGOUT,   ?'DIAMETER_BASE_TERMINATION-CAUSE_LOGOUT').
109-define(MOVED,    ?'DIAMETER_BASE_TERMINATION-CAUSE_USER_MOVED').
110-define(TIMEOUT,  ?'DIAMETER_BASE_TERMINATION-CAUSE_SESSION_TIMEOUT').
111
112%% ===========================================================================
113
114suite() ->
115    [{timetrap, {seconds, 60}}].
116
117all() ->
118    [start,
119     start_services,
120     connect,
121     send_ok,
122     send_nok,
123     send_discard_1,
124     send_discard_2,
125     stop_services,
126     empty,
127     stop].
128
129%% ===========================================================================
130%% start/stop testcases
131
132start(_Config) ->
133    ok = diameter:start().
134
135start_services(_Config) ->
136    Servers = [server(N) || N <- ?SERVERS],
137    [] = [T || C <- ?CLIENTS,
138               T <- [diameter:start_service(C, ?SERVICE(C))],
139               T /= ok],
140
141    {save_config, Servers}.
142
143connect(Config) ->
144    {start_services, Servers} = proplists:get_value(saved_config, Config),
145
146    lists:foreach(fun(C) -> connect(C, Servers) end, ?CLIENTS).
147
148stop_services(_Config) ->
149    [] = [{H,T} || H <- ?CLIENTS ++ ?SERVERS,
150                   T <- [diameter:stop_service(H)],
151                   T /= ok].
152
153%% Ensure transports have been removed from request table.
154empty(_Config) ->
155    [] = ets:tab2list(diameter_request).
156
157stop(_Config) ->
158    ok = diameter:stop().
159
160%% ----------------------------------------
161
162server(Name) ->
163    ok = diameter:start_service(Name, ?SERVICE(Name)),
164    {Name, ?util:listen(Name, tcp)}.
165
166connect(Name, Refs) ->
167    [{{Name, ?util:connect(Name, tcp, LRef)}, T} || {_, LRef} = T <- Refs].
168
169%% ===========================================================================
170%% traffic testcases
171
172%% Send an STR and expect success after SERVER3 answers after a couple
173%% of failovers.
174send_ok(_Config) ->
175    Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER1),
176                             'Termination-Cause' = ?LOGOUT,
177                             'Auth-Application-Id' = ?APP_ID},
178    #diameter_base_STA{'Result-Code' = ?SUCCESS,
179                       'Origin-Host' = ?SERVER3}
180        = call(?CLIENT1, Req).
181
182%% Send an STR and expect failure when both servers fail.
183send_nok(_Config) ->
184    Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER4),
185                             'Termination-Cause' = ?LOGOUT,
186                             'Auth-Application-Id' = ?APP_ID},
187    {failover, ?LOGOUT} = call(?CLIENT1, Req).
188
189%% Send an STR and have prepare_retransmit discard it.
190send_discard_1(_Config) ->
191    Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER1),
192                             'Termination-Cause' = ?TIMEOUT,
193                             'Auth-Application-Id' = ?APP_ID},
194    {rejected, ?TIMEOUT} = call(?CLIENT2, Req).
195send_discard_2(_Config) ->
196    Req = #diameter_base_STR{'Destination-Realm' = realm(?SERVER4),
197                             'Termination-Cause' = ?MOVED,
198                             'Auth-Application-Id' = ?APP_ID},
199    {discarded, ?MOVED} = call(?CLIENT2, Req).
200
201%% ===========================================================================
202
203realm(Host) ->
204    tl(lists:dropwhile(fun(C) -> C /= $. end, Host)).
205
206call(Svc, Req) ->
207    diameter:call(Svc, ?APP_ALIAS, Req, [{filter, realm}]).
208
209%% ===========================================================================
210%% diameter callbacks
211
212%% pick_peer/4
213
214%% Choose a server other than SERVER3 or SERVER5 if possible.
215pick_peer(Peers, _, Svc, _State)
216  when ?IS_CLIENT(Svc) ->
217    case lists:partition(fun({_, #diameter_caps{origin_host = {_, OH}}}) ->
218                                 OH /= ?SERVER3 andalso OH /= ?SERVER5
219                         end,
220                         Peers)
221    of
222        {[], [Peer]} ->
223            {ok, Peer};
224        {[Peer | _], _} ->
225            {ok, Peer}
226    end.
227
228%% prepare_request/3
229
230prepare_request(Pkt, Svc, {_Ref, Caps})
231  when ?IS_CLIENT(Svc) ->
232    {send, prepare(Pkt, Caps)}.
233
234prepare(#diameter_packet{msg = Req}, Caps) ->
235    #diameter_caps{origin_host  = {OH, _},
236                   origin_realm = {OR, _}}
237        = Caps,
238    Req#diameter_base_STR{'Origin-Host' = OH,
239                          'Origin-Realm' = OR,
240                          'Session-Id' = diameter:session_id(OH)}.
241
242%% prepare_retransmit/3
243
244prepare_retransmit(#diameter_packet{header = H} = P, Svc, {_,_})
245  when ?IS_CLIENT(Svc) ->
246    #diameter_header{is_retransmitted = true} = H,  %% assert
247    prepare(P).
248
249prepare(#diameter_packet{msg = M} = P) ->
250    case M#diameter_base_STR.'Termination-Cause' of
251        ?LOGOUT  -> {send, P};
252        ?MOVED   -> discard;
253        ?TIMEOUT -> {discard, rejected}
254    end.
255
256%% handle_error/4
257
258handle_error(Reason, Req, _, _) ->
259    {Reason, Req#diameter_base_STR.'Termination-Cause'}.
260
261%% handle_answer/4
262
263handle_answer(Pkt, _Req, Svc, _Peer)
264  when ?IS_CLIENT(Svc) ->
265    #diameter_packet{msg = Rec, errors = []} = Pkt,
266    Rec.
267
268%% handle_request/3
269
270%% Only SERVER3 actually answers.
271handle_request(Pkt, ?SERVER3, {_, Caps}) ->
272    #diameter_packet{header = #diameter_header{is_retransmitted = true},
273                     msg = #diameter_base_STR{'Session-Id' = SId}}
274        = Pkt,
275    #diameter_caps{origin_host  = {OH, _},
276                   origin_realm = {OR, _}}
277        = Caps,
278
279    {reply, #diameter_base_STA{'Result-Code' = ?SUCCESS,
280                               'Session-Id' = SId,
281                               'Origin-Host' = OH,
282                               'Origin-Realm' = OR}};
283
284%% Others kill the transport to force failover.
285handle_request(_, _, {TPid, _}) ->
286    exit(TPid, kill),
287    discard.
288