1#![cfg_attr(not(windows), allow(dead_code))]
31
32use std::net::Ipv4Addr;
33
34pub const PROFILE_DOMAIN: u32 = 1;
36pub const PROFILE_PRIVATE: u32 = 2;
37pub const PROFILE_PUBLIC: u32 = 4;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Category {
42 Public,
43 Private,
44 Domain,
45}
46
47impl Category {
48 pub fn bit(self) -> u32 {
49 match self {
50 Category::Domain => PROFILE_DOMAIN,
51 Category::Private => PROFILE_PRIVATE,
52 Category::Public => PROFILE_PUBLIC,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Proto {
59 Tcp,
60 Udp,
61 Any,
62 Other,
64}
65
66impl Proto {
67 pub fn from_win(n: i32) -> Proto {
69 match n {
70 6 => Proto::Tcp,
71 17 => Proto::Udp,
72 256 => Proto::Any,
73 _ => Proto::Other,
74 }
75 }
76
77 fn covers(self, want: Proto) -> bool {
78 match self {
79 Proto::Any => matches!(want, Proto::Tcp | Proto::Udp),
80 Proto::Other => false,
81 p => p == want,
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum PortSpec {
90 Any,
91 Ranges(Vec<(u16, u16)>),
92 Unparseable,
93}
94
95impl PortSpec {
96 pub fn parse(spec: &str) -> PortSpec {
100 let spec = spec.trim();
101 if spec.is_empty() || spec == "*" {
102 return PortSpec::Any;
103 }
104 let mut ranges = Vec::new();
105 for part in spec.split(',') {
106 let part = part.trim();
107 let (lo, hi) = match part.split_once('-') {
108 Some((a, b)) => (a.trim().parse::<u16>(), b.trim().parse::<u16>()),
109 None => {
110 let p = part.parse::<u16>();
111 (p.clone(), p)
112 }
113 };
114 match (lo, hi) {
115 (Ok(lo), Ok(hi)) if lo <= hi => ranges.push((lo, hi)),
116 _ => return PortSpec::Unparseable,
117 }
118 }
119 PortSpec::Ranges(ranges)
120 }
121
122 pub fn contains(&self, port: u16) -> bool {
123 match self {
124 PortSpec::Any => true,
125 PortSpec::Ranges(r) => r.iter().any(|&(lo, hi)| (lo..=hi).contains(&port)),
126 PortSpec::Unparseable => false,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum Remote {
135 Any,
137 LocalSubnet,
139 Other,
141}
142
143impl Remote {
144 pub fn parse(spec: &str) -> Remote {
145 let spec = spec.trim();
146 if spec.is_empty() || spec == "*" {
147 Remote::Any
148 } else if spec.eq_ignore_ascii_case("LocalSubnet") {
149 Remote::LocalSubnet
150 } else {
151 Remote::Other
152 }
153 }
154
155 fn reaches_lan_peers(self) -> bool {
156 matches!(self, Remote::Any | Remote::LocalSubnet)
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum LocalScope {
167 Any,
169 Addrs(Vec<(Ipv4Addr, Ipv4Addr)>),
172 Unparseable,
175}
176
177impl LocalScope {
178 pub fn parse(spec: &str) -> LocalScope {
179 let spec = spec.trim();
180 if spec.is_empty() || spec == "*" {
181 return LocalScope::Any;
182 }
183 let mut out = Vec::new();
184 for part in spec.split(',') {
185 let part = part.trim();
186 if part == "*" || part.eq_ignore_ascii_case("LocalSubnet") {
187 return LocalScope::Any;
188 }
189 if part.contains(':') {
190 continue; }
192 match Self::parse_v4(part) {
193 Some(r) => out.push(r),
194 None => return LocalScope::Unparseable,
195 }
196 }
197 LocalScope::Addrs(out)
198 }
199
200 fn parse_v4(part: &str) -> Option<(Ipv4Addr, Ipv4Addr)> {
201 if let Some((a, b)) = part.split_once('-') {
202 let (lo, hi) = (a.trim().parse::<Ipv4Addr>().ok()?, b.trim().parse::<Ipv4Addr>().ok()?);
203 return (u32::from(lo) <= u32::from(hi)).then_some((lo, hi));
204 }
205 if let Some((a, m)) = part.split_once('/') {
206 let ip = u32::from(a.trim().parse::<Ipv4Addr>().ok()?);
207 let m = m.trim();
208 let mask = match m.parse::<u32>() {
210 Ok(len) if len <= 32 => if len == 0 { 0 } else { u32::MAX << (32 - len) },
211 _ => u32::from(m.parse::<Ipv4Addr>().ok()?),
212 };
213 return Some((Ipv4Addr::from(ip & mask), Ipv4Addr::from(ip | !mask)));
214 }
215 let ip = part.parse::<Ipv4Addr>().ok()?;
216 Some((ip, ip))
217 }
218
219 pub fn applies_to(&self, ips: &[Ipv4Addr]) -> bool {
221 match self {
222 LocalScope::Any => true,
223 LocalScope::Unparseable => false,
224 LocalScope::Addrs(ranges) => ips.iter().any(|ip| {
225 let n = u32::from(*ip);
226 ranges.iter().any(|&(lo, hi)| u32::from(lo) <= n && n <= u32::from(hi))
227 }),
228 }
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct FwRule {
235 #[cfg(test)]
236 pub name: String,
237 pub enabled: bool,
238 pub inbound: bool,
239 pub allow: bool,
240 pub profiles: u32,
242 pub program: Option<String>,
244 pub proto: Proto,
245 pub local_ports: PortSpec,
246 pub remote: Remote,
247 pub local_addresses: LocalScope,
249 pub interface_scoped: bool,
252 pub restricted: bool,
259}
260
261pub fn same_path(a: &str, b: &str) -> bool {
265 fn norm(p: &str) -> String {
266 p.trim()
267 .trim_start_matches(r"\\?\")
268 .replace('/', "\\")
269 .to_lowercase()
270 }
271 let (a, b) = (norm(a), norm(b));
272 !a.is_empty() && a == b
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub struct Need {
278 pub proto: Proto,
279 pub port: u16,
280}
281
282pub fn lan_needs(web_port: u16, ws_port: u16) -> Vec<Need> {
287 vec![
288 Need { proto: Proto::Tcp, port: web_port },
289 Need { proto: Proto::Tcp, port: ws_port },
290 Need { proto: Proto::Udp, port: 5353 },
291 Need { proto: Proto::Udp, port: super::lan_discovery::UDP_DISCOVERY_PORT },
292 Need { proto: Proto::Udp, port: super::lan_discovery::DESKTOP_DISCOVERY_PORT },
293 ]
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298enum Applies {
299 Yes,
301 Maybe,
305 No,
307}
308
309fn applies_to(rule: &FwRule, exe: &str, need: Need, category: Category, local_ips: &[Ipv4Addr]) -> Applies {
310 if !rule.enabled
312 || !rule.inbound
313 || rule.profiles & category.bit() == 0
314 || !rule.proto.covers(need.proto)
315 || !rule.remote.reaches_lan_peers()
316 || rule.program.as_deref().is_some_and(|p| !same_path(p, exe))
317 {
318 return Applies::No;
319 }
320 let mut uncertain = rule.interface_scoped || rule.restricted;
321 match &rule.local_ports {
322 PortSpec::Any => {}
323 PortSpec::Ranges(_) if rule.local_ports.contains(need.port) => {}
324 PortSpec::Ranges(_) => return Applies::No,
325 PortSpec::Unparseable => uncertain = true,
326 }
327 match &rule.local_addresses {
328 LocalScope::Any => {}
329 LocalScope::Addrs(_) if rule.local_addresses.applies_to(local_ips) => {}
330 LocalScope::Addrs(_) => return Applies::No,
331 LocalScope::Unparseable => uncertain = true,
332 }
333 if uncertain {
334 Applies::Maybe
335 } else {
336 Applies::Yes
337 }
338}
339
340fn rule_applies(rule: &FwRule, exe: &str, need: Need, category: Category, local_ips: &[Ipv4Addr]) -> bool {
346 applies_to(rule, exe, need, category, local_ips) == Applies::Yes
347}
348
349#[cfg(test)]
352pub fn matching_rules<'a>(
353 rules: &'a [FwRule],
354 exe: &str,
355 need: Need,
356 category: Category,
357 local_ips: &[Ipv4Addr],
358 allow: bool,
359) -> Vec<&'a FwRule> {
360 rules
361 .iter()
362 .filter(|r| r.allow == allow && rule_applies(r, exe, need, category, local_ips))
363 .collect()
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub struct ProfileSettings {
371 pub enabled: bool,
374 pub block_all_inbound: bool,
376 pub default_inbound_allow: bool,
379}
380
381impl Default for ProfileSettings {
382 fn default() -> Self {
384 ProfileSettings { enabled: true, block_all_inbound: false, default_inbound_allow: false }
385 }
386}
387
388#[derive(Debug, Clone, Copy, Default)]
389pub struct Profiles {
390 pub domain: ProfileSettings,
391 pub private: ProfileSettings,
392 pub public: ProfileSettings,
393}
394
395impl Profiles {
396 pub fn get(&self, c: Category) -> ProfileSettings {
397 match c {
398 Category::Domain => self.domain,
399 Category::Private => self.private,
400 Category::Public => self.public,
401 }
402 }
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum Coverage {
407 Covered,
409 Blocked,
411 Missing,
413 Uncertain,
418}
419
420#[cfg(test)]
423pub fn coverage(rules: &[FwRule], exe: &str, needs: &[Need], category: Category) -> Coverage {
424 coverage_with(rules, exe, needs, category, ProfileSettings::default(), &[])
425}
426
427pub fn coverage_with(
434 rules: &[FwRule],
435 exe: &str,
436 needs: &[Need],
437 category: Category,
438 settings: ProfileSettings,
439 local_ips: &[Ipv4Addr],
440) -> Coverage {
441 if !settings.enabled {
442 return Coverage::Covered;
443 }
444 if settings.block_all_inbound {
445 return Coverage::Blocked;
446 }
447 let applies = |allow: bool, need: Need| {
448 rules
449 .iter()
450 .any(|r| r.allow == allow && rule_applies(r, exe, need, category, local_ips))
451 };
452 if needs.iter().any(|&n| applies(false, n)) {
453 return Coverage::Blocked;
454 }
455 let maybe_blocked = needs.iter().any(|&n| {
457 rules
458 .iter()
459 .any(|r| !r.allow && applies_to(r, exe, n, category, local_ips) == Applies::Maybe)
460 });
461 if settings.default_inbound_allow || needs.iter().all(|&n| applies(true, n)) {
462 if maybe_blocked {
463 Coverage::Uncertain
464 } else {
465 Coverage::Covered
466 }
467 } else {
468 Coverage::Missing
469 }
470}
471
472#[derive(Debug, Clone)]
474pub struct Adapter {
475 #[cfg(test)]
477 pub id: String,
478 pub name: String,
479 pub ipv4: Vec<Ipv4Addr>,
480 pub category: Option<Category>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub enum AdapterState {
486 Covered,
487 NeedsSetup,
488 Blocked,
489 PublicNotTrusted,
492 Unknown,
494}
495
496#[cfg(test)]
497pub fn adapter_state(rules: &[FwRule], exe: &str, needs: &[Need], adapter: &Adapter) -> AdapterState {
498 adapter_state_with(rules, exe, needs, adapter, Profiles::default())
499}
500
501pub fn adapter_state_with(
502 rules: &[FwRule],
503 exe: &str,
504 needs: &[Need],
505 adapter: &Adapter,
506 profiles: Profiles,
507) -> AdapterState {
508 let Some(category) = adapter.category else {
509 return AdapterState::Unknown;
510 };
511 match coverage_with(rules, exe, needs, category, profiles.get(category), &adapter.ipv4) {
512 Coverage::Covered => AdapterState::Covered,
513 Coverage::Blocked => AdapterState::Blocked,
514 Coverage::Missing if category == Category::Public => AdapterState::PublicNotTrusted,
515 Coverage::Missing => AdapterState::NeedsSetup,
516 Coverage::Uncertain => AdapterState::Unknown,
518 }
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523pub enum FirewallStatus {
524 Ok,
526 NeedsSetup,
527 Blocked,
528 PublicNetwork,
529 Managed,
532 Unknown,
534}
535
536impl FirewallStatus {
537 pub fn wire(self) -> &'static str {
538 match self {
539 FirewallStatus::Ok => "ok",
540 FirewallStatus::NeedsSetup => "needs-setup",
541 FirewallStatus::Blocked => "blocked",
542 FirewallStatus::PublicNetwork => "public-network",
543 FirewallStatus::Managed => "managed",
544 FirewallStatus::Unknown => "unknown",
545 }
546 }
547}
548
549pub fn overall(states: &[AdapterState], local_rules_ignored: bool) -> FirewallStatus {
555 let any = |s: AdapterState| states.iter().any(|&x| x == s);
556 if any(AdapterState::Covered) {
557 FirewallStatus::Ok
558 } else if any(AdapterState::Blocked) {
559 FirewallStatus::Blocked
560 } else if local_rules_ignored && (any(AdapterState::NeedsSetup) || any(AdapterState::PublicNotTrusted)) {
561 FirewallStatus::Managed
562 } else if any(AdapterState::NeedsSetup) {
563 FirewallStatus::NeedsSetup
564 } else if any(AdapterState::PublicNotTrusted) {
565 FirewallStatus::PublicNetwork
566 } else {
567 FirewallStatus::Unknown
568 }
569}
570
571#[derive(Debug, Clone, Default)]
573pub struct Snapshot {
574 pub rules: Vec<FwRule>,
575 pub adapters: Vec<Adapter>,
576 pub local_rules_ignored: bool,
578 pub profiles: Profiles,
580 pub unreadable_rules: u32,
583}
584
585#[derive(Debug, Clone)]
586pub struct Report {
587 pub status: FirewallStatus,
588 pub per_adapter: Vec<(Adapter, AdapterState)>,
589}
590
591pub fn report(snapshot: &Snapshot, exe: &str, needs: &[Need]) -> Report {
592 let per_adapter: Vec<(Adapter, AdapterState)> = snapshot
593 .adapters
594 .iter()
595 .map(|a| (a.clone(), adapter_state_with(&snapshot.rules, exe, needs, a, snapshot.profiles)))
596 .collect();
597 let states: Vec<AdapterState> = per_adapter.iter().map(|(_, s)| *s).collect();
598 let mut status = overall(&states, snapshot.local_rules_ignored);
599 if status == FirewallStatus::Ok && snapshot.unreadable_rules > 0 {
604 status = FirewallStatus::Unknown;
605 }
606 Report { status, per_adapter }
607}
608
609pub fn should_publish(status: FirewallStatus, changed: bool) -> bool {
615 match status {
616 FirewallStatus::Ok | FirewallStatus::Unknown => changed,
617 FirewallStatus::NeedsSetup
618 | FirewallStatus::Blocked
619 | FirewallStatus::PublicNetwork
620 | FirewallStatus::Managed => true,
621 }
622}
623
624fn category_wire(c: Option<Category>) -> &'static str {
625 match c {
626 Some(Category::Public) => "public",
627 Some(Category::Private) => "private",
628 Some(Category::Domain) => "domain",
629 None => "unknown",
630 }
631}
632
633fn state_wire(s: AdapterState) -> &'static str {
634 match s {
635 AdapterState::Covered => "covered",
636 AdapterState::NeedsSetup => "needs-setup",
637 AdapterState::Blocked => "blocked",
638 AdapterState::PublicNotTrusted => "public-not-trusted",
639 AdapterState::Unknown => "unknown",
640 }
641}
642
643pub fn payload(
645 status: FirewallStatus,
646 per_adapter: &[(Adapter, AdapterState)],
647 local_rules_ignored: bool,
648) -> LanFirewall {
649 LanFirewall {
650 status: status.wire().to_string(),
651 local_rules_ignored,
652 adapters: per_adapter
653 .iter()
654 .map(|(a, s)| LanFirewallAdapter {
655 name: a.name.clone(),
656 category: category_wire(a.category).to_string(),
657 state: state_wire(*s).to_string(),
658 })
659 .collect(),
660 }
661}
662
663#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
665#[ts(export, export_to = "../../../frontend/types/rpc/")]
666pub struct LanFirewall {
667 #[ts(type = r#""ok" | "needs-setup" | "blocked" | "public-network" | "managed" | "unknown" | "off""#)]
668 pub status: String,
669 pub adapters: Vec<LanFirewallAdapter>,
671 #[serde(rename = "localRulesIgnored")]
673 #[ts(rename = "localRulesIgnored")]
674 pub local_rules_ignored: bool,
675}
676
677impl LanFirewall {
678 fn bare(status: &str) -> Self {
680 LanFirewall { status: status.to_string(), adapters: Vec::new(), local_rules_ignored: false }
681 }
682}
683
684#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
686#[ts(export, export_to = "../../../frontend/types/rpc/")]
687pub struct LanFirewallAdapter {
688 pub name: String,
689 #[ts(type = r#""public" | "private" | "domain" | "unknown""#)]
690 pub category: String,
691 #[ts(type = r#""covered" | "needs-setup" | "blocked" | "public-not-trusted" | "unknown""#)]
692 pub state: String,
693}
694
695const WATCH_TICK: std::time::Duration = std::time::Duration::from_secs(30);
699
700pub fn spawn_watcher(
708 lan: std::sync::Arc<super::lan_discovery::LanDiscoveryController>,
709 web_port: u16,
710 ws_port: u16,
711 event_bus: std::sync::Arc<super::eventbus::EventBus>,
712) {
713 if !cfg!(windows) {
714 return;
715 }
716 tokio::spawn(async move {
717 let mut last: Option<String> = None;
718 let mut tick = tokio::time::interval(WATCH_TICK);
719 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
720 loop {
721 tick.tick().await;
722 if !lan.is_wanted() {
723 if last.as_deref().is_some_and(|s| s != "off") {
725 event_bus.broadcast_event(&super::eventbus::EventBus::typed_ws_event::<crate::backend::mps_events::LanInstancesFirewall>(&LanFirewall::bare("off"), String::new()));
726 last = Some("off".to_string());
727 }
728 continue;
729 }
730 let exe = match std::env::current_exe() {
731 Ok(p) => p.to_string_lossy().to_string(),
732 Err(e) => {
733 tracing::warn!("firewall check: cannot find our own executable: {e}");
734 continue;
735 }
736 };
737 let snapshot = match tokio::task::spawn_blocking(read_snapshot).await {
738 Ok(Ok(s)) => s,
739 Ok(Err(e)) => {
740 if last.as_deref() != Some("unknown") {
745 tracing::warn!("could not read the OS firewall, so no verdict: {e}");
746 event_bus.broadcast_event(&super::eventbus::EventBus::typed_ws_event::<crate::backend::mps_events::LanInstancesFirewall>(&LanFirewall::bare("unknown"), String::new()));
747 last = Some("unknown".to_string());
748 }
749 continue;
750 }
751 Err(e) => {
752 tracing::warn!("firewall reader task failed: {e}");
753 continue;
754 }
755 };
756 let report = report(&snapshot, &exe, &lan_needs(web_port, ws_port));
757 let wire = report.status.wire().to_string();
758 let changed = last.as_deref() != Some(wire.as_str());
759 if changed && report.status != FirewallStatus::Ok {
760 tracing::warn!(
761 status = %wire,
762 adapters = ?report.per_adapter.iter().map(|(a, s)| (a.name.clone(), *s)).collect::<Vec<_>>(),
763 "OS firewall may keep other machines from reaching this one"
764 );
765 }
766 if should_publish(report.status, changed) {
767 let verdict = payload(report.status, &report.per_adapter, snapshot.local_rules_ignored);
768 event_bus.broadcast_event(&super::eventbus::EventBus::typed_ws_event::<crate::backend::mps_events::LanInstancesFirewall>(&verdict, String::new()));
769 }
770 last = Some(wire);
771 }
772 });
773}
774
775#[cfg(windows)]
779pub use super::lan_firewall_windows::read_snapshot;
780
781#[cfg(not(windows))]
782pub fn read_snapshot() -> Result<Snapshot, String> {
783 Err("firewall inspection is only implemented on Windows".to_string())
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 const ALL: u32 = 0x7FFF_FFFF;
791 const EXE: &str = r"C:\Users\user\Desktop\agentmux-0.59.1\runtime\agentmux-srv-0.59.1-windows.x64.exe";
792 const OLD_EXE: &str = r"C:\Users\user\Desktop\agentmux-0.58.2\runtime\agentmux-srv-0.58.2-windows.x64.exe";
793
794 fn needs() -> Vec<Need> {
795 lan_needs(29700, 29701)
796 }
797
798 fn rule(name: &str) -> FwRule {
799 FwRule {
800 name: name.to_string(),
801 enabled: true,
802 inbound: true,
803 allow: true,
804 profiles: PROFILE_PRIVATE,
805 program: None,
806 proto: Proto::Any,
807 local_ports: PortSpec::Any,
808 remote: Remote::Any,
809 local_addresses: LocalScope::Any,
810 interface_scoped: false,
811 restricted: false,
812 }
813 }
814
815 fn program_rule(exe: &str, proto: Proto, profiles: u32, allow: bool) -> FwRule {
816 FwRule {
817 program: Some(exe.to_string()),
818 proto,
819 profiles,
820 allow,
821 ..rule("agentmux-srv")
822 }
823 }
824
825 fn adapter(name: &str, cat: Option<Category>) -> Adapter {
826 Adapter {
827 id: format!("{{{name}}}"),
828 name: name.to_string(),
829 ipv4: vec!["192.168.1.26".parse().unwrap()],
830 category: cat,
831 }
832 }
833
834 #[test]
837 fn port_specs_parse_the_forms_windows_uses() {
838 assert_eq!(PortSpec::parse(""), PortSpec::Any);
839 assert_eq!(PortSpec::parse("*"), PortSpec::Any);
840 let p = PortSpec::parse("5353, 29700-29799");
841 assert!(p.contains(5353) && p.contains(29700) && p.contains(29799) && p.contains(29750));
842 assert!(!p.contains(29800) && !p.contains(5354) && !p.contains(29699));
843 }
844
845 #[test]
846 fn port_keywords_and_junk_never_match() {
847 for bad in ["RPC", "IPHTTPS", "Teredo", "80-", "-80", "70000", "90-80", "1,,2", "a-b"] {
848 let p = PortSpec::parse(bad);
849 assert_eq!(p, PortSpec::Unparseable, "{bad}");
850 assert!(!p.contains(80));
851 }
852 }
853
854 #[test]
855 fn remote_addresses_reduce_to_whether_lan_peers_are_reached() {
856 assert!(Remote::parse("").reaches_lan_peers());
857 assert!(Remote::parse("*").reaches_lan_peers());
858 assert!(Remote::parse("LocalSubnet").reaches_lan_peers());
859 assert!(Remote::parse("localsubnet").reaches_lan_peers());
860 assert!(!Remote::parse("192.168.1.5").reaches_lan_peers());
861 assert!(!Remote::parse("Internet").reaches_lan_peers());
862 assert!(!Remote::parse("LocalSubnet,10.0.0.1").reaches_lan_peers());
863 }
864
865 #[test]
866 fn protocols_from_the_windows_numbers() {
867 assert_eq!(Proto::from_win(6), Proto::Tcp);
868 assert_eq!(Proto::from_win(17), Proto::Udp);
869 assert_eq!(Proto::from_win(256), Proto::Any);
870 assert_eq!(Proto::from_win(1), Proto::Other);
871 assert!(!Proto::Other.covers(Proto::Tcp));
872 assert!(Proto::Any.covers(Proto::Udp));
873 }
874
875 #[test]
876 fn paths_compare_the_way_windows_does() {
877 assert!(same_path(r"C:\A\B.exe", r"c:\a\b.EXE"));
878 assert!(same_path("C:/A/B.exe", r"C:\A\B.exe"));
879 assert!(same_path(r"\\?\C:\A\B.exe", r"C:\A\B.exe"));
880 assert!(!same_path(r"C:\A\B.exe", r"C:\A\C.exe"));
881 assert!(!same_path("", ""));
882 assert!(!same_path(r"%SystemRoot%\x.exe", r"C:\Windows\x.exe"), "env vars are not expanded");
883 }
884
885 #[test]
891 fn narko_with_only_stale_per_path_rules_needs_setup() {
892 let mut rules: Vec<FwRule> = (0..606)
893 .map(|i| program_rule(&format!(r"C:\old\{i}\agentmux-srv.exe"), Proto::Any, ALL, true))
894 .collect();
895 rules.push(program_rule(OLD_EXE, Proto::Any, ALL, true));
896 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Missing);
897 let a = adapter("Ethernet", Some(Category::Private));
898 assert_eq!(adapter_state(&rules, EXE, &needs(), &a), AdapterState::NeedsSetup);
899 }
900
901 #[test]
903 fn area54_with_rules_for_the_current_binary_is_covered() {
904 let mut rules: Vec<FwRule> = (0..390)
905 .map(|i| program_rule(&format!(r"C:\old\{i}\agentmux-srv.exe"), Proto::Any, ALL, true))
906 .collect();
907 rules.push(program_rule(EXE, Proto::Tcp, PROFILE_PRIVATE, true));
908 rules.push(program_rule(EXE, Proto::Udp, PROFILE_PRIVATE, true));
909 rules.push(program_rule(EXE, Proto::Tcp, PROFILE_PUBLIC, false));
911 rules.push(program_rule(EXE, Proto::Udp, PROFILE_PUBLIC, false));
912 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Covered);
913 assert_eq!(coverage(&rules, EXE, &needs(), Category::Public), Coverage::Blocked);
914 }
915
916 #[test]
917 fn a_program_rule_for_another_path_does_not_cover_this_binary() {
918 let rules = vec![program_rule(OLD_EXE, Proto::Any, ALL, true)];
920 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Missing);
921 }
922
923 #[test]
924 fn port_rules_survive_updates_and_new_paths() {
925 let tcp = FwRule {
926 proto: Proto::Tcp,
927 local_ports: PortSpec::parse("29700-29799"),
928 remote: Remote::LocalSubnet,
929 ..rule("AgentMux LAN (TCP)")
930 };
931 let udp = FwRule {
932 proto: Proto::Udp,
933 local_ports: PortSpec::parse("5353,29700,47891"),
934 remote: Remote::LocalSubnet,
935 ..rule("AgentMux LAN (UDP)")
936 };
937 let rules = vec![tcp, udp];
938 for exe in [EXE, OLD_EXE, r"D:\portable\anything.exe"] {
939 assert_eq!(coverage(&rules, exe, &needs(), Category::Private), Coverage::Covered, "{exe}");
940 }
941 }
942
943 #[test]
944 fn srv_ports_outside_the_rule_range_are_not_covered() {
945 let tcp = FwRule { proto: Proto::Tcp, local_ports: PortSpec::parse("29700-29799"), ..rule("t") };
947 let udp = FwRule { proto: Proto::Udp, local_ports: PortSpec::parse("5353"), ..rule("u") };
948 let outside = lan_needs(51234, 51235);
949 assert_eq!(coverage(&[tcp, udp], EXE, &outside, Category::Private), Coverage::Missing);
950 }
951
952 #[test]
955 fn disabled_outbound_interface_scoped_and_remote_limited_rules_do_not_count() {
956 let base = program_rule(EXE, Proto::Any, PROFILE_PRIVATE, true);
957 for broken in [
958 FwRule { enabled: false, ..base.clone() },
959 FwRule { inbound: false, ..base.clone() },
960 FwRule { interface_scoped: true, ..base.clone() },
961 FwRule { restricted: true, program: None, ..base.clone() },
963 FwRule { remote: Remote::Other, ..base.clone() },
964 FwRule { proto: Proto::Other, ..base.clone() },
965 FwRule { local_ports: PortSpec::Unparseable, ..base.clone() },
966 ] {
967 assert_eq!(coverage(&[broken.clone()], EXE, &needs(), Category::Private), Coverage::Missing, "{broken:?}");
968 }
969 }
970
971 #[test]
972 fn a_rule_for_another_profile_does_not_cover_this_one() {
973 let rules = vec![program_rule(EXE, Proto::Any, PROFILE_PUBLIC | PROFILE_DOMAIN, true)];
974 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Missing);
975 assert_eq!(coverage(&rules, EXE, &needs(), Category::Domain), Coverage::Covered);
976 }
977
978 #[test]
979 fn a_block_beats_an_allow_as_in_windows() {
980 let rules = vec![
981 program_rule(EXE, Proto::Any, ALL, true),
982 program_rule(EXE, Proto::Tcp, PROFILE_PRIVATE, false),
983 ];
984 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Blocked);
985 assert_eq!(coverage(&rules, EXE, &needs(), Category::Domain), Coverage::Covered);
987 }
988
989 #[test]
990 fn a_block_for_a_different_program_does_not_block_us() {
991 let rules = vec![
992 program_rule(EXE, Proto::Any, ALL, true),
993 program_rule(OLD_EXE, Proto::Any, ALL, false),
994 ];
995 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Covered);
996 }
997
998 #[test]
999 fn tcp_alone_is_not_enough_mdns_needs_udp_too() {
1000 let rules = vec![program_rule(EXE, Proto::Tcp, PROFILE_PRIVATE, true)];
1001 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Missing);
1002 }
1003
1004 #[test]
1007 fn a_public_adapter_with_no_rule_is_not_trusted_rather_than_needs_setup() {
1008 let rules = vec![program_rule(EXE, Proto::Any, PROFILE_PRIVATE, true)];
1009 let public = adapter("Wi-Fi", Some(Category::Public));
1010 assert_eq!(adapter_state(&rules, EXE, &needs(), &public), AdapterState::PublicNotTrusted);
1011 let private = adapter("Ethernet", Some(Category::Private));
1012 assert_eq!(adapter_state(&rules, EXE, &needs(), &private), AdapterState::Covered);
1013 }
1014
1015 #[test]
1016 fn an_adapter_with_an_unknown_category_makes_no_claim() {
1017 let rules = vec![program_rule(EXE, Proto::Any, ALL, true)];
1018 assert_eq!(adapter_state(&rules, EXE, &needs(), &adapter("x", None)), AdapterState::Unknown);
1019 }
1020
1021 #[test]
1022 fn one_covered_adapter_is_enough_virtual_adapters_do_not_flag_a_working_lan() {
1023 use AdapterState::*;
1024 assert_eq!(overall(&[Covered, PublicNotTrusted, NeedsSetup, Blocked], false), FirewallStatus::Ok);
1025 }
1026
1027 #[test]
1028 fn overall_priority_is_block_then_managed_then_setup_then_public() {
1029 use AdapterState::*;
1030 assert_eq!(overall(&[NeedsSetup, Blocked], false), FirewallStatus::Blocked);
1031 assert_eq!(overall(&[NeedsSetup], true), FirewallStatus::Managed);
1032 assert_eq!(overall(&[PublicNotTrusted], true), FirewallStatus::Managed);
1033 assert_eq!(overall(&[PublicNotTrusted, NeedsSetup], false), FirewallStatus::NeedsSetup);
1034 assert_eq!(overall(&[PublicNotTrusted], false), FirewallStatus::PublicNetwork);
1035 assert_eq!(overall(&[Unknown, Unknown], false), FirewallStatus::Unknown);
1036 assert_eq!(overall(&[], false), FirewallStatus::Unknown);
1037 }
1038
1039 #[test]
1040 fn a_managed_policy_does_not_hide_a_working_lan() {
1041 assert_eq!(overall(&[AdapterState::Covered], true), FirewallStatus::Ok);
1042 }
1043
1044 #[test]
1045 fn the_report_ties_the_snapshot_together() {
1046 let snap = Snapshot {
1047 rules: vec![program_rule(EXE, Proto::Any, PROFILE_PRIVATE, true)],
1048 adapters: vec![
1049 adapter("Ethernet", Some(Category::Private)),
1050 adapter("vEthernet (Default Switch)", Some(Category::Public)),
1051 ],
1052 local_rules_ignored: false,
1053 profiles: Profiles::default(),
1054 unreadable_rules: 0,
1055 };
1056 let r = report(&snap, EXE, &needs());
1057 assert_eq!(r.status, FirewallStatus::Ok);
1058 assert_eq!(r.per_adapter[0].1, AdapterState::Covered);
1059 assert_eq!(r.per_adapter[1].1, AdapterState::PublicNotTrusted);
1060
1061 let none = Snapshot { rules: vec![], ..snap };
1062 assert_eq!(report(&none, EXE, &needs()).status, FirewallStatus::NeedsSetup);
1063 }
1064
1065 #[test]
1066 fn a_standing_problem_is_resent_every_tick_ok_and_unknown_only_on_change() {
1067 for problem in [
1068 FirewallStatus::NeedsSetup,
1069 FirewallStatus::Blocked,
1070 FirewallStatus::PublicNetwork,
1071 FirewallStatus::Managed,
1072 ] {
1073 assert!(should_publish(problem, false), "{problem:?}: a late window must learn of it");
1074 }
1075 for quiet in [FirewallStatus::Ok, FirewallStatus::Unknown] {
1076 assert!(!should_publish(quiet, false));
1077 assert!(should_publish(quiet, true), "{quiet:?}: a change must clear an earlier warning");
1078 }
1079 }
1080
1081 #[test]
1082 fn the_event_payload_is_the_frontend_contract() {
1083 let a = adapter("Ethernet", Some(Category::Private));
1084 let v = serde_json::to_value(payload(FirewallStatus::NeedsSetup, &[(a, AdapterState::NeedsSetup)], false)).unwrap();
1085 assert_eq!(v["status"], "needs-setup");
1086 assert_eq!(v["localRulesIgnored"], false);
1087 assert_eq!(v["adapters"][0]["name"], "Ethernet");
1088 assert_eq!(v["adapters"][0]["category"], "private");
1089 assert_eq!(v["adapters"][0]["state"], "needs-setup");
1090 }
1091
1092 #[test]
1093 fn a_rule_bound_to_a_service_or_store_app_is_not_an_any_program_rule() {
1094 let store = FwRule { restricted: true, program: None, ..rule("Microsoft Store") };
1096 let rules = vec![store];
1097 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Missing);
1098 }
1099
1100 #[test]
1102 fn a_profile_with_the_firewall_off_needs_no_rule_and_blocks_nothing() {
1103 let off = ProfileSettings { enabled: false, ..ProfileSettings::default() };
1104 assert_eq!(coverage_with(&[], EXE, &needs(), Category::Private, off, &[]), Coverage::Covered);
1105 let blocked = vec![program_rule(EXE, Proto::Any, ALL, false)];
1107 assert_eq!(coverage_with(&blocked, EXE, &needs(), Category::Private, off, &[]), Coverage::Covered);
1108 }
1109
1110 #[test]
1111 fn a_default_inbound_allow_needs_no_allow_rule_but_a_block_still_wins() {
1112 let open = ProfileSettings { default_inbound_allow: true, ..ProfileSettings::default() };
1113 assert_eq!(coverage_with(&[], EXE, &needs(), Category::Private, open, &[]), Coverage::Covered);
1114 let blocked = vec![program_rule(EXE, Proto::Tcp, PROFILE_PRIVATE, false)];
1115 assert_eq!(coverage_with(&blocked, EXE, &needs(), Category::Private, open, &[]), Coverage::Blocked);
1116 }
1117
1118 #[test]
1119 fn block_all_incoming_beats_every_allow_rule() {
1120 let all = ProfileSettings { block_all_inbound: true, ..ProfileSettings::default() };
1121 let rules = vec![program_rule(EXE, Proto::Any, ALL, true)];
1122 assert_eq!(coverage_with(&rules, EXE, &needs(), Category::Private, all, &[]), Coverage::Blocked);
1123 }
1124
1125 #[test]
1126 fn settings_are_per_profile_in_the_report() {
1127 let profiles = Profiles {
1128 private: ProfileSettings { enabled: false, ..ProfileSettings::default() },
1129 ..Profiles::default()
1130 };
1131 let snap = Snapshot {
1132 rules: vec![],
1133 adapters: vec![
1134 adapter("Ethernet", Some(Category::Private)),
1135 adapter("Wi-Fi", Some(Category::Public)),
1136 ],
1137 local_rules_ignored: false,
1138 profiles,
1139 unreadable_rules: 0,
1140 };
1141 let r = report(&snap, EXE, &needs());
1142 assert_eq!(r.per_adapter[0].1, AdapterState::Covered, "Private firewall is off");
1143 assert_eq!(r.per_adapter[1].1, AdapterState::PublicNotTrusted, "Public still defaults to block");
1144 assert_eq!(r.status, FirewallStatus::Ok);
1145 }
1146
1147 #[test]
1149 fn the_udp_broadcast_responder_port_is_a_need() {
1150 let port = crate::backend::lan_discovery::UDP_DISCOVERY_PORT;
1151 assert!(needs().iter().any(|n| n.proto == Proto::Udp && n.port == port));
1152 let tcp = FwRule { proto: Proto::Tcp, local_ports: PortSpec::parse("29700-29799"), ..rule("t") };
1154 let mdns = FwRule { proto: Proto::Udp, local_ports: PortSpec::parse("5353"), ..rule("u") };
1155 assert_eq!(coverage(&[tcp.clone(), mdns], EXE, &needs(), Category::Private), Coverage::Missing);
1156 let no_desktop = FwRule { proto: Proto::Udp, local_ports: PortSpec::parse("5353,47891"), ..rule("u") };
1159 assert_eq!(coverage(&[tcp.clone(), no_desktop], EXE, &needs(), Category::Private), Coverage::Missing);
1160 let both = FwRule { proto: Proto::Udp, local_ports: PortSpec::parse("5353,29700,47891"), ..rule("u") };
1161 assert_eq!(coverage(&[tcp, both], EXE, &needs(), Category::Private), Coverage::Covered);
1162 }
1163
1164 #[test]
1166 fn local_address_scopes_parse_the_forms_windows_uses() {
1167 assert_eq!(LocalScope::parse(""), LocalScope::Any);
1168 assert_eq!(LocalScope::parse("*"), LocalScope::Any);
1169 assert_eq!(LocalScope::parse("LocalSubnet"), LocalScope::Any);
1170 let ip = |s: &str| s.parse::<Ipv4Addr>().unwrap();
1171 let one = LocalScope::parse("192.168.1.26");
1172 assert!(one.applies_to(&[ip("192.168.1.26")]) && !one.applies_to(&[ip("192.168.1.27")]));
1173 let net = LocalScope::parse("192.168.1.0/24, 10.0.0.5");
1174 assert!(net.applies_to(&[ip("192.168.1.200")]) && net.applies_to(&[ip("10.0.0.5")]));
1175 assert!(!net.applies_to(&[ip("192.168.2.1")]));
1176 let masked = LocalScope::parse("172.17.16.0/255.255.255.0");
1177 assert!(masked.applies_to(&[ip("172.17.16.1")]) && !masked.applies_to(&[ip("172.17.17.1")]));
1178 let range = LocalScope::parse("192.168.1.10-192.168.1.20");
1179 assert!(range.applies_to(&[ip("192.168.1.15")]) && !range.applies_to(&[ip("192.168.1.21")]));
1180 for bad in ["DHCP", "Defaultgateway", "192.168.1", "1.2.3.4/33", "9-1", "10.0.0.1-bad"] {
1182 assert_eq!(LocalScope::parse(bad), LocalScope::Unparseable, "{bad}");
1183 }
1184 assert!(!LocalScope::parse("fe80::1").applies_to(&[ip("192.168.1.26")]));
1185 assert!(one.applies_to(&[ip("10.9.9.9"), ip("192.168.1.26")]));
1187 assert!(!one.applies_to(&[]));
1188 }
1189
1190 #[test]
1191 fn an_address_scoped_allow_covers_only_the_adapter_that_owns_the_address() {
1192 let scoped = |name: &str| FwRule {
1193 local_addresses: LocalScope::parse("192.168.1.26"),
1194 program: Some(EXE.to_string()),
1195 ..rule(name)
1196 };
1197 let rules = vec![scoped("a")];
1198 let owner = adapter("Ethernet", Some(Category::Private)); let other = Adapter { ipv4: vec!["10.0.0.5".parse().unwrap()], ..adapter("Wi-Fi", Some(Category::Private)) };
1200 assert_eq!(adapter_state(&rules, EXE, &needs(), &owner), AdapterState::Covered);
1201 assert_eq!(adapter_state(&rules, EXE, &needs(), &other), AdapterState::NeedsSetup);
1202 }
1203
1204 #[test]
1205 fn an_address_scoped_block_does_not_block_other_adapters() {
1206 let allow = program_rule(EXE, Proto::Any, ALL, true);
1207 let block = FwRule { local_addresses: LocalScope::parse("10.0.0.5"), ..program_rule(EXE, Proto::Any, ALL, false) };
1208 let rules = vec![allow, block];
1209 let ethernet = adapter("Ethernet", Some(Category::Private)); let wifi = Adapter { ipv4: vec!["10.0.0.5".parse().unwrap()], ..adapter("Wi-Fi", Some(Category::Private)) };
1211 assert_eq!(adapter_state(&rules, EXE, &needs(), ðernet), AdapterState::Covered);
1212 assert_eq!(adapter_state(&rules, EXE, &needs(), &wifi), AdapterState::Blocked);
1213 }
1214
1215 #[test]
1217 fn an_unreadable_rule_stops_the_snapshot_from_claiming_ok() {
1218 let snap = Snapshot {
1219 rules: vec![program_rule(EXE, Proto::Any, PROFILE_PRIVATE, true)],
1220 adapters: vec![adapter("Ethernet", Some(Category::Private))],
1221 local_rules_ignored: false,
1222 profiles: Profiles::default(),
1223 unreadable_rules: 0,
1224 };
1225 assert_eq!(report(&snap, EXE, &needs()).status, FirewallStatus::Ok);
1226
1227 let hidden = Snapshot { unreadable_rules: 1, ..snap.clone() };
1228 let r = report(&hidden, EXE, &needs());
1229 assert_eq!(r.status, FirewallStatus::Unknown, "coverage cannot be proven");
1230 assert_eq!(r.per_adapter[0].1, AdapterState::Covered, "the adapter-level reading is unchanged");
1231
1232 let none = Snapshot { rules: vec![], unreadable_rules: 3, ..snap };
1234 assert_eq!(report(&none, EXE, &needs()).status, FirewallStatus::NeedsSetup);
1235 }
1236
1237 #[test]
1239 fn a_block_that_might_apply_cannot_be_skipped_toward_covered() {
1240 let allow = program_rule(EXE, Proto::Any, ALL, true);
1241 let block = || program_rule(EXE, Proto::Any, ALL, false);
1242 let cases = [
1243 ("interface-scoped", FwRule { interface_scoped: true, ..block() }),
1246 ("restricted", FwRule { restricted: true, ..block() }),
1247 ("unparseable ports", FwRule { local_ports: PortSpec::Unparseable, ..block() }),
1248 ("unparseable local addresses", FwRule { local_addresses: LocalScope::Unparseable, ..block() }),
1249 ];
1250 for (label, b) in cases {
1251 let rules = vec![allow.clone(), b];
1252 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Uncertain, "{label}");
1253 let a = adapter("Ethernet", Some(Category::Private));
1254 assert_eq!(adapter_state(&rules, EXE, &needs(), &a), AdapterState::Unknown, "{label}");
1255 }
1256 let snap = Snapshot {
1257 rules: vec![allow.clone(), FwRule { interface_scoped: true, ..block() }],
1258 adapters: vec![adapter("Ethernet", Some(Category::Private))],
1259 ..Snapshot::default()
1260 };
1261 assert_eq!(report(&snap, EXE, &needs()).status, FirewallStatus::Unknown);
1262 }
1263
1264 #[test]
1265 fn a_block_that_cannot_apply_is_still_ignored() {
1266 let allow = program_rule(EXE, Proto::Any, ALL, true);
1267 for no in [
1268 FwRule { enabled: false, ..program_rule(EXE, Proto::Any, ALL, false) },
1269 FwRule { inbound: false, ..program_rule(EXE, Proto::Any, ALL, false) },
1270 program_rule(OLD_EXE, Proto::Any, ALL, false),
1271 program_rule(EXE, Proto::Any, PROFILE_PUBLIC, false),
1272 FwRule { proto: Proto::Other, ..program_rule(EXE, Proto::Any, ALL, false) },
1273 FwRule { remote: Remote::Other, ..program_rule(EXE, Proto::Any, ALL, false) },
1275 FwRule { local_ports: PortSpec::parse("80"), ..program_rule(EXE, Proto::Any, ALL, false) },
1277 FwRule { local_addresses: LocalScope::parse("10.9.9.9"), ..program_rule(EXE, Proto::Any, ALL, false) },
1278 ] {
1279 let rules = vec![allow.clone(), no.clone()];
1280 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Covered, "{no:?}");
1281 }
1282 }
1283
1284 #[test]
1285 fn an_uncertain_block_does_not_make_a_missing_allow_less_missing() {
1286 let uncertain = FwRule { interface_scoped: true, ..program_rule(EXE, Proto::Any, ALL, false) };
1287 assert_eq!(coverage(&[uncertain], EXE, &needs(), Category::Private), Coverage::Missing);
1288 }
1289
1290 #[test]
1291 fn a_certain_block_still_wins_over_an_uncertain_one() {
1292 let rules = vec![
1293 program_rule(EXE, Proto::Any, ALL, true),
1294 FwRule { interface_scoped: true, ..program_rule(EXE, Proto::Any, ALL, false) },
1295 program_rule(EXE, Proto::Tcp, PROFILE_PRIVATE, false),
1296 ];
1297 assert_eq!(coverage(&rules, EXE, &needs(), Category::Private), Coverage::Blocked);
1298 }
1299
1300 #[test]
1301 fn wire_names_are_the_frontend_contract() {
1302 assert_eq!(FirewallStatus::Ok.wire(), "ok");
1303 assert_eq!(FirewallStatus::NeedsSetup.wire(), "needs-setup");
1304 assert_eq!(FirewallStatus::Blocked.wire(), "blocked");
1305 assert_eq!(FirewallStatus::PublicNetwork.wire(), "public-network");
1306 assert_eq!(FirewallStatus::Managed.wire(), "managed");
1307 assert_eq!(FirewallStatus::Unknown.wire(), "unknown");
1308 }
1309}