diff --git a/src/reality/reality_server.rs b/src/reality/reality_server.rs index 8118af29..49204fff 100644 --- a/src/reality/reality_server.rs +++ b/src/reality/reality_server.rs @@ -135,7 +135,14 @@ impl AuthenticatedClientHello { rustls::ServerConnection::new_reality(Arc::new(config), prepared).map_err(tls_error)?; // Stage the complete flight without socket access. On failure even queued alerts are discarded. let mut staged = TlsConnection::new_reality(server); - staged.feed(&self.record).map_err(|error| { + // A ClientHello reassembled from several records can exceed one record's limit. + let mut hello_records = Vec::with_capacity(self.record.len() + 5 * (hello.len() / 16384)); + for fragment in hello.chunks(16384) { + hello_records.extend_from_slice(&self.record[..3]); + hello_records.extend_from_slice(&(fragment.len() as u16).to_be_bytes()); + hello_records.extend_from_slice(fragment); + } + staged.feed(&hello_records).map_err(|error| { if error.kind() == io::ErrorKind::Unsupported { io::Error::new( io::ErrorKind::Unsupported, diff --git a/src/reality/reality_server_handler.rs b/src/reality/reality_server_handler.rs index d94f4ec0..a82e38c3 100644 --- a/src/reality/reality_server_handler.rs +++ b/src/reality/reality_server_handler.rs @@ -119,7 +119,8 @@ pub async fn setup_reality_server_stream( cipher_suites: target.cipher_suites.clone(), }; - let auth_result = reality_config.authenticate_client_hello(client_hello_frame); + let auth_result = + reality_config.authenticate_client_hello(&parsed_client_hello.client_hello_record); let TargetFlight { records: dest_records, diff --git a/src/reality/tests/reality_handshake.rs b/src/reality/tests/reality_handshake.rs index 80bfe735..21e7fd5c 100644 --- a/src/reality/tests/reality_handshake.rs +++ b/src/reality/tests/reality_handshake.rs @@ -83,6 +83,48 @@ fn real_target_certificate_never_authenticates_reality() { assert!(client.is_handshaking() && !client.wants_write()); } +#[test] +fn authenticated_hellos_longer_than_one_record_reach_rustls() { + use crate::reality::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh}; + let (cc, sc) = configs(CipherSuite::AES_128_GCM_SHA256); + let mut client = cc.create_connection().unwrap(); + let mut records = Vec::new(); + client.write_tls(&mut records).unwrap(); + // Pad the hello past 16 KiB, as a client could after reassembly, and authenticate it again. + let mut padding = vec![0xff, 0xa5, 0x45, 0x00]; + padding.resize(4 + 0x4500, 0); + let records = crate::sniff::tests::extend_hello(records, &padding); + // Rejoin the records into the single record that reassembly produces. + let mut hello = crate::sniff::tests::records(&records, u16::MAX.into()); + let message_len = usize::from(u16::from_be_bytes([hello[3], hello[4]])); + assert_eq!(hello.len(), 5 + message_len); + assert!(message_len > 16384); + let shares = extract_client_key_shares(&hello).unwrap(); + let shared = + perform_ecdh(&sc.private_key, shares.authentication_public_key().unwrap()).unwrap(); + let auth = derive_auth_key(&shared, &hello[11..31], b"REALITY").unwrap(); + let mut aad = hello[5..].to_vec(); + aad[39..71].fill(0); + let mut payload = [0; 16]; + payload[..3].copy_from_slice(&[1, 8, 0]); + payload[8..].fill(1); + let session = encrypt_session_id(&payload, &auth, &hello[31..43], &aad).unwrap(); + hello[44..76].copy_from_slice(&session); + + let mut server = sc + .authenticate_client_hello(&hello) + .unwrap() + .into_connection(&target_records( + RealityGroup::X25519MlKem768, + CipherSuite::AES_128_GCM_SHA256, + &hello[44..76], + )) + .unwrap(); + let mut flight = Vec::new(); + server.write_tls(&mut flight).unwrap(); + assert!(!flight.is_empty()); +} + #[test] fn ordinary_tls_client_hello_is_denied() { let (_, sc) = configs(CipherSuite::AES_128_GCM_SHA256); diff --git a/src/reality/tests/reality_server_handler.rs b/src/reality/tests/reality_server_handler.rs index a0d1983d..e9865a86 100644 --- a/src/reality/tests/reality_server_handler.rs +++ b/src/reality/tests/reality_server_handler.rs @@ -474,31 +474,35 @@ async fn authenticated_non_vision_oversized_preread_returns_error_without_panick ), }; let resolver: Arc = Arc::new(crate::resolver::NativeResolver::new()); - let (io, mut peer) = tokio::io::duplex(65540); - let mut flight = hello.clone(); - flight.extend_from_slice(&[0x17, 0x03, 0x03, 0, 1, 0].repeat(6000)); - peer.write_all(&flight).await.unwrap(); - let mut io: Box = Box::new(io); - let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap(); - assert!(parsed.client_reader.unparsed_data().len() > 33290); + // A ClientHello split across records authenticates like a single-record one, while + // dest still receives the client's records unchanged. + for wire_hello in [hello.clone(), crate::sniff::tests::records(&hello, 100)] { + let (io, mut peer) = tokio::io::duplex(65540); + let mut flight = wire_hello.clone(); + flight.extend_from_slice(&[0x17, 0x03, 0x03, 0, 1, 0].repeat(6000)); + peer.write_all(&flight).await.unwrap(); + let mut io: Box = Box::new(io); + let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap(); + assert!(parsed.client_reader.unparsed_data().len() > 33290); - let decoy_io = async { - let (mut socket, _) = listener.accept().await.unwrap(); - let mut forwarded_hello = vec![0; hello.len()]; - socket.read_exact(&mut forwarded_hello).await.unwrap(); - assert_eq!(forwarded_hello, hello); - socket.write_all(&response).await.unwrap(); - }; - let (result, ()) = tokio::time::timeout(Duration::from_secs(2), async { - tokio::join!( - setup_reality_server_stream(io, &target, parsed, &resolver), - decoy_io, - ) - }) - .await - .unwrap(); - let error = result.err().expect("invalid encrypted records must fail"); - assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{error}"); + let decoy_io = async { + let (mut socket, _) = listener.accept().await.unwrap(); + let mut forwarded_hello = vec![0; wire_hello.len()]; + socket.read_exact(&mut forwarded_hello).await.unwrap(); + assert_eq!(forwarded_hello, wire_hello); + socket.write_all(&response).await.unwrap(); + }; + let (result, ()) = tokio::time::timeout(Duration::from_secs(2), async { + tokio::join!( + setup_reality_server_stream(io, &target, parsed, &resolver), + decoy_io, + ) + }) + .await + .unwrap(); + let error = result.err().expect("invalid encrypted records must fail"); + assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{error}"); + } } #[tokio::test] diff --git a/src/rustls_connection_util.rs b/src/rustls_connection_util.rs index 4077d6bb..b4e39e5e 100644 --- a/src/rustls_connection_util.rs +++ b/src/rustls_connection_util.rs @@ -25,6 +25,14 @@ pub fn feed_rustls_server_connection( )); } i += n; + // rustls buffers little unprocessed input, which a ClientHello split into many + // small records can exceed. + connection.process_new_packets().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("failed to process fed TLS records: {e}"), + ) + })?; } Ok(()) } diff --git a/src/shadow_tls/shadow_tls_server_handler.rs b/src/shadow_tls/shadow_tls_server_handler.rs index bf26dcb9..3499c50a 100644 --- a/src/shadow_tls/shadow_tls_server_handler.rs +++ b/src/shadow_tls/shadow_tls_server_handler.rs @@ -98,6 +98,7 @@ const CONTENT_TYPE_APPLICATION_DATA: u8 = 0x17; const HANDSHAKE_TYPE_SERVER_HELLO: u8 = 0x02; const HANDSHAKE_TYPE_CLIENT_HELLO: u8 = 0x01; +const TLS_EXT_SERVER_NAME: u16 = 0x0000; const TLS_EXT_SUPPORTED_VERSIONS: u16 = 0x002b; // retry request random value, see https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.3 @@ -129,7 +130,7 @@ fn validate_shadowtls_client_hello( None => { return Err(std::io::Error::new( std::io::ErrorKind::PermissionDenied, - "client did not send a 32-byte session id", + "client did not send a single-record ClientHello with a 32-byte session id", )); } }; @@ -341,11 +342,16 @@ pub async fn setup_shadowtls_server_stream( } pub struct ParsedClientHello { + /// The ClientHello records exactly as received. pub client_hello_frame: Vec, + /// The ClientHello as one record. Equals `client_hello_frame` unless the client split + /// the message across records. + pub client_hello_record: Vec, pub client_hello_record_legacy_version_major: u8, pub client_hello_record_legacy_version_minor: u8, pub client_hello_content_version_major: u8, pub client_hello_content_version_minor: u8, + /// Only set for single-record ClientHellos, which ShadowTLS clients send. pub parsed_digest: Option, pub client_reader: StreamReader, pub requested_server_name: Option, @@ -358,88 +364,92 @@ pub struct ParsedClientHelloDigest { pub client_hello_digest_end_index: usize, } +/// Bounds a ClientHello split across records to what one record can carry. +const MAX_CLIENT_HELLO_LEN: usize = 65535; + +fn invalid_client_hello(message: impl Into) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, message.into()) +} + #[inline] pub async fn read_client_hello( server_stream: &mut Box, ) -> std::io::Result { let mut client_reader = StreamReader::new_with_buffer_size(TLS_FRAME_MAX_LEN); + let mut client_hello_frame = Vec::new(); + let mut message = Vec::new(); + let mut record_count = 0; + let mut client_legacy_version = [0; 2]; - // Allocates to allow borrowing the payload below. - let client_tls_header_bytes = client_reader - .read_slice(server_stream, TLS_HEADER_LEN) - .await? - .to_vec(); + // Clients may split the ClientHello across handshake records, e.g. to evade DPI. + loop { + let mut header = [0; TLS_HEADER_LEN]; + client_reader + .read_slice_into(server_stream, &mut header) + .await?; + if header[0] != CONTENT_TYPE_HANDSHAKE { + return Err(invalid_client_hello("expected client handshake")); + } + let payload_len = u16::from_be_bytes([header[3], header[4]]) as usize; + if payload_len == 0 { + return Err(invalid_client_hello("empty handshake record")); + } + let payload = client_reader.read_slice(server_stream, payload_len).await?; + if record_count == 0 { + client_legacy_version = [header[1], header[2]]; + } + record_count += 1; + client_hello_frame.extend_from_slice(&header); + client_hello_frame.extend_from_slice(payload); + message.extend_from_slice(payload); - let client_content_type = client_tls_header_bytes[0]; - if client_content_type != CONTENT_TYPE_HANDSHAKE { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "expected client handshake", - )); + if message[0] != HANDSHAKE_TYPE_CLIENT_HELLO { + return Err(invalid_client_hello("expected ClientHello")); + } + if message.len() < 4 { + continue; + } + let message_len = 4 + u32::from_be_bytes([0, message[1], message[2], message[3]]) as usize; + if message_len > MAX_CLIENT_HELLO_LEN { + return Err(invalid_client_hello(format!( + "ClientHello too large ({message_len} bytes)" + ))); + } + if message.len() > message_len { + return Err(invalid_client_hello("unexpected data after ClientHello")); + } + if message.len() == message_len { + break; + } } - let client_legacy_version_major = client_tls_header_bytes[1]; - let client_legacy_version_minor = client_tls_header_bytes[2]; - - let client_payload_len = - u16::from_be_bytes([client_tls_header_bytes[3], client_tls_header_bytes[4]]) as usize; - let client_payload_bytes = client_reader - .read_slice(server_stream, client_payload_len) - .await?; - - let mut client_hello = BufReader::new(client_payload_bytes); - if client_hello.read_u8()? != HANDSHAKE_TYPE_CLIENT_HELLO { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "expected ClientHello", - )); - } - - let client_hello_message_len = client_hello.read_u24_be()? as usize; - // this should be 4 bytes less than the payload length (handshake type + 3 bytes length) - if client_hello_message_len + 4 != client_payload_len { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "client hello message length mismatch", - )); - } + let mut client_hello = BufReader::new(&message); + client_hello.skip(4)?; // handshake type and length let client_version_major = client_hello.read_u8()?; let client_version_minor = client_hello.read_u8()?; let record_protocol_version_ok = client_version_major == 0x03 && (client_version_minor == 0x01 || client_version_minor == 0x03); if !record_protocol_version_ok { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!( - "unexpected ClientHello TLS version {client_version_major}.{client_version_minor}" - ), - )); + return Err(invalid_client_hello(format!( + "unexpected ClientHello TLS version {client_version_major}.{client_version_minor}" + ))); } client_hello.skip(32)?; // client random - let client_session_id_len = client_hello.read_u8()?; - - let parsed_digest = if client_session_id_len == 32 { - let client_session_id = client_hello.read_slice(32)?; + let client_session_id_len = client_hello.read_u8()? as usize; + let client_session_id = client_hello.read_slice(client_session_id_len)?; + let parsed_digest = if client_session_id_len == 32 && record_count == 1 { // Saves HMAC digest and session ID position for later validation. - let client_hello_digest = client_session_id[28..].to_vec(); let post_session_id_index = client_hello.position(); - - let client_hello_digest_start_index = TLS_HEADER_LEN + post_session_id_index - 4; - let client_hello_digest_end_index = TLS_HEADER_LEN + post_session_id_index; - Some(ParsedClientHelloDigest { - client_hello_digest, - client_hello_digest_start_index, - client_hello_digest_end_index, + client_hello_digest: client_session_id[28..].to_vec(), + client_hello_digest_start_index: TLS_HEADER_LEN + post_session_id_index - 4, + client_hello_digest_end_index: TLS_HEADER_LEN + post_session_id_index, }) } else { - if client_session_id_len > 0 { - client_hello.skip(client_session_id_len as usize)?; - } None }; @@ -460,59 +470,51 @@ pub async fn read_client_hello( while !client_extensions.is_consumed() { let extension_type = client_extensions.read_u16_be()?; let extension_len = client_extensions.read_u16_be()? as usize; + let mut extension = BufReader::new(client_extensions.read_slice(extension_len)?); - if extension_type == 0x0000 { - // server_name - if requested_server_name.is_some() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "multiple server names", - )); + if extension_type == TLS_EXT_SERVER_NAME { + let server_name_list_len = extension.read_u16_be()? as usize; + if server_name_list_len == 0 || server_name_list_len != extension.remaining() { + return Err(invalid_client_hello("invalid server name list length")); } - // TODO: assert lengths - let _server_name_list_len = client_extensions.read_u16_be()?; - let server_name_type = client_extensions.read_u8()?; - if server_name_type != 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "expected server name type to be hostname (0)", - )); + while !extension.is_consumed() { + if extension.read_u8()? != 0 { + return Err(invalid_client_hello( + "expected server name type to be hostname (0)", + )); + } + if requested_server_name.is_some() { + return Err(invalid_client_hello("multiple server names")); + } + let server_name_len = extension.read_u16_be()? as usize; + requested_server_name = Some(extension.read_str(server_name_len)?.to_string()); } - let server_name_len = client_extensions.read_u16_be()?; - let server_name_str = client_extensions.read_str(server_name_len as usize)?; - requested_server_name = Some(server_name_str.to_string()); - } else if extension_type == 0x002b { - // supported_versions - let version_list_len = client_extensions.read_u8()?; - if version_list_len % 2 != 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("invalid odd version list length: 0x{version_list_len:02x}"), - )); + } else if extension_type == TLS_EXT_SUPPORTED_VERSIONS { + let version_list_len = extension.read_u8()? as usize; + if !version_list_len.is_multiple_of(2) || version_list_len != extension.remaining() { + return Err(invalid_client_hello(format!( + "invalid version list length: 0x{version_list_len:02x}" + ))); } - let version_list_bytes = client_extensions.read_slice(version_list_len as usize)?; - for i in (0..version_list_bytes.len()).step_by(2) { - let version_major = version_list_bytes[i]; - let version_minor = version_list_bytes[i + 1]; - if version_major == 3 && version_minor == 4 { + while !extension.is_consumed() { + if extension.read_u16_be()? == 0x0304 { client_supports_tls13 = true; - break; } } - } else { - client_extensions.skip(extension_len)?; } } - let mut client_hello_frame = - Vec::with_capacity(client_tls_header_bytes.len() + client_payload_bytes.len()); - client_hello_frame.extend_from_slice(&client_tls_header_bytes); - client_hello_frame.extend_from_slice(client_payload_bytes); + let mut client_hello_record = Vec::with_capacity(TLS_HEADER_LEN + message.len()); + client_hello_record.push(CONTENT_TYPE_HANDSHAKE); + client_hello_record.extend_from_slice(&client_legacy_version); + client_hello_record.extend_from_slice(&(message.len() as u16).to_be_bytes()); + client_hello_record.extend_from_slice(&message); Ok(ParsedClientHello { client_hello_frame, - client_hello_record_legacy_version_major: client_legacy_version_major, - client_hello_record_legacy_version_minor: client_legacy_version_minor, + client_hello_record, + client_hello_record_legacy_version_major: client_legacy_version[0], + client_hello_record_legacy_version_minor: client_legacy_version[1], client_hello_content_version_major: client_version_major, client_hello_content_version_minor: client_version_minor, parsed_digest, diff --git a/src/shadow_tls/tests/client_hello.rs b/src/shadow_tls/tests/client_hello.rs index 8099e6e1..2417635a 100644 --- a/src/shadow_tls/tests/client_hello.rs +++ b/src/shadow_tls/tests/client_hello.rs @@ -2,9 +2,6 @@ use super::*; use std::time::Duration; use tokio::io::AsyncWriteExt; -const SNI: u16 = 0x0000; -const SUPPORTED_VERSIONS: u16 = 0x002b; - fn extension(kind: u16, data: &[u8]) -> Vec { let mut bytes = kind.to_be_bytes().to_vec(); bytes.extend_from_slice(&u16::try_from(data.len()).unwrap().to_be_bytes()); @@ -18,13 +15,13 @@ fn server_name(name_type: u8, name: &[u8]) -> Vec { entry.extend_from_slice(name); let mut list = u16::try_from(entry.len()).unwrap().to_be_bytes().to_vec(); list.extend_from_slice(&entry); - extension(SNI, &list) + extension(TLS_EXT_SERVER_NAME, &list) } fn supported_versions(versions: &[[u8; 2]]) -> Vec { let mut list = vec![u8::try_from(versions.len() * 2).unwrap()]; list.extend(versions.iter().flatten()); - extension(SUPPORTED_VERSIONS, &list) + extension(TLS_EXT_SUPPORTED_VERSIONS, &list) } fn handshake_body(session_id: &[u8], extensions: &[u8]) -> Vec { @@ -176,6 +173,20 @@ async fn malformed_hellos_are_rejected() { let mut short_session_id = vec![3, 3]; short_session_id.extend_from_slice(&[0x11; 32]); short_session_id.extend_from_slice(&[32, 1, 2, 3]); + let set_extension_len = |mut bytes: Vec, len: usize| { + bytes[2..4].copy_from_slice(&u16::try_from(len).unwrap().to_be_bytes()); + bytes + }; + let name = server_name(0, b"front.example"); + let name_overrun = set_extension_len(name.clone(), name.len() - 4 - 4); + let mut name_trailing = name.clone(); + name_trailing.push(0); + let name_trailing = set_extension_len(name_trailing, name.len() - 4 + 1); + let mut versions_overrun = extension(TLS_EXT_SUPPORTED_VERSIONS, &[4, 3, 4]); + versions_overrun.extend(extension(0xff01, &[0])); + let mut versions_trailing = supported_versions(&[[3, 4]]); + versions_trailing.push(0); + let versions_trailing = set_extension_len(versions_trailing, 4); let cases = [ ("wrong content type", wrong_content_type), @@ -201,12 +212,32 @@ async fn malformed_hellos_are_rejected() { ), ( "odd supported_versions length", - client_hello(&[], &extension(SUPPORTED_VERSIONS, &odd_versions)), + client_hello(&[], &extension(TLS_EXT_SUPPORTED_VERSIONS, &odd_versions)), ), ( "session id overruns the hello", record(HANDSHAKE_TYPE_CLIENT_HELLO, &short_session_id), ), + ( + "server name overruns its extension", + client_hello(&[], &name_overrun), + ), + ( + "server name extension with trailing bytes", + client_hello(&[], &name_trailing), + ), + ( + "empty server name list", + client_hello(&[], &extension(TLS_EXT_SERVER_NAME, &[0, 0])), + ), + ( + "supported_versions overruns its extension", + client_hello(&[], &versions_overrun), + ), + ( + "supported_versions with trailing bytes", + client_hello(&[], &versions_trailing), + ), ]; for (name, bytes) in cases { assert!(parse(&bytes).await.is_err(), "{name} was accepted"); @@ -220,3 +251,55 @@ async fn truncated_records_are_rejected_at_eof() { assert!(parse(&hello[..len]).await.is_err(), "{len}-byte prefix"); } } + +#[tokio::test] +async fn hellos_split_across_records_are_reassembled() { + let hello = crate::sniff::tests::hello(&rustls::version::TLS13, true); + for chunk in [1, 2, 3, 4, 137] { + let split = crate::sniff::tests::records(&hello, chunk); + let parsed = parse(&split).await.unwrap(); + assert_eq!(parsed.client_hello_frame, split, "chunk={chunk}"); + assert_eq!(parsed.client_hello_record, hello, "chunk={chunk}"); + assert_eq!( + parsed.requested_server_name.as_deref(), + Some("sniff.example") + ); + assert!(parsed.supports_tls13); + // ShadowTLS authenticates its digest within a single record. + assert!(parsed.parsed_digest.is_none(), "chunk={chunk}"); + } +} + +#[tokio::test] +async fn invalid_record_framing_is_rejected() { + let hello = crate::sniff::tests::hello(&rustls::version::TLS13, true); + let split = crate::sniff::tests::records(&hello, 100); + let mut interleaved = split[..105].to_vec(); + interleaved.extend_from_slice(&[0x14, 3, 3, 0, 1, 1]); + interleaved.extend_from_slice(&split[105..]); + let mut empty_record = split[..105].to_vec(); + empty_record.extend_from_slice(&[CONTENT_TYPE_HANDSHAKE, 3, 1, 0, 0]); + empty_record.extend_from_slice(&split[105..]); + let mut trailing = hello.clone(); + trailing.push(HANDSHAKE_TYPE_CLIENT_HELLO); + let record_len = u16::from_be_bytes([trailing[3], trailing[4]]) + 1; + trailing[3..5].copy_from_slice(&record_len.to_be_bytes()); + for (name, bytes) in [ + ("non-handshake record inside the hello", interleaved), + ("empty handshake record", empty_record), + ("handshake data after the hello", trailing), + ( + "hello longer than one record can carry", + vec![CONTENT_TYPE_HANDSHAKE, 3, 1, 0, 4, 1, 1, 0, 0], + ), + ] { + let Err(error) = parse(&bytes).await else { + panic!("{name} was accepted"); + }; + assert_eq!( + error.kind(), + std::io::ErrorKind::InvalidData, + "{name}: {error}" + ); + } +} diff --git a/src/sniff.rs b/src/sniff.rs index b14752d9..b81a8064 100644 --- a/src/sniff.rs +++ b/src/sniff.rs @@ -151,7 +151,7 @@ pub(crate) mod tests { bytes } - fn records(bytes: &[u8], chunk: usize) -> Vec { + pub(crate) fn records(bytes: &[u8], chunk: usize) -> Vec { let mut handshake = Vec::new(); let mut remaining = bytes; while !remaining.is_empty() { @@ -168,7 +168,7 @@ pub(crate) mod tests { result } - fn extend_hello(mut bytes: Vec, extensions: &[u8]) -> Vec { + pub(crate) fn extend_hello(mut bytes: Vec, extensions: &[u8]) -> Vec { assert_eq!( bytes.len(), 5 + u16::from_be_bytes([bytes[3], bytes[4]]) as usize diff --git a/src/tls_server_handler.rs b/src/tls_server_handler.rs index fb41902c..fedaad3f 100644 --- a/src/tls_server_handler.rs +++ b/src/tls_server_handler.rs @@ -225,3 +225,83 @@ impl TcpServerHandler for TlsServerHandler { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::port_forward_handler::PortForwardServerHandler; + + fn server_config(cert: &rcgen::CertifiedKey) -> rustls::ServerConfig { + rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![cert.cert.der().clone()], + rustls::pki_types::PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()) + .into(), + ) + .unwrap() + } + + #[test] + fn large_client_hellos_split_into_tiny_records_reach_rustls() { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let mut padding = vec![0xff, 0xa5, 0x10, 0x00]; + padding.resize(4 + 0x1000, 0); + let hello = crate::sniff::tests::extend_hello( + crate::sniff::tests::hello(&rustls::version::TLS13, true), + &padding, + ); + let records = crate::sniff::tests::records(&hello, 1); + let mut connection = rustls::ServerConnection::new(Arc::new(server_config(&cert))).unwrap(); + feed_rustls_server_connection(&mut connection, &records).unwrap(); + connection.process_new_packets().unwrap(); + assert!( + connection.wants_write(), + "rustls must answer the reassembled ClientHello" + ); + } + + #[tokio::test] + async fn client_hellos_split_across_records_complete_the_handshake() { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let server_config = server_config(&cert); + let selector = Arc::new(ClientProxySelector::new(Vec::new())); + let target = TlsServerTarget::Tls { + server_config: Arc::new(server_config), + effective_selector: selector.clone(), + inner_protocol: InnerProtocol::Normal(Box::new(PortForwardServerHandler::new( + vec![NetLocation::new( + crate::address::Address::Ipv4(std::net::Ipv4Addr::LOCALHOST), + 9, + )], + selector, + ))), + }; + let handler = TlsServerHandler::new( + FxHashMap::default(), + Some(target), + None, + Arc::new(crate::resolver::NativeResolver::new()), + ); + let mut roots = rustls::RootCertStore::empty(); + roots.add(cert.cert.der().clone()).unwrap(); + let mut client_config = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + // The smallest fragment rustls allows splits the ClientHello across many records. + client_config.max_fragment_size = Some(32); + let (server_io, client_io) = tokio::io::duplex(65536); + let client = async { + tokio_rustls::TlsConnector::from(Arc::new(client_config)) + .connect("localhost".try_into().unwrap(), client_io) + .await + }; + let (setup, client) = tokio::time::timeout(std::time::Duration::from_secs(5), async { + tokio::join!(handler.setup_server_stream(Box::new(server_io)), client) + }) + .await + .unwrap(); + setup.unwrap(); + client.unwrap(); + } +}