sandbox_run/sandbox.rs
1use std::path::{Path, PathBuf};
2
3use crate::error::SandboxError;
4
5/// Protection level for system directories (systemd: ProtectSystem=)
6#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
7pub enum ProtectSystem {
8 /// No protection
9 #[default]
10 No,
11 /// /usr, /boot, /efi read-only
12 Yes,
13 /// + /etc read-only
14 Full,
15 /// Entire FS read-only except /dev, /proc, /sys
16 Strict,
17}
18
19/// Protection level for home directories (systemd: ProtectHome=)
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub enum ProtectHome {
22 /// No protection
23 #[default]
24 No,
25 /// /home, /root, /run/user inaccessible
26 Yes,
27 /// /home, /root, /run/user read-only
28 ReadOnly,
29}
30
31/// Builder for creating sandbox policies mirroring systemd.exec options.
32///
33/// # Example
34///
35/// ```no_run
36/// use std::path::Path;
37/// use sandbox_run::{Sandbox, ProtectSystem, ProtectHome};
38///
39/// let sandbox = Sandbox::new()
40/// .protect_system(ProtectSystem::Strict)
41/// .protect_home(ProtectHome::Yes)
42/// .read_write_paths([Path::new("/tmp/myapp")])
43/// .no_new_privileges(true)
44/// .limit_nofile(256)
45/// .build()
46/// .unwrap();
47/// ```
48#[derive(Debug, Clone, Default)]
49pub struct Sandbox {
50 // Filesystem protection
51 pub(crate) protect_system: ProtectSystem,
52 pub(crate) protect_home: ProtectHome,
53
54 // Path-based access control
55 pub(crate) read_only_paths: Vec<PathBuf>,
56 pub(crate) read_write_paths: Vec<PathBuf>,
57 pub(crate) inaccessible_paths: Vec<PathBuf>,
58 pub(crate) exec_paths: Vec<PathBuf>,
59 pub(crate) no_exec_paths: Vec<PathBuf>,
60
61 // Network
62 pub(crate) private_network: bool,
63
64 // Privilege control
65 pub(crate) no_new_privileges: bool,
66
67 // Resource limits (None = don't set)
68 pub(crate) limit_nofile: Option<u64>,
69 pub(crate) limit_nproc: Option<u64>,
70 pub(crate) limit_core: Option<u64>,
71 pub(crate) limit_fsize: Option<u64>,
72 pub(crate) limit_data: Option<u64>,
73 pub(crate) limit_stack: Option<u64>,
74 pub(crate) limit_cpu: Option<u64>,
75 pub(crate) limit_memlock: Option<u64>,
76}
77
78impl Sandbox {
79 /// Create a new sandbox builder with default settings (no restrictions).
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 // --- Filesystem Protection ---
85
86 /// Set system directory protection level (systemd: ProtectSystem=)
87 ///
88 /// - `No`: No protection
89 /// - `Yes`: /usr, /boot, /efi read-only
90 /// - `Full`: + /etc read-only
91 /// - `Strict`: Entire filesystem read-only except API filesystems
92 pub fn protect_system(mut self, level: ProtectSystem) -> Self {
93 self.protect_system = level;
94 self
95 }
96
97 /// Set home directory protection level (systemd: ProtectHome=)
98 ///
99 /// - `No`: No protection
100 /// - `Yes`: /home, /root, /run/user inaccessible
101 /// - `ReadOnly`: /home, /root, /run/user read-only
102 pub fn protect_home(mut self, level: ProtectHome) -> Self {
103 self.protect_home = level;
104 self
105 }
106
107 // --- Path Access Control ---
108
109 /// Make paths read-only (systemd: ReadOnlyPaths=)
110 ///
111 /// Writing to these paths will be denied.
112 pub fn read_only_paths<I, P>(mut self, paths: I) -> Self
113 where
114 I: IntoIterator<Item = P>,
115 P: AsRef<Path>,
116 {
117 self.read_only_paths
118 .extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
119 self
120 }
121
122 /// Allow read-write access to paths (systemd: ReadWritePaths=)
123 ///
124 /// Use this to create exceptions within ProtectSystem=strict.
125 pub fn read_write_paths<I, P>(mut self, paths: I) -> Self
126 where
127 I: IntoIterator<Item = P>,
128 P: AsRef<Path>,
129 {
130 self.read_write_paths
131 .extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
132 self
133 }
134
135 /// Make paths completely inaccessible (systemd: InaccessiblePaths=)
136 ///
137 /// Neither reading nor writing will be possible.
138 pub fn inaccessible_paths<I, P>(mut self, paths: I) -> Self
139 where
140 I: IntoIterator<Item = P>,
141 P: AsRef<Path>,
142 {
143 self.inaccessible_paths
144 .extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
145 self
146 }
147
148 /// Allow execution only from these paths (systemd: ExecPaths=)
149 ///
150 /// Binaries outside these paths cannot be executed.
151 pub fn exec_paths<I, P>(mut self, paths: I) -> Self
152 where
153 I: IntoIterator<Item = P>,
154 P: AsRef<Path>,
155 {
156 self.exec_paths
157 .extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
158 self
159 }
160
161 /// Deny execution from these paths (systemd: NoExecPaths=)
162 ///
163 /// Binaries in these paths cannot be executed.
164 pub fn no_exec_paths<I, P>(mut self, paths: I) -> Self
165 where
166 I: IntoIterator<Item = P>,
167 P: AsRef<Path>,
168 {
169 self.no_exec_paths
170 .extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
171 self
172 }
173
174 // --- Network ---
175
176 /// Isolate network access (systemd: PrivateNetwork=)
177 ///
178 /// When enabled, all network access is blocked.
179 pub fn private_network(mut self, enable: bool) -> Self {
180 self.private_network = enable;
181 self
182 }
183
184 // --- Privilege Control ---
185
186 /// Prevent privilege escalation (systemd: NoNewPrivileges=)
187 ///
188 /// When enabled, the process cannot gain new privileges through
189 /// setuid/setgid bits or file capabilities.
190 pub fn no_new_privileges(mut self, enable: bool) -> Self {
191 self.no_new_privileges = enable;
192 self
193 }
194
195 // --- Resource Limits ---
196
197 /// Max open file descriptors (systemd: LimitNOFILE=)
198 pub fn limit_nofile(mut self, limit: u64) -> Self {
199 self.limit_nofile = Some(limit);
200 self
201 }
202
203 /// Max number of processes (systemd: LimitNPROC=)
204 pub fn limit_nproc(mut self, limit: u64) -> Self {
205 self.limit_nproc = Some(limit);
206 self
207 }
208
209 /// Max core dump size in bytes (systemd: LimitCORE=)
210 ///
211 /// Set to 0 to disable core dumps.
212 pub fn limit_core(mut self, limit: u64) -> Self {
213 self.limit_core = Some(limit);
214 self
215 }
216
217 /// Max file size in bytes (systemd: LimitFSIZE=)
218 pub fn limit_fsize(mut self, limit: u64) -> Self {
219 self.limit_fsize = Some(limit);
220 self
221 }
222
223 /// Max data segment size in bytes (systemd: LimitDATA=)
224 pub fn limit_data(mut self, limit: u64) -> Self {
225 self.limit_data = Some(limit);
226 self
227 }
228
229 /// Max stack size in bytes (systemd: LimitSTACK=)
230 pub fn limit_stack(mut self, limit: u64) -> Self {
231 self.limit_stack = Some(limit);
232 self
233 }
234
235 /// Max CPU time in seconds (systemd: LimitCPU=)
236 pub fn limit_cpu(mut self, limit: u64) -> Self {
237 self.limit_cpu = Some(limit);
238 self
239 }
240
241 /// Max locked memory in bytes (systemd: LimitMEMLOCK=)
242 pub fn limit_memlock(mut self, limit: u64) -> Self {
243 self.limit_memlock = Some(limit);
244 self
245 }
246
247 /// Build the sandbox policy.
248 ///
249 /// This validates the configuration and prepares platform-specific data
250 /// needed to apply the sandbox.
251 pub fn build(self) -> Result<SandboxPolicy, SandboxError> {
252 Ok(SandboxPolicy { config: self })
253 }
254}
255
256/// A compiled sandbox policy ready to be applied to a command.
257///
258/// Create this using `Sandbox::build()`.
259#[derive(Debug, Clone)]
260pub struct SandboxPolicy {
261 pub(crate) config: Sandbox,
262}