1%%%-------------------------------------------------------------------
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2015-2020. 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-module(ssh_bench_SUITE).
21
22-export([
23         suite/0,
24         all/0,
25         init_per_suite/1,
26         end_per_suite/1,
27         init_per_testcase/2,
28         end_per_testcase/2
29        ]).
30
31-export([
32         connect/1,
33         transfer_text/1,
34         send_wait_acc/3
35        ]).
36
37-include_lib("common_test/include/ct_event.hrl").
38-include_lib("common_test/include/ct.hrl").
39
40-include("ssh.hrl").
41-include("ssh_transport.hrl").
42-include("ssh_connect.hrl").
43-include("ssh_auth.hrl").
44
45%%%================================================================
46%%%
47%%% Suite declarations
48%%%
49
50suite() -> [{ct_hooks,[{ts_install_cth,[{nodenames,2}]}]},
51	    {timetrap,{minutes,1}}
52	   ].
53all() -> [connect,
54          transfer_text
55	 ].
56
57-define(UID, "foo").
58-define(PWD, "bar").
59-define(Nruns, 8).
60
61%%%================================================================
62%%%
63%%% Init per suite
64%%%
65
66init_per_suite(Config) ->
67    catch ssh:stop(),
68    try
69	ok = ssh:start()
70    of
71        ok ->
72            DataSize = 1000000,
73            SystemDir = proplists:get_value(data_dir, Config),
74            Algs = ssh:default_algorithms(),
75            {_ServerPid, _Host, Port} =
76                ssh_test_lib:daemon([{system_dir, SystemDir},
77                                     {user_passwords, [{?UID,?PWD}]},
78                                     {failfun, fun ssh_test_lib:failfun/2},
79                                     {preferred_algorithms, Algs},
80                                     {modify_algorithms,[{prepend,[{cipher,[none]},
81                                                                   {mac,[none]}
82                                                                  ]}
83                                                         %% ,{rm, [{cipher,['aes256-gcm@openssh.com',
84                                                         %%                'aes128-gcm@openssh.com']}
85                                                         %%      ]}
86                                                        ]},
87                                     {max_random_length_padding, 0},
88                                     {subsystems, [{"/dev/null", {ssh_bench_dev_null,[DataSize]}}]}
89                                    ]),
90            [{host,"localhost"}, {port,Port}, {uid,?UID}, {pwd,?PWD}, {data_size,DataSize} | Config]
91    catch
92	C:E ->
93	    {skip, io_lib:format("Couldn't start ~p:~p",[C,E])}
94    end.
95
96end_per_suite(_Config) ->
97    catch ssh:stop(),
98    ok.
99
100%%%================================================================
101%%%
102%%% Init per testcase
103%%%
104
105init_per_testcase(_Func, Conf) ->
106    Conf.
107
108end_per_testcase(_Func, _Conf) ->
109    ok.
110
111%%%================================================================
112%%%
113%%% Testcases
114%%%
115
116%%%----------------------------------------------------------------
117%%% Measure the time for an Erlang client to connect to an Erlang
118%%% server on the localhost
119
120connect(Config) ->
121    KexAlgs = proplists:get_value(kex, ssh:default_algorithms()),
122    ct:log("KexAlgs = ~p",[KexAlgs]),
123    lists:foreach(
124      fun(KexAlg) ->
125              PrefAlgs = preferred_algorithms(KexAlg),
126              TimeMicroSec =  measure_connect(Config,
127                                              [{preferred_algorithms,PrefAlgs}]),
128              report(["Connect erlc erld ",KexAlg," [connects per sec]"],
129                     1000000 / TimeMicroSec)
130      end, KexAlgs).
131
132
133measure_connect(Config, Opts) ->
134    Port = proplists:get_value(port, Config),
135    ConnectOptions = [{user,     proplists:get_value(uid,      Config)},
136                      {password, proplists:get_value(pwd,      Config)},
137                      {user_dir, proplists:get_value(priv_dir, Config)},
138                      {silently_accept_hosts, true},
139                      {user_interaction, false},
140                      {max_random_length_padding, 0}
141                     ] ++ Opts,
142    median(
143      [begin
144           {Time, {ok,Pid}} = timer:tc(ssh,connect,["localhost", Port, ConnectOptions]),
145           ssh:close(Pid),
146           Time % in µs
147       end || _ <- lists:seq(1,?Nruns)]).
148
149%%%----------------------------------------------------------------
150%%% Measure the time to transfer a set of data with
151%%% and without crypto
152
153transfer_text(Config) ->
154    Port = proplists:get_value(port, Config),
155    Options = [{user,     proplists:get_value(uid,      Config)},
156               {password, proplists:get_value(pwd,      Config)},
157               {user_dir, proplists:get_value(priv_dir, Config)},
158               {silently_accept_hosts, true},
159               {save_accepted_host, false},
160               {user_interaction, false},
161               {max_random_length_padding, 0}
162              ],
163    Data = gen_data(proplists:get_value(data_size,Config)),
164
165    [connect_measure(Port, Crypto, Mac, Data, Options)
166     || {Crypto,Mac} <- [{        none,                    none},
167                         {'aes128-ctr',             'hmac-sha1'},
168                         {'aes256-ctr',             'hmac-sha1'},
169{'aes128-gcm@openssh.com', 'hmac-sha1'},
170{'chacha20-poly1305@openssh.com', 'hmac-sha1'},
171                         {'aes128-cbc',             'hmac-sha1'},
172                         {'3des-cbc',               'hmac-sha1'},
173                         {'aes128-ctr',             'hmac-sha2-256'},
174                         {'aes128-ctr',             'hmac-sha2-512'}
175                        ],
176        crypto_mac_supported(Crypto,Mac)].
177
178
179crypto_mac_supported(none, none) ->
180    true;
181crypto_mac_supported(C, M) ->
182    Algs = ssh:default_algorithms(),
183    [{_,Cs},_] = proplists:get_value(cipher, Algs),
184    [{_,Ms},_] = proplists:get_value(mac, Algs),
185    lists:member(C,Cs) andalso lists:member(M,Ms).
186
187
188gen_data(DataSz) ->
189    Data0 = << <<C>> || _ <- lists:seq(1,DataSz div 256),
190                        C <- lists:seq(0,255) >>,
191    Data1 = << <<C>> || C <- lists:seq(0,(DataSz rem 256) - 1) >>,
192    <<Data0/binary, Data1/binary>>.
193
194
195connect_measure(Port, Cipher, Mac, Data, Options) ->
196    _AES_GCM = {cipher,
197               []},
198               %% ['aes256-gcm@openssh.com',
199               %%  'aes128-gcm@openssh.com']},
200
201    AlgOpt = case {Cipher,Mac} of
202                 {none,none} ->
203                     [{modify_algorithms,[{prepend, [{cipher,[Cipher]},
204                                                     {mac,[Mac]}]}
205%%%                                          ,{rm,[_AES_GCM]}
206                                         ]}];
207                 {none,_} ->
208                     [{modify_algorithms,[{prepend, [{cipher,[Cipher]}]}
209%%%                                          ,{rm,[_AES_GCM]}
210                                         ]},
211                      {preferred_algorithms, [{mac,[Mac]}]}];
212                 {_,none} ->
213                     [{modify_algorithms,[{prepend, [{mac,[Mac]}]}
214%%%                                          ,{rm,[_AES_GCM]}
215                                         ]},
216                      {preferred_algorithms, [{cipher,[Cipher]}]}];
217                 _ ->
218                     [{preferred_algorithms, [{cipher,[Cipher]},
219                                              {mac,[Mac]}]}
220%%%                      ,{modify_algorithms, [{rm,[_AES_GCM]}]}
221                     ]
222             end,
223    Times =
224        [begin
225             {ok,C} = ssh:connect("localhost", Port, AlgOpt ++ Options),
226             {ok,Ch} = ssh_connection:session_channel(C, 10000),
227             success = ssh_connection:subsystem(C, Ch, "/dev/null", 10000),
228             {Time,ok} = timer:tc(?MODULE, send_wait_acc, [C, Ch, Data]),
229             ok = ssh_connection:send_eof(C, Ch),
230             ssh:close(C),
231             Time
232         end || _ <- lists:seq(1,?Nruns)],
233    report(["Transfer ",Cipher,"/",Mac," [Mbyte per sec]"],
234           1000000 / median(Times)).
235
236send_wait_acc(C, Ch, Data) ->
237    ssh_connection:send(C, Ch, Data),
238    receive
239        {ssh_cm, C, {data, Ch, 0, <<"READY">>}} -> ok
240    end.
241
242
243%%%================================================================
244%%%
245%%% Private
246%%%
247
248%%%----------------------------------------------------------------
249preferred_algorithms(KexAlg) ->
250     [{kex,         [KexAlg]},
251      {public_key,  ['rsa-sha2-256']},
252      {cipher,      ['aes128-ctr']},
253      {mac,         ['hmac-sha1']},
254      {compression, [none]}
255     ].
256
257%%%----------------------------------------------------------------
258median(Data) when is_list(Data) ->
259    SortedData = lists:sort(Data),
260    N = length(Data),
261    Median =
262        case N rem 2 of
263            0 ->
264                MeanOfMiddle = (lists:nth(N div 2, SortedData) +
265                                    lists:nth(N div 2 + 1, SortedData)) / 2,
266                round(MeanOfMiddle);
267            1 ->
268                lists:nth(N div 2 + 1, SortedData)
269        end,
270    ct:log("median(~p) = ~p",[SortedData,Median]),
271    Median.
272
273%%%----------------------------------------------------------------
274report(LabelList, Value) ->
275    Label = report_chars(lists:concat(LabelList)),
276    ct:log("ct_event:notify ~p: ~p", [Label, Value]),
277    ct_event:notify(
278      #event{name = benchmark_data,
279             data = [{suite, ?MODULE},
280                     {name,  Label},
281                     {value, Value}]}).
282
283report_chars(Cs) ->
284    [case C of
285         $- -> $_;
286         _ -> C
287     end || C <- Cs].
288
289