netcfg/
socketproxy.rs

1// Copyright 2025 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
5use {
6    fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext,
7    fidl_fuchsia_net_policy_socketproxy as fnp_socketproxy,
8    fidl_fuchsia_posix_socket as fposix_socket,
9};
10
11use socket_proxy::{NetworkConversionError, NetworkExt, NetworkRegistryError};
12use std::collections::hash_map::Entry;
13use std::collections::HashMap;
14use thiserror::Error;
15use todo_unused::todo_unused;
16
17use crate::InterfaceId;
18
19#[derive(Debug)]
20pub struct SocketProxyState {
21    fuchsia_networks: fnp_socketproxy::FuchsiaNetworksProxy,
22    default_id: Option<InterfaceId>,
23    networks: HashMap<InterfaceId, fnp_socketproxy::Network>,
24}
25
26// Fuchsia Networks should only be added to the socketproxy when the link has a default v4 and/or
27// v6 route. The functions that propagate calls to the socketproxy prioritize maintaining a correct
28// version of local state and logging an error if the socketproxy state is not aligned.
29impl SocketProxyState {
30    #[todo_unused("https://fxbug.dev/385368910")]
31    pub fn new(fuchsia_networks: fnp_socketproxy::FuchsiaNetworksProxy) -> Self {
32        Self { fuchsia_networks, default_id: None, networks: HashMap::new() }
33    }
34
35    #[todo_unused("https://fxbug.dev/385368910")]
36    /// Set or unset an existing Fuchsia network as the default in the socket proxy registry.
37    ///
38    /// # Errors
39    /// The network does not exist
40    pub(crate) async fn handle_default_network(
41        &mut self,
42        network_id: Option<InterfaceId>,
43    ) -> Result<(), SocketProxyError> {
44        // Default id is already the same, no reason to change the default network.
45        if self.default_id == network_id {
46            return Ok(());
47        }
48
49        let socketproxy_network_id = match network_id {
50            Some(id) => {
51                if !self.networks.contains_key(&id) {
52                    return Err(SocketProxyError::SetDefaultNonexistentNetwork(id));
53                }
54
55                // We expect interface ids to safely fit in the range of u32 values.
56                let id_u32: u32 = match id.get().try_into() {
57                    Err(_) => {
58                        return Err(SocketProxyError::InvalidInterfaceId(id));
59                    }
60                    Ok(id) => id,
61                };
62
63                fposix_socket::OptionalUint32::Value(id_u32)
64            }
65            None => fposix_socket::OptionalUint32::Unset(fposix_socket::Empty),
66        };
67
68        self.default_id = network_id;
69
70        Ok(self.fuchsia_networks.set_default(&socketproxy_network_id).await??)
71    }
72
73    #[todo_unused("https://fxbug.dev/385368910")]
74    /// Handle the removal of the current default network. When other Fuchsia networks exist,
75    /// fallback to one of them instead, prioritizing the network with the lowest id. If no other
76    /// networks exist, the Fuchsia default network will be set to None.
77    pub(crate) async fn handle_default_network_removal(
78        &mut self,
79    ) -> Result<Option<InterfaceId>, SocketProxyError> {
80        if let None = self.default_id {
81            // Do nothing. There is no default network.
82            return Ok(None);
83        }
84        let mut interface_ids = self
85            .networks
86            .keys()
87            .filter(|network| Some(network.get()) != self.default_id.map(|id| id.get()))
88            .cloned()
89            .peekable();
90        if interface_ids.peek().is_none() {
91            // No need to fallback to another Fuchsia network if one doesn't
92            // exist. Simply set the default to None.
93            self.handle_default_network(None).await.map(|_| None)
94        } else {
95            // Fallback to the network with the lowest id.
96            let new_id = interface_ids.into_iter().min();
97            self.handle_default_network(new_id).await.map(|_| new_id)
98        }
99    }
100
101    #[todo_unused("https://fxbug.dev/385368910")]
102    /// Add a new Fuchsia network to the socket proxy registry.
103    ///
104    /// # Errors
105    /// The network already exists
106    pub(crate) async fn handle_add_network(
107        &mut self,
108        properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
109    ) -> Result<(), SocketProxyError> {
110        // TODO(https://fxrev.dev/385368910): Include DNS servers
111        let network = fnp_socketproxy::Network::from_watcher_properties(properties)?;
112        match self.networks.entry(InterfaceId(properties.id)) {
113            Entry::Vacant(entry) => {
114                let _ = entry.insert(network.clone());
115            }
116            Entry::Occupied(_entry) => {
117                return Err(SocketProxyError::AddedExistingNetwork(network));
118            }
119        }
120
121        Ok(self.fuchsia_networks.add(&network).await??)
122    }
123
124    #[todo_unused("https://fxbug.dev/385368910")]
125    /// Remove an existing Fuchsia network in the socket proxy registry.
126    ///
127    /// # Errors
128    /// The network does not exist or is the current default network
129    pub(crate) async fn handle_remove_network(
130        &mut self,
131        network_id: InterfaceId,
132    ) -> Result<(), SocketProxyError> {
133        if !self.networks.contains_key(&network_id) {
134            return Err(SocketProxyError::RemovedNonexistentNetwork(network_id));
135        } else if self.default_id.map(|id| id == network_id).unwrap_or(false) {
136            return Err(SocketProxyError::RemovedDefaultNetwork(network_id));
137        }
138
139        // We expect interface ids to safely fit in the range of u32 values.
140        let id_u32: u32 = match network_id.get().try_into() {
141            Err(_) => {
142                return Err(SocketProxyError::InvalidInterfaceId(network_id));
143            }
144            Ok(id) => id,
145        };
146        let _ = self.networks.remove(&network_id);
147
148        Ok(self.fuchsia_networks.remove(id_u32).await??)
149    }
150}
151
152#[todo_unused("https://fxbug.dev/385368910")]
153// Errors produced when maintaining registry state or communicating
154// updates to the socket proxy.
155#[derive(Clone, Debug, Error)]
156pub enum SocketProxyError {
157    #[error("Error adding network that already exists: {0:?}")]
158    AddedExistingNetwork(fnp_socketproxy::Network),
159    #[error("Error converting the watcher properties to a network: {0}")]
160    ConversionError(#[from] NetworkConversionError),
161    #[error("Error calling FIDL on socketproxy: {0:?}")]
162    Fidl(#[from] fidl::Error),
163    #[error("Error converting id to socketproxy network: {0}")]
164    InvalidInterfaceId(InterfaceId),
165    #[error("Network Registry error: {0:?}")]
166    NetworkRegistry(#[from] NetworkRegistryError),
167    #[error("Error removing a current default network with id: {0}")]
168    RemovedDefaultNetwork(InterfaceId),
169    #[error("Error removing network that does not exist with id: {0}")]
170    RemovedNonexistentNetwork(InterfaceId),
171    #[error("Error setting default network that does not exist with id: {0}")]
172    SetDefaultNonexistentNetwork(InterfaceId),
173}
174
175impl From<fnp_socketproxy::NetworkRegistryAddError> for SocketProxyError {
176    fn from(error: fnp_socketproxy::NetworkRegistryAddError) -> Self {
177        SocketProxyError::NetworkRegistry(NetworkRegistryError::Add(error))
178    }
179}
180
181impl From<fnp_socketproxy::NetworkRegistryRemoveError> for SocketProxyError {
182    fn from(error: fnp_socketproxy::NetworkRegistryRemoveError) -> Self {
183        SocketProxyError::NetworkRegistry(NetworkRegistryError::Remove(error))
184    }
185}
186
187impl From<fnp_socketproxy::NetworkRegistrySetDefaultError> for SocketProxyError {
188    fn from(error: fnp_socketproxy::NetworkRegistrySetDefaultError) -> Self {
189        SocketProxyError::NetworkRegistry(NetworkRegistryError::SetDefault(error))
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use socket_proxy_testing::respond_to_socketproxy;
196    use std::num::NonZeroU64;
197
198    use super::*;
199
200    fn interface_properties_from_id(
201        id: u64,
202    ) -> fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest> {
203        fnet_interfaces_ext::Properties {
204            id: NonZeroU64::new(id).expect("this is a valid u64"),
205            online: true,
206            name: String::from("network"),
207            has_default_ipv4_route: true,
208            has_default_ipv6_route: true,
209            addresses: vec![],
210            port_class: fnet_interfaces_ext::PortClass::Ethernet,
211        }
212    }
213
214    #[fuchsia::test]
215    async fn test_set_default_network_id() -> Result<(), SocketProxyError> {
216        let (fuchsia_networks, mut fuchsia_networks_req_stream) =
217            fidl::endpoints::create_proxy_and_stream::<fnp_socketproxy::FuchsiaNetworksMarker>();
218        let mut state = SocketProxyState::new(fuchsia_networks);
219        const NETWORK_ID_U64: u64 = 1u64;
220        const NETWORK_ID: InterfaceId = InterfaceId(NonZeroU64::new(NETWORK_ID_U64).unwrap());
221
222        // Attempt to set a network as default when it isn't known
223        // to the SocketProxyState.
224        assert_matches::assert_matches!(
225            state.handle_default_network(Some(NETWORK_ID)).await,
226            Err(SocketProxyError::SetDefaultNonexistentNetwork(id))
227            if id.get() == NETWORK_ID_U64
228        );
229
230        // Add a network without 'officially' adding it so we can
231        // test `handle_default_network` in isolation.
232        assert_matches::assert_matches!(
233            state.networks.insert(
234                NETWORK_ID,
235                fnp_socketproxy::Network::from_watcher_properties(&interface_properties_from_id(
236                    NETWORK_ID_U64
237                ))?,
238            ),
239            None
240        );
241
242        // Set the default network as an existing network.
243        let (set_default_network_result, ()) = futures::join!(
244            state.handle_default_network(Some(NETWORK_ID)),
245            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
246        );
247        assert_matches::assert_matches!(set_default_network_result, Ok(()));
248
249        // Unset the default network.
250        let (set_default_network_result2, ()) = futures::join!(
251            state.handle_default_network(None),
252            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
253        );
254        assert_matches::assert_matches!(set_default_network_result2, Ok(()));
255
256        Ok(())
257    }
258
259    #[fuchsia::test]
260    async fn test_add_network() {
261        let (fuchsia_networks, mut fuchsia_networks_req_stream) =
262            fidl::endpoints::create_proxy_and_stream::<fnp_socketproxy::FuchsiaNetworksMarker>();
263        let mut state = SocketProxyState::new(fuchsia_networks);
264        const NETWORK_ID1_U32: u32 = 1u32;
265        const NETWORK_ID2_U64: u64 = 2u64;
266
267        let network1 = interface_properties_from_id(NETWORK_ID1_U32.into());
268        let (add_network_result, ()) = futures::join!(
269            state.handle_add_network(&network1),
270            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
271        );
272        assert_matches::assert_matches!(add_network_result, Ok(()));
273
274        // Ensure we cannot add a network with the same id twice.
275        assert_matches::assert_matches!(
276            state.handle_add_network(&network1).await,
277            Err(SocketProxyError::AddedExistingNetwork(fnp_socketproxy::Network {
278                network_id: Some(NETWORK_ID1_U32),
279                ..
280            }))
281        );
282
283        // Ensure we can add a network with a different id.
284        let network2 = interface_properties_from_id(NETWORK_ID2_U64);
285        let (add_network_result2, ()) = futures::join!(
286            state.handle_add_network(&network2),
287            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
288        );
289        assert_matches::assert_matches!(add_network_result2, Ok(()));
290    }
291
292    #[fuchsia::test]
293    async fn test_remove_network() -> Result<(), SocketProxyError> {
294        let (fuchsia_networks, mut fuchsia_networks_req_stream) =
295            fidl::endpoints::create_proxy_and_stream::<fnp_socketproxy::FuchsiaNetworksMarker>();
296        let mut state = SocketProxyState::new(fuchsia_networks);
297        const NETWORK_ID_U64: u64 = 1;
298        const NETWORK_ID: InterfaceId = InterfaceId(NonZeroU64::new(NETWORK_ID_U64).unwrap());
299
300        // Attempt to remove a network when it isn't known
301        // to the SocketProxyState.
302        assert_matches::assert_matches!(
303            state.handle_remove_network(NETWORK_ID).await,
304            Err(SocketProxyError::RemovedNonexistentNetwork(id))
305            if id.get() == NETWORK_ID_U64
306        );
307
308        // Add a network and make it default without 'officially' adding or
309        // setting it so we can test `handle_remove_network` in isolation.
310        assert_matches::assert_matches!(
311            state.networks.insert(
312                NETWORK_ID,
313                fnp_socketproxy::Network::from_watcher_properties(&interface_properties_from_id(
314                    NETWORK_ID_U64
315                ),)?,
316            ),
317            None
318        );
319        state.default_id = Some(NETWORK_ID);
320
321        // Attempt to remove the network although it is the default network.
322        assert_matches::assert_matches!(
323            state.handle_remove_network(NETWORK_ID).await,
324            Err(SocketProxyError::RemovedDefaultNetwork(id))
325            if id.get() == NETWORK_ID_U64
326        );
327
328        // Directly unset the default network so we can test
329        // `handle_remove_network` in isolation.
330        state.default_id = None;
331
332        // Attempt to remove the network. This should succeed since it is
333        // not the current default network.
334        let (remove_network_result, ()) = futures::join!(
335            state.handle_remove_network(NETWORK_ID),
336            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
337        );
338        assert_matches::assert_matches!(remove_network_result, Ok(()));
339        assert_matches::assert_matches!(state.networks.get(&NETWORK_ID), None);
340
341        Ok(())
342    }
343
344    #[fuchsia::test]
345    async fn test_default_network_removal() -> Result<(), SocketProxyError> {
346        let (fuchsia_networks, mut fuchsia_networks_req_stream) =
347            fidl::endpoints::create_proxy_and_stream::<fnp_socketproxy::FuchsiaNetworksMarker>();
348        let mut state = SocketProxyState::new(fuchsia_networks);
349        const NETWORK_ID1_U64: u64 = 1;
350        const NETWORK_ID1: InterfaceId = InterfaceId(NonZeroU64::new(NETWORK_ID1_U64).unwrap());
351        const NETWORK_ID2_U64: u64 = 2;
352        const NETWORK_ID2: InterfaceId = InterfaceId(NonZeroU64::new(NETWORK_ID2_U64).unwrap());
353
354        // Attempting to remove the default network when one does not exist
355        // should result in Ok(None).
356        assert_matches::assert_matches!(state.handle_default_network_removal().await, Ok(None));
357
358        // Add two networks and set one as the default without 'officially'
359        // adding or setting it so we can test `handle_default_network_removal`
360        // in isolation.
361        assert_matches::assert_matches!(
362            state.networks.insert(
363                NETWORK_ID1,
364                fnp_socketproxy::Network::from_watcher_properties(&interface_properties_from_id(
365                    NETWORK_ID1_U64
366                ),)?,
367            ),
368            None
369        );
370        assert_matches::assert_matches!(
371            state.networks.insert(
372                NETWORK_ID2,
373                fnp_socketproxy::Network::from_watcher_properties(&interface_properties_from_id(
374                    NETWORK_ID2_U64
375                ),)?,
376            ),
377            None
378        );
379        state.default_id = Some(NETWORK_ID1);
380
381        // Remove the default network. Since NETWORK_ID1 is the current default
382        // then NETWORK_ID2 should be the chosen fallback.
383        let (default_network_removal_result, ()) = futures::join!(
384            state.handle_default_network_removal(),
385            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
386        );
387        assert_matches::assert_matches!(default_network_removal_result, Ok(Some(NETWORK_ID2)));
388        assert_matches::assert_matches!(state.default_id, Some(NETWORK_ID2));
389
390        // `handle_default_network_removal` does not remove the network itself,
391        // it performs the logic to fallback to another default network or
392        // unset it if one does not exist. Remove NETWORK_ID1 manually.
393        assert_matches::assert_matches!(
394            state.networks.remove(&NETWORK_ID1),
395            Some(network) if network.network_id.unwrap() == NETWORK_ID1_U64 as u32
396        );
397
398        // Remove the default network. Since there is no other network, then
399        // the default network should be unset.
400        let (default_network_removal_result2, ()) = futures::join!(
401            state.handle_default_network_removal(),
402            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
403        );
404        assert_matches::assert_matches!(default_network_removal_result2, Ok(None));
405        assert_matches::assert_matches!(state.default_id, None);
406
407        Ok(())
408    }
409
410    #[fuchsia::test]
411    async fn test_multiple_operations() {
412        let (fuchsia_networks, mut fuchsia_networks_req_stream) =
413            fidl::endpoints::create_proxy_and_stream::<fnp_socketproxy::FuchsiaNetworksMarker>();
414        let mut state = SocketProxyState::new(fuchsia_networks);
415        const NETWORK_ID1_U64: u64 = 1;
416        const NETWORK_ID1: InterfaceId = InterfaceId(NonZeroU64::new(NETWORK_ID1_U64).unwrap());
417        const NETWORK_ID2_U64: u64 = 2;
418        const NETWORK_ID2: InterfaceId = InterfaceId(NonZeroU64::new(NETWORK_ID2_U64).unwrap());
419
420        // Add a network.
421        let network1 = interface_properties_from_id(NETWORK_ID1_U64);
422        let (add_network_result, ()) = futures::join!(
423            state.handle_add_network(&network1),
424            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
425        );
426        assert_matches::assert_matches!(add_network_result, Ok(()));
427
428        // Set the default network.
429        let (set_default_network_result, ()) = futures::join!(
430            state.handle_default_network(Some(NETWORK_ID1)),
431            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
432        );
433        assert_matches::assert_matches!(set_default_network_result, Ok(()));
434
435        // Add another network.
436        let network2 = interface_properties_from_id(NETWORK_ID2_U64);
437        let (add_network_result, ()) = futures::join!(
438            state.handle_add_network(&network2),
439            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
440        );
441        assert_matches::assert_matches!(add_network_result, Ok(()));
442
443        // Attempt to remove the first network. We should get an error
444        // since network1 is the current default network.
445        assert_matches::assert_matches!(
446            state.handle_remove_network(NETWORK_ID1).await,
447            Err(SocketProxyError::RemovedDefaultNetwork(id))
448            if id.get() == NETWORK_ID1_U64
449        );
450
451        // Ensure the first network still exists.
452        assert!(state.networks.get(&NETWORK_ID1).is_some());
453
454        // Set the default network to the second network.
455        let (default_network_removal_result, ()) = futures::join!(
456            state.handle_default_network_removal(),
457            respond_to_socketproxy(&mut fuchsia_networks_req_stream, Ok(()))
458        );
459        assert_matches::assert_matches!(default_network_removal_result, Ok(Some(NETWORK_ID2)));
460
461        // Remove the first network as it is no longer the default network.
462        assert_matches::assert_matches!(
463            state.networks.remove(&NETWORK_ID1),
464            Some(network) if network.network_id.unwrap() == NETWORK_ID1_U64 as u32
465        );
466    }
467}