Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
run: cd server_manager && cargo build --verbose
- name: Run tests
run: cargo test --verbose
run: cd server_manager && cargo test --verbose
2 changes: 1 addition & 1 deletion server_manager/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions server_manager/src/core/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
net.core.somaxconn=4096
net.ipv4.tcp_fastopen=3
vm.max_map_count=262144
net.core.rmem_max=4194304
net.core.wmem_max=1048576
"#, swappiness);

let path = Path::new("/etc/sysctl.d/99-server-manager-optimization.conf");
Expand Down
14 changes: 7 additions & 7 deletions server_manager/src/services/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct VaultwardenService;
impl Service for VaultwardenService {
fn name(&self) -> &'static str { "vaultwarden" }
fn image(&self) -> &'static str { "vaultwarden/server:latest" }
fn ports(&self) -> Vec<String> { vec!["8001:80".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:8001:80".to_string()] }
fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert("ADMIN_TOKEN".to_string(), secrets.vaultwarden_admin_token.clone().unwrap_or_default());
Expand All @@ -25,7 +25,7 @@ pub struct FilebrowserService;
impl Service for FilebrowserService {
fn name(&self) -> &'static str { "filebrowser" }
fn image(&self) -> &'static str { "filebrowser/filebrowser:latest" }
fn ports(&self) -> Vec<String> { vec!["8002:80".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:8002:80".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec![
"./config/filebrowser:/config".to_string(),
Expand All @@ -39,7 +39,7 @@ pub struct YourlsService;
impl Service for YourlsService {
fn name(&self) -> &'static str { "yourls" }
fn image(&self) -> &'static str { "yourls/yourls:latest" }
fn ports(&self) -> Vec<String> { vec!["8003:80".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:8003:80".to_string()] }
fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert("YOURLS_DB_HOST".to_string(), "mariadb".to_string());
Expand All @@ -57,7 +57,7 @@ pub struct GLPIService;
impl Service for GLPIService {
fn name(&self) -> &'static str { "glpi" }
fn image(&self) -> &'static str { "diouxx/glpi:latest" } // Common community image, official docker-library is scarce
fn ports(&self) -> Vec<String> { vec!["8088:80".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:8088:80".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/glpi:/var/www/html/glpi".to_string()]
}
Expand All @@ -68,7 +68,7 @@ pub struct GiteaService;
impl Service for GiteaService {
fn name(&self) -> &'static str { "gitea" }
fn image(&self) -> &'static str { "gitea/gitea:latest" }
fn ports(&self) -> Vec<String> { vec!["3000:3000".to_string(), "2222:22".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:3000:3000".to_string(), "2222:22".to_string()] }
fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert("GITEA__database__DB_TYPE".to_string(), "mysql".to_string());
Expand All @@ -92,7 +92,7 @@ pub struct RoundcubeService;
impl Service for RoundcubeService {
fn name(&self) -> &'static str { "roundcube" }
fn image(&self) -> &'static str { "roundcube/roundcubemail:latest" }
fn ports(&self) -> Vec<String> { vec!["8090:80".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:8090:80".to_string()] }
fn env_vars(&self, _hw: &HardwareInfo, _secrets: &Secrets) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert("ROUNDCUBEMAIL_DB_TYPE".to_string(), "sqlite".to_string()); // Defaulting to sqlite as per memory hints or keeping simple.
Expand All @@ -114,7 +114,7 @@ pub struct NextcloudService;
impl Service for NextcloudService {
fn name(&self) -> &'static str { "nextcloud" }
fn image(&self) -> &'static str { "lscr.io/linuxserver/nextcloud:latest" }
fn ports(&self) -> Vec<String> { vec!["4443:443".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:4443:443".to_string()] }
fn configure(&self, _hw: &HardwareInfo, secrets: &Secrets) -> Result<()> {
let config_dir = Path::new("./config/nextcloud");
fs::create_dir_all(config_dir).context("Failed to create nextcloud config dir")?;
Expand Down
2 changes: 1 addition & 1 deletion server_manager/src/services/arr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ macro_rules! define_arr_service {
impl Service for $struct_name {
fn name(&self) -> &'static str { $name }
fn image(&self) -> &'static str { $image }
fn ports(&self) -> Vec<String> { vec![format!("{}:{}", $port, $port)] }
fn ports(&self) -> Vec<String> { vec![format!("127.0.0.1:{}:{}", $port, $port)] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec![
format!("./config/{}:/config", $name),
Expand Down
2 changes: 1 addition & 1 deletion server_manager/src/services/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ impl Service for QBittorrentService {
fn image(&self) -> &'static str { "lscr.io/linuxserver/qbittorrent:latest" }

fn ports(&self) -> Vec<String> {
vec!["8080:8080".to_string(), "6881:6881".to_string(), "6881:6881/udp".to_string()]
vec!["127.0.0.1:8080:8080".to_string(), "6881:6881".to_string(), "6881:6881/udp".to_string()]
}

fn env_vars(&self, hw: &HardwareInfo, _secrets: &Secrets) -> HashMap<String, String> {
Expand Down
28 changes: 22 additions & 6 deletions server_manager/src/services/infra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ pub struct MariaDBService;
impl Service for MariaDBService {
fn name(&self) -> &'static str { "mariadb" }
fn image(&self) -> &'static str { "lscr.io/linuxserver/mariadb:latest" }
fn ports(&self) -> Vec<String> { vec!["3306:3306".to_string()] }
fn ports(&self) -> Vec<String> { vec![] } // Internal only

fn configure(&self, _hw: &HardwareInfo, secrets: &Secrets) -> Result<()> {
fn configure(&self, hw: &HardwareInfo, secrets: &Secrets) -> Result<()> {
let init_dir = Path::new("./config/mariadb/initdb.d");
fs::create_dir_all(init_dir).context("Failed to create mariadb initdb.d")?;

Expand Down Expand Up @@ -46,6 +46,22 @@ impl Service for MariaDBService {

fs::write(init_dir.join("init.sql"), sql).context("Failed to write init.sql")?;

// Optimization: Generate custom.cnf
let (buffer_pool, log_file_size, max_connections) = match hw.profile {
HardwareProfile::High => ("4G", "1G", "500"),
HardwareProfile::Standard => ("1G", "256M", "100"),
HardwareProfile::Low => ("256M", "64M", "50"),
};

let custom_cnf = format!(r#"[mysqld]
innodb_buffer_pool_size={}
innodb_log_file_size={}
max_connections={}
"#, buffer_pool, log_file_size, max_connections);

// Parent dir is ./config/mariadb/
fs::write(init_dir.parent().unwrap().join("custom.cnf"), custom_cnf).context("Failed to write custom.cnf")?;

Ok(())
}

Expand Down Expand Up @@ -83,7 +99,7 @@ pub struct RedisService;
impl Service for RedisService {
fn name(&self) -> &'static str { "redis" }
fn image(&self) -> &'static str { "redis:alpine" }
fn ports(&self) -> Vec<String> { vec!["6379:6379".to_string()] }
fn ports(&self) -> Vec<String> { vec![] } // Internal only
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/redis:/data".to_string()]
}
Expand Down Expand Up @@ -149,7 +165,7 @@ pub struct PortainerService;
impl Service for PortainerService {
fn name(&self) -> &'static str { "portainer" }
fn image(&self) -> &'static str { "portainer/portainer-ce:latest" }
fn ports(&self) -> Vec<String> { vec!["9000:9000".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:9000:9000".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["/var/run/docker.sock:/var/run/docker.sock".to_string(), "./config/portainer:/data".to_string()]
}
Expand All @@ -160,7 +176,7 @@ pub struct NetdataService;
impl Service for NetdataService {
fn name(&self) -> &'static str { "netdata" }
fn image(&self) -> &'static str { "netdata/netdata" }
fn ports(&self) -> Vec<String> { vec!["19999:19999".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:19999:19999".to_string()] }
fn cap_add(&self) -> Vec<String> { vec!["SYS_PTRACE".to_string()] }
fn security_opts(&self) -> Vec<String> { vec!["apparmor:unconfined".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
Expand All @@ -176,7 +192,7 @@ pub struct UptimeKumaService;
impl Service for UptimeKumaService {
fn name(&self) -> &'static str { "uptime-kuma" }
fn image(&self) -> &'static str { "louislam/uptime-kuma:1" }
fn ports(&self) -> Vec<String> { vec!["3001:3001".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:3001:3001".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/uptime-kuma:/app/data".to_string()]
}
Expand Down
4 changes: 2 additions & 2 deletions server_manager/src/services/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub struct TautulliService;
impl Service for TautulliService {
fn name(&self) -> &'static str { "tautulli" }
fn image(&self) -> &'static str { "lscr.io/linuxserver/tautulli:latest" }
fn ports(&self) -> Vec<String> { vec!["8181:8181".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:8181:8181".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/tautulli:/config".to_string()]
}
Expand All @@ -95,7 +95,7 @@ pub struct OverseerrService;
impl Service for OverseerrService {
fn name(&self) -> &'static str { "overseerr" }
fn image(&self) -> &'static str { "lscr.io/linuxserver/overseerr:latest" }
fn ports(&self) -> Vec<String> { vec!["5055:5055".to_string()] }
fn ports(&self) -> Vec<String> { vec!["127.0.0.1:5055:5055".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/overseerr:/config".to_string()]
}
Expand Down
38 changes: 38 additions & 0 deletions server_manager/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,42 @@ fn test_generate_compose_structure() {
assert!(plex_nets.contains(&serde_yaml::Value::from("server_manager_net")));
}

#[test]
fn test_security_bindings() {
// Test that sensitive services are bound to localhost and internal DBs have no ports
let hw = HardwareInfo {
profile: HardwareProfile::Standard,
ram_gb: 8,
cpu_cores: 4,
has_nvidia: false,
has_intel_quicksync: false,
disk_gb: 512,
swap_gb: 2,
user_id: "1000".to_string(),
group_id: "1000".to_string(),
};
let secrets = Secrets::default();

let result = build_compose_structure(&hw, &secrets).unwrap();
let services = result.get(&serde_yaml::Value::from("services")).unwrap().as_mapping().unwrap();

// 1. MariaDB should have NO ports
let mariadb = services.get(&serde_yaml::Value::from("mariadb")).unwrap().as_mapping().unwrap();
assert!(!mariadb.contains_key(&serde_yaml::Value::from("ports")), "MariaDB should not expose ports");

// 2. Sonarr should be bound to 127.0.0.1
let sonarr = services.get(&serde_yaml::Value::from("sonarr")).unwrap().as_mapping().unwrap();
let ports = sonarr.get(&serde_yaml::Value::from("ports")).unwrap().as_sequence().unwrap();
let port_str = ports[0].as_str().unwrap();
assert!(port_str.starts_with("127.0.0.1:"), "Sonarr port should be bound to localhost: {}", port_str);

// 3. Plex should still be exposed (host mapping implied or explicit 0.0.0.0)
let plex = services.get(&serde_yaml::Value::from("plex")).unwrap().as_mapping().unwrap();
let ports = plex.get(&serde_yaml::Value::from("ports")).unwrap().as_sequence().unwrap();
let port_str = ports[0].as_str().unwrap();
assert!(!port_str.starts_with("127.0.0.1:"), "Plex port should be exposed: {}", port_str);
}

#[test]
fn test_profile_logic_low() {
// Test that Low profile disables SpamAssassin in MailService
Expand Down Expand Up @@ -121,6 +157,8 @@ fn test_resource_generation() {
has_intel_quicksync: false,
disk_gb: 1000,
swap_gb: 4,
user_id: "1000".to_string(),
group_id: "1000".to_string(),
};
let secrets = Secrets::default();

Expand Down