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 <myzhang1029@hotmail.com>
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
70 .split_whitespace()
71 .map(std::string::ToString::to_string)
72 .collect();
73 // A shorter one is certainly not an entry
74 // Check if the label is "inet6"
75 if fields.len() > 1 && fields[0] == "inet6" {
76 let address_stripped = match fields[1].split_once('/') {
77 // `ip` includes the prefix length in the address
78 Some((addr, _prefixlen)) => addr,
79 // but `ifconfig` doesn't
80 None => &fields[1],
81 };
82 if let Ok(addr6) = IpAddr::from_str(address_stripped) {
83 // If "secured" (for RFC 3041, ifconfig) or "mngtmpaddr" (RFC 3041, ip) is in the flags, it is permanent
84 let is_perm = fields.iter().any(|f| f == "secured" || f == "mngtmpaddr")
85 // But any "temporary" tells us it is not.
86 && fields.iter().all(|f| f != "temporary");
87 // Treating non-RFC-3041 interface's addresses all the same
88 if !addr6.is_loopback() {
89 addrs.push((addr6, is_perm));
90 }
91 }
92 }
93 }
94 if addrs.is_empty() {
95 debug!("Short-circuting NoAddress because an ip command succeeded without addresses");
96 Err(crate::Error::NoAddress)
97 } else {
98 Ok(addrs
99 .iter()
100 // If is_perm == permanent, it is the one we are looking for
101 .filter(|(_, is_perm)| *is_perm == self.prefer_permanent)
102 .map(|(addr, _)| *addr)
103 .next()
104 .unwrap_or(addrs[0].0))
105 }
106 }
107
108 fn get_type(&self) -> IpType {
109 // This Provider only has IPv6 capabilities
110 IpType::Ipv6
111 }
112}
113
114/// Run a chain of ip/ifconfig commands and returns the output of first succeeded one
115async fn chain_ip_cmd_until_succeed(nic: &str) -> Result<Output> {
116 // TODO: netsh.exe IPv6 backend
117 // netsh interface ipv6 show addresses interface="Ethernet" level=normal
118 // TODO: Enable IPv4 to be queried like this
119 let commands = [
120 // First try to use `ip`
121 (
122 "ip",
123 vec![
124 "address", "show", "dev", nic,
125 // This scope filters out unique-local addresses
126 "scope", "global",
127 ],
128 ),
129 // If that failed, try (BSD) ifconfig
130 ("ifconfig", vec!["-L", nic, "inet6"]),
131 // Linux ifconfig cannot distinguish between RFC 3041 temporary/permanent addresses
132 ("ifconfig", vec![nic]),
133 ];
134 // Record only the last failure
135 let mut last_error: Option<Error> = None;
136 for (cmd, args) in commands {
137 let mut command = Command::new(cmd);
138 command.stdout(Stdio::piped());
139 debug!("Running command {:?} with arguments {:?}", cmd, args);
140 let output = command.args(&args).output().await;
141 match output {
142 Ok(output) => {
143 if output.status.success() {
144 return Ok(output);
145 }
146 // Since a chain of commands are executed, these are not really errors
147 debug!(
148 "Command {:?} failed with status: {}",
149 command, output.status
150 );
151 last_error = Some(Error::NonZeroExit(output.status));
152 }
153 Err(exec_error) => {
154 debug!(
155 "Command {:?} failed to be executed: {}",
156 command, exec_error
157 );
158 last_error = Some(Error::IoError(exec_error));
159 }
160 }
161 }
162 info!("None of the commands to extract the IPv6 address succeeded.");
163 // Not failable
164 Err(last_error.unwrap())
165}
166
167/// Force-cast $id: `IpAddr` to `Ipv4Addr`
168macro_rules! cast_ipv4 {
169 ($id: expr) => {
170 if let IpAddr::V4(ip) = $id {
171 ip
172 } else {
173 unreachable!()
174 }
175 };
176}
177
178/// Force-cast $id: `IpAddr` to `Ipv6Addr`
179macro_rules! cast_ipv6 {
180 ($id: expr) => {
181 if let IpAddr::V6(ip) = $id {
182 ip
183 } else {
184 unreachable!()
185 }
186 };
187}
188
189/// Filter function that removes loopback/link local/unspecified IPv4 addresses
190#[allow(clippy::trivially_copy_pass_by_ref)]
191fn filter_nonroute_ipv4(addr: &&IpAddr) -> bool {
192 let remove = !addr.is_loopback() && !cast_ipv4!(addr).is_link_local() && !addr.is_unspecified();
193 if remove {
194 debug!(
195 "Removing address {:?} because it is loopback/link local/unspecified",
196 addr
197 );
198 }
199 remove
200}
201
202/// Filter function that removes loopback/link local/unspecified IPv6 addresses
203#[allow(clippy::trivially_copy_pass_by_ref)]
204fn filter_nonroute_ipv6(addr: &&IpAddr) -> bool {
205 !addr.is_loopback()
206 && !addr.is_unspecified()
207 && !(cast_ipv6!(addr).segments()[0] & 0xffc0) == 0xfe80
208}
209
210/// Filter function that prefers global IPv4 addresses
211#[allow(clippy::trivially_copy_pass_by_ref)]
212fn filter_nonlocal_ipv4(addr: &&&IpAddr) -> bool {
213 !cast_ipv4!(addr).is_private()
214}
215
216/// Filter function that prefers global IPv6 addresses
217#[allow(clippy::trivially_copy_pass_by_ref)]
218fn filter_nonlocal_ipv6(_addr: &&&IpAddr) -> bool {
219 // XXX: `IpAddr::is_global` is probably a better choice but it's currently unstable.
220 false
221}
222
223/// Provider that queries information from libc interface.
224#[derive(Debug, Clone)]
225pub struct LocalLibcProvider {
226 nic: Option<String>,
227 ip_type: IpType,
228}
229
230impl<'a> LocalLibcProvider {
231 #[must_use]
232 /// Create a new `LocalLibcProvider`.
233 pub fn new(nic: Option<&'a str>, ip_type: IpType) -> Self {
234 Self {
235 nic: nic.map(std::string::ToString::to_string),
236 ip_type,
237 }
238 }
239}
240
241#[async_trait]
242impl Provider for LocalLibcProvider {
243 /// Get a local address on the specified interface.
244 ///
245 /// # Errors
246 ///
247 /// This function propagates the error from `libc_getips::get_iface_addrs`.
248 async fn get_addr(&self) -> Result<IpAddr> {
249 let addrs = get_iface_addrs(Some(self.ip_type), self.nic.as_deref())?;
250 let addrs: Vec<&IpAddr> = addrs
251 .iter()
252 // Remove loopback, link local, and unspecified
253 .filter(if self.ip_type == IpType::Ipv4 {
254 filter_nonroute_ipv4
255 } else {
256 filter_nonroute_ipv6
257 })
258 .collect();
259 // Prefer the ones that are likely global
260 let first_non_local_addr: Option<&&IpAddr> =
261 addrs.iter().find(if self.ip_type == IpType::Ipv4 {
262 filter_nonlocal_ipv4
263 } else {
264 filter_nonlocal_ipv6
265 });
266 first_non_local_addr.map_or_else(|| Ok(*addrs[0]), |addr| Ok(**addr))
267 }
268
269 fn get_type(&self) -> IpType {
270 self.ip_type
271 }
272}