1use crate::args::*;
28use crate::common::*;
29
30use std::io::prelude::*;
31
32use std::rc::Rc;
33
34use std::cell::RefCell;
35
36use ring::rand::*;
37
38const MAX_DATAGRAM_SIZE: usize = 1350;
39
40#[derive(Debug)]
41pub enum ClientError {
42 HandshakeFail,
43 HttpFail,
44 Other(String),
45}
46
47pub fn connect(
48 args: ClientArgs, conn_args: CommonArgs,
49 output_sink: impl FnMut(String) + 'static,
50) -> Result<(), ClientError> {
51 let mut buf = [0; 65535];
52 let mut out = [0; MAX_DATAGRAM_SIZE];
53
54 let output_sink =
55 Rc::new(RefCell::new(output_sink)) as Rc<RefCell<dyn FnMut(_)>>;
56
57 let mut poll = mio::Poll::new().unwrap();
59 let mut events = mio::Events::with_capacity(1024);
60
61 let connect_url = &args.urls[0];
63
64 let peer_addr = if let Some(addr) = &args.connect_to {
66 addr.parse().expect("--connect-to is expected to be a string containing an IPv4 or IPv6 address with a port. E.g. 192.0.2.0:443")
67 } else {
68 *connect_url.socket_addrs(|| None).unwrap().first().unwrap()
69 };
70
71 let bind_addr = match peer_addr {
75 std::net::SocketAddr::V4(_) => format!("0.0.0.0:{}", args.source_port),
76 std::net::SocketAddr::V6(_) => format!("[::]:{}", args.source_port),
77 };
78
79 let mut socket =
82 mio::net::UdpSocket::bind(bind_addr.parse().unwrap()).unwrap();
83 poll.registry()
84 .register(&mut socket, mio::Token(0), mio::Interest::READABLE)
85 .unwrap();
86
87 let migrate_socket = if args.perform_migration {
88 let mut socket =
89 mio::net::UdpSocket::bind(bind_addr.parse().unwrap()).unwrap();
90 poll.registry()
91 .register(&mut socket, mio::Token(1), mio::Interest::READABLE)
92 .unwrap();
93
94 Some(socket)
95 } else {
96 None
97 };
98
99 let mut config = quiche::Config::new(args.version).unwrap();
101
102 if let Some(ref trust_origin_ca_pem) = args.trust_origin_ca_pem {
103 config
104 .load_verify_locations_from_file(trust_origin_ca_pem)
105 .map_err(|e| {
106 ClientError::Other(format!("error loading origin CA file : {e}"))
107 })?;
108 } else {
109 config.verify_peer(!args.no_verify);
110 }
111
112 config.set_application_protos(&conn_args.alpns).unwrap();
113
114 config.set_max_idle_timeout(conn_args.idle_timeout);
115 config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE);
116 config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE);
117 config.set_initial_max_data(conn_args.max_data);
118 config.set_initial_max_stream_data_bidi_local(conn_args.max_stream_data);
119 config.set_initial_max_stream_data_bidi_remote(conn_args.max_stream_data);
120 config.set_initial_max_stream_data_uni(conn_args.max_stream_data);
121 config.set_initial_max_streams_bidi(conn_args.max_streams_bidi);
122 config.set_initial_max_streams_uni(conn_args.max_streams_uni);
123 config.set_disable_active_migration(!conn_args.enable_active_migration);
124 config.set_active_connection_id_limit(conn_args.max_active_cids);
125
126 config.set_max_connection_window(conn_args.max_window);
127 config.set_max_stream_window(conn_args.max_stream_window);
128
129 let mut keylog = None;
130
131 if let Some(keylog_path) = std::env::var_os("SSLKEYLOGFILE") {
132 let file = std::fs::OpenOptions::new()
133 .create(true)
134 .append(true)
135 .open(keylog_path)
136 .unwrap();
137
138 keylog = Some(file);
139
140 config.log_keys();
141 }
142
143 if conn_args.no_grease {
144 config.grease(false);
145 }
146
147 if conn_args.early_data {
148 config.enable_early_data();
149 }
150
151 config
152 .set_cc_algorithm_name(&conn_args.cc_algorithm)
153 .unwrap();
154
155 if conn_args.disable_hystart {
156 config.enable_hystart(false);
157 }
158
159 if conn_args.dgrams_enabled {
160 config.enable_dgram(true, 1000, 1000);
161 }
162
163 let mut http_conn: Option<Box<dyn HttpConn>> = None;
164
165 let mut app_proto_selected = false;
166
167 let rng = SystemRandom::new();
169
170 let scid = if !cfg!(feature = "fuzzing") {
171 let mut conn_id = [0; quiche::MAX_CONN_ID_LEN];
172 rng.fill(&mut conn_id[..]).unwrap();
173
174 conn_id.to_vec()
175 } else {
176 [0; quiche::MAX_CONN_ID_LEN].to_vec()
178 };
179
180 let scid = quiche::ConnectionId::from_ref(&scid);
181
182 let local_addr = socket.local_addr().unwrap();
183
184 let mut conn = quiche::connect(
186 connect_url.domain(),
187 &scid,
188 local_addr,
189 peer_addr,
190 &mut config,
191 )
192 .unwrap();
193
194 if let Some(keylog) = &mut keylog {
195 if let Ok(keylog) = keylog.try_clone() {
196 conn.set_keylog(Box::new(keylog));
197 }
198 }
199
200 #[cfg(feature = "qlog")]
202 {
203 if let Some(dir) = std::env::var_os("QLOGDIR") {
204 let id = format!("{scid:?}");
205 let writer = make_qlog_writer(&dir, "client", &id);
206
207 conn.set_qlog(
208 std::boxed::Box::new(writer),
209 "quiche-client qlog".to_string(),
210 format!("{} id={}", "quiche-client qlog", id),
211 );
212 }
213 }
214
215 if let Some(session_file) = &args.session_file {
216 if let Ok(session) = std::fs::read(session_file) {
217 conn.set_session(&session).ok();
218 }
219 }
220
221 info!(
222 "connecting to {:} from {:} with scid {:?}",
223 peer_addr,
224 socket.local_addr().unwrap(),
225 scid,
226 );
227
228 let (write, send_info) = conn.send(&mut out).expect("initial send failed");
229
230 while let Err(e) = socket.send_to(&out[..write], send_info.to) {
231 if e.kind() == std::io::ErrorKind::WouldBlock {
232 trace!(
233 "{} -> {}: send() would block",
234 socket.local_addr().unwrap(),
235 send_info.to
236 );
237 continue;
238 }
239
240 return Err(ClientError::Other(format!("send() failed: {e:?}")));
241 }
242
243 trace!("written {write}");
244
245 let app_data_start = std::time::Instant::now();
246
247 let mut pkt_count = 0;
248
249 let mut scid_sent = false;
250 let mut new_path_probed = false;
251 let mut migrated = false;
252
253 loop {
254 if !conn.is_in_early_data() || app_proto_selected {
255 poll.poll(&mut events, conn.timeout()).unwrap();
256 }
257
258 if events.is_empty() {
262 trace!("timed out");
263
264 conn.on_timeout();
265 }
266
267 for event in &events {
270 let socket = match event.token() {
271 mio::Token(0) => &socket,
272
273 mio::Token(1) => migrate_socket.as_ref().unwrap(),
274
275 _ => unreachable!(),
276 };
277
278 let local_addr = socket.local_addr().unwrap();
279 'read: loop {
280 let (len, from) = match socket.recv_from(&mut buf) {
281 Ok(v) => v,
282
283 Err(e) => {
284 if e.kind() == std::io::ErrorKind::WouldBlock {
287 trace!("{local_addr}: recv() would block");
288 break 'read;
289 }
290
291 return Err(ClientError::Other(format!(
292 "{local_addr}: recv() failed: {e:?}"
293 )));
294 },
295 };
296
297 trace!("got {len} bytes from {from} to {local_addr}");
298
299 if let Some(target_path) = conn_args.dump_packet_path.as_ref() {
300 let path = format!("{target_path}/{pkt_count}.pkt");
301
302 if let Ok(f) = std::fs::File::create(path) {
303 let mut f = std::io::BufWriter::new(f);
304 f.write_all(&buf[..len]).ok();
305 }
306 }
307
308 pkt_count += 1;
309
310 let recv_info = quiche::RecvInfo {
311 to: local_addr,
312 from,
313 };
314
315 let read = match conn.recv(&mut buf[..len], recv_info) {
317 Ok(v) => v,
318
319 Err(e) => {
320 error!("{local_addr}: recv failed: {e:?}");
321 continue 'read;
322 },
323 };
324
325 trace!("{local_addr}: processed {read} bytes");
326 }
327 }
328
329 trace!("done reading");
330
331 if conn.is_closed() {
332 info!(
333 "connection closed, {:?} {:?}",
334 conn.stats(),
335 conn.path_stats().collect::<Vec<quiche::PathStats>>()
336 );
337
338 if !conn.is_established() {
339 error!(
340 "connection timed out after {:?}",
341 app_data_start.elapsed(),
342 );
343
344 return Err(ClientError::HandshakeFail);
345 }
346
347 if let Some(session_file) = &args.session_file {
348 if let Some(session) = conn.session() {
349 std::fs::write(session_file, session).ok();
350 }
351 }
352
353 if let Some(h_conn) = http_conn {
354 if h_conn.report_incomplete(&app_data_start) {
355 return Err(ClientError::HttpFail);
356 }
357 }
358
359 break;
360 }
361
362 if (conn.is_established() || conn.is_in_early_data()) &&
365 (!args.perform_migration || migrated) &&
366 !app_proto_selected
367 {
368 let app_proto = conn.application_proto();
377
378 if alpns::HTTP_09.contains(&app_proto) {
379 http_conn = Some(Http09Conn::with_urls(
380 &args.urls,
381 args.reqs_cardinal,
382 Rc::clone(&output_sink),
383 ));
384
385 app_proto_selected = true;
386 } else if alpns::HTTP_3.contains(&app_proto) {
387 let dgram_sender = if conn_args.dgrams_enabled {
388 Some(Http3DgramSender::new(
389 conn_args.dgram_count,
390 conn_args.dgram_data.clone(),
391 0,
392 ))
393 } else {
394 None
395 };
396
397 http_conn = Some(Http3Conn::with_urls(
398 &mut conn,
399 &args.urls,
400 args.reqs_cardinal,
401 &args.req_headers,
402 &args.body,
403 &args.method,
404 args.send_priority_update,
405 conn_args.max_field_section_size,
406 conn_args.qpack_max_table_capacity,
407 conn_args.qpack_blocked_streams,
408 args.dump_json,
409 dgram_sender,
410 Rc::clone(&output_sink),
411 ));
412
413 app_proto_selected = true;
414 }
415 }
416
417 if let Some(h_conn) = http_conn.as_mut() {
420 h_conn.send_requests(&mut conn, &args.dump_response_path);
421 h_conn.handle_responses(&mut conn, &mut buf, &app_data_start);
422 }
423
424 while let Some(qe) = conn.path_event_next() {
426 match qe {
427 quiche::PathEvent::New(..) => unreachable!(),
428
429 quiche::PathEvent::Validated(local_addr, peer_addr) => {
430 info!("Path ({local_addr}, {peer_addr}) is now validated");
431 conn.migrate(local_addr, peer_addr).unwrap();
432 migrated = true;
433 },
434
435 quiche::PathEvent::FailedValidation(local_addr, peer_addr) => {
436 info!("Path ({local_addr}, {peer_addr}) failed validation");
437 },
438
439 quiche::PathEvent::Closed(local_addr, peer_addr) => {
440 info!(
441 "Path ({local_addr}, {peer_addr}) is now closed and unusable"
442 );
443 },
444
445 quiche::PathEvent::ReusedSourceConnectionId(
446 cid_seq,
447 old,
448 new,
449 ) => {
450 info!(
451 "Peer reused cid seq {cid_seq} (initially {old:?}) on {new:?}"
452 );
453 },
454
455 quiche::PathEvent::PeerMigrated(..) => unreachable!(),
456 }
457 }
458
459 while let Some(retired_scid) = conn.retired_scid_next() {
461 info!("Retiring source CID {retired_scid:?}");
462 }
463
464 while conn.scids_left() > 0 {
466 let (scid, reset_token) = generate_cid_and_reset_token(&rng);
467
468 if conn.new_scid(&scid, reset_token, false).is_err() {
469 break;
470 }
471
472 scid_sent = true;
473 }
474
475 if args.perform_migration &&
476 !new_path_probed &&
477 scid_sent &&
478 conn.available_dcids() > 0
479 {
480 let additional_local_addr =
481 migrate_socket.as_ref().unwrap().local_addr().unwrap();
482 conn.probe_path(additional_local_addr, peer_addr).unwrap();
483
484 new_path_probed = true;
485 }
486
487 let mut sockets = vec![&socket];
490 if let Some(migrate_socket) = migrate_socket.as_ref() {
491 sockets.push(migrate_socket);
492 }
493
494 for socket in sockets {
495 let local_addr = socket.local_addr().unwrap();
496
497 for peer_addr in conn.paths_iter(local_addr) {
498 loop {
499 let (write, send_info) = match conn.send_on_path(
500 &mut out,
501 Some(local_addr),
502 Some(peer_addr),
503 ) {
504 Ok(v) => v,
505
506 Err(quiche::Error::Done) => {
507 trace!("{local_addr} -> {peer_addr}: done writing");
508 break;
509 },
510
511 Err(e) => {
512 error!(
513 "{local_addr} -> {peer_addr}: send failed: {e:?}"
514 );
515
516 conn.close(false, 0x1, b"fail").ok();
517 break;
518 },
519 };
520
521 if let Err(e) = socket.send_to(&out[..write], send_info.to) {
522 if e.kind() == std::io::ErrorKind::WouldBlock {
523 trace!(
524 "{} -> {}: send() would block",
525 local_addr,
526 send_info.to
527 );
528 break;
529 }
530
531 return Err(ClientError::Other(format!(
532 "{} -> {}: send() failed: {:?}",
533 local_addr, send_info.to, e
534 )));
535 }
536
537 trace!(
538 "written {write} bytes from {local_addr} to {}",
539 send_info.to
540 );
541 }
542 }
543 }
544
545 if conn.is_closed() {
546 info!(
547 "connection closed, {:?} {:?}",
548 conn.stats(),
549 conn.path_stats().collect::<Vec<quiche::PathStats>>()
550 );
551
552 if !conn.is_established() {
553 error!(
554 "connection timed out after {:?}",
555 app_data_start.elapsed(),
556 );
557
558 return Err(ClientError::HandshakeFail);
559 }
560
561 if let Some(session_file) = &args.session_file {
562 if let Some(session) = conn.session() {
563 std::fs::write(session_file, session).ok();
564 }
565 }
566
567 if let Some(h_conn) = http_conn {
568 if h_conn.report_incomplete(&app_data_start) {
569 return Err(ClientError::HttpFail);
570 }
571 }
572
573 break;
574 }
575 }
576
577 Ok(())
578}