Coverage Report

Created: 2026-06-30 07:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata7/rust/src/quic/cyu.rs
Line
Count
Source
1
/* Copyright (C) 2021 Open Information Security Foundation
2
 *
3
 * You can copy, redistribute or modify this Program under the terms of
4
 * the GNU General Public License version 2 as published by the Free
5
 * Software Foundation.
6
 *
7
 * This program is distributed in the hope that it will be useful,
8
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * version 2 along with this program; if not, write to the Free Software
14
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15
 * 02110-1301, USA.
16
 */
17
18
use super::{
19
    frames::Frame,
20
    parser::{QuicHeader, QuicVersion},
21
};
22
use md5::{Digest, Md5};
23
24
#[derive(Debug, PartialEq, Eq)]
25
pub struct Cyu {
26
    pub string: String,
27
    pub hash: String,
28
}
29
30
impl Cyu {
31
7.44k
    pub(crate) fn new(string: String, hash: String) -> Self {
32
7.44k
        Self { string, hash }
33
7.44k
    }
34
35
1.40M
    pub(crate) fn generate(header: &QuicHeader, frames: &[Frame]) -> Vec<Cyu> {
36
1.40M
        let version = match header.version {
37
7.89k
            QuicVersion::Q043 => Some("43"),
38
2.04k
            QuicVersion::Q044 => Some("44"),
39
1.13k
            QuicVersion::Q045 => Some("44"),
40
7.61k
            QuicVersion::Q046 => Some("46"),
41
            _ => {
42
                SCLogDebug!(
43
                    "Cannot match QUIC version {:?} to CYU version",
44
                    header.version
45
                );
46
1.38M
                None
47
            }
48
        };
49
50
1.40M
        let mut cyu_hashes = Vec::new();
51
52
1.40M
        if let Some(version) = version {
53
71.0k
            for frame in frames {
54
52.4k
                if let Frame::Stream(stream) = frame {
55
12.7k
                    if let Some(tags) = &stream.tags {
56
7.44k
                        let tags = tags
57
7.44k
                            .iter()
58
36.5k
                            .map(|(tag, _value)| tag.to_string())
59
7.44k
                            .collect::<Vec<String>>()
60
7.44k
                            .join("-");
61
62
7.44k
                        let cyu_string = format!("{},{}", version, tags);
63
64
7.44k
                        let mut hasher = Md5::new();
65
7.44k
                        hasher.update(cyu_string.as_bytes());
66
7.44k
                        let hash = hasher.finalize();
67
68
7.44k
                        let cyu_hash = format!("{:x}", hash);
69
70
7.44k
                        cyu_hashes.push(Cyu::new(cyu_string, cyu_hash));
71
5.29k
                    }
72
39.6k
                }
73
            }
74
1.38M
        }
75
76
1.40M
        cyu_hashes
77
1.40M
    }
78
}
79
80
#[cfg(test)]
81
#[allow(clippy::unused_unit)]
82
mod tests {
83
    use super::*;
84
    use crate::quic::frames::{Frame, Stream, StreamTag};
85
    use crate::quic::parser::{PublicFlags, QuicType};
86
    use test_case::test_case;
87
88
    macro_rules! mock_header_and_frames {
89
        ($version:expr, $($variants:expr),+) => {{
90
            let header = QuicHeader::new(
91
                PublicFlags::new(0x80),
92
                QuicType::Initial,
93
                $version,
94
                vec![],
95
                vec![],
96
            );
97
98
            let frames = vec![
99
                Frame::Stream(Stream {
100
                    fin: false,
101
                    stream_id: vec![],
102
                    offset: vec![],
103
                    tags: Some(vec![$(($variants, vec![])),*])
104
                })
105
            ];
106
107
            (header, frames)
108
        }};
109
    }
110
111
    // Salesforce tests here:
112
    // https://engineering.salesforce.com/gquic-protocol-analysis-and-fingerprinting-in-zeek-a4178855d75f
113
    #[test_case(
114
        mock_header_and_frames!(
115
            // version
116
            QuicVersion::Q046,
117
            // tags
118
            StreamTag::Pad, StreamTag::Sni,
119
            StreamTag::Stk, StreamTag::Ver,
120
            StreamTag::Ccs, StreamTag::Nonc,
121
            StreamTag::Aead, StreamTag::Uaid,
122
            StreamTag::Scid, StreamTag::Tcid,
123
            StreamTag::Pdmd, StreamTag::Smhl,
124
            StreamTag::Icsl, StreamTag::Nonp,
125
            StreamTag::Pubs, StreamTag::Mids,
126
            StreamTag::Scls, StreamTag::Kexs,
127
            StreamTag::Xlct, StreamTag::Csct,
128
            StreamTag::Copt, StreamTag::Ccrt,
129
            StreamTag::Irtt, StreamTag::Cfcw,
130
            StreamTag::Sfcw
131
        ),
132
        Cyu {
133
            string: "46,PAD-SNI-STK-VER-CCS-NONC-AEAD-UAID-SCID-TCID-PDMD-SMHL-ICSL-NONP-PUBS-MIDS-SCLS-KEXS-XLCT-CSCT-COPT-CCRT-IRTT-CFCW-SFCW".to_string(),
134
            hash: "a46560d4548108cf99308319b3b85346".to_string(),
135
        }; "test cyu 1"
136
    )]
