fidl_fuchsia_net_filter_ext/
sync.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
5use crate::{
6    handle_change_validation_result, handle_commit_result, Change, CommitError,
7    ControllerCreationError, ControllerId, PushChangesError,
8};
9use fidl::marker::SourceBreaking;
10use fidl_fuchsia_net_filter as fnet_filter;
11
12/// A controller for filtering state with blocking methods.
13pub struct Controller {
14    controller: fnet_filter::NamespaceControllerSynchronousProxy,
15    // The client provides an ID when creating a new controller, but the server
16    // may need to assign a different ID to avoid conflicts; either way, the
17    // server informs the client of the final `ControllerId` on creation.
18    id: ControllerId,
19    // Changes that have been pushed to the server but not yet committed. This
20    // allows the `Controller` to report more informative errors by correlating
21    // error codes with particular changes.
22    pending_changes: Vec<Change>,
23}
24
25impl Controller {
26    /// Creates a new `Controller`.
27    ///
28    /// Note that the provided `ControllerId` may need to be modified server-
29    /// side to avoid collisions; to obtain the final ID assigned to the
30    /// `Controller`, use the `id` method.
31    pub fn new(
32        control: &fnet_filter::ControlSynchronousProxy,
33        ControllerId(id): &ControllerId,
34        deadline: zx::MonotonicInstant,
35    ) -> Result<Self, ControllerCreationError> {
36        let (controller, server_end) = fidl::endpoints::create_sync_proxy();
37        control.open_controller(id, server_end).map_err(ControllerCreationError::OpenController)?;
38
39        let fnet_filter::NamespaceControllerEvent::OnIdAssigned { id } =
40            controller.wait_for_event(deadline).map_err(ControllerCreationError::IdAssignment)?;
41        Ok(Self { controller, id: ControllerId(id), pending_changes: Vec::new() })
42    }
43
44    pub fn id(&self) -> &ControllerId {
45        &self.id
46    }
47
48    pub fn push_changes(
49        &mut self,
50        changes: Vec<Change>,
51        deadline: zx::MonotonicInstant,
52    ) -> Result<(), PushChangesError> {
53        let fidl_changes = changes.iter().cloned().map(Into::into).collect::<Vec<_>>();
54        let result = self
55            .controller
56            .push_changes(&fidl_changes, deadline)
57            .map_err(PushChangesError::CallMethod)?;
58        handle_change_validation_result(result, &changes)?;
59        // Maintain a client-side copy of the pending changes we've pushed to
60        // the server in order to provide better error messages if a commit
61        // fails.
62        self.pending_changes.extend(changes);
63        Ok(())
64    }
65
66    pub fn commit_with_options(
67        &mut self,
68        options: fnet_filter::CommitOptions,
69        deadline: zx::MonotonicInstant,
70    ) -> Result<(), CommitError> {
71        let committed_changes = std::mem::take(&mut self.pending_changes);
72        let result = self.controller.commit(options, deadline).map_err(CommitError::CallMethod)?;
73        handle_commit_result(result, committed_changes)
74    }
75
76    pub fn commit(&mut self, deadline: zx::MonotonicInstant) -> Result<(), CommitError> {
77        self.commit_with_options(fnet_filter::CommitOptions::default(), deadline)
78    }
79
80    pub fn commit_idempotent(&mut self, deadline: zx::MonotonicInstant) -> Result<(), CommitError> {
81        self.commit_with_options(
82            fnet_filter::CommitOptions {
83                idempotent: Some(true),
84                __source_breaking: SourceBreaking,
85            },
86            deadline,
87        )
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::tests::{
95        handle_commit, handle_open_controller, handle_push_changes, invalid_resource,
96        test_resource, test_resource_id, unknown_resource_id,
97    };
98    use crate::{ChangeCommitError, ChangeValidationError};
99    use assert_matches::assert_matches;
100
101    #[fuchsia::test(threads = 2)]
102    async fn controller_push_changes_reports_invalid_change() {
103        let (control_sync, request_stream) =
104            fidl::endpoints::create_sync_proxy_and_stream::<fnet_filter::ControlMarker>();
105
106        let run_controller = fuchsia_async::Task::spawn(async {
107            let mut stream = handle_open_controller(request_stream).await;
108            handle_push_changes(
109                &mut stream,
110                fnet_filter::ChangeValidationResult::ErrorOnChange(vec![
111                    fnet_filter::ChangeValidationError::Ok,
112                    fnet_filter::ChangeValidationError::InvalidPortMatcher,
113                    fnet_filter::ChangeValidationError::NotReached,
114                ]),
115            )
116            .await;
117        });
118
119        let mut controller = Controller::new(
120            &control_sync,
121            &ControllerId(String::from("test")),
122            zx::MonotonicInstant::INFINITE,
123        )
124        .expect("create controller");
125        let result = controller.push_changes(
126            vec![
127                Change::Create(test_resource()),
128                Change::Create(invalid_resource()),
129                Change::Remove(test_resource_id()),
130            ],
131            zx::MonotonicInstant::INFINITE,
132        );
133
134        assert_matches!(
135            result,
136            Err(PushChangesError::ErrorOnChange(errors)) if errors == vec![(
137                Change::Create(invalid_resource()),
138                ChangeValidationError::InvalidPortMatcher
139            )]
140        );
141
142        run_controller.await;
143    }
144
145    #[fuchsia::test(threads = 2)]
146    async fn controller_commit_reports_invalid_change() {
147        let (control_sync, request_stream) =
148            fidl::endpoints::create_sync_proxy_and_stream::<fnet_filter::ControlMarker>();
149
150        let run_controller = fuchsia_async::Task::spawn(async {
151            let mut stream = handle_open_controller(request_stream).await;
152            handle_push_changes(
153                &mut stream,
154                fnet_filter::ChangeValidationResult::Ok(fnet_filter::Empty {}),
155            )
156            .await;
157            handle_commit(
158                &mut stream,
159                fnet_filter::CommitResult::ErrorOnChange(vec![
160                    fnet_filter::CommitError::Ok,
161                    fnet_filter::CommitError::NamespaceNotFound,
162                    fnet_filter::CommitError::Ok,
163                ]),
164            )
165            .await;
166        });
167
168        let mut controller = Controller::new(
169            &control_sync,
170            &ControllerId(String::from("test")),
171            zx::MonotonicInstant::INFINITE,
172        )
173        .expect("create controller");
174        controller
175            .push_changes(
176                vec![
177                    Change::Create(test_resource()),
178                    Change::Remove(unknown_resource_id()),
179                    Change::Remove(test_resource_id()),
180                ],
181                zx::MonotonicInstant::INFINITE,
182            )
183            .expect("push changes");
184
185        let result = controller.commit(zx::MonotonicInstant::INFINITE);
186        assert_matches!(
187            result,
188            Err(CommitError::ErrorOnChange(errors)) if errors == vec![(
189                Change::Remove(unknown_resource_id()),
190                ChangeCommitError::NamespaceNotFound,
191            )]
192        );
193
194        run_controller.await;
195    }
196}