Skip to main content

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