Skip to main content

zond_engine/system/interface/
lan.rs

1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7use super::os::{is_physical, is_wireless};
8use crate::info;
9use pnet::datalink::NetworkInterface;
10use pnet::ipnetwork::{IpNetwork, Ipv4Network};
11
12/// Errors arising from network validation constraints during LAN interface selection.
13#[derive(Debug, PartialEq, Eq, Clone, Copy)]
14pub enum ViabilityError {
15    /// The interface is operationally down.
16    IsDown,
17    /// The interface was filtered out as "not physical" by the provided logic.
18    NotPhysical,
19    /// The interface does not have a MAC address.
20    NoMacAddress,
21    /// The interface does not support broadcast (required for ARP).
22    NotBroadcast,
23    /// The interface is a point-to-point link (e.g., a VPN).
24    IsPointToPoint,
25    /// The interface has no IPv4 address (for ARP) AND no IPv6 Link-Local (for NDP).
26    NoValidLanIp,
27}
28
29/// Identifies the best local area network (LAN) connected to the current host context.
30///
31/// Under the hood, this iterates over `pnet::datalink::interfaces()` directly.
32pub fn get_lan_network() -> anyhow::Result<Option<Ipv4Network>> {
33    let interfaces: Vec<NetworkInterface> = pnet::datalink::interfaces();
34    get_lan_network_with(interfaces)
35}
36
37/// Core LAN selection logic, decoupled from OS interface dependencies for testing.
38pub(crate) fn get_lan_network_with(
39    interfaces: Vec<NetworkInterface>,
40) -> anyhow::Result<Option<Ipv4Network>> {
41    let interfaces_str: &str = match interfaces.len() {
42        1 => "interface",
43        _ => "interfaces",
44    };
45
46    info!(
47        verbosity = 1,
48        "Identified {} network {}, picking the best one...",
49        interfaces.len(),
50        interfaces_str
51    );
52
53    let interfaces: Vec<NetworkInterface> = interfaces
54        .into_iter()
55        .filter_map(
56            |interface| match is_viable_lan_interface(&interface, is_physical) {
57                Ok(()) => Some(interface),
58                Err(_) => None,
59            },
60        )
61        .collect();
62
63    let interface: NetworkInterface =
64        if let Some(interface) = select_best_lan_interface(interfaces, is_wired) {
65            info!(
66                verbosity = 1,
67                "Performing LAN scan on interface {}", interface.name
68            );
69            interface
70        } else {
71            anyhow::bail!("No interfaces available for LAN discovery");
72        };
73    let private_v4_net: Option<Ipv4Network> = interface.ips.iter().find_map(|net| match net {
74        IpNetwork::V4(v4) if v4.ip().is_private() => Some(*v4),
75        _ => None,
76    });
77    Ok(private_v4_net)
78}
79
80fn is_viable_lan_interface(
81    interface: &NetworkInterface,
82    is_physical: impl Fn(&NetworkInterface) -> bool,
83) -> Result<(), ViabilityError> {
84    if !interface.is_up() {
85        return Err(ViabilityError::IsDown);
86    }
87    if !is_physical(interface) {
88        return Err(ViabilityError::NotPhysical);
89    }
90    if interface.is_loopback() {
91        return Err(ViabilityError::NotPhysical);
92    }
93    if interface.mac.is_none() {
94        return Err(ViabilityError::NoMacAddress);
95    }
96    if !interface.is_broadcast() {
97        return Err(ViabilityError::NotBroadcast);
98    }
99    if interface.is_point_to_point() {
100        return Err(ViabilityError::IsPointToPoint);
101    }
102    let has_valid_ip = interface.ips.iter().any(|net| match net {
103        IpNetwork::V4(ipv4) => ipv4.ip().is_private(),
104        IpNetwork::V6(ipv6) => ipv6.ip().is_unicast_link_local(),
105    });
106    if !has_valid_ip {
107        return Err(ViabilityError::NoValidLanIp);
108    }
109
110    Ok(())
111}
112
113fn select_best_lan_interface(
114    interfaces: Vec<NetworkInterface>,
115    is_wired: impl Fn(&NetworkInterface) -> bool,
116) -> Option<NetworkInterface> {
117    match interfaces.len() {
118        0 => None,
119        1 => Some(interfaces[0].clone()),
120        _ => interfaces
121            .iter()
122            .find(|&interface| is_wired(interface))
123            .cloned()
124            .or(Some(interfaces[0].clone())),
125    }
126}
127
128/// Identifies if the specified interface is wired directly to the machine locally.
129///
130/// Considers virtual and remote connections as non-wired.
131pub fn is_wired(interface: &NetworkInterface) -> bool {
132    is_physical(interface) && !is_wireless(interface)
133}
134
135// ╔════════════════════════════════════════════╗
136// ║ ████████╗███████╗███████╗████████╗███████╗ ║
137// ║ ╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝██╔════╝ ║
138// ║    ██║   █████╗  ███████╗   ██║   ███████╗ ║
139// ║    ██║   ██╔══╝  ╚════██║   ██║   ╚════██║ ║
140// ║    ██║   ███████╗███████║   ██║   ███████║ ║
141// ║    ╚═╝   ╚══════╝╚══════╝   ╚═╝   ╚══════╝ ║
142// ╚════════════════════════════════════════════╝
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use pnet::datalink::MacAddr;
148    use pnet::ipnetwork::IpNetwork;
149    use std::net::Ipv4Addr;
150
151    fn mock_interface(
152        up: bool,
153        mac: bool,
154        broadcast: bool,
155        p2p: bool,
156        loopback: bool,
157        ip: bool,
158    ) -> NetworkInterface {
159        NetworkInterface {
160            name: "test0".to_string(),
161            description: "".to_string(),
162            index: 0,
163            mac: if mac {
164                Some(MacAddr::new(1, 2, 3, 4, 5, 6))
165            } else {
166                None
167            },
168            ips: if ip {
169                vec![IpNetwork::V4(
170                    Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 100), 24).unwrap(),
171                )]
172            } else {
173                vec![]
174            },
175            flags: {
176                let mut flags = 0;
177                if up {
178                    flags |= 1;
179                }
180                if broadcast {
181                    flags |= 2;
182                }
183                if p2p {
184                    flags |= 16;
185                }
186                if loopback {
187                    flags |= 8;
188                } // roughly matching bitmasks
189                flags
190            },
191        }
192    }
193
194    #[test]
195    fn is_viable_down() {
196        let intf = mock_interface(false, true, true, false, false, true);
197        assert_eq!(
198            is_viable_lan_interface(&intf, |_| true),
199            Err(ViabilityError::IsDown)
200        );
201    }
202
203    #[test]
204    fn is_viable_not_physical() {
205        let intf = mock_interface(true, true, true, false, false, true);
206        assert_eq!(
207            is_viable_lan_interface(&intf, |_| false),
208            Err(ViabilityError::NotPhysical)
209        );
210    }
211
212    #[test]
213    fn is_viable_no_mac() {
214        let intf = mock_interface(true, false, true, false, false, true);
215        assert_eq!(
216            is_viable_lan_interface(&intf, |_| true),
217            Err(ViabilityError::NoMacAddress)
218        );
219    }
220
221    #[test]
222    fn is_viable_success() {
223        let intf = mock_interface(true, true, true, false, false, true);
224        assert_eq!(is_viable_lan_interface(&intf, |_| true), Ok(()));
225    }
226}