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 six Diameter nodes connected as follows.
23%%
24%%                  ---- SERVER.REALM1  (TLS after capabilities exchange)
25%%                /
26%%               /  ---- SERVER.REALM2  (ditto)
27%%              | /
28%%   CLIENT.REALM0 ----- SERVER.REALM3  (no security)
29%%              | \
30%%               \  ---- SERVER.REALM4  (TLS at connection establishment)
31%%                \
32%%                  ---- SERVER.REALM5  (ditto)
33%%
34
35-module(diameter_tls_SUITE).
36
37-export([suite/0,
38         all/0,
39         groups/0,
40         init_per_suite/1,
41         end_per_suite/1]).
42
43%% testcases
44-export([start_ssl/1,
45         start_diameter/1,
46         make_certs/1, make_certs/0,
47         start_services/1,
48         add_transports/1,
49         send1/1,
50         send2/1,
51         send3/1,
52         send4/1,
53         send5/1,
54         remove_transports/1,
55         stop_services/1,
56         stop_diameter/1,
57         stop_ssl/1]).
58
59%% diameter callbacks
60-export([prepare_request/3,
61         prepare_retransmit/3,
62         handle_answer/4,
63         handle_request/3]).
64
65-include("diameter.hrl").
66-include("diameter_gen_base_rfc3588.hrl").
67
68%% ===========================================================================
69
70-define(util, diameter_util).
71
72-define(ADDR, {127,0,0,1}).
73
74-define(CLIENT,  "CLIENT.REALM0").
75-define(SERVER1, "SERVER.REALM1").
76-define(SERVER2, "SERVER.REALM2").
77-define(SERVER3, "SERVER.REALM3").
78-define(SERVER4, "SERVER.REALM4").
79-define(SERVER5, "SERVER.REALM5").
80
81-define(SERVERS, [?SERVER1, ?SERVER2, ?SERVER3, ?SERVER4, ?SERVER5]).
82
83-define(DICT_COMMON,  ?DIAMETER_DICT_COMMON).
84
85-define(APP_ALIAS, the_app).
86-define(APP_ID, ?DICT_COMMON:id()).
87
88-define(NO_INBAND_SECURITY, 0).
89-define(TLS, 1).
90
91%% Config for diameter:start_service/2.
92-define(SERVICE(Host, Dict),
93        [{'Origin-Host', Host},
94         {'Origin-Realm', realm(Host)},
95         {'Host-IP-Address', [?ADDR]},
96         {'Vendor-Id', 12345},
97         {'Product-Name', "OTP/diameter"},
98         {'Inband-Security-Id', [?NO_INBAND_SECURITY]},
99         {'Auth-Application-Id', [Dict:id()]},
100         {application, [{alias, ?APP_ALIAS},
101                        {dictionary, Dict},
102                        {module, #diameter_callback{peer_up = false,
103                                                    peer_down = false,
104                                                    pick_peer = false,
105                                                    handle_error = false,
106                                                    default = ?MODULE}},
107                        {answer_errors, callback}]}]).
108
109%% Config for diameter:add_transport/2. In the listening case, listen
110%% on a free port that we then lookup using the implementation detail
111%% that diameter_tcp registers the port with diameter_reg.
112-define(CONNECT(PortNr, Caps, Opts),
113        {connect, [{transport_module, diameter_tcp},
114                   {transport_config, [{raddr, ?ADDR},
115                                       {rport, PortNr},
116                                       {ip, ?ADDR},
117                                       {port, 0}
118                                       | Opts]},
119                   {capabilities, Caps}]}).
120-define(LISTEN(Caps, Opts),
121        {listen, [{transport_module, diameter_tcp},
122                  {transport_config, [{ip, ?ADDR}, {port, 0} | Opts]},
123                  {capabilities, Caps}]}).
124
125-define(SUCCESS, 2001).
126-define(LOGOUT, ?'DIAMETER_BASE_TERMINATION-CAUSE_LOGOUT').
127
128%% ===========================================================================
129
130suite() ->
131    [{timetrap, {seconds, 60}}].
132
133all() ->
134    [start_ssl,
135     start_diameter,
136     make_certs,
137     start_services,
138     add_transports,
139     {group, all},
140     {group, all, [parallel]},
141     remove_transports,
142     stop_services,
143     stop_diameter,
144     stop_ssl].
145
146groups() ->
147    [{all, [], tc()}].
148
149%% Shouldn't really have to know about crypto here but 'ok' from
150%% ssl:start() isn't enough to guarantee that TLS is available.
151init_per_suite(Config) ->
152    try
153        false /= os:find_executable("openssl")
154            orelse throw({?MODULE, no_openssl}),
155        ok == (catch crypto:start())
156            orelse throw({?MODULE, no_crypto}),
157        Config
158    catch
159        {?MODULE, E} ->
160            {skip, E}
161    end.
162
163end_per_suite(_Config) ->
164    crypto:stop().
165
166%% Testcases to run when services are started and connections
167%% established.
168tc() ->
169    [send1,
170     send2,
171     send3,
172     send4,
173     send5].
174
175%% ===========================================================================
176%% testcases
177
178start_ssl(_Config) ->
179    ok = ssl:start().
180
181start_diameter(_Config) ->
182    ok = diameter:start().
183
184make_certs() ->
185    [{timetrap, {minutes, 2}}].
186
187make_certs(Config) ->
188    Dir = proplists:get_value(priv_dir, Config),
189
190    [] = ?util:run([[fun make_cert/2, Dir, B] || B <- ["server1",
191                                                       "server2",
192                                                       "server4",
193                                                       "server5",
194                                                       "client"]]).
195
196start_services(Config) ->
197    Dir = proplists:get_value(priv_dir, Config),
198    Servers = [server(S, sopts(S, Dir)) || S <- ?SERVERS],
199
200    ok = diameter:start_service(?CLIENT, ?SERVICE(?CLIENT, ?DICT_COMMON)),
201
202    {save_config, [Dir | Servers]}.
203
204add_transports(Config) ->
205    {_, [Dir | Servers]} = proplists:get_value(saved_config, Config),
206
207    true = diameter:subscribe(?CLIENT),
208
209    Opts = ssl_options(Dir, "client"),
210    Connections = [connect(?CLIENT, S, copts(N, Opts))
211                   || {S,N} <- lists:zip(Servers, ?SERVERS)],
212
213    ?util:write_priv(Config, "cfg", lists:zip(Servers, Connections)).
214
215
216%% Remove the client transports and expect the corresponding server
217%% transport to go down.
218remove_transports(Config) ->
219    Ts = ?util:read_priv(Config, "cfg"),
220    [] = [T || S <- ?SERVERS, T <- [diameter:subscribe(S)], T /= true],
221    lists:map(fun disconnect/1, Ts).
222
223stop_services(_Config) ->
224    [] = [{H,T} || H <- [?CLIENT | ?SERVERS],
225                   T <- [diameter:stop_service(H)],
226                   T /= ok].
227
228stop_diameter(_Config) ->
229    ok = diameter:stop().
230
231stop_ssl(_Config) ->
232    ok = ssl:stop().
233
234%% Send an STR intended for a specific server and expect success.
235send1(_Config) ->
236    call(?SERVER1).
237send2(_Config) ->
238    call(?SERVER2).
239send3(_Config) ->
240    call(?SERVER3).
241send4(_Config) ->
242    call(?SERVER4).
243send5(_Config) ->
244    call(?SERVER5).
245
246%% ===========================================================================
247%% diameter callbacks
248
249%% prepare_request/3
250
251prepare_request(#diameter_packet{msg = Req},
252                ?CLIENT,
253                {_Ref, Caps}) ->
254    #diameter_caps{origin_host  = {OH, _},
255                   origin_realm = {OR, _}}
256        = Caps,
257
258    {send, set(Req, [{'Session-Id', diameter:session_id(OH)},
259                     {'Origin-Host',  OH},
260                     {'Origin-Realm', OR}])}.
261
262%% prepare_retransmit/3
263
264prepare_retransmit(_Pkt, false, _Peer) ->
265    discard.
266
267%% handle_answer/4
268
269handle_answer(Pkt, _Req, ?CLIENT, _Peer) ->
270    #diameter_packet{msg = Rec, errors = []} = Pkt,
271    Rec.
272
273%% handle_request/3
274
275handle_request(#diameter_packet{msg = #diameter_base_STR{'Session-Id' = SId}},
276               OH,
277               {_Ref, #diameter_caps{origin_host = {OH,_},
278                                     origin_realm = {OR, _}}})
279  when OH /= ?CLIENT ->
280    {reply, #diameter_base_STA{'Result-Code' = ?SUCCESS,
281                               'Session-Id' = SId,
282                               'Origin-Host' = OH,
283                               'Origin-Realm' = OR}}.
284
285%% ===========================================================================
286%% support functions
287
288call(Server) ->
289    Realm = realm(Server),
290    Req = ['STR', {'Destination-Realm', Realm},
291                  {'Termination-Cause', ?LOGOUT},
292                  {'Auth-Application-Id', ?APP_ID}],
293    #diameter_base_STA{'Result-Code' = ?SUCCESS,
294                       'Origin-Host' = Server,
295                       'Origin-Realm' = Realm}
296        = call(Req, [{filter, realm}]).
297
298call(Req, Opts) ->
299    diameter:call(?CLIENT, ?APP_ALIAS, Req, Opts).
300
301set([H|T], Vs) ->
302    [H | Vs ++ T].
303
304disconnect({{LRef, _PortNr}, CRef}) ->
305    ok = diameter:remove_transport(?CLIENT, CRef),
306    receive #diameter_event{info = {down, LRef, _, _}} -> ok end.
307
308realm(Host) ->
309    tl(lists:dropwhile(fun(C) -> C /= $. end, Host)).
310
311inband_security(Ids) ->
312    [{'Inband-Security-Id', Ids}].
313
314ssl_options(Dir, Base) ->
315    Root = filename:join([Dir, Base]),
316    [{ssl_options, [{certfile, Root ++ "_ca.pem"},
317                    {keyfile,  Root ++ "_key.pem"}]}].
318
319make_cert(Dir, Base) ->
320    make_cert(Dir, Base ++ "_key.pem", Base ++ "_ca.pem").
321
322make_cert(Dir, Keyfile, Certfile) ->
323    [KP,CP] = [filename:join([Dir, F]) || F <- [Keyfile, Certfile]],
324
325    KC = join(["openssl genrsa -out", KP, "2048"]),
326    CC = join(["openssl req -new -x509 -key", KP, "-out", CP, "-days 7",
327               "-subj /C=SE/ST=./L=Stockholm/CN=www.erlang.org"]),
328
329    %% Hope for the best and only check that files are written.
330    KR = os:cmd(KC),
331    {_, {ok, _}} = {KR, file:read_file_info(KP)},
332    CR = os:cmd(CC),
333    {_, {ok, _}} = {CR, file:read_file_info(CP)},
334
335    {KP,CP}.
336
337join(Strs) ->
338    string:join(Strs, " ").
339
340%% server/2
341
342server(Host, {Caps, Opts}) ->
343    ok = diameter:start_service(Host, ?SERVICE(Host, ?DICT_COMMON)),
344    {ok, LRef} = diameter:add_transport(Host, ?LISTEN(Caps, Opts)),
345    {LRef, hd([_] = ?util:lport(tcp, LRef))}.
346
347sopts(?SERVER1, Dir) ->
348    {inband_security([?TLS]),
349     ssl_options(Dir, "server1")};
350sopts(?SERVER2, Dir) ->
351    {inband_security([?NO_INBAND_SECURITY, ?TLS]),
352     ssl_options(Dir, "server2")};
353sopts(?SERVER3, _) ->
354    {[], []};
355sopts(?SERVER4, Dir) ->
356    {[], ssl(ssl_options(Dir, "server4"))};
357sopts(?SERVER5, Dir) ->
358    {[], ssl(ssl_options(Dir, "server5"))}.
359
360ssl([{ssl_options = T, Opts}]) ->
361    [{T, true} | Opts].
362
363%% connect/3
364
365connect(Host, {_LRef, PortNr}, {Caps, Opts}) ->
366    {ok, Ref} = diameter:add_transport(Host, ?CONNECT(PortNr, Caps, Opts)),
367    receive
368        #diameter_event{service = Host,
369                        info = {up, Ref, _, _, #diameter_packet{}}} ->
370            ok
371    end,
372    Ref.
373
374copts(S, Opts)
375  when S == ?SERVER1;
376       S == ?SERVER2;
377       S == ?SERVER3 ->
378    {inband_security([?NO_INBAND_SECURITY, ?TLS]), Opts};
379copts(S, Opts)
380  when S == ?SERVER4;
381       S == ?SERVER5 ->
382    {[], ssl(Opts)}.
383