1#![deny(clippy::unused_async)]
6
7pub mod dig;
8pub mod fetch;
9mod inspect;
10mod neighbor_cache;
11pub mod ping;
12pub mod route_table;
13pub mod telemetry;
14pub mod watchdog;
15
16#[cfg(test)]
17mod testutil;
18
19use crate::route_table::{Route, RouteTable};
20use crate::telemetry::processors::link_properties_state::LinkProperties;
21use crate::telemetry::{TelemetryEvent, TelemetrySender};
22use anyhow::anyhow;
23use fidl_fuchsia_net as fnet;
24use fidl_fuchsia_net_ext::{self as fnet_ext, IpExt};
25use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
26use fuchsia_async as fasync;
27use fuchsia_inspect::{Inspector, Node as InspectNode};
28use futures::channel::mpsc;
29use inspect::InspectInfo;
30use log::{debug, error, info};
31use named_timer::DeadlineId;
32use net_declare::{fidl_subnet, std_ip};
33use net_types::ScopeableAddress as _;
34use num_derive::FromPrimitive;
35use std::collections::HashSet;
36use std::collections::hash_map::{Entry, HashMap};
37
38use std::net::IpAddr;
39
40pub use neighbor_cache::{InterfaceNeighborCache, NeighborCache};
41
42const IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS: std::net::IpAddr = std_ip!("8.8.8.8");
43const IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS: std::net::IpAddr = std_ip!("2001:4860:4860::8888");
44const UNSPECIFIED_V4: fidl_fuchsia_net::Subnet = fidl_subnet!("0.0.0.0/0");
45const UNSPECIFIED_V6: fidl_fuchsia_net::Subnet = fidl_subnet!("::0/0");
46const GSTATIC: &'static str = "www.gstatic.com";
47const GENERATE_204: &'static str = "/generate_204";
48const DNS_PROBE_PERIOD: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(300);
52
53pub const FIDL_TIMEOUT_ID: DeadlineId<'static> =
56 DeadlineId::new("reachability", "fidl-request-timeout");
57
58#[derive(Debug, Default, Clone)]
60pub struct Stats {
61 pub events: u64,
63 pub state_updates: HashMap<Id, u64>,
65}
66
67#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy, FromPrimitive)]
70#[repr(u8)]
71pub enum LinkState {
72 #[default]
74 None = 1,
75 Removed = 5,
77 Down = 10,
79 Up = 15,
81 Local = 20,
83 Gateway = 25,
85 Internet = 30,
87}
88
89impl LinkState {
90 fn log_state_vals_inspect(node: &InspectNode, name: &str) {
91 let child = node.create_child(name);
92 for i in LinkState::None as u32..=LinkState::Internet as u32 {
93 match <LinkState as num_traits::FromPrimitive>::from_u32(i) {
94 Some(state) => child.record_string(i.to_string(), format!("{:?}", state)),
95 None => (),
96 }
97 }
98 node.record(child);
99 }
100}
101
102#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)]
104pub struct ApplicationState {
105 pub dns_resolved: bool,
106 pub http_fetch_succeeded: bool,
107}
108
109#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)]
111pub struct State {
112 pub link: LinkState,
113 pub application: ApplicationState,
114}
115
116impl From<LinkState> for State {
117 fn from(link: LinkState) -> Self {
118 State { link, ..Default::default() }
119 }
120}
121
122impl LinkState {
123 fn has_interface_up(&self) -> bool {
124 match self {
125 LinkState::None | LinkState::Removed | LinkState::Down => false,
126 LinkState::Up | LinkState::Local | LinkState::Gateway | LinkState::Internet => true,
127 }
128 }
129
130 fn has_internet(&self) -> bool {
131 match self {
132 LinkState::None
133 | LinkState::Removed
134 | LinkState::Down
135 | LinkState::Up
136 | LinkState::Local
137 | LinkState::Gateway => false,
138 LinkState::Internet => true,
139 }
140 }
141
142 fn has_gateway(&self) -> bool {
143 match self {
144 LinkState::None
145 | LinkState::Removed
146 | LinkState::Down
147 | LinkState::Up
148 | LinkState::Local => false,
149 LinkState::Gateway | LinkState::Internet => true,
150 }
151 }
152}
153
154impl State {
155 fn set_link_state(&mut self, link: LinkState) {
156 *self = State { link, ..Default::default() };
157 }
158
159 fn has_interface_up(&self) -> bool {
160 self.link.has_interface_up()
161 }
162
163 fn has_internet(&self) -> bool {
164 self.link.has_internet()
165 }
166
167 fn has_gateway(&self) -> bool {
168 self.link.has_gateway()
169 }
170
171 fn has_dns(&self) -> bool {
172 self.application.dns_resolved
173 }
174
175 fn has_http(&self) -> bool {
176 self.application.http_fetch_succeeded
177 }
178}
179
180impl std::fmt::Display for LinkState {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 f.write_str(match self {
183 LinkState::None => "None",
184 LinkState::Removed => "Removed",
185 LinkState::Down => "Down",
186 LinkState::Up => "Up",
187 LinkState::Local => "Local",
188 LinkState::Gateway => "Gateway",
189 LinkState::Internet => "Internet",
190 })
191 }
192}
193
194impl std::fmt::Display for State {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(
197 f,
198 "{} (dns: {}, http: {})",
199 self.link, self.application.dns_resolved, self.application.http_fetch_succeeded
200 )
201 }
202}
203
204impl std::str::FromStr for LinkState {
205 type Err = ();
206
207 fn from_str(s: &str) -> Result<Self, Self::Err> {
208 match s {
209 "None" => Ok(Self::None),
210 "Removed" => Ok(Self::Removed),
211 "Down" => Ok(Self::Down),
212 "Up" => Ok(Self::Up),
213 "Local" => Ok(Self::Local),
214 "Gateway" => Ok(Self::Gateway),
215 "Internet" => Ok(Self::Internet),
216 _ => Err(()),
217 }
218 }
219}
220
221#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
222pub enum Proto {
223 IPv4,
224 IPv6,
225}
226impl std::fmt::Display for Proto {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 f.write_str(match self {
229 Proto::IPv4 => "IPv4",
230 Proto::IPv6 => "IPv6",
231 })
232 }
233}
234
235trait StateEq {
237 fn compare_state(&self, other: &Self) -> bool;
239}
240
241#[derive(Debug, Clone, Copy)]
245#[cfg_attr(test, derive(PartialEq))]
246struct StateEvent {
247 state: State,
249 time: fasync::MonotonicInstant,
251}
252
253impl StateEvent {
254 fn update(&mut self, other: Self) -> Delta<Self> {
257 let previous = Some(*self);
258 if self.state != other.state {
259 *self = other;
260 }
261 Delta { previous, current: *self }
262 }
263}
264
265impl StateEq for StateEvent {
266 fn compare_state(&self, &Self { state, time: _ }: &Self) -> bool {
267 self.state == state
268 }
269}
270
271impl std::fmt::Display for StateEvent {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 std::fmt::Display::fmt(&self.state, f)
274 }
275}
276
277#[derive(Clone, Debug, PartialEq)]
278struct Delta<T> {
279 current: T,
280 previous: Option<T>,
281}
282
283impl<T: StateEq> Delta<T> {
284 fn change_observed(&self) -> bool {
285 match &self.previous {
286 Some(previous) => !previous.compare_state(&self.current),
287 None => true,
288 }
289 }
290}
291
292#[derive(Debug)]
295#[cfg_attr(test, derive(PartialEq))]
296struct StateDelta {
297 port: IpVersions<Delta<StateEvent>>,
298 system: IpVersions<Delta<SystemState>>,
299}
300
301#[derive(Clone, Default, Debug, PartialEq)]
302pub struct IpVersions<T> {
303 ipv4: T,
304 ipv6: T,
305}
306
307impl<T> IpVersions<T> {
308 fn with_version<F: FnMut(Proto, &T)>(&self, mut f: F) {
309 let () = f(Proto::IPv4, &self.ipv4);
310 let () = f(Proto::IPv6, &self.ipv6);
311 }
312}
313
314impl IpVersions<Option<SystemState>> {
315 fn state(&self) -> IpVersions<Option<State>> {
316 IpVersions {
317 ipv4: self.ipv4.map(|s| s.state.state),
318 ipv6: self.ipv6.map(|s| s.state.state),
319 }
320 }
321}
322
323impl IpVersions<Option<State>> {
324 fn has_interface_up(&self) -> bool {
325 self.satisfies(State::has_interface_up)
326 }
327
328 fn has_internet(&self) -> bool {
329 self.satisfies(State::has_internet)
330 }
331
332 fn has_dns(&self) -> bool {
333 self.satisfies(State::has_dns)
334 }
335
336 fn has_http(&self) -> bool {
337 self.satisfies(State::has_http)
338 }
339
340 fn has_gateway(&self) -> bool {
341 self.satisfies(State::has_gateway)
342 }
343
344 fn satisfies<F>(&self, f: F) -> bool
345 where
346 F: Fn(&State) -> bool,
347 {
348 return [self.ipv4, self.ipv6].iter().filter_map(|state| state.as_ref()).any(f);
349 }
350}
351
352type Id = u64;
353
354#[derive(Copy, Clone, Debug)]
357#[cfg_attr(test, derive(PartialEq))]
358struct SystemState {
359 id: Id,
360 state: StateEvent,
361}
362
363impl SystemState {
364 fn max(self, other: Self) -> Self {
365 if other.state.state > self.state.state { other } else { self }
366 }
367}
368
369impl StateEq for SystemState {
370 fn compare_state(&self, &Self { id, state: StateEvent { state, time: _ } }: &Self) -> bool {
371 self.id == id && self.state.state == state
372 }
373}
374
375impl std::fmt::Display for SystemState {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 write!(f, "{} via Interface [{}]", self.state, self.id)
378 }
379}
380
381#[derive(Debug, Default, Clone)]
385#[cfg_attr(test, derive(PartialEq))]
386pub struct StateInfo {
387 per_interface: HashMap<Id, IpVersions<StateEvent>>,
389 system: IpVersions<Option<Id>>,
391}
392
393impl StateInfo {
394 fn get(&self, id: Id) -> Option<&IpVersions<StateEvent>> {
396 self.per_interface.get(&id)
397 }
398
399 fn get_system_ipv4(&self) -> Option<SystemState> {
401 self.system.ipv4.map(|id| SystemState {
402 id,
403 state: self
404 .get(id)
405 .unwrap_or_else(|| {
406 panic!("inconsistent system IPv4 state: no interface with ID {:?}", id)
407 })
408 .ipv4,
409 })
410 }
411
412 fn get_system_ipv6(&self) -> Option<SystemState> {
414 self.system.ipv6.map(|id| SystemState {
415 id,
416 state: self
417 .get(id)
418 .unwrap_or_else(|| {
419 panic!("inconsistent system IPv6 state: no interface with ID {:?}", id)
420 })
421 .ipv6,
422 })
423 }
424
425 fn get_system(&self) -> IpVersions<Option<SystemState>> {
426 IpVersions { ipv4: self.get_system_ipv4(), ipv6: self.get_system_ipv6() }
427 }
428
429 pub fn system_has_internet(&self) -> bool {
430 self.get_system().state().has_internet()
431 }
432
433 pub fn system_has_gateway(&self) -> bool {
434 self.get_system().state().has_gateway()
435 }
436
437 pub fn system_has_dns(&self) -> bool {
438 self.get_system().state().has_dns()
439 }
440
441 pub fn system_has_http(&self) -> bool {
442 self.get_system().state().has_http()
443 }
444
445 fn report(&self) {
447 let time = fasync::MonotonicInstant::now();
448 debug!("system reachability state IPv4 {:?}", self.get_system_ipv4());
449 debug!("system reachability state IPv6 {:?}", self.get_system_ipv6());
450 for (id, IpVersions { ipv4, ipv6 }) in self.per_interface.iter() {
451 debug!(
452 "reachability state {:?} IPv4 {:?} with duration {:?}",
453 id,
454 ipv4,
455 time - ipv4.time
456 );
457 debug!(
458 "reachability state {:?} IPv6 {:?} with duration {:?}",
459 id,
460 ipv6,
461 time - ipv6.time
462 );
463 }
464 }
465
466 fn update(&mut self, id: Id, new_reachability: IpVersions<StateEvent>) -> StateDelta {
470 let previous_system_ipv4 = self.get_system_ipv4();
471 let previous_system_ipv6 = self.get_system_ipv6();
472 let port = match self.per_interface.entry(id) {
473 Entry::Occupied(mut occupied) => {
474 let IpVersions { ipv4, ipv6 } = occupied.get_mut();
475 let IpVersions { ipv4: new_ipv4, ipv6: new_ipv6 } = new_reachability;
476
477 IpVersions { ipv4: ipv4.update(new_ipv4), ipv6: ipv6.update(new_ipv6) }
478 }
479 Entry::Vacant(vacant) => {
480 let IpVersions { ipv4, ipv6 } = vacant.insert(new_reachability);
481 IpVersions {
482 ipv4: Delta { previous: None, current: *ipv4 },
483 ipv6: Delta { previous: None, current: *ipv6 },
484 }
485 }
486 };
487
488 let IpVersions { ipv4: system_ipv4, ipv6: system_ipv6 } = self.per_interface.iter().fold(
489 {
490 let IpVersions {
491 ipv4: Delta { previous: _, current: curr_ipv4 },
492 ipv6: Delta { previous: _, current: curr_ipv6 },
493 } = port;
494 let ipv4 = previous_system_ipv4
499 .map(|prev| {
500 if prev.id != id {
501 SystemState { id: prev.id, state: prev.state }
502 } else {
503 SystemState { id, state: curr_ipv4 }
504 }
505 })
506 .unwrap_or(SystemState { id, state: curr_ipv4 });
507 let ipv6 = previous_system_ipv6
508 .map(|prev| {
509 if prev.id != id {
510 SystemState { id: prev.id, state: prev.state }
511 } else {
512 SystemState { id, state: curr_ipv6 }
513 }
514 })
515 .unwrap_or(SystemState { id, state: curr_ipv6 });
516 IpVersions { ipv4, ipv6 }
517 },
518 |IpVersions { ipv4: system_ipv4, ipv6: system_ipv6 },
519 (&id, &IpVersions { ipv4, ipv6 })| {
520 IpVersions {
521 ipv4: system_ipv4.max(SystemState { id, state: ipv4 }),
522 ipv6: system_ipv6.max(SystemState { id, state: ipv6 }),
523 }
524 },
525 );
526
527 self.system = IpVersions { ipv4: Some(system_ipv4.id), ipv6: Some(system_ipv6.id) };
528
529 StateDelta {
530 port,
531 system: IpVersions {
532 ipv4: Delta { previous: previous_system_ipv4, current: system_ipv4 },
533 ipv6: Delta { previous: previous_system_ipv6, current: system_ipv6 },
534 },
535 }
536 }
537}
538
539#[derive(Copy, Clone, Debug)]
541pub struct InterfaceView<'a> {
542 pub properties: &'a fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
543 pub routes: &'a RouteTable,
544 pub neighbors: Option<&'a InterfaceNeighborCache>,
545}
546
547#[derive(Debug)]
550pub enum NetworkCheckerOutcome {
551 MustResume,
553 Complete,
556}
557
558pub trait NetworkChecker {
561 fn begin(&mut self, view: InterfaceView<'_>) -> Result<NetworkCheckerOutcome, anyhow::Error>;
565
566 fn resume(
568 &mut self,
569 cookie: NetworkCheckCookie,
570 result: NetworkCheckResult,
571 ) -> Result<NetworkCheckerOutcome, anyhow::Error>;
572}
573
574#[derive(Debug, Default)]
576enum NetworkCheckState {
577 #[default]
582 Begin,
583 PingGateway,
588 PingInternet,
591 ResolveDns,
595 FetchHttp,
598 Idle,
601}
602impl std::fmt::Display for NetworkCheckState {
603 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
604 match self {
605 NetworkCheckState::Begin => write!(f, "Begin"),
606 NetworkCheckState::PingGateway => write!(f, "Ping Gateway"),
607 NetworkCheckState::PingInternet => write!(f, "Ping Internet"),
608 NetworkCheckState::ResolveDns => write!(f, "Resolve DNS"),
609 NetworkCheckState::FetchHttp => write!(f, "Fetch URL"),
610 NetworkCheckState::Idle => write!(f, "Idle"),
611 }
612 }
613}
614
615#[derive(Debug, Clone, Default)]
616pub struct ResolvedIps {
617 v4: Vec<std::net::Ipv4Addr>,
618 v6: Vec<std::net::Ipv6Addr>,
619}
620
621struct PersistentNetworkCheckContext {
622 resolved_addrs: HashMap<String, ResolvedIps>,
624 resolved_time: zx::MonotonicInstant,
626 telemetry: TelemetryContext,
628}
629
630impl Default for PersistentNetworkCheckContext {
631 fn default() -> Self {
632 Self {
633 resolved_addrs: Default::default(),
634 resolved_time: zx::MonotonicInstant::INFINITE_PAST,
635 telemetry: Default::default(),
636 }
637 }
638}
639
640impl From<TelemetryContext> for PersistentNetworkCheckContext {
641 fn from(value: TelemetryContext) -> Self {
642 Self {
643 resolved_addrs: Default::default(),
644 resolved_time: zx::MonotonicInstant::INFINITE_PAST,
645 telemetry: value,
646 }
647 }
648}
649
650#[derive(Clone, Default)]
653struct TelemetryContext {
654 interface_identifiers: Vec<telemetry::processors::InterfaceIdentifier>,
657 has_v4_address: bool,
658 has_default_ipv4_route: bool,
659 has_v6_address: bool,
660 has_default_ipv6_route: bool,
661}
662
663impl TelemetryContext {
664 fn new(
665 port_class: fnet_interfaces_ext::PortClass,
666 addresses: &Vec<fnet_interfaces_ext::Address<fnet_interfaces_ext::DefaultInterest>>,
667 has_default_ipv4_route: bool,
668 has_default_ipv6_route: bool,
669 ) -> Self {
670 let interface_identifiers = telemetry::processors::identifiers_from_port_class(port_class);
671 let (has_v4_address, has_v6_address) = {
674 addresses.iter().fold((false, false), |(mut has_v4, mut has_v6), addr| {
675 match addr.addr.addr {
676 fnet::IpAddress::Ipv4(_) => {
677 has_v4 = true;
678 }
679 fnet::IpAddress::Ipv6(v6) => {
680 has_v6 = has_v6 || !v6.is_unicast_link_local();
681 }
682 };
683 (has_v4, has_v6)
684 })
685 };
686 Self {
687 interface_identifiers,
688 has_v4_address,
689 has_default_ipv4_route,
690 has_v6_address,
691 has_default_ipv6_route,
692 }
693 }
694}
695
696struct NetworkCheckContext {
698 checker_state: NetworkCheckState,
700 ping_addrs: Vec<std::net::SocketAddr>,
702 pings_expected: usize,
704 pings_completed: usize,
706 fetches_expected: usize,
708 fetches_completed: usize,
710 discovered_state: IpVersions<State>,
712 always_ping_internet: bool,
714 router_discoverable: IpVersions<bool>,
716 gateway_pingable: IpVersions<bool>,
718 persistent_context: PersistentNetworkCheckContext,
720 }
725
726impl NetworkCheckContext {
727 fn set_global_link_state(&mut self, link: LinkState) {
728 self.discovered_state.ipv4.set_link_state(link);
729 self.discovered_state.ipv6.set_link_state(link);
730 }
731
732 fn initiate_ping(
733 &mut self,
734 id: Id,
735 interface_name: &str,
736 network_check_sender: &mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
737 new_state: NetworkCheckState,
738 addrs: Vec<std::net::SocketAddr>,
739 ) {
740 self.checker_state = new_state;
741 self.ping_addrs = addrs;
742 self.pings_expected = self.ping_addrs.len();
743 self.pings_completed = 0;
744 self.ping_addrs
745 .iter()
746 .map(|addr| {
747 let action = NetworkCheckAction::Ping(PingParameters {
748 interface_name: interface_name.to_string(),
749 addr: addr.clone(),
750 });
751 (action, NetworkCheckCookie { id })
752 })
753 .for_each(|message| match network_check_sender.unbounded_send(message) {
754 Ok(()) => {}
755 Err(e) => {
756 debug!("unable to send network check internet msg: {:?}", e)
757 }
758 });
759 }
760}
761
762impl Default for NetworkCheckContext {
763 fn default() -> Self {
765 NetworkCheckContext {
766 checker_state: Default::default(),
767 ping_addrs: Vec::new(),
768 pings_expected: 0usize,
769 pings_completed: 0usize,
770 fetches_expected: 0usize,
771 fetches_completed: 0usize,
772 discovered_state: IpVersions {
773 ipv4: State { link: LinkState::None, ..Default::default() },
774 ipv6: State { link: LinkState::None, ..Default::default() },
775 },
776 always_ping_internet: true,
777 router_discoverable: Default::default(),
778 gateway_pingable: Default::default(),
779 persistent_context: Default::default(),
780 }
781 }
782}
783
784impl From<TelemetryContext> for NetworkCheckContext {
785 fn from(value: TelemetryContext) -> Self {
786 NetworkCheckContext {
787 persistent_context: PersistentNetworkCheckContext::from(value),
788 ..Default::default()
789 }
790 }
791}
792
793#[derive(Clone)]
795pub struct NetworkCheckCookie {
796 id: Id,
798}
799
800#[derive(Debug)]
801pub enum NetworkCheckResult {
802 Ping { parameters: PingParameters, result: Result<(), ping::PingError> },
803 ResolveDns { parameters: ResolveDnsParameters, ips: Option<ResolvedIps> },
804 Fetch { parameters: FetchParameters, result: Result<u16, fetch::FetchError> },
805}
806
807#[derive(Debug, Clone)]
808pub struct PingParameters {
809 pub interface_name: std::string::String,
811 pub addr: std::net::SocketAddr,
813}
814
815#[derive(Debug, Clone)]
816pub struct ResolveDnsParameters {
817 pub interface_name: std::string::String,
819 pub domain: String,
821}
822
823#[derive(Debug, Clone)]
824pub struct FetchParameters {
825 pub interface_name: std::string::String,
827 pub domain: std::string::String,
829 pub ip: std::net::IpAddr,
831 pub path: String,
833 pub expected_statuses: Vec<u16>,
835}
836
837impl NetworkCheckResult {
838 fn interface_name(&self) -> &str {
839 match self {
840 NetworkCheckResult::Ping {
841 parameters: PingParameters { interface_name, .. }, ..
842 } => interface_name,
843 NetworkCheckResult::ResolveDns {
844 parameters: ResolveDnsParameters { interface_name, .. },
845 ..
846 } => interface_name,
847 NetworkCheckResult::Fetch {
848 parameters: FetchParameters { interface_name, .. },
849 ..
850 } => interface_name,
851 }
852 }
853
854 fn ping_result(self) -> Option<(PingParameters, Result<(), ping::PingError>)> {
855 match self {
856 NetworkCheckResult::Ping { parameters, result } => Some((parameters, result)),
857 _ => None,
858 }
859 }
860
861 fn resolve_dns_result(self) -> Option<(ResolveDnsParameters, Option<ResolvedIps>)> {
862 match self {
863 NetworkCheckResult::ResolveDns { parameters, ips } => Some((parameters, ips)),
864 _ => None,
865 }
866 }
867
868 fn fetch_result(self) -> Option<(FetchParameters, Result<u16, fetch::FetchError>)> {
869 match self {
870 NetworkCheckResult::Fetch { parameters, result } => Some((parameters, result)),
871 _ => None,
872 }
873 }
874}
875
876#[derive(Debug, Clone)]
878pub enum NetworkCheckAction {
879 Ping(PingParameters),
880 ResolveDns(ResolveDnsParameters),
881 Fetch(FetchParameters),
882}
883
884pub trait TimeProvider {
885 fn now(&mut self) -> zx::MonotonicInstant;
886}
887
888#[derive(Debug, Default)]
889pub struct MonotonicInstant;
890impl TimeProvider for MonotonicInstant {
891 fn now(&mut self) -> zx::MonotonicInstant {
892 zx::MonotonicInstant::get()
893 }
894}
895
896pub struct Monitor<Time = MonotonicInstant> {
898 state: StateInfo,
899 stats: Stats,
900 inspector: Option<&'static Inspector>,
901 system_node: Option<InspectInfo>,
902 nodes: HashMap<Id, InspectInfo>,
903 telemetry_sender: Option<TelemetrySender>,
904 network_check_sender: mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
908 interface_context: HashMap<Id, NetworkCheckContext>,
909 time_provider: Time,
910}
911
912impl<Time: TimeProvider + Default> Monitor<Time> {
913 pub fn new(
915 network_check_sender: mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
916 ) -> anyhow::Result<Self> {
917 Ok(Monitor {
918 state: Default::default(),
919 stats: Default::default(),
920 inspector: None,
921 system_node: None,
922 nodes: HashMap::new(),
923 telemetry_sender: None,
924 network_check_sender,
925 interface_context: HashMap::new(),
926 time_provider: Default::default(),
927 })
928 }
929}
930
931impl<Time> Monitor<Time> {
932 pub fn new_with_time_provider(
934 network_check_sender: mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
935 time_provider: Time,
936 ) -> anyhow::Result<Self> {
937 Ok(Monitor {
938 state: Default::default(),
939 stats: Default::default(),
940 inspector: None,
941 system_node: None,
942 nodes: HashMap::new(),
943 telemetry_sender: None,
944 network_check_sender,
945 interface_context: HashMap::new(),
946 time_provider,
947 })
948 }
949}
950
951impl<Time: TimeProvider> Monitor<Time> {
952 pub fn state(&self) -> &StateInfo {
953 &self.state
954 }
955
956 pub fn report_state(&self) {
958 self.state.report();
959 debug!("reachability stats {:?}", self.stats);
960 }
961
962 pub fn set_inspector(&mut self, inspector: &'static Inspector) {
964 self.inspector = Some(inspector);
965
966 let system_node = InspectInfo::new(inspector.root(), "system", "");
967 self.system_node = Some(system_node);
968
969 LinkState::log_state_vals_inspect(inspector.root(), "state_vals");
970 }
971
972 pub fn set_telemetry_sender(&mut self, telemetry_sender: TelemetrySender) {
973 self.telemetry_sender = Some(telemetry_sender);
974 }
975
976 fn interface_node(&mut self, id: Id, name: &str) -> Option<&mut InspectInfo> {
977 self.inspector.map(move |inspector| {
978 self.nodes.entry(id).or_insert_with_key(|id| {
979 InspectInfo::new(inspector.root(), &format!("{:?}", id), name)
980 })
981 })
982 }
983
984 fn update_state_from_context(
985 &mut self,
986 id: Id,
987 name: &str,
988 ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
989 let ctx = self.interface_context.get_mut(&id).ok_or_else(|| {
990 anyhow!(
991 "attempting to update state with context but context for id {} does not exist",
992 id
993 )
994 })?;
995
996 ctx.checker_state = NetworkCheckState::Idle;
997
998 if let Some(IpVersions { ipv4, ipv6 }) = self.state.get(id) {
999 if ipv4.state.link == LinkState::Removed && ipv6.state.link == LinkState::Removed {
1000 debug!("interface {} was removed, skipping state update", id);
1001 return Ok(NetworkCheckerOutcome::Complete);
1002 }
1003 }
1004
1005 let info = IpVersions {
1006 ipv4: StateEvent {
1007 state: ctx.discovered_state.ipv4,
1008 time: fasync::MonotonicInstant::now(),
1009 },
1010 ipv6: StateEvent {
1011 state: ctx.discovered_state.ipv6,
1012 time: fasync::MonotonicInstant::now(),
1013 },
1014 };
1015
1016 let gateway_event_v4 = TelemetryEvent::GatewayProbe {
1017 gateway_discoverable: ctx.router_discoverable.ipv4,
1018 gateway_pingable: ctx.gateway_pingable.ipv4,
1019 internet_available: ctx.discovered_state.ipv4.has_internet(),
1020 };
1021 let gateway_event_v6 = TelemetryEvent::GatewayProbe {
1022 gateway_discoverable: ctx.router_discoverable.ipv6,
1023 gateway_pingable: ctx.gateway_pingable.ipv6,
1024 internet_available: ctx.discovered_state.ipv6.has_internet(),
1025 };
1026
1027 if let Some(telemetry_sender) = &mut self.telemetry_sender {
1028 telemetry_sender.send(gateway_event_v4);
1029 telemetry_sender.send(gateway_event_v6);
1030 telemetry_sender.send(TelemetryEvent::SystemStateUpdate {
1031 update: telemetry::SystemStateUpdate {
1032 system_state: self.state.get_system().state(),
1033 },
1034 });
1035 let telemetry_context = &ctx.persistent_context.telemetry;
1036 let interface_identifiers = &telemetry_context.interface_identifiers;
1037 telemetry_sender.send(TelemetryEvent::LinkPropertiesUpdate {
1038 interface_identifiers: interface_identifiers.clone(),
1039 link_properties: IpVersions {
1040 ipv4: LinkProperties {
1041 has_address: telemetry_context.has_v4_address,
1042 has_default_route: telemetry_context.has_default_ipv4_route,
1043 has_dns: ctx.discovered_state.ipv4.has_dns(),
1044 has_http_reachability: ctx.discovered_state.ipv4.has_http(),
1045 },
1046 ipv6: LinkProperties {
1047 has_address: telemetry_context.has_v6_address,
1048 has_default_route: telemetry_context.has_default_ipv6_route,
1049 has_dns: ctx.discovered_state.ipv6.has_dns(),
1050 has_http_reachability: ctx.discovered_state.ipv6.has_http(),
1051 },
1052 },
1053 });
1054 telemetry_sender.send(TelemetryEvent::LinkStateUpdate {
1055 interface_identifiers: interface_identifiers.clone(),
1056 link_state: IpVersions {
1057 ipv4: ctx.discovered_state.ipv4.link,
1058 ipv6: ctx.discovered_state.ipv6.link,
1059 },
1060 });
1061 }
1062
1063 let () = self.update_state(id, &name, info);
1064 Ok(NetworkCheckerOutcome::Complete)
1065 }
1066
1067 fn update_state(&mut self, id: Id, name: &str, reachability: IpVersions<StateEvent>) {
1069 let StateDelta { port, system } = self.state.update(id, reachability);
1070
1071 let () = port.with_version(|proto, delta| {
1072 if delta.change_observed() {
1073 let &Delta { previous, current } = delta;
1074 if let Some(previous) = previous {
1075 info!(
1076 "Interface [{}] updated --> {} current: {}, previous: {}",
1077 id, proto, current, previous
1078 );
1079 } else {
1080 info!("New interface [{}] --> {}: {}", id, proto, current);
1081 }
1082 let () = log_state(self.interface_node(id, name), proto, current.state);
1083 *self.stats.state_updates.entry(id).or_insert(0) += 1;
1084 }
1085 });
1086
1087 let () = system.with_version(|proto, delta| {
1088 if delta.change_observed() {
1089 let &Delta { previous, current } = delta;
1090 if let Some(previous) = previous {
1091 info!(
1092 "System reachability updated --> {} current: {}, previous: {}",
1093 proto, current, previous
1094 );
1095 } else {
1096 info!("Initial system reachability --> {}: {}", proto, current);
1097 }
1098 let () = log_state(self.system_node.as_mut(), proto, current.state.state);
1099 }
1100 });
1101 }
1102
1103 pub fn handle_interface_removed(
1105 &mut self,
1106 fnet_interfaces_ext::Properties { id, name, .. }: fnet_interfaces_ext::Properties<
1107 fnet_interfaces_ext::DefaultInterest,
1108 >,
1109 ) {
1110 let time = fasync::MonotonicInstant::now();
1111 if let Some(mut reachability) = self.state.get(id.into()).cloned() {
1112 reachability.ipv4 = StateEvent {
1113 state: State { link: LinkState::Removed, ..Default::default() },
1114 time,
1115 };
1116 reachability.ipv6 = StateEvent {
1117 state: State { link: LinkState::Removed, ..Default::default() },
1118 time,
1119 };
1120 let () = self.update_state(id.into(), &name, reachability);
1121 }
1122 }
1123
1124 fn handle_fetch_success(ctx: &mut NetworkCheckContext, ip: std::net::IpAddr) {
1125 match ctx.checker_state {
1126 NetworkCheckState::FetchHttp => match ip {
1127 IpAddr::V4(_) => {
1128 ctx.discovered_state.ipv4.application.http_fetch_succeeded = true;
1129 }
1130 IpAddr::V6(_) => {
1131 ctx.discovered_state.ipv6.application.http_fetch_succeeded = true;
1132 }
1133 },
1134 NetworkCheckState::PingGateway
1135 | NetworkCheckState::PingInternet
1136 | NetworkCheckState::Begin
1137 | NetworkCheckState::Idle
1138 | NetworkCheckState::ResolveDns => {
1139 panic!("continue check had an invalid state")
1140 }
1141 }
1142 }
1143
1144 fn handle_ping_success(ctx: &mut NetworkCheckContext, addr: &std::net::SocketAddr) {
1145 match ctx.checker_state {
1146 NetworkCheckState::PingGateway => match addr {
1147 std::net::SocketAddr::V4 { .. } => {
1148 ctx.gateway_pingable.ipv4 = true;
1149 ctx.discovered_state.ipv4.set_link_state(LinkState::Gateway);
1150 }
1151 std::net::SocketAddr::V6 { .. } => {
1152 ctx.gateway_pingable.ipv6 = true;
1153 ctx.discovered_state.ipv6.set_link_state(LinkState::Gateway);
1154 }
1155 },
1156 NetworkCheckState::PingInternet => match addr {
1157 std::net::SocketAddr::V4 { .. } => {
1158 ctx.discovered_state.ipv4.set_link_state(LinkState::Internet)
1159 }
1160 std::net::SocketAddr::V6 { .. } => {
1161 ctx.discovered_state.ipv6.set_link_state(LinkState::Internet)
1162 }
1163 },
1164 NetworkCheckState::FetchHttp
1165 | NetworkCheckState::Begin
1166 | NetworkCheckState::Idle
1167 | NetworkCheckState::ResolveDns => {
1168 panic!("continue check had an invalid state")
1169 }
1170 }
1171 }
1172}
1173
1174impl<Time: TimeProvider> NetworkChecker for Monitor<Time> {
1175 fn begin(
1176 &mut self,
1177 InterfaceView {
1178 properties:
1179 &fnet_interfaces_ext::Properties {
1180 id,
1181 ref name,
1182 port_class,
1183 online,
1184 ref addresses,
1185 has_default_ipv4_route,
1186 has_default_ipv6_route,
1187 port_identity_koid: _,
1188 },
1189 routes,
1190 neighbors,
1191 }: InterfaceView<'_>,
1192 ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
1193 let id = Id::from(id);
1194 let telemetry_context = TelemetryContext::new(
1202 port_class,
1203 &addresses,
1204 has_default_ipv4_route,
1205 has_default_ipv6_route,
1206 );
1207 let ctx = self
1208 .interface_context
1209 .entry(id)
1210 .or_insert_with(|| NetworkCheckContext::from(telemetry_context.clone()));
1211
1212 match ctx.checker_state {
1213 NetworkCheckState::Begin => {}
1214 NetworkCheckState::Idle => {
1215 let mut new_ctx = NetworkCheckContext::default();
1216 std::mem::swap(&mut new_ctx.persistent_context, &mut ctx.persistent_context);
1218 new_ctx.persistent_context.telemetry = telemetry_context;
1220 *ctx = new_ctx;
1221 }
1222 NetworkCheckState::PingGateway
1223 | NetworkCheckState::PingInternet
1224 | NetworkCheckState::FetchHttp
1225 | NetworkCheckState::ResolveDns => {
1226 ctx.persistent_context.telemetry = telemetry_context;
1229 return Err(anyhow!("skipped, non-idle state found on Interface [{id}]"));
1230 }
1231 }
1232
1233 if !online {
1234 ctx.set_global_link_state(LinkState::Down);
1235 return self.update_state_from_context(id, name);
1236 }
1237
1238 ctx.set_global_link_state(LinkState::Up);
1239
1240 let device_routes: Vec<_> = routes.device_routes(id).collect();
1243
1244 let neighbor_scan_health = scan_neighbor_health(neighbors, &device_routes);
1245
1246 let has_route = IpVersions {
1247 ipv4: device_routes
1248 .iter()
1249 .any(|route| matches!(route.destination.addr, fnet::IpAddress::Ipv4(_))),
1250 ipv6: device_routes
1251 .iter()
1252 .any(|route| matches!(route.destination.addr, fnet::IpAddress::Ipv6(_))),
1253 };
1254
1255 if neighbor_scan_health.ipv4 == NeighborHealthScanResult::NoneHealthy
1256 && neighbor_scan_health.ipv6 == NeighborHealthScanResult::NoneHealthy
1257 {
1258 if !has_route.ipv4 && !has_route.ipv6 {
1259 return self.update_state_from_context(id, name);
1261 }
1262
1263 ctx.always_ping_internet = false;
1266 }
1267 if has_route.ipv4 || neighbor_scan_health.ipv4.is_healthy() {
1268 ctx.discovered_state.ipv4.set_link_state(LinkState::Local);
1269 }
1270 if has_route.ipv6 || neighbor_scan_health.ipv6.is_healthy() {
1271 ctx.discovered_state.ipv6.set_link_state(LinkState::Local);
1272 }
1273
1274 let gateway_ping_addrs = device_routes
1275 .iter()
1276 .filter_map(move |Route { destination, outbound_interface, next_hop }| {
1277 if *destination != UNSPECIFIED_V4 && *destination != UNSPECIFIED_V6 {
1278 return None;
1279 }
1280 next_hop.and_then(|next_hop| {
1281 let fnet_ext::IpAddress(next_hop) = next_hop.into();
1282 match next_hop.into() {
1283 std::net::IpAddr::V4(v4) => {
1284 Some(std::net::SocketAddr::V4(std::net::SocketAddrV4::new(v4, 0)))
1285 }
1286 std::net::IpAddr::V6(v6) => match (*outbound_interface).try_into() {
1287 Err(std::num::TryFromIntError { .. }) => {
1288 error!("device id {} doesn't fit in u32", outbound_interface);
1289 None
1290 }
1291 Ok(device_id) => {
1292 if device_id == 0
1293 && net_types::ip::Ipv6Addr::from_bytes(v6.octets()).scope()
1294 != net_types::ip::Ipv6Scope::Global
1295 {
1296 None
1297 } else {
1298 Some(std::net::SocketAddr::V6(std::net::SocketAddrV6::new(
1299 v6, 0, 0, device_id,
1300 )))
1301 }
1302 }
1303 },
1304 }
1305 })
1306 })
1307 .map(|next_hop| next_hop)
1308 .collect::<Vec<_>>();
1309
1310 ctx.router_discoverable = IpVersions {
1312 ipv4: neighbor_scan_health.ipv4 == NeighborHealthScanResult::HealthyRouter,
1313 ipv6: neighbor_scan_health.ipv6 == NeighborHealthScanResult::HealthyRouter,
1314 };
1315 if gateway_ping_addrs.is_empty() {
1316 if neighbor_scan_health.ipv4 == NeighborHealthScanResult::HealthyRouter
1328 || neighbor_scan_health.ipv6 == NeighborHealthScanResult::HealthyRouter
1329 {
1330 ctx.initiate_ping(
1335 id,
1336 name,
1337 &self.network_check_sender,
1338 NetworkCheckState::PingInternet,
1339 [
1340 IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1341 IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1342 ]
1343 .into_iter()
1344 .map(|ip| std::net::SocketAddr::new(ip, 0))
1345 .collect(),
1346 );
1347 } else {
1348 return self.update_state_from_context(id, name);
1352 }
1353 } else {
1354 if neighbor_scan_health.ipv4.is_healthy_router() {
1356 ctx.discovered_state.ipv4.set_link_state(LinkState::Gateway);
1357 }
1358 if neighbor_scan_health.ipv6.is_healthy_router() {
1359 ctx.discovered_state.ipv6.set_link_state(LinkState::Gateway);
1360 }
1361 ctx.initiate_ping(
1362 id,
1363 name,
1364 &self.network_check_sender,
1365 NetworkCheckState::PingGateway,
1366 gateway_ping_addrs,
1367 );
1368 }
1369 Ok(NetworkCheckerOutcome::MustResume)
1370 }
1371
1372 fn resume(
1373 &mut self,
1374 cookie: NetworkCheckCookie,
1375 result: NetworkCheckResult,
1376 ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
1377 let ctx = self.interface_context.get_mut(&cookie.id).ok_or_else(|| {
1378 anyhow!("resume: interface id {} should already exist in map", cookie.id)
1379 })?;
1380 let interface_name = result.interface_name().to_string();
1381 match ctx.checker_state {
1382 NetworkCheckState::Begin | NetworkCheckState::Idle => {
1383 return Err(anyhow!(
1384 "skipped, idle state found in resume for interface {}",
1385 cookie.id
1386 ));
1387 }
1388 NetworkCheckState::PingGateway | NetworkCheckState::PingInternet => {
1389 let (parameters, result) = result.ping_result().ok_or_else(|| {
1390 anyhow!("resume: mismatched state and result {interface_name} ({})", cookie.id)
1391 })?;
1392 ctx.pings_completed = ctx.pings_completed + 1;
1393
1394 let ping_is_ok = result.is_ok();
1396
1397 if let Some(telemetry_sender) = &mut self.telemetry_sender {
1398 let interface_identifiers =
1399 ctx.persistent_context.telemetry.interface_identifiers.clone();
1400 if let NetworkCheckState::PingInternet = ctx.checker_state {
1401 telemetry_sender.send(TelemetryEvent::InternetPingResult {
1402 interface_identifiers,
1403 ping_parameters: parameters.clone(),
1404 internet_ping_result: result,
1405 });
1406 } else {
1407 telemetry_sender.send(TelemetryEvent::GatewayPingResult {
1408 interface_identifiers,
1409 ping_parameters: parameters.clone(),
1410 gateway_ping_result: result,
1411 });
1412 }
1413 }
1414
1415 let PingParameters { interface_name, addr, .. } = parameters;
1416 if ping_is_ok {
1417 let () = Self::handle_ping_success(ctx, &addr);
1418 }
1419
1420 if ctx.pings_completed == ctx.pings_expected {
1421 if let NetworkCheckState::PingGateway = ctx.checker_state {
1422 ctx.initiate_ping(
1423 cookie.id,
1424 &interface_name,
1425 &self.network_check_sender,
1426 NetworkCheckState::PingInternet,
1427 [
1428 IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1429 IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1430 ]
1431 .into_iter()
1432 .map(|ip| std::net::SocketAddr::new(ip, 0))
1433 .collect(),
1434 );
1435 } else {
1436 let parameters = ResolveDnsParameters {
1437 interface_name: interface_name.to_string(),
1438 domain: GSTATIC.into(),
1439 };
1440 ctx.checker_state = NetworkCheckState::ResolveDns;
1441
1442 if self.time_provider.now() - ctx.persistent_context.resolved_time
1443 < DNS_PROBE_PERIOD
1444 {
1445 debug!(
1446 "Skipping ResolveDns since it has not yet been {} seconds",
1447 DNS_PROBE_PERIOD.clone().into_seconds()
1448 );
1449 if let Some(ips) = ctx.persistent_context.resolved_addrs.get(GSTATIC) {
1450 if !ips.v4.is_empty() {
1451 ctx.discovered_state.ipv4.application.dns_resolved = true;
1452 }
1453 if !ips.v6.is_empty() {
1454 ctx.discovered_state.ipv6.application.dns_resolved = true;
1455 }
1456 }
1457 return self.resume(
1458 cookie,
1459 NetworkCheckResult::ResolveDns { parameters, ips: None },
1460 );
1461 }
1462
1463 let action = NetworkCheckAction::ResolveDns(parameters);
1464 match self
1465 .network_check_sender
1466 .unbounded_send((action, NetworkCheckCookie { id: cookie.id }))
1467 {
1468 Ok(()) => {}
1469 Err(e) => {
1470 debug!("unable to send network check internet msg: {e:?}")
1471 }
1472 }
1473 }
1474 }
1475 }
1476 NetworkCheckState::ResolveDns => {
1477 let (ResolveDnsParameters { interface_name, domain }, ips) =
1478 result.resolve_dns_result().ok_or_else(|| {
1479 anyhow!(
1480 "resume: mismatched state and result {interface_name} ({})",
1481 cookie.id
1482 )
1483 })?;
1484
1485 if let Some(ips) = ips {
1486 if !ips.v4.is_empty() {
1487 ctx.discovered_state.ipv4.application.dns_resolved = true;
1488 }
1489 if !ips.v6.is_empty() {
1490 ctx.discovered_state.ipv6.application.dns_resolved = true;
1491 }
1492 ctx.persistent_context.resolved_time = self.time_provider.now();
1493 let _: Option<ResolvedIps> =
1494 ctx.persistent_context.resolved_addrs.insert(domain.clone(), ips);
1495 }
1496
1497 ctx.checker_state = NetworkCheckState::FetchHttp;
1498 ctx.fetches_expected = 0;
1499
1500 let mut add_fetch = |ip: IpAddr| {
1501 ctx.fetches_expected += 1;
1502 let action = NetworkCheckAction::Fetch(FetchParameters {
1503 interface_name: interface_name.clone(),
1504 domain: domain.clone(),
1505 ip,
1506 path: GENERATE_204.into(),
1507 expected_statuses: vec![204],
1508 });
1509 match self
1510 .network_check_sender
1511 .unbounded_send((action, NetworkCheckCookie { id: cookie.id }))
1512 {
1513 Ok(()) => {}
1514 Err(e) => debug!("unable to send network check internet message: {e:?}"),
1515 }
1516 };
1517
1518 if let Some(v4) =
1519 ctx.persistent_context.resolved_addrs.get(&domain).and_then(|ips| ips.v4.get(0))
1520 {
1521 add_fetch(IpAddr::V4(*v4));
1522 }
1523 if let Some(v6) =
1524 ctx.persistent_context.resolved_addrs.get(&domain).and_then(|ips| ips.v6.get(0))
1525 {
1526 add_fetch(IpAddr::V6(*v6));
1527 }
1528
1529 if ctx.fetches_expected == 0 {
1530 return self.update_state_from_context(cookie.id, &interface_name);
1531 }
1532 }
1533 NetworkCheckState::FetchHttp => {
1534 let (parameters, result) = result.fetch_result().ok_or_else(|| {
1535 anyhow!("resume: mismatched state and result {interface_name} ({})", cookie.id)
1536 })?;
1537 ctx.fetches_completed += 1;
1538
1539 let fetch_ok_status = result.as_ref().copied().ok();
1541
1542 if let Some(telemetry_sender) = &mut self.telemetry_sender {
1543 telemetry_sender.send(TelemetryEvent::FetchResult {
1544 interface_identifiers: ctx
1545 .persistent_context
1546 .telemetry
1547 .interface_identifiers
1548 .clone(),
1549 fetch_parameters: parameters.clone(),
1550 fetch_result: result,
1551 });
1552 }
1553
1554 let FetchParameters { interface_name, ip, expected_statuses, .. } = parameters;
1555 if let Some(status) = fetch_ok_status {
1556 if expected_statuses.contains(&status) {
1557 let () = Self::handle_fetch_success(ctx, ip);
1558 }
1559 }
1560
1561 if ctx.fetches_completed == ctx.fetches_expected {
1562 return self.update_state_from_context(cookie.id, &interface_name);
1563 }
1564 }
1565 }
1566 Ok(NetworkCheckerOutcome::MustResume)
1567 }
1568}
1569
1570fn log_state(info: Option<&mut InspectInfo>, proto: Proto, state: State) {
1571 info.into_iter().for_each(|info| info.log_link_state(proto, state.link))
1572}
1573
1574#[derive(Default, PartialEq)]
1575enum NeighborHealthScanResult {
1576 #[default]
1578 NoneHealthy,
1579 HealthyNeighbor,
1581 HealthyRouter,
1585}
1586
1587impl NeighborHealthScanResult {
1588 fn update_scan_result(&mut self, is_router: bool) {
1591 *self = match (&self, is_router) {
1592 (_, true) | (Self::HealthyRouter, _) => Self::HealthyRouter,
1594 _ => Self::HealthyNeighbor,
1595 }
1596 }
1597
1598 fn is_healthy(&self) -> bool {
1599 match self {
1600 Self::NoneHealthy => false,
1601 Self::HealthyNeighbor | Self::HealthyRouter => true,
1602 }
1603 }
1604
1605 fn is_healthy_router(&self) -> bool {
1606 match self {
1607 Self::NoneHealthy | Self::HealthyNeighbor => false,
1608 Self::HealthyRouter => true,
1609 }
1610 }
1611}
1612
1613fn scan_neighbor_health(
1616 neighbors: Option<&InterfaceNeighborCache>,
1617 device_routes: &Vec<route_table::Route>,
1618) -> IpVersions<NeighborHealthScanResult> {
1619 match neighbors {
1620 None => Default::default(),
1621 Some(neighbors) => {
1622 let router_next_hops: HashSet<_> =
1623 device_routes.iter().filter_map(|Route { next_hop, .. }| *next_hop).collect();
1624 neighbors.iter_health().fold(
1625 Default::default(),
1626 |mut neighbor_health_scan, (neighbor, health)| {
1627 match health {
1628 neighbor_cache::NeighborHealth::Unhealthy { .. }
1631 | neighbor_cache::NeighborHealth::Unknown => neighbor_health_scan,
1632 neighbor_cache::NeighborHealth::Healthy { .. } => {
1636 let scan = match neighbor {
1637 fnet::IpAddress::Ipv4(..) => &mut neighbor_health_scan.ipv4,
1638 fnet::IpAddress::Ipv6(..) => &mut neighbor_health_scan.ipv6,
1639 };
1640
1641 scan.update_scan_result(router_next_hops.contains(neighbor));
1642 neighbor_health_scan
1643 }
1644 }
1645 },
1646 )
1647 }
1648 }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653 use crate::fetch::FetchAddr;
1654
1655 use super::*;
1656 use crate::dig::Dig;
1657 use crate::fetch::Fetch;
1658 use crate::neighbor_cache::{NeighborHealth, NeighborState};
1659 use crate::ping::Ping;
1660 use async_trait::async_trait;
1661 use diagnostics_assertions::assert_data_tree;
1662 use fidl_fuchsia_net as fnet;
1663 use fidl_fuchsia_net_interfaces as fnet_interfaces;
1664 use fuchsia_async as fasync;
1665 use futures::StreamExt as _;
1666 use net_declare::{fidl_ip, fidl_subnet, std_ip, std_socket_addr};
1667 use net_types::ip;
1668 use std::pin::pin;
1669 use std::task::Poll;
1670 use test_case::test_case;
1671
1672 const ETHERNET_INTERFACE_NAME: &str = "eth1";
1673 const ID1: u64 = 1;
1674 const ID2: u64 = 2;
1675 const IPV4_ADDR: fnet::IpAddress = fidl_ip!("192.168.0.1");
1677 const IPV6_ADDR: fnet::IpAddress = fidl_ip!("2001:db8::");
1679
1680 trait Construct<T> {
1685 fn construct(_: T) -> Self;
1686 }
1687
1688 impl<S: Into<State>> Construct<S> for StateEvent {
1689 fn construct(link: S) -> Self {
1690 Self { state: link.into(), time: fasync::MonotonicInstant::INFINITE }
1691 }
1692 }
1693
1694 impl Construct<(LinkState, bool, bool)> for StateEvent {
1695 fn construct((link, dns_resolved, http_fetch_succeeded): (LinkState, bool, bool)) -> Self {
1696 Self {
1697 state: State {
1698 link,
1699 application: ApplicationState { dns_resolved, http_fetch_succeeded },
1700 },
1701 time: fasync::MonotonicInstant::INFINITE,
1702 }
1703 }
1704 }
1705
1706 impl Construct<StateEvent> for IpVersions<StateEvent> {
1707 fn construct(state: StateEvent) -> Self {
1708 Self { ipv4: state, ipv6: state }
1709 }
1710 }
1711
1712 struct FakeTime {
1713 increment: zx::MonotonicDuration,
1714 time: zx::MonotonicInstant,
1715 }
1716
1717 impl TimeProvider for FakeTime {
1718 fn now(&mut self) -> zx::MonotonicInstant {
1719 let result = self.time;
1720 self.time += self.increment;
1721 result
1722 }
1723 }
1724
1725 #[fuchsia::test]
1726 async fn test_log_state_vals_inspect() {
1727 let inspector = Inspector::default();
1728 LinkState::log_state_vals_inspect(inspector.root(), "state_vals");
1729 assert_data_tree!(inspector, root: {
1730 state_vals: {
1731 "1": "None",
1732 "5": "Removed",
1733 "10": "Down",
1734 "15": "Up",
1735 "20": "Local",
1736 "25": "Gateway",
1737 "30": "Internet",
1738 }
1739 })
1740 }
1741
1742 #[fuchsia::test]
1743 fn test_display() {
1744 assert_eq!(Proto::IPv4.to_string(), "IPv4");
1745 assert_eq!(Proto::IPv6.to_string(), "IPv6");
1746
1747 let system_state = SystemState {
1748 id: 2,
1749 state: StateEvent {
1750 state: State {
1751 link: LinkState::Local,
1752 application: ApplicationState {
1753 dns_resolved: false,
1754 http_fetch_succeeded: false,
1755 },
1756 },
1757 time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1758 },
1759 };
1760 assert_eq!(system_state.to_string(), "Local (dns: false, http: false) via Interface [2]");
1761
1762 let system_state_zero = SystemState {
1763 id: 0,
1764 state: StateEvent {
1765 state: State {
1766 link: LinkState::Down,
1767 application: ApplicationState {
1768 dns_resolved: false,
1769 http_fetch_succeeded: false,
1770 },
1771 },
1772 time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1773 },
1774 };
1775 assert_eq!(
1776 system_state_zero.to_string(),
1777 "Down (dns: false, http: false) via Interface [0]"
1778 );
1779
1780 let system_state_internet = SystemState {
1781 id: 42,
1782 state: StateEvent {
1783 state: State {
1784 link: LinkState::Internet,
1785 application: ApplicationState {
1786 dns_resolved: true,
1787 http_fetch_succeeded: true,
1788 },
1789 },
1790 time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1791 },
1792 };
1793 assert_eq!(
1794 system_state_internet.to_string(),
1795 "Internet (dns: true, http: true) via Interface [42]"
1796 );
1797
1798 let system_state_max_id = SystemState {
1799 id: u64::MAX,
1800 state: StateEvent {
1801 state: State {
1802 link: LinkState::Gateway,
1803 application: ApplicationState {
1804 dns_resolved: true,
1805 http_fetch_succeeded: false,
1806 },
1807 },
1808 time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1809 },
1810 };
1811 assert_eq!(
1812 system_state_max_id.to_string(),
1813 "Gateway (dns: true, http: false) via Interface [18446744073709551615]"
1814 );
1815
1816 for formatted in [
1818 system_state.to_string(),
1819 system_state_zero.to_string(),
1820 system_state_internet.to_string(),
1821 system_state_max_id.to_string(),
1822 ] {
1823 assert!(!formatted.contains("PhantomData"));
1824 assert!(!formatted.contains("Instant"));
1825 assert!(!formatted.contains("MonotonicTimeline"));
1826 }
1827 }
1828
1829 #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("1.2.3.0:8080")];
1830 "gateway ping on ipv4")]
1831 #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("[123::]:0")];
1832 "gateway ping on ipv6")]
1833 #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("1.2.3.0:8080"),
1834 std_socket_addr!("[123::]:0")]; "gateway ping on ipv4/ipv6")]
1835 #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("8.8.8.8:0")];
1836 "internet ping on ipv4")]
1837 #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("[2001:4860:4860::8888]:0")];
1838 "internet ping on ipv6")]
1839 #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("8.8.8.8:0"),
1840 std_socket_addr!("[2001:4860:4860::8888]:0")]; "internet ping on ipv4/ipv6")]
1841 fn test_handle_ping_success(checker_state: NetworkCheckState, addrs: &[std::net::SocketAddr]) {
1842 let mut expected_state_v4: State = Default::default();
1843 let mut expected_state_v6: State = Default::default();
1844
1845 let mut ctx = NetworkCheckContext { checker_state, ..Default::default() };
1846 assert_eq!(ctx.discovered_state.ipv4, expected_state_v4);
1848 assert_eq!(ctx.discovered_state.ipv6, expected_state_v6);
1849
1850 let expected_state = match ctx.checker_state {
1851 NetworkCheckState::PingGateway => LinkState::Gateway.into(),
1852 NetworkCheckState::PingInternet => LinkState::Internet.into(),
1853 NetworkCheckState::ResolveDns => LinkState::Internet.into(),
1854 NetworkCheckState::FetchHttp => State {
1855 link: LinkState::Internet,
1856 application: ApplicationState { dns_resolved: true, http_fetch_succeeded: true },
1857 },
1858 NetworkCheckState::Begin | NetworkCheckState::Idle => Default::default(),
1859 };
1860
1861 addrs.iter().for_each(|addr| {
1862 let () = Monitor::<FakeTime>::handle_ping_success(&mut ctx, addr);
1864 match addr {
1866 std::net::SocketAddr::V4 { .. } => {
1867 expected_state_v4 = expected_state;
1868 }
1869 std::net::SocketAddr::V6 { .. } => {
1870 expected_state_v6 = expected_state;
1871 }
1872 }
1873 });
1874 assert_eq!(ctx.discovered_state.ipv4, expected_state_v4);
1876 assert_eq!(ctx.discovered_state.ipv6, expected_state_v6);
1877 }
1878
1879 #[derive(Default, Clone)]
1880 struct FakePing {
1881 gateway_addrs: std::collections::HashSet<std::net::IpAddr>,
1882 gateway_response: bool,
1883 internet_response: bool,
1884 }
1885
1886 #[async_trait]
1887 impl Ping for FakePing {
1888 async fn ping(
1889 &self,
1890 _interface_name: &str,
1891 addr: std::net::SocketAddr,
1892 ) -> Result<(), crate::ping::PingError> {
1893 let Self { gateway_addrs, gateway_response, internet_response } = self;
1894 let ip = addr.ip();
1895 let success = if [
1896 IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1897 IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1898 ]
1899 .contains(&ip)
1900 {
1901 *internet_response
1902 } else if gateway_addrs.contains(&ip) {
1903 *gateway_response
1904 } else {
1905 false
1906 };
1907 if success { Ok(()) } else { Err(crate::ping::PingError::NoReply) }
1908 }
1909 }
1910
1911 #[derive(Default)]
1912 struct FakeDig {
1913 response: Option<ResolvedIps>,
1914 }
1915
1916 impl FakeDig {
1917 fn new(ips: Vec<std::net::IpAddr>) -> Self {
1918 let mut ips_out = ResolvedIps::default();
1919 for ip in ips {
1920 match ip {
1921 IpAddr::V4(v4) => ips_out.v4.push(v4),
1922 IpAddr::V6(v6) => ips_out.v6.push(v6),
1923 }
1924 }
1925 FakeDig { response: Some(ips_out) }
1926 }
1927 }
1928
1929 #[async_trait]
1930 impl Dig for FakeDig {
1931 async fn dig(&self, _interface_name: &str, _domain: &str) -> Option<ResolvedIps> {
1932 self.response.clone()
1933 }
1934 }
1935
1936 #[derive(Default)]
1937 struct FakeFetch {
1938 expected_url: Option<&'static str>,
1939 response: Option<Box<dyn Fn() -> Result<u16, fetch::FetchError> + Send + Sync>>,
1940 }
1941
1942 #[async_trait]
1943 impl Fetch for FakeFetch {
1944 async fn fetch<FA: FetchAddr + std::marker::Sync>(
1945 &self,
1946 _interface_name: &str,
1947 domain: &str,
1948 path: &str,
1949 _addr: &FA,
1950 ) -> Result<u16, fetch::FetchError> {
1951 if let Some(expected) = self.expected_url {
1952 assert_eq!(
1953 format!("http://{domain}{path}"),
1954 expected,
1955 "Did not receive expected URL"
1956 );
1957 }
1958 if let Some(response) = &self.response {
1959 response()
1960 } else {
1961 Err(fetch::FetchError::ReadTcpStreamTimeout)
1962 }
1963 }
1964 }
1965
1966 struct NetworkCheckTestResponder {
1967 receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
1968 }
1969
1970 impl NetworkCheckTestResponder {
1971 fn new(
1972 receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
1973 ) -> Self {
1974 Self { receiver }
1975 }
1976
1977 async fn respond_to_messages<P: Ping, D: Dig, F: Fetch, Time: TimeProvider>(
1978 &mut self,
1979 monitor: &mut Monitor<Time>,
1980 p: P,
1981 d: D,
1982 f: F,
1983 ) {
1984 loop {
1985 if let Some((action, cookie)) = self.receiver.next().await {
1986 match action {
1987 NetworkCheckAction::Ping(parameters) => {
1988 let result = p.ping(¶meters.interface_name, parameters.addr).await;
1989 match monitor
1990 .resume(cookie, NetworkCheckResult::Ping { parameters, result })
1991 {
1992 Ok(NetworkCheckerOutcome::Complete) => return,
1994 _ => {}
1995 }
1996 }
1997 NetworkCheckAction::ResolveDns(parameters) => {
1998 let ips = d.dig(¶meters.interface_name, ¶meters.domain).await;
1999 match monitor
2000 .resume(cookie, NetworkCheckResult::ResolveDns { parameters, ips })
2001 {
2002 Ok(NetworkCheckerOutcome::Complete) => return,
2004 _ => {}
2005 }
2006 }
2007 NetworkCheckAction::Fetch(parameters) => {
2008 let result = f
2009 .fetch(
2010 ¶meters.interface_name,
2011 ¶meters.domain,
2012 ¶meters.path,
2013 ¶meters.ip,
2014 )
2015 .await;
2016 match monitor
2017 .resume(cookie, NetworkCheckResult::Fetch { parameters, result })
2018 {
2019 Ok(NetworkCheckerOutcome::Complete) => return,
2021 _ => {}
2022 }
2023 }
2024 }
2025 }
2026 }
2027 }
2028 }
2029
2030 fn run_network_check_partial_properties_repeated<P: Ping, D: Dig, F: Fetch>(
2031 exec: &mut fasync::TestExecutor,
2032 name: &str,
2033 interface_id: u64,
2034 routes: &RouteTable,
2035 mocks: Vec<(P, D, F)>,
2036 neighbors: Option<&InterfaceNeighborCache>,
2037 internet_ping_address: std::net::IpAddr,
2038 sleep_between: Option<zx::MonotonicDuration>,
2039 ) -> Vec<State> {
2040 let properties = &fnet_interfaces_ext::Properties {
2041 id: interface_id.try_into().expect("should be nonzero"),
2042 name: name.to_string(),
2043 port_class: fnet_interfaces_ext::PortClass::Ethernet,
2044 online: true,
2045 addresses: Default::default(),
2046 has_default_ipv4_route: Default::default(),
2047 has_default_ipv6_route: Default::default(),
2048 port_identity_koid: Default::default(),
2049 };
2050
2051 let mock_count = mocks.len();
2052 match run_network_check_repeated(exec, properties, routes, neighbors, mocks, sleep_between)
2053 {
2054 Ok(Some(events)) => {
2055 events
2059 .into_iter()
2060 .map(|event| match internet_ping_address {
2061 std::net::IpAddr::V4 { .. } => event.ipv4.state,
2062 std::net::IpAddr::V6 { .. } => event.ipv6.state,
2063 })
2064 .collect()
2065 }
2066 Ok(None) => {
2067 error!("id for interface unexpectedly did not exist after network check");
2068 std::iter::repeat(LinkState::None.into()).take(mock_count).collect()
2069 }
2070 Err(e) => {
2071 error!("network check had an issue calculating state: {:?}", e);
2072 std::iter::repeat(LinkState::None.into()).take(mock_count).collect()
2073 }
2074 }
2075 }
2076
2077 fn run_network_check_partial_properties<P: Ping, D: Dig, F: Fetch>(
2078 exec: &mut fasync::TestExecutor,
2079 name: &str,
2080 interface_id: u64,
2081 routes: &RouteTable,
2082 pinger: P,
2083 digger: D,
2084 fetcher: F,
2085 neighbors: Option<&InterfaceNeighborCache>,
2086 internet_ping_address: std::net::IpAddr,
2087 ) -> State {
2088 run_network_check_partial_properties_repeated(
2089 exec,
2090 name,
2091 interface_id,
2092 routes,
2093 vec![(pinger, digger, fetcher)],
2094 neighbors,
2095 internet_ping_address,
2096 None,
2097 )
2098 .pop()
2099 .unwrap_or_else(|| {
2100 error!("network check returned no states");
2101 LinkState::None.into()
2102 })
2103 }
2104
2105 fn run_network_check_repeated<P: Ping, D: Dig, F: Fetch>(
2106 exec: &mut fasync::TestExecutor,
2107 properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
2108 routes: &RouteTable,
2109 neighbors: Option<&InterfaceNeighborCache>,
2110 mocks: Vec<(P, D, F)>,
2111 sleep_between: Option<zx::MonotonicDuration>,
2112 ) -> Result<Option<Vec<IpVersions<StateEvent>>>, anyhow::Error> {
2113 let (sender, receiver) = mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
2114 let mut monitor = Monitor::new_with_time_provider(
2115 sender,
2116 FakeTime {
2117 increment: sleep_between.unwrap_or(zx::MonotonicDuration::from_nanos(10)),
2118 time: zx::MonotonicInstant::get(),
2119 },
2120 )
2121 .unwrap();
2122 let mut network_check_responder = NetworkCheckTestResponder::new(receiver);
2123
2124 let view = InterfaceView { properties, routes, neighbors };
2125 let network_check_fut = async {
2126 let mut states = Vec::new();
2127 for (pinger, digger, fetcher) in mocks {
2128 match monitor.begin(view) {
2129 Ok(NetworkCheckerOutcome::Complete) => {}
2130 Ok(NetworkCheckerOutcome::MustResume) => {
2131 let () = network_check_responder
2132 .respond_to_messages(&mut monitor, pinger, digger, fetcher)
2133 .await;
2134 }
2135 Err(e) => {
2136 error!("begin had an issue calculating state: {:?}", e)
2137 }
2138 }
2139 states.push(monitor.state().get(properties.id.get()).map(Clone::clone));
2140 }
2141 states
2142 };
2143
2144 let mut network_check_fut = pin!(network_check_fut);
2145 match exec.run_until_stalled(&mut network_check_fut) {
2146 Poll::Ready(got) => Ok(got.into_iter().collect()),
2147 Poll::Pending => Err(anyhow::anyhow!("network_check blocked unexpectedly")),
2148 }
2149 }
2150
2151 fn run_network_check<P: Ping, D: Dig, F: Fetch>(
2152 exec: &mut fasync::TestExecutor,
2153 properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
2154 routes: &RouteTable,
2155 neighbors: Option<&InterfaceNeighborCache>,
2156 pinger: P,
2157 digger: D,
2158 fetcher: F,
2159 ) -> Result<Option<IpVersions<StateEvent>>, anyhow::Error> {
2160 run_network_check_repeated(
2161 exec,
2162 properties,
2163 routes,
2164 neighbors,
2165 vec![(pinger, digger, fetcher)],
2166 None,
2167 )
2168 .map(|res| res.and_then(|mut v| v.pop()))
2169 }
2170
2171 #[test]
2172 fn test_network_check_ipv6_local_only() {
2173 let mut exec = fasync::TestExecutor::new_with_fake_time();
2174 let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2175 let () = exec.set_fake_time(time.into());
2176
2177 let routes = testutil::build_route_table_from_flattened_routes([Route {
2180 destination: UNSPECIFIED_V6,
2181 outbound_interface: ID1,
2182 next_hop: Some(IPV6_ADDR),
2183 }]);
2184 let properties = &fnet_interfaces_ext::Properties {
2185 id: ID1.try_into().expect("should be nonzero"),
2186 name: ETHERNET_INTERFACE_NAME.to_string(),
2187 port_class: fnet_interfaces_ext::PortClass::Ethernet,
2188 online: true,
2189 addresses: vec![],
2190 has_default_ipv4_route: false,
2191 has_default_ipv6_route: true,
2192 port_identity_koid: Default::default(),
2193 };
2194 let neighbors = InterfaceNeighborCache::default();
2195
2196 let got = run_network_check(
2197 &mut exec,
2198 properties,
2199 &routes,
2200 Some(&neighbors),
2201 FakePing::default(),
2202 FakeDig::default(),
2203 FakeFetch::default(),
2204 )
2205 .expect("run_network_check failed")
2206 .expect("interface state not found");
2207
2208 let want_ipv4 =
2209 StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2210 let want_ipv6 =
2211 StateEvent { state: State { link: LinkState::Local, ..Default::default() }, time };
2212 assert_eq!(got.ipv4, want_ipv4);
2213 assert_eq!(got.ipv6, want_ipv6);
2214 }
2215
2216 #[test]
2217 fn test_network_check_ipv6_local_only_not_default_route() {
2218 let mut exec = fasync::TestExecutor::new_with_fake_time();
2219 let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2220 let () = exec.set_fake_time(time.into());
2221
2222 let routes = testutil::build_route_table_from_flattened_routes([Route {
2225 destination: fidl_subnet!("::/1"),
2226 outbound_interface: ID1,
2227 next_hop: Some(IPV6_ADDR),
2228 }]);
2229 let properties = &fnet_interfaces_ext::Properties {
2230 id: ID1.try_into().expect("should be nonzero"),
2231 name: ETHERNET_INTERFACE_NAME.to_string(),
2232 port_class: fnet_interfaces_ext::PortClass::Ethernet,
2233 online: true,
2234 addresses: vec![],
2235 has_default_ipv4_route: false,
2236 has_default_ipv6_route: true,
2237 port_identity_koid: Default::default(),
2238 };
2239 let neighbors = InterfaceNeighborCache::default();
2240
2241 let got = run_network_check(
2242 &mut exec,
2243 properties,
2244 &routes,
2245 Some(&neighbors),
2246 FakePing::default(),
2247 FakeDig::default(),
2248 FakeFetch::default(),
2249 )
2250 .expect("run_network_check failed")
2251 .expect("interface state not found");
2252
2253 let want_ipv4 =
2254 StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2255 let want_ipv6 =
2256 StateEvent { state: State { link: LinkState::Local, ..Default::default() }, time };
2257 assert_eq!(got.ipv4, want_ipv4);
2258 assert_eq!(got.ipv6, want_ipv6);
2259 }
2260
2261 #[test]
2262 fn test_network_check_ipv6_gateway_only() {
2263 let mut exec = fasync::TestExecutor::new_with_fake_time();
2264 let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2265 let () = exec.set_fake_time(time.into());
2266
2267 let routes = testutil::build_route_table_from_flattened_routes([Route {
2270 destination: UNSPECIFIED_V6,
2271 outbound_interface: ID1,
2272 next_hop: Some(IPV6_ADDR),
2273 }]);
2274 let properties = &fnet_interfaces_ext::Properties {
2275 id: ID1.try_into().expect("should be nonzero"),
2276 name: ETHERNET_INTERFACE_NAME.to_string(),
2277 port_class: fnet_interfaces_ext::PortClass::Ethernet,
2278 online: true,
2279 addresses: vec![],
2280 has_default_ipv4_route: false,
2281 has_default_ipv6_route: true,
2282 port_identity_koid: Default::default(),
2283 };
2284 let neighbors = InterfaceNeighborCache {
2285 neighbors: [(
2286 IPV6_ADDR,
2287 NeighborState::new(NeighborHealth::Healthy {
2288 last_observed: zx::MonotonicInstant::default(),
2289 }),
2290 )]
2291 .into_iter()
2292 .collect::<HashMap<fnet::IpAddress, NeighborState>>(),
2293 };
2294
2295 let got = run_network_check(
2296 &mut exec,
2297 properties,
2298 &routes,
2299 Some(&neighbors),
2300 FakePing::default(),
2301 FakeDig::default(),
2302 FakeFetch::default(),
2303 )
2304 .expect("run_network_check failed")
2305 .expect("interface state not found");
2306
2307 let want_ipv4 =
2308 StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2309 let want_ipv6 =
2310 StateEvent { state: State { link: LinkState::Gateway, ..Default::default() }, time };
2311 assert_eq!(got.ipv4, want_ipv4);
2312 assert_eq!(got.ipv6, want_ipv6);
2313 }
2314
2315 #[fuchsia::test]
2316 fn test_network_check_ipv4_and_ipv6_gateway() {
2317 let mut exec = fasync::TestExecutor::new_with_fake_time();
2318 let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2319 let () = exec.set_fake_time(time.into());
2320
2321 let routes = testutil::build_route_table_from_flattened_routes([
2324 Route {
2325 destination: UNSPECIFIED_V4,
2326 outbound_interface: ID1,
2327 next_hop: Some(IPV4_ADDR),
2328 },
2329 Route {
2330 destination: UNSPECIFIED_V6,
2331 outbound_interface: ID1,
2332 next_hop: Some(IPV6_ADDR),
2333 },
2334 ]);
2335 let properties = &fnet_interfaces_ext::Properties {
2336 id: ID1.try_into().expect("should be nonzero"),
2337 name: ETHERNET_INTERFACE_NAME.to_string(),
2338 port_class: fnet_interfaces_ext::PortClass::Ethernet,
2339 online: true,
2340 addresses: vec![],
2341 has_default_ipv4_route: true,
2342 has_default_ipv6_route: true,
2343 port_identity_koid: Default::default(),
2344 };
2345 let neighbors = InterfaceNeighborCache {
2346 neighbors: [
2347 (
2348 IPV4_ADDR,
2349 NeighborState::new(NeighborHealth::Healthy {
2350 last_observed: zx::MonotonicInstant::default(),
2351 }),
2352 ),
2353 (
2354 IPV6_ADDR,
2355 NeighborState::new(NeighborHealth::Healthy {
2356 last_observed: zx::MonotonicInstant::default(),
2357 }),
2358 ),
2359 ]
2360 .into_iter()
2361 .collect::<HashMap<fnet::IpAddress, NeighborState>>(),
2362 };
2363
2364 let got = run_network_check(
2365 &mut exec,
2366 properties,
2367 &routes,
2368 Some(&neighbors),
2369 FakePing::default(),
2370 FakeDig::default(),
2371 FakeFetch::default(),
2372 )
2373 .expect("run_network_check failed")
2374 .expect("interface state not found");
2375
2376 assert_eq!(
2377 got,
2378 IpVersions::construct(StateEvent {
2379 state: State { link: LinkState::Gateway, ..Default::default() },
2380 time
2381 })
2382 );
2383 }
2384
2385 #[test]
2386 fn test_network_check_ethernet_ipv4() {
2387 test_network_check_ethernet::<ip::Ipv4>(
2388 fidl_ip!("1.2.3.0"),
2389 fidl_ip!("1.2.3.4"),
2390 fidl_ip!("1.2.3.1"),
2391 fidl_ip!("2.2.3.0"),
2392 fidl_ip!("2.2.3.1"),
2393 UNSPECIFIED_V4,
2394 fidl_subnet!("0.0.0.0/1"),
2395 IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
2396 24,
2397 );
2398 }
2399
2400 #[test]
2401 fn test_network_check_ethernet_ipv6() {
2402 test_network_check_ethernet::<ip::Ipv6>(
2403 fidl_ip!("123::"),
2404 fidl_ip!("123::4"),
2405 fidl_ip!("123::1"),
2406 fidl_ip!("223::"),
2407 fidl_ip!("223::1"),
2408 UNSPECIFIED_V6,
2409 fidl_subnet!("::/1"),
2410 IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
2411 64,
2412 );
2413 }
2414
2415 fn test_network_check_ethernet<I: ip::Ip>(
2416 net1: fnet::IpAddress,
2417 _net1_addr: fnet::IpAddress,
2418 net1_gateway: fnet::IpAddress,
2419 net2: fnet::IpAddress,
2420 net2_gateway: fnet::IpAddress,
2421 unspecified_addr: fnet::Subnet,
2422 non_default_addr: fnet::Subnet,
2423 ping_internet_addr: std::net::IpAddr,
2424 prefix_len: u8,
2425 ) {
2426 let route_table = testutil::build_route_table_from_flattened_routes([
2427 Route {
2428 destination: unspecified_addr,
2429 outbound_interface: ID1,
2430 next_hop: Some(net1_gateway),
2431 },
2432 Route {
2433 destination: fnet::Subnet { addr: net1, prefix_len },
2434 outbound_interface: ID1,
2435 next_hop: None,
2436 },
2437 ]);
2438 let route_table_2 = testutil::build_route_table_from_flattened_routes([
2439 Route {
2440 destination: unspecified_addr,
2441 outbound_interface: ID1,
2442 next_hop: Some(net2_gateway),
2443 },
2444 Route {
2445 destination: fnet::Subnet { addr: net1, prefix_len },
2446 outbound_interface: ID1,
2447 next_hop: None,
2448 },
2449 Route {
2450 destination: fnet::Subnet { addr: net2, prefix_len },
2451 outbound_interface: ID1,
2452 next_hop: None,
2453 },
2454 ]);
2455 let route_table_3 = testutil::build_route_table_from_flattened_routes([
2456 Route {
2457 destination: unspecified_addr,
2458 outbound_interface: ID2,
2459 next_hop: Some(net1_gateway),
2460 },
2461 Route {
2462 destination: fnet::Subnet { addr: net1, prefix_len },
2463 outbound_interface: ID2,
2464 next_hop: None,
2465 },
2466 ]);
2467 let route_table_4 = testutil::build_route_table_from_flattened_routes([
2468 Route {
2469 destination: non_default_addr,
2470 outbound_interface: ID1,
2471 next_hop: Some(net1_gateway),
2472 },
2473 Route {
2474 destination: fnet::Subnet { addr: net1, prefix_len },
2475 outbound_interface: ID1,
2476 next_hop: None,
2477 },
2478 ]);
2479
2480 let fnet_ext::IpAddress(net1_gateway_ext) = net1_gateway.into();
2481 let mut exec = fasync::TestExecutor::new();
2482
2483 assert_eq!(
2485 run_network_check_partial_properties(
2486 &mut exec,
2487 ETHERNET_INTERFACE_NAME,
2488 ID1,
2489 &route_table,
2490 FakePing {
2491 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2492 gateway_response: true,
2493 internet_response: true,
2494 },
2495 FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]),
2496 FakeFetch {
2497 expected_url: Some("http://www.gstatic.com/generate_204"),
2498 response: Some(Box::new(|| Ok(204))),
2499 },
2500 None,
2501 ping_internet_addr,
2502 ),
2503 State {
2504 link: LinkState::Internet,
2505 application: ApplicationState { dns_resolved: true, http_fetch_succeeded: true },
2506 },
2507 "All is good. Can reach internet"
2508 );
2509
2510 assert_eq!(
2511 run_network_check_partial_properties(
2512 &mut exec,
2513 ETHERNET_INTERFACE_NAME,
2514 ID1,
2515 &route_table,
2516 FakePing {
2517 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2518 gateway_response: true,
2519 internet_response: true,
2520 },
2521 FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]),
2522 FakeFetch::default(),
2523 None,
2524 ping_internet_addr,
2525 ),
2526 State {
2527 link: LinkState::Internet,
2528 application: ApplicationState { dns_resolved: true, ..Default::default() },
2529 },
2530 "HTTP Fetch fails"
2531 );
2532
2533 assert_eq!(
2534 run_network_check_partial_properties(
2535 &mut exec,
2536 ETHERNET_INTERFACE_NAME,
2537 ID1,
2538 &route_table,
2539 FakePing {
2540 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2541 gateway_response: true,
2542 internet_response: true,
2543 },
2544 FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("1.2.4.0")]),
2545 FakeFetch::default(),
2546 None,
2547 ping_internet_addr,
2548 ),
2549 State {
2550 link: LinkState::Internet,
2551 application: ApplicationState {
2552 dns_resolved: ping_internet_addr.is_ipv4(),
2553 ..Default::default()
2554 },
2555 },
2556 "DNS Resolves only IPV4",
2557 );
2558
2559 assert_eq!(
2560 run_network_check_partial_properties(
2561 &mut exec,
2562 ETHERNET_INTERFACE_NAME,
2563 ID1,
2564 &route_table,
2565 FakePing {
2566 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2567 gateway_response: true,
2568 internet_response: true,
2569 },
2570 FakeDig::new(vec![std_ip!("123::"), std_ip!("124::")]),
2571 FakeFetch::default(),
2572 None,
2573 ping_internet_addr,
2574 ),
2575 State {
2576 link: LinkState::Internet,
2577 application: ApplicationState {
2578 dns_resolved: ping_internet_addr.is_ipv6(),
2579 ..Default::default()
2580 },
2581 },
2582 "DNS Resolves only IPV6",
2583 );
2584
2585 assert_eq!(
2586 run_network_check_partial_properties(
2587 &mut exec,
2588 ETHERNET_INTERFACE_NAME,
2589 ID1,
2590 &route_table,
2591 FakePing {
2592 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2593 gateway_response: false,
2594 internet_response: true,
2595 },
2596 FakeDig::default(),
2597 FakeFetch::default(),
2598 Some(&InterfaceNeighborCache {
2599 neighbors: [(
2600 net1_gateway,
2601 NeighborState::new(NeighborHealth::Healthy {
2602 last_observed: zx::MonotonicInstant::default(),
2603 })
2604 )]
2605 .into_iter()
2606 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2607 }),
2608 ping_internet_addr,
2609 ),
2610 LinkState::Internet.into(),
2611 "Can reach internet, gateway responding via ARP/ND"
2612 );
2613
2614 assert_eq!(
2615 run_network_check_partial_properties(
2616 &mut exec,
2617 ETHERNET_INTERFACE_NAME,
2618 ID1,
2619 &route_table,
2620 FakePing {
2621 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2622 gateway_response: false,
2623 internet_response: true,
2624 },
2625 FakeDig::default(),
2626 FakeFetch::default(),
2627 Some(&InterfaceNeighborCache {
2628 neighbors: [(
2629 net1,
2630 NeighborState::new(NeighborHealth::Healthy {
2631 last_observed: zx::MonotonicInstant::default(),
2632 })
2633 )]
2634 .into_iter()
2635 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2636 }),
2637 ping_internet_addr,
2638 ),
2639 LinkState::Internet.into(),
2640 "Gateway not responding via ping or ARP/ND. Can reach internet"
2641 );
2642
2643 assert_eq!(
2644 run_network_check_partial_properties(
2645 &mut exec,
2646 ETHERNET_INTERFACE_NAME,
2647 ID1,
2648 &route_table_4,
2649 FakePing {
2650 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2651 gateway_response: true,
2652 internet_response: true,
2653 },
2654 FakeDig::default(),
2655 FakeFetch::default(),
2656 Some(&InterfaceNeighborCache {
2657 neighbors: [(
2658 net1_gateway,
2659 NeighborState::new(NeighborHealth::Healthy {
2660 last_observed: zx::MonotonicInstant::default(),
2661 })
2662 )]
2663 .into_iter()
2664 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2665 }),
2666 ping_internet_addr,
2667 ),
2668 LinkState::Internet.into(),
2669 "No default route, but healthy gateway with internet/gateway response"
2670 );
2671
2672 assert_eq!(
2673 run_network_check_partial_properties(
2674 &mut exec,
2675 ETHERNET_INTERFACE_NAME,
2676 ID1,
2677 &route_table,
2678 FakePing {
2679 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2680 gateway_response: true,
2681 internet_response: false,
2682 },
2683 FakeDig::default(),
2684 FakeFetch::default(),
2685 None,
2686 ping_internet_addr,
2687 ),
2688 LinkState::Gateway.into(),
2689 "Can reach gateway via ping"
2690 );
2691
2692 assert_eq!(
2693 run_network_check_partial_properties(
2694 &mut exec,
2695 ETHERNET_INTERFACE_NAME,
2696 ID1,
2697 &route_table,
2698 FakePing::default(),
2699 FakeDig::default(),
2700 FakeFetch::default(),
2701 Some(&InterfaceNeighborCache {
2702 neighbors: [(
2703 net1_gateway,
2704 NeighborState::new(NeighborHealth::Healthy {
2705 last_observed: zx::MonotonicInstant::default(),
2706 })
2707 )]
2708 .into_iter()
2709 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2710 }),
2711 ping_internet_addr,
2712 ),
2713 LinkState::Gateway.into(),
2714 "Can reach gateway via ARP/ND"
2715 );
2716
2717 assert_eq!(
2718 run_network_check_partial_properties(
2719 &mut exec,
2720 ETHERNET_INTERFACE_NAME,
2721 ID1,
2722 &route_table,
2723 FakePing {
2724 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2725 gateway_response: false,
2726 internet_response: false,
2727 },
2728 FakeDig::default(),
2729 FakeFetch::default(),
2730 None,
2731 ping_internet_addr,
2732 ),
2733 LinkState::Local.into(),
2734 "Local only, Cannot reach gateway"
2735 );
2736
2737 assert_eq!(
2738 run_network_check_partial_properties(
2739 &mut exec,
2740 ETHERNET_INTERFACE_NAME,
2741 ID1,
2742 &route_table_2,
2743 FakePing::default(),
2744 FakeDig::default(),
2745 FakeFetch::default(),
2746 None,
2747 ping_internet_addr,
2748 ),
2749 LinkState::Local.into(),
2750 "No default route"
2751 );
2752
2753 assert_eq!(
2754 run_network_check_partial_properties(
2755 &mut exec,
2756 ETHERNET_INTERFACE_NAME,
2757 ID1,
2758 &route_table_4,
2759 FakePing {
2760 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2761 gateway_response: true,
2762 internet_response: false,
2763 },
2764 FakeDig::default(),
2765 FakeFetch::default(),
2766 None,
2767 ping_internet_addr,
2768 ),
2769 LinkState::Local.into(),
2770 "No default route, with only gateway response"
2771 );
2772
2773 assert_eq!(
2774 run_network_check_partial_properties(
2775 &mut exec,
2776 ETHERNET_INTERFACE_NAME,
2777 ID1,
2778 &route_table_2,
2779 FakePing::default(),
2780 FakeDig::default(),
2781 FakeFetch::default(),
2782 Some(&InterfaceNeighborCache {
2783 neighbors: [(
2784 net1,
2785 NeighborState::new(NeighborHealth::Healthy {
2786 last_observed: zx::MonotonicInstant::default(),
2787 })
2788 )]
2789 .into_iter()
2790 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2791 }),
2792 ping_internet_addr,
2793 ),
2794 LinkState::Local.into(),
2795 "Local only, neighbors responsive with no default route"
2796 );
2797
2798 assert_eq!(
2799 run_network_check_partial_properties(
2800 &mut exec,
2801 ETHERNET_INTERFACE_NAME,
2802 ID1,
2803 &route_table,
2804 FakePing::default(),
2805 FakeDig::default(),
2806 FakeFetch::default(),
2807 Some(&InterfaceNeighborCache {
2808 neighbors: [(
2809 net1,
2810 NeighborState::new(NeighborHealth::Healthy {
2811 last_observed: zx::MonotonicInstant::default(),
2812 })
2813 )]
2814 .into_iter()
2815 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2816 }),
2817 ping_internet_addr
2818 ),
2819 LinkState::Local.into(),
2820 "Local only, neighbors responsive with a default route"
2821 );
2822
2823 assert_eq!(
2824 run_network_check_partial_properties(
2825 &mut exec,
2826 ETHERNET_INTERFACE_NAME,
2827 ID1,
2828 &route_table_3,
2829 FakePing::default(),
2830 FakeDig::default(),
2831 FakeFetch::default(),
2832 Some(&InterfaceNeighborCache {
2833 neighbors: [(
2834 net1,
2835 NeighborState::new(NeighborHealth::Healthy {
2836 last_observed: zx::MonotonicInstant::default(),
2837 })
2838 )]
2839 .into_iter()
2840 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2841 }),
2842 ping_internet_addr,
2843 ),
2844 LinkState::Local.into(),
2845 "Local only, neighbors responsive with no routes"
2846 );
2847
2848 assert_eq!(
2849 run_network_check_partial_properties(
2850 &mut exec,
2851 ETHERNET_INTERFACE_NAME,
2852 ID1,
2853 &route_table,
2854 FakePing::default(),
2855 FakeDig::default(),
2856 FakeFetch::default(),
2857 Some(&InterfaceNeighborCache {
2858 neighbors: [
2859 (
2860 net1,
2861 NeighborState::new(NeighborHealth::Healthy {
2862 last_observed: zx::MonotonicInstant::default(),
2863 })
2864 ),
2865 (
2866 net1_gateway,
2867 NeighborState::new(NeighborHealth::Unhealthy { last_healthy: None })
2868 )
2869 ]
2870 .into_iter()
2871 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2872 }),
2873 ping_internet_addr,
2874 ),
2875 LinkState::Local.into(),
2876 "Local only, gateway unhealthy with healthy neighbor"
2877 );
2878
2879 assert_eq!(
2880 run_network_check_partial_properties(
2881 &mut exec,
2882 ETHERNET_INTERFACE_NAME,
2883 ID1,
2884 &route_table_3,
2885 FakePing::default(),
2886 FakeDig::default(),
2887 FakeFetch::default(),
2888 Some(&InterfaceNeighborCache {
2889 neighbors: [(
2890 net1_gateway,
2891 NeighborState::new(NeighborHealth::Unhealthy { last_healthy: None })
2892 )]
2893 .into_iter()
2894 .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2895 }),
2896 ping_internet_addr,
2897 ),
2898 LinkState::Up.into(),
2899 "No routes and unhealthy gateway"
2900 );
2901
2902 assert_eq!(
2903 run_network_check_partial_properties(
2904 &mut exec,
2905 ETHERNET_INTERFACE_NAME,
2906 ID1,
2907 &route_table_3,
2908 FakePing::default(),
2909 FakeDig::default(),
2910 FakeFetch::default(),
2911 None,
2912 ping_internet_addr,
2913 ),
2914 LinkState::Up.into(),
2915 "No routes",
2916 );
2917
2918 assert_eq!(
2919 run_network_check_partial_properties_repeated(
2920 &mut exec,
2921 ETHERNET_INTERFACE_NAME,
2922 ID1,
2923 &route_table,
2924 vec![
2925 (
2926 FakePing {
2927 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2928 gateway_response: true,
2929 internet_response: true,
2930 },
2931 FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), FakeFetch {
2933 expected_url: Some("http://www.gstatic.com/generate_204"),
2934 response: Some(Box::new(|| Ok(204))),
2935 },
2936 ),
2937 (
2938 FakePing {
2939 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2940 gateway_response: true,
2941 internet_response: true,
2942 },
2943 FakeDig { response: None }, FakeFetch {
2945 expected_url: Some("http://www.gstatic.com/generate_204"),
2946 response: Some(Box::new(|| Ok(204))),
2947 },
2948 ),
2949 ],
2950 None,
2951 ping_internet_addr,
2952 None,
2953 ),
2954 vec![
2955 State {
2956 link: LinkState::Internet,
2957 application: ApplicationState {
2958 dns_resolved: true,
2959 http_fetch_succeeded: true
2960 }
2961 },
2962 State {
2963 link: LinkState::Internet,
2964 application: ApplicationState {
2965 dns_resolved: true,
2966 http_fetch_succeeded: true
2967 }
2968 }
2969 ],
2970 "Fail DNS on second check; fetch succeeds; no pause"
2971 );
2972
2973 assert_eq!(
2974 run_network_check_partial_properties_repeated(
2975 &mut exec,
2976 ETHERNET_INTERFACE_NAME,
2977 ID1,
2978 &route_table,
2979 vec![
2980 (
2981 FakePing {
2982 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2983 gateway_response: true,
2984 internet_response: true,
2985 },
2986 FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), FakeFetch {
2988 expected_url: Some("http://www.gstatic.com/generate_204"),
2989 response: Some(Box::new(|| Ok(204))),
2990 },
2991 ),
2992 (
2993 FakePing {
2994 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2995 gateway_response: true,
2996 internet_response: true,
2997 },
2998 FakeDig { response: None }, FakeFetch {
3000 expected_url: Some("http://www.gstatic.com/generate_204"),
3001 response: Some(Box::new(|| Ok(204))),
3002 },
3003 ),
3004 ],
3005 None,
3006 ping_internet_addr,
3007 Some(DNS_PROBE_PERIOD),
3008 ),
3009 vec![
3010 State {
3011 link: LinkState::Internet,
3012 application: ApplicationState {
3013 dns_resolved: true,
3014 http_fetch_succeeded: true
3015 }
3016 },
3017 State {
3018 link: LinkState::Internet,
3019 application: ApplicationState {
3020 dns_resolved: false,
3021 http_fetch_succeeded: true
3022 }
3023 }
3024 ],
3025 "Fail DNS on second check; fetch succeeds"
3026 );
3027
3028 assert_eq!(
3029 run_network_check_partial_properties_repeated(
3030 &mut exec,
3031 ETHERNET_INTERFACE_NAME,
3032 ID1,
3033 &route_table,
3034 vec![
3035 (
3036 FakePing {
3037 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3038 gateway_response: true,
3039 internet_response: true,
3040 },
3041 FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), FakeFetch {
3043 expected_url: Some("http://www.gstatic.com/generate_204"),
3044 response: Some(Box::new(|| Err(
3045 fetch::FetchError::ReadTcpStreamTimeout
3046 ))),
3047 },
3048 ),
3049 (
3050 FakePing {
3051 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3052 gateway_response: true,
3053 internet_response: true,
3054 },
3055 FakeDig { response: None }, FakeFetch {
3057 expected_url: Some("http://www.gstatic.com/generate_204"),
3058 response: Some(Box::new(|| Err(
3059 fetch::FetchError::ReadTcpStreamTimeout
3060 ))),
3061 },
3062 ),
3063 ],
3064 None,
3065 ping_internet_addr,
3066 None,
3067 ),
3068 vec![
3069 State {
3070 link: LinkState::Internet,
3071 application: ApplicationState { dns_resolved: true, ..Default::default() }
3072 },
3073 State {
3074 link: LinkState::Internet,
3075 application: ApplicationState { dns_resolved: true, ..Default::default() }
3076 }
3077 ],
3078 "Fail DNS on second check; fetch fails; no pause"
3079 );
3080
3081 assert_eq!(
3082 run_network_check_partial_properties_repeated(
3083 &mut exec,
3084 ETHERNET_INTERFACE_NAME,
3085 ID1,
3086 &route_table,
3087 vec![
3088 (
3089 FakePing {
3090 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3091 gateway_response: true,
3092 internet_response: true,
3093 },
3094 FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), FakeFetch {
3096 expected_url: Some("http://www.gstatic.com/generate_204"),
3097 response: Some(Box::new(|| Err(
3098 fetch::FetchError::ReadTcpStreamTimeout
3099 ))),
3100 },
3101 ),
3102 (
3103 FakePing {
3104 gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3105 gateway_response: true,
3106 internet_response: true,
3107 },
3108 FakeDig { response: None }, FakeFetch {
3110 expected_url: Some("http://www.gstatic.com/generate_204"),
3111 response: Some(Box::new(|| Err(
3112 fetch::FetchError::ReadTcpStreamTimeout
3113 ))),
3114 },
3115 ),
3116 ],
3117 None,
3118 ping_internet_addr,
3119 Some(DNS_PROBE_PERIOD),
3120 ),
3121 vec![
3122 State {
3123 link: LinkState::Internet,
3124 application: ApplicationState { dns_resolved: true, ..Default::default() }
3125 },
3126 State {
3127 link: LinkState::Internet,
3128 application: ApplicationState { dns_resolved: false, ..Default::default() }
3129 }
3130 ],
3131 "Fail DNS on second check; fetch fails"
3132 );
3133 }
3134
3135 #[test]
3136 fn test_network_check_varying_properties() {
3137 let properties = fnet_interfaces_ext::Properties {
3138 id: ID1.try_into().expect("should be nonzero"),
3139 name: ETHERNET_INTERFACE_NAME.to_string(),
3140 port_class: fnet_interfaces_ext::PortClass::Ethernet,
3141 has_default_ipv4_route: true,
3142 has_default_ipv6_route: true,
3143 online: true,
3144 addresses: vec![
3145 fnet_interfaces_ext::Address {
3146 addr: fidl_subnet!("1.2.3.0/24"),
3147 valid_until: fnet_interfaces_ext::NoInterest,
3148 preferred_lifetime_info: fnet_interfaces_ext::NoInterest,
3149 assignment_state: fnet_interfaces::AddressAssignmentState::Assigned,
3150 },
3151 fnet_interfaces_ext::Address {
3152 addr: fidl_subnet!("123::4/64"),
3153 valid_until: fnet_interfaces_ext::NoInterest,
3154 preferred_lifetime_info: fnet_interfaces_ext::NoInterest,
3155 assignment_state: fnet_interfaces::AddressAssignmentState::Assigned,
3156 },
3157 ],
3158 port_identity_koid: Default::default(),
3159 };
3160 let local_routes = testutil::build_route_table_from_flattened_routes([
3161 Route {
3162 destination: fidl_subnet!("1.2.3.0/24"),
3163 outbound_interface: ID1,
3164 next_hop: None,
3165 },
3166 Route {
3167 destination: fidl_subnet!("123::/64"),
3168 outbound_interface: ID1,
3169 next_hop: None,
3170 },
3171 ]);
3172 let route_table = testutil::build_route_table_from_flattened_routes([
3173 Route {
3174 destination: fidl_subnet!("0.0.0.0/0"),
3175 outbound_interface: ID1,
3176 next_hop: Some(fidl_ip!("1.2.3.1")),
3177 },
3178 Route {
3179 destination: fidl_subnet!("::0/0"),
3180 outbound_interface: ID1,
3181 next_hop: Some(fidl_ip!("123::1")),
3182 },
3183 ]);
3184 let route_table2 = testutil::build_route_table_from_flattened_routes([
3185 Route {
3186 destination: fidl_subnet!("0.0.0.0/0"),
3187 outbound_interface: ID1,
3188 next_hop: Some(fidl_ip!("2.2.3.1")),
3189 },
3190 Route {
3191 destination: fidl_subnet!("::0/0"),
3192 outbound_interface: ID1,
3193 next_hop: Some(fidl_ip!("223::1")),
3194 },
3195 ]);
3196
3197 const NON_ETHERNET_INTERFACE_NAME: &str = "test01";
3198
3199 let mut exec = fasync::TestExecutor::new_with_fake_time();
3200 let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
3201 let () = exec.set_fake_time(time.into());
3202
3203 let got = run_network_check(
3204 &mut exec,
3205 &fnet_interfaces_ext::Properties {
3206 id: ID1.try_into().expect("should be nonzero"),
3207 name: NON_ETHERNET_INTERFACE_NAME.to_string(),
3208 port_class: fnet_interfaces_ext::PortClass::Virtual,
3209 online: false,
3210 has_default_ipv4_route: false,
3211 has_default_ipv6_route: false,
3212 addresses: vec![],
3213 port_identity_koid: Default::default(),
3214 },
3215 &Default::default(),
3216 None,
3217 FakePing::default(),
3218 FakeDig::default(),
3219 FakeFetch::default(),
3220 )
3221 .expect(
3222 "error calling network check with non-ethernet interface, no addresses, interface down",
3223 );
3224 assert_eq!(
3225 got,
3226 Some(IpVersions::construct(StateEvent {
3227 state: State { link: LinkState::Down, ..Default::default() },
3228 time
3229 }))
3230 );
3231
3232 let got = run_network_check(
3233 &mut exec,
3234 &fnet_interfaces_ext::Properties { online: false, ..properties.clone() },
3235 &Default::default(),
3236 None,
3237 FakePing::default(),
3238 FakeDig::default(),
3239 FakeFetch::default(),
3240 )
3241 .expect("error calling network check, want Down state");
3242 let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3243 state: State { link: LinkState::Down, ..Default::default() },
3244 time,
3245 }));
3246 assert_eq!(got, want);
3247
3248 let got = run_network_check(
3249 &mut exec,
3250 &fnet_interfaces_ext::Properties {
3251 has_default_ipv4_route: false,
3252 has_default_ipv6_route: false,
3253 ..properties.clone()
3254 },
3255 &local_routes,
3256 None,
3257 FakePing::default(),
3258 FakeDig::default(),
3259 FakeFetch::default(),
3260 )
3261 .expect("error calling network check, want Local state due to no default routes");
3262 let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3263 state: State { link: LinkState::Local, ..Default::default() },
3264 time,
3265 }));
3266 assert_eq!(got, want);
3267
3268 let got = run_network_check(
3269 &mut exec,
3270 &properties,
3271 &route_table2,
3272 None,
3273 FakePing::default(),
3274 FakeDig::default(),
3275 FakeFetch::default(),
3276 )
3277 .expect("error calling network check, want Local state due to no matching default route");
3278 let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3279 state: State { link: LinkState::Local, ..Default::default() },
3280 time,
3281 }));
3282 assert_eq!(got, want);
3283
3284 let got = run_network_check(
3285 &mut exec,
3286 &properties,
3287 &route_table,
3288 None,
3289 FakePing {
3290 gateway_addrs: [std_ip!("1.2.3.1"), std_ip!("123::1")].into_iter().collect(),
3291 gateway_response: true,
3292 internet_response: false,
3293 },
3294 FakeDig::default(),
3295 FakeFetch::default(),
3296 )
3297 .expect("error calling network check, want Gateway state");
3298 let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3299 state: State { link: LinkState::Gateway, ..Default::default() },
3300 time,
3301 }));
3302 assert_eq!(got, want);
3303
3304 let got = run_network_check(
3305 &mut exec,
3306 &properties,
3307 &route_table,
3308 None,
3309 FakePing {
3310 gateway_addrs: [std_ip!("1.2.3.1"), std_ip!("123::1")].into_iter().collect(),
3311 gateway_response: true,
3312 internet_response: true,
3313 },
3314 FakeDig::default(),
3315 FakeFetch::default(),
3316 )
3317 .expect("error calling network check, want Internet state");
3318 let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3319 state: State { link: LinkState::Internet, ..Default::default() },
3320 time,
3321 }));
3322 assert_eq!(got, want);
3323 }
3324
3325 fn update_delta(port: Delta<StateEvent>, system: Delta<SystemState>) -> StateDelta {
3326 StateDelta {
3327 port: IpVersions { ipv4: port.clone(), ipv6: port },
3328 system: IpVersions { ipv4: system.clone(), ipv6: system },
3329 }
3330 }
3331
3332 #[test]
3333 fn test_state_info_update() {
3334 let if1_local_event = StateEvent::construct(LinkState::Local);
3335 let if1_local = IpVersions::<StateEvent>::construct(if1_local_event);
3336 let mut state = StateInfo::default();
3338 let want = update_delta(
3339 Delta { previous: None, current: if1_local_event },
3340 Delta { previous: None, current: SystemState { id: ID1, state: if1_local_event } },
3341 );
3342 assert_eq!(state.update(ID1, if1_local.clone()), want);
3343 let want_state = StateInfo {
3344 per_interface: std::iter::once((ID1, if1_local.clone())).collect::<HashMap<_, _>>(),
3345 system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3346 };
3347 assert_eq!(state, want_state);
3348
3349 let if2_gateway_event = StateEvent::construct(LinkState::Gateway);
3350 let if2_gateway = IpVersions::<StateEvent>::construct(if2_gateway_event);
3351 let want = update_delta(
3354 Delta { previous: None, current: if2_gateway_event },
3355 Delta {
3356 previous: Some(SystemState { id: ID1, state: if1_local_event }),
3357 current: SystemState { id: ID2, state: if2_gateway_event },
3358 },
3359 );
3360 assert_eq!(state.update(ID2, if2_gateway.clone()), want);
3361 let want_state = StateInfo {
3362 per_interface: [(ID1, if1_local.clone()), (ID2, if2_gateway.clone())]
3363 .into_iter()
3364 .collect::<HashMap<_, _>>(),
3365 system: IpVersions { ipv4: Some(ID2), ipv6: Some(ID2) },
3366 };
3367 assert_eq!(state, want_state);
3368
3369 let if2_removed_event = StateEvent::construct(LinkState::Removed);
3370 let if2_removed = IpVersions::<StateEvent>::construct(if2_removed_event);
3371 let want = update_delta(
3374 Delta { previous: Some(if2_gateway_event), current: if2_removed_event },
3375 Delta {
3376 previous: Some(SystemState { id: ID2, state: if2_gateway_event }),
3377 current: SystemState { id: ID1, state: if1_local_event },
3378 },
3379 );
3380 assert_eq!(state.update(ID2, if2_removed.clone()), want);
3381 let want_state = StateInfo {
3382 per_interface: [(ID1, if1_local.clone()), (ID2, if2_removed.clone())]
3383 .into_iter()
3384 .collect::<HashMap<_, _>>(),
3385 system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3386 };
3387 assert_eq!(state, want_state);
3388 }
3389
3390 #[test]
3394 fn test_state_info_update_same_link_state() {
3395 let if_local_event = StateEvent::construct(LinkState::Local);
3396 let if_local = IpVersions::<StateEvent>::construct(if_local_event);
3397 let mut state = StateInfo::default();
3399 let want = update_delta(
3400 Delta { previous: None, current: if_local_event },
3401 Delta { previous: None, current: SystemState { id: ID1, state: if_local_event } },
3402 );
3403 assert_eq!(state.update(ID1, if_local.clone()), want);
3404 let want_state = StateInfo {
3405 per_interface: std::iter::once((ID1, if_local.clone())).collect::<HashMap<_, _>>(),
3406 system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3407 };
3408 assert_eq!(state, want_state);
3409
3410 let want = update_delta(
3413 Delta { previous: None, current: if_local_event },
3414 Delta {
3415 previous: Some(SystemState { id: ID1, state: if_local_event }),
3416 current: SystemState { id: ID1, state: if_local_event },
3417 },
3418 );
3419 assert_eq!(state.update(ID2, if_local.clone()), want);
3420 let want_state = StateInfo {
3421 per_interface: [(ID1, if_local.clone()), (ID2, if_local.clone())]
3422 .into_iter()
3423 .collect::<HashMap<_, _>>(),
3424 system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3425 };
3426 assert_eq!(state, want_state);
3427
3428 let if_removed_event = StateEvent::construct(LinkState::Removed);
3431 let if_removed = IpVersions::<StateEvent>::construct(if_removed_event);
3432 let want = update_delta(
3433 Delta { previous: Some(if_local_event), current: if_removed_event },
3434 Delta {
3435 previous: Some(SystemState { id: ID1, state: if_local_event }),
3436 current: SystemState { id: ID2, state: if_local_event },
3437 },
3438 );
3439 assert_eq!(state.update(ID1, if_removed.clone()), want);
3440 let want_state = StateInfo {
3441 per_interface: [(ID1, if_removed.clone()), (ID2, if_local.clone())]
3442 .into_iter()
3443 .collect::<HashMap<_, _>>(),
3444 system: IpVersions { ipv4: Some(ID2), ipv6: Some(ID2) },
3445 };
3446 assert_eq!(state, want_state);
3447 }
3448
3449 #[test_case(None::<LinkState>, None::<LinkState>, false, false, false, false;
3450 "no interfaces available")]
3451 #[test_case(Some(LinkState::Local), Some(LinkState::Local), false, false, false, false;
3452 "no interfaces with gateway or internet state")]
3453 #[test_case(Some(LinkState::Local), Some(LinkState::Gateway), false, false, false, true;
3454 "only one interface with gateway state or above")]
3455 #[test_case(Some(LinkState::Local), Some(LinkState::Internet), false, false, true, true;
3456 "only one interface with internet state")]
3457 #[test_case(Some(LinkState::Internet), Some(LinkState::Internet), false, false, true, true;
3458 "all interfaces with internet")]
3459 #[test_case(Some(LinkState::Internet), None::<LinkState>, false, false, true, true;
3460 "only one interface available, has internet state")]
3461 #[test_case(Some(LinkState::Local), Some((LinkState::Internet, true, false)), false, true, true, true;
3462 "only one interface with DNS resolved state")]
3463 #[test_case(Some((LinkState::Internet, true, false)), Some((LinkState::Internet, true, false)), false, true, true, true;
3464 "all interfaces with DNS resolved state")]
3465 #[test_case(Some((LinkState::Internet, true, false)), None::<LinkState>, false, true, true, true;
3466 "only one interface available, has DNS resolved state")]
3467 #[test_case(Some(LinkState::Local), Some((LinkState::Internet, true, true)), true, true, true, true;
3468 "only one interface with HTTP resolved state")]
3469 #[test_case(Some((LinkState::Internet, true, true)), Some((LinkState::Internet, true, true)), true, true, true, true;
3470 "all interfaces with HTTP resolved state")]
3471 #[test_case(Some((LinkState::Internet, true, true)), None::<LinkState>, true, true, true, true;
3472 "only one interface available, has HTTP resolved state")]
3473 #[test_case(Some((LinkState::Internet, false, true)), None::<LinkState>, true, false, true, true;
3474 "only one interface available, has HTTP resolved state, but no DNS")]
3475 fn test_system_has_state<S1, S2>(
3476 ipv4_state: Option<S1>,
3477 ipv6_state: Option<S2>,
3478 expect_http: bool,
3479 expect_dns: bool,
3480 expect_internet: bool,
3481 expect_gateway: bool,
3482 ) where
3483 StateEvent: Construct<S1>,
3484 StateEvent: Construct<S2>,
3485 {
3486 let if1 = ipv4_state
3487 .map(|state| IpVersions::<StateEvent>::construct(StateEvent::construct(state)));
3488 let if2 = ipv6_state
3489 .map(|state| IpVersions::<StateEvent>::construct(StateEvent::construct(state)));
3490
3491 let mut system_interfaces: HashMap<u64, IpVersions<StateEvent>> = HashMap::new();
3492
3493 let system_interface_ipv4 = if1.map(|interface| {
3494 let _ = system_interfaces.insert(ID1, interface);
3495 ID1
3496 });
3497
3498 let system_interface_ipv6 = if2.map(|interface| {
3499 let _ = system_interfaces.insert(ID2, interface);
3500 ID2
3501 });
3502
3503 let state = StateInfo {
3504 per_interface: system_interfaces,
3505 system: IpVersions { ipv4: system_interface_ipv4, ipv6: system_interface_ipv6 },
3506 };
3507
3508 assert_eq!(state.system_has_http(), expect_http);
3509 assert_eq!(state.system_has_dns(), expect_dns);
3510 assert_eq!(state.system_has_internet(), expect_internet);
3511 assert_eq!(state.system_has_gateway(), expect_gateway);
3512 }
3513
3514 #[test]
3515 fn test_resume_after_interface_removed() {
3516 use assert_matches::assert_matches;
3517
3518 let _exec = fasync::TestExecutor::new();
3519 let (sender, _receiver) = mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
3520 let mut monitor: Monitor<MonotonicInstant> = Monitor::new(sender).unwrap();
3521
3522 let properties = fnet_interfaces_ext::Properties {
3523 id: ID1.try_into().expect("should be nonzero"),
3524 name: ETHERNET_INTERFACE_NAME.to_string(),
3525 port_class: fnet_interfaces_ext::PortClass::Ethernet,
3526 online: false,
3527 addresses: vec![],
3528 has_default_ipv4_route: false,
3529 has_default_ipv6_route: false,
3530 port_identity_koid: Default::default(),
3531 };
3532
3533 let initial_state = IpVersions {
3535 ipv4: StateEvent {
3536 state: State { link: LinkState::None, ..Default::default() },
3537 time: fasync::MonotonicInstant::now(),
3538 },
3539 ipv6: StateEvent {
3540 state: State { link: LinkState::None, ..Default::default() },
3541 time: fasync::MonotonicInstant::now(),
3542 },
3543 };
3544 monitor.update_state(ID1, ETHERNET_INTERFACE_NAME, initial_state);
3545
3546 monitor.handle_interface_removed(properties.clone());
3549
3550 let removed_state = monitor.state().get(ID1).unwrap();
3552 assert_eq!(removed_state.ipv4.state.link, LinkState::Removed);
3553 assert_eq!(removed_state.ipv6.state.link, LinkState::Removed);
3554
3555 let routes = testutil::build_route_table_from_flattened_routes([]);
3560 let view = InterfaceView { properties: &properties, routes: &routes, neighbors: None };
3561 assert_matches!(monitor.begin(view), Ok(NetworkCheckerOutcome::Complete));
3562
3563 let interface_context = monitor.interface_context.get(&ID1).unwrap();
3566 assert_matches!(interface_context.discovered_state.ipv4.link, LinkState::Down);
3567 assert_matches!(interface_context.discovered_state.ipv6.link, LinkState::Down);
3568
3569 let final_state = monitor.state().get(ID1).unwrap();
3572 assert_eq!(final_state.ipv4.state.link, LinkState::Removed);
3573 assert_eq!(final_state.ipv6.state.link, LinkState::Removed);
3574 }
3575
3576 #[test]
3577 fn test_ping_and_fetch_telemetry_events_sent() {
3578 let mut exec = fasync::TestExecutor::new_with_fake_time();
3579 let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
3580 let () = exec.set_fake_time(time.into());
3581
3582 let (action_sender, action_receiver) = mpsc::unbounded();
3583 let mut monitor = Monitor::new_with_time_provider(
3584 action_sender,
3585 FakeTime {
3586 increment: zx::MonotonicDuration::from_nanos(10),
3587 time: zx::MonotonicInstant::get(),
3588 },
3589 )
3590 .unwrap();
3591
3592 let (telemetry_tx, mut telemetry_rx) = mpsc::channel(100);
3593 monitor.set_telemetry_sender(TelemetrySender::new(telemetry_tx));
3594
3595 let properties = &fnet_interfaces_ext::Properties {
3596 id: ID1.try_into().unwrap(),
3597 name: ETHERNET_INTERFACE_NAME.to_string(),
3598 port_class: fnet_interfaces_ext::PortClass::Ethernet,
3599 online: true,
3600 addresses: vec![],
3601 has_default_ipv4_route: true,
3602 has_default_ipv6_route: false,
3603 port_identity_koid: Default::default(),
3604 };
3605
3606 let net_gateway = fidl_ip!("192.168.0.254");
3607 let net_gateway_std = std_ip!("192.168.0.254");
3608
3609 let routes = testutil::build_route_table_from_flattened_routes([Route {
3611 destination: UNSPECIFIED_V4,
3612 outbound_interface: ID1,
3613 next_hop: Some(net_gateway),
3614 }]);
3615
3616 let neighbors_map = [(
3618 net_gateway,
3619 NeighborState::new(NeighborHealth::Healthy {
3620 last_observed: zx::MonotonicInstant::default(),
3621 }),
3622 )]
3623 .into_iter()
3624 .collect::<HashMap<fnet::IpAddress, NeighborState>>();
3625 let neighbors = InterfaceNeighborCache { neighbors: neighbors_map };
3626
3627 let view = InterfaceView { properties, routes: &routes, neighbors: Some(&neighbors) };
3628
3629 let mut network_check_responder = NetworkCheckTestResponder::new(action_receiver);
3630
3631 let pinger = FakePing {
3632 gateway_addrs: vec![net_gateway_std].into_iter().collect(),
3633 gateway_response: true,
3634 internet_response: true,
3635 };
3636 let digger = FakeDig::new(vec![std_ip!("1.2.3.0")]);
3637 let fetcher = FakeFetch::default();
3638
3639 let network_check_fut = async {
3640 match monitor.begin(view) {
3641 Ok(NetworkCheckerOutcome::MustResume) => {
3642 let () = network_check_responder
3643 .respond_to_messages(&mut monitor, pinger, digger, fetcher)
3644 .await;
3645 }
3646 _ => panic!("Expected MustResume"),
3647 }
3648 };
3649
3650 let mut network_check_fut = pin!(network_check_fut);
3651 match exec.run_until_stalled(&mut network_check_fut) {
3652 Poll::Ready(()) => {}
3653 Poll::Pending => panic!("network_check blocked unexpectedly"),
3654 }
3655
3656 let mut events = Vec::new();
3657 while let Poll::Ready(Some(event)) = exec.run_until_stalled(&mut telemetry_rx.next()) {
3658 events.push(event);
3659 }
3660
3661 let has_gateway =
3662 events.iter().any(|e| matches!(e, TelemetryEvent::GatewayPingResult { .. }));
3663 let has_internet =
3664 events.iter().any(|e| matches!(e, TelemetryEvent::InternetPingResult { .. }));
3665 let has_fetch = events.iter().any(|e| matches!(e, TelemetryEvent::FetchResult { .. }));
3666
3667 assert!(has_gateway, "Expected GatewayPingResult telemetry event");
3668 assert!(has_internet, "Expected InternetPingResult telemetry event");
3669 assert!(has_fetch, "Expected FetchResult telemetry event");
3670 }
3671}