quinn-rs / quinn

Async-friendly QUIC implementation in Rust
Apache License 2.0
3.76k stars 380 forks source link

received unexpected handshake message: got `Finished` when expecting `EndOfEarlyData` #1302

Closed Lichtso closed 2 years ago

Lichtso commented 2 years ago

When reconnecting to a peer after connecting and disconnecting from it, the TLS 1.3 Early Data (0-RTT) gets send but is messed up. Or at least that is my interpretation of what is happening, maybe I am using quinn incorrectly.

Minimal? Deterministic Reproducer

use quinn_proto::{ClientConfig, Connection, ConnectionHandle, DatagramEvent, Endpoint, EndpointConfig, ServerConfig};
use std::{cell::RefCell, collections::VecDeque, net::{Ipv6Addr, SocketAddr}, sync::Arc, time::Instant};

struct Side {
    sock_addr: SocketAddr,
    endpoint: Endpoint,
    connection: Option<Connection>,
    queue: VecDeque<bytes::BytesMut>,
}
fn main() {
    let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
    let key = rustls::PrivateKey(cert.serialize_private_key_der());
    let cert = rustls::Certificate(cert.serialize_der().unwrap());
    let mut roots = rustls::RootCertStore::empty();
    roots.add(&cert).unwrap();
    let client_config = ClientConfig::with_root_certificates(roots);
    let server_config = ServerConfig::with_single_cert(vec![cert], key).unwrap();
    let sides = (0..2).map(|side_index| RefCell::new(Side {
        sock_addr: (Ipv6Addr::LOCALHOST, side_index + 1).into(),
        endpoint: Endpoint::new(
            Arc::new(EndpointConfig::default()),
            if side_index == 0 { Some(Arc::new(server_config.clone())) } else { None }
        ),
        connection: None,
        queue: VecDeque::with_capacity(4),
    })).collect::<Vec<RefCell<Side>>>();
    let mut now = Instant::now();
    for round in 0..10 {
        println!("\nROUND {}", round);
        {
            let mut server = sides[0].borrow_mut();
            server.connection = None;
            let mut client = sides[1].borrow_mut();
            client.connection = Some(client.endpoint.connect(client_config.clone(), server.sock_addr, "localhost").unwrap().1);
        }
        loop {
            let mut open_connections = 0;
            let mut unclosed_connections = 0;
            for side_index in [0, 1] {
                let mut side = sides[side_index].borrow_mut();
                let mut other_side = sides[1 - side_index].borrow_mut();
                while let Some(packet) = side.queue.pop_front() {
                    match side.endpoint.handle(now, other_side.sock_addr, None, None, packet) {
                        Some((_connection_handle, DatagramEvent::NewConnection(connection))) => {
                            side.connection = Some(connection);
                        }
                        Some((_connection_handle, DatagramEvent::ConnectionEvent(connection_event))) => {
                            side.connection.as_mut().unwrap().handle_event(connection_event);
                        }
                        None => {}
                    }
                }
                if let Some(connection) = side.connection.as_mut() {
                    connection.handle_timeout(now);
                    if let Some(connection_event) = connection.poll() {
                        println!("{}: {:?}", side_index, connection_event);
                    }
                    while let Some(transmit) = connection.poll_transmit(now, 1) {
                        other_side.queue.push_back(transmit.contents.as_slice().into());
                    }
                    if !connection.is_closed() && !connection.is_handshaking() {
                        open_connections += 1;
                    }
                    if !connection.is_drained() {
                        unclosed_connections += 1;
                    }
                    if let Some(endpoint_event) = connection.poll_endpoint_events() {
                        side.endpoint.handle_event(ConnectionHandle(0), endpoint_event);
                    }
                } else {
                    unclosed_connections += 1;
                }
                while let Some(transmit) = side.endpoint.poll_transmit() {
                    other_side.queue.push_back(transmit.contents.as_slice().into());
                }
                now += std::time::Duration::from_millis(50);
            }
            println!("open {}, unclosed {}", open_connections, unclosed_connections);
            if open_connections == 2 {
                sides[0].borrow_mut().connection.as_mut().unwrap().close(now, 0u32.into(), bytes::Bytes::new());
            }
            if unclosed_connections == 0 {
                break;
            }
        }
    }
}
[dependencies]
quinn-proto = "0.8.0"
bytes = "1"
webpki = { version = "0.22", default-features = false }
rustls = { version = "0.20", default-features = false, features = ["quic"] }
rcgen = "0.9.1"

Output

ROUND 0
open 0, unclosed 2
0: HandshakeDataReady
1: HandshakeDataReady
open 1, unclosed 2
0: Connected
1: Connected
open 2, unclosed 2
1: ConnectionLost { reason: ApplicationClosed(ApplicationClose { error_code: 0, reason: b"" }) }
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 1
open 0, unclosed 0

ROUND 1
open 0, unclosed 2
0: HandshakeDataReady
1: HandshakeDataReady
open 1, unclosed 2
0: ConnectionLost { reason: TransportError(Error { code: Code::crypto(0a), frame: None, reason: "received unexpected handshake message: got Finished when expecting EndOfEarlyData" }) }
1: Connected
open 0, unclosed 2
1: ConnectionLost { reason: ConnectionClosed(ConnectionClose { error_code: Code::crypto(0a), frame_type: None, reason: b"received unexpected handshake message: got Finished when expecting EndOfEarlyData" }) }
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 2
open 0, unclosed 1
open 0, unclosed 0

...

Also interesting to note: The behavior alternates, with every even round index / try to succeed and odd one to fail.

Ralith commented 2 years ago

Thanks for the report! This looks like a rustls bug; EndOfEarlyData is illegal in QUIC, so it should not be expected. It's quite surprising that this doesn't manifest in our tests.

The behavior alternates

This is expected; 0-RTT is only permitted by rustls when the session cache is populated.

djc commented 2 years ago

Let's close this in favor of rustls/rustls#1005.