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}