getip/
lib.rs

1//! Asynchronous library for retrieving IP address information
2//!
3//! For retrieval of a single address, simply use `get_ip` and set
4//! appropriate parameters.
5//!
6//! In case a finer-grained control over the source of addresses is required,
7//! individual `Providers` on which `get_ip` is based can be found in `gip`
8//! (for global) and `hostip` (for local).
9//!
10//! Module `libc_getips` contains a low-level function to receive all addresses
11//! directly from the interfaces/adapters.
12//
13//  Copyright (C) 2021 Zhang Maiyun <me@maiyun.me>
14//
15//  This file is part of DNS updater.
16//
17//  DNS updater is free software: you can redistribute it and/or modify
18//  it under the terms of the GNU Affero General Public License as published by
19//  the Free Software Foundation, either version 3 of the License, or
20//  (at your option) any later version.
21//
22//  DNS updater is distributed in the hope that it will be useful,
23//  but WITHOUT ANY WARRANTY; without even the implied warranty of
24//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25//  GNU Affero General Public License for more details.
26//
27//  You should have received a copy of the GNU Affero General Public License
28//  along with DNS updater.  If not, see <https://www.gnu.org/licenses/>.
29//
30
31#![warn(
32    clippy::pedantic,
33    missing_docs,
34    missing_debug_implementations,
35    missing_copy_implementations,
36    trivial_casts,
37    trivial_numeric_casts,
38    unsafe_op_in_unsafe_fn,
39    unused_extern_crates,
40    unused_import_braces,
41    unused_qualifications,
42    variant_size_differences
43)]
44#![allow(clippy::no_effect_underscore_binding)]
45
46pub mod gip;
47pub mod hostip;
48pub mod libc_getips;
49
50use async_trait::async_trait;
51use serde::Deserialize;
52use std::net::IpAddr;
53use thiserror::Error;
54
55/// Scope of the IP to be received.
56#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
57pub enum IpScope {
58    /// Address as found by an external service.
59    /// If used behind NAT, the address outside the NAT is received.
60    /// If IPv6 private address extension is enabled, the preferred address is usually used.
61    Global,
62    /// Address of the NIC.
63    /// If `IpType` is `Ipv6`, the permanent or secured address is preferred.
64    /// If `IpType` is `Ipv4`, the first address is used.
65    Local,
66}
67
68/// Type of global address.
69#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
70pub enum IpType {
71    /// An IPv4 address.
72    #[serde(rename = "IPv4")]
73    Ipv4,
74    /// An IPv6 address.
75    #[serde(rename = "IPv6")]
76    Ipv6,
77}
78
79/// Error type of IP retrieval methods.
80#[derive(Debug, Error)]
81pub enum Error {
82    /// Error from a global IP provider.
83    #[error(transparent)]
84    GlobalIpError(#[from] gip::GlobalIpError),
85
86    /// Cannot parse string as an IP.
87    #[error(transparent)]
88    AddrParseError(#[from] std::net::AddrParseError),
89
90    /// Cannot parse data as UTF-8.
91    #[error(transparent)]
92    UnicodeParseError(#[from] std::string::FromUtf8Error),
93
94    /// Command exited with a non-zero status.
95    #[error("Command exited with status {0}")]
96    NonZeroExit(std::process::ExitStatus),
97
98    /// All libc-related errors.
99    #[error(transparent)]
100    IoError(#[from] std::io::Error),
101
102    /// No address found.
103    #[error("no address returned")]
104    NoAddress,
105}
106
107/// A `Result` alias where the `Err` variant is `getip::Error`.
108pub type Result<R> = std::result::Result<R, Error>;
109
110/// Any IP Provider.
111#[async_trait]
112pub trait Provider: Sized {
113    /// Get the address provided by this provider.
114    async fn get_addr(&self) -> Result<IpAddr>;
115    /// Get the `IpType` that this provider gives.
116    fn get_type(&self) -> IpType;
117}
118
119/// Receive a IP address of family `ip_type` that has a scope of `ip_scope` on an interface named `nic`.
120///
121/// If `ip_scope` is `Global`, the address is received from an external service, and `nic` is ignored.
122/// If `nic` is `None`, this function uses the first one returned by the OS.
123///
124/// # Examples
125///
126/// Get an global IPv4 address:
127///
128/// ```
129/// use getip::get_ip;
130/// use getip::{IpScope, IpType, Result};
131///
132/// #[tokio::main]
133/// async fn main() -> Result<()> {
134///     let address = get_ip(IpType::Ipv4, IpScope::Global, None).await?;
135///     println!("{}", address);
136///     Ok(())
137/// }
138/// ```
139///
140/// # Errors
141///
142/// Any errors returned by the underlying provider is propagated here.
143///
144pub async fn get_ip(ip_type: IpType, ip_scope: IpScope, nic: Option<&str>) -> Result<IpAddr> {
145    match (ip_type, ip_scope) {
146        (IpType::Ipv4, IpScope::Global) => {
147            // Get a global IPv4 address
148            let p = gip::ProviderMultiple::default();
149            p.get_addr().await
150        }
151        (IpType::Ipv6, IpScope::Global) => {
152            // Get a global IPv6 address
153            let p = gip::ProviderMultiple::default_v6();
154            p.get_addr().await
155        }
156        (IpType::Ipv4, IpScope::Local) => {
157            // Get a local IPv4 address
158            let p = hostip::LocalLibcProvider::new(nic, IpType::Ipv4);
159            p.get_addr().await
160        }
161        (IpType::Ipv6, IpScope::Local) => {
162            // Get a local IPv6 address
163            if let Some(nic) = nic {
164                let command_provider = hostip::LocalIpv6CommandProvider::new(nic, true);
165                let command_result = command_provider.get_addr().await;
166                if command_result.is_ok() || matches!(command_result, Err(Error::NoAddress)) {
167                    return command_result;
168                }
169            }
170            let p = hostip::LocalLibcProvider::new(nic, IpType::Ipv6);
171            p.get_addr().await
172        }
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use crate::{get_ip, gip::ProviderMultiple, libc_getips, IpScope, IpType, Provider};
179    use std::net::IpAddr;
180
181    /// Test if any IPv6 address is available on an interface
182    /// If not, the test is skipped
183    fn has_any_ipv6_address(iface_name: Option<&str>, global: bool) -> bool {
184        libc_getips::get_iface_addrs(Some(IpType::Ipv6), iface_name).is_ok_and(|addresses| {
185            addresses.iter().any(|ip| {
186                !ip.is_loopback()
187                    && !ip.is_unspecified()
188                    && if global {
189                        if let IpAddr::V6(dcasted) = ip {
190                            // !is_unicast_link_local
191                            (dcasted.segments()[0] & 0xffc0) != 0xfe80
192                                        // !is_unique_local
193                                        && (dcasted.segments()[0] & 0xfe00) != 0xfc00
194                        } else {
195                            unreachable!()
196                        }
197                    } else {
198                        true
199                    }
200            })
201        })
202    }
203
204    #[tokio::test]
205    async fn test_global_ipv4_is_any() {
206        let addr = get_ip(IpType::Ipv4, IpScope::Global, None).await;
207        assert!(addr.is_ok(), "The result of get_addr() should be Ok()");
208        if let IpAddr::V4(addr) = addr.unwrap() {
209            assert!(
210                !addr.is_private(),
211                "The result of get_addr() should not be private"
212            );
213            assert!(
214                !addr.is_loopback(),
215                "The result of get_addr() should not be loopback"
216            );
217        } else {
218            panic!("The result of get_addr() should be an IPv4 address");
219        }
220    }
221
222    #[tokio::test]
223    async fn test_global_ipv4_just_dns_is_any() {
224        const DNS_PROVIDERS: &str = r#"[
225            {
226              "method": "dns",
227              "name": "opendns.com",
228              "type": "IPv4",
229              "url": "myip.opendns.com@resolver1.opendns.com"
230            },
231            {
232              "method": "dns",
233              "name": "opendns.com",
234              "type": "IPv6",
235              "url": "myip.opendns.com@resolver1.opendns.com"
236            },
237            {
238              "method": "dns",
239              "name": "akamai.com",
240              "type": "IPv4",
241              "url": "whoami.akamai.com@ns1-1.akamaitech.net"
242            }
243          ]"#;
244        let provider = ProviderMultiple::from_json(DNS_PROVIDERS).unwrap();
245        let addr = provider.get_addr().await;
246        assert!(addr.is_ok(), "The result of get_addr() should be Ok()");
247    }
248
249    #[tokio::test]
250    async fn test_global_ipv6_is_any() {
251        if !has_any_ipv6_address(None, true) {
252            return;
253        }
254        let addr = get_ip(IpType::Ipv6, IpScope::Global, None).await;
255        assert!(addr.is_ok(), "The result of get_addr() should be Ok()");
256        if let IpAddr::V6(addr) = addr.unwrap() {
257            assert!(
258                !addr.is_loopback(),
259                "The result of get_addr() should not be loopback"
260            );
261            assert!(
262                !addr.is_unspecified(),
263                "The result of get_addr() should not be unspecified"
264            );
265        } else {
266            panic!("The result of get_addr() should be an IPv6 address");
267        }
268    }
269
270    #[tokio::test]
271    async fn test_local_ipv4_is_any() {
272        let addr = get_ip(IpType::Ipv4, IpScope::Local, None).await;
273        assert!(addr.is_ok(), "The result of get_addr() should be Ok()");
274        assert!(
275            addr.unwrap().is_ipv4(),
276            "The result of get_addr() should be an IPv4 address"
277        );
278    }
279
280    #[tokio::test]
281    async fn test_local_ipv6_is_any() {
282        if !has_any_ipv6_address(None, true) {
283            return;
284        }
285        let addr = get_ip(IpType::Ipv6, IpScope::Local, None).await;
286        assert!(addr.is_ok(), "The result of get_addr() should be Ok()");
287        assert!(
288            addr.unwrap().is_ipv6(),
289            "The result of get_addr() should be an IPv6 address"
290        );
291    }
292}