1use crate::value::VmDictExt;
20use std::collections::BTreeMap;
21use std::net::IpAddr;
22use std::str::FromStr;
23use std::sync::Arc;
24
25use ipnet::IpNet;
26use serde_json::json;
27use url::Url;
28
29use crate::event_log::{active_event_log, EventLog, LogEvent, Topic};
30use crate::value::{VmClosure, VmError, VmValue};
31
32pub const NET_POLICY_AUDIT_TOPIC: &str = "harness.net.policy.audit";
34
35pub const HARN_NET_POLICY_BYPASS_ENV: &str = "HARN_NET_POLICY_BYPASS";
38
39#[derive(Clone, Debug)]
41pub struct NetPolicyRule {
42 pub raw: String,
43 pub matcher: NetMatcher,
44 pub ports: Option<Vec<u16>>,
45}
46
47#[derive(Clone, Debug)]
48pub enum NetMatcher {
49 Host(String),
52 Suffix(String),
54 Ip(IpAddr),
56 Cidr(IpNet),
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum NetPolicyDefault {
63 Allow,
64 Deny,
65}
66
67impl NetPolicyDefault {
68 pub fn as_str(self) -> &'static str {
69 match self {
70 NetPolicyDefault::Allow => "allow",
71 NetPolicyDefault::Deny => "deny",
72 }
73 }
74
75 pub fn parse(raw: &str) -> Result<Self, VmError> {
76 match raw.trim().to_ascii_lowercase().as_str() {
77 "allow" => Ok(NetPolicyDefault::Allow),
78 "" | "deny" => Ok(NetPolicyDefault::Deny),
79 other => Err(vm_error(format!(
80 "NetPolicy.create: default must be `allow` or `deny`, got `{other}`"
81 ))),
82 }
83 }
84}
85
86#[derive(Clone, Debug)]
89pub enum OnViolation {
90 Error,
92 AuditOnly,
95 Quarantine,
99 Callback(Arc<VmClosure>),
103}
104
105impl OnViolation {
106 pub fn parse_str(raw: &str) -> Result<Self, VmError> {
107 match raw.trim() {
108 "error" => Ok(OnViolation::Error),
109 "audit_only" => Ok(OnViolation::AuditOnly),
110 "quarantine" => Ok(OnViolation::Quarantine),
111 other => Err(vm_error(format!(
112 "NetPolicy.create: on_violation must be one of `error`, `audit_only`, `quarantine`, or a callback, got `{other}`"
113 ))),
114 }
115 }
116}
117
118#[derive(Clone, Debug)]
120pub struct NetPolicy {
121 pub allow: Arc<Vec<NetPolicyRule>>,
122 pub deny: Arc<Vec<NetPolicyRule>>,
123 pub default: NetPolicyDefault,
124 pub on_violation: OnViolation,
125}
126
127#[derive(Clone, Debug)]
130pub enum NetPolicyDecision {
131 Allow {
134 audited: bool,
135 audit: Option<NetPolicyAudit>,
136 },
137 Deny {
140 audit: NetPolicyAudit,
141 quarantine: bool,
142 },
143}
144
145#[derive(Clone, Debug)]
147pub struct NetPolicyAudit {
148 pub method: String,
149 pub url: String,
150 pub host: String,
151 pub port: Option<u16>,
152 pub reason: String,
153 pub outcome: &'static str,
154 pub bypass: bool,
155 pub matched_rule: Option<String>,
156}
157
158impl NetPolicyAudit {
159 fn to_json(&self) -> serde_json::Value {
160 json!({
161 "method": self.method,
162 "url": self.url,
163 "host": self.host,
164 "port": self.port,
165 "reason": self.reason,
166 "outcome": self.outcome,
167 "bypass": self.bypass,
168 "matched_rule": self.matched_rule,
169 })
170 }
171}
172
173#[derive(Clone, Debug)]
174pub struct NetTarget {
175 pub host: String,
176 pub ip: Option<IpAddr>,
177 pub port: Option<u16>,
178}
179
180impl NetTarget {
181 pub fn parse(raw_url: &str) -> Result<Self, VmError> {
182 let parsed = Url::parse(raw_url)
183 .map_err(|error| vm_error(format!("harness.net: invalid URL `{raw_url}`: {error}")))?;
184 let host = parsed.host_str().ok_or_else(|| {
185 vm_error(format!(
186 "harness.net: URL `{raw_url}` does not include a host"
187 ))
188 })?;
189 let host = normalize_host(host);
190 let ip = IpAddr::from_str(&host).ok();
191 Ok(Self {
192 host,
193 ip,
194 port: parsed.port_or_known_default(),
195 })
196 }
197}
198
199impl NetPolicyRule {
200 pub fn parse_host(raw: &str, ports: Option<Vec<u16>>) -> Result<Self, VmError> {
201 let raw = raw.trim();
202 if raw.is_empty() {
203 return Err(vm_error("NetPolicy.host: empty host"));
204 }
205 let host = normalize_host(raw);
206 let matcher = if let Some(suffix) = host.strip_prefix("*.") {
207 if suffix.is_empty() {
208 return Err(vm_error(format!(
209 "NetPolicy.domain_wildcard: invalid wildcard `{raw}`"
210 )));
211 }
212 NetMatcher::Suffix(suffix.to_string())
213 } else if let Ok(ip) = IpAddr::from_str(&host) {
214 NetMatcher::Ip(ip)
215 } else {
216 NetMatcher::Host(host)
217 };
218 Ok(Self {
219 raw: raw.to_string(),
220 matcher,
221 ports,
222 })
223 }
224
225 pub fn parse_domain(raw: &str) -> Result<Self, VmError> {
226 Self::parse_host(raw, None)
227 }
228
229 pub fn parse_domain_wildcard(raw: &str) -> Result<Self, VmError> {
230 let trimmed = raw.trim();
231 if !trimmed.starts_with("*.") {
232 return Err(vm_error(format!(
233 "NetPolicy.domain_wildcard: pattern must start with `*.`, got `{raw}`"
234 )));
235 }
236 Self::parse_host(trimmed, None)
237 }
238
239 pub fn parse_cidr(raw: &str) -> Result<Self, VmError> {
240 let trimmed = raw.trim();
241 let net = IpNet::from_str(trimmed)
242 .map_err(|error| vm_error(format!("NetPolicy.cidr: invalid CIDR `{raw}`: {error}")))?;
243 Ok(Self {
244 raw: trimmed.to_string(),
245 matcher: NetMatcher::Cidr(net),
246 ports: None,
247 })
248 }
249
250 pub fn matches(&self, target: &NetTarget) -> bool {
251 if let Some(ports) = &self.ports {
252 match target.port {
253 Some(port) if ports.contains(&port) => {}
254 _ => return false,
255 }
256 }
257 match &self.matcher {
258 NetMatcher::Host(host) => target.host == *host,
259 NetMatcher::Suffix(suffix) => {
260 target.host.len() > suffix.len()
261 && target.host.ends_with(suffix)
262 && target
263 .host
264 .as_bytes()
265 .get(target.host.len() - suffix.len() - 1)
266 == Some(&b'.')
267 }
268 NetMatcher::Ip(ip) => target.ip == Some(*ip),
269 NetMatcher::Cidr(net) => target.ip.is_some_and(|ip| net.contains(&ip)),
270 }
271 }
272}
273
274impl NetPolicy {
275 pub fn evaluate(&self, method: &str, raw_url: &str) -> Result<NetPolicyDecision, VmError> {
279 let target = NetTarget::parse(raw_url)?;
280 if let Some(rule) = self.deny.iter().find(|rule| rule.matches(&target)) {
281 return Ok(self.deny_decision(
282 method,
283 raw_url,
284 &target,
285 format!("matched deny rule `{}`", rule.raw),
286 Some(rule.raw.clone()),
287 ));
288 }
289 if let Some(rule) = self.allow.iter().find(|rule| rule.matches(&target)) {
290 return Ok(NetPolicyDecision::Allow {
291 audited: false,
292 audit: Some(NetPolicyAudit {
293 method: method.to_string(),
294 url: raw_url.to_string(),
295 host: target.host,
296 port: target.port,
297 reason: format!("matched allow rule `{}`", rule.raw),
298 outcome: "allow",
299 bypass: false,
300 matched_rule: Some(rule.raw.clone()),
301 }),
302 });
303 }
304 if self.default == NetPolicyDefault::Allow {
305 return Ok(NetPolicyDecision::Allow {
306 audited: false,
307 audit: None,
308 });
309 }
310 Ok(self.deny_decision(
311 method,
312 raw_url,
313 &target,
314 "no allow rule matched (default deny)".to_string(),
315 None,
316 ))
317 }
318
319 fn deny_decision(
320 &self,
321 method: &str,
322 raw_url: &str,
323 target: &NetTarget,
324 reason: String,
325 matched_rule: Option<String>,
326 ) -> NetPolicyDecision {
327 match &self.on_violation {
328 OnViolation::Error => NetPolicyDecision::Deny {
329 audit: NetPolicyAudit {
330 method: method.to_string(),
331 url: raw_url.to_string(),
332 host: target.host.clone(),
333 port: target.port,
334 reason,
335 outcome: "error",
336 bypass: false,
337 matched_rule,
338 },
339 quarantine: false,
340 },
341 OnViolation::AuditOnly => NetPolicyDecision::Allow {
342 audited: true,
343 audit: Some(NetPolicyAudit {
344 method: method.to_string(),
345 url: raw_url.to_string(),
346 host: target.host.clone(),
347 port: target.port,
348 reason,
349 outcome: "audit_only",
350 bypass: false,
351 matched_rule,
352 }),
353 },
354 OnViolation::Quarantine => NetPolicyDecision::Deny {
355 audit: NetPolicyAudit {
356 method: method.to_string(),
357 url: raw_url.to_string(),
358 host: target.host.clone(),
359 port: target.port,
360 reason,
361 outcome: "quarantine",
362 bypass: false,
363 matched_rule,
364 },
365 quarantine: true,
366 },
367 OnViolation::Callback(_) => NetPolicyDecision::Deny {
372 audit: NetPolicyAudit {
373 method: method.to_string(),
374 url: raw_url.to_string(),
375 host: target.host.clone(),
376 port: target.port,
377 reason,
378 outcome: "callback",
379 bypass: false,
380 matched_rule,
381 },
382 quarantine: false,
383 },
384 }
385 }
386}
387
388pub fn violation_vm_error(audit: &NetPolicyAudit) -> VmError {
392 let mut dict = BTreeMap::new();
393 dict.put_str("type", "NetPolicyViolation");
394 dict.put_str("category", "net_policy_violation");
395 dict.put_str(
396 "message",
397 format!(
398 "harness.net.{} blocked {}: {}",
399 audit.method, audit.url, audit.reason
400 ),
401 );
402 dict.put_str("method", audit.method.as_str());
403 dict.put_str("url", audit.url.as_str());
404 dict.put_str("host", audit.host.as_str());
405 dict.insert(
406 "port".to_string(),
407 audit
408 .port
409 .map(|port| VmValue::Int(port as i64))
410 .unwrap_or(VmValue::Nil),
411 );
412 dict.put_str("reason", audit.reason.as_str());
413 dict.put_str("outcome", audit.outcome);
414 dict.insert(
415 "matched_rule".to_string(),
416 audit
417 .matched_rule
418 .as_deref()
419 .map(|raw| VmValue::String(std::sync::Arc::from(raw)))
420 .unwrap_or(VmValue::Nil),
421 );
422 if audit.bypass {
423 dict.insert("bypass".to_string(), VmValue::Bool(true));
424 }
425 VmError::Thrown(VmValue::dict(dict))
426}
427
428pub fn violation_request_value(audit: &NetPolicyAudit) -> VmValue {
432 let mut dict = BTreeMap::new();
433 dict.put_str("method", audit.method.as_str());
434 dict.put_str("url", audit.url.as_str());
435 dict.put_str("host", audit.host.as_str());
436 dict.insert(
437 "port".to_string(),
438 audit
439 .port
440 .map(|port| VmValue::Int(port as i64))
441 .unwrap_or(VmValue::Nil),
442 );
443 dict.put_str("reason", audit.reason.as_str());
444 dict.insert(
445 "matched_rule".to_string(),
446 audit
447 .matched_rule
448 .as_deref()
449 .map(|raw| VmValue::String(std::sync::Arc::from(raw)))
450 .unwrap_or(VmValue::Nil),
451 );
452 VmValue::dict(dict)
453}
454
455pub async fn record_audit(audit: &NetPolicyAudit) {
459 let Some(log) = active_event_log() else {
460 return;
461 };
462 let Ok(topic) = Topic::new(NET_POLICY_AUDIT_TOPIC) else {
463 return;
464 };
465 let _ = log
466 .append(
467 &topic,
468 LogEvent::new("net.policy.evaluated", audit.to_json()),
469 )
470 .await;
471}
472
473pub fn bypass_enabled() -> bool {
477 match std::env::var(HARN_NET_POLICY_BYPASS_ENV) {
478 Ok(value) => matches!(
479 value.trim().to_ascii_lowercase().as_str(),
480 "1" | "true" | "yes" | "on"
481 ),
482 Err(_) => false,
483 }
484}
485
486fn normalize_host(host: &str) -> String {
487 host.trim()
488 .trim_end_matches('.')
489 .trim_matches('[')
490 .trim_matches(']')
491 .to_ascii_lowercase()
492}
493
494fn vm_error(message: impl Into<String>) -> VmError {
495 VmError::Thrown(VmValue::String(std::sync::Arc::from(message.into())))
496}
497
498pub mod parse {
502 use super::*;
503
504 pub const RULE_TAG_KEY: &str = "__net_policy_rule";
506 pub const POLICY_TAG_KEY: &str = "__net_policy";
508
509 pub fn rule_from_vm(value: &VmValue) -> Result<NetPolicyRule, VmError> {
514 match value {
515 VmValue::Dict(dict) => rule_from_dict(dict),
516 VmValue::String(raw) => {
517 let raw = raw.as_ref();
518 if raw.starts_with("*.") {
519 NetPolicyRule::parse_domain_wildcard(raw)
520 } else if raw.contains('/') {
521 NetPolicyRule::parse_cidr(raw)
522 } else {
523 NetPolicyRule::parse_domain(raw)
524 }
525 }
526 other => Err(vm_error(format!(
527 "NetPolicy: rule must be a tagged dict or string, got {}",
528 other.type_name()
529 ))),
530 }
531 }
532
533 fn rule_from_dict(dict: &crate::value::DictMap) -> Result<NetPolicyRule, VmError> {
534 let tag = dict
535 .get(RULE_TAG_KEY)
536 .and_then(|v| match v {
537 VmValue::String(s) => Some(s.to_string()),
538 _ => None,
539 })
540 .ok_or_else(|| {
541 vm_error(
542 "NetPolicy: rule dict is missing the `__net_policy_rule` tag; build rules via NetPolicy.domain/.domain_wildcard/.cidr/.host",
543 )
544 })?;
545 match tag.as_str() {
546 "domain" => {
547 let host = require_string(dict, "host", "NetPolicy.domain")?;
548 NetPolicyRule::parse_domain(&host)
549 }
550 "domain_wildcard" => {
551 let pattern = require_string(dict, "pattern", "NetPolicy.domain_wildcard")?;
552 NetPolicyRule::parse_domain_wildcard(&pattern)
553 }
554 "cidr" => {
555 let range = require_string(dict, "range", "NetPolicy.cidr")?;
556 NetPolicyRule::parse_cidr(&range)
557 }
558 "host" => {
559 let host = require_string(dict, "host", "NetPolicy.host")?;
560 let ports = match dict.get("ports") {
561 Some(VmValue::List(list)) => {
562 let mut parsed = Vec::with_capacity(list.len());
563 for value in list.iter() {
564 let port = value
565 .as_int()
566 .and_then(|n| u16::try_from(n).ok())
567 .ok_or_else(|| {
568 vm_error("NetPolicy.host: ports must be a list of u16 integers")
569 })?;
570 parsed.push(port);
571 }
572 Some(parsed)
573 }
574 Some(VmValue::Nil) | None => None,
575 Some(_) => {
576 return Err(vm_error(
577 "NetPolicy.host: ports must be a list of u16 integers",
578 ))
579 }
580 };
581 NetPolicyRule::parse_host(&host, ports)
582 }
583 other => Err(vm_error(format!("NetPolicy: unknown rule kind `{other}`"))),
584 }
585 }
586
587 fn require_string(
588 dict: &crate::value::DictMap,
589 key: &str,
590 callee: &str,
591 ) -> Result<String, VmError> {
592 match dict.get(key) {
593 Some(VmValue::String(s)) => Ok(s.as_ref().to_string()),
594 Some(other) => Err(vm_error(format!(
595 "{callee}: `{key}` must be a string, got {}",
596 other.type_name()
597 ))),
598 None => Err(vm_error(format!("{callee}: missing `{key}` field"))),
599 }
600 }
601
602 pub fn policy_from_dict(dict: &crate::value::DictMap) -> Result<NetPolicy, VmError> {
605 let allow = parse_rule_list(dict.get("allow"), "allow")?;
606 let deny = parse_rule_list(dict.get("deny"), "deny")?;
607 let default = match dict.get("default") {
608 Some(VmValue::String(s)) => NetPolicyDefault::parse(s.as_ref())?,
609 Some(VmValue::Nil) | None => NetPolicyDefault::Deny,
610 Some(other) => {
611 return Err(vm_error(format!(
612 "NetPolicy.create: default must be a string, got {}",
613 other.type_name()
614 )))
615 }
616 };
617 let on_violation = match dict.get("on_violation") {
618 Some(VmValue::String(s)) => OnViolation::parse_str(s.as_ref())?,
619 Some(VmValue::Closure(closure)) => OnViolation::Callback(Arc::clone(closure)),
620 Some(VmValue::Nil) | None => OnViolation::Error,
621 Some(other) => {
622 return Err(vm_error(format!(
623 "NetPolicy.create: on_violation must be a string or callback, got {}",
624 other.type_name()
625 )))
626 }
627 };
628 Ok(NetPolicy {
629 allow: Arc::new(allow),
630 deny: Arc::new(deny),
631 default,
632 on_violation,
633 })
634 }
635
636 fn parse_rule_list(value: Option<&VmValue>, side: &str) -> Result<Vec<NetPolicyRule>, VmError> {
637 match value {
638 None | Some(VmValue::Nil) => Ok(Vec::new()),
639 Some(VmValue::List(items)) => items.iter().map(rule_from_vm).collect(),
640 Some(other) => Err(vm_error(format!(
641 "NetPolicy.create: `{side}` must be a list, got {}",
642 other.type_name()
643 ))),
644 }
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651
652 fn rule(raw: &str, ports: Option<Vec<u16>>) -> NetPolicyRule {
653 NetPolicyRule::parse_host(raw, ports).expect("rule parses")
654 }
655
656 fn cidr(raw: &str) -> NetPolicyRule {
657 NetPolicyRule::parse_cidr(raw).expect("cidr parses")
658 }
659
660 fn build(
661 allow: Vec<NetPolicyRule>,
662 deny: Vec<NetPolicyRule>,
663 default: NetPolicyDefault,
664 ) -> NetPolicy {
665 NetPolicy {
666 allow: Arc::new(allow),
667 deny: Arc::new(deny),
668 default,
669 on_violation: OnViolation::Error,
670 }
671 }
672
673 #[test]
674 fn exact_host_match_allows() {
675 let policy = build(
676 vec![rule("github.com", None)],
677 Vec::new(),
678 NetPolicyDefault::Deny,
679 );
680 let decision = policy
681 .evaluate("get", "https://github.com/foo")
682 .expect("evaluates");
683 assert!(matches!(decision, NetPolicyDecision::Allow { .. }));
684 }
685
686 #[test]
687 fn wildcard_does_not_match_bare_apex() {
688 let policy = build(
689 vec![NetPolicyRule::parse_domain_wildcard("*.github.com").unwrap()],
690 Vec::new(),
691 NetPolicyDefault::Deny,
692 );
693 let allow = policy.evaluate("get", "https://api.github.com/x").unwrap();
694 assert!(matches!(allow, NetPolicyDecision::Allow { .. }));
695 let deny = policy.evaluate("get", "https://github.com/x").unwrap();
696 assert!(matches!(deny, NetPolicyDecision::Deny { .. }));
697 }
698
699 #[test]
700 fn cidr_matches_ip_literal() {
701 let policy = build(vec![cidr("10.0.0.0/8")], Vec::new(), NetPolicyDefault::Deny);
702 let allowed = policy.evaluate("get", "http://10.5.5.5/x").unwrap();
703 assert!(matches!(allowed, NetPolicyDecision::Allow { .. }));
704 let denied = policy.evaluate("get", "http://192.168.1.1/x").unwrap();
705 assert!(matches!(denied, NetPolicyDecision::Deny { .. }));
706 }
707
708 #[test]
709 fn host_port_rule_requires_matching_port() {
710 let policy = build(
711 vec![rule("api.anthropic.com", Some(vec![443]))],
712 Vec::new(),
713 NetPolicyDefault::Deny,
714 );
715 let allow = policy
716 .evaluate("get", "https://api.anthropic.com/v1/messages")
717 .unwrap();
718 assert!(matches!(allow, NetPolicyDecision::Allow { .. }));
719 let deny = policy
720 .evaluate("get", "http://api.anthropic.com/v1/messages")
721 .unwrap();
722 assert!(matches!(deny, NetPolicyDecision::Deny { .. }));
723 }
724
725 #[test]
726 fn deny_overrides_allow() {
727 let policy = build(
728 vec![NetPolicyRule::parse_domain_wildcard("*.github.com").unwrap()],
729 vec![rule("evil.github.com", None)],
730 NetPolicyDefault::Deny,
731 );
732 let decision = policy.evaluate("get", "https://evil.github.com/x").unwrap();
733 match decision {
734 NetPolicyDecision::Deny { audit, .. } => {
735 assert!(audit.reason.contains("deny rule"));
736 }
737 other => panic!("expected deny, got {other:?}"),
738 }
739 }
740
741 #[test]
742 fn default_allow_lets_unmatched_through() {
743 let policy = build(Vec::new(), Vec::new(), NetPolicyDefault::Allow);
744 let allow = policy.evaluate("get", "https://example.test/x").unwrap();
745 assert!(matches!(allow, NetPolicyDecision::Allow { .. }));
746 }
747
748 #[test]
749 fn audit_only_allows_but_carries_audit() {
750 let mut policy = build(Vec::new(), Vec::new(), NetPolicyDefault::Deny);
751 policy.on_violation = OnViolation::AuditOnly;
752 let decision = policy
753 .evaluate("get", "https://blocked.test/x")
754 .expect("evaluates");
755 match decision {
756 NetPolicyDecision::Allow { audited, audit } => {
757 assert!(audited);
758 let audit = audit.expect("audit attached");
759 assert_eq!(audit.outcome, "audit_only");
760 assert_eq!(audit.host, "blocked.test");
761 }
762 other => panic!("expected audit_only allow, got {other:?}"),
763 }
764 }
765
766 #[test]
767 fn quarantine_denies_with_signal() {
768 let mut policy = build(Vec::new(), Vec::new(), NetPolicyDefault::Deny);
769 policy.on_violation = OnViolation::Quarantine;
770 match policy
771 .evaluate("get", "https://blocked.test/x")
772 .expect("evaluates")
773 {
774 NetPolicyDecision::Deny { audit, quarantine } => {
775 assert!(quarantine);
776 assert_eq!(audit.outcome, "quarantine");
777 }
778 other => panic!("expected quarantine deny, got {other:?}"),
779 }
780 }
781
782 #[test]
783 fn invalid_url_surfaces_typed_error() {
784 let policy = build(Vec::new(), Vec::new(), NetPolicyDefault::Deny);
785 let err = policy.evaluate("get", "not a url").unwrap_err();
786 match err {
787 VmError::Thrown(VmValue::String(s)) => {
788 assert!(s.contains("invalid URL"), "unexpected error: {s}");
789 }
790 other => panic!("expected Thrown, got {other:?}"),
791 }
792 }
793
794 #[test]
795 fn parse_string_rule_branches_on_shape() {
796 let domain =
797 parse::rule_from_vm(&VmValue::String(std::sync::Arc::from("github.com"))).unwrap();
798 assert!(matches!(domain.matcher, NetMatcher::Host(_)));
799 let wildcard =
800 parse::rule_from_vm(&VmValue::String(std::sync::Arc::from("*.github.com"))).unwrap();
801 assert!(matches!(wildcard.matcher, NetMatcher::Suffix(_)));
802 let cidr_rule =
803 parse::rule_from_vm(&VmValue::String(std::sync::Arc::from("10.0.0.0/8"))).unwrap();
804 assert!(matches!(cidr_rule.matcher, NetMatcher::Cidr(_)));
805 }
806
807 #[test]
808 fn bypass_env_recognised() {
809 let original = std::env::var(HARN_NET_POLICY_BYPASS_ENV).ok();
810 std::env::set_var(HARN_NET_POLICY_BYPASS_ENV, "1");
811 assert!(bypass_enabled());
812 std::env::set_var(HARN_NET_POLICY_BYPASS_ENV, "0");
813 assert!(!bypass_enabled());
814 match original {
815 Some(value) => std::env::set_var(HARN_NET_POLICY_BYPASS_ENV, value),
816 None => std::env::remove_var(HARN_NET_POLICY_BYPASS_ENV),
817 }
818 }
819}