Skip to content

Commit 2915a62

Browse files
committed
1.0.5 - Added DNS Address classification, just rough for now, needs updates in future.
1 parent 31e2a12 commit 2915a62

File tree

4 files changed

+280
-34
lines changed

4 files changed

+280
-34
lines changed

Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[package]
22
name = "distributed-metrics"
3-
version = "1.0.4"
3+
version = "1.0.5"
44
edition = "2021"
55
repository = "https://github.com/BitpingApp/distributed-metrics"
66
homepage = "https://bitping.com"
77
authors = ["Firaenix <[email protected]>"]
88
description = "A monitoring tool backed by Bitping's distributed network, exposed as a Prometheus metrics endpoint"
99

1010
[dependencies]
11-
progenitor = { version = "0.8.0", features = [] }
11+
progenitor = { version = "0.9.0", features = [] }
1212
reqwest = { version = "0.12", default-features = false, features = [
1313
"json",
1414
"stream",
@@ -29,14 +29,15 @@ strum = { version = "0.26", features = ["derive"] }
2929
figment = { version = "0.10", features = ["toml", "yaml", "env"] }
3030
color-eyre = "0.6"
3131
tracing = "0.1"
32-
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
33-
regress = "0.10.1"
32+
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
33+
regress = { version = "0.10.1", features = [] }
3434
metrics = "0.24.1"
3535
metrics-exporter-prometheus = "0.16.0"
3636
metrics-dashboard = "0.3.3"
3737
poem = { version = "3.1.5", features = ["rustls"] }
3838
metrics-util = "0.18.0"
3939
humantime-serde = "1.1.1"
40+
serde_regex = "1.1.0"
4041

4142
# The profile that 'dist' will build with
4243
[profile.dist]

src/collectors/dns.rs

Lines changed: 260 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use reqwest::StatusCode;
1414
use std::collections::hash_map::DefaultHasher;
1515
use std::collections::{HashMap, HashSet};
1616
use std::hash::{Hash, Hasher};
17+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
18+
use std::ops::Deref;
1719
use std::str::FromStr;
1820
use tracing::{error, info, warn};
1921

@@ -132,6 +134,22 @@ impl Collector for DnsCollector {
132134
.and_then(|mo| PerformDnsBodyProxy::from_str(&mo).ok())
133135
.unwrap_or_default();
134136

137+
let isp = self
138+
.config
139+
.common_config
140+
.network
141+
.as_ref()
142+
.map(|n| n.isp_regex.clone())
143+
.unwrap_or_default();
144+
145+
let node_id = self
146+
.config
147+
.common_config
148+
.network
149+
.as_ref()
150+
.map(|n| n.node_id.clone())
151+
.unwrap_or_default();
152+
135153
info!(?self.config.common_config, ?country_code, "Sending DNS request");
136154

137155
match API_CLIENT
@@ -142,6 +160,8 @@ impl Collector for DnsCollector {
142160
.continent_code(continent_code)
143161
.mobile(mobile)
144162
.residential(residential)
163+
.isp_regex(isp)
164+
.node_id(node_id)
145165
.proxy(proxy)
146166
})
147167
.send()
@@ -303,35 +323,250 @@ where
303323
I::Item: AsRef<str>,
304324
{
305325
let mut providers = HashSet::new();
306-
let provider_map: HashMap<&str, &str> = [
307-
("8.8.8.8", "Google"),
308-
("8.8.4.4", "Google"),
309-
("1.1.1.1", "Cloudflare"),
310-
("1.0.0.1", "Cloudflare"),
311-
("9.9.9.9", "Quad9"),
312-
("149.112.112.112", "Quad9"),
313-
("208.67.222.222", "OpenDNS"),
314-
("208.67.220.220", "OpenDNS"),
315-
("94.140.14.14", "AdGuard"),
316-
("94.140.15.15", "AdGuard"),
317-
("185.228.168.9", "CleanBrowsing"),
318-
("185.228.169.9", "CleanBrowsing"),
319-
("76.76.19.19", "Alternate DNS"),
320-
("76.223.122.150", "Alternate DNS"),
321-
("76.76.2.0", "Control D"),
322-
("76.76.10.0", "Control D"),
323-
]
324-
.iter()
325-
.cloned()
326-
.collect();
326+
let provider_map = build_provider_map();
327327

328328
for ip in ips {
329-
let provider = provider_map
330-
.get(ip.as_ref())
331-
.map(|&p| p.to_string())
332-
.unwrap_or_else(|| ip.as_ref().to_string());
329+
let ip_str = ip.as_ref();
330+
331+
let ip_addr = match IpAddr::from_str(ip_str) {
332+
Ok(addr) => addr,
333+
Err(_) => {
334+
providers.insert("Invalid IP".to_string());
335+
continue;
336+
}
337+
};
338+
339+
let provider = match ip_addr {
340+
IpAddr::V4(ipv4) => {
341+
if is_loopback_v4(&ipv4) {
342+
"Localhost".to_string()
343+
} else if is_private_ipv4(&ipv4) {
344+
classify_private_network(&ipv4)
345+
} else if is_tailscale_range(&ipv4) {
346+
"Tailscale".to_string()
347+
} else if let Some(provider) = provider_map.get(ip_str) {
348+
provider.to_string()
349+
} else {
350+
classify_public_dns(&ipv4)
351+
}
352+
}
353+
IpAddr::V6(ipv6) => {
354+
if is_loopback_v6(&ipv6) {
355+
"Localhost".to_string()
356+
} else if is_private_ipv6(&ipv6) {
357+
classify_private_ipv6(&ipv6)
358+
} else if let Some(provider) = provider_map.get(ip_str) {
359+
provider.to_string()
360+
} else {
361+
classify_public_ipv6_dns(&ipv6)
362+
}
363+
}
364+
};
365+
333366
providers.insert(provider);
334367
}
335368

336369
providers
337370
}
371+
372+
fn is_loopback_v4(ip: &Ipv4Addr) -> bool {
373+
ip.octets()[0] == 127
374+
}
375+
376+
fn is_loopback_v6(ip: &Ipv6Addr) -> bool {
377+
*ip == Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)
378+
}
379+
380+
fn is_private_ipv4(ip: &Ipv4Addr) -> bool {
381+
let octets = ip.octets();
382+
match octets[0] {
383+
10 => true,
384+
172 => (16..=31).contains(&octets[1]),
385+
192 => octets[1] == 168,
386+
169 => octets[1] == 254,
387+
_ => false,
388+
}
389+
}
390+
391+
fn is_private_ipv6(ip: &Ipv6Addr) -> bool {
392+
let segments = ip.segments();
393+
394+
// fc00::/7 (unique local)
395+
(segments[0] & 0xfe00) == 0xfc00 ||
396+
// fe80::/10 (link local)
397+
(segments[0] & 0xffc0) == 0xfe80
398+
}
399+
400+
fn is_tailscale_range(ip: &Ipv4Addr) -> bool {
401+
let octets = ip.octets();
402+
octets[0] == 100 && (64..=127).contains(&octets[1])
403+
}
404+
405+
fn build_provider_map() -> HashMap<String, String> {
406+
let mut map = HashMap::new();
407+
408+
// Google DNS
409+
for ip in [
410+
"8.8.8.8",
411+
"8.8.4.4",
412+
"2001:4860:4860::8888",
413+
"2001:4860:4860::8844",
414+
]
415+
.iter()
416+
{
417+
map.insert((*ip).to_string(), "Google".to_string());
418+
}
419+
420+
// Cloudflare DNS
421+
for ip in [
422+
"1.1.1.1",
423+
"1.0.0.1",
424+
"1.1.1.2",
425+
"1.0.0.2",
426+
"2606:4700:4700::1111",
427+
"2606:4700:4700::1001",
428+
"2606:4700:4700::1112",
429+
"2606:4700:4700::1002",
430+
]
431+
.iter()
432+
{
433+
map.insert((*ip).to_string(), "Cloudflare".to_string());
434+
}
435+
436+
// Quad9
437+
for ip in ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"].iter() {
438+
map.insert((*ip).to_string(), "Quad9".to_string());
439+
}
440+
441+
// OpenDNS
442+
for ip in [
443+
"208.67.222.222",
444+
"208.67.220.220",
445+
"2620:119:35::35",
446+
"2620:119:53::53",
447+
]
448+
.iter()
449+
{
450+
map.insert((*ip).to_string(), "OpenDNS".to_string());
451+
}
452+
453+
// AdGuard
454+
for ip in [
455+
"94.140.14.14",
456+
"94.140.15.15",
457+
"2a10:50c0::ad1:ff",
458+
"2a10:50c0::ad2:ff",
459+
]
460+
.iter()
461+
{
462+
map.insert((*ip).to_string(), "AdGuard".to_string());
463+
}
464+
465+
// NextDNS (including all Anycast ranges)
466+
let nextdns_ranges = (0..=255)
467+
.filter(|&n| {
468+
matches!(
469+
n,
470+
0 | 1 | 11 | 42 | 68 | 99 | 139 | 165 | 185 | 216 | 233 | 241
471+
)
472+
})
473+
.flat_map(|n| {
474+
vec![
475+
format!("45.90.28.{}", n),
476+
format!("45.90.30.{}", n),
477+
format!("2a07:a8c0::{:x}", n),
478+
format!("2a07:a8c1::{:x}", n),
479+
]
480+
});
481+
482+
for ip in nextdns_ranges {
483+
map.insert(ip, "NextDNS".to_string());
484+
}
485+
486+
map
487+
}
488+
489+
fn classify_private_network(ip: &Ipv4Addr) -> String {
490+
let octets = ip.octets();
491+
match octets[0] {
492+
10 => "Private Network (10.0.0.0/8)".to_string(),
493+
172 if (16..=31).contains(&octets[1]) => "Private Network (172.16.0.0/12)".to_string(),
494+
192 if octets[1] == 168 => "Home Network (192.168.0.0/16)".to_string(),
495+
169 if octets[1] == 254 => "Link-local Network".to_string(),
496+
_ => "Unknown Private Network".to_string(),
497+
}
498+
}
499+
500+
fn classify_private_ipv6(ip: &Ipv6Addr) -> String {
501+
let segments = ip.segments();
502+
if (segments[0] & 0xfe00) == 0xfc00 {
503+
"Unique Local Address".to_string()
504+
} else if (segments[0] & 0xffc0) == 0xfe80 {
505+
"Link-local Network".to_string()
506+
} else {
507+
"Private IPv6 Network".to_string()
508+
}
509+
}
510+
511+
fn classify_public_dns(ip: &Ipv4Addr) -> String {
512+
let octets = ip.octets();
513+
514+
match octets[0] {
515+
// Reserved for IANA special use
516+
0 | 127 | 169 | 192 | 198 => "Reserved Range".to_string(),
517+
518+
// Major ISP ranges
519+
1..=100 => {
520+
if is_known_isp_range(ip) {
521+
"ISP DNS".to_string()
522+
} else {
523+
"Unknown Public DNS".to_string()
524+
}
525+
}
526+
527+
// Enterprise ranges
528+
128..=191 => "Enterprise DNS".to_string(),
529+
530+
_ => "Unknown Public DNS".to_string(),
531+
}
532+
}
533+
534+
fn classify_public_ipv6_dns(ip: &Ipv6Addr) -> String {
535+
let segments = ip.segments();
536+
537+
match segments[0] {
538+
// Global Unicast Address (2000::/3)
539+
s if (0x2000..=0x3FFF).contains(&s) => {
540+
if is_known_ipv6_dns_range(ip) {
541+
"Known IPv6 DNS Provider".to_string()
542+
} else {
543+
"ISP IPv6 DNS".to_string()
544+
}
545+
}
546+
547+
_ => "Unknown IPv6 DNS".to_string(),
548+
}
549+
}
550+
551+
fn is_known_isp_range(ip: &Ipv4Addr) -> bool {
552+
let octets = ip.octets();
553+
554+
matches!(
555+
(octets[0], octets[1]),
556+
(24..=50, _) | // ARIN space
557+
(62..=70, _) | // RIPE space
558+
(80..=90, _) | // RIPE space
559+
(98..=100, _) // APNIC space
560+
)
561+
}
562+
563+
fn is_known_ipv6_dns_range(ip: &Ipv6Addr) -> bool {
564+
let segments = ip.segments();
565+
566+
matches!(
567+
segments[0],
568+
0x2001 | // Teredo
569+
0x2606 | // Various providers
570+
0x2620 // Various providers
571+
)
572+
}

src/config.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{collections::HashMap, time::Duration};
22

3-
use serde::Deserialize;
3+
use regress::Regex;
4+
use serde::{Deserialize, Deserializer};
45

56
use eyre::{Context, Result};
67
use figment::{
@@ -92,11 +93,16 @@ pub enum ContinentCode {
9293

9394
#[derive(Deserialize, Clone, Debug)]
9495
pub struct NetworkCriteria {
96+
#[serde(default)]
9597
pub proxy: Policy,
98+
#[serde(default)]
9699
pub mobile: Policy,
100+
#[serde(default)]
97101
pub residential: Policy,
98102
pub country_code: Option<keshvar::Alpha3>,
99103
pub continent_code: Option<ContinentCode>,
104+
pub isp_regex: Option<String>,
105+
pub node_id: Option<String>,
100106
}
101107

102108
impl Conf {

0 commit comments

Comments
 (0)