Skip to main content

fips_core/upper/
tcp_mss.rs

1//! TCP MSS (Maximum Segment Size) clamping for MTU handling.
2//!
3//! Intercepts TCP SYN packets and reduces the MSS option to ensure
4//! TCP segments fit within the FIPS effective MTU after encapsulation.
5
6/// TCP header minimum length (without options).
7const TCP_HEADER_MIN_LEN: usize = 20;
8
9/// TCP option kind for MSS.
10const TCP_OPT_MSS: u8 = 2;
11
12/// TCP option length for MSS (kind + length + value).
13const TCP_OPT_MSS_LEN: u8 = 4;
14
15/// TCP flags offset in header.
16const TCP_FLAGS_OFFSET: usize = 13;
17
18/// TCP SYN flag bit.
19const TCP_FLAG_SYN: u8 = 0x02;
20
21/// Check if a TCP packet is a SYN packet (has SYN flag set).
22fn is_tcp_syn(tcp_header: &[u8]) -> bool {
23    if tcp_header.len() < TCP_HEADER_MIN_LEN {
24        return false;
25    }
26    (tcp_header[TCP_FLAGS_OFFSET] & TCP_FLAG_SYN) != 0
27}
28
29/// Get the TCP data offset (header length in 32-bit words).
30fn get_tcp_data_offset(tcp_header: &[u8]) -> usize {
31    if tcp_header.len() < TCP_HEADER_MIN_LEN {
32        return 0;
33    }
34    ((tcp_header[12] >> 4) as usize) * 4
35}
36
37/// Clamp TCP MSS in a SYN packet if needed.
38///
39/// Searches for the MSS option in TCP options and reduces it if it exceeds
40/// the maximum safe MSS for the given MTU.
41///
42/// Returns true if the packet was modified (MSS was clamped).
43pub fn clamp_tcp_mss(ipv6_packet: &mut [u8], max_mss: u16) -> bool {
44    // Validate IPv6 header
45    if ipv6_packet.len() < 40 || ipv6_packet[0] >> 4 != 6 {
46        return false;
47    }
48
49    // Check if next header is TCP (6)
50    let next_header = ipv6_packet[6];
51    if next_header != 6 {
52        return false;
53    }
54
55    // Get TCP header start
56    let tcp_start = 40;
57    if ipv6_packet.len() < tcp_start + TCP_HEADER_MIN_LEN {
58        return false;
59    }
60
61    let tcp_header = &ipv6_packet[tcp_start..];
62
63    // Only process SYN packets
64    if !is_tcp_syn(tcp_header) {
65        return false;
66    }
67
68    // Get TCP header length
69    let tcp_header_len = get_tcp_data_offset(tcp_header);
70    if tcp_header_len < TCP_HEADER_MIN_LEN || tcp_header_len > tcp_header.len() {
71        return false;
72    }
73
74    // Parse TCP options
75    let options_start = tcp_start + TCP_HEADER_MIN_LEN;
76    let options_end = tcp_start + tcp_header_len;
77
78    if options_end > ipv6_packet.len() {
79        return false;
80    }
81
82    let mut modified = false;
83    let mut i = options_start;
84
85    while i < options_end {
86        let kind = ipv6_packet[i];
87
88        // End of options
89        if kind == 0 {
90            break;
91        }
92
93        // NOP (padding)
94        if kind == 1 {
95            i += 1;
96            continue;
97        }
98
99        // All other options have length field
100        if i + 1 >= options_end {
101            break;
102        }
103
104        let length = ipv6_packet[i + 1] as usize;
105        if length < 2 || i + length > options_end {
106            break;
107        }
108
109        // Check for MSS option
110        if kind == TCP_OPT_MSS && length == TCP_OPT_MSS_LEN as usize {
111            // Read current MSS value
112            let current_mss = u16::from_be_bytes([ipv6_packet[i + 2], ipv6_packet[i + 3]]);
113
114            // Clamp if needed
115            if current_mss > max_mss {
116                ipv6_packet[i + 2..i + 4].copy_from_slice(&max_mss.to_be_bytes());
117
118                // Recalculate TCP checksum
119                recalculate_tcp_checksum(ipv6_packet, tcp_start);
120
121                modified = true;
122            }
123            break; // MSS option found, no need to continue
124        }
125
126        i += length;
127    }
128
129    modified
130}
131
132/// Recalculate TCP checksum after modifying the packet.
133fn recalculate_tcp_checksum(ipv6_packet: &mut [u8], tcp_start: usize) {
134    // Zero out existing checksum
135    ipv6_packet[tcp_start + 16] = 0;
136    ipv6_packet[tcp_start + 17] = 0;
137
138    // Extract addresses
139    let src = &ipv6_packet[8..24];
140    let dst = &ipv6_packet[24..40];
141
142    // Get TCP segment length
143    let payload_len = u16::from_be_bytes([ipv6_packet[4], ipv6_packet[5]]) as usize;
144    let tcp_segment = &ipv6_packet[tcp_start..tcp_start + payload_len];
145
146    // Calculate checksum with pseudo-header
147    let mut sum: u32 = 0;
148
149    // Pseudo-header: source address
150    for chunk in src.chunks(2) {
151        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
152    }
153
154    // Pseudo-header: destination address
155    for chunk in dst.chunks(2) {
156        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
157    }
158
159    // Pseudo-header: TCP length
160    sum += payload_len as u32;
161
162    // Pseudo-header: next header (TCP = 6)
163    sum += 6;
164
165    // TCP segment
166    for chunk in tcp_segment.chunks(2) {
167        let value = if chunk.len() == 2 {
168            u16::from_be_bytes([chunk[0], chunk[1]])
169        } else {
170            u16::from_be_bytes([chunk[0], 0])
171        };
172        sum += value as u32;
173    }
174
175    // Fold 32-bit sum to 16 bits
176    while sum >> 16 != 0 {
177        sum = (sum & 0xffff) + (sum >> 16);
178    }
179
180    // One's complement
181    let checksum = !sum as u16;
182    ipv6_packet[tcp_start + 16..tcp_start + 18].copy_from_slice(&checksum.to_be_bytes());
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    fn make_tcp_syn_packet(src: [u8; 16], dst: [u8; 16], mss: u16) -> Vec<u8> {
190        let mut packet = vec![0u8; 40 + 40]; // IPv6 + TCP with options
191
192        // IPv6 header
193        packet[0] = 0x60; // Version 6
194        packet[4..6].copy_from_slice(&40u16.to_be_bytes()); // Payload length
195        packet[6] = 6; // Next header = TCP
196        packet[7] = 64; // Hop limit
197        packet[8..24].copy_from_slice(&src);
198        packet[24..40].copy_from_slice(&dst);
199
200        // TCP header
201        let tcp_start = 40;
202        packet[tcp_start..tcp_start + 2].copy_from_slice(&12345u16.to_be_bytes()); // Source port
203        packet[tcp_start + 2..tcp_start + 4].copy_from_slice(&80u16.to_be_bytes()); // Dest port
204        packet[tcp_start + 4..tcp_start + 8].copy_from_slice(&1000u32.to_be_bytes()); // Seq
205        packet[tcp_start + 8..tcp_start + 12].copy_from_slice(&0u32.to_be_bytes()); // Ack
206        packet[tcp_start + 12] = 0xa0; // Data offset = 10 (40 bytes header)
207        packet[tcp_start + 13] = TCP_FLAG_SYN; // Flags = SYN
208        packet[tcp_start + 14..tcp_start + 16].copy_from_slice(&8192u16.to_be_bytes()); // Window
209
210        // TCP options: MSS
211        packet[tcp_start + 20] = TCP_OPT_MSS; // Kind
212        packet[tcp_start + 21] = TCP_OPT_MSS_LEN; // Length
213        packet[tcp_start + 22..tcp_start + 24].copy_from_slice(&mss.to_be_bytes()); // MSS value
214
215        // End of options
216        packet[tcp_start + 24] = 0;
217
218        // Calculate checksum
219        recalculate_tcp_checksum(&mut packet, tcp_start);
220
221        packet
222    }
223
224    #[test]
225    fn test_clamp_tcp_mss_reduces_large_mss() {
226        let src = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
227        let dst = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2];
228        let mut packet = make_tcp_syn_packet(src, dst, 1460);
229
230        let modified = clamp_tcp_mss(&mut packet, 1200);
231
232        assert!(modified);
233
234        // Check MSS was clamped
235        let tcp_start = 40;
236        let mss = u16::from_be_bytes([packet[tcp_start + 22], packet[tcp_start + 23]]);
237        assert_eq!(mss, 1200);
238    }
239
240    #[test]
241    fn test_clamp_tcp_mss_leaves_small_mss_unchanged() {
242        let src = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
243        let dst = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2];
244        let mut packet = make_tcp_syn_packet(src, dst, 1000);
245
246        let modified = clamp_tcp_mss(&mut packet, 1200);
247
248        assert!(!modified);
249
250        // Check MSS unchanged
251        let tcp_start = 40;
252        let mss = u16::from_be_bytes([packet[tcp_start + 22], packet[tcp_start + 23]]);
253        assert_eq!(mss, 1000);
254    }
255
256    #[test]
257    fn test_clamp_tcp_mss_ignores_non_syn() {
258        let src = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
259        let dst = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2];
260        let mut packet = make_tcp_syn_packet(src, dst, 1460);
261
262        // Clear SYN flag
263        packet[40 + 13] = 0x10; // ACK only
264
265        let modified = clamp_tcp_mss(&mut packet, 1200);
266
267        assert!(!modified);
268    }
269
270    #[test]
271    fn test_clamp_tcp_mss_ignores_non_tcp() {
272        let mut packet = vec![0u8; 80];
273        packet[0] = 0x60; // IPv6
274        packet[6] = 17; // UDP, not TCP
275
276        let modified = clamp_tcp_mss(&mut packet, 1200);
277
278        assert!(!modified);
279    }
280}