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    Change, CommitError, ControllerCreationError, ControllerId, PushChangesError,
7    RegisterEbpfProgramError, handle_change_validation_result, handle_commit_result,
8};
9use fidl::marker::SourceBreaking;
10use {fidl_fuchsia_ebpf as febpf, 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 register_ebpf_program(
49        &mut self,
50        handle: febpf::ProgramHandle,
51        program: febpf::VerifiedProgram,
52        deadline: zx::MonotonicInstant,
53    ) -> Result<(), RegisterEbpfProgramError> {
54        self.controller
55            .register_ebpf_program(handle, program, deadline)
56            .map_err(RegisterEbpfProgramError::CallMethod)?
57            .map_err(RegisterEbpfProgramError::from)
58    }
59
60    pub fn push_changes(
61        &mut self,
62        changes: Vec<Change>,
63        deadline: zx::MonotonicInstant,
64    ) -> Result<(), PushChangesError> {
65        let fidl_changes = changes.iter().cloned().map(Into::into).collect::<Vec<_>>();
66        let result = self
67            .controller
68            .push_changes(&fidl_changes, deadline)
69            .map_err(PushChangesError::CallMethod)?;
70        handle_change_validation_result(result, &changes)?;
71        // Maintain a client-side copy of the pending changes we've pushed to
72        // the server in order to provide better error messages if a commit
73        // fails.
74        self.pending_changes.extend(changes);
75        Ok(())
76    }
77
78    pub fn commit_with_options(
79        &mut self,
80        options: fnet_filter::CommitOptions,
81        deadline: zx::MonotonicInstant,
82    ) -> Result<(), CommitError> {
83        let committed_changes = std::mem::take(&mut self.pending_changes);
84        let result = self.controller.commit(options, deadline).map_err(CommitError::CallMethod)?;
85        handle_commit_result(result, committed_changes)
86    }
87
88    pub fn commit(&mut self, deadline: zx::MonotonicInstant) -> Result<(), CommitError> {
89        self.commit_with_options(fnet_filter::CommitOptions::default(), deadline)
90    }
91
92    pub fn commit_idempotent(&mut self, deadline: zx::MonotonicInstant) -> Result<(), CommitError> {
93        self.commit_with_options(
94            fnet_filter::CommitOptions {
95                idempotent: Some(true),
96                __source_breaking: SourceBreaking,
97            },
98            deadline,
99        )
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::tests::{
107        handle_commit, handle_open_controller, handle_push_changes, pretend_invalid_resource,
108        test_resource, test_resource_id, unknown_resource_id,
109    };
110    use crate::{ChangeCommitError, ChangeValidationError};
111    use assert_matches::assert_matches;
112
113    #[fuchsia::test(threads = 2)]
114    async fn controller_push_changes_reports_invalid_change() {
115        let (control_sync, request_stream) =
116            fidl::endpoints::create_sync_proxy_and_stream::<fnet_filter::ControlMarker>();
117
118        let run_controller = fuchsia_async::Task::spawn(async {
119            let mut stream = handle_open_controller(request_stream).await;
120            handle_push_changes(
121                &mut stream,
122                fnet_filter::ChangeValidationResult::ErrorOnChange(vec![
123                    fnet_filter::ChangeValidationError::Ok,
124                    fnet_filter::ChangeValidationError::InvalidPortMatcher,
125                    fnet_filter::ChangeValidationError::NotReached,
126                ]),
127            )
128            .await;
129        });
130
131        let mut controller = Controller::new(
132            &control_sync,
133            &ControllerId(String::from("test")),
134            zx::MonotonicInstant::INFINITE,
135        )
136        .expect("create controller");
137        let result = controller.push_changes(
138            vec![
139                Change::Create(test_resource()),
140                // We fake the server response to say this is invalid even
141                // though it really isn't.
142                Change::Create(pretend_invalid_resource()),
143                Change::Remove(test_resource_id()),
144            ],
145            zx::MonotonicInstant::INFINITE,
146        );
147
148        assert_matches!(
149            result,
150            Err(PushChangesError::ErrorOnChange(errors)) if errors == vec![(
151                Change::Create(pretend_invalid_resource()),
152                ChangeValidationError::InvalidPortMatcher
153            )]
154        );
155
156        run_controller.await;
157    }
158
159    #[fuchsia::test(threads = 2)]
160    async fn controller_commit_reports_invalid_change() {
161        let (control_sync, request_stream) =
162            fidl::endpoints::create_sync_proxy_and_stream::<fnet_filter::ControlMarker>();
163
164        let run_controller = fuchsia_async::Task::spawn(async {
165            let mut stream = handle_open_controller(request_stream).await;
166            handle_push_changes(
167                &mut stream,
168                fnet_filter::ChangeValidationResult::Ok(fnet_filter::Empty {}),
169            )
170            .await;
171            handle_commit(
172                &mut stream,
173                fnet_filter::CommitResult::ErrorOnChange(vec![
174                    fnet_filter::CommitError::Ok,
175                    fnet_filter::CommitError::NamespaceNotFound,
176                    fnet_filter::CommitError::Ok,
177                ]),
178            )
179            .await;
180        });
181
182        let mut controller = Controller::new(
183            &control_sync,
184            &ControllerId(String::from("test")),
185            zx::MonotonicInstant::INFINITE,
186        )
187        .expect("create controller");
188        controller
189            .push_changes(
190                vec![
191                    Change::Create(test_resource()),
192                    Change::Remove(unknown_resource_id()),
193                    Change::Remove(test_resource_id()),
194                ],
195                zx::MonotonicInstant::INFINITE,
196            )
197            .expect("push changes");
198
199        let result = controller.commit(zx::MonotonicInstant::INFINITE);
200        assert_matches!(
201            result,
202            Err(CommitError::ErrorOnChange(errors)) if errors == vec![(
203                Change::Remove(unknown_resource_id()),
204                ChangeCommitError::NamespaceNotFound,
205            )]
206        );
207
208        run_controller.await;
209    }
210}