Skip to main content

socket_proxy/
registry.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Implements fuchsia.net.policy.socketproxy.NetworkRegistry.
6
7use anyhow::{Context, Error, anyhow};
8use fidl::endpoints::{ControlHandle, RequestStream};
9use fidl_fuchsia_net_policy_socketproxy::{
10    self as fnp_socketproxy, FuchsiaNetworkInfo, FuchsiaNetworksRequest, Network,
11    NetworkDnsServers, NetworkInfo, NetworkRegistryAddError, NetworkRegistryRemoveError,
12    NetworkRegistrySetDefaultError, NetworkRegistryUpdateError, StarnixNetworksRequest,
13};
14use fuchsia_component::client::connect_to_protocol;
15use fuchsia_inspect_derive::{IValue, Inspect, Unit};
16use futures::channel::mpsc;
17use futures::lock::Mutex;
18use futures::{SinkExt as _, StreamExt as _, TryStreamExt as _};
19use log::{error, info, warn};
20use std::collections::HashMap;
21use std::sync::Arc;
22use thiserror::Error;
23
24use {
25    fidl_fuchsia_net as fnet, fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext,
26    fidl_fuchsia_posix_socket as fposix_socket,
27};
28
29/// RFC-1035ยง4.2 specifies port 53 (decimal) as the default port for DNS requests.
30const DEFAULT_DNS_PORT: u16 = 53;
31
32/// If there are networks registered, but no default has been set, this value
33/// will be used, otherwise the mark will be OptionalUint32::Unset(Empty).
34pub(crate) const DEFAULT_SOCKET_MARK: u32 = 0;
35
36pub(crate) struct RequestForwarder {
37    forwarder_rx: mpsc::Receiver<NetworkRegistryRequest>,
38    registry: fnp_socketproxy::NetworkRegistryProxy,
39}
40
41impl RequestForwarder {
42    pub(crate) fn new(
43        forwarder_rx: mpsc::Receiver<NetworkRegistryRequest>,
44    ) -> Result<Self, anyhow::Error> {
45        Ok(Self {
46            forwarder_rx,
47            registry: connect_to_protocol::<fnp_socketproxy::NetworkRegistryMarker>()
48                .context("error connecting to network registry")?,
49        })
50    }
51
52    pub(crate) async fn run(&mut self) -> Result<(), anyhow::Error> {
53        while let Some(request) = self.forwarder_rx.next().await {
54            match self.forward_request(request).await {
55                Err(e) => {
56                    info!(
57                        "FIDL error while forwarding request to delegated networks. \
58                            Protocol likely not available: {e:?}"
59                    );
60                    return Err(e);
61                }
62                Ok(Err(e)) => {
63                    error!(
64                        "Failed to forward request to delegated networks. Future updates \
65                                 will be ignored, and netcfg will not have complete state: {e:?}"
66                    );
67                    return Err(anyhow!("Delegated registry has inconsistent state: {e:?}"));
68                }
69                Ok(Ok(_)) => continue,
70            }
71        }
72        error!(
73            "RequestForwarder stream ended. No more requests will be \
74            forwarded to netcfg"
75        );
76        Ok(())
77    }
78
79    async fn forward_request(
80        &self,
81        request: NetworkRegistryRequest,
82    ) -> Result<Result<(), NetworkRegistryError>, anyhow::Error> {
83        info!("forwarding Starnix NetworkRegistry change to netcfg: {request:?}");
84        let res = match request {
85            NetworkRegistryRequest::SetDefault { network_id } => self
86                .registry
87                .set_default(&match network_id {
88                    Some(id) => fposix_socket::OptionalUint32::Value(id),
89                    None => fposix_socket::OptionalUint32::Unset(fposix_socket::Empty),
90                })
91                .await
92                .context("fidl error forwarding set_default")?
93                .map_err(|e| e.into()),
94            NetworkRegistryRequest::Add { network } => self
95                .registry
96                .add(&network)
97                .await
98                .context("fidl error forwarding add")?
99                .map_err(|e| e.into()),
100            NetworkRegistryRequest::Update { network } => self
101                .registry
102                .update(&network)
103                .await
104                .context("fidl error forwarding update")?
105                .map_err(|e| e.into()),
106            NetworkRegistryRequest::Remove { network_id } => self
107                .registry
108                .remove(network_id)
109                .await
110                .context("fidl error forwarding remove")?
111                .map_err(|e| e.into()),
112        };
113        Ok(res)
114    }
115}
116
117enum CommonErrors {
118    MissingNetworkId,
119    MissingNetworkInfo,
120    MissingNetworkDnsServers,
121}
122
123trait IpAddressExt {
124    fn to_dns_socket_address(self) -> fnet::SocketAddress;
125}
126
127impl<T: IpAddressExt + Copy> IpAddressExt for &T {
128    fn to_dns_socket_address(self) -> fnet::SocketAddress {
129        (*self).to_dns_socket_address()
130    }
131}
132
133impl IpAddressExt for fnet::Ipv4Address {
134    fn to_dns_socket_address(self) -> fnet::SocketAddress {
135        fnet::SocketAddress::Ipv4(fnet::Ipv4SocketAddress { address: self, port: DEFAULT_DNS_PORT })
136    }
137}
138
139impl IpAddressExt for fnet::Ipv6Address {
140    fn to_dns_socket_address(self) -> fnet::SocketAddress {
141        fnet::SocketAddress::Ipv6(fnet::Ipv6SocketAddress {
142            address: self,
143            port: DEFAULT_DNS_PORT,
144            zone_index: 0,
145        })
146    }
147}
148
149trait NetworkInfoExt {
150    fn mark(&self) -> Option<u32>;
151}
152
153impl NetworkInfoExt for NetworkInfo {
154    fn mark(&self) -> Option<u32> {
155        match self {
156            NetworkInfo::Starnix(s) => s.mark,
157            // Sockets express using Fuchsia's default network by setting
158            // the mark to None.
159            NetworkInfo::Fuchsia(_) | _ => None,
160        }
161    }
162}
163
164// Errors produced when communicating updates to
165// the socket proxy.
166#[derive(Clone, Debug, Error)]
167pub enum NetworkRegistryError {
168    #[error("Error during socketproxy Add: {0:?}")]
169    Add(NetworkRegistryAddError),
170    #[error("Error during socketproxy Remove: {0:?}")]
171    Remove(NetworkRegistryRemoveError),
172    #[error("Error during socketproxy SetDefault: {0:?}")]
173    SetDefault(NetworkRegistrySetDefaultError),
174    #[error("Error during socketproxy Update: {0:?}")]
175    Update(NetworkRegistryUpdateError),
176}
177
178impl From<NetworkRegistryAddError> for NetworkRegistryError {
179    fn from(error: NetworkRegistryAddError) -> Self {
180        NetworkRegistryError::Add(error)
181    }
182}
183
184impl From<NetworkRegistryRemoveError> for NetworkRegistryError {
185    fn from(error: NetworkRegistryRemoveError) -> Self {
186        NetworkRegistryError::Remove(error)
187    }
188}
189
190impl From<NetworkRegistrySetDefaultError> for NetworkRegistryError {
191    fn from(error: NetworkRegistrySetDefaultError) -> Self {
192        NetworkRegistryError::SetDefault(error)
193    }
194}
195
196impl From<NetworkRegistryUpdateError> for NetworkRegistryError {
197    fn from(error: NetworkRegistryUpdateError) -> Self {
198        NetworkRegistryError::Update(error)
199    }
200}
201
202#[derive(Clone, Debug, Error)]
203pub enum NetworkConversionError {
204    #[error("Could not convert id ({0}) to u32")]
205    InvalidInterfaceId(u64),
206}
207
208pub trait NetworkExt<I: fnet_interfaces_ext::FieldInterests> {
209    fn from_watcher_properties(
210        properties: &fnet_interfaces_ext::Properties<I>,
211    ) -> Result<Self, NetworkConversionError>
212    where
213        Self: Sized;
214}
215
216impl<I: fnet_interfaces_ext::FieldInterests> NetworkExt<I> for Network {
217    fn from_watcher_properties(
218        properties: &fnet_interfaces_ext::Properties<I>,
219    ) -> Result<Self, NetworkConversionError> {
220        // We expect interface ids to safely fit in the range of u32 values.
221        let network_id: u32 =
222            properties.id.get().try_into().or_else(|_| {
223                Err(NetworkConversionError::InvalidInterfaceId(properties.id.into()))
224            })?;
225        let network = Self {
226            network_id: Some(network_id),
227            info: Some(NetworkInfo::Fuchsia(FuchsiaNetworkInfo {
228                // No Fuchsia-specific information to provide.
229                ..Default::default()
230            })),
231            // DNS servers of Fuchsia networks are observable in netcfg already, so don't provide
232            // them to the Socketproxy. Socketproxy requires these fields to be provided so
233            // instantiate the v4 and v6 fields as empty vectors.
234            dns_servers: Some(fnp_socketproxy::NetworkDnsServers {
235                v4: Some(vec![]),
236                v6: Some(vec![]),
237                ..Default::default()
238            }),
239            ..Default::default()
240        };
241        Ok(network)
242    }
243}
244
245#[derive(Debug, Clone, PartialEq)]
246/// A generic version of a NetworkRegistry request with the responder removed.
247pub enum NetworkRegistryRequest {
248    /// Sets the default network.
249    ///
250    /// The network must have previously been registered by a call to `Add`.
251    SetDefault { network_id: Option<u32> },
252    /// Add a new network.
253    ///
254    /// This call will not return until the DNS servers have been successfully
255    /// updated in netcfg.
256    Add { network: Network },
257    /// Update a previously Added network.
258    ///
259    /// This call will not return until the DNS servers have been
260    /// successfully updated in netcfg.
261    Update { network: Network },
262    /// Remove a previously Added network.
263    ///
264    /// This call will not return until the DNS servers have been
265    /// successfully updated in netcfg.
266    Remove { network_id: u32 },
267}
268
269impl From<&fnp_socketproxy::NetworkRegistryRequest> for NetworkRegistryRequest {
270    fn from(value: &fnp_socketproxy::NetworkRegistryRequest) -> Self {
271        match *value {
272            fnp_socketproxy::NetworkRegistryRequest::SetDefault { network_id, responder: _ } => {
273                NetworkRegistryRequest::SetDefault {
274                    network_id: match network_id {
275                        fposix_socket::OptionalUint32::Value(v) => Some(v),
276                        fposix_socket::OptionalUint32::Unset(_) => None,
277                    },
278                }
279            }
280            fnp_socketproxy::NetworkRegistryRequest::Add { ref network, responder: _ } => {
281                NetworkRegistryRequest::Add { network: network.clone() }
282            }
283            fnp_socketproxy::NetworkRegistryRequest::Update { ref network, responder: _ } => {
284                NetworkRegistryRequest::Update { network: network.clone() }
285            }
286            fnp_socketproxy::NetworkRegistryRequest::Remove { network_id, responder: _ } => {
287                NetworkRegistryRequest::Remove { network_id }
288            }
289        }
290    }
291}
292impl From<&StarnixNetworksRequest> for NetworkRegistryRequest {
293    fn from(value: &StarnixNetworksRequest) -> Self {
294        match *value {
295            StarnixNetworksRequest::SetDefault { network_id, responder: _ } => {
296                NetworkRegistryRequest::SetDefault {
297                    network_id: match network_id {
298                        fposix_socket::OptionalUint32::Value(v) => Some(v),
299                        fposix_socket::OptionalUint32::Unset(_) => None,
300                    },
301                }
302            }
303            StarnixNetworksRequest::Add { ref network, responder: _ } => {
304                NetworkRegistryRequest::Add { network: network.clone() }
305            }
306            StarnixNetworksRequest::Update { ref network, responder: _ } => {
307                NetworkRegistryRequest::Update { network: network.clone() }
308            }
309            StarnixNetworksRequest::Remove { network_id, responder: _ } => {
310                NetworkRegistryRequest::Remove { network_id }
311            }
312        }
313    }
314}
315impl From<&FuchsiaNetworksRequest> for NetworkRegistryRequest {
316    fn from(value: &FuchsiaNetworksRequest) -> Self {
317        match *value {
318            FuchsiaNetworksRequest::SetDefault { network_id, responder: _ } => {
319                NetworkRegistryRequest::SetDefault {
320                    network_id: match network_id {
321                        fposix_socket::OptionalUint32::Value(v) => Some(v),
322                        fposix_socket::OptionalUint32::Unset(_) => None,
323                    },
324                }
325            }
326            FuchsiaNetworksRequest::Add { ref network, responder: _ } => {
327                NetworkRegistryRequest::Add { network: network.clone() }
328            }
329            FuchsiaNetworksRequest::Update { ref network, responder: _ } => {
330                NetworkRegistryRequest::Update { network: network.clone() }
331            }
332            FuchsiaNetworksRequest::Remove { network_id, responder: _ } => {
333                NetworkRegistryRequest::Remove { network_id }
334            }
335        }
336    }
337}
338
339/// A copy of fnp_socketproxy::Network that ensures that all fields are present.
340#[derive(Debug, Clone)]
341pub(crate) struct ValidatedNetwork {
342    network_id: u32,
343    info: NetworkInfo,
344    dns_servers: NetworkDnsServers,
345}
346
347impl ValidatedNetwork {
348    fn dns_servers(&self) -> Vec<fnet::SocketAddress> {
349        self.dns_servers
350            .v4
351            .iter()
352            .flat_map(|a| a.iter().map(IpAddressExt::to_dns_socket_address))
353            .chain(
354                self.dns_servers
355                    .v6
356                    .iter()
357                    .flat_map(|a| a.iter().map(IpAddressExt::to_dns_socket_address)),
358            )
359            .collect()
360    }
361}
362
363trait ValidateNetworkExt {
364    fn validate(self) -> Result<ValidatedNetwork, CommonErrors>;
365}
366
367impl ValidateNetworkExt for Network {
368    fn validate(self) -> Result<ValidatedNetwork, CommonErrors> {
369        match self {
370            Network { network_id: None, .. } => Err(CommonErrors::MissingNetworkId),
371            Network { info: None, .. } => Err(CommonErrors::MissingNetworkInfo),
372            Network { dns_servers: None, .. } => Err(CommonErrors::MissingNetworkDnsServers),
373            Network {
374                network_id: Some(network_id),
375                info: Some(info),
376                dns_servers: Some(dns_servers),
377                ..
378            } => Ok(ValidatedNetwork { network_id, info, dns_servers }),
379        }
380    }
381}
382
383macro_rules! common_errors_impl {
384    ($($p:ty),+) => {
385        $(
386            impl From<CommonErrors> for $p {
387                fn from(value: CommonErrors) -> Self {
388                    use CommonErrors::*;
389                    match value {
390                        MissingNetworkId => <$p>::MissingNetworkId,
391                        MissingNetworkInfo => <$p>::MissingNetworkInfo,
392                        MissingNetworkDnsServers => <$p>::MissingNetworkDnsServers,
393                    }
394                }
395            }
396        )+
397    }
398}
399
400common_errors_impl!(
401    fnp_socketproxy::NetworkRegistryAddError,
402    fnp_socketproxy::NetworkRegistryUpdateError
403);
404
405/// NetworkRegistry tracks the networks that have been registered.
406#[derive(Inspect, Debug, Default)]
407struct NetworkRegistry {
408    networks: IValue<RegisteredNetworks>,
409
410    inspect_node: fuchsia_inspect::Node,
411}
412
413impl NetworkRegistry {
414    /// Returns a collated list of DnsServerList objects.
415    pub(crate) fn dns_servers(&self) -> Vec<fnp_socketproxy::DnsServerList> {
416        self.networks.dns_servers()
417    }
418
419    /// Returns whether the network registry has a default network set.
420    pub(crate) fn has_default_network(&self) -> bool {
421        self.networks.default_network_id.is_some()
422    }
423
424    /// Returns current socket mark for the default network.
425    pub(crate) fn current_mark(&self) -> Option<u32> {
426        self.networks.current_mark()
427    }
428}
429
430#[derive(Unit, Debug, Default)]
431struct MethodInspect {
432    successes: u32,
433    errors: u32,
434}
435
436#[derive(Unit, Default, Debug)]
437struct RegisteredNetworks {
438    default_network_id: Option<u32>,
439
440    #[inspect(skip)]
441    /// A mapping from network id to ValidatedNetwork for each registered network.
442    networks: HashMap<u32, ValidatedNetwork>,
443
444    adds: MethodInspect,
445    removes: MethodInspect,
446    set_defaults: MethodInspect,
447    updates: MethodInspect,
448}
449
450impl RegisteredNetworks {
451    fn add_network(&mut self, network: Network) -> fnp_socketproxy::NetworkRegistryAddResult {
452        let network = network.validate()?;
453        #[allow(clippy::map_entry, reason = "mass allow for https://fxbug.dev/381896734")]
454        if self.networks.contains_key(&network.network_id) {
455            self.adds.errors += 1;
456            Err(fnp_socketproxy::NetworkRegistryAddError::DuplicateNetworkId)
457        } else {
458            let _: Option<_> = self.networks.insert(network.network_id, network);
459            self.adds.successes += 1;
460            Ok(())
461        }
462    }
463
464    /// Empties the registered networks.
465    pub(crate) fn clear(&mut self) {
466        self.default_network_id = None;
467        self.networks.clear();
468    }
469
470    fn update_network(&mut self, network: Network) -> fnp_socketproxy::NetworkRegistryUpdateResult {
471        let network = network.validate()?;
472        let network_id = network.network_id;
473        *self
474            .networks
475            .get_mut(&network_id)
476            .ok_or(fnp_socketproxy::NetworkRegistryUpdateError::NotFound)
477            .inspect(|_| self.updates.successes += 1)
478            .inspect_err(|_| self.updates.errors += 1)? = network;
479        Ok(())
480    }
481
482    fn remove_network(&mut self, network_id: u32) -> fnp_socketproxy::NetworkRegistryRemoveResult {
483        if self.default_network_id == Some(network_id) {
484            self.removes.errors += 1;
485            return Err(fnp_socketproxy::NetworkRegistryRemoveError::CannotRemoveDefaultNetwork);
486        }
487        match self.networks.remove(&network_id) {
488            Some(_) => {
489                self.removes.successes += 1;
490                Ok(())
491            }
492            None => {
493                self.removes.errors += 1;
494                Err(fnp_socketproxy::NetworkRegistryRemoveError::NotFound)
495            }
496        }
497    }
498
499    /// Update the currently set default network id.
500    ///
501    /// If `network_id` is None, the default network id will be unset.
502    fn set_default_network(
503        &mut self,
504        network_id: Option<u32>,
505    ) -> fnp_socketproxy::NetworkRegistrySetDefaultResult {
506        if let Some(network_id) = network_id {
507            if !self.networks.contains_key(&network_id) {
508                self.set_defaults.errors += 1;
509                return Err(fnp_socketproxy::NetworkRegistrySetDefaultError::NotFound);
510            }
511        }
512        self.set_defaults.successes += 1;
513        self.default_network_id = network_id;
514
515        Ok(())
516    }
517
518    /// Returns a collated list of DnsServerList objects.
519    pub(crate) fn dns_servers(&self) -> Vec<fnp_socketproxy::DnsServerList> {
520        self.networks
521            .iter()
522            .map(|(id, network)| fnp_socketproxy::DnsServerList {
523                source_network_id: Some(*id),
524                addresses: Some(network.dns_servers()),
525                ..Default::default()
526            })
527            .collect()
528    }
529
530    fn current_mark(&self) -> Option<u32> {
531        match (self.default_network_id, self.networks.is_empty()) {
532            (None, false) => Some(DEFAULT_SOCKET_MARK),
533            (id, _) => id.and_then(|id| self.networks[&id].info.mark()),
534        }
535    }
536
537    fn len(&self) -> usize {
538        self.networks.len()
539    }
540}
541
542#[derive(Inspect, Clone, Debug, Default)]
543pub struct NetworkRegistries {
544    starnix: Arc<Mutex<NetworkRegistry>>,
545    fuchsia: Arc<Mutex<NetworkRegistry>>,
546}
547
548impl NetworkRegistries {
549    // When Fuchsia has a default network, then prefer its mark
550    // over any existing mark. When it is unset, then fallback
551    // to the mark provided by Starnix.
552    async fn current_mark(&self) -> Option<u32> {
553        {
554            let fuchsia = self.fuchsia.lock().await;
555            if fuchsia.has_default_network() {
556                return fuchsia.current_mark();
557            }
558        }
559
560        return self.starnix.lock().await.networks.current_mark();
561    }
562
563    // When Fuchsia has a default network, then prefer its DNS
564    // over any existing DNS servers. When it is unset, then
565    // fallback to the DNS servers provided by Starnix.
566    async fn current_dns_servers(&self) -> Vec<fnp_socketproxy::DnsServerList> {
567        {
568            let fuchsia = self.fuchsia.lock().await;
569            if fuchsia.has_default_network() {
570                return fuchsia.dns_servers();
571            }
572        }
573
574        return self.starnix.lock().await.dns_servers();
575    }
576}
577
578#[derive(Debug)]
579enum RegistryType {
580    Starnix,
581    Fuchsia,
582}
583
584#[derive(Inspect, Debug)]
585pub struct Registry {
586    #[inspect(forward)]
587    networks: NetworkRegistries,
588    // Reflects the marks that are set on the sockets vended
589    // by this component.
590    marks: Arc<Mutex<crate::SocketMarks>>,
591    dns_tx: mpsc::Sender<Vec<fnp_socketproxy::DnsServerList>>,
592    forwarder_tx: mpsc::Sender<NetworkRegistryRequest>,
593    starnix_occupant: Mutex<()>,
594    fuchsia_occupant: Mutex<()>,
595}
596
597macro_rules! handle_registry_request {
598    ($request_type:ident, $request:expr, $network_registry:expr, $registry_type:expr) => {{
599        let mut networks = $network_registry.networks.as_mut();
600        let (op, send): (_, Box<dyn FnOnce() -> Result<(), _> + Send + Sync + 'static>) =
601            match $request {
602                $request_type::SetDefault { network_id, responder } => {
603                    let result = networks.set_default_network(match network_id {
604                        fposix_socket::OptionalUint32::Value(value) => Some(value),
605                        fposix_socket::OptionalUint32::Unset(_) => None,
606                    });
607                    ("set default", Box::new(move || responder.send(result)))
608                }
609                $request_type::Add { network, responder } => {
610                    let result = networks.add_network(network);
611                    ("add", Box::new(move || responder.send(result)))
612                }
613                $request_type::Update { network, responder } => {
614                    let result = networks.update_network(network);
615                    ("update", Box::new(move || responder.send(result)))
616                }
617                $request_type::Remove { network_id, responder } => {
618                    let result = networks.remove_network(network_id);
619                    ("remove", Box::new(move || responder.send(result)))
620                }
621            };
622        let new_mark = networks.current_mark();
623        info!(
624            "{:?} registry {op}. mark: {new_mark:?}, networks count: {}",
625            $registry_type,
626            networks.len()
627        );
628        std::mem::drop(networks);
629        send
630    }};
631}
632
633impl Registry {
634    pub(crate) fn new(
635        marks: Arc<Mutex<crate::SocketMarks>>,
636        dns_tx: mpsc::Sender<Vec<fnp_socketproxy::DnsServerList>>,
637        forwarder_tx: mpsc::Sender<NetworkRegistryRequest>,
638    ) -> Result<Self, anyhow::Error> {
639        Ok(Self {
640            networks: Default::default(),
641            marks,
642            dns_tx,
643            forwarder_tx,
644            starnix_occupant: Default::default(),
645            fuchsia_occupant: Default::default(),
646        })
647    }
648}
649
650impl Registry {
651    pub(crate) async fn run_starnix(
652        &self,
653        stream: fnp_socketproxy::StarnixNetworksRequestStream,
654    ) -> Result<(), Error> {
655        let _occupant = match self.starnix_occupant.try_lock() {
656            Some(o) => o,
657            None => {
658                warn!("Only one connection to StarnixNetworks is allowed at a time");
659                stream.control_handle().shutdown_with_epitaph(fidl::Status::ACCESS_DENIED);
660                return Ok(());
661            }
662        };
663
664        info!("Starting fuchsia.net.policy.socketproxy.StarnixNetworks server");
665        self.networks.starnix.lock().await.networks.as_mut().clear();
666        stream
667            .map(|result| result.context("failed request"))
668            .try_for_each(|request| {
669                async {
670                    self.forwarder_tx.clone().feed((&request).into()).await.unwrap_or_else(
671                        |e| {
672                            if !e.is_disconnected() {
673                                // Log if the feed fails for reasons other than disconnection.
674                                error!("Unable to feed request forward: {e:?}")
675                            }
676                        },
677                    );
678                    let mut network_registry = self.networks.starnix.lock().await;
679                    let send: Box<dyn FnOnce() -> Result<(), _> + Send + Sync + 'static> =
680                        handle_registry_request!(
681                            StarnixNetworksRequest,
682                            request,
683                            network_registry,
684                            RegistryType::Starnix
685                        );
686                    std::mem::drop(network_registry);
687
688                    self.handle_state_changed().await?;
689                    send().context("error sending response")?;
690                    Ok(())
691                }
692            })
693            .await
694    }
695
696    pub(crate) async fn run_fuchsia(
697        &self,
698        stream: fnp_socketproxy::FuchsiaNetworksRequestStream,
699    ) -> Result<(), Error> {
700        let _occupant = match self.fuchsia_occupant.try_lock() {
701            Some(o) => o,
702            None => {
703                warn!("Only one connection to FuchsiaNetworks is allowed at a time");
704                stream.control_handle().shutdown_with_epitaph(fidl::Status::ACCESS_DENIED);
705                return Ok(());
706            }
707        };
708
709        info!("Starting fuchsia.net.policy.socketproxy.FuchsiaNetworks server");
710        self.networks.fuchsia.lock().await.networks.as_mut().clear();
711        stream
712            .map(|result| result.context("failed request"))
713            .try_for_each(|request| {
714                async {
715                    let mut network_registry = self.networks.fuchsia.lock().await;
716                    let send: Box<dyn FnOnce() -> Result<(), _> + Send + Sync + 'static> =
717                        handle_registry_request!(
718                            FuchsiaNetworksRequest,
719                            request,
720                            network_registry,
721                            RegistryType::Fuchsia
722                        );
723                    std::mem::drop(network_registry);
724
725                    self.handle_state_changed().await?;
726                    send().context("error sending response")?;
727                    Ok(())
728                }
729            })
730            .await
731    }
732
733    async fn handle_state_changed(&self) -> Result<(), Error> {
734        // We do feed here instead of send so that we don't wait for a flush
735        // in the event that the DnsServerWatcher is not running.
736        self.dns_tx.clone().feed(self.networks.current_dns_servers().await).await.unwrap_or_else(
737            |e| {
738                if !e.is_disconnected() {
739                    // Log if the feed fails for reasons other than disconnection.
740                    error!("Unable to feed DNS update: {e:?}")
741                }
742            },
743        );
744
745        // Ensure the mark is updated prior to sending out the response
746        // and dropping the registry.
747        let mark = self.networks.current_mark().await;
748        self.marks.lock().await.set_mark(fnet::MARK_DOMAIN_SO_MARK, mark);
749        Ok(())
750    }
751}
752
753#[cfg(test)]
754mod test {
755    use super::*;
756    use fuchsia_component::server::ServiceFs;
757    use fuchsia_component_test::{
758        Capability, ChildOptions, LocalComponentHandles, RealmBuilder, RealmInstance, Ref, Route,
759    };
760    use futures::channel::mpsc::Receiver;
761    use futures::future;
762    use net_declare::{fidl_ip, fidl_socket_addr};
763    use pretty_assertions::assert_eq;
764    use socket_proxy_testing::{RegistryType, ToDnsServerList as _, ToNetwork};
765    use test_case::test_case;
766
767    #[derive(Clone, Debug)]
768    enum Op<N: ToNetwork> {
769        SetDefault {
770            network_id: Option<u32>,
771            result: Result<(), fnp_socketproxy::NetworkRegistrySetDefaultError>,
772        },
773        Add {
774            network: N,
775            result: Result<(), fnp_socketproxy::NetworkRegistryAddError>,
776        },
777        Update {
778            network: N,
779            result: Result<(), fnp_socketproxy::NetworkRegistryUpdateError>,
780        },
781        Remove {
782            network_id: u32,
783            result: Result<(), fnp_socketproxy::NetworkRegistryRemoveError>,
784        },
785    }
786
787    impl<N: ToNetwork + Clone> From<&Op<N>> for NetworkRegistryRequest {
788        fn from(value: &Op<N>) -> Self {
789            match value {
790                Op::SetDefault { network_id, result: _ } => {
791                    NetworkRegistryRequest::SetDefault { network_id: *network_id }
792                }
793                Op::Add { network, result: _ } => NetworkRegistryRequest::Add {
794                    network: network.clone().to_network(RegistryType::Starnix),
795                },
796                Op::Update { network, result: _ } => NetworkRegistryRequest::Update {
797                    network: network.clone().to_network(RegistryType::Starnix),
798                },
799                Op::Remove { network_id, result: _ } => {
800                    NetworkRegistryRequest::Remove { network_id: *network_id }
801                }
802            }
803        }
804    }
805
806    macro_rules! execute {
807        ($self:ident, $proxy:ident, $registry:expr) => {{
808            match $self {
809                Op::SetDefault { network_id, result } => {
810                    assert_eq!(
811                        $proxy
812                            .set_default(&match network_id {
813                                Some(value) => fposix_socket::OptionalUint32::Value(*value),
814                                None => fposix_socket::OptionalUint32::Unset(fposix_socket::Empty),
815                            })
816                            .await?,
817                        *result
818                    )
819                }
820                Op::Add { network, result } => {
821                    assert_eq!($proxy.add(&network.to_network($registry)).await?, *result)
822                }
823                Op::Update { network, result } => {
824                    assert_eq!($proxy.update(&network.to_network($registry)).await?, *result)
825                }
826                Op::Remove { network_id, result } => {
827                    assert_eq!($proxy.remove(*network_id).await?, *result)
828                }
829            }
830            Ok(())
831        }};
832    }
833
834    impl<N: ToNetwork + Clone> Op<N> {
835        async fn execute_starnix(
836            &self,
837            starnix: &fnp_socketproxy::StarnixNetworksProxy,
838        ) -> Result<(), Error> {
839            execute!(self, starnix, RegistryType::Starnix)
840        }
841
842        async fn execute_fuchsia(
843            &self,
844            fuchsia: &fnp_socketproxy::FuchsiaNetworksProxy,
845        ) -> Result<(), Error> {
846            execute!(self, fuchsia, RegistryType::Fuchsia)
847        }
848
849        fn is_err(&self) -> bool {
850            match &self {
851                Op::SetDefault { network_id: _, result } => result.is_err(),
852                Op::Add { network: _, result } => result.is_err(),
853                Op::Update { network: _, result } => result.is_err(),
854                Op::Remove { network_id: _, result } => result.is_err(),
855            }
856        }
857    }
858
859    enum IncomingService {
860        StarnixNetworks(fnp_socketproxy::StarnixNetworksRequestStream),
861        FuchsiaNetworks(fnp_socketproxy::FuchsiaNetworksRequestStream),
862    }
863
864    async fn run_registry(
865        handles: LocalComponentHandles,
866        starnix_networks: Arc<Mutex<NetworkRegistry>>,
867        fuchsia_networks: Arc<Mutex<NetworkRegistry>>,
868        marks: Arc<Mutex<crate::SocketMarks>>,
869        dns_tx: mpsc::Sender<Vec<fnp_socketproxy::DnsServerList>>,
870        forwarder_tx: mpsc::Sender<NetworkRegistryRequest>,
871    ) -> Result<(), Error> {
872        let mut fs = ServiceFs::new();
873        let _ = fs
874            .dir("svc")
875            .add_fidl_service(IncomingService::StarnixNetworks)
876            .add_fidl_service(IncomingService::FuchsiaNetworks);
877        let _ = fs.serve_connection(handles.outgoing_dir)?;
878
879        let registry = Registry {
880            networks: NetworkRegistries { starnix: starnix_networks, fuchsia: fuchsia_networks },
881            marks,
882            dns_tx,
883            forwarder_tx,
884            starnix_occupant: Default::default(),
885            fuchsia_occupant: Default::default(),
886        };
887
888        fs.for_each_concurrent(0, |service| async {
889            match service {
890                IncomingService::StarnixNetworks(stream) => registry.run_starnix(stream).await,
891                IncomingService::FuchsiaNetworks(stream) => registry.run_fuchsia(stream).await,
892            }
893            .unwrap_or_else(|e| error!("{e:?}"))
894        })
895        .await;
896
897        Ok(())
898    }
899
900    async fn setup_test() -> Result<
901        (
902            RealmInstance,
903            Receiver<Vec<fnp_socketproxy::DnsServerList>>,
904            Receiver<NetworkRegistryRequest>,
905        ),
906        Error,
907    > {
908        let builder = RealmBuilder::new().await?;
909        let starnix_networks = Arc::new(Mutex::new(Default::default()));
910        let fuchsia_networks = Arc::new(Mutex::new(Default::default()));
911        let (dns_tx, dns_rx) = mpsc::channel(1);
912        let (forwarder_tx, forwarder_rx) = mpsc::channel(1);
913        let marks = Arc::new(Mutex::new(crate::SocketMarks::default()));
914        let registry = builder
915            .add_local_child(
916                "registry",
917                {
918                    let starnix_networks = starnix_networks.clone();
919                    let fuchsia_networks = fuchsia_networks.clone();
920                    let marks = marks.clone();
921                    let dns_tx = dns_tx.clone();
922                    move |handles: LocalComponentHandles| {
923                        Box::pin(run_registry(
924                            handles,
925                            starnix_networks.clone(),
926                            fuchsia_networks.clone(),
927                            marks.clone(),
928                            dns_tx.clone(),
929                            forwarder_tx.clone(),
930                        ))
931                    }
932                },
933                ChildOptions::new(),
934            )
935            .await?;
936
937        builder
938            .add_route(
939                Route::new()
940                    .capability(Capability::protocol::<fnp_socketproxy::StarnixNetworksMarker>())
941                    .from(&registry)
942                    .to(Ref::parent()),
943            )
944            .await?;
945
946        builder
947            .add_route(
948                Route::new()
949                    .capability(Capability::protocol::<fnp_socketproxy::FuchsiaNetworksMarker>())
950                    .from(&registry)
951                    .to(Ref::parent()),
952            )
953            .await?;
954
955        let realm = builder.build().await?;
956
957        Ok((realm, dns_rx, forwarder_rx))
958    }
959
960    #[test_case(&[
961        Op::Add { network: 1, result: Ok(()) },
962        Op::Update { network: 1, result: Ok(()) },
963        Op::Remove { network_id: 1, result: Ok(()) },
964    ]; "normal operation")]
965    #[test_case(&[
966        Op::Add { network: 1, result: Ok(()) },
967        Op::Add { network: 1, result: Err(fnp_socketproxy::NetworkRegistryAddError::DuplicateNetworkId) },
968    ]; "duplicate add")]
969    #[test_case(&[
970        Op::Update { network: 1, result: Err(fnp_socketproxy::NetworkRegistryUpdateError::NotFound) },
971    ]; "update missing")]
972    #[test_case(&[
973        Op::<u32>::Remove { network_id: 1, result: Err(fnp_socketproxy::NetworkRegistryRemoveError::NotFound) },
974    ]; "remove missing")]
975    #[test_case(&[
976        Op::<u32>::SetDefault { network_id: Some(1), result: Err(fnp_socketproxy::NetworkRegistrySetDefaultError::NotFound) },
977    ]; "set default missing")]
978    #[test_case(&[
979        Op::Add { network: 1, result: Ok(()) },
980        Op::SetDefault { network_id: Some(1), result: Ok(()) },
981        Op::Remove { network_id: 1, result: Err(fnp_socketproxy::NetworkRegistryRemoveError::CannotRemoveDefaultNetwork)},
982    ]; "remove default network")]
983    #[test_case(&[
984        Op::Add { network: 1, result: Ok(()) },
985        Op::SetDefault { network_id: Some(1), result: Ok(()) },
986        Op::Remove { network_id: 1, result: Err(fnp_socketproxy::NetworkRegistryRemoveError::CannotRemoveDefaultNetwork)},
987        Op::Add { network: 2, result: Ok(()) },
988        Op::SetDefault { network_id: Some(2), result: Ok(()) },
989        Op::Remove { network_id: 1, result: Ok(()) },
990    ]; "remove formerly default network")]
991    #[test_case(&[
992        Op::Add { network: 1, result: Ok(()) },
993        Op::SetDefault { network_id: Some(1), result: Ok(()) },
994        Op::Remove { network_id: 1, result: Err(fnp_socketproxy::NetworkRegistryRemoveError::CannotRemoveDefaultNetwork)},
995        Op::SetDefault { network_id: None, result: Ok(()) },
996        Op::Remove { network_id: 1, result: Ok(()) },
997    ]; "remove last network")]
998    #[test_case(&[
999        Op::Add { network: 1, result: Ok(()) },
1000        Op::Update { network: 1, result: Ok(()) },
1001        Op::Add { network: 2, result: Ok(()) },
1002        Op::Add { network: 3, result: Ok(()) },
1003        Op::Add { network: 4, result: Ok(()) },
1004        Op::Update { network: 4, result: Ok(()) },
1005        Op::Update { network: 2, result: Ok(()) },
1006        Op::Update { network: 3, result: Ok(()) },
1007        Op::Add { network: 5, result: Ok(()) },
1008        Op::Update { network: 5, result: Ok(()) },
1009        Op::Add { network: 6, result: Ok(()) },
1010        Op::Add { network: 7, result: Ok(()) },
1011        Op::Add { network: 8, result: Ok(()) },
1012        Op::Update { network: 8, result: Ok(()) },
1013        Op::Update { network: 6, result: Ok(()) },
1014        Op::Add { network: 9, result: Ok(()) },
1015        Op::Update { network: 9, result: Ok(()) },
1016        Op::Update { network: 7, result: Ok(()) },
1017        Op::Add { network: 10, result: Ok(()) },
1018        Op::Update { network: 10, result: Ok(()) },
1019    ]; "many updates")]
1020    #[fuchsia::test]
1021    async fn test_operations<N: ToNetwork + Clone>(operations: &[Op<N>]) -> Result<(), Error> {
1022        let (realm, _, _) = setup_test().await?;
1023        let starnix_networks = realm
1024            .root
1025            .connect_to_protocol_at_exposed_dir()
1026            .context("While connecting to StarnixNetworks")?;
1027        let fuchsia_networks = realm
1028            .root
1029            .connect_to_protocol_at_exposed_dir()
1030            .context("While connecting to FuchsiaNetworks")?;
1031
1032        for op in operations {
1033            // Demonstrate that the same operations can be applied
1034            // independently in both registries.
1035            op.execute_starnix(&starnix_networks).await?;
1036            op.execute_fuchsia(&fuchsia_networks).await?;
1037        }
1038
1039        Ok(())
1040    }
1041
1042    #[test_case(&[
1043        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) },
1044    ], vec![(1, vec![fidl_socket_addr!("192.0.2.0:53")]).to_dns_server_list()]
1045    ; "normal operation (v4)")]
1046    #[test_case(&[
1047        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) },
1048        Op::Update { network: (1, vec![fidl_ip!("192.0.2.1")]), result: Ok(()) },
1049    ], vec![(1, vec![fidl_socket_addr!("192.0.2.1:53")]).to_dns_server_list()]
1050    ; "update server list (v4)")]
1051    #[test_case(&[
1052        Op::Add { network: (1, vec![fidl_ip!("2001:db8::1")]), result: Ok(()) },
1053    ], vec![(1, vec![fidl_socket_addr!("[2001:db8::1]:53")]).to_dns_server_list()]
1054    ; "normal operation (v6)")]
1055    #[test_case(&[
1056        Op::Add { network: (1, vec![fidl_ip!("2001:db8::1")]), result: Ok(()) },
1057        Op::Update { network: (1, vec![fidl_ip!("2001:db8::2")]), result: Ok(()) },
1058    ], vec![(1, vec![fidl_socket_addr!("[2001:db8::2]:53")]).to_dns_server_list()]
1059    ; "update server list (v6)")]
1060    #[test_case(&[
1061        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0"), fidl_ip!("2001:db8::1")]), result: Ok(()) },
1062    ], vec![(1, vec![fidl_socket_addr!("192.0.2.0:53"), fidl_socket_addr!("[2001:db8::1]:53")]).to_dns_server_list()]
1063    ; "normal operation (mixed)")]
1064    #[test_case(&[
1065        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0"), fidl_ip!("2001:db8::1")]), result: Ok(()) },
1066        Op::Update { network: (1, vec![fidl_ip!("192.0.2.1"), fidl_ip!("2001:db8::2")]), result: Ok(()) },
1067    ], vec![(1, vec![fidl_socket_addr!("192.0.2.1:53"), fidl_socket_addr!("[2001:db8::2]:53")]).to_dns_server_list()]
1068    ; "update server list (mixed)")]
1069    #[test_case(&[
1070        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0"), fidl_ip!("2001:db8::1")]), result: Ok(()) },
1071        Op::Add { network: (2, vec![fidl_ip!("192.0.2.1"), fidl_ip!("2001:db8::2")]), result: Ok(()) },
1072        Op::Add { network: (3, vec![fidl_ip!("192.0.2.2"), fidl_ip!("2001:db8::3")]), result: Ok(()) },
1073    ], vec![
1074        (1, vec![fidl_socket_addr!("192.0.2.0:53"), fidl_socket_addr!("[2001:db8::1]:53")]).to_dns_server_list(),
1075        (2, vec![fidl_socket_addr!("192.0.2.1:53"), fidl_socket_addr!("[2001:db8::2]:53")]).to_dns_server_list(),
1076        (3, vec![fidl_socket_addr!("192.0.2.2:53"), fidl_socket_addr!("[2001:db8::3]:53")]).to_dns_server_list(),
1077    ]; "multiple networks")]
1078    #[fuchsia::test]
1079    async fn test_dns_tracking<N: ToNetwork + Clone>(
1080        operations: &[Op<N>],
1081        dns_servers: Vec<fnp_socketproxy::DnsServerList>,
1082    ) -> Result<(), Error> {
1083        let (realm, mut dns_rx, _) = setup_test().await?;
1084        let starnix_networks = realm
1085            .root
1086            .connect_to_protocol_at_exposed_dir()
1087            .context("While connecting to StarnixNetworks")?;
1088
1089        let mut last_dns = None;
1090        for op in operations {
1091            // Starnix and Fuchsia registries have the same handling logic, so
1092            // use the Starnix registry to confirm this behavior.
1093            op.execute_starnix(&starnix_networks).await?;
1094            last_dns = Some(dns_rx.next().await.expect("dns update expected after each operation"));
1095        }
1096
1097        let mut last_dns = last_dns.expect("there should be at least one dns update");
1098        last_dns.sort_by_key(|a| a.source_network_id);
1099        assert_eq!(last_dns, dns_servers);
1100
1101        Ok(())
1102    }
1103
1104    #[test_case(&[
1105        (RegistryType::Fuchsia, Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) }),
1106        (RegistryType::Fuchsia, Op::SetDefault { network_id: Some(1), result: Ok(()) }),
1107    ], vec![(1, vec![fidl_socket_addr!("192.0.2.0:53")]).to_dns_server_list()]
1108    ; "normal operation Fuchsia (v4)")]
1109    #[test_case(&[
1110        (RegistryType::Fuchsia, Op::Add { network: (1, vec![fidl_ip!("2001:db8::1")]), result: Ok(()) }),
1111        (RegistryType::Fuchsia, Op::SetDefault { network_id: Some(1), result: Ok(()) }),
1112    ], vec![(1, vec![fidl_socket_addr!("[2001:db8::1]:53")]).to_dns_server_list()]
1113    ; "normal operation Fuchsia (v6)")]
1114    #[test_case(&[
1115        (RegistryType::Starnix, Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) }),
1116        (RegistryType::Fuchsia, Op::Remove { network_id: 1, result: Err(fnp_socketproxy::NetworkRegistryRemoveError::NotFound) }),
1117    ], vec![(1, vec![fidl_socket_addr!("192.0.2.0:53")]).to_dns_server_list()]
1118    ; "attempt remove in wrong registry")]
1119    #[test_case(&[
1120        (RegistryType::Starnix, Op::Add { network: (1, vec![fidl_ip!("192.0.2.0"), fidl_ip!("2001:db8::1")]), result: Ok(()) }),
1121        (RegistryType::Fuchsia, Op::Add { network: (2, vec![fidl_ip!("192.0.2.1"), fidl_ip!("2001:db8::2")]), result: Ok(()) }),
1122    ], vec![
1123        (1, vec![fidl_socket_addr!("192.0.2.0:53"), fidl_socket_addr!("[2001:db8::1]:53")]).to_dns_server_list(),
1124    ]; "Fuchsia default absent, use Starnix")]
1125    #[test_case(&[
1126        (RegistryType::Starnix, Op::Add { network: (1, vec![fidl_ip!("192.0.2.0"), fidl_ip!("2001:db8::1")]), result: Ok(()) }),
1127        (RegistryType::Fuchsia, Op::Add { network: (2, vec![fidl_ip!("192.0.2.1"), fidl_ip!("2001:db8::2")]), result: Ok(()) }),
1128        (RegistryType::Fuchsia, Op::SetDefault { network_id: Some(2), result: Ok(()) }),
1129        ], vec![
1130        (2, vec![fidl_socket_addr!("192.0.2.1:53"), fidl_socket_addr!("[2001:db8::2]:53")]).to_dns_server_list(),
1131    ]; "Fuchsia default present, use Fuchsia")]
1132    #[test_case(&[
1133        (RegistryType::Starnix, Op::Add { network: (1, vec![fidl_ip!("192.0.2.0"), fidl_ip!("2001:db8::1")]), result: Ok(()) }),
1134        (RegistryType::Fuchsia, Op::Add { network: (2, vec![fidl_ip!("192.0.2.1"), fidl_ip!("2001:db8::2")]), result: Ok(()) }),
1135        (RegistryType::Fuchsia, Op::SetDefault { network_id: Some(2), result: Ok(()) }),
1136        (RegistryType::Fuchsia, Op::SetDefault { network_id: None, result: Ok(()) }),
1137        ], vec![
1138        (1, vec![fidl_socket_addr!("192.0.2.0:53"), fidl_socket_addr!("[2001:db8::1]:53")]).to_dns_server_list(),
1139    ]; "Fallback to Starnix network")]
1140    #[test_case(&[
1141        (RegistryType::Starnix, Op::Add { network: (1, vec![fidl_ip!("192.0.2.0"), fidl_ip!("2001:db8::1")]), result: Ok(()) }),
1142        (RegistryType::Fuchsia, Op::Add { network: (2, vec![fidl_ip!("192.0.2.1"), fidl_ip!("2001:db8::2")]), result: Ok(()) }),
1143        (RegistryType::Fuchsia, Op::SetDefault { network_id: Some(2), result: Ok(()) }),
1144        (RegistryType::Fuchsia, Op::Update { network: (2, vec![fidl_ip!("192.0.2.2"), fidl_ip!("2001:db8::3")]), result: Ok(()) }),
1145        ], vec![
1146        (2, vec![fidl_socket_addr!("192.0.2.2:53"), fidl_socket_addr!("[2001:db8::3]:53")]).to_dns_server_list(),
1147    ]; "Fuchsia default present then updated")]
1148    #[fuchsia::test]
1149    async fn test_dns_tracking_across_registries<N: ToNetwork + Clone>(
1150        operations: &[(RegistryType, Op<N>)],
1151        dns_servers: Vec<fnp_socketproxy::DnsServerList>,
1152    ) -> Result<(), Error> {
1153        let (realm, mut dns_rx, _) = setup_test().await?;
1154        let starnix_networks = realm
1155            .root
1156            .connect_to_protocol_at_exposed_dir()
1157            .context("While connecting to StarnixNetworks")?;
1158        let fuchsia_networks = realm
1159            .root
1160            .connect_to_protocol_at_exposed_dir()
1161            .context("While connecting to FuchsiaNetworks")?;
1162
1163        let mut last_dns = None;
1164        for (registry, op) in operations {
1165            match registry {
1166                RegistryType::Starnix => {
1167                    op.execute_starnix(&starnix_networks).await?;
1168                }
1169                RegistryType::Fuchsia => {
1170                    op.execute_fuchsia(&fuchsia_networks).await?;
1171                }
1172            }
1173            // When the operation results in an error, we don't expect that to
1174            // result in an additional DNS update.
1175            if !op.is_err() {
1176                last_dns =
1177                    Some(dns_rx.next().await.expect("dns update expected after each operation"));
1178            }
1179        }
1180
1181        let mut last_dns = last_dns.expect("there should be at least one dns update");
1182        last_dns.sort_by_key(|a| a.source_network_id);
1183        assert_eq!(last_dns, dns_servers);
1184
1185        Ok(())
1186    }
1187
1188    #[test_case(&[
1189        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) },
1190    ]
1191    ; "Add but no default")]
1192    #[test_case(&[
1193        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) },
1194        Op::SetDefault { network_id: Some(1), result: Ok(()) },
1195    ]
1196    ; "Add and set default")]
1197    #[test_case(&[
1198        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) },
1199        Op::Add { network: (2, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) },
1200        Op::SetDefault { network_id: Some(1), result: Ok(()) },
1201        Op::SetDefault { network_id: Some(2), result: Ok(()) },
1202    ]
1203    ; "Add two and set default")]
1204    #[test_case(&[
1205        Op::Add { network: (1, vec![fidl_ip!("192.0.2.0")]), result: Ok(()) },
1206        Op::SetDefault { network_id: Some(1), result: Ok(()) },
1207        Op::SetDefault { network_id: None, result: Ok(()) },
1208        Op::Remove { network_id: 1, result: Ok(()) },
1209    ]
1210    ; "Add default and delete")]
1211    #[fuchsia::test]
1212    async fn test_forward_network_update<N: ToNetwork + Clone + std::fmt::Debug>(
1213        operations: &[Op<N>],
1214    ) -> Result<(), Error> {
1215        let (realm, _, mut forwarder_rx) = setup_test().await?;
1216        let starnix_networks = realm
1217            .root
1218            .connect_to_protocol_at_exposed_dir()
1219            .context("While connecting to StarnixNetworks")?;
1220
1221        let (_, seen_updates) = future::join(
1222            async move {
1223                for op in operations {
1224                    op.execute_starnix(&starnix_networks).await?;
1225                }
1226                std::mem::drop(realm);
1227                Ok::<(), anyhow::Error>(())
1228            },
1229            async move {
1230                let mut forwarded_requests = Vec::new();
1231                while let Some(req) = forwarder_rx.next().await {
1232                    forwarded_requests.push(req);
1233                }
1234                forwarded_requests
1235            },
1236        )
1237        .await;
1238
1239        let expected_updates =
1240            operations.iter().map(NetworkRegistryRequest::from).collect::<Vec<_>>();
1241        assert_eq!(expected_updates, seen_updates);
1242
1243        Ok(())
1244    }
1245}