Add stable local IPv6 provider

This commit is contained in:
Mygod
2026-05-18 13:24:14 -04:00
parent fddabc7a3d
commit 9574f67b98
3 changed files with 172 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
use crate::pp::{self, PP};
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use reqwest::Client;
use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
use std::time::Duration;
@@ -36,6 +37,7 @@ pub enum ProviderType {
Ipify,
Local,
LocalIface { interface: String },
StableLocalIface { interface: String },
CustomURL { url: String },
Literal { ips: Vec<IpAddr> },
None,
@@ -49,6 +51,7 @@ impl ProviderType {
ProviderType::Ipify => "ipify",
ProviderType::Local => "local",
ProviderType::LocalIface { .. } => "local.iface",
ProviderType::StableLocalIface { .. } => "local.iface.stable",
ProviderType::CustomURL { .. } => "url:",
ProviderType::Literal { .. } => "literal:",
ProviderType::None => "none",
@@ -78,6 +81,11 @@ impl ProviderType {
if input == "local" {
return Ok(ProviderType::Local);
}
if let Some(iface) = input.strip_prefix("local.iface.stable:") {
return Ok(ProviderType::StableLocalIface {
interface: iface.to_string(),
});
}
if let Some(iface) = input.strip_prefix("local.iface:") {
return Ok(ProviderType::LocalIface {
interface: iface.to_string(),
@@ -131,6 +139,9 @@ impl ProviderType {
ProviderType::LocalIface { interface } => {
detect_local_iface(interface, ip_type, ppfmt)
}
ProviderType::StableLocalIface { interface } => {
detect_stable_local_iface(interface, ip_type, ppfmt)
}
ProviderType::CustomURL { url } => {
detect_custom_url(client, url, ip_type, timeout, ppfmt).await
}
@@ -525,6 +536,105 @@ fn detect_local_iface(interface: &str, ip_type: IpType, ppfmt: &PP) -> Vec<IpAdd
}
}
// --- Stable Local Interface ---
const IF_INET6_PATH: &str = "/proc/net/if_inet6";
const IFA_F_TEMPORARY: u32 = 0x01;
const IFA_F_DADFAILED: u32 = 0x08;
const IFA_F_DEPRECATED: u32 = 0x20;
const IFA_F_TENTATIVE: u32 = 0x40;
const IPV6_SCOPE_GLOBAL: u8 = 0x00;
#[derive(Debug, Clone, PartialEq, Eq)]
struct IfInet6Address {
ip: Ipv6Addr,
prefix_len: u8,
scope: u8,
flags: u32,
interface: String,
}
fn detect_stable_local_iface(interface: &str, ip_type: IpType, ppfmt: &PP) -> Vec<IpAddr> {
if ip_type == IpType::V4 {
return detect_local_iface(interface, ip_type, ppfmt);
}
let contents = match fs::read_to_string(IF_INET6_PATH) {
Ok(contents) => contents,
Err(e) => {
ppfmt.warningf(
pp::EMOJI_WARNING,
&format!("Failed to read {IF_INET6_PATH} for stable IPv6 detection: {e}"),
);
return Vec::new();
}
};
let ip = stable_ipv6_addresses_from_if_inet6(&contents, interface)
.into_iter()
.next();
if ip.is_none() {
ppfmt.warningf(
pp::EMOJI_WARNING,
&format!("No stable global IPv6 address found on interface {interface}"),
);
}
ip.into_iter().map(IpAddr::V6).collect()
}
fn stable_ipv6_addresses_from_if_inet6(contents: &str, interface: &str) -> Vec<Ipv6Addr> {
let mut entries: Vec<IfInet6Address> = contents
.lines()
.filter_map(parse_if_inet6_line)
.filter(|addr| addr.interface == interface && is_stable_global_ipv6(addr))
.collect();
entries.sort_by(|a, b| {
a.prefix_len
.cmp(&b.prefix_len)
.then_with(|| a.ip.to_string().cmp(&b.ip.to_string()))
});
let mut ips: Vec<Ipv6Addr> = entries.into_iter().map(|addr| addr.ip).collect();
ips.dedup();
ips
}
fn is_stable_global_ipv6(addr: &IfInet6Address) -> bool {
addr.scope == IPV6_SCOPE_GLOBAL
&& IpAddr::V6(addr.ip).is_global_()
&& addr.flags & (IFA_F_TEMPORARY | IFA_F_DADFAILED | IFA_F_DEPRECATED | IFA_F_TENTATIVE)
== 0
}
fn parse_if_inet6_line(line: &str) -> Option<IfInet6Address> {
let mut fields = line.split_whitespace();
let addr_hex = fields.next()?;
let _ifindex = fields.next()?;
let prefix_hex = fields.next()?;
let scope_hex = fields.next()?;
let flags_hex = fields.next()?;
let interface = fields.next()?.to_string();
if addr_hex.len() != 32 {
return None;
}
let mut octets = [0_u8; 16];
for (index, octet) in octets.iter_mut().enumerate() {
let start = index * 2;
*octet = u8::from_str_radix(&addr_hex[start..start + 2], 16).ok()?;
}
Some(IfInet6Address {
ip: Ipv6Addr::from(octets),
prefix_len: u8::from_str_radix(prefix_hex, 16).ok()?,
scope: u8::from_str_radix(scope_hex, 16).ok()?,
flags: u32::from_str_radix(flags_hex, 16).ok()?,
interface,
})
}
// --- Custom URL ---
async fn detect_custom_url(
@@ -695,6 +805,16 @@ mod tests {
}
}
#[test]
fn test_provider_parse_stable_local_iface() {
match ProviderType::parse("local.iface.stable:eth0").unwrap() {
ProviderType::StableLocalIface { interface } => {
assert_eq!(interface, "eth0");
}
_ => panic!("Expected StableLocalIface provider"),
}
}
#[test]
fn test_provider_parse_custom_url() {
match ProviderType::parse("url:https://example.com/ip").unwrap() {
@@ -1286,6 +1406,49 @@ mod tests {
assert!(is_global_v6(&Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1)));
}
#[test]
fn test_parse_if_inet6_line() {
let addr = parse_if_inet6_line(
"20010db8000000011111222233334444 03 40 00 00 eth0",
)
.unwrap();
assert_eq!(
addr.ip,
"2001:db8:0:1:1111:2222:3333:4444"
.parse::<Ipv6Addr>()
.unwrap()
);
assert_eq!(addr.prefix_len, 64);
assert_eq!(addr.scope, IPV6_SCOPE_GLOBAL);
assert_eq!(addr.flags, 0);
assert_eq!(addr.interface, "eth0");
}
#[test]
fn test_stable_ipv6_addresses_from_if_inet6_filters_privacy_addresses() {
let contents = "\
20010db8000000015555666677778888 03 40 00 01 eth0
20010db8000000010000000000003486 03 80 00 00 eth0
20010db8000000011111222233334444 03 40 00 00 eth0
20010db8000000019999aaaabbbbcccc 03 40 00 21 eth0
fe80000000000000d399115858c872af 03 40 20 80 eth0
fdaa149d3b9900000000000000000001 0a 40 00 82 br-990e55930a86
";
let ips = stable_ipv6_addresses_from_if_inet6(contents, "eth0");
assert_eq!(
ips,
vec![
"2001:db8:0:1:1111:2222:3333:4444"
.parse::<Ipv6Addr>()
.unwrap(),
"2001:db8:0:1::3486".parse::<Ipv6Addr>().unwrap(),
]
);
}
// ---- ProviderType::name ----
#[test]
@@ -1302,6 +1465,10 @@ mod tests {
ProviderType::LocalIface { interface: "eth0".into() }.name(),
"local.iface"
);
assert_eq!(
ProviderType::StableLocalIface { interface: "eth0".into() }.name(),
"local.iface.stable"
);
assert_eq!(
ProviderType::CustomURL { url: "https://x".into() }.name(),
"url:"