use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;
use rustls::ProtocolVersion;
use rustls::SupportedProtocolVersion;
use rustls::crypto::CryptoProvider;
use rustls::pki_types::ServerName;
use super::cert_info::InsecureVerifier;
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ProbeOutcome {
Supported,
NotSupported,
}
pub(crate) fn build_client_hello(version: u16, hostname: &str) -> Vec<u8> {
let mut body: Vec<u8> = Vec::new();
body.extend_from_slice(&version.to_be_bytes()); body.extend_from_slice(&[0u8; 32]); body.push(0);
let suites: [u16; 10] = [
0xc014, 0xc013, 0xc00a, 0xc009, 0x0035, 0x002f, 0x000a, 0xc012, 0xc011, 0x0005, ];
let mut suite_bytes = Vec::with_capacity(suites.len() * 2);
for s in suites {
suite_bytes.extend_from_slice(&s.to_be_bytes());
}
body.extend_from_slice(&(suite_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(&suite_bytes);
body.push(1);
body.push(0);
if version >= 0x0301 {
let mut ext: Vec<u8> = Vec::new();
if !hostname.is_empty() {
let host = hostname.as_bytes();
let mut list = Vec::new();
list.push(0u8); list.extend_from_slice(&(host.len() as u16).to_be_bytes());
list.extend_from_slice(host);
let mut sni = Vec::new();
sni.extend_from_slice(&(list.len() as u16).to_be_bytes()); sni.extend_from_slice(&list);
ext.extend_from_slice(&0x0000u16.to_be_bytes());
ext.extend_from_slice(&(sni.len() as u16).to_be_bytes());
ext.extend_from_slice(&sni);
}
{
let groups: [u16; 2] = [0x0017, 0x001d];
let mut gl = Vec::new();
for g in groups {
gl.extend_from_slice(&g.to_be_bytes());
}
let mut payload = Vec::new();
payload.extend_from_slice(&(gl.len() as u16).to_be_bytes());
payload.extend_from_slice(&gl);
ext.extend_from_slice(&0x000au16.to_be_bytes());
ext.extend_from_slice(&(payload.len() as u16).to_be_bytes());
ext.extend_from_slice(&payload);
}
{
let payload: [u8; 2] = [1, 0]; ext.extend_from_slice(&0x000bu16.to_be_bytes());
ext.extend_from_slice(&(payload.len() as u16).to_be_bytes());
ext.extend_from_slice(&payload);
}
body.extend_from_slice(&(ext.len() as u16).to_be_bytes());
body.extend_from_slice(&ext);
}
let mut hs: Vec<u8> = Vec::with_capacity(body.len() + 4);
hs.push(0x01);
let blen = body.len();
hs.push((blen >> 16) as u8);
hs.push((blen >> 8) as u8);
hs.push(blen as u8);
hs.extend_from_slice(&body);
let mut rec: Vec<u8> = Vec::with_capacity(hs.len() + 5);
rec.push(0x16);
rec.extend_from_slice(&version.to_be_bytes());
rec.extend_from_slice(&(hs.len() as u16).to_be_bytes());
rec.extend_from_slice(&hs);
rec
}
pub(crate) fn parse_probe_response(buf: &[u8], requested: u16) -> ProbeOutcome {
if buf.len() < 11 {
return ProbeOutcome::NotSupported;
}
if buf[0] != 0x16 {
return ProbeOutcome::NotSupported; }
if buf[5] != 0x02 {
return ProbeOutcome::NotSupported; }
let server_version = u16::from_be_bytes([buf[9], buf[10]]);
if server_version == requested {
ProbeOutcome::Supported
} else {
ProbeOutcome::NotSupported
}
}
pub(crate) fn probe_legacy_version(hostname: &str, port: u16, version: u16) -> Option<bool> {
let mut sock = match TcpStream::connect(format!("{}:{}", hostname, port)) {
Ok(s) => s,
Err(_) => return None,
};
let _ = sock.set_read_timeout(Some(PROBE_TIMEOUT));
let _ = sock.set_write_timeout(Some(PROBE_TIMEOUT));
let hello = build_client_hello(version, hostname);
if sock.write_all(&hello).is_err() {
return Some(false);
}
let mut buf: Vec<u8> = Vec::with_capacity(64);
let mut tmp = [0u8; 512];
loop {
match sock.read(&mut tmp) {
Ok(0) => break,
Ok(n) => {
buf.extend_from_slice(&tmp[..n]);
if buf.len() >= 11 {
break;
}
}
Err(_) => break,
}
}
if buf.is_empty() {
return Some(false);
}
Some(parse_probe_response(&buf, version) == ProbeOutcome::Supported)
}
pub(crate) fn detect_modern_version(
hostname: &str,
port: u16,
version: &'static SupportedProtocolVersion,
expected: ProtocolVersion,
) -> bool {
let provider = match CryptoProvider::get_default() {
Some(p) => p.clone(),
None => Arc::new(rustls::crypto::ring::default_provider()),
};
let config = match rustls::ClientConfig::builder_with_provider(provider).with_protocol_versions(&[version]) {
Ok(b) => b,
Err(_) => return false,
}
.dangerous()
.with_custom_certificate_verifier(Arc::new(InsecureVerifier))
.with_no_client_auth();
let server_name = match ServerName::try_from(hostname.to_string()) {
Ok(s) => s,
Err(_) => return false,
};
let mut conn = match rustls::ClientConnection::new(Arc::new(config), server_name) {
Ok(c) => c,
Err(_) => return false,
};
let mut sock = match TcpStream::connect(format!("{}:{}", hostname, port)) {
Ok(s) => s,
Err(_) => return false,
};
let _ = sock.set_read_timeout(Some(PROBE_TIMEOUT));
let _ = sock.set_write_timeout(Some(PROBE_TIMEOUT));
while conn.is_handshaking() {
if conn.complete_io(&mut sock).is_err() {
break;
}
}
conn.protocol_version() == Some(expected)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_hello_has_correct_record_and_handshake_framing() {
let hello = build_client_hello(0x0301, "example.com"); assert_eq!(hello[0], 0x16);
assert_eq!(&hello[1..3], &[0x03, 0x01]);
let rec_len = u16::from_be_bytes([hello[3], hello[4]]) as usize;
assert_eq!(rec_len, hello.len() - 5, "record length must cover the rest");
assert_eq!(hello[5], 0x01);
let hs_len = ((hello[6] as usize) << 16) | ((hello[7] as usize) << 8) | hello[8] as usize;
assert_eq!(hs_len, hello.len() - 9, "handshake length must cover the body");
assert_eq!(&hello[9..11], &[0x03, 0x01]);
}
#[test]
fn sslv3_hello_carries_no_extensions() {
let v3 = build_client_hello(0x0300, "example.com");
let v10 = build_client_hello(0x0301, "example.com");
assert!(v3.len() < v10.len(), "sslv3 hello should be shorter (no extensions)");
}
#[test]
fn parse_supported_when_serverhello_echoes_version() {
let resp = [
0x16, 0x03, 0x01, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00, 0x03, 0x01, ];
assert_eq!(parse_probe_response(&resp, 0x0301), ProbeOutcome::Supported);
}
#[test]
fn parse_not_supported_on_alert() {
let resp = [0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x46];
assert_eq!(parse_probe_response(&resp, 0x0301), ProbeOutcome::NotSupported);
}
#[test]
fn parse_not_supported_on_version_mismatch() {
let resp = [0x16, 0x03, 0x03, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00, 0x03, 0x03];
assert_eq!(parse_probe_response(&resp, 0x0301), ProbeOutcome::NotSupported);
}
#[test]
fn parse_not_supported_on_truncated() {
assert_eq!(parse_probe_response(&[0x16, 0x03], 0x0301), ProbeOutcome::NotSupported);
}
}