Skip to main content

specter/fingerprint/
http3.rs

1//! HTTP/3 and QUIC fingerprint configuration.
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4pub struct Http3Fingerprint {
5    pub alpn_protocols: Vec<Vec<u8>>,
6    pub transport: QuicTransportParams,
7    pub settings: H3Settings,
8    pub stream: H3StreamFingerprint,
9}
10
11impl Default for Http3Fingerprint {
12    fn default() -> Self {
13        Self::chrome()
14    }
15}
16
17impl Http3Fingerprint {
18    pub fn chrome() -> Self {
19        Self {
20            alpn_protocols: vec![b"h3".to_vec()],
21            transport: QuicTransportParams::chrome(),
22            settings: H3Settings::chrome(),
23            stream: H3StreamFingerprint::chrome(),
24        }
25    }
26
27    pub fn firefox() -> Self {
28        Self {
29            alpn_protocols: vec![b"h3".to_vec()],
30            transport: QuicTransportParams::firefox(),
31            settings: H3Settings::firefox(),
32            stream: H3StreamFingerprint::firefox(),
33        }
34    }
35
36    pub fn pool_key_string(&self) -> String {
37        let alpn = self
38            .alpn_protocols
39            .iter()
40            .map(|proto| String::from_utf8_lossy(proto).into_owned())
41            .collect::<Vec<_>>()
42            .join(",");
43        format!(
44            "alpn={alpn};transport={};settings={};stream={}",
45            self.transport.pool_key_string(),
46            self.settings.pool_key_string(),
47            self.stream.pool_key_string(),
48        )
49    }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53pub struct RawQuicTransportParameter {
54    pub id: u64,
55    pub value: Vec<u8>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub(crate) enum RawQuicTransportParameterConnectionId {
60    OriginalDestination,
61    InitialSource,
62    RetrySource,
63}
64
65impl RawQuicTransportParameter {
66    const ORIGINAL_DESTINATION_CONNECTION_ID_ID: u64 = 0x00;
67    const INITIAL_SOURCE_CONNECTION_ID_ID: u64 = 0x0f;
68    const RETRY_SOURCE_CONNECTION_ID_ID: u64 = 0x10;
69    const ORIGINAL_DESTINATION_CONNECTION_ID_PLACEHOLDER: &'static [u8] =
70        b"$specter:original_destination_connection_id";
71    const INITIAL_SOURCE_CONNECTION_ID_PLACEHOLDER: &'static [u8] =
72        b"$specter:initial_source_connection_id";
73    const RETRY_SOURCE_CONNECTION_ID_PLACEHOLDER: &'static [u8] =
74        b"$specter:retry_source_connection_id";
75
76    pub fn varint(id: u64, value: u64) -> Self {
77        Self {
78            id,
79            value: encode_quic_varint(value),
80        }
81    }
82
83    pub fn empty(id: u64) -> Self {
84        Self {
85            id,
86            value: Vec::new(),
87        }
88    }
89
90    pub fn original_destination_connection_id() -> Self {
91        Self {
92            id: Self::ORIGINAL_DESTINATION_CONNECTION_ID_ID,
93            value: Self::ORIGINAL_DESTINATION_CONNECTION_ID_PLACEHOLDER.to_vec(),
94        }
95    }
96
97    pub fn initial_source_connection_id() -> Self {
98        Self {
99            id: Self::INITIAL_SOURCE_CONNECTION_ID_ID,
100            value: Self::INITIAL_SOURCE_CONNECTION_ID_PLACEHOLDER.to_vec(),
101        }
102    }
103
104    pub fn retry_source_connection_id() -> Self {
105        Self {
106            id: Self::RETRY_SOURCE_CONNECTION_ID_ID,
107            value: Self::RETRY_SOURCE_CONNECTION_ID_PLACEHOLDER.to_vec(),
108        }
109    }
110
111    pub(crate) fn connection_id_placeholder(
112        &self,
113    ) -> Option<RawQuicTransportParameterConnectionId> {
114        match (self.id, self.value.as_slice()) {
115            (
116                Self::ORIGINAL_DESTINATION_CONNECTION_ID_ID,
117                Self::ORIGINAL_DESTINATION_CONNECTION_ID_PLACEHOLDER,
118            ) => Some(RawQuicTransportParameterConnectionId::OriginalDestination),
119            (
120                Self::INITIAL_SOURCE_CONNECTION_ID_ID,
121                Self::INITIAL_SOURCE_CONNECTION_ID_PLACEHOLDER,
122            ) => Some(RawQuicTransportParameterConnectionId::InitialSource),
123            (Self::RETRY_SOURCE_CONNECTION_ID_ID, Self::RETRY_SOURCE_CONNECTION_ID_PLACEHOLDER) => {
124                Some(RawQuicTransportParameterConnectionId::RetrySource)
125            }
126            _ => None,
127        }
128    }
129}
130
131fn encode_quic_varint(value: u64) -> Vec<u8> {
132    if value < 64 {
133        vec![value as u8]
134    } else if value < 16_384 {
135        let encoded = 0x4000 | value;
136        vec![(encoded >> 8) as u8, encoded as u8]
137    } else if value < 1_073_741_824 {
138        let encoded = 0x8000_0000 | value;
139        vec![
140            (encoded >> 24) as u8,
141            (encoded >> 16) as u8,
142            (encoded >> 8) as u8,
143            encoded as u8,
144        ]
145    } else {
146        let encoded = 0xc000_0000_0000_0000 | value;
147        encoded.to_be_bytes().to_vec()
148    }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum QuicEcnCodepoint {
153    Ect0,
154    Ect1,
155}
156
157impl QuicEcnCodepoint {
158    pub fn ip_tos_bits(self) -> u32 {
159        match self {
160            Self::Ect0 => 0b10,
161            Self::Ect1 => 0b01,
162        }
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Hash)]
167pub struct QuicTransportParams {
168    pub max_idle_timeout_ms: u64,
169    pub max_recv_udp_payload_size: usize,
170    pub max_send_udp_payload_size: usize,
171    pub initial_datagram_size: usize,
172    pub initial_max_data: u64,
173    pub initial_max_stream_data_bidi_local: u64,
174    pub initial_max_stream_data_bidi_remote: u64,
175    pub initial_max_stream_data_uni: u64,
176    pub initial_max_streams_bidi: u64,
177    pub initial_max_streams_uni: u64,
178    pub ack_delay_exponent: u64,
179    pub max_ack_delay_ms: u64,
180    pub ack_eliciting_threshold: usize,
181    pub active_connection_id_limit: u64,
182    pub disable_active_migration: bool,
183    pub disable_dcid_reuse: bool,
184    pub grease: bool,
185    pub additional_transport_parameters: Vec<(u64, Vec<u8>)>,
186    pub raw_ordered_transport_parameters: Option<Vec<RawQuicTransportParameter>>,
187    pub max_datagram_frame_size: Option<u64>,
188    pub destination_connection_id_len: usize,
189    pub source_connection_id_len: usize,
190    pub max_amplification_factor: usize,
191    pub initial_rtt_ms: u64,
192    pub initial_congestion_window_packets: usize,
193    pub pacing_enabled: bool,
194    pub max_pacing_rate: Option<u64>,
195    pub relaxed_loss_threshold: bool,
196    pub max_connection_window: u64,
197    pub max_stream_window: u64,
198    pub ecn_codepoint: Option<QuicEcnCodepoint>,
199}
200
201impl QuicTransportParams {
202    pub fn chrome() -> Self {
203        Self {
204            max_idle_timeout_ms: 30_000,
205            max_recv_udp_payload_size: 65_535,
206            max_send_udp_payload_size: 1350,
207            initial_datagram_size: 1200,
208            initial_max_data: 15_663_105,
209            initial_max_stream_data_bidi_local: 1_000_000,
210            initial_max_stream_data_bidi_remote: 1_000_000,
211            initial_max_stream_data_uni: 1_000_000,
212            initial_max_streams_bidi: 100,
213            initial_max_streams_uni: 100,
214            ack_delay_exponent: 3,
215            max_ack_delay_ms: 25,
216            ack_eliciting_threshold: 10,
217            active_connection_id_limit: 2,
218            disable_active_migration: true,
219            disable_dcid_reuse: false,
220            grease: true,
221            additional_transport_parameters: Vec::new(),
222            raw_ordered_transport_parameters: None,
223            max_datagram_frame_size: None,
224            destination_connection_id_len: 16,
225            source_connection_id_len: 16,
226            max_amplification_factor: 3,
227            initial_rtt_ms: 333,
228            initial_congestion_window_packets: 10,
229            pacing_enabled: true,
230            max_pacing_rate: None,
231            relaxed_loss_threshold: false,
232            max_connection_window: 24 * 1024 * 1024,
233            max_stream_window: 16 * 1024 * 1024,
234            ecn_codepoint: None,
235        }
236    }
237
238    pub fn chrome_capture_ordered() -> Self {
239        let mut params = Self::chrome();
240        params.raw_ordered_transport_parameters =
241            Some(capture_ordered_transport_parameters(&params));
242        params
243    }
244
245    pub fn firefox() -> Self {
246        Self {
247            grease: false,
248            max_ack_delay_ms: 20,
249            ack_eliciting_threshold: 2,
250            initial_max_stream_data_bidi_local: 4 * 1024 * 1024,
251            initial_max_stream_data_bidi_remote: 4 * 1024 * 1024,
252            initial_max_stream_data_uni: 4 * 1024 * 1024,
253            ..Self::chrome()
254        }
255    }
256
257    pub fn firefox_capture_ordered() -> Self {
258        let mut params = Self::firefox();
259        params.raw_ordered_transport_parameters =
260            Some(capture_ordered_transport_parameters(&params));
261        params
262    }
263
264    pub fn pool_key_string(&self) -> String {
265        let additional_transport_parameters = self
266            .additional_transport_parameters
267            .iter()
268            .map(|(key, value)| {
269                let value_hex = value
270                    .iter()
271                    .map(|byte| format!("{byte:02x}"))
272                    .collect::<String>();
273                format!("{key}:{value_hex}")
274            })
275            .collect::<Vec<_>>()
276            .join(",");
277        let raw_ordered_transport_parameters = self
278            .raw_ordered_transport_parameters
279            .as_ref()
280            .map(|parameters| {
281                parameters
282                    .iter()
283                    .map(|parameter| {
284                        let value_hex = parameter
285                            .value
286                            .iter()
287                            .map(|byte| format!("{byte:02x}"))
288                            .collect::<String>();
289                        format!("{}:{value_hex}", parameter.id)
290                    })
291                    .collect::<Vec<_>>()
292                    .join(",")
293            })
294            .unwrap_or_else(|| "none".to_string());
295        format!(
296            "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={:?}",
297            self.max_idle_timeout_ms,
298            self.max_recv_udp_payload_size,
299            self.max_send_udp_payload_size,
300            self.initial_datagram_size,
301            self.initial_max_data,
302            self.initial_max_stream_data_bidi_local,
303            self.initial_max_stream_data_bidi_remote,
304            self.initial_max_stream_data_uni,
305            self.initial_max_streams_bidi,
306            self.initial_max_streams_uni,
307            self.ack_delay_exponent,
308            self.max_ack_delay_ms,
309            self.ack_eliciting_threshold,
310            self.active_connection_id_limit,
311            self.disable_active_migration,
312            self.disable_dcid_reuse,
313            self.grease,
314            self.max_datagram_frame_size,
315            self.destination_connection_id_len,
316            self.source_connection_id_len,
317            self.max_amplification_factor,
318            self.initial_rtt_ms,
319            self.initial_congestion_window_packets,
320            self.pacing_enabled,
321            self.max_pacing_rate,
322            self.relaxed_loss_threshold,
323            self.max_connection_window,
324            self.max_stream_window,
325            self.ecn_codepoint,
326        )
327    }
328}
329
330fn capture_ordered_transport_parameters(
331    params: &QuicTransportParams,
332) -> Vec<RawQuicTransportParameter> {
333    const TP_MAX_IDLE_TIMEOUT: u64 = 0x01;
334    const TP_MAX_UDP_PAYLOAD_SIZE: u64 = 0x03;
335    const TP_INITIAL_MAX_DATA: u64 = 0x04;
336    const TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL: u64 = 0x05;
337    const TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE: u64 = 0x06;
338    const TP_INITIAL_MAX_STREAM_DATA_UNI: u64 = 0x07;
339    const TP_INITIAL_MAX_STREAMS_BIDI: u64 = 0x08;
340    const TP_INITIAL_MAX_STREAMS_UNI: u64 = 0x09;
341    const TP_ACK_DELAY_EXPONENT: u64 = 0x0a;
342    const TP_MAX_ACK_DELAY: u64 = 0x0b;
343    const TP_DISABLE_ACTIVE_MIGRATION: u64 = 0x0c;
344    const TP_ACTIVE_CONNECTION_ID_LIMIT: u64 = 0x0e;
345    const TP_GREASE_RESERVED: u64 = 27;
346    const TP_MAX_DATAGRAM_FRAME_SIZE: u64 = 0x20;
347
348    let mut ordered = vec![
349        RawQuicTransportParameter::varint(TP_MAX_IDLE_TIMEOUT, params.max_idle_timeout_ms),
350        RawQuicTransportParameter::varint(
351            TP_MAX_UDP_PAYLOAD_SIZE,
352            params.max_recv_udp_payload_size as u64,
353        ),
354        RawQuicTransportParameter::varint(TP_INITIAL_MAX_DATA, params.initial_max_data),
355        RawQuicTransportParameter::varint(
356            TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
357            params.initial_max_stream_data_bidi_local,
358        ),
359        RawQuicTransportParameter::varint(
360            TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
361            params.initial_max_stream_data_bidi_remote,
362        ),
363        RawQuicTransportParameter::varint(
364            TP_INITIAL_MAX_STREAM_DATA_UNI,
365            params.initial_max_stream_data_uni,
366        ),
367        RawQuicTransportParameter::varint(
368            TP_INITIAL_MAX_STREAMS_BIDI,
369            params.initial_max_streams_bidi,
370        ),
371        RawQuicTransportParameter::varint(
372            TP_INITIAL_MAX_STREAMS_UNI,
373            params.initial_max_streams_uni,
374        ),
375        RawQuicTransportParameter::varint(TP_ACK_DELAY_EXPONENT, params.ack_delay_exponent),
376        RawQuicTransportParameter::varint(TP_MAX_ACK_DELAY, params.max_ack_delay_ms),
377    ];
378    if params.disable_active_migration {
379        ordered.push(RawQuicTransportParameter::empty(
380            TP_DISABLE_ACTIVE_MIGRATION,
381        ));
382    }
383    ordered.push(RawQuicTransportParameter::varint(
384        TP_ACTIVE_CONNECTION_ID_LIMIT,
385        params.active_connection_id_limit,
386    ));
387    ordered.push(RawQuicTransportParameter::initial_source_connection_id());
388    if let Some(value) = params.max_datagram_frame_size {
389        ordered.push(RawQuicTransportParameter::varint(
390            TP_MAX_DATAGRAM_FRAME_SIZE,
391            value,
392        ));
393    }
394    if params.grease {
395        ordered.push(RawQuicTransportParameter::empty(TP_GREASE_RESERVED));
396    }
397    ordered.extend(
398        params
399            .additional_transport_parameters
400            .iter()
401            .map(|(id, value)| RawQuicTransportParameter {
402                id: *id,
403                value: value.clone(),
404            }),
405    );
406    ordered
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Hash)]
410pub struct H3Settings {
411    pub qpack_max_table_capacity: Option<u64>,
412    pub qpack_blocked_streams: Option<u64>,
413    pub max_field_section_size: Option<u64>,
414    pub enable_extended_connect: bool,
415    pub additional_settings: Vec<(u64, u64)>,
416    pub raw_ordered_settings: Option<Vec<(u64, u64)>>,
417}
418
419impl H3Settings {
420    pub fn chrome() -> Self {
421        Self {
422            qpack_max_table_capacity: Some(0),
423            qpack_blocked_streams: Some(0),
424            max_field_section_size: None,
425            enable_extended_connect: true,
426            additional_settings: Vec::new(),
427            raw_ordered_settings: None,
428        }
429    }
430
431    pub fn firefox() -> Self {
432        Self {
433            enable_extended_connect: true,
434            ..Self::chrome()
435        }
436    }
437
438    pub fn pool_key_string(&self) -> String {
439        let additional = self
440            .additional_settings
441            .iter()
442            .map(|(key, value)| format!("{key}:{value}"))
443            .collect::<Vec<_>>()
444            .join(",");
445        let raw_ordered = self
446            .raw_ordered_settings
447            .as_ref()
448            .map(|settings| {
449                settings
450                    .iter()
451                    .map(|(key, value)| format!("{key}:{value}"))
452                    .collect::<Vec<_>>()
453                    .join(",")
454            })
455            .unwrap_or_default();
456        format!(
457            "qpack_table={:?};qpack_blocked={:?};max_field={:?};extended_connect={};additional={additional};raw_ordered={raw_ordered}",
458            self.qpack_max_table_capacity,
459            self.qpack_blocked_streams,
460            self.max_field_section_size,
461            self.enable_extended_connect,
462        )
463    }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
467pub enum QpackHeaderBlockStrategy {
468    StaticThenLiteral,
469    LiteralOnly,
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
473pub enum QpackStringEncodingStrategy {
474    Plain,
475    Huffman,
476    HuffmanIfSmaller,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Hash)]
480pub struct H3StreamFingerprint {
481    pub open_control_stream_first: bool,
482    pub open_qpack_encoder_before_decoder: bool,
483    pub send_grease_stream: bool,
484    pub send_grease_frames: bool,
485    pub qpack_encoder_stream_payload: Vec<u8>,
486    pub qpack_decoder_stream_payload: Vec<u8>,
487    pub request_header_block_strategy: QpackHeaderBlockStrategy,
488    pub request_string_encoding: QpackStringEncodingStrategy,
489}
490
491impl H3StreamFingerprint {
492    pub fn chrome() -> Self {
493        Self {
494            open_control_stream_first: true,
495            open_qpack_encoder_before_decoder: true,
496            send_grease_stream: true,
497            send_grease_frames: true,
498            qpack_encoder_stream_payload: Vec::new(),
499            qpack_decoder_stream_payload: Vec::new(),
500            request_header_block_strategy: QpackHeaderBlockStrategy::StaticThenLiteral,
501            request_string_encoding: QpackStringEncodingStrategy::Plain,
502        }
503    }
504
505    pub fn firefox() -> Self {
506        Self {
507            send_grease_stream: false,
508            send_grease_frames: false,
509            ..Self::chrome()
510        }
511    }
512
513    pub fn pool_key_string(&self) -> String {
514        let qpack_encoder = self
515            .qpack_encoder_stream_payload
516            .iter()
517            .map(|byte| format!("{byte:02x}"))
518            .collect::<String>();
519        let qpack_decoder = self
520            .qpack_decoder_stream_payload
521            .iter()
522            .map(|byte| format!("{byte:02x}"))
523            .collect::<String>();
524        format!(
525            "control_first={};qpack_encoder_first={};grease_stream={};grease_frames={};qpack_encoder={qpack_encoder};qpack_decoder={qpack_decoder};request_header_strategy={:?};request_string_encoding={:?}",
526            self.open_control_stream_first,
527            self.open_qpack_encoder_before_decoder,
528            self.send_grease_stream,
529            self.send_grease_frames,
530            self.request_header_block_strategy,
531            self.request_string_encoding,
532        )
533    }
534}