Skip to main content

agentos_kernel/
network_policy.rs

1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2
3pub fn is_loopback_ip(ip: IpAddr) -> bool {
4    match ip {
5        IpAddr::V4(ip) => ip.is_loopback(),
6        IpAddr::V6(ip) => {
7            ip.is_loopback()
8                || ip
9                    .to_ipv4_mapped()
10                    .is_some_and(|mapped| mapped.is_loopback())
11        }
12    }
13}
14
15pub fn loopback_cidr(ip: IpAddr) -> &'static str {
16    match ip {
17        IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
18        IpAddr::V6(ip)
19            if ip
20                .to_ipv4_mapped()
21                .is_some_and(|mapped| mapped.is_loopback()) =>
22        {
23            "127.0.0.0/8"
24        }
25        IpAddr::V6(_) => "::1/128",
26        IpAddr::V4(_) => "127.0.0.0/8",
27    }
28}
29
30pub fn format_tcp_resource(host: &str, port: u16) -> String {
31    format!("tcp://{host}:{port}")
32}
33
34pub fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
35    match ip {
36        IpAddr::V4(ip) => {
37            if ip.is_unspecified() {
38                return Some(("0.0.0.0/32", "unspecified"));
39            }
40            let [first, second, ..] = ip.octets();
41            match (first, second) {
42                (10, _) => Some(("10.0.0.0/8", "private")),
43                (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
44                (172, 16..=31) => Some(("172.16.0.0/12", "private")),
45                (192, 168) => Some(("192.168.0.0/16", "private")),
46                (169, 254) => Some(("169.254.0.0/16", "link-local")),
47                (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
48                (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
49                _ => None,
50            }
51        }
52        IpAddr::V6(ip) => {
53            if let Some(mapped) = ip.to_ipv4_mapped() {
54                return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
55            }
56            if let Some(compat) = ipv4_compatible_embedded(ip) {
57                return restricted_non_loopback_ip_range(IpAddr::V4(compat));
58            }
59            if ip.is_unspecified() {
60                return Some(("::/128", "unspecified"));
61            }
62
63            let segments = ip.segments();
64            if (segments[0] & 0xfe00) == 0xfc00 {
65                return Some(("fc00::/7", "unique-local"));
66            }
67            if (segments[0] & 0xffc0) == 0xfe80 {
68                return Some(("fe80::/10", "link-local"));
69            }
70            None
71        }
72    }
73}
74
75fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
76    let segments = ip.segments();
77    if segments[0..6].iter().any(|&s| s != 0) {
78        return None;
79    }
80    let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
81    if embedded == 0 || embedded == 1 {
82        return None;
83    }
84    Some(Ipv4Addr::from(embedded))
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    fn assert_restricted(ip: IpAddr, expected_label: &str) {
92        let classification = restricted_non_loopback_ip_range(ip);
93        assert!(
94            classification.is_some(),
95            "{ip} must be classified as a restricted egress target"
96        );
97        let (_cidr, label) = classification.unwrap();
98        assert_eq!(
99            label, expected_label,
100            "{ip} should be labelled {expected_label}, got {label}"
101        );
102    }
103
104    #[test]
105    fn classifier_denies_unspecified_and_cgnat_targets() {
106        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
107        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
108        assert_restricted(
109            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
110            "carrier-grade-nat",
111        );
112        assert_restricted(
113            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
114            "carrier-grade-nat",
115        );
116        assert!(
117            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255,)))
118                .is_none()
119        );
120        assert!(
121            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none()
122        );
123    }
124
125    #[test]
126    fn classifier_denies_ipv6_spelled_metadata_addresses() {
127        assert_restricted(
128            IpAddr::V6("::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap()),
129            "link-local",
130        );
131        assert_restricted(
132            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
133            "link-local",
134        );
135        assert_restricted(
136            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
137            "private",
138        );
139        assert_restricted(
140            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
141            "carrier-grade-nat",
142        );
143        assert_eq!(
144            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
145            Some(("::/128", "unspecified"))
146        );
147        assert!(
148            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
149                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST))
150        );
151        assert!(restricted_non_loopback_ip_range(IpAddr::V6(
152            "::8.8.8.8".parse::<Ipv6Addr>().unwrap()
153        ))
154        .is_none());
155    }
156
157    #[test]
158    fn classifier_denies_reserved_and_multicast_targets() {
159        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
160        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
161        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
162        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
163        assert_restricted(
164            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
165            "multicast",
166        );
167        assert_restricted(
168            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
169            "reserved",
170        );
171        assert!(
172            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
173                .is_none()
174        );
175    }
176
177    #[test]
178    fn tcp_resource_format_matches_permission_resources() {
179        assert_eq!(
180            format_tcp_resource("127.0.0.1", 8080),
181            "tcp://127.0.0.1:8080"
182        );
183        assert_eq!(
184            format_tcp_resource("example.test", 443),
185            "tcp://example.test:443"
186        );
187    }
188}