1use std::collections::BTreeMap;
23
24use k8s_openapi::api::networking::v1::{
25 IPBlock, NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyIngressRule, NetworkPolicyPeer,
26 NetworkPolicyPort, NetworkPolicySpec,
27};
28use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta};
29use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
30
31use fakecloud_k8s::{labels, names};
32
33use super::firewall::{FirewallRule, InstanceRules};
34
35const INSTANCE_LABEL: &str = "fakecloud-ec2";
39
40pub fn policy_name(instance_id: &str) -> String {
42 format!("fakecloud-ec2-{}", names::label_safe(instance_id))
43}
44
45pub fn build_policies(
51 rules: &[InstanceRules],
52 namespace: &str,
53 instance_label: &str,
54) -> Vec<NetworkPolicy> {
55 rules
56 .iter()
57 .map(|r| build_one(r, namespace, instance_label))
58 .collect()
59}
60
61fn build_one(r: &InstanceRules, namespace: &str, instance_label: &str) -> NetworkPolicy {
62 let slug = names::label_safe(&r.instance_id);
63
64 let mut policy_labels = BTreeMap::new();
65 policy_labels.insert(
66 labels::MANAGED_BY.to_string(),
67 labels::MANAGED_BY_VALUE.to_string(),
68 );
69 policy_labels.insert(labels::INSTANCE.to_string(), instance_label.to_string());
70 policy_labels.insert(labels::SERVICE.to_string(), "ec2".to_string());
71
72 let mut selector_labels = BTreeMap::new();
73 selector_labels.insert(INSTANCE_LABEL.to_string(), slug);
74
75 NetworkPolicy {
76 metadata: ObjectMeta {
77 name: Some(policy_name(&r.instance_id)),
78 namespace: Some(namespace.to_string()),
79 labels: Some(policy_labels),
80 ..ObjectMeta::default()
81 },
82 spec: Some(NetworkPolicySpec {
83 pod_selector: LabelSelector {
84 match_labels: Some(selector_labels),
85 ..LabelSelector::default()
86 },
87 policy_types: Some(vec!["Ingress".to_string(), "Egress".to_string()]),
88 ingress: Some(r.ingress.iter().map(ingress_rule).collect()),
89 egress: Some(r.egress.iter().map(egress_rule).collect()),
90 }),
91 }
92}
93
94fn ingress_rule(r: &FirewallRule) -> NetworkPolicyIngressRule {
95 NetworkPolicyIngressRule {
96 from: peers(&r.cidr),
97 ports: ports(r),
98 }
99}
100
101fn egress_rule(r: &FirewallRule) -> NetworkPolicyEgressRule {
102 NetworkPolicyEgressRule {
103 to: peers(&r.cidr),
104 ports: ports(r),
105 }
106}
107
108fn peers(cidr: &Option<String>) -> Option<Vec<NetworkPolicyPeer>> {
112 let c = cidr.as_deref()?;
113 if c == "0.0.0.0/0" || c.is_empty() {
114 return None;
115 }
116 Some(vec![NetworkPolicyPeer {
117 ip_block: Some(IPBlock {
118 cidr: c.to_string(),
119 except: None,
120 }),
121 ..NetworkPolicyPeer::default()
122 }])
123}
124
125fn ports(r: &FirewallRule) -> Option<Vec<NetworkPolicyPort>> {
129 let proto = match r.protocol.as_str() {
130 "tcp" | "6" => "TCP",
131 "udp" | "17" => "UDP",
132 _ => return None,
135 };
136 let valid = |p: i64| (0..=65535).contains(&p);
141 if !valid(r.from_port) || !valid(r.to_port) {
142 return None;
143 }
144 let mut port = NetworkPolicyPort {
145 protocol: Some(proto.to_string()),
146 port: Some(IntOrString::Int(r.from_port as i32)),
147 end_port: None,
148 };
149 if r.to_port > r.from_port {
150 port.end_port = Some(r.to_port as i32);
151 }
152 Some(vec![port])
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum CniDriver {
160 Calico,
162 Cilium,
164 Unknown,
167}
168
169impl CniDriver {
170 pub fn enforces(self) -> bool {
172 matches!(self, CniDriver::Calico | CniDriver::Cilium)
173 }
174
175 pub fn from_components<I, S>(component_names: I) -> Self
179 where
180 I: IntoIterator<Item = S>,
181 S: AsRef<str>,
182 {
183 let mut calico = false;
184 let mut cilium = false;
185 for name in component_names {
186 let n = name.as_ref();
187 if n.contains("calico") {
188 calico = true;
189 }
190 if n.contains("cilium") {
191 cilium = true;
192 }
193 }
194 if calico {
195 CniDriver::Calico
196 } else if cilium {
197 CniDriver::Cilium
198 } else {
199 CniDriver::Unknown
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn rules(
209 instance: &str,
210 ingress: Vec<FirewallRule>,
211 egress: Vec<FirewallRule>,
212 ) -> InstanceRules {
213 InstanceRules {
214 instance_id: instance.to_string(),
215 subnet_id: "subnet-1".to_string(),
216 private_ip: "172.30.0.2".to_string(),
217 ingress,
218 egress,
219 }
220 }
221
222 fn tcp(port: i64, cidr: Option<&str>) -> FirewallRule {
223 FirewallRule {
224 protocol: "tcp".into(),
225 from_port: port,
226 to_port: port,
227 cidr: cidr.map(str::to_string),
228 }
229 }
230
231 #[test]
232 fn policy_selects_instance_and_sets_both_directions() {
233 let r = rules("i-0ABC", vec![tcp(22, Some("10.0.0.0/8"))], vec![]);
234 let p = &build_policies(&[r], "fakecloud", "fakecloud-123")[0];
235 assert_eq!(p.metadata.name.as_deref(), Some("fakecloud-ec2-i-0abc"));
236 let spec = p.spec.as_ref().unwrap();
237 assert_eq!(
239 spec.pod_selector
240 .match_labels
241 .as_ref()
242 .unwrap()
243 .get("fakecloud-ec2")
244 .map(String::as_str),
245 Some("i-0abc")
246 );
247 assert_eq!(
248 spec.policy_types.as_ref().unwrap(),
249 &vec!["Ingress".to_string(), "Egress".to_string()]
250 );
251 let ing = &spec.ingress.as_ref().unwrap()[0];
253 let peer = &ing.from.as_ref().unwrap()[0];
254 assert_eq!(peer.ip_block.as_ref().unwrap().cidr, "10.0.0.0/8");
255 let port = &ing.ports.as_ref().unwrap()[0];
256 assert_eq!(port.protocol.as_deref(), Some("TCP"));
257 assert_eq!(port.port, Some(IntOrString::Int(22)));
258 }
259
260 #[test]
261 fn anywhere_all_protocols_rule_has_no_peer_or_ports() {
262 let all = FirewallRule {
263 protocol: "-1".into(),
264 from_port: -1,
265 to_port: -1,
266 cidr: Some("0.0.0.0/0".into()),
267 };
268 let r = rules("i-1", vec![all], vec![]);
269 let p = &build_policies(&[r], "fakecloud", "x")[0];
270 let ing = &p.spec.as_ref().unwrap().ingress.as_ref().unwrap()[0];
271 assert!(ing.from.is_none(), "0.0.0.0/0 -> any source");
272 assert!(ing.ports.is_none(), "all protocols -> any port");
273 }
274
275 #[test]
276 fn port_range_uses_end_port() {
277 let range = FirewallRule {
278 protocol: "tcp".into(),
279 from_port: 8000,
280 to_port: 8100,
281 cidr: None,
282 };
283 let r = rules("i-1", vec![range], vec![]);
284 let p = &build_policies(&[r], "fakecloud", "x")[0];
285 let port = &p.spec.as_ref().unwrap().ingress.as_ref().unwrap()[0]
286 .ports
287 .as_ref()
288 .unwrap()[0];
289 assert_eq!(port.port, Some(IntOrString::Int(8000)));
290 assert_eq!(port.end_port, Some(8100));
291 }
292
293 #[test]
294 fn out_of_range_port_is_rejected_not_truncated() {
295 let huge = FirewallRule {
298 protocol: "tcp".into(),
299 from_port: 70000,
300 to_port: 70000,
301 cidr: None,
302 };
303 let r = rules("i-1", vec![huge], vec![]);
304 let p = &build_policies(&[r], "fakecloud", "x")[0];
305 assert!(p.spec.as_ref().unwrap().ingress.as_ref().unwrap()[0]
307 .ports
308 .is_none());
309 }
310
311 #[test]
312 fn referenced_member_ip_becomes_ipblock() {
313 let r = rules("i-1", vec![tcp(80, Some("172.30.0.3/32"))], vec![]);
314 let p = &build_policies(&[r], "fakecloud", "x")[0];
315 let peer = &p.spec.as_ref().unwrap().ingress.as_ref().unwrap()[0]
316 .from
317 .as_ref()
318 .unwrap()[0];
319 assert_eq!(peer.ip_block.as_ref().unwrap().cidr, "172.30.0.3/32");
320 }
321
322 #[test]
323 fn cni_detection_and_enforcement() {
324 assert_eq!(
325 CniDriver::from_components(["calico-node", "coredns"]),
326 CniDriver::Calico
327 );
328 assert_eq!(
329 CniDriver::from_components(["cilium-abc"]),
330 CniDriver::Cilium
331 );
332 assert_eq!(
333 CniDriver::from_components(["kindnet-xyz", "kube-proxy"]),
334 CniDriver::Unknown
335 );
336 assert!(CniDriver::Calico.enforces());
337 assert!(CniDriver::Cilium.enforces());
338 assert!(!CniDriver::Unknown.enforces());
339 }
340}