Skip to content
Snippets Groups Projects
main.rs 13.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • // Copyright (C) 2019, Cloudflare, Inc.
    // All rights reserved.
    //
    // Redistribution and use in source and binary forms, with or without
    // modification, are permitted provided that the following conditions are
    // met:
    //
    //     * Redistributions of source code must retain the above copyright notice,
    //       this list of conditions and the following disclaimer.
    //
    //     * Redistributions in binary form must reproduce the above copyright
    //       notice, this list of conditions and the following disclaimer in the
    //       documentation and/or other materials provided with the distribution.
    //
    // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
    // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
    // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
    // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
    // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    
    // PKG_CONFIG_PATH=${OPENSSL_INSTALL}/lib64/pkgconfig LD_LIBRARY_PATH=${OPENSSL_INSTALL}/lib64 cargo run -- https://localhost:8443/
    
    #[macro_use]
    extern crate log;
    
    use quiche::Connection;
    use ring::rand::*;
    
    // const MAX_DATAGRAM_SIZE: usize = 1350; // This value is in quiche/apps/src/client.rs
    const MAX_DATAGRAM_SIZE: usize = 1200; // This value is used by openssl s_timer
    
    const SERVER_ADDRESS: &str = "https://10.0.0.1:8443";
    const SERVER_NAME: &str = "localhost";
    
    // NOTE this version does not implement pacing, so it is comparable to openssl s_timer
    // if pacing should be implemented, probably should work user spaced based, with using the event polling used here for simplicity
    
    fn main() {
        let mut buf = [0; 65535];
        let mut out = [0; MAX_DATAGRAM_SIZE];
    
        let (group, amount_of_measurments, ccalgo) = initialize_process();
        let (mut poll, mut events, peer_addr, bind_addr) = general_setup();
    
        let mut measurements = Vec::with_capacity(amount_of_measurments);
        let mut error_count = 0;
        let mut i = 0;
        while i < amount_of_measurments {
            let (mut conn, socket, local_addr) =
                prepare_handshake(&mut poll, peer_addr, &bind_addr, &group, &ccalgo);
    
            match time_handshake(
                &mut poll,
                &mut events,
                &mut conn,
                &socket,
                &mut out,
                &mut buf,
                local_addr,
            ) {
                Some(duration) => {
                    info!("Handshake took {:?}", duration);
                    measurements.push(duration);
                }
                None => {
    
    Bartolomeo Berend Müller's avatar
    Bartolomeo Berend Müller committed
                    eprintln!("Handshake failed");
    
                    error_count += 1;
    
    Bartolomeo Berend Müller's avatar
    Bartolomeo Berend Müller committed
                    continue;
    
                }
            }
    
            i += 1;
        }
    
        print!("{error_count};");
        let measurements: Vec<f64> = measurements
            .iter()
            .map(|m| m.as_nanos() as f64 / 1_000_000.0)
            .collect();
        for i in 0..amount_of_measurments - 1 {
            print!("{:.6},", measurements[i]);
        }
        print!("{:.6}", measurements[amount_of_measurments - 1]);
    }
    
    fn initialize_process() -> (String, usize, String) {
        env_logger::builder()
            .filter_level(log::LevelFilter::Error)
            .target(env_logger::Target::Stderr)
            .init();
    
        let mut args = std::env::args();
        let cmd = &args.next().unwrap();
    
        // only looks at remaining args
        if args.len() != 3 {
            error!("Usage: {cmd} GROUP AMOUNT_OF_MEASUREMENTS CCALGORITHM");
            std::process::exit(1);
        }
    
        let group = args.next().unwrap();
        let amount_of_measurments = args.next().unwrap().parse::<usize>().unwrap();
        let ccalgorithm = args.next().unwrap();
        return (group, amount_of_measurments, ccalgorithm);
    }
    
    fn general_setup() -> (mio::Poll, mio::Events, std::net::SocketAddr, String) {
        let url = url::Url::parse(SERVER_ADDRESS).unwrap();
        // Resolve server address.
        let peer_addr = url.socket_addrs(|| None).unwrap()[0];
        // Bind to INADDR_ANY or IN6ADDR_ANY depending on the IP family of the
        // server address. This is needed on macOS and BSD variants that don't
        // support binding to IN6ADDR_ANY for both v4 and v6.
        let bind_addr = match peer_addr {
            std::net::SocketAddr::V4(_) => "0.0.0.0:0".to_string(),
            std::net::SocketAddr::V6(_) => "[::]:0".to_string(),
        };
    
        // Setup the event loop.
        let poll = mio::Poll::new().unwrap();
        let events = mio::Events::with_capacity(1024);
    
        (poll, events, peer_addr, bind_addr)
    }
    
    fn prepare_handshake(
        poll: &mut mio::Poll,
        peer_addr: std::net::SocketAddr,
        bind_addr: &String,
        group: &String,
        ccalgo: &String,
    ) -> (
        quiche::Connection,
        mio::net::UdpSocket,
        std::net::SocketAddr,
    ) {
        // Create the UDP socket backing the QUIC connection, and register it with
        // the event loop.
        let mut socket = mio::net::UdpSocket::bind(bind_addr.parse().unwrap()).unwrap();
        poll.registry()
            .register(&mut socket, mio::Token(0), mio::Interest::READABLE)
            .unwrap();
    
        let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap();
        config.set_groups(&group).unwrap();
        config
            .set_application_protos(quiche::h3::APPLICATION_PROTOCOL)
            .unwrap();
        config.verify_peer(true);
        config
            .load_verify_locations_from_file("../tmp/.local/nginx/conf/CA.crt")
            .unwrap();
        // config.log_keys();
        // config.discover_pmtu(true);
    
        // NOTE think about how these can work in this implementation, they can only differ in what cwd they assume and in what timeouts they use
        match ccalgo.as_str() {
            "cubic" => config.set_cc_algorithm(quiche::CongestionControlAlgorithm::CUBIC),
            "reno" => config.set_cc_algorithm(quiche::CongestionControlAlgorithm::Reno),
            "bbr" => config.set_cc_algorithm(quiche::CongestionControlAlgorithm::BBR),
            "bbr2" => config.set_cc_algorithm(quiche::CongestionControlAlgorithm::BBR2),
            _ => {
                error!("Unknown congestion control algorithm: {ccalgo}");
                std::process::exit(1);
            }
        }
    
        // Disable pacing? implicitly, cuz packets have to be sent later by our application
        // see https://docs.rs/quiche/latest/quiche/index.html#pacing
        config.enable_pacing(false);
    
        // TODO read about every option and evaluate if default is good and comparable to openssl's impl
        config.set_max_idle_timeout(30000);
        config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE);
        config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE);
        config.set_initial_max_data(10_000_000);
        config.set_initial_max_stream_data_bidi_local(1_000_000);
        config.set_initial_max_stream_data_bidi_remote(1_000_000);
        config.set_initial_max_stream_data_uni(1_000_000);
        config.set_initial_max_streams_bidi(100);
        config.set_initial_max_streams_uni(100);
        // config.set_disable_active_migration(true);
    
    
        // // Generate a random source connection ID for the connection.
        // let mut scid = [0; quiche::MAX_CONN_ID_LEN];
        // SystemRandom::new().fill(&mut scid[..]).unwrap();
        // let scid = quiche::ConnectionId::from_ref(&scid);
        let scid = quiche::ConnectionId::from_ref(&[]); // empty like for quic_s_timer
    
    
        // Get local address.
        let local_addr = socket.local_addr().unwrap();
    
        // Create a QUIC connection and initiate handshake.
        let mut conn =
            quiche::connect(Some(SERVER_NAME), &scid, local_addr, peer_addr, &mut config).unwrap();
    
        if let Some(dir) = std::env::var_os("QLOGDIR") {
    
    Bartolomeo Berend Müller's avatar
    Bartolomeo Berend Müller committed
            let mut log_id = [0; quiche::MAX_CONN_ID_LEN];
            SystemRandom::new().fill(&mut log_id[..]).unwrap();
            let log_id = quiche::ConnectionId::from_ref(&log_id);
            let log_id = format!("{log_id:?}");
            let writer = make_qlog_writer(&dir, "client", &log_id);
    
    
            conn.set_qlog_with_level(
                std::boxed::Box::new(writer),
                "cquiche_s_timer qlog".to_string(),
    
    Bartolomeo Berend Müller's avatar
    Bartolomeo Berend Müller committed
                format!("{} id={}", "cquiche_s_timer qlog", log_id),
    
                quiche::QlogLevel::Extra,
            );
        }
    
        info!(
            "prepared handshake to connect to {:} from {:} with scid {:?}",
            peer_addr,
            socket.local_addr().unwrap(),
            &scid
        );
    
        return (conn, socket, local_addr);
    }
    
    fn time_handshake(
        poll: &mut mio::Poll,
        events: &mut mio::Events,
        conn: &mut quiche::Connection,
        socket: &mio::net::UdpSocket,
        out: &mut [u8; MAX_DATAGRAM_SIZE],
        buf: &mut [u8],
        local_addr: std::net::SocketAddr,
    ) -> Option<std::time::Duration> {
        let req_start = std::time::Instant::now();
        initial_send(conn, socket, out);
    
        loop {
            poll.poll(events, conn.timeout()).unwrap();
    
            receive(conn, socket, events, buf, local_addr);
    
            if conn.is_closed() {
                info!("connection closed, {:?}", conn.stats());
                return None;
            }
    
            send(conn, socket, out);
    
            if conn.is_established() {
                let elapsed_time = req_start.elapsed();
                conn.close(true, 0x00, b"kthxbye").unwrap();
                send(conn, socket, out);
                return Some(elapsed_time);
            }
        }
    }
    
    /// Initial send does not stop sending, if the socket is blocked and retries sending.
    fn initial_send(
        conn: &mut Connection,
        socket: &mio::net::UdpSocket,
        out: &mut [u8; MAX_DATAGRAM_SIZE],
    ) {
        loop {
            let (write, send_info) = match conn.send(out) {
                Ok(v) => v,
                Err(quiche::Error::Done) => {
                    debug!("done writing");
                    break;
                }
                Err(e) => {
                    error!("send failed: {:?}", e);
                    conn.close(false, 0x1, b"send failed").ok();
                    break;
                }
            };
    
            while let Err(e) = socket.send_to(&out[..write], send_info.to) {
                if e.kind() == std::io::ErrorKind::WouldBlock {
                    debug!("send() would block");
                    continue;
                }
    
                panic!("send() failed: {:?}", e);
            }
    
            debug!("written {}", write);
        }
    }
    
    /// Send does not retry sending if the socket is blocked.
    fn send(conn: &mut Connection, socket: &mio::net::UdpSocket, out: &mut [u8; MAX_DATAGRAM_SIZE]) {
        loop {
            let (write, send_info) = match conn.send(out) {
                Ok(v) => v,
                Err(quiche::Error::Done) => {
                    debug!("done writing");
                    break;
                }
                Err(e) => {
                    error!("send failed: {:?}", e);
                    conn.close(false, 0x1, b"send failed").ok();
                    break;
                }
            };
    
            if let Err(e) = socket.send_to(&out[..write], send_info.to) {
                if e.kind() == std::io::ErrorKind::WouldBlock {
                    debug!("send() would block");
                    break;
                }
    
                panic!("send() failed: {:?}", e);
            }
    
            debug!("written {}", write);
        }
    }
    
    fn receive(
        conn: &mut Connection,
        socket: &mio::net::UdpSocket,
        events: &mio::Events,
        buf: &mut [u8],
        local_addr: std::net::SocketAddr,
    ) {
        // Read incoming UDP packets from the socket and feed them to quiche,
        // until there are no more packets to read.
        'read: loop {
            // If the event loop reported no events, it means that the timeout
            // has expired, so handle it without attempting to read packets. We
            // will then proceed with the send loop.
            if events.is_empty() {
                debug!("timed out");
    
                conn.on_timeout();
    
                break 'read;
            }
    
            let (len, from) = match socket.recv_from(buf) {
                Ok(v) => v,
    
                Err(e) => {
                    // There are no more UDP packets to read, so end the read
                    // loop.
                    if e.kind() == std::io::ErrorKind::WouldBlock {
                        debug!("recv() would block");
                        break 'read;
                    }
    
                    panic!("recv() failed: {:?}", e);
                }
            };
    
            debug!("got {} bytes", len);
    
            let recv_info = quiche::RecvInfo {
                to: local_addr,
                from,
            };
    
            // Process potentially coalesced packets.
            let read = match conn.recv(&mut buf[..len], recv_info) {
                Ok(v) => v,
    
                Err(e) => {
                    error!("recv failed: {:?}", e);
                    continue 'read;
                }
            };
    
            debug!("processed {} bytes", read);
        }
    
        debug!("done reading");
    }
    
    /// Makes a buffered writer for a qlog.
    fn make_qlog_writer(
        dir: &std::ffi::OsStr,
        role: &str,
        id: &str,
    ) -> std::io::BufWriter<std::fs::File> {
        let mut path = std::path::PathBuf::from(dir);
        let filename = format!("{role}-{id}.sqlog");
        path.push(filename);
    
        match std::fs::File::create(&path) {
            Ok(f) => std::io::BufWriter::new(f),
            Err(e) => panic!(
                "Error creating qlog file attempted path was {:?}: {}",
                path, e
            ),
        }
    }