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