137
    #[test_case(
138
        mock_header_and_frames!(
139
            // version
140
            QuicVersion::Q043,
141
            // tags
142
            StreamTag::Pad, StreamTag::Sni,
143
            StreamTag::Ver, StreamTag::Ccs,
144
            StreamTag::Pdmd, StreamTag::Icsl,
145
            StreamTag::Mids, StreamTag::Cfcw,
146
            StreamTag::Sfcw
147
        ),
148
        Cyu {
149
            string: "43,PAD-SNI-VER-CCS-PDMD-ICSL-MIDS-CFCW-SFCW".to_string(),
150
            hash: "e030dea1f2eea44ac7db5fe4de792acd".to_string(),
151
        }; "test cyu 2"
152
    )]
153
    #[test_case(
154
        mock_header_and_frames!(
155
            // version
156
            QuicVersion::Q043,
157
            // tags
158
            StreamTag::Pad, StreamTag::Sni,
159
            StreamTag::Stk, StreamTag::Ver,
160
            StreamTag::Ccs, StreamTag::Scid,
161
            StreamTag::Pdmd, StreamTag::Icsl,
162
            StreamTag::Mids, StreamTag::Cfcw,
163
            StreamTag::Sfcw
164
        ),
165
        Cyu {
166
            string: "43,PAD-SNI-STK-VER-CCS-SCID-PDMD-ICSL-MIDS-CFCW-SFCW".to_string(),
167
            hash: "0811fab28e41e8c8a33e220a15b964d9".to_string(),
168
        }; "test cyu 3"
169
    )]
170
    #[test_case(
171
        mock_header_and_frames!(
172
            // version
173
            QuicVersion::Q043,
174
            // tags
175
            StreamTag::Pad, StreamTag::Sni,
176
            StreamTag::Stk, StreamTag::Ver,
177
            StreamTag::Ccs, StreamTag::Nonc,
178
            StreamTag::Aead, StreamTag::Scid,
179
            StreamTag::Pdmd, StreamTag::Icsl,
180
            StreamTag::Pubs, StreamTag::Mids,
181
            StreamTag::Kexs, StreamTag::Xlct,
182
            StreamTag::Cfcw, StreamTag::Sfcw
183
        ),
184
        Cyu {
185
            string: "43,PAD-SNI-STK-VER-CCS-NONC-AEAD-SCID-PDMD-ICSL-PUBS-MIDS-KEXS-XLCT-CFCW-SFCW".to_string(),
186
            hash: "d8b208b236d176c89407500dbefb04c2".to_string(),
187
        }; "test cyu 4"
188
    )]
189
    fn test_cyu_generate(input: (QuicHeader, Vec<Frame>), expected: Cyu) {
190
        let (header, frames) = input;
191
192
        let cyu = Cyu::generate(&header, &frames);
193
        assert_eq!(1, cyu.len());
194
        assert_eq!(expected, cyu[0]);
195
    }
196
}