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