rtnetlink/
ns.rs

1// SPDX-License-Identifier: MIT
2
3use crate::Error;
4use nix::{
5    fcntl::OFlag,
6    sched::CloneFlags,
7    sys::{
8        stat::Mode,
9        wait::{waitpid, WaitStatus},
10    },
11    unistd::{fork, ForkResult},
12};
13use std::{option::Option, path::Path, process::exit};
14
15// if "only" smol or smol+tokio were enabled, we use smol because
16// it doesn't require an active tokio runtime - just to be sure.
17#[cfg(feature = "smol_socket")]
18async fn try_spawn_blocking<F, R>(fut: F) -> R
19where
20    F: FnOnce() -> R + Send + 'static,
21    R: Send + 'static,
22{
23    async_global_executor::spawn_blocking(fut).await
24}
25
26// only tokio enabled, so use tokio
27#[cfg(all(not(feature = "smol_socket"), feature = "tokio_socket"))]
28async fn try_spawn_blocking<F, R>(fut: F) -> R
29where
30    F: FnOnce() -> R + Send + 'static,
31    R: Send + 'static,
32{
33    match tokio::task::spawn_blocking(fut).await {
34        Ok(v) => v,
35        Err(err) => {
36            std::panic::resume_unwind(err.into_panic());
37        }
38    }
39}
40
41// neither smol nor tokio - just run blocking op directly.
42// hopefully not too blocking...
43#[cfg(all(not(feature = "smol_socket"), not(feature = "tokio_socket")))]
44async fn try_spawn_blocking<F, R>(fut: F) -> R
45where
46    F: FnOnce() -> R + Send + 'static,
47    R: Send + 'static,
48{
49    fut()
50}
51
52pub const NETNS_PATH: &str = "/run/netns/";
53pub const SELF_NS_PATH: &str = "/proc/self/ns/net";
54pub const NONE_FS: &str = "none";
55
56pub struct NetworkNamespace();
57
58impl NetworkNamespace {
59    /// Add a new network namespace.
60    /// This is equivalent to `ip netns add NS_NAME`.
61    pub async fn add(ns_name: String) -> Result<(), Error> {
62        // Forking process to avoid moving caller into new namespace
63        NetworkNamespace::prep_for_fork()?;
64        log::trace!("Forking...");
65        match unsafe { fork() } {
66            Ok(ForkResult::Parent { child, .. }) => {
67                NetworkNamespace::parent_process(child)
68            }
69            Ok(ForkResult::Child) => {
70                NetworkNamespace::child_process(ns_name);
71            }
72            Err(e) => {
73                let err_msg = format!("Fork failed: {e}");
74                Err(Error::NamespaceError(err_msg))
75            }
76        }
77    }
78
79    /// Remove a network namespace
80    /// This is equivalent to `ip netns del NS_NAME`.
81    pub async fn del(ns_name: String) -> Result<(), Error> {
82        try_spawn_blocking(move || {
83            let mut netns_path = String::new();
84            netns_path.push_str(NETNS_PATH);
85            netns_path.push_str(&ns_name);
86            let ns_path = Path::new(&netns_path);
87
88            if nix::mount::umount2(ns_path, nix::mount::MntFlags::MNT_DETACH)
89                .is_err()
90            {
91                let err_msg = String::from(
92                    "Namespace unmount failed (are you running as root?)",
93                );
94                return Err(Error::NamespaceError(err_msg));
95            }
96
97            if nix::unistd::unlink(ns_path).is_err() {
98                let err_msg = String::from(
99                    "Namespace file remove failed (are you running as root?)",
100                );
101                return Err(Error::NamespaceError(err_msg));
102            }
103
104            Ok(())
105        })
106        .await
107    }
108
109    pub fn prep_for_fork() -> Result<(), Error> {
110        // Placeholder function, nothing to do here.
111        Ok(())
112    }
113
114    /// This is the parent process form the fork, it waits for the
115    /// child to exit properly
116    pub fn parent_process(child: nix::unistd::Pid) -> Result<(), Error> {
117        log::trace!("parent_process child PID: {}", child);
118        log::trace!("Waiting for child to finish...");
119        match waitpid(child, None) {
120            Ok(wait_status) => match wait_status {
121                WaitStatus::Exited(_, res) => {
122                    log::trace!("Child exited with: {}", res);
123                    if res == 0 {
124                        return Ok(());
125                    }
126                    log::error!("Error child result: {}", res);
127                    let err_msg = format!("Error child result: {res}");
128                    Err(Error::NamespaceError(err_msg))
129                }
130                WaitStatus::Signaled(_, signal, has_dump) => {
131                    log::error!("Error child killed by signal: {}", signal);
132                    let err_msg = format!(
133                        "Error child process was killed by signal: {signal} with core dump {has_dump}"
134                    );
135                    Err(Error::NamespaceError(err_msg))
136                }
137                _ => {
138                    log::error!("Unknown child process status");
139                    let err_msg = String::from("Unknown child process status");
140                    Err(Error::NamespaceError(err_msg))
141                }
142            },
143            Err(e) => {
144                log::error!("wait error: {}", e);
145                let err_msg = format!("wait error: {e}");
146                Err(Error::NamespaceError(err_msg))
147            }
148        }
149    }
150
151    fn child_process(ns_name: String) -> ! {
152        let res = std::panic::catch_unwind(|| -> Result<(), Error> {
153            let netns_path =
154                NetworkNamespace::child_process_create_ns(ns_name)?;
155            NetworkNamespace::unshare_processing(netns_path)?;
156            Ok(())
157        });
158        match res {
159            Err(_panic) => {
160                // panic should have already been printed by the handler
161                log::error!("child process crashed");
162                std::process::abort()
163            }
164            Ok(Err(fail)) => {
165                log::error!("child process failed: {}", fail);
166                exit(1)
167            }
168            Ok(Ok(())) => exit(0),
169        }
170    }
171
172    /// This is the child process, it will actually create the namespace
173    /// resources. It creates the folder and namespace file.
174    /// Returns the namespace file path
175    pub fn child_process_create_ns(ns_name: String) -> Result<String, Error> {
176        log::trace!("child_process will create the namespace");
177
178        let mut netns_path = String::new();
179
180        let dir_path = Path::new(NETNS_PATH);
181        let mut mkdir_mode = Mode::empty();
182        let mut open_flags = OFlag::empty();
183        let mut mount_flags = nix::mount::MsFlags::empty();
184        let none_fs = Path::new(&NONE_FS);
185        let none_p4: Option<&Path> = None;
186
187        // flags in mkdir
188        mkdir_mode.insert(Mode::S_IRWXU);
189        mkdir_mode.insert(Mode::S_IRGRP);
190        mkdir_mode.insert(Mode::S_IXGRP);
191        mkdir_mode.insert(Mode::S_IROTH);
192        mkdir_mode.insert(Mode::S_IXOTH);
193
194        open_flags.insert(OFlag::O_RDONLY);
195        open_flags.insert(OFlag::O_CREAT);
196        open_flags.insert(OFlag::O_EXCL);
197
198        netns_path.push_str(NETNS_PATH);
199        netns_path.push_str(&ns_name);
200
201        // creating namespaces folder if not exists
202        #[allow(clippy::collapsible_if)]
203        if nix::sys::stat::stat(dir_path).is_err() {
204            if let Err(e) = nix::unistd::mkdir(dir_path, mkdir_mode) {
205                log::error!("mkdir error: {}", e);
206                let err_msg = format!("mkdir error: {e}");
207                return Err(Error::NamespaceError(err_msg));
208            }
209        }
210
211        // Try to mount /run/netns, with MS_REC | MS_SHARED
212        // If it fails, creates the mount with MS_BIND | MS_REC
213        // This is the same strategy used by `ip netns add NS`
214        mount_flags.insert(nix::mount::MsFlags::MS_REC);
215        mount_flags.insert(nix::mount::MsFlags::MS_SHARED);
216        if nix::mount::mount(
217            Some(Path::new("")),
218            dir_path,
219            Some(none_fs),
220            mount_flags,
221            none_p4,
222        )
223        .is_err()
224        {
225            mount_flags = nix::mount::MsFlags::empty();
226            mount_flags.insert(nix::mount::MsFlags::MS_BIND);
227            mount_flags.insert(nix::mount::MsFlags::MS_REC);
228
229            if let Err(e) = nix::mount::mount(
230                Some(Path::new(dir_path)),
231                dir_path,
232                Some(none_fs),
233                mount_flags,
234                none_p4,
235            ) {
236                log::error!("mount error: {}", e);
237                let err_msg = format!("mount error: {e}");
238                return Err(Error::NamespaceError(err_msg));
239            }
240        }
241
242        mount_flags = nix::mount::MsFlags::empty();
243        mount_flags.insert(nix::mount::MsFlags::MS_REC);
244        mount_flags.insert(nix::mount::MsFlags::MS_SHARED);
245        if let Err(e) = nix::mount::mount(
246            Some(Path::new("")),
247            dir_path,
248            Some(none_fs),
249            mount_flags,
250            none_p4,
251        ) {
252            log::error!("mount error: {}", e);
253            let err_msg = format!("mount error: {e}");
254            return Err(Error::NamespaceError(err_msg));
255        }
256
257        let ns_path = Path::new(&netns_path);
258
259        // creating the netns file
260        let fd = match nix::fcntl::open(ns_path, open_flags, Mode::empty()) {
261            Ok(raw_fd) => raw_fd,
262            Err(e) => {
263                log::error!("open error: {}", e);
264                let err_msg = format!("open error: {e}");
265                return Err(Error::NamespaceError(err_msg));
266            }
267        };
268
269        if let Err(e) = nix::unistd::close(fd) {
270            log::error!("close error: {}", e);
271            let err_msg = format!("close error: {e}");
272            let _ = nix::unistd::unlink(ns_path);
273            return Err(Error::NamespaceError(err_msg));
274        }
275
276        Ok(netns_path)
277    }
278
279    /// This function unshare the calling process and move into
280    /// the given network namespace
281    #[allow(unused)]
282    pub fn unshare_processing(netns_path: String) -> Result<(), Error> {
283        let mut setns_flags = CloneFlags::empty();
284        let mut open_flags = OFlag::empty();
285        let ns_path = Path::new(&netns_path);
286
287        let none_fs = Path::new(&NONE_FS);
288        let none_p4: Option<&Path> = None;
289
290        // unshare to the new network namespace
291        if let Err(e) = nix::sched::unshare(CloneFlags::CLONE_NEWNET) {
292            log::error!("unshare error: {}", e);
293            let err_msg = format!("unshare error: {e}");
294            let _ = nix::unistd::unlink(ns_path);
295            return Err(Error::NamespaceError(err_msg));
296        }
297
298        open_flags = OFlag::empty();
299        open_flags.insert(OFlag::O_RDONLY);
300        open_flags.insert(OFlag::O_CLOEXEC);
301
302        let fd = match nix::fcntl::open(
303            Path::new(&SELF_NS_PATH),
304            open_flags,
305            Mode::empty(),
306        ) {
307            Ok(raw_fd) => raw_fd,
308            Err(e) => {
309                log::error!("open error: {}", e);
310                let err_msg = format!("open error: {e}");
311                return Err(Error::NamespaceError(err_msg));
312            }
313        };
314
315        let self_path = Path::new(&SELF_NS_PATH);
316
317        // bind to the netns
318        if let Err(e) = nix::mount::mount(
319            Some(self_path),
320            ns_path,
321            Some(none_fs),
322            nix::mount::MsFlags::MS_BIND,
323            none_p4,
324        ) {
325            log::error!("mount error: {}", e);
326            let err_msg = format!("mount error: {e}");
327            let _ = nix::unistd::unlink(ns_path);
328            return Err(Error::NamespaceError(err_msg));
329        }
330
331        setns_flags.insert(CloneFlags::CLONE_NEWNET);
332        if let Err(e) = nix::sched::setns(fd, setns_flags) {
333            log::error!("setns error: {}", e);
334            let err_msg = format!("setns error: {e}");
335            let _ = nix::unistd::unlink(ns_path);
336            return Err(Error::NamespaceError(err_msg));
337        }
338
339        Ok(())
340    }
341}