ddns_core/
detector.rs

1//! Public-IP detector set
2//!
3//! * HTTP       – cross-platform  
4//! * Command    – cross-platform  
5//! * Interface  – uses `pnet_datalink` on Unix; not supported on Windows
6
7use crate::cfg::DetectCfg;
8use anyhow::{Result, anyhow};
9use reqwest::Client;
10use std::time::Duration;
11use tokio::{process::Command, time::timeout};
12use tracing::info;
13
14/*──────── interface detector (platform split) ────────*/
15#[cfg(unix)]
16fn detect_iface(iface: &str) -> Result<String> {
17    use pnet_datalink::interfaces;
18    use std::net::IpAddr;
19
20    for i in interfaces() {
21        if i.name == iface {
22            for ipn in i.ips {
23                if let IpAddr::V4(v4) = ipn.ip() {
24                    return Ok(v4.to_string());
25                }
26            }
27            return Err(anyhow!("interface `{iface}` has no IPv4 address"));
28        }
29    }
30    Err(anyhow!("interface `{iface}` not found"))
31}
32
33#[cfg(windows)]
34fn detect_iface(_iface: &str) -> Result<String> {
35    Err(anyhow!(
36        r#"kind = "interface" is not supported on Windows; \
37please use `http` or `command` instead"#
38    ))
39}
40
41/*──────── HTTP detector ────────*/
42async fn detect_http(url: &str, to: Option<u64>) -> Result<String> {
43    let fut = async {
44        Ok::<_, anyhow::Error>(
45            Client::new()
46                .get(url)
47                .send()
48                .await?
49                .text()
50                .await?
51                .trim()
52                .to_owned(),
53        )
54    };
55    match to {
56        Some(ms) => Ok(timeout(Duration::from_millis(ms), fut).await??),
57        None => fut.await,
58    }
59}
60
61/*──────── Command detector ────────*/
62async fn detect_cmd(cmd: &str, to: Option<u64>) -> Result<String> {
63    let fut = async {
64        let out = Command::new("sh").arg("-c").arg(cmd).output().await?;
65        Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
66    };
67    match to {
68        Some(ms) => Ok(timeout(Duration::from_millis(ms), fut).await??),
69        None => fut.await,
70    }
71}
72
73/*──────── orchestrator ────────*/
74pub async fn detect_ip(list: &[DetectCfg]) -> Result<String> {
75    // default priority is 100 if unspecified
76    let mut items = list.to_vec();
77    items.sort_by_key(|d| match d {
78        DetectCfg::Http { priority, .. }
79        | DetectCfg::Interface { priority, .. }
80        | DetectCfg::Command { priority, .. } => priority.unwrap_or(100),
81    });
82
83    for det in items {
84        match det {
85            DetectCfg::Http {
86                url, timeout: to, ..
87            } => {
88                if let Ok(ip) = detect_http(&url, to).await {
89                    info!("detect/http {url} -> {ip}");
90                    return Ok(ip);
91                }
92            }
93            DetectCfg::Interface { iface, .. } => {
94                if let Ok(ip) = detect_iface(&iface) {
95                    info!("detect/iface {iface} -> {ip}");
96                    return Ok(ip);
97                }
98            }
99            DetectCfg::Command {
100                cmd, timeout: to, ..
101            } => {
102                if let Ok(ip) = detect_cmd(&cmd, to).await {
103                    info!("detect/cmd `{cmd}` -> {ip}");
104                    return Ok(ip);
105                }
106            }
107        }
108    }
109    Err(anyhow!("all detectors failed"))
110}