Skip to main content

agentshield/egress/policy/
network.rs

1use serde::{Deserialize, Serialize};
2
3/// Network-level IP range blocking policy.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct NetworkPolicy {
6    /// Block private IP ranges (10.x, 172.16-31.x, 192.168.x). Default: true.
7    #[serde(default = "default_true")]
8    pub block_private: bool,
9    /// Block link-local addresses (169.254.x). Default: true.
10    #[serde(default = "default_true")]
11    pub block_link_local: bool,
12    /// Block localhost (127.x, ::1). Default: true.
13    #[serde(default = "default_true")]
14    pub block_localhost: bool,
15    /// Block cloud metadata endpoints (169.254.169.254, etc.). Default: true.
16    #[serde(default = "default_true")]
17    pub block_metadata: bool,
18}
19
20fn default_true() -> bool {
21    true
22}
23
24impl Default for NetworkPolicy {
25    fn default() -> Self {
26        Self {
27            block_private: true,
28            block_link_local: true,
29            block_localhost: true,
30            block_metadata: true,
31        }
32    }
33}
34
35impl NetworkPolicy {
36    /// Check if an IP address is blocked by network policy.
37    pub(super) fn is_ip_blocked(&self, ip: &str) -> bool {
38        if self.block_localhost && is_localhost(ip) {
39            return true;
40        }
41        if self.block_private && is_private_ip(ip) {
42            return true;
43        }
44        if self.block_link_local && is_link_local(ip) {
45            return true;
46        }
47        if self.block_metadata && is_metadata_ip(ip) {
48            return true;
49        }
50        false
51    }
52}
53
54pub(super) fn is_localhost(ip: &str) -> bool {
55    ip.starts_with("127.") || ip == "::1" || ip == "localhost"
56}
57
58pub(super) fn is_private_ip(ip: &str) -> bool {
59    ip.starts_with("10.")
60        || (ip.starts_with("172.") && is_172_private(ip))
61        || ip.starts_with("192.168.")
62        || ip.starts_with("fd") // IPv6 ULA
63}
64
65pub(super) fn is_172_private(ip: &str) -> bool {
66    if let Some(second_octet) = ip
67        .strip_prefix("172.")
68        .and_then(|rest| rest.split('.').next())
69    {
70        if let Ok(n) = second_octet.parse::<u8>() {
71            return (16..=31).contains(&n);
72        }
73    }
74    false
75}
76
77pub(super) fn is_link_local(ip: &str) -> bool {
78    ip.starts_with("169.254.") || ip.starts_with("fe80:")
79}
80
81pub(super) fn is_metadata_ip(ip: &str) -> bool {
82    ip == "169.254.169.254"
83        || ip.contains("metadata.google.internal")
84        || ip == "100.100.100.200" // Alibaba Cloud
85        || ip == "169.254.170.2" // AWS ECS
86}