CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(identity): classify labeler hostnames as local vs remote

Adds is_local_labeler_hostname classifier for determining whether a labeler
URL is locally reachable (localhost, ::1, RFC 1918 private, .local mDNS) or
remote. Used by later phases to validate self-mint did:web viability. Includes
comprehensive test table covering localhost variants, private ranges, and public hosts.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

authored by

Jack Grigg
Claude Haiku 4.5
and committed by
Tangled
bd77b0ba f7c34ac2

+71
+71
src/common/identity.rs
··· 877 877 Ok(decoded.to_string()) 878 878 } 879 879 880 + /// Classify a URL's hostname as "locally reachable from the tool's 881 + /// machine" for the purposes of self-mint `did:web` viability. 882 + /// 883 + /// Returns `true` when the hostname is one of: 884 + /// - `localhost` (case-insensitive) 885 + /// - `127.0.0.1` (or any IPv4 loopback / `::1`) 886 + /// - Any `.local` mDNS suffix (case-insensitive) 887 + /// - Any RFC 1918 IPv4 private address (10/8, 172.16/12, 192.168/16) 888 + /// 889 + /// Returns `false` for all other hostnames. IPv6 private ranges (fc00::/7, 890 + /// link-local) are deliberately NOT classified as local in v1; revisit if 891 + /// users report issues. 892 + pub fn is_local_labeler_hostname(url: &Url) -> bool { 893 + use url::Host; 894 + 895 + let host = match url.host() { 896 + Some(h) => h, 897 + None => return false, 898 + }; 899 + 900 + match host { 901 + Host::Ipv4(addr) => addr.is_loopback() || addr.is_private(), 902 + Host::Ipv6(addr) => addr.is_loopback(), 903 + Host::Domain(domain) => { 904 + let lower = domain.to_ascii_lowercase(); 905 + if lower == "localhost" { 906 + return true; 907 + } 908 + if lower.ends_with(".local") { 909 + return true; 910 + } 911 + false 912 + } 913 + } 914 + } 915 + 880 916 #[cfg(test)] 881 917 mod tests { 882 918 use super::*; ··· 1541 1577 64, 1542 1578 "P256 signature should be 64 bytes after JWS serialization" 1543 1579 ); 1580 + } 1581 + 1582 + #[test] 1583 + fn is_local_labeler_hostname_classifies_expected_hosts() { 1584 + let cases: &[(&str, bool)] = &[ 1585 + // Positive: localhost variants. 1586 + ("http://localhost/", true), 1587 + ("https://LOCALHOST:8080/foo", true), 1588 + ("http://127.0.0.1/", true), 1589 + ("http://127.1.2.3/", true), 1590 + ("http://[::1]/", true), 1591 + // Positive: .local mDNS. 1592 + ("http://mybox.local/", true), 1593 + ("https://mybox.LOCAL:8443/", true), 1594 + // Positive: RFC 1918. 1595 + ("http://10.0.0.1/", true), 1596 + ("http://172.16.0.1/", true), 1597 + ("http://172.31.255.255/", true), 1598 + ("http://192.168.1.100/", true), 1599 + // Negative: public. 1600 + ("https://labeler.example.com/", false), 1601 + ("http://8.8.8.8/", false), 1602 + ("http://172.15.0.1/", false), // outside 172.16/12 1603 + ("http://172.32.0.1/", false), // outside 172.16/12 1604 + ("http://11.0.0.1/", false), // outside 10/8 once we pass 10.x 1605 + ("http://172.17.1.1/", true), // inside 172.16/12 1606 + ]; 1607 + for (url, expected) in cases { 1608 + let parsed = Url::parse(url).expect("test URLs are valid"); 1609 + assert_eq!( 1610 + is_local_labeler_hostname(&parsed), 1611 + *expected, 1612 + "classification mismatch for {url}" 1613 + ); 1614 + } 1544 1615 } 1545 1616 }