manycast/net/
packet.rs

1use crate::custom_module::manycastr::{Address, Origin};
2use crate::net::{ICMPPacket, TCPPacket, UDPPacket};
3use crate::{A_ID, CHAOS_ID};
4use mac_address::mac_address_by_name;
5use pnet::ipnetwork::IpNetwork;
6use std::fs::File;
7use std::io::{BufRead, BufReader};
8use std::net::IpAddr;
9use std::process::Command;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12fn get_default_gateway_ip_linux() -> Result<String, String> {
13    let file = File::open("/proc/net/route")
14        .map_err(|e| format!("Failed to open /proc/net/route: {e}"))?;
15    let reader = BufReader::new(file);
16
17    for line in reader.lines().skip(1) {
18        let line = line.map_err(|e| format!("Failed to read line: {e}"))?;
19        let fields: Vec<&str> = line.split_whitespace().collect();
20        if fields.len() >= 3 && fields[1] == "00000000" {
21            // Gateway is in hex, little-endian
22            let hex = fields[2];
23            if hex.len() != 8 {
24                return Err(format!("Invalid gateway hex: {hex}"));
25            }
26
27            let bytes: Vec<u8> = (0..4)
28                .map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).unwrap())
29                .collect();
30
31            // Return bytes in reverse order (little-endian format)
32            return Ok(format!(
33                "{}.{}.{}.{}",
34                bytes[3], bytes[2], bytes[1], bytes[0]
35            ));
36        }
37    }
38
39    Err("Could not find default gateway in /proc/net/route".to_string())
40}
41fn get_default_gateway_ip_freebsd() -> Result<String, String> {
42    let output = Command::new("route")
43        .args(["-n", "get", "default"])
44        .output()
45        .map_err(|e| format!("Failed to execute 'route -n get default': {e}"))?;
46
47    if !output.status.success() {
48        return Err(format!(
49            "Failed to get default route (FreeBSD): {}",
50            String::from_utf8_lossy(&output.stderr)
51        ));
52    }
53
54    let stdout = String::from_utf8_lossy(&output.stdout);
55    for line in stdout.lines() {
56        let trimmed_line = line.trim();
57        if trimmed_line.starts_with("gateway:") {
58            let parts: Vec<&str> = trimmed_line.split_whitespace().collect();
59            if parts.len() >= 2 {
60                return Ok(parts[1].to_string());
61            }
62        }
63    }
64    Err("Could not parse default gateway IP from 'route -n get default' output".to_string())
65}
66
67/// Returns the ethernet header to use for the outbound packets.
68///
69/// # Arguments
70///
71/// * 'is_ipv6' - whether we are using IPv6 or not
72///
73/// * 'if_name' - the name of the interface to use
74pub fn get_ethernet_header(is_ipv6: bool, if_name: String) -> Vec<u8> {
75    // Get the source MAC address for the used interface
76    let mac_src = mac_address_by_name(&if_name)
77        .unwrap_or_else(|_| panic!("No MAC address found for interface: {if_name}"))
78        .unwrap()
79        .bytes()
80        .to_vec();
81
82    // Get the default gateway IP address (to get the destination MAC address)
83    let gateway_ip = if cfg!(target_os = "freebsd") {
84        get_default_gateway_ip_freebsd().expect("Could not get default gateway IP")
85    } else if cfg!(target_os = "linux") {
86        get_default_gateway_ip_linux().expect("Could not get default gateway IP")
87    } else {
88        panic!("Unsupported OS");
89    };
90
91    let lines: Vec<String> = if cfg!(target_os = "freebsd") {
92        let output = std::process::Command::new("arp")
93            .arg("-an")
94            .output()
95            .map_err(|e| format!("Failed to run arp command on FreeBSD: {e}"))
96            .unwrap();
97
98        if !output.status.success() {
99            panic!("arp command failed on FreeBSD");
100        }
101
102        let stdout = String::from_utf8_lossy(&output.stdout);
103        stdout.lines().map(|s| s.to_string()).collect()
104    } else {
105        let file = File::open("/proc/net/arp")
106            .map_err(|e| format!("Failed to open /proc/net/arp: {e}"))
107            .unwrap();
108        let reader = BufReader::new(file);
109
110        reader
111            .lines()
112            .map(|line| line.expect("Failed to read line from /proc/net/arp"))
113            .collect()
114    };
115
116    // Get the destination MAC addresses
117    let mut mac_dst: Option<[u8; 6]> = None;
118
119    for (i, line) in lines.iter().enumerate() {
120        if cfg!(target_os = "linux") && i == 0 {
121            // Skip the header on Linux
122            continue;
123        }
124        let parts: Vec<&str> = line.split_whitespace().collect();
125
126        if parts.len() > 5 {
127            let addr = parts[0]; // IP address
128
129            if addr != gateway_ip {
130                // Skip if not the default gateway
131                continue;
132            }
133
134            let mac_address = if cfg!(target_os = "freebsd") {
135                // For FreeBSD, MAC address is in the second column
136                parts[1]
137            } else {
138                // For Linux, MAC address is in the fourth column
139                parts[3]
140            };
141
142            // Skip local-loopback and broadcast
143            if (mac_address == "00:00:00:00:00:00") || (mac_address == "ff:ff:ff:ff:ff:ff") {
144                continue;
145            }
146
147            // If interface name matches, return the MAC address
148            if parts[5] == if_name {
149                let mac_bytes: [u8; 6] = mac_address
150                    .split(':')
151                    .map(|s| u8::from_str_radix(s, 16).unwrap())
152                    .collect::<Vec<u8>>()
153                    .try_into()
154                    .expect("MAC address should have 6 bytes");
155
156                mac_dst = Some(mac_bytes);
157                break;
158            }
159        }
160    }
161
162    // panic if no MAC address was found
163    if mac_dst.is_none() {
164        panic!("No destination MAC address found for interface: {if_name}");
165    }
166
167    // Construct the ethernet header
168    let ether_type = if is_ipv6 { 0x86DDu16 } else { 0x0800u16 };
169    let mut ethernet_header: Vec<u8> = Vec::with_capacity(14);
170    ethernet_header.extend_from_slice(&mac_dst.unwrap());
171    ethernet_header.extend_from_slice(&mac_src);
172    ethernet_header.extend_from_slice(&ether_type.to_be_bytes());
173
174    ethernet_header
175}
176
177/// Creates a ping packet to send.
178///
179/// # Arguments
180///
181/// * 'origin' - the source address and ICMP identifier (dst port) we use for our probes
182///
183/// * 'dst' - the destination address for the ping packet
184///
185/// * 'worker_id' - the unique worker ID of this worker
186///
187/// * 'measurement_id' - the unique ID of the current measurement
188///
189/// * 'info_url' - URL to encode in packet payload (e.g., opt-out URL)
190///
191/// # Returns
192///
193/// A ping packet (including the IP header) as a byte vector.
194pub fn create_icmp(
195    origin: &Origin,
196    dst: &Address,
197    worker_id: u32,
198    measurement_id: u32,
199    info_url: &str,
200) -> Vec<u8> {
201    let tx_time = SystemTime::now()
202        .duration_since(UNIX_EPOCH)
203        .unwrap()
204        .as_micros() as u64;
205    let src = origin.src.expect("None IP address");
206
207    // Create the ping payload bytes
208    let mut payload_bytes: Vec<u8> = Vec::new();
209    payload_bytes.extend_from_slice(&measurement_id.to_be_bytes()); // Bytes 0 - 3
210    payload_bytes.extend_from_slice(&tx_time.to_be_bytes()); // Bytes 4 - 11
211    payload_bytes.extend_from_slice(&worker_id.to_be_bytes()); // Bytes 12 - 15
212
213    // add the source address
214    if src.is_v6() {
215        payload_bytes.extend_from_slice(&src.get_v6().to_be_bytes()); // Bytes 16 - 33
216        payload_bytes.extend_from_slice(&dst.get_v6().to_be_bytes()); // Bytes 34 - 51
217
218        ICMPPacket::echo_request_v6(
219            origin.dport as u16,
220            2,
221            payload_bytes,
222            src.get_v6(),
223            dst.get_v6(),
224            255,
225            info_url,
226        )
227    } else {
228        payload_bytes.extend_from_slice(&src.get_v4().to_be_bytes()); // Bytes 16 - 19
229        payload_bytes.extend_from_slice(&dst.get_v4().to_be_bytes()); // Bytes 20 - 23
230
231        ICMPPacket::echo_request(
232            origin.dport as u16,
233            2,
234            payload_bytes,
235            src.get_v4(),
236            dst.get_v4(),
237            255,
238            info_url,
239        )
240    }
241}
242
243/// Creates a DNS packet.
244///
245/// # Arguments
246///
247/// * 'origin' - the source address and port values we use for our probes
248///
249/// * 'worker_id' - the unique worker ID of this worker
250///
251/// * 'dst' - the destination address for the DNS packet
252///
253/// * 'measurement_type' - the type of measurement being performed (2 = DNS/A, 4 = DNS/CHAOS)
254///
255/// * 'is_ipv6' - whether we are using IPv6 or not
256///
257/// * 'qname' - the DNS record to request
258///
259/// # Returns
260///
261/// A DNS packet (including the IP header) as a byte vector.
262///
263/// # Panics
264///
265/// If the measurement type is not 2 or 4
266pub fn create_dns(
267    origin: &Origin,
268    dst: &Address,
269    worker_id: u32,
270    measurement_type: u8,
271    qname: &str,
272) -> Vec<u8> {
273    let tx_time = SystemTime::now()
274        .duration_since(UNIX_EPOCH)
275        .unwrap()
276        .as_micros() as u64;
277    let src = &origin.src.expect("None IP address");
278    let sport = origin.sport as u16;
279
280    if measurement_type == A_ID {
281        UDPPacket::dns_request(src, dst, sport, qname, tx_time, worker_id, 255)
282    } else if measurement_type == CHAOS_ID {
283        UDPPacket::chaos_request(src, dst, sport, worker_id, qname)
284    } else {
285        panic!("Invalid measurement type")
286    }
287}
288
289/// Creates a TCP packet.
290///
291/// # Arguments
292///
293/// * 'origin' - the source address and port values we use for our probes
294///
295/// * 'dst' - the destination address for the TCP packet
296///
297/// * 'worker_id' - the unique worker ID of this worker
298///
299/// * 'is_ipv6' - whether we are using IPv6 or not
300///
301/// * 'is_symmetric' - whether we are measuring latency
302///
303/// * 'info_url' - URL to encode in packet payload (e.g., opt-out URL)
304///
305/// # Returns
306///
307/// A TCP packet (including the IP header) as a byte vector.
308pub fn create_tcp(
309    origin: &Origin,
310    dst: &Address,
311    worker_id: u32,
312    is_symmetric: bool,
313    info_url: &str,
314) -> Vec<u8> {
315    let ack = if !is_symmetric || worker_id > u16::MAX as u32 {
316        // Catchment mapping (or discovery probe for latency measurement)
317        worker_id
318    } else {
319        // Latency measurement
320        SystemTime::now()
321            .duration_since(UNIX_EPOCH)
322            .unwrap()
323            .as_millis() as u32
324    };
325
326    TCPPacket::tcp_syn_ack(
327        &origin.src.unwrap(),
328        dst,
329        origin.sport as u16,
330        origin.dport as u16,
331        ack,
332        255,
333        info_url,
334    )
335}
336
337/// Checks if the given address is in the given prefix.
338///
339/// # Arguments
340///
341/// * 'address' - the address to check
342///
343/// * 'prefix' - the prefix to check against
344///
345/// # Returns
346///
347/// True if the address is in the prefix, false otherwise.
348///
349/// # Panics
350///
351/// If the address is not a valid IP address.
352///
353/// If the prefix is not a valid prefix.
354pub fn is_in_prefix(address: &str, prefix: &IpNetwork) -> bool {
355    // Convert the address string to an IpAddr
356    let address = address
357        .parse::<IpAddr>()
358        .expect("Invalid IP address format");
359
360    match address {
361        IpAddr::V4(ipv4) => {
362            if let IpNetwork::V4(network_ip) = prefix {
363                // Use the contains method to check if the IP is in the network range
364                network_ip.contains(ipv4)
365            } else {
366                false
367            }
368        }
369        IpAddr::V6(ipv6) => {
370            if let IpNetwork::V6(network_ip) = prefix {
371                // Use the contains method to check if the IP is in the network range
372                network_ip.contains(ipv6)
373            } else {
374                false
375            }
376        }
377    }
378}