From 90b73dd3207857c9cc3be76d21865dec7b9d4a9f Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 11 Mar 2026 15:14:20 -0600 Subject: [PATCH] feat: support multiple echoip URLs with fallback Rename ifconfig_url to echoip_urls and iterate through configured URLs, falling back to the next one on failure. Reduces timeout per attempt from 10s to 5s. --- core/locales/i18n.yaml | 21 +++++++ core/src/db/model/public.rs | 15 +++-- core/src/lib.rs | 6 +- core/src/net/gateway.rs | 117 ++++++++++++++++++++++-------------- core/src/system/mod.rs | 14 ++--- 5 files changed, 111 insertions(+), 62 deletions(-) diff --git a/core/locales/i18n.yaml b/core/locales/i18n.yaml index d0f92d305..856617307 100644 --- a/core/locales/i18n.yaml +++ b/core/locales/i18n.yaml @@ -1592,6 +1592,13 @@ net.gateway.cannot-delete-without-connection: fr_FR: "Impossible de supprimer l'appareil sans connexion active" pl_PL: "Nie można usunąć urządzenia bez aktywnego połączenia" +net.gateway.no-configured-echoip-urls: + en_US: "No configured echoip URLs" + de_DE: "Keine konfigurierten EchoIP-URLs" + es_ES: "No hay URLs de echoip configuradas" + fr_FR: "Aucune URL echoip configurée" + pl_PL: "Brak skonfigurowanych adresów URL echoip" + # net/dns.rs net.dns.timeout-updating-catalog: en_US: "timed out waiting to update dns catalog" @@ -2753,6 +2760,13 @@ help.arg.download-directory: fr_FR: "Chemin du répertoire de téléchargement" pl_PL: "Ścieżka katalogu do pobrania" +help.arg.echoip-urls: + en_US: "Echo IP service URLs for external IP detection" + de_DE: "Echo-IP-Dienst-URLs zur externen IP-Erkennung" + es_ES: "URLs del servicio Echo IP para detección de IP externa" + fr_FR: "URLs du service Echo IP pour la détection d'IP externe" + pl_PL: "Adresy URL usługi Echo IP do wykrywania zewnętrznego IP" + help.arg.emulate-missing-arch: en_US: "Emulate missing architecture using this one" de_DE: "Fehlende Architektur mit dieser emulieren" @@ -5260,6 +5274,13 @@ about.set-country: fr_FR: "Définir le pays" pl_PL: "Ustaw kraj" +about.set-echoip-urls: + en_US: "Set the Echo IP service URLs" + de_DE: "Die Echo-IP-Dienst-URLs festlegen" + es_ES: "Establecer las URLs del servicio Echo IP" + fr_FR: "Définir les URLs du service Echo IP" + pl_PL: "Ustaw adresy URL usługi Echo IP" + about.set-hostname: en_US: "Set the server hostname" de_DE: "Den Server-Hostnamen festlegen" diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index 30ee515fd..a07375adf 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -146,7 +146,7 @@ impl Public { zram: true, governor: None, smtp: None, - ifconfig_url: default_ifconfig_url(), + echoip_urls: default_echoip_urls(), ram: 0, devices: Vec::new(), kiosk, @@ -168,8 +168,11 @@ fn get_platform() -> InternedString { (&*PLATFORM).into() } -pub fn default_ifconfig_url() -> Url { - "https://ifconfig.co".parse().unwrap() +pub fn default_echoip_urls() -> Vec { + vec![ + "https://ipconfig.io".parse().unwrap(), + "https://ifconfig.co".parse().unwrap(), + ] } #[derive(Debug, Deserialize, Serialize, HasModel, TS)] @@ -206,9 +209,9 @@ pub struct ServerInfo { pub zram: bool, pub governor: Option, pub smtp: Option, - #[serde(default = "default_ifconfig_url")] - #[ts(type = "string")] - pub ifconfig_url: Url, + #[serde(default = "default_echoip_urls")] + #[ts(type = "string[]")] + pub echoip_urls: Vec, #[ts(type = "number")] pub ram: u64, pub devices: Vec, diff --git a/core/src/lib.rs b/core/src/lib.rs index 10913503d..bf5805e88 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -400,10 +400,10 @@ pub fn server() -> ParentHandler { .with_call_remote::(), ) .subcommand( - "set-ifconfig-url", - from_fn_async(system::set_ifconfig_url) + "set-echoip-urls", + from_fn_async(system::set_echoip_urls) .no_display() - .with_about("about.set-ifconfig-url") + .with_about("about.set-echoip-urls") .with_call_remote::(), ) .subcommand( diff --git a/core/src/net/gateway.rs b/core/src/net/gateway.rs index a377eb453..33a608f14 100644 --- a/core/src/net/gateway.rs +++ b/core/src/net/gateway.rs @@ -205,7 +205,7 @@ pub async fn check_port( CheckPortParams { port, gateway }: CheckPortParams, ) -> Result { let db = ctx.db.peek().await; - let base_url = db.as_public().as_server_info().as_ifconfig_url().de()?; + let base_urls = db.as_public().as_server_info().as_echoip_urls().de()?; let gateways = db .as_public() .as_server_info() @@ -240,22 +240,41 @@ pub async fn check_port( let client = reqwest::Client::builder(); #[cfg(target_os = "linux")] let client = client.interface(gateway.as_str()); - let url = base_url - .join(&format!("/port/{port}")) - .with_kind(ErrorKind::ParseUrl)?; - let IfconfigPortRes { + let client = client.build()?; + + let mut res = None; + for base_url in base_urls { + let url = base_url + .join(&format!("/port/{port}")) + .with_kind(ErrorKind::ParseUrl)?; + res = Some( + async { + client + .get(url) + .timeout(Duration::from_secs(5)) + .send() + .await? + .error_for_status()? + .json() + .await + } + .await, + ); + if res.as_ref().map_or(false, |r| r.is_ok()) { + break; + } + } + let Some(IfconfigPortRes { ip, port, reachable: open_externally, - } = client - .build()? - .get(url) - .timeout(Duration::from_secs(10)) - .send() - .await? - .error_for_status()? - .json() - .await?; + }) = res.transpose()? + else { + return Err(Error::new( + eyre!("{}", t!("net.gateway.no-configured-echoip-urls")), + ErrorKind::Network, + )); + }; let hairpinning = tokio::time::timeout( Duration::from_secs(5), @@ -761,7 +780,7 @@ async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result, E let text = client .build()? .get(url) - .timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(5)) .send() .await? .error_for_status()? @@ -857,7 +876,7 @@ async fn watch_ip( .fuse() }); - let mut prev_attempt: Option = None; + let mut echoip_ratelimit_state: BTreeMap = BTreeMap::new(); loop { until @@ -967,7 +986,7 @@ async fn watch_ip( &dhcp4_proxy, &policy_guard, &iface, - &mut prev_attempt, + &mut echoip_ratelimit_state, db, write_to, device_type, @@ -1174,7 +1193,7 @@ async fn poll_ip_info( dhcp4_proxy: &Option>, policy_guard: &Option, iface: &GatewayId, - prev_attempt: &mut Option, + echoip_ratelimit_state: &mut BTreeMap, db: Option<&TypedPatchDb>, write_to: &Watch>, device_type: Option, @@ -1221,43 +1240,49 @@ async fn poll_ip_info( apply_policy_routing(guard, iface, &lan_ip).await?; } - let ifconfig_url = if let Some(db) = db { + let echoip_urls = if let Some(db) = db { db.peek() .await .as_public() .as_server_info() - .as_ifconfig_url() + .as_echoip_urls() .de() - .unwrap_or_else(|_| crate::db::model::public::default_ifconfig_url()) + .unwrap_or_else(|_| crate::db::model::public::default_echoip_urls()) } else { - crate::db::model::public::default_ifconfig_url() + crate::db::model::public::default_echoip_urls() }; - let wan_ip = if prev_attempt.map_or(true, |i| i.elapsed() > Duration::from_secs(300)) - && !subnets.is_empty() - && !matches!( - device_type, - Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback) - ) { - let res = match get_wan_ipv4(iface.as_str(), &ifconfig_url).await { - Ok(a) => a, - Err(e) => { - tracing::error!( - "{}", - t!( - "net.gateway.failed-to-determine-wan-ip", - iface = iface.to_string(), - error = e.to_string() - ) - ); - tracing::debug!("{e:?}"); - None + let mut wan_ip = None; + for echoip_url in echoip_urls { + let wan_ip = if echoip_ratelimit_state + .get(&echoip_url) + .map_or(true, |i| i.elapsed() > Duration::from_secs(300)) + && !subnets.is_empty() + && !matches!( + device_type, + Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback) + ) { + match get_wan_ipv4(iface.as_str(), &echoip_url).await { + Ok(a) => { + wan_ip = a; + } + Err(e) => { + tracing::error!( + "{}", + t!( + "net.gateway.failed-to-determine-wan-ip", + iface = iface.to_string(), + error = e.to_string() + ) + ); + tracing::debug!("{e:?}"); + } + }; + echoip_ratelimit_state.insert(echoip_url, Instant::now()); + if wan_ip.is_some() { + break; } }; - *prev_attempt = Some(Instant::now()); - res - } else { - None - }; + } let mut ip_info = IpInfo { name: name.clone(), scope_id, diff --git a/core/src/system/mod.rs b/core/src/system/mod.rs index b0570379b..9caf9687b 100644 --- a/core/src/system/mod.rs +++ b/core/src/system/mod.rs @@ -1172,21 +1172,21 @@ pub async fn clear_system_smtp(ctx: RpcContext) -> Result<(), Error> { } #[derive(Debug, Clone, Deserialize, Serialize, Parser)] -pub struct SetIfconfigUrlParams { - #[arg(help = "help.arg.ifconfig-url")] - pub url: url::Url, +pub struct SetEchoipUrlsParams { + #[arg(help = "help.arg.echoip-urls")] + pub urls: Vec, } -pub async fn set_ifconfig_url( +pub async fn set_echoip_urls( ctx: RpcContext, - SetIfconfigUrlParams { url }: SetIfconfigUrlParams, + SetEchoipUrlsParams { urls }: SetEchoipUrlsParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { db.as_public_mut() .as_server_info_mut() - .as_ifconfig_url_mut() - .ser(&url) + .as_echoip_urls_mut() + .ser(&urls) }) .await .result