getip/
hostip.rs

1//! Implementations of `Provider` that receive local IP addresses of the machine
2//! from `libc` APIs and command-line helper programs (if available).
3//
4//  Copyright (C) 2021 Zhang Maiyun <me@maiyun.me>
5//
6//  This file is part of DNS updater.
7//
8//  DNS updater is free software: you can redistribute it and/or modify
9//  it under the terms of the GNU Affero General Public License as published by
10//  the Free Software Foundation, either version 3 of the License, or
11//  (at your option) any later version.
12//
13//  DNS updater is distributed in the hope that it will be useful,
14//  but WITHOUT ANY WARRANTY; without even the implied warranty of
15//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16//  GNU Affero General Public License for more details.
17//
18//  You should have received a copy of the GNU Affero General Public License
19//  along with DNS updater.  If not, see <https://www.gnu.org/licenses/>.
20//
21
22use crate::libc_getips::get_iface_addrs;
23use crate::Provider;
24use crate::{Error, IpType, Result};
25use async_trait::async_trait;
26use log::{debug, info};
27use std::net::IpAddr;
28use std::process::{Output, Stdio};
29use std::str::FromStr;
30use tokio::process::Command;
31
32/// IPv6 Provider that queries information from `ip` or `ifconfig`.
33#[derive(Debug, Clone)]
34pub struct LocalIpv6CommandProvider {
35    nic: String,
36    // XXX: follow address selection algorithm for non-permanent
37    prefer_permanent: bool,
38}
39
40impl LocalIpv6CommandProvider {
41    /// Create a new `LocalIpv6CommandProvider`.
42    #[must_use]
43    pub fn new(nic: &str, permanent: bool) -> Self {
44        Self {
45            nic: nic.to_string(),
46            prefer_permanent: permanent,
47        }
48    }
49}
50
51#[async_trait]
52impl Provider for LocalIpv6CommandProvider {
53    /// Get a local IPv6 address on the specified interface with `ip` or `ifconfig`.
54    ///
55    /// # Errors
56    ///
57    /// This function returns `NoAddress` if the IP commands return no addresses.
58    /// In case that none of those commands succeed, it returns the last process execution error.
59    async fn get_addr(&self) -> Result<IpAddr> {
60        let out = chain_ip_cmd_until_succeed(&self.nic).await?;
61        // Extract output
62        let out_br = out.stdout.split(|c| *c == b'\n');
63        // Some systems use temporary addresses and one permanent address.
64        // The second fields indicates whether the "secured"/permanent flag is present.
65        let mut addrs: Vec<(IpAddr, bool)> = Vec::with_capacity(4);
66        // Extract addresses line by line
67        for line in out_br {
68            let line = String::from_utf8(line.to_vec())?;
69            let fields: Vec<String> = line.split_whitespace().map(ToString::to_string).collect();
70            // A shorter one is certainly not an entry
71            // Check if the label is "inet6"
72            if fields.len() > 1 && fields[0] == "inet6" {
73                let address_stripped = match fields[1].split_once('/') {
74                    // `ip` includes the prefix length in the address
75                    Some((addr, _prefixlen)) => addr,
76                    // but `ifconfig` doesn't
77                    None => &fields[1],
78                };
79                if let Ok(addr6) = IpAddr::from_str(address_stripped) {
80                    // If "secured" (for RFC 3041, ifconfig) or "mngtmpaddr" (RFC 3041, ip) is in the flags, it is permanent
81                    let is_perm = fields.iter().any(|f| f == "secured" || f == "mngtmpaddr")
82                    // But any "temporary" tells us it is not.
83                        && fields.iter().all(|f| f != "temporary");
84                    // Treating non-RFC-3041 interface's addresses all the same
85                    if !addr6.is_loopback() {
86                        addrs.push((addr6, is_perm));
87                    }
88                }
89            }
90        }
91        if addrs.is_empty() {
92            debug!("Short-circuting NoAddress because an ip command succeeded without addresses");
93            Err(Error::NoAddress)
94        } else {
95            Ok(addrs
96                .iter()
97                // If is_perm == permanent, it is the one we are looking for
98                .filter(|(_, is_perm)| *is_perm == self.prefer_permanent)
99                .map(|(addr, _)| *addr)
100                .next()
101                .unwrap_or(addrs[0].0))
102        }
103    }
104
105    fn get_type(&self) -> IpType {
106        // This Provider only has IPv6 capabilities
107        IpType::Ipv6
108    }
109}
110
111/// Run a chain of ip/ifconfig commands and returns the output of first succeeded one
112async fn chain_ip_cmd_until_succeed(nic: &str) -> Result<Output> {
113    // TODO: netsh.exe IPv6 backend
114    // netsh interface ipv6 show addresses interface="Ethernet" level=normal
115    // TODO: Enable IPv4 to be queried like this
116    let commands = [
117        // First try to use `ip`
118        (
119            "ip",
120            vec![
121                "address", "show", "dev", nic,
122                // This scope filters out unique-local addresses
123                "scope", "global",
124            ],
125        ),
126        // If that failed, try (BSD) ifconfig
127        ("ifconfig", vec!["-L", nic, "inet6"]),
128        // Linux ifconfig cannot distinguish between RFC 3041 temporary/permanent addresses
129        ("ifconfig", vec![nic]),
130    ];
131    // Record only the last failure
132    let mut last_error: Option<Error> = None;
133    for (cmd, args) in commands {
134        let mut command = Command::new(cmd);
135        command.stdout(Stdio::piped());
136        debug!("Running command {cmd:?} with arguments {args:?}");
137        let output = command.args(&args).output().await;
138        match output {
139            Ok(output) => {
140                if output.status.success() {
141                    return Ok(output);
142                }
143                // Since a chain of commands are executed, these are not really errors
144                debug!(
145                    "Command {:?} failed with status: {}",
146                    command, output.status
147                );
148                last_error = Some(Error::NonZeroExit(output.status));
149            }
150            Err(exec_error) => {
151                debug!("Command {command:?} failed to be executed: {exec_error}");
152                last_error = Some(Error::IoError(exec_error));
153            }
154        }
155    }
156    info!("None of the commands to extract the IPv6 address succeeded.");
157    // Not failable
158    Err(last_error.unwrap())
159}
160
161/// Force-cast $id: `IpAddr` to `Ipv4Addr`
162macro_rules! cast_ipv4 {
163    ($id: expr) => {
164        if let IpAddr::V4(ip) = $id {
165            ip
166        } else {
167            unreachable!()
168        }
169    };
170}
171
172/// Force-cast $id: `IpAddr` to `Ipv6Addr`
173macro_rules! cast_ipv6 {
174    ($id: expr) => {
175        if let IpAddr::V6(ip) = $id {
176            ip
177        } else {
178            unreachable!()
179        }
180    };
181}
182
183/// Filter function that removes loopback/link local/unspecified IPv4 addresses
184#[allow(clippy::trivially_copy_pass_by_ref)]
185fn filter_nonroute_ipv4(addr: &&IpAddr) -> bool {
186    let remove = !addr.is_loopback() && !cast_ipv4!(addr).is_link_local() && !addr.is_unspecified();
187    if remove {
188        debug!("Removing address {addr:?} because it is loopback/link local/unspecified");
189    }
190    remove
191}
192
193/// Filter function that removes loopback/link local/unspecified IPv6 addresses
194#[allow(clippy::trivially_copy_pass_by_ref)]
195fn filter_nonroute_ipv6(addr: &&IpAddr) -> bool {
196    !addr.is_loopback()
197        && !addr.is_unspecified()
198        && (cast_ipv6!(addr).segments()[0] & 0xffc0) != 0xfe80
199}
200
201/// Filter function that prefers global IPv4 addresses
202#[allow(clippy::trivially_copy_pass_by_ref)]
203fn filter_nonlocal_ipv4(addr: &&&IpAddr) -> bool {
204    !cast_ipv4!(addr).is_private()
205}
206
207/// Filter function that prefers global IPv6 addresses
208#[allow(clippy::trivially_copy_pass_by_ref)]
209const fn filter_nonlocal_ipv6(_addr: &&&IpAddr) -> bool {
210    // XXX: `IpAddr::is_global` is probably a better choice but it's currently unstable.
211    false
212}
213
214/// Provider that queries information from libc interface.
215#[derive(Debug, Clone)]
216pub struct LocalLibcProvider {
217    nic: Option<String>,
218    ip_type: IpType,
219}
220
221impl LocalLibcProvider {
222    #[must_use]
223    /// Create a new `LocalLibcProvider`.
224    pub fn new(nic: Option<&str>, ip_type: IpType) -> Self {
225        Self {
226            nic: nic.map(ToString::to_string),
227            ip_type,
228        }
229    }
230}
231
232#[async_trait]
233impl Provider for LocalLibcProvider {
234    /// Get a local address on the specified interface.
235    ///
236    /// # Errors
237    ///
238    /// This function propagates the error from `libc_getips::get_iface_addrs`.
239    async fn get_addr(&self) -> Result<IpAddr> {
240        let addrs = get_iface_addrs(Some(self.ip_type), self.nic.as_deref())?;
241        let addrs: Vec<&IpAddr> = addrs
242            .iter()
243            // Remove loopback, link local, and unspecified
244            .filter(if self.ip_type == IpType::Ipv4 {
245                filter_nonroute_ipv4
246            } else {
247                filter_nonroute_ipv6
248            })
249            .collect();
250        // Prefer the ones that are likely global
251        let first_non_local_addr: Option<&&IpAddr> =
252            addrs.iter().find(if self.ip_type == IpType::Ipv4 {
253                filter_nonlocal_ipv4
254            } else {
255                filter_nonlocal_ipv6
256            });
257        first_non_local_addr.map_or_else(|| Ok(*addrs[0]), |addr| Ok(**addr))
258    }
259
260    fn get_type(&self) -> IpType {
261        self.ip_type
262    }
263}