1 // This program does assorted benchmarking of rustls.
2 //
3 // Note: we don't use any of the standard 'cargo bench', 'test::Bencher',
4 // etc. because it's unstable at the time of writing.
5 
6 use std::env;
7 use std::fs;
8 use std::io::{self, Read, Write};
9 use std::sync::Arc;
10 use std::time::{Duration, Instant};
11 
12 use rustls;
13 use rustls::internal::pemfile;
14 use rustls::ClientSessionMemoryCache;
15 use rustls::NoClientSessionStorage;
16 use rustls::NoServerSessionStorage;
17 use rustls::ProtocolVersion;
18 use rustls::ServerSessionMemoryCache;
19 use rustls::Session;
20 use rustls::Ticketer;
21 use rustls::{AllowAnyAuthenticatedClient, NoClientAuth, RootCertStore};
22 use rustls::{ClientConfig, ClientSession};
23 use rustls::{ServerConfig, ServerSession};
24 
25 use webpki;
26 
duration_nanos(d: Duration) -> f6427 fn duration_nanos(d: Duration) -> f64 {
28     (d.as_secs() as f64) + f64::from(d.subsec_nanos()) / 1e9
29 }
30 
_bench<Fsetup, Ftest, S>(count: usize, name: &'static str, f_setup: Fsetup, f_test: Ftest) where Fsetup: Fn() -> S, Ftest: Fn(S),31 fn _bench<Fsetup, Ftest, S>(count: usize, name: &'static str, f_setup: Fsetup, f_test: Ftest)
32 where
33     Fsetup: Fn() -> S,
34     Ftest: Fn(S),
35 {
36     let mut times = Vec::new();
37 
38     for _ in 0..count {
39         let state = f_setup();
40         let start = Instant::now();
41         f_test(state);
42         times.push(duration_nanos(Instant::now().duration_since(start)));
43     }
44 
45     println!("{}", name);
46     println!("{:?}", times);
47 }
48 
time<F>(mut f: F) -> f64 where F: FnMut(),49 fn time<F>(mut f: F) -> f64
50 where
51     F: FnMut(),
52 {
53     let start = Instant::now();
54     f();
55     let end = Instant::now();
56     let dur = duration_nanos(end.duration_since(start));
57     f64::from(dur)
58 }
59 
transfer(left: &mut dyn Session, right: &mut dyn Session) -> f6460 fn transfer(left: &mut dyn Session, right: &mut dyn Session) -> f64 {
61     let mut buf = [0u8; 262144];
62     let mut read_time = 0f64;
63 
64     loop {
65         let mut sz = 0;
66 
67         while left.wants_write() {
68             let written = left
69                 .write_tls(&mut buf[sz..].as_mut())
70                 .unwrap();
71             if written == 0 {
72                 break;
73             }
74 
75             sz += written;
76         }
77 
78         if sz == 0 {
79             return read_time;
80         }
81 
82         let mut offs = 0;
83         loop {
84             let start = Instant::now();
85             offs += right
86                 .read_tls(&mut buf[offs..sz].as_ref())
87                 .unwrap();
88             let end = Instant::now();
89             read_time += f64::from(duration_nanos(end.duration_since(start)));
90             if sz == offs {
91                 break;
92             }
93         }
94     }
95 }
96 
drain(d: &mut dyn Session, expect_len: usize)97 fn drain(d: &mut dyn Session, expect_len: usize) {
98     let mut left = expect_len;
99     let mut buf = [0u8; 8192];
100     loop {
101         let sz = d.read(&mut buf).unwrap();
102         left -= sz;
103         if left == 0 {
104             break;
105         }
106     }
107 }
108 
109 #[derive(PartialEq, Clone, Copy)]
110 enum ClientAuth {
111     No,
112     Yes,
113 }
114 
115 #[derive(PartialEq, Clone, Copy)]
116 enum Resumption {
117     No,
118     SessionID,
119     Tickets,
120 }
121 
122 impl Resumption {
label(&self) -> &'static str123     fn label(&self) -> &'static str {
124         match *self {
125             Resumption::No => "no-resume",
126             Resumption::SessionID => "sessionid",
127             Resumption::Tickets => "tickets",
128         }
129     }
130 }
131 
132 // copied from tests/api.rs
133 #[derive(PartialEq, Clone, Copy, Debug)]
134 enum KeyType {
135     RSA,
136     ECDSA,
137     ED25519,
138 }
139 
140 struct BenchmarkParam {
141     key_type: KeyType,
142     ciphersuite: &'static rustls::SupportedCipherSuite,
143     version: ProtocolVersion,
144 }
145 
146 impl BenchmarkParam {
new( key_type: KeyType, ciphersuite: &'static rustls::SupportedCipherSuite, version: ProtocolVersion, ) -> BenchmarkParam147     const fn new(
148         key_type: KeyType,
149         ciphersuite: &'static rustls::SupportedCipherSuite,
150         version: ProtocolVersion,
151     ) -> BenchmarkParam {
152         BenchmarkParam {
153             key_type,
154             ciphersuite,
155             version,
156         }
157     }
158 }
159 
160 static ALL_BENCHMARKS: &[BenchmarkParam] = &[
161     BenchmarkParam::new(
162         KeyType::RSA,
163         &rustls::ciphersuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
164         ProtocolVersion::TLSv1_2,
165     ),
166     BenchmarkParam::new(
167         KeyType::ECDSA,
168         &rustls::ciphersuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
169         ProtocolVersion::TLSv1_2,
170     ),
171     BenchmarkParam::new(
172         KeyType::RSA,
173         &rustls::ciphersuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
174         ProtocolVersion::TLSv1_2,
175     ),
176     BenchmarkParam::new(
177         KeyType::RSA,
178         &rustls::ciphersuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
179         ProtocolVersion::TLSv1_2,
180     ),
181     BenchmarkParam::new(
182         KeyType::RSA,
183         &rustls::ciphersuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
184         ProtocolVersion::TLSv1_2,
185     ),
186     BenchmarkParam::new(
187         KeyType::ECDSA,
188         &rustls::ciphersuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
189         ProtocolVersion::TLSv1_2,
190     ),
191     BenchmarkParam::new(
192         KeyType::ECDSA,
193         &rustls::ciphersuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
194         ProtocolVersion::TLSv1_2,
195     ),
196     BenchmarkParam::new(
197         KeyType::RSA,
198         &rustls::ciphersuite::TLS13_CHACHA20_POLY1305_SHA256,
199         ProtocolVersion::TLSv1_3,
200     ),
201     BenchmarkParam::new(
202         KeyType::RSA,
203         &rustls::ciphersuite::TLS13_AES_256_GCM_SHA384,
204         ProtocolVersion::TLSv1_3,
205     ),
206     BenchmarkParam::new(
207         KeyType::RSA,
208         &rustls::ciphersuite::TLS13_AES_128_GCM_SHA256,
209         ProtocolVersion::TLSv1_3,
210     ),
211     BenchmarkParam::new(
212         KeyType::ECDSA,
213         &rustls::ciphersuite::TLS13_AES_128_GCM_SHA256,
214         ProtocolVersion::TLSv1_3,
215     ),
216     BenchmarkParam::new(
217         KeyType::ED25519,
218         &rustls::ciphersuite::TLS13_AES_128_GCM_SHA256,
219         ProtocolVersion::TLSv1_3,
220     ),
221 ];
222 
223 impl KeyType {
path_for(&self, part: &str) -> String224     fn path_for(&self, part: &str) -> String {
225         match self {
226             KeyType::RSA => format!("test-ca/rsa/{}", part),
227             KeyType::ECDSA => format!("test-ca/ecdsa/{}", part),
228             KeyType::ED25519 => format!("test-ca/eddsa/{}", part),
229         }
230     }
231 
get_chain(&self) -> Vec<rustls::Certificate>232     fn get_chain(&self) -> Vec<rustls::Certificate> {
233         pemfile::certs(&mut io::BufReader::new(
234             fs::File::open(self.path_for("end.fullchain")).unwrap(),
235         ))
236         .unwrap()
237     }
238 
get_key(&self) -> rustls::PrivateKey239     fn get_key(&self) -> rustls::PrivateKey {
240         pemfile::pkcs8_private_keys(&mut io::BufReader::new(
241             fs::File::open(self.path_for("end.key")).unwrap(),
242         ))
243         .unwrap()[0]
244             .clone()
245     }
246 
get_client_chain(&self) -> Vec<rustls::Certificate>247     fn get_client_chain(&self) -> Vec<rustls::Certificate> {
248         pemfile::certs(&mut io::BufReader::new(
249             fs::File::open(self.path_for("client.fullchain")).unwrap(),
250         ))
251         .unwrap()
252     }
253 
get_client_key(&self) -> rustls::PrivateKey254     fn get_client_key(&self) -> rustls::PrivateKey {
255         pemfile::pkcs8_private_keys(&mut io::BufReader::new(
256             fs::File::open(self.path_for("client.key")).unwrap(),
257         ))
258         .unwrap()[0]
259             .clone()
260     }
261 }
262 
make_server_config( params: &BenchmarkParam, client_auth: ClientAuth, resume: Resumption, mtu: Option<usize>, ) -> ServerConfig263 fn make_server_config(
264     params: &BenchmarkParam,
265     client_auth: ClientAuth,
266     resume: Resumption,
267     mtu: Option<usize>,
268 ) -> ServerConfig {
269     let client_auth = match client_auth {
270         ClientAuth::Yes => {
271             let roots = params.key_type.get_chain();
272             let mut client_auth_roots = RootCertStore::empty();
273             for root in roots {
274                 client_auth_roots.add(&root).unwrap();
275             }
276             AllowAnyAuthenticatedClient::new(client_auth_roots)
277         }
278         ClientAuth::No => NoClientAuth::new(),
279     };
280 
281     let mut cfg = ServerConfig::new(client_auth);
282     cfg.set_single_cert(params.key_type.get_chain(), params.key_type.get_key())
283         .expect("bad certs/private key?");
284 
285     if resume == Resumption::SessionID {
286         cfg.set_persistence(ServerSessionMemoryCache::new(128));
287     } else if resume == Resumption::Tickets {
288         cfg.ticketer = Ticketer::new();
289     } else {
290         cfg.set_persistence(Arc::new(NoServerSessionStorage {}));
291     }
292 
293     cfg.versions.clear();
294     cfg.versions.push(params.version);
295 
296     cfg.mtu = mtu;
297 
298     cfg
299 }
300 
make_client_config( params: &BenchmarkParam, clientauth: ClientAuth, resume: Resumption, ) -> ClientConfig301 fn make_client_config(
302     params: &BenchmarkParam,
303     clientauth: ClientAuth,
304     resume: Resumption,
305 ) -> ClientConfig {
306     let mut cfg = ClientConfig::new();
307     let mut rootbuf =
308         io::BufReader::new(fs::File::open(params.key_type.path_for("ca.cert")).unwrap());
309     cfg.root_store
310         .add_pem_file(&mut rootbuf)
311         .unwrap();
312     cfg.ciphersuites.clear();
313     cfg.ciphersuites
314         .push(params.ciphersuite);
315     cfg.versions.clear();
316     cfg.versions.push(params.version);
317 
318     if clientauth == ClientAuth::Yes {
319         cfg.set_single_client_cert(
320             params.key_type.get_client_chain(),
321             params.key_type.get_client_key(),
322         )
323         .unwrap();
324     }
325 
326     if resume != Resumption::No {
327         cfg.set_persistence(ClientSessionMemoryCache::new(128));
328     } else {
329         cfg.set_persistence(Arc::new(NoClientSessionStorage {}));
330     }
331 
332     cfg
333 }
334 
apply_work_multiplier(work: u64) -> u64335 fn apply_work_multiplier(work: u64) -> u64 {
336     let mul = match env::var("BENCH_MULTIPLIER") {
337         Ok(val) => val
338             .parse::<f64>()
339             .expect("invalid BENCH_MULTIPLIER value"),
340         Err(_) => 1.,
341     };
342 
343     ((work as f64) * mul).round() as u64
344 }
345 
bench_handshake(params: &BenchmarkParam, clientauth: ClientAuth, resume: Resumption)346 fn bench_handshake(params: &BenchmarkParam, clientauth: ClientAuth, resume: Resumption) {
347     let client_config = Arc::new(make_client_config(params, clientauth, resume));
348     let server_config = Arc::new(make_server_config(params, clientauth, resume, None));
349 
350     assert!(
351         params
352             .ciphersuite
353             .usable_for_version(params.version)
354     );
355 
356     let rounds = apply_work_multiplier(if resume == Resumption::No { 512 } else { 4096 });
357     let mut client_time = 0f64;
358     let mut server_time = 0f64;
359 
360     for _ in 0..rounds {
361         let dns_name = webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap();
362         let mut client = ClientSession::new(&client_config, dns_name);
363         let mut server = ServerSession::new(&server_config);
364 
365         server_time += time(|| {
366             transfer(&mut client, &mut server);
367             server.process_new_packets().unwrap()
368         });
369         client_time += time(|| {
370             transfer(&mut server, &mut client);
371             client.process_new_packets().unwrap()
372         });
373         server_time += time(|| {
374             transfer(&mut client, &mut server);
375             server.process_new_packets().unwrap()
376         });
377         client_time += time(|| {
378             transfer(&mut server, &mut client);
379             client.process_new_packets().unwrap()
380         });
381     }
382 
383     println!(
384         "handshakes\t{:?}\t{:?}\t{:?}\tclient\t{}\t{}\t{:.2}\thandshake/s",
385         params.version,
386         params.key_type,
387         params.ciphersuite.suite,
388         if clientauth == ClientAuth::Yes {
389             "mutual"
390         } else {
391             "server-auth"
392         },
393         resume.label(),
394         (rounds as f64) / client_time
395     );
396     println!(
397         "handshakes\t{:?}\t{:?}\t{:?}\tserver\t{}\t{}\t{:.2}\thandshake/s",
398         params.version,
399         params.key_type,
400         params.ciphersuite.suite,
401         if clientauth == ClientAuth::Yes {
402             "mutual"
403         } else {
404             "server-auth"
405         },
406         resume.label(),
407         (rounds as f64) / server_time
408     );
409 }
410 
do_handshake_step(client: &mut ClientSession, server: &mut ServerSession) -> bool411 fn do_handshake_step(client: &mut ClientSession, server: &mut ServerSession) -> bool {
412     if server.is_handshaking() || client.is_handshaking() {
413         transfer(client, server);
414         server.process_new_packets().unwrap();
415         transfer(server, client);
416         client.process_new_packets().unwrap();
417         true
418     } else {
419         false
420     }
421 }
422 
do_handshake(client: &mut ClientSession, server: &mut ServerSession)423 fn do_handshake(client: &mut ClientSession, server: &mut ServerSession) {
424     while do_handshake_step(client, server) {}
425 }
426 
bench_bulk(params: &BenchmarkParam, plaintext_size: u64, mtu: Option<usize>)427 fn bench_bulk(params: &BenchmarkParam, plaintext_size: u64, mtu: Option<usize>) {
428     let client_config = Arc::new(make_client_config(params, ClientAuth::No, Resumption::No));
429     let server_config = Arc::new(make_server_config(
430         params,
431         ClientAuth::No,
432         Resumption::No,
433         mtu,
434     ));
435 
436     let dns_name = webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap();
437     let mut client = ClientSession::new(&client_config, dns_name);
438     let mut server = ServerSession::new(&server_config);
439 
440     do_handshake(&mut client, &mut server);
441 
442     let mut buf = Vec::new();
443     buf.resize(plaintext_size as usize, 0u8);
444 
445     let total_data = apply_work_multiplier(if plaintext_size < 8192 {
446         64 * 1024 * 1024
447     } else {
448         1024 * 1024 * 1024
449     });
450     let rounds = total_data / plaintext_size;
451     let mut time_send = 0f64;
452     let mut time_recv = 0f64;
453 
454     for _ in 0..rounds {
455         time_send += time(|| {
456             server.write_all(&buf).unwrap();
457             ()
458         });
459 
460         time_recv += transfer(&mut server, &mut client);
461 
462         time_recv += time(|| client.process_new_packets().unwrap());
463         drain(&mut client, buf.len());
464     }
465 
466     let mtu_str = format!(
467         "mtu:{}",
468         mtu.map(|v| v.to_string())
469             .unwrap_or("default".to_string())
470     );
471     let total_mbs = ((plaintext_size * rounds) as f64) / (1024. * 1024.);
472     println!(
473         "bulk\t{:?}\t{:?}\t{}\tsend\t{:.2}\tMB/s",
474         params.version,
475         params.ciphersuite.suite,
476         mtu_str,
477         total_mbs / time_send
478     );
479     println!(
480         "bulk\t{:?}\t{:?}\t{}\trecv\t{:.2}\tMB/s",
481         params.version,
482         params.ciphersuite.suite,
483         mtu_str,
484         total_mbs / time_recv
485     );
486 }
487 
bench_memory(params: &BenchmarkParam, session_count: u64)488 fn bench_memory(params: &BenchmarkParam, session_count: u64) {
489     let client_config = Arc::new(make_client_config(params, ClientAuth::No, Resumption::No));
490     let server_config = Arc::new(make_server_config(
491         params,
492         ClientAuth::No,
493         Resumption::No,
494         None,
495     ));
496 
497     // The target here is to end up with session_count post-handshake
498     // server and client sessions.
499     let session_count = (session_count / 2) as usize;
500     let mut servers = Vec::with_capacity(session_count);
501     let mut clients = Vec::with_capacity(session_count);
502 
503     for _i in 0..session_count {
504         servers.push(ServerSession::new(&server_config));
505         let dns_name = webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap();
506         clients.push(ClientSession::new(&client_config, dns_name));
507     }
508 
509     for _step in 0..5 {
510         for (mut client, mut server) in clients
511             .iter_mut()
512             .zip(servers.iter_mut())
513         {
514             do_handshake_step(&mut client, &mut server);
515         }
516     }
517 
518     for client in clients.iter_mut() {
519         client.write_all(&[0u8; 1024]).unwrap();
520     }
521 
522     for (client, server) in clients
523         .iter_mut()
524         .zip(servers.iter_mut())
525     {
526         transfer(client, server);
527         let mut buf = [0u8; 1024];
528         server.read(&mut buf).unwrap();
529     }
530 }
531 
lookup_matching_benches(name: &str) -> Vec<&BenchmarkParam>532 fn lookup_matching_benches(name: &str) -> Vec<&BenchmarkParam> {
533     let r: Vec<&BenchmarkParam> = ALL_BENCHMARKS
534         .iter()
535         .filter(|params| {
536             format!("{:?}", params.ciphersuite.suite).to_lowercase() == name.to_lowercase()
537         })
538         .collect();
539 
540     if r.is_empty() {
541         panic!("unknown suite {:?}", name);
542     }
543 
544     r
545 }
546 
selected_tests(mut args: env::Args)547 fn selected_tests(mut args: env::Args) {
548     let mode = args
549         .next()
550         .expect("first argument must be mode");
551 
552     match mode.as_ref() {
553         "bulk" => match args.next() {
554             Some(suite) => {
555                 let len = args
556                     .next()
557                     .map(|arg| {
558                         arg.parse::<u64>()
559                             .expect("3rd arg must be plaintext size integer")
560                     })
561                     .unwrap_or(1048576);
562                 let mtu = args.next().map(|arg| {
563                     arg.parse::<usize>()
564                         .expect("4th arg must be mtu integer")
565                 });
566                 for param in lookup_matching_benches(&suite).iter() {
567                     bench_bulk(param, len, mtu);
568                 }
569             }
570             None => {
571                 panic!("bulk needs ciphersuite argument");
572             }
573         },
574 
575         "handshake" | "handshake-resume" | "handshake-ticket" => match args.next() {
576             Some(suite) => {
577                 let resume = if mode == "handshake" {
578                     Resumption::No
579                 } else if mode == "handshake-resume" {
580                     Resumption::SessionID
581                 } else {
582                     Resumption::Tickets
583                 };
584 
585                 for param in lookup_matching_benches(&suite).iter() {
586                     bench_handshake(param, ClientAuth::No, resume);
587                 }
588             }
589             None => {
590                 panic!("handshake* needs ciphersuite argument");
591             }
592         },
593 
594         "memory" => match args.next() {
595             Some(suite) => {
596                 let count = args
597                     .next()
598                     .map(|arg| {
599                         arg.parse::<u64>()
600                             .expect("3rd arg must be session count integer")
601                     })
602                     .unwrap_or(1000000);
603                 for param in lookup_matching_benches(&suite).iter() {
604                     bench_memory(param, count);
605                 }
606             }
607             None => {
608                 panic!("memory needs ciphersuite argument");
609             }
610         },
611 
612         _ => {
613             panic!("unsupported mode {:?}", mode);
614         }
615     }
616 }
617 
all_tests()618 fn all_tests() {
619     for test in ALL_BENCHMARKS.iter() {
620         bench_bulk(test, 1024 * 1024, None);
621         bench_bulk(test, 1024 * 1024, Some(10000));
622         bench_handshake(test, ClientAuth::No, Resumption::No);
623         bench_handshake(test, ClientAuth::Yes, Resumption::No);
624         bench_handshake(test, ClientAuth::No, Resumption::SessionID);
625         bench_handshake(test, ClientAuth::Yes, Resumption::SessionID);
626         bench_handshake(test, ClientAuth::No, Resumption::Tickets);
627         bench_handshake(test, ClientAuth::Yes, Resumption::Tickets);
628     }
629 }
630 
main()631 fn main() {
632     let mut args = env::args();
633     if args.len() > 1 {
634         args.next();
635         selected_tests(args);
636     } else {
637         all_tests();
638     }
639 }
640