#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Http3Fingerprint {
pub alpn_protocols: Vec<Vec<u8>>,
pub transport: QuicTransportParams,
pub settings: H3Settings,
pub stream: H3StreamFingerprint,
}
impl Default for Http3Fingerprint {
fn default() -> Self {
Self::chrome()
}
}
impl Http3Fingerprint {
pub fn chrome() -> Self {
Self {
alpn_protocols: vec![b"h3".to_vec()],
transport: QuicTransportParams::chrome(),
settings: H3Settings::chrome(),
stream: H3StreamFingerprint::chrome(),
}
}
pub fn firefox() -> Self {
Self {
alpn_protocols: vec![b"h3".to_vec()],
transport: QuicTransportParams::firefox(),
settings: H3Settings::firefox(),
stream: H3StreamFingerprint::firefox(),
}
}
pub fn pool_key_string(&self) -> String {
let alpn = self
.alpn_protocols
.iter()
.map(|proto| String::from_utf8_lossy(proto).into_owned())
.collect::<Vec<_>>()
.join(",");
format!(
"alpn={alpn};transport={};settings={};stream={}",
self.transport.pool_key_string(),
self.settings.pool_key_string(),
self.stream.pool_key_string(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RawQuicTransportParameter {
pub id: u64,
pub value: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum RawQuicTransportParameterConnectionId {
OriginalDestination,
InitialSource,
RetrySource,
}
impl RawQuicTransportParameter {
const ORIGINAL_DESTINATION_CONNECTION_ID_ID: u64 = 0x00;
const INITIAL_SOURCE_CONNECTION_ID_ID: u64 = 0x0f;
const RETRY_SOURCE_CONNECTION_ID_ID: u64 = 0x10;
const ORIGINAL_DESTINATION_CONNECTION_ID_PLACEHOLDER: &'static [u8] =
b"$specter:original_destination_connection_id";
const INITIAL_SOURCE_CONNECTION_ID_PLACEHOLDER: &'static [u8] =
b"$specter:initial_source_connection_id";
const RETRY_SOURCE_CONNECTION_ID_PLACEHOLDER: &'static [u8] =
b"$specter:retry_source_connection_id";
pub fn varint(id: u64, value: u64) -> Self {
Self {
id,
value: encode_quic_varint(value),
}
}
pub fn empty(id: u64) -> Self {
Self {
id,
value: Vec::new(),
}
}
pub fn original_destination_connection_id() -> Self {
Self {
id: Self::ORIGINAL_DESTINATION_CONNECTION_ID_ID,
value: Self::ORIGINAL_DESTINATION_CONNECTION_ID_PLACEHOLDER.to_vec(),
}
}
pub fn initial_source_connection_id() -> Self {
Self {
id: Self::INITIAL_SOURCE_CONNECTION_ID_ID,
value: Self::INITIAL_SOURCE_CONNECTION_ID_PLACEHOLDER.to_vec(),
}
}
pub fn retry_source_connection_id() -> Self {
Self {
id: Self::RETRY_SOURCE_CONNECTION_ID_ID,
value: Self::RETRY_SOURCE_CONNECTION_ID_PLACEHOLDER.to_vec(),
}
}
pub(crate) fn connection_id_placeholder(
&self,
) -> Option<RawQuicTransportParameterConnectionId> {
match (self.id, self.value.as_slice()) {
(
Self::ORIGINAL_DESTINATION_CONNECTION_ID_ID,
Self::ORIGINAL_DESTINATION_CONNECTION_ID_PLACEHOLDER,
) => Some(RawQuicTransportParameterConnectionId::OriginalDestination),
(
Self::INITIAL_SOURCE_CONNECTION_ID_ID,
Self::INITIAL_SOURCE_CONNECTION_ID_PLACEHOLDER,
) => Some(RawQuicTransportParameterConnectionId::InitialSource),
(Self::RETRY_SOURCE_CONNECTION_ID_ID, Self::RETRY_SOURCE_CONNECTION_ID_PLACEHOLDER) => {
Some(RawQuicTransportParameterConnectionId::RetrySource)
}
_ => None,
}
}
}
fn encode_quic_varint(value: u64) -> Vec<u8> {
if value < 64 {
vec![value as u8]
} else if value < 16_384 {
let encoded = 0x4000 | value;
vec![(encoded >> 8) as u8, encoded as u8]
} else if value < 1_073_741_824 {
let encoded = 0x8000_0000 | value;
vec![
(encoded >> 24) as u8,
(encoded >> 16) as u8,
(encoded >> 8) as u8,
encoded as u8,
]
} else {
let encoded = 0xc000_0000_0000_0000 | value;
encoded.to_be_bytes().to_vec()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuicEcnCodepoint {
Ect0,
Ect1,
}
impl QuicEcnCodepoint {
pub fn ip_tos_bits(self) -> u32 {
match self {
Self::Ect0 => 0b10,
Self::Ect1 => 0b01,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QuicTransportParams {
pub max_idle_timeout_ms: u64,
pub max_recv_udp_payload_size: usize,
pub max_send_udp_payload_size: usize,
pub initial_datagram_size: usize,
pub initial_max_data: u64,
pub initial_max_stream_data_bidi_local: u64,
pub initial_max_stream_data_bidi_remote: u64,
pub initial_max_stream_data_uni: u64,
pub initial_max_streams_bidi: u64,
pub initial_max_streams_uni: u64,
pub ack_delay_exponent: u64,
pub max_ack_delay_ms: u64,
pub ack_eliciting_threshold: usize,
pub active_connection_id_limit: u64,
pub disable_active_migration: bool,
pub disable_dcid_reuse: bool,
pub grease: bool,
pub additional_transport_parameters: Vec<(u64, Vec<u8>)>,
pub raw_ordered_transport_parameters: Option<Vec<RawQuicTransportParameter>>,
pub max_datagram_frame_size: Option<u64>,
pub destination_connection_id_len: usize,
pub source_connection_id_len: usize,
pub max_amplification_factor: usize,
pub initial_rtt_ms: u64,
pub initial_congestion_window_packets: usize,
pub pacing_enabled: bool,
pub max_pacing_rate: Option<u64>,
pub relaxed_loss_threshold: bool,
pub max_connection_window: u64,
pub max_stream_window: u64,
pub ecn_codepoint: Option<QuicEcnCodepoint>,
}
impl QuicTransportParams {
pub fn chrome() -> Self {
Self {
max_idle_timeout_ms: 30_000,
max_recv_udp_payload_size: 65_535,
max_send_udp_payload_size: 1350,
initial_datagram_size: 1200,
initial_max_data: 15_663_105,
initial_max_stream_data_bidi_local: 1_000_000,
initial_max_stream_data_bidi_remote: 1_000_000,
initial_max_stream_data_uni: 1_000_000,
initial_max_streams_bidi: 100,
initial_max_streams_uni: 100,
ack_delay_exponent: 3,
max_ack_delay_ms: 25,
ack_eliciting_threshold: 10,
active_connection_id_limit: 2,
disable_active_migration: true,
disable_dcid_reuse: false,
grease: true,
additional_transport_parameters: Vec::new(),
raw_ordered_transport_parameters: None,
max_datagram_frame_size: None,
destination_connection_id_len: 16,
source_connection_id_len: 16,
max_amplification_factor: 3,
initial_rtt_ms: 333,
initial_congestion_window_packets: 10,
pacing_enabled: true,
max_pacing_rate: None,
relaxed_loss_threshold: false,
max_connection_window: 24 * 1024 * 1024,
max_stream_window: 16 * 1024 * 1024,
ecn_codepoint: None,
}
}
pub fn chrome_capture_ordered() -> Self {
let mut params = Self::chrome();
params.raw_ordered_transport_parameters =
Some(capture_ordered_transport_parameters(¶ms));
params
}
pub fn firefox() -> Self {
Self {
grease: false,
max_ack_delay_ms: 20,
ack_eliciting_threshold: 2,
initial_max_stream_data_bidi_local: 4 * 1024 * 1024,
initial_max_stream_data_bidi_remote: 4 * 1024 * 1024,
initial_max_stream_data_uni: 4 * 1024 * 1024,
..Self::chrome()
}
}
pub fn firefox_capture_ordered() -> Self {
let mut params = Self::firefox();
params.raw_ordered_transport_parameters =
Some(capture_ordered_transport_parameters(¶ms));
params
}
pub fn pool_key_string(&self) -> String {
let additional_transport_parameters = self
.additional_transport_parameters
.iter()
.map(|(key, value)| {
let value_hex = value
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
format!("{key}:{value_hex}")
})
.collect::<Vec<_>>()
.join(",");
let raw_ordered_transport_parameters = self
.raw_ordered_transport_parameters
.as_ref()
.map(|parameters| {
parameters
.iter()
.map(|parameter| {
let value_hex = parameter
.value
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
format!("{}:{value_hex}", parameter.id)
})
.collect::<Vec<_>>()
.join(",")
})
.unwrap_or_else(|| "none".to_string());
format!(
"idle={};recv_udp={};send_udp={};initial_dgram={};max_data={};bidi_local={};bidi_remote={};uni_data={};bidi_streams={};uni_streams={};ack_exp={};ack_delay={};ack_threshold={};cid_limit={};disable_migration={};disable_dcid_reuse={};grease={};additional={additional_transport_parameters};raw_ordered={raw_ordered_transport_parameters};max_datagram={:?};dcid_len={};scid_len={};amp={};rtt={};cwnd={};pacing={};max_pacing={:?};relaxed_loss={};conn_win={};stream_win={};ecn={:?}",
self.max_idle_timeout_ms,
self.max_recv_udp_payload_size,
self.max_send_udp_payload_size,
self.initial_datagram_size,
self.initial_max_data,
self.initial_max_stream_data_bidi_local,
self.initial_max_stream_data_bidi_remote,
self.initial_max_stream_data_uni,
self.initial_max_streams_bidi,
self.initial_max_streams_uni,
self.ack_delay_exponent,
self.max_ack_delay_ms,
self.ack_eliciting_threshold,
self.active_connection_id_limit,
self.disable_active_migration,
self.disable_dcid_reuse,
self.grease,
self.max_datagram_frame_size,
self.destination_connection_id_len,
self.source_connection_id_len,
self.max_amplification_factor,
self.initial_rtt_ms,
self.initial_congestion_window_packets,
self.pacing_enabled,
self.max_pacing_rate,
self.relaxed_loss_threshold,
self.max_connection_window,
self.max_stream_window,
self.ecn_codepoint,
)
}
}
fn capture_ordered_transport_parameters(
params: &QuicTransportParams,
) -> Vec<RawQuicTransportParameter> {
const TP_MAX_IDLE_TIMEOUT: u64 = 0x01;
const TP_MAX_UDP_PAYLOAD_SIZE: u64 = 0x03;
const TP_INITIAL_MAX_DATA: u64 = 0x04;
const TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL: u64 = 0x05;
const TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE: u64 = 0x06;
const TP_INITIAL_MAX_STREAM_DATA_UNI: u64 = 0x07;
const TP_INITIAL_MAX_STREAMS_BIDI: u64 = 0x08;
const TP_INITIAL_MAX_STREAMS_UNI: u64 = 0x09;
const TP_ACK_DELAY_EXPONENT: u64 = 0x0a;
const TP_MAX_ACK_DELAY: u64 = 0x0b;
const TP_DISABLE_ACTIVE_MIGRATION: u64 = 0x0c;
const TP_ACTIVE_CONNECTION_ID_LIMIT: u64 = 0x0e;
const TP_GREASE_RESERVED: u64 = 27;
const TP_MAX_DATAGRAM_FRAME_SIZE: u64 = 0x20;
let mut ordered = vec![
RawQuicTransportParameter::varint(TP_MAX_IDLE_TIMEOUT, params.max_idle_timeout_ms),
RawQuicTransportParameter::varint(
TP_MAX_UDP_PAYLOAD_SIZE,
params.max_recv_udp_payload_size as u64,
),
RawQuicTransportParameter::varint(TP_INITIAL_MAX_DATA, params.initial_max_data),
RawQuicTransportParameter::varint(
TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
params.initial_max_stream_data_bidi_local,
),
RawQuicTransportParameter::varint(
TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
params.initial_max_stream_data_bidi_remote,
),
RawQuicTransportParameter::varint(
TP_INITIAL_MAX_STREAM_DATA_UNI,
params.initial_max_stream_data_uni,
),
RawQuicTransportParameter::varint(
TP_INITIAL_MAX_STREAMS_BIDI,
params.initial_max_streams_bidi,
),
RawQuicTransportParameter::varint(
TP_INITIAL_MAX_STREAMS_UNI,
params.initial_max_streams_uni,
),
RawQuicTransportParameter::varint(TP_ACK_DELAY_EXPONENT, params.ack_delay_exponent),
RawQuicTransportParameter::varint(TP_MAX_ACK_DELAY, params.max_ack_delay_ms),
];
if params.disable_active_migration {
ordered.push(RawQuicTransportParameter::empty(
TP_DISABLE_ACTIVE_MIGRATION,
));
}
ordered.push(RawQuicTransportParameter::varint(
TP_ACTIVE_CONNECTION_ID_LIMIT,
params.active_connection_id_limit,
));
ordered.push(RawQuicTransportParameter::initial_source_connection_id());
if let Some(value) = params.max_datagram_frame_size {
ordered.push(RawQuicTransportParameter::varint(
TP_MAX_DATAGRAM_FRAME_SIZE,
value,
));
}
if params.grease {
ordered.push(RawQuicTransportParameter::empty(TP_GREASE_RESERVED));
}
ordered.extend(
params
.additional_transport_parameters
.iter()
.map(|(id, value)| RawQuicTransportParameter {
id: *id,
value: value.clone(),
}),
);
ordered
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct H3Settings {
pub qpack_max_table_capacity: Option<u64>,
pub qpack_blocked_streams: Option<u64>,
pub max_field_section_size: Option<u64>,
pub enable_extended_connect: bool,
pub additional_settings: Vec<(u64, u64)>,
pub raw_ordered_settings: Option<Vec<(u64, u64)>>,
}
impl H3Settings {
pub fn chrome() -> Self {
Self {
qpack_max_table_capacity: Some(0),
qpack_blocked_streams: Some(0),
max_field_section_size: None,
enable_extended_connect: true,
additional_settings: Vec::new(),
raw_ordered_settings: None,
}
}
pub fn firefox() -> Self {
Self {
enable_extended_connect: true,
..Self::chrome()
}
}
pub fn pool_key_string(&self) -> String {
let additional = self
.additional_settings
.iter()
.map(|(key, value)| format!("{key}:{value}"))
.collect::<Vec<_>>()
.join(",");
let raw_ordered = self
.raw_ordered_settings
.as_ref()
.map(|settings| {
settings
.iter()
.map(|(key, value)| format!("{key}:{value}"))
.collect::<Vec<_>>()
.join(",")
})
.unwrap_or_default();
format!(
"qpack_table={:?};qpack_blocked={:?};max_field={:?};extended_connect={};additional={additional};raw_ordered={raw_ordered}",
self.qpack_max_table_capacity,
self.qpack_blocked_streams,
self.max_field_section_size,
self.enable_extended_connect,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QpackHeaderBlockStrategy {
StaticThenLiteral,
LiteralOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QpackStringEncodingStrategy {
Plain,
Huffman,
HuffmanIfSmaller,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct H3StreamFingerprint {
pub open_control_stream_first: bool,
pub open_qpack_encoder_before_decoder: bool,
pub send_grease_stream: bool,
pub send_grease_frames: bool,
pub qpack_encoder_stream_payload: Vec<u8>,
pub qpack_decoder_stream_payload: Vec<u8>,
pub request_header_block_strategy: QpackHeaderBlockStrategy,
pub request_string_encoding: QpackStringEncodingStrategy,
}
impl H3StreamFingerprint {
pub fn chrome() -> Self {
Self {
open_control_stream_first: true,
open_qpack_encoder_before_decoder: true,
send_grease_stream: true,
send_grease_frames: true,
qpack_encoder_stream_payload: Vec::new(),
qpack_decoder_stream_payload: Vec::new(),
request_header_block_strategy: QpackHeaderBlockStrategy::StaticThenLiteral,
request_string_encoding: QpackStringEncodingStrategy::Plain,
}
}
pub fn firefox() -> Self {
Self {
send_grease_stream: false,
send_grease_frames: false,
..Self::chrome()
}
}
pub fn pool_key_string(&self) -> String {
let qpack_encoder = self
.qpack_encoder_stream_payload
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
let qpack_decoder = self
.qpack_decoder_stream_payload
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
format!(
"control_first={};qpack_encoder_first={};grease_stream={};grease_frames={};qpack_encoder={qpack_encoder};qpack_decoder={qpack_decoder};request_header_strategy={:?};request_string_encoding={:?}",
self.open_control_stream_first,
self.open_qpack_encoder_before_decoder,
self.send_grease_stream,
self.send_grease_frames,
self.request_header_block_strategy,
self.request_string_encoding,
)
}
}