1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::{Arc, RwLock};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct ResourceLimits {
13 pub max_memory: Option<u64>,
15 pub max_cpu: Option<u32>,
17 pub max_processes: Option<u32>,
19 pub max_file_size: Option<u64>,
21 pub max_execution_time: Option<u64>,
23 pub max_file_descriptors: Option<u32>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum SandboxType {
31 Bubblewrap,
33 Docker,
35 Firejail,
37 Seatbelt,
39 #[default]
41 None,
42}
43
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46pub struct AuditLogging {
47 pub enabled: bool,
49 pub log_file: Option<PathBuf>,
51 pub log_level: LogLevel,
53}
54
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum LogLevel {
59 Debug,
60 #[default]
61 Info,
62 Warn,
63 Error,
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct DockerConfig {
69 pub image: Option<String>,
71 pub container_name: Option<String>,
73 pub volumes: Vec<String>,
75 pub ports: Vec<String>,
77 pub network: Option<String>,
79 pub user: Option<String>,
81 pub workdir: Option<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct SandboxConfig {
88 pub enabled: bool,
90 pub sandbox_type: SandboxType,
92 pub allowed_paths: Vec<PathBuf>,
94 pub denied_paths: Vec<PathBuf>,
96 pub network_access: bool,
98 pub environment_variables: HashMap<String, String>,
100 pub read_only_paths: Vec<PathBuf>,
102 pub writable_paths: Vec<PathBuf>,
104 pub allow_dev_access: bool,
106 pub allow_proc_access: bool,
108 pub allow_sys_access: bool,
110 pub env_whitelist: Vec<String>,
112 pub tmpfs_size: String,
114 pub unshare_all: bool,
116 pub die_with_parent: bool,
118 pub new_session: bool,
120 pub docker: Option<DockerConfig>,
122 pub custom_args: Vec<String>,
124 pub audit_logging: Option<AuditLogging>,
126 pub resource_limits: Option<ResourceLimits>,
128}
129
130impl Default for SandboxConfig {
131 fn default() -> Self {
132 Self {
133 enabled: true,
134 sandbox_type: SandboxType::None,
135 allowed_paths: Vec::new(),
136 denied_paths: Vec::new(),
137 network_access: false,
138 environment_variables: HashMap::new(),
139 read_only_paths: vec![
140 PathBuf::from("/usr"),
141 PathBuf::from("/lib"),
142 PathBuf::from("/lib64"),
143 PathBuf::from("/bin"),
144 PathBuf::from("/sbin"),
145 PathBuf::from("/etc"),
146 ],
147 writable_paths: vec![PathBuf::from("/tmp")],
148 allow_dev_access: true,
149 allow_proc_access: true,
150 allow_sys_access: false,
151 env_whitelist: Vec::new(),
152 tmpfs_size: "100M".to_string(),
153 unshare_all: true,
154 die_with_parent: true,
155 new_session: true,
156 docker: None,
157 custom_args: Vec::new(),
158 audit_logging: None,
159 resource_limits: None,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166#[serde(rename_all = "lowercase")]
167pub enum SandboxPreset {
168 Strict,
170 Development,
172 Testing,
174 Production,
176 Docker,
178 Unrestricted,
180 WebScraping,
182 AiCode,
184}
185
186pub static SANDBOX_PRESETS: once_cell::sync::Lazy<HashMap<SandboxPreset, SandboxConfig>> =
188 once_cell::sync::Lazy::new(|| {
189 let mut presets = HashMap::new();
190
191 presets.insert(
193 SandboxPreset::Strict,
194 SandboxConfig {
195 enabled: true,
196 sandbox_type: SandboxType::Bubblewrap,
197 allowed_paths: Vec::new(),
198 denied_paths: vec![PathBuf::from("/home"), PathBuf::from("/root")],
199 network_access: false,
200 read_only_paths: vec![
201 PathBuf::from("/usr"),
202 PathBuf::from("/lib"),
203 PathBuf::from("/lib64"),
204 PathBuf::from("/bin"),
205 PathBuf::from("/sbin"),
206 PathBuf::from("/etc"),
207 ],
208 writable_paths: vec![PathBuf::from("/tmp")],
209 allow_dev_access: false,
210 allow_proc_access: false,
211 allow_sys_access: false,
212 tmpfs_size: "50M".to_string(),
213 resource_limits: Some(ResourceLimits {
214 max_memory: Some(512 * 1024 * 1024),
215 max_cpu: Some(50),
216 max_processes: Some(10),
217 max_file_size: Some(10 * 1024 * 1024),
218 max_execution_time: Some(60000),
219 max_file_descriptors: Some(100),
220 }),
221 ..Default::default()
222 },
223 );
224
225 presets.insert(
227 SandboxPreset::Development,
228 SandboxConfig {
229 enabled: true,
230 sandbox_type: SandboxType::Bubblewrap,
231 network_access: true,
232 allow_dev_access: true,
233 allow_proc_access: true,
234 tmpfs_size: "200M".to_string(),
235 resource_limits: Some(ResourceLimits {
236 max_memory: Some(2 * 1024 * 1024 * 1024),
237 max_cpu: Some(80),
238 max_processes: Some(50),
239 max_execution_time: Some(300000),
240 ..Default::default()
241 }),
242 ..Default::default()
243 },
244 );
245
246 presets.insert(
248 SandboxPreset::Testing,
249 SandboxConfig {
250 enabled: true,
251 sandbox_type: SandboxType::Bubblewrap,
252 network_access: true,
253 allow_dev_access: true,
254 allow_proc_access: true,
255 tmpfs_size: "200M".to_string(),
256 resource_limits: Some(ResourceLimits {
257 max_memory: Some(1024 * 1024 * 1024),
258 max_cpu: Some(75),
259 max_processes: Some(30),
260 max_execution_time: Some(120000),
261 ..Default::default()
262 }),
263 ..Default::default()
264 },
265 );
266
267 presets
268 });
269
270#[derive(Debug, Clone)]
272pub struct ValidationResult {
273 pub valid: bool,
275 pub errors: Vec<String>,
277 pub warnings: Vec<String>,
279}
280
281pub struct SandboxConfigManager {
283 config_dir: PathBuf,
285 config_file: PathBuf,
287 current_config: Arc<RwLock<SandboxConfig>>,
289}
290
291impl SandboxConfigManager {
292 pub fn new(config_dir: Option<PathBuf>) -> Self {
294 let config_dir = config_dir.unwrap_or_else(|| {
295 dirs::home_dir()
296 .unwrap_or_else(|| PathBuf::from("~"))
297 .join(".aster")
298 .join("sandbox")
299 });
300 let config_file = config_dir.join("config.json");
301 let current_config = Arc::new(RwLock::new(SandboxConfig::default()));
302
303 let mut manager = Self {
304 config_dir,
305 config_file,
306 current_config,
307 };
308 manager.load_config_sync();
309 manager
310 }
311
312 fn load_config_sync(&mut self) {
314 if let Ok(content) = std::fs::read_to_string(&self.config_file) {
315 if let Ok(config) = serde_json::from_str::<SandboxConfig>(&content) {
316 if let Ok(mut current) = self.current_config.write() {
317 *current = config;
318 }
319 }
320 }
321 }
322
323 pub async fn load_config(&self) -> anyhow::Result<SandboxConfig> {
325 let content = tokio::fs::read_to_string(&self.config_file).await?;
326 let config: SandboxConfig = serde_json::from_str(&content)?;
327 if let Ok(mut current) = self.current_config.write() {
328 *current = config.clone();
329 }
330 Ok(config)
331 }
332
333 pub fn validate_config(&self, config: &SandboxConfig) -> ValidationResult {
335 let errors = Vec::new();
336 let mut warnings = Vec::new();
337
338 if config.enabled && config.sandbox_type == SandboxType::Bubblewrap {
340 #[cfg(not(target_os = "linux"))]
341 warnings.push("Bubblewrap 仅在 Linux 上可用,沙箱将被禁用".to_string());
342 }
343
344 if config.enabled && config.sandbox_type == SandboxType::Seatbelt {
345 #[cfg(not(target_os = "macos"))]
346 warnings.push("Seatbelt 仅在 macOS 上可用,沙箱将被禁用".to_string());
347 }
348
349 for allowed in &config.allowed_paths {
351 for denied in &config.denied_paths {
352 if allowed.starts_with(denied) || denied.starts_with(allowed) {
353 warnings.push(format!(
354 "路径冲突: {} vs {}",
355 allowed.display(),
356 denied.display()
357 ));
358 }
359 }
360 }
361
362 if let Some(ref limits) = config.resource_limits {
364 if let Some(max_memory) = limits.max_memory {
365 if max_memory > 4 * 1024 * 1024 * 1024 {
366 warnings.push("max_memory > 4GB 可能在某些系统上导致问题".to_string());
367 }
368 }
369 }
370
371 ValidationResult {
372 valid: errors.is_empty(),
373 errors,
374 warnings,
375 }
376 }
377
378 pub fn merge_configs(
380 &self,
381 base: &SandboxConfig,
382 override_config: &SandboxConfig,
383 ) -> SandboxConfig {
384 SandboxConfig {
385 enabled: override_config.enabled,
386 sandbox_type: override_config.sandbox_type,
387 allowed_paths: if override_config.allowed_paths.is_empty() {
388 base.allowed_paths.clone()
389 } else {
390 override_config.allowed_paths.clone()
391 },
392 denied_paths: if override_config.denied_paths.is_empty() {
393 base.denied_paths.clone()
394 } else {
395 override_config.denied_paths.clone()
396 },
397 network_access: override_config.network_access,
398 environment_variables: {
399 let mut env = base.environment_variables.clone();
400 env.extend(override_config.environment_variables.clone());
401 env
402 },
403 read_only_paths: if override_config.read_only_paths.is_empty() {
404 base.read_only_paths.clone()
405 } else {
406 override_config.read_only_paths.clone()
407 },
408 writable_paths: if override_config.writable_paths.is_empty() {
409 base.writable_paths.clone()
410 } else {
411 override_config.writable_paths.clone()
412 },
413 allow_dev_access: override_config.allow_dev_access,
414 allow_proc_access: override_config.allow_proc_access,
415 allow_sys_access: override_config.allow_sys_access,
416 env_whitelist: if override_config.env_whitelist.is_empty() {
417 base.env_whitelist.clone()
418 } else {
419 override_config.env_whitelist.clone()
420 },
421 tmpfs_size: override_config.tmpfs_size.clone(),
422 unshare_all: override_config.unshare_all,
423 die_with_parent: override_config.die_with_parent,
424 new_session: override_config.new_session,
425 docker: override_config
426 .docker
427 .clone()
428 .or_else(|| base.docker.clone()),
429 custom_args: if override_config.custom_args.is_empty() {
430 base.custom_args.clone()
431 } else {
432 override_config.custom_args.clone()
433 },
434 audit_logging: override_config
435 .audit_logging
436 .clone()
437 .or_else(|| base.audit_logging.clone()),
438 resource_limits: override_config
439 .resource_limits
440 .clone()
441 .or_else(|| base.resource_limits.clone()),
442 }
443 }
444
445 pub fn get_preset(&self, preset: SandboxPreset) -> Option<SandboxConfig> {
447 SANDBOX_PRESETS.get(&preset).cloned()
448 }
449
450 pub fn get_config(&self) -> SandboxConfig {
452 self.current_config
453 .read()
454 .map(|c| c.clone())
455 .unwrap_or_default()
456 }
457
458 pub async fn update_config(&self, config: SandboxConfig) -> anyhow::Result<()> {
460 if let Ok(mut current) = self.current_config.write() {
461 *current = config;
462 }
463 self.save_config().await
464 }
465
466 pub async fn save_config(&self) -> anyhow::Result<()> {
468 tokio::fs::create_dir_all(&self.config_dir).await?;
469 let config = self.get_config();
470 let content = serde_json::to_string_pretty(&config)?;
471 tokio::fs::write(&self.config_file, content).await?;
472 Ok(())
473 }
474
475 pub async fn reset(&self) -> anyhow::Result<()> {
477 self.update_config(SandboxConfig::default()).await
478 }
479
480 pub fn is_path_allowed(&self, target_path: &std::path::Path) -> bool {
482 let config = self.get_config();
483
484 for denied in &config.denied_paths {
486 if target_path.starts_with(denied) {
487 return false;
488 }
489 }
490
491 if config.allowed_paths.is_empty() {
493 return true;
494 }
495
496 for allowed in &config.allowed_paths {
497 if target_path.starts_with(allowed) {
498 return true;
499 }
500 }
501
502 false
503 }
504
505 pub fn is_path_writable(&self, target_path: &std::path::Path) -> bool {
507 if !self.is_path_allowed(target_path) {
508 return false;
509 }
510
511 let config = self.get_config();
512 for writable in &config.writable_paths {
513 if target_path.starts_with(writable) {
514 return true;
515 }
516 }
517
518 false
519 }
520}