From 632033a30596e0ea8a103ced3bfce9dfc5a083a6 Mon Sep 17 00:00:00 2001 From: cfal Date: Thu, 22 Oct 2026 23:35:34 +0000 Subject: [PATCH] Reassemble ClientHellos split across TLS records The ClientHello parser shared by the TLS, REALITY and ShadowTLS inbounds required the whole ClientHello in one TLS record. Clients that split it across records to evade DPI, such as sing-box with `record_fragment`, could not connect, and REALITY and ShadowTLS closed those connections instead of relaying them the way their camouflage targets would answer. Go's TLS stack, which sing-box and mihomo use, reassembles such hellos. Read handshake records until the ClientHello is complete, up to what one record could carry. TLS targets receive the records as sent, processed read by read so that many small records stay within rustls's limit on unprocessed input. REALITY authenticates the reassembled message, as its Go implementations do, and passes it to rustls in records of valid size. ShadowTLS still needs a single record for its digest and otherwise falls back to the handshake server. Parsing of server_name and supported_versions is now also bounded by each extension's declared length, as in Go's parser. --- src/reality/reality_server.rs | 9 +- src/reality/reality_server_handler.rs | 3 +- src/reality/tests/reality_handshake.rs | 42 +++++ src/reality/tests/reality_server_handler.rs | 52 ++--- src/rustls_connection_util.rs | 8 + src/shadow_tls/shadow_tls_server_handler.rs | 198 ++++++++++---------- src/shadow_tls/tests/client_hello.rs | 95 +++++++++- src/sniff.rs | 4 +- src/tls_server_handler.rs | 80 ++++++++ 9 files changed, 359 insertions(+), 132 deletions(-) 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(); + } +}