diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index 0245839..d72c124 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -73,7 +73,38 @@ android { if (keystorePassword.isEmpty()) { println("WARNING: KEYSTORE_PASSWORD not set in local.properties or gradle.properties, using default debug keystore") } + + // Ensure debug keystore exists (CI environment might lack default ~/.android/debug.keystore) + val debugConfig = getByName("debug") + var targetStoreFile = debugConfig.storeFile + + if (targetStoreFile == null || !targetStoreFile!!.exists()) { + val localKeystore = rootProject.file("debug.keystore") + if (!localKeystore.exists()) { + println("Generating temporary debug keystore at ${localKeystore.absolutePath}...") + try { + exec { + commandLine("keytool", "-genkey", "-v", + "-keystore", localKeystore.absolutePath, + "-storepass", "android", + "-alias", "androiddebugkey", + "-keypass", "android", + "-keyalg", "RSA", + "-keysize", "2048", + "-validity", "10000", + "-dname", "CN=Android Debug,O=Android,C=US") + } + } catch (e: Exception) { + println("Warning: Failed to generate debug keystore: ${e.message}") + } + } + targetStoreFile = localKeystore + } + getByName("debug") { + if (targetStoreFile != null && targetStoreFile!!.exists()) { + storeFile = targetStoreFile + } storePassword = "android" keyAlias = "androiddebugkey" keyPassword = "android" @@ -92,7 +123,8 @@ android { // Enable R8 full mode for maximum optimization isDebuggable = false isJniDebuggable = false - signingConfig = signingConfigs.getByName("release") + // Use release config if it exists (created above), otherwise fallback to debug (safe default) + signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug") } debug { // Disable minification in debug for faster builds diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/NetworkModeSelector.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/NetworkModeSelector.kt new file mode 100644 index 0000000..713d7c2 --- /dev/null +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/NetworkModeSelector.kt @@ -0,0 +1,82 @@ +package com.droidspaces.app.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.droidspaces.app.R + +@Composable +fun NetworkModeSelector( + networkMode: String, + onModeSelected: (String) -> Unit +) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Wifi, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text( + text = stringResource(R.string.network_mode_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(R.string.network_mode_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + val modes = listOf( + "host" to stringResource(R.string.network_mode_host), + "nat" to stringResource(R.string.network_mode_nat), + "macvlan" to stringResource(R.string.network_mode_macvlan) + ) + + modes.forEach { (mode, label) -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { onModeSelected(mode) } + ) { + RadioButton( + selected = (networkMode == mode), + onClick = { onModeSelected(mode) } + ) + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + } +} diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt b/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt index 2e465cf..cbe7e56 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt @@ -287,13 +287,15 @@ fun DroidspacesNavigation( initialEnableIPv6 = viewModel.enableIPv6, initialEnableAndroidStorage = viewModel.enableAndroidStorage, initialEnableHwAccess = viewModel.enableHwAccess, + initialEnableSensors = viewModel.enableSensors, + initialNetworkMode = viewModel.networkMode, initialSelinuxPermissive = viewModel.selinuxPermissive, initialVolatileMode = viewModel.volatileMode, initialBindMounts = viewModel.bindMounts, initialDnsServers = viewModel.dnsServers, initialRunAtBoot = viewModel.runAtBoot, - onNext = { enableIPv6, enableAndroidStorage, enableHwAccess, selinuxPermissive, volatileMode, bindMounts, dnsServers, runAtBoot -> - viewModel.setConfig(enableIPv6, enableAndroidStorage, enableHwAccess, selinuxPermissive, volatileMode, bindMounts, dnsServers, runAtBoot) + onNext = { enableIPv6, enableAndroidStorage, enableHwAccess, enableSensors, networkMode, selinuxPermissive, volatileMode, bindMounts, dnsServers, runAtBoot -> + viewModel.setConfig(enableIPv6, enableAndroidStorage, enableHwAccess, enableSensors, networkMode, selinuxPermissive, volatileMode, bindMounts, dnsServers, runAtBoot) navController.navigate(Screen.SparseImageConfig.route) }, onBack = { diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerConfigScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerConfigScreen.kt index 87375ee..e094b65 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerConfigScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerConfigScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.droidspaces.app.ui.component.ToggleCard +import com.droidspaces.app.ui.component.NetworkModeSelector import androidx.compose.ui.platform.LocalContext import com.droidspaces.app.R @@ -27,6 +28,8 @@ fun ContainerConfigScreen( initialEnableIPv6: Boolean = false, initialEnableAndroidStorage: Boolean = false, initialEnableHwAccess: Boolean = false, + initialEnableSensors: Boolean = false, + initialNetworkMode: String = "host", initialSelinuxPermissive: Boolean = false, initialVolatileMode: Boolean = false, initialBindMounts: List = emptyList(), @@ -36,6 +39,8 @@ fun ContainerConfigScreen( enableIPv6: Boolean, enableAndroidStorage: Boolean, enableHwAccess: Boolean, + enableSensors: Boolean, + networkMode: String, selinuxPermissive: Boolean, volatileMode: Boolean, bindMounts: List, @@ -47,6 +52,8 @@ fun ContainerConfigScreen( var enableIPv6 by remember { mutableStateOf(initialEnableIPv6) } var enableAndroidStorage by remember { mutableStateOf(initialEnableAndroidStorage) } var enableHwAccess by remember { mutableStateOf(initialEnableHwAccess) } + var enableSensors by remember { mutableStateOf(initialEnableSensors) } + var networkMode by remember { mutableStateOf(initialNetworkMode) } var selinuxPermissive by remember { mutableStateOf(initialSelinuxPermissive) } var volatileMode by remember { mutableStateOf(initialVolatileMode) } var bindMounts by remember { mutableStateOf(initialBindMounts) } @@ -123,7 +130,7 @@ fun ContainerConfigScreen( ) { Button( onClick = { - onNext(enableIPv6, enableAndroidStorage, enableHwAccess, selinuxPermissive, volatileMode, bindMounts, dnsServers, runAtBoot) + onNext(enableIPv6, enableAndroidStorage, enableHwAccess, enableSensors, networkMode, selinuxPermissive, volatileMode, bindMounts, dnsServers, runAtBoot) }, modifier = Modifier .fillMaxWidth() @@ -199,6 +206,19 @@ fun ContainerConfigScreen( onCheckedChange = { enableHwAccess = it } ) + ToggleCard( + icon = Icons.Default.BatteryChargingFull, + title = context.getString(R.string.enable_sensors), + description = context.getString(R.string.enable_sensors_description), + checked = enableSensors, + onCheckedChange = { enableSensors = it } + ) + + NetworkModeSelector( + networkMode = networkMode, + onModeSelected = { networkMode = it } + ) + ToggleCard( icon = Icons.Default.Security, title = context.getString(R.string.selinux_permissive), diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/EditContainerScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/EditContainerScreen.kt index 3c81c09..d220e8d 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/EditContainerScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/EditContainerScreen.kt @@ -26,6 +26,7 @@ import com.droidspaces.app.ui.util.ClearFocusOnClickOutside import com.droidspaces.app.ui.util.FocusUtils import androidx.compose.foundation.clickable import com.droidspaces.app.ui.component.ToggleCard +import com.droidspaces.app.ui.component.NetworkModeSelector import com.droidspaces.app.util.ContainerInfo import com.droidspaces.app.util.ContainerManager import com.droidspaces.app.util.SystemInfoManager @@ -56,6 +57,8 @@ fun EditContainerScreen( var enableIPv6 by remember { mutableStateOf(container.enableIPv6) } var enableAndroidStorage by remember { mutableStateOf(container.enableAndroidStorage) } var enableHwAccess by remember { mutableStateOf(container.enableHwAccess) } + var enableSensors by remember { mutableStateOf(container.enableSensors) } + var networkMode by remember { mutableStateOf(container.networkMode) } var selinuxPermissive by remember { mutableStateOf(container.selinuxPermissive) } var volatileMode by remember { mutableStateOf(container.volatileMode) } var bindMounts by remember { mutableStateOf(container.bindMounts) } @@ -67,6 +70,8 @@ fun EditContainerScreen( var savedEnableIPv6 by remember { mutableStateOf(container.enableIPv6) } var savedEnableAndroidStorage by remember { mutableStateOf(container.enableAndroidStorage) } var savedEnableHwAccess by remember { mutableStateOf(container.enableHwAccess) } + var savedEnableSensors by remember { mutableStateOf(container.enableSensors) } + var savedNetworkMode by remember { mutableStateOf(container.networkMode) } var savedSelinuxPermissive by remember { mutableStateOf(container.selinuxPermissive) } var savedVolatileMode by remember { mutableStateOf(container.volatileMode) } var savedBindMounts by remember { mutableStateOf(container.bindMounts) } @@ -90,6 +95,8 @@ fun EditContainerScreen( enableIPv6 != savedEnableIPv6 || enableAndroidStorage != savedEnableAndroidStorage || enableHwAccess != savedEnableHwAccess || + enableSensors != savedEnableSensors || + networkMode != savedNetworkMode || selinuxPermissive != savedSelinuxPermissive || volatileMode != savedVolatileMode || bindMounts != savedBindMounts || @@ -118,6 +125,8 @@ fun EditContainerScreen( enableIPv6 = enableIPv6, enableAndroidStorage = enableAndroidStorage, enableHwAccess = enableHwAccess, + enableSensors = enableSensors, + networkMode = networkMode, selinuxPermissive = selinuxPermissive, volatileMode = volatileMode, bindMounts = bindMounts, @@ -137,6 +146,8 @@ fun EditContainerScreen( savedEnableIPv6 = enableIPv6 savedEnableAndroidStorage = enableAndroidStorage savedEnableHwAccess = enableHwAccess + savedEnableSensors = enableSensors + savedNetworkMode = networkMode savedSelinuxPermissive = selinuxPermissive savedVolatileMode = volatileMode savedBindMounts = bindMounts @@ -410,6 +421,25 @@ fun EditContainerScreen( } ) + ToggleCard( + icon = Icons.Default.BatteryChargingFull, + title = context.getString(R.string.enable_sensors), + description = context.getString(R.string.enable_sensors_description), + checked = enableSensors, + onCheckedChange = { + clearFocus() + enableSensors = it + } + ) + + NetworkModeSelector( + networkMode = networkMode, + onModeSelected = { + clearFocus() + networkMode = it + } + ) + ToggleCard( icon = Icons.Default.Security, title = context.getString(R.string.selinux_permissive), diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/ContainerInstallationViewModel.kt b/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/ContainerInstallationViewModel.kt index 04523c2..dd6acd2 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/ContainerInstallationViewModel.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/ContainerInstallationViewModel.kt @@ -31,6 +31,12 @@ class ContainerInstallationViewModel : ViewModel() { var enableHwAccess: Boolean by mutableStateOf(false) private set + var enableSensors: Boolean by mutableStateOf(false) + private set + + var networkMode: String by mutableStateOf("host") + private set + var selinuxPermissive: Boolean by mutableStateOf(false) private set @@ -70,6 +76,8 @@ class ContainerInstallationViewModel : ViewModel() { enableIPv6: Boolean, enableAndroidStorage: Boolean, enableHwAccess: Boolean, + enableSensors: Boolean, + networkMode: String, selinuxPermissive: Boolean, volatileMode: Boolean, bindMounts: List, @@ -79,6 +87,8 @@ class ContainerInstallationViewModel : ViewModel() { this.enableIPv6 = enableIPv6 this.enableAndroidStorage = enableAndroidStorage this.enableHwAccess = enableHwAccess + this.enableSensors = enableSensors + this.networkMode = networkMode this.selinuxPermissive = selinuxPermissive this.volatileMode = volatileMode this.bindMounts = bindMounts @@ -101,6 +111,8 @@ class ContainerInstallationViewModel : ViewModel() { enableIPv6 = enableIPv6, enableAndroidStorage = enableAndroidStorage, enableHwAccess = enableHwAccess, + enableSensors = enableSensors, + networkMode = networkMode, selinuxPermissive = selinuxPermissive, volatileMode = volatileMode, bindMounts = bindMounts, @@ -119,6 +131,8 @@ class ContainerInstallationViewModel : ViewModel() { enableIPv6 = false enableAndroidStorage = false enableHwAccess = false + enableSensors = false + networkMode = "host" selinuxPermissive = false volatileMode = false bindMounts = emptyList() diff --git a/Android/app/src/main/java/com/droidspaces/app/util/ContainerCommandBuilder.kt b/Android/app/src/main/java/com/droidspaces/app/util/ContainerCommandBuilder.kt index bc6867b..030f3aa 100644 --- a/Android/app/src/main/java/com/droidspaces/app/util/ContainerCommandBuilder.kt +++ b/Android/app/src/main/java/com/droidspaces/app/util/ContainerCommandBuilder.kt @@ -60,6 +60,14 @@ object ContainerCommandBuilder { parts.add("--hw-access") } + if (container.enableSensors) { + parts.add("--sensors") + } + + if (container.networkMode != "host") { + parts.add("--network-mode=${quote(container.networkMode)}") + } + if (container.selinuxPermissive) { parts.add("--selinux-permissive") } @@ -114,6 +122,8 @@ object ContainerCommandBuilder { if (container.enableIPv6) parts.add("--enable-ipv6") if (container.enableAndroidStorage) parts.add("--enable-android-storage") if (container.enableHwAccess) parts.add("--hw-access") + if (container.enableSensors) parts.add("--sensors") + if (container.networkMode != "host") parts.add("--network-mode=${quote(container.networkMode)}") if (container.selinuxPermissive) parts.add("--selinux-permissive") if (container.volatileMode) parts.add("-V") diff --git a/Android/app/src/main/java/com/droidspaces/app/util/ContainerManager.kt b/Android/app/src/main/java/com/droidspaces/app/util/ContainerManager.kt index 6ffafc1..010b283 100644 --- a/Android/app/src/main/java/com/droidspaces/app/util/ContainerManager.kt +++ b/Android/app/src/main/java/com/droidspaces/app/util/ContainerManager.kt @@ -25,6 +25,8 @@ data class ContainerInfo( val enableIPv6: Boolean = false, val enableAndroidStorage: Boolean = false, val enableHwAccess: Boolean = false, + val enableSensors: Boolean = false, + val networkMode: String = "host", val selinuxPermissive: Boolean = false, val volatileMode: Boolean = false, val bindMounts: List = emptyList(), @@ -48,6 +50,8 @@ data class ContainerInfo( appendLine("enable_ipv6=${if (enableIPv6) "1" else "0"}") appendLine("enable_android_storage=${if (enableAndroidStorage) "1" else "0"}") appendLine("enable_hw_access=${if (enableHwAccess) "1" else "0"}") + appendLine("enable_sensors=${if (enableSensors) "1" else "0"}") + appendLine("network_mode=$networkMode") appendLine("selinux_permissive=${if (selinuxPermissive) "1" else "0"}") appendLine("volatile_mode=${if (volatileMode) "1" else "0"}") if (bindMounts.isNotEmpty()) { @@ -209,6 +213,8 @@ object ContainerManager { enableIPv6 = configMap["enable_ipv6"] == "1", enableAndroidStorage = configMap["enable_android_storage"] == "1", enableHwAccess = configMap["enable_hw_access"] == "1", + enableSensors = configMap["enable_sensors"] == "1", + networkMode = configMap["network_mode"] ?: "host", selinuxPermissive = configMap["selinux_permissive"] == "1", volatileMode = configMap["volatile_mode"] == "1", bindMounts = bindMounts, diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index ce65b48..4132ca0 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -155,8 +155,15 @@ Enable IPv6 networking Android Storage Mount Android storage - Hardware Access - Full hardware access + GPU / Hardware Access + Enable GPU acceleration and hardware access + Enable Sensors + Expose battery and thermal sensors + Network Mode + Choose how the container connects to the network. + Host (Shared IP) + NAT (Private IP / Fake MAC) + Macvlan (Bridge) SELinux Permissive Set SELinux permissive Run at Boot diff --git a/Makefile b/Makefile index 2638da5..7410ec5 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ SRCS = $(SRC_DIR)/main.c \ # Compiler flags — hardened warning set, all warnings are errors CFLAGS = -Wall -Wextra -Wpedantic -Werror -O2 -flto -std=gnu99 -I$(SRC_DIR) -no-pie -pthread -CFLAGS += -Wformat=2 -Wformat-security -Wformat-overflow=2 -Wformat-truncation=2 +CFLAGS += -Wformat=2 -Wformat-security CFLAGS += -Wnull-dereference -Wcast-qual -Wlogical-op CFLAGS += -Wduplicated-cond -Wduplicated-branches -Wimplicit-fallthrough=3 LDFLAGS = -static -no-pie -flto -pthread @@ -50,6 +50,8 @@ find-cc = $(shell \ echo "$(1)-gcc"; \ elif [ -d "/opt/cross/bin" ] && [ -f "/opt/cross/bin/$(1)-gcc" ]; then \ echo "/opt/cross/bin/$(1)-gcc"; \ + elif [ -f "$(HOME_VAR)/toolchains/$(1)/bin/$(1)-gcc" ]; then \ + echo "$(HOME_VAR)/toolchains/$(1)/bin/$(1)-gcc"; \ else \ echo ""; \ fi) @@ -139,10 +141,10 @@ x86: all-build: @echo "[*] Building for all architectures..." @rm -rf $(OUT_DIR) - @$(MAKE) --no-print-directory x86_64 && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-x86_64 || echo "✗ x86_64 failed" - @$(MAKE) --no-print-directory aarch64 && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-aarch64 || echo "✗ aarch64 failed" - @$(MAKE) --no-print-directory armhf && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-armhf || echo "✗ armhf failed" - @$(MAKE) --no-print-directory x86 && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-x86 || echo "✗ x86 failed" + @$(MAKE) --no-print-directory x86_64 && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-x86_64 || exit 1 + @$(MAKE) --no-print-directory aarch64 && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-aarch64 || exit 1 + @$(MAKE) --no-print-directory armhf && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-armhf || exit 1 + @$(MAKE) --no-print-directory x86 && mv $(OUT_DIR)/$(BINARY_NAME) $(OUT_DIR)/$(BINARY_NAME)-x86 || exit 1 @echo "[+] All architectures built successfully in $(OUT_DIR)/" tarball: diff --git a/src/android.c b/src/android.c index 703d2bb..d288036 100644 --- a/src/android.c +++ b/src/android.c @@ -121,24 +121,18 @@ void android_configure_iptables(void) { if (!is_android()) return; - ds_log("Configuring iptables for container networking..."); - - char *cmds[][32] = {{"iptables", "-t", "filter", "-F", NULL}, - {"ip6tables", "-t", "filter", "-F", NULL}, - {"iptables", "-P", "FORWARD", "ACCEPT", NULL}, - {"iptables", "-t", "nat", "-A", "POSTROUTING", "-s", - "10.0.3.0/24", "!", "-d", "10.0.3.0/24", "-j", - "MASQUERADE", NULL}, - {"iptables", "-t", "nat", "-A", "OUTPUT", "-p", "tcp", - "-d", "127.0.0.1", "-m", "tcp", "--dport", "1:65535", - "-j", "REDIRECT", "--to-ports", "1-65535", NULL}, - {"iptables", "-t", "nat", "-A", "OUTPUT", "-p", "udp", - "-d", "127.0.0.1", "-m", "udp", "--dport", "1:65535", - "-j", "REDIRECT", "--to-ports", "1-65535", NULL}}; - - for (size_t i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++) { - run_command_quiet(cmds[i]); - } + /* + * Droidspaces uses shared network namespace (CLONE_NEWNET is NOT used). + * Therefore, iptables rules are applied to the host's global network stack. + * + * Previously, we flushed the filter table ('iptables -F') which broke Android's + * connectivity (WiFi/Cellular) by removing system rules. We also added NAT/REDIRECT + * rules for a non-existent 10.0.3.0/24 subnet. + * + * For shared networking, no iptables rules are needed. Containers share localhost + * and external interfaces directly. + */ + ds_log("Shared network mode active: Skipping iptables configuration to preserve host connectivity."); } /* --------------------------------------------------------------------------- diff --git a/src/boot.c b/src/boot.c index d1dbb40..8cecd0f 100644 --- a/src/boot.c +++ b/src/boot.c @@ -69,7 +69,7 @@ int internal_boot(struct ds_config *cfg) { } /* 8. Setup /dev (device nodes, devtmpfs) */ - if (setup_dev(".", cfg->hw_access) < 0) { + if (setup_dev(".", cfg) < 0) { ds_error("Failed to setup /dev."); return -1; } @@ -145,6 +145,22 @@ int internal_boot(struct ds_config *cfg) { } } + /* Expose sensors (battery/thermal) as RW bind mounts (pinned) */ + if (cfg->sensors) { + ds_log("Sensors: Exposing battery and thermal info..."); + const char *sensor_paths[] = {"sys/class/power_supply", "sys/class/thermal", + NULL}; + for (int i = 0; sensor_paths[i]; i++) { + if (access(sensor_paths[i], F_OK) == 0) { + /* Pin it as RW mount so it survives the RO remount of parent /sys */ + if (mount(sensor_paths[i], sensor_paths[i], NULL, MS_BIND | MS_REC, + NULL) < 0) { + ds_warn("Failed to expose sensor path: %s", sensor_paths[i]); + } + } + } + } + if (mount(NULL, "sys", NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL) < 0) { ds_warn("Failed to remount /sys as read-only: %s", strerror(errno)); } @@ -251,7 +267,7 @@ int internal_boot(struct ds_config *cfg) { /* Sticky permissions again just in case systemd's TTYReset stripped them */ fchmod(console_fd, 0620); - fchown(console_fd, 0, 5); + if (fchown(console_fd, 0, 5) < 0) { /* ignore */ } if (console_fd > 2) close(console_fd); } diff --git a/src/cgroup.c b/src/cgroup.c index 7072024..bc8514d 100644 --- a/src/cgroup.c +++ b/src/cgroup.c @@ -289,7 +289,7 @@ int setup_cgroups(void) { snprintf(link_path, sizeof(link_path), "sys/fs/cgroup/%s", tok); if (strcmp(tok, suffix) != 0) { if (access(link_path, F_OK) != 0) { - symlink(suffix, link_path); + if (symlink(suffix, link_path) < 0) { /* ignore */ } } } tok = strtok_r(NULL, ",", &saveptr); diff --git a/src/check.c b/src/check.c index e51e7a5..5b1ef70 100644 --- a/src/check.c +++ b/src/check.c @@ -181,6 +181,65 @@ int check_requirements(void) { return 0; } +/* --------------------------------------------------------------------------- + * GPU Check Command + * ---------------------------------------------------------------------------*/ + +static void check_gpu_node(const char *path, const char *desc) { + int exists = (access(path, F_OK) == 0); + int rw = (access(path, R_OK | W_OK) == 0); + + const char *status_color = exists ? (rw ? C_GREEN : C_YELLOW) : C_RED; + const char *status_icon = exists ? (rw ? "✓" : "!") : "✗"; + const char *perm_str = exists ? (rw ? "RW" : "RO/Restricted") : "Not Found"; + + check_append(" [%s%s%s] %-20s : %s (%s)\n", + status_color, status_icon, C_RESET, + path, desc, perm_str); +} + +void print_gpu_check(void) { + check_buf_pos = 0; + check_buf[0] = '\0'; + + check_root(); /* Update is_root status */ + + check_append("\n" C_BOLD "Droidspaces GPU Compatibility Check" C_RESET "\n"); + if (!is_root) { + check_append(C_YELLOW "Warning: Running as non-root. Permission checks may fail." C_RESET "\n"); + } + check_append("\n"); + + check_append(C_BOLD "ARM Mali (Pixel/Exynos/MediaTek):" C_RESET "\n"); + check_gpu_node("/dev/mali0", "Mali GPU Device"); + check_gpu_node("/dev/mali", "Legacy Mali Device"); + + check_append("\n" C_BOLD "Qualcomm Adreno:" C_RESET "\n"); + check_gpu_node("/dev/kgsl-3d0", "Adreno GPU Device"); + check_gpu_node("/dev/genlock", "Genlock Device"); + + check_append("\n" C_BOLD "Standard Linux DRI (Mesa/Turnip):" C_RESET "\n"); + check_gpu_node("/dev/dri/card0", "DRI Card 0"); + check_gpu_node("/dev/dri/renderD128", "DRI Render Node"); + + check_append("\n" C_BOLD "DMA Buffers (Required for newer drivers):" C_RESET "\n"); + check_gpu_node("/dev/dma_heap/system", "System Heap"); + check_gpu_node("/dev/dma_heap/linux,cma", "CMA Heap"); + check_gpu_node("/dev/udmabuf", "DMA Buffer Sharing"); + + check_append("\n" C_BOLD "Other Accelerators:" C_RESET "\n"); + check_gpu_node("/dev/edgetpu", "EdgeTPU (Tensor)"); + check_gpu_node("/dev/video0", "Video V4L2 Device"); + + check_append("\n" C_BOLD "Summary:" C_RESET "\n"); + check_append(" To use GPU in container, enable " C_GREEN "GPU / Hardware Access" C_RESET " in the app,\n"); + check_append(" or run with: " C_GREEN "--gpu" C_RESET " (alias: --hw-access)\n"); + check_append(" This will expose these devices and fix permissions automatically.\n\n"); + + fwrite(check_buf, 1, check_buf_pos, stdout); + fflush(stdout); +} + /* --------------------------------------------------------------------------- * Detailed 'check' command * ---------------------------------------------------------------------------*/ diff --git a/src/container.c b/src/container.c index e8195d2..2a3c797 100644 --- a/src/container.c +++ b/src/container.c @@ -334,10 +334,22 @@ int start_rootfs(struct ds_config *cfg) { } /* 4. Pipe for synchronization */ - int sync_pipe[2]; + /* Main creates sync_pipe. Main reads [0]. Monitor forks Init. + * Init writes its PID to [1]. monitor_pipe is for Monitor->Init sync. + * init_ready_pipe is for Init->Monitor sync (NetNS ready). */ + int sync_pipe[2]; /* Init -> Main (sends PID) */ if (pipe(sync_pipe) < 0) ds_die("pipe failed: %s", strerror(errno)); + int monitor_pipe[2]; /* Monitor -> Init (sends "Network Ready" signal) */ + if (pipe(monitor_pipe) < 0) + ds_die("pipe failed: %s", strerror(errno)); + + /* Init -> Monitor (sends "Network Unshared" signal) to sync race */ + int init_ready_pipe[2]; + if (pipe(init_ready_pipe) < 0) + ds_die("pipe failed: %s", strerror(errno)); + /* 5. Configure host-side networking (NAT, ip_forward, DNS) BEFORE fork. * This eliminates the race condition where the child boots and reads * DNS before the parent has written it. */ @@ -352,6 +364,9 @@ int start_rootfs(struct ds_config *cfg) { if (monitor_pid == 0) { /* MONITOR PROCESS */ close(sync_pipe[0]); + close(monitor_pipe[0]); /* Write end only */ + close(init_ready_pipe[1]); /* Read end only */ + if (setsid() < 0 && errno != EPERM) { /* Fatal only if it's not EPERM (which means already leader) */ ds_error("setsid failed: %s", strerror(errno)); @@ -362,7 +377,10 @@ int start_rootfs(struct ds_config *cfg) { /* Unshare namespaces - Monitor enters new UTS, IPC, and optionally Cgroup * namespaces immediately. PID namespace unshare means only CHILDREN of the * monitor will be in the new PID NS. Node: we no longer unshare MNT here so - * monitor can cleanup host mounts. */ + * monitor can cleanup host mounts. + * Note: We intentionally do NOT unshare CLONE_NEWNET in the monitor. + * The monitor must remain in the host network namespace to configure + * veth pairs and NAT rules. The child (init) will unshare netns itself. */ int ns_flags = CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID; /* Adaptive Cgroup Namespace (introduced in Linux 4.6) */ @@ -400,17 +418,133 @@ int start_rootfs(struct ds_config *cfg) { if (init_pid == 0) { /* CONTAINER INIT */ + /* sync_pipe[0] was already closed by Monitor before forking, + * but good practice to ensure we don't hold it if logic changes. + * However, we'll remove it here to be precise. */ + /* close(sync_pipe[0]); */ + close(monitor_pipe[1]); + close(init_ready_pipe[0]); + + /* Close PTY masters inherited from parent to prevent hangs/leaks */ + if (cfg->console.master >= 0) close(cfg->console.master); + for (int i = 0; i < cfg->tty_count; i++) { + if (cfg->ttys[i].master >= 0) close(cfg->ttys[i].master); + } + + /* Unshare Network Namespace if requested */ + if (cfg->net_mode != DS_NET_HOST) { + ds_log("INIT: Unsharing network namespace..."); + fflush(NULL); /* Ensure log is written before potential crash */ + + if (unshare(CLONE_NEWNET) < 0) { + ds_error("Failed to unshare network namespace: %s", strerror(errno)); + exit(EXIT_FAILURE); + } + ds_log("INIT: Unshare success."); + fflush(NULL); + + /* Signal Monitor that netns is ready */ + ssize_t n; + while ((n = write(init_ready_pipe[1], "1", 1)) < 0) { + if (errno != EINTR) break; + } + if (n != 1) { + ds_error("Failed to signal Monitor (netns ready): %s", strerror(errno)); + exit(EXIT_FAILURE); + } + } + close(init_ready_pipe[1]); + + /* Notify Main that we are alive/unshared. + * Main reads this from sync_pipe[0]. Monitor does not see it. */ + pid_t self_pid = getpid(); + ds_log("INIT: Writing PID %d to parent...", self_pid); + /* Variable n is already declared in this scope if the previous block ran, + * but we are inside an if block for net_mode != HOST. + * Let's redeclare safely or assume new block. */ + ssize_t nw; + while ((nw = write(sync_pipe[1], &self_pid, sizeof(pid_t))) < 0) { + if (errno != EINTR) break; + } + if (nw != sizeof(pid_t)) { + ds_error("Failed to write PID to parent: %s", strerror(errno)); + exit(EXIT_FAILURE); + } close(sync_pipe[1]); + + /* Wait for Monitor to configure network */ + if (cfg->net_mode != DS_NET_HOST) { + char buf; + ds_log("INIT: Waiting for monitor configuration..."); + /* Loop to handle EINTR (signals) */ + ssize_t n; + while ((n = read(monitor_pipe[0], &buf, 1)) < 0) { + if (errno != EINTR) break; + } + if (n != 1) { + ds_error("Failed to sync with monitor (network setup)"); + exit(EXIT_FAILURE); + } + ds_log("INIT: Network configured."); + } + close(monitor_pipe[0]); + /* internal_boot will handle its own stdfds. */ exit(internal_boot(cfg)); } - /* Write child PID to sync pipe so parent knows it */ - write(sync_pipe[1], &init_pid, sizeof(pid_t)); + /* MONITOR CONTINUES */ + /* Close PTY masters (Main owns them, Monitor doesn't need them) */ + if (cfg->console.master >= 0) close(cfg->console.master); + for (int i = 0; i < cfg->tty_count; i++) { + if (cfg->ttys[i].master >= 0) close(cfg->ttys[i].master); + } + + /* Write child PID to sync pipe? No, Init did that. */ + /* Monitor doesn't use sync_pipe. */ close(sync_pipe[1]); + /* Monitor needs to know Init PID. */ + /* init_pid is known here. */ + + /* Configure network namespace if requested (from Monitor context) */ + if (cfg->net_mode != DS_NET_HOST) { + /* Wait for Init to signal that it has unshared CLONE_NEWNET */ + char buf; + /* Loop to handle EINTR (signals) */ + ssize_t n; + while ((n = read(init_ready_pipe[0], &buf, 1)) < 0) { + if (errno != EINTR) break; + } + if (n != 1) { + ds_error("Failed to sync with Init (netns creation)"); + kill(init_pid, SIGKILL); + exit(EXIT_FAILURE); + } + /* Removed redundant close(init_ready_pipe[0]) */ + + if (ds_configure_network_namespace(init_pid, cfg) < 0) { + ds_error("Failed to configure network namespace. Killing container."); + kill(init_pid, SIGKILL); + exit(EXIT_FAILURE); + } + + /* Signal Init to proceed */ + ssize_t nw; + while ((nw = write(monitor_pipe[1], "1", 1)) < 0) { + if (errno != EINTR) break; + } + if (nw != 1) { + ds_error("Failed to signal Init (network setup): %s", strerror(errno)); + kill(init_pid, SIGKILL); + exit(EXIT_FAILURE); + } + } + close(monitor_pipe[1]); + close(init_ready_pipe[0]); + /* Ensure monitor is not sitting inside any mount point */ - chdir("/"); + (void)chdir("/"); /* Stdio handling for monitor in background mode */ if (!cfg->foreground) { @@ -445,10 +579,21 @@ int start_rootfs(struct ds_config *cfg) { /* PARENT PROCESS */ close(sync_pipe[1]); + close(monitor_pipe[0]); + close(monitor_pipe[1]); + close(init_ready_pipe[0]); + close(init_ready_pipe[1]); + + /* Wait for Init (via Monitor's fork) to send child PID */ + /* Loop to handle EINTR (signals) */ + ssize_t n; + while ((n = read(sync_pipe[0], &cfg->container_pid, sizeof(pid_t))) < 0) { + if (errno != EINTR) break; + } - /* Wait for Monitor to send child PID */ - if (read(sync_pipe[0], &cfg->container_pid, sizeof(pid_t)) != sizeof(pid_t)) { - ds_error("Monitor failed to send container PID."); + if (n != sizeof(pid_t)) { + ds_error("Init failed to send container PID."); + close(sync_pipe[0]); return -1; } close(sync_pipe[0]); @@ -648,12 +793,12 @@ int enter_namespace(pid_t pid) { return -1; } - const char *ns_names[] = {"mnt", "uts", "ipc", "pid", "cgroup"}; - int ns_fds[5]; + const char *ns_names[] = {"mnt", "uts", "ipc", "pid", "cgroup", "net"}; + int ns_fds[6]; char path[PATH_MAX]; /* 1. Open all namespace descriptors first (CRITICAL: before any setns) */ - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 6; i++) { snprintf(path, sizeof(path), "/proc/%d/ns/%s", pid, ns_names[i]); ns_fds[i] = open(path, O_RDONLY); if (ns_fds[i] < 0) { @@ -673,14 +818,14 @@ int enter_namespace(pid_t pid) { } /* 2. Enter namespaces */ - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 6; i++) { if (ns_fds[i] < 0) continue; if (setns(ns_fds[i], 0) < 0) { if (i == 0) { /* mnt is mandatory */ ds_error("setns(mnt) failed: %s", strerror(errno)); - for (int j = i; j < 5; j++) + for (int j = i; j < 6; j++) if (ns_fds[j] >= 0) close(ns_fds[j]); return -1; @@ -1032,9 +1177,9 @@ int show_info(struct ds_config *cfg, int trust_cfg_pid) { /* HW access */ int hw = detect_hw_access_in_container(pid); if (hw) - printf(" " C_RED "HW access:" C_RESET " enabled\n"); + printf(" " C_RED "HW / GPU access:" C_RESET " enabled\n"); else - printf(" HW access: disabled\n"); + printf(" HW / GPU access: disabled\n"); } else { /* Best effort: read os-release from rootfs path */ if (cfg->rootfs_path[0]) { diff --git a/src/droidspace.h b/src/droidspace.h index 028563c..d9c6c82 100644 --- a/src/droidspace.h +++ b/src/droidspace.h @@ -89,6 +89,13 @@ #define DS_DNS_DEFAULT_1 "1.1.1.1" #define DS_DNS_DEFAULT_2 "8.8.8.8" +/* Network Modes */ +enum ds_net_mode { + DS_NET_HOST = 0, /* Shared network namespace (default) */ + DS_NET_NAT, /* veth pair + NAT (fake MAC/own IP, works on Mobile) */ + DS_NET_MACVLAN /* Macvlan bridge (real LAN IP, Wi-Fi only, requires driver support) */ +}; + /* Common Paths & Patterns */ #define DS_PROC_ROOT_FMT "/proc/%d/root" #define DS_PROC_CMDLINE_FMT "/proc/%d/cmdline" @@ -177,6 +184,10 @@ struct ds_config { /* Flags */ int foreground; /* --foreground */ int hw_access; /* --hw-access */ + mode_t gpu_mode; /* --gpu-mode (default 0660) */ + gid_t gpu_group; /* --gpu-group (default -1) */ + int sensors; /* --sensors */ + enum ds_net_mode net_mode; /* --network-mode */ int volatile_mode; /* --volatile */ int enable_ipv6; /* --enable-ipv6 */ int android_storage; /* --enable-android-storage */ @@ -254,7 +265,7 @@ int android_seccomp_setup(int is_systemd); int domount(const char *src, const char *tgt, const char *fstype, unsigned long flags, const char *data); int bind_mount(const char *src, const char *tgt); -int setup_dev(const char *rootfs, int hw_access); +int setup_dev(const char *rootfs, struct ds_config *cfg); int create_devices(const char *rootfs, int hw_access); int setup_devpts(int hw_access); int setup_volatile_overlay(struct ds_config *cfg); @@ -282,6 +293,7 @@ int ds_cgroup_attach(pid_t target_pid); * ---------------------------------------------------------------------------*/ int fix_networking_host(struct ds_config *cfg); +int ds_configure_network_namespace(pid_t container_pid, struct ds_config *cfg); int fix_networking_rootfs(struct ds_config *cfg); int ds_get_dns_servers(const char *custom_dns, char *out, size_t size); int detect_ipv6_in_container(pid_t pid); @@ -364,5 +376,6 @@ void print_documentation(const char *argv0); int check_requirements(void); int check_requirements_detailed(void); +void print_gpu_check(void); #endif /* DROIDSPACE_H */ diff --git a/src/main.c b/src/main.c index b63e877..21fb0d1 100644 --- a/src/main.c +++ b/src/main.c @@ -31,6 +31,7 @@ void print_usage(void) { printf(" show List all running containers\n"); printf(" scan Scan for untracked containers\n"); printf(" check Check system requirements\n"); + printf(" gpu-check Check GPU availability and permissions\n"); printf(" docs Show interactive documentation\n"); printf(" help Show this help message\n"); printf(" version Show version information\n"); @@ -45,6 +46,12 @@ void print_usage(void) { printf( " -d, --dns=SERVERS Set custom DNS servers (comma separated)\n"); printf(" -f, --foreground Run in foreground (attach console)\n"); + printf(" -g, --gpu Enable GPU access (alias for --hw-access)\n"); + printf(" --gpu-mode=MODE Set GPU device permissions (default 0660)\n"); + printf(" --gpu-group=GID Set GPU device group owner\n"); + printf(" -s, --sensors Expose battery/thermal sensors to container\n"); + printf( + " -N, --network-mode=MODE Set network mode: host (default), nat, macvlan\n"); printf(" -V, --volatile Discard changes on exit (OverlayFS)\n"); printf( " -B, --bind-mount=SRC:DEST Bind mount host directory into container\n"); @@ -93,6 +100,8 @@ static int validate_kernel_version(void) { int main(int argc, char **argv) { struct ds_config cfg = {0}; + cfg.gpu_mode = 0660; + cfg.gpu_group = (gid_t)-1; safe_strncpy(cfg.prog_name, argv[0], sizeof(cfg.prog_name)); static struct option long_options[] = { @@ -104,6 +113,11 @@ int main(int argc, char **argv) { {"dns", required_argument, 0, 'd'}, {"foreground", no_argument, 0, 'f'}, {"hw-access", no_argument, 0, 'H'}, + {"gpu", no_argument, 0, 'g'}, + {"gpu-mode", required_argument, 0, 1001}, + {"gpu-group", required_argument, 0, 1002}, + {"sensors", no_argument, 0, 's'}, + {"network-mode", required_argument, 0, 'N'}, {"enable-ipv6", no_argument, 0, 'I'}, {"enable-android-storage", no_argument, 0, 'S'}, {"selinux-permissive", no_argument, 0, 'P'}, @@ -124,7 +138,7 @@ int main(int argc, char **argv) { */ const char *discovered_cmd = NULL; int temp_optind = optind; - while (getopt_long(argc, argv, "+r:i:n:p:h:d:fHISPvVB:", long_options, + while (getopt_long(argc, argv, "+r:i:n:p:h:d:fHISPvVB:gsN:", long_options, NULL) != -1) ; if (optind < argc) @@ -133,7 +147,7 @@ int main(int argc, char **argv) { int strict = (discovered_cmd && (strcmp(discovered_cmd, "run") == 0)); const char *optstring = - strict ? "+r:i:n:p:h:d:fHISPvVB:" : "r:i:n:p:h:d:fHISPvVB:"; + strict ? "+r:i:n:p:h:d:fHISPvVB:gsN:" : "r:i:n:p:h:d:fHISPvVB:gsN:"; int opt; while ((opt = getopt_long(argc, argv, optstring, long_options, NULL)) != -1) { @@ -162,6 +176,47 @@ int main(int argc, char **argv) { case 'H': cfg.hw_access = 1; break; + case 'g': + cfg.hw_access = 1; /* Alias for hw-access */ + break; + case 1001: { /* --gpu-mode */ + char *endptr; + errno = 0; + unsigned long val = strtoul(optarg, &endptr, 8); + if (errno != 0 || endptr == optarg || *endptr != '\0' || val > 0777) { + ds_error("Invalid --gpu-mode: %s (must be octal 0-0777)", optarg); + return 1; + } + cfg.gpu_mode = (mode_t)val; + break; + } + case 1002: { /* --gpu-group */ + char *endptr; + errno = 0; + unsigned long val = strtoul(optarg, &endptr, 10); + if (errno != 0 || endptr == optarg || *endptr != '\0' || + (unsigned long)(gid_t)val != val || (gid_t)val == (gid_t)-1) { + ds_error("Invalid --gpu-group: %s (must be valid GID)", optarg); + return 1; + } + cfg.gpu_group = (gid_t)val; + break; + } + case 's': + cfg.sensors = 1; + break; + case 'N': + if (strcmp(optarg, "host") == 0) + cfg.net_mode = DS_NET_HOST; + else if (strcmp(optarg, "nat") == 0) + cfg.net_mode = DS_NET_NAT; + else if (strcmp(optarg, "macvlan") == 0) + cfg.net_mode = DS_NET_MACVLAN; + else { + ds_error("Invalid --network-mode: %s (allowed: host, nat, macvlan)", optarg); + return 1; + } + break; case 'I': cfg.enable_ipv6 = 1; break; @@ -249,6 +304,10 @@ int main(int argc, char **argv) { /* Commands that don't need root or config */ if (strcmp(cmd, "check") == 0) return check_requirements_detailed(); + if (strcmp(cmd, "gpu-check") == 0) { + print_gpu_check(); + return 0; + } if (strcmp(cmd, "version") == 0) { printf("v%s\n", DS_VERSION); return 0; diff --git a/src/mount.c b/src/mount.c index e135814..16616a6 100644 --- a/src/mount.c +++ b/src/mount.c @@ -104,7 +104,7 @@ int bind_mount(const char *src, const char *tgt) { * This preserves UID/GID/mode so bind mounts behave like Docker: * the kernel overlays the source transparently. */ mkdir(tgt, st_src.st_mode & 07777); - chown(tgt, st_src.st_uid, st_src.st_gid); + if (chown(tgt, st_src.st_uid, st_src.st_gid) < 0) { /* ignore */ } } else { write_file(tgt, ""); /* Create empty file as mount point */ } @@ -117,14 +117,33 @@ int bind_mount(const char *src, const char *tgt) { * /dev setup * ---------------------------------------------------------------------------*/ -int setup_dev(const char *rootfs, int hw_access) { +static int update_gpu_node_permissions(const char *path, mode_t mode, + gid_t group) { + int updated = 0; + if (chmod(path, mode) == 0) { + updated = 1; + } else { + ds_warn("Failed to chmod GPU node %s: %s", path, strerror(errno)); + } + + if (group != (gid_t)-1) { + if (chown(path, -1, group) == 0) { + updated = 1; + } else { + ds_warn("Failed to chown GPU node %s: %s", path, strerror(errno)); + } + } + return updated; +} + +int setup_dev(const char *rootfs, struct ds_config *cfg) { char dev_path[PATH_MAX]; snprintf(dev_path, sizeof(dev_path), "%s/dev", rootfs); /* Ensure the directory exists */ mkdir(dev_path, 0755); - if (hw_access) { + if (cfg->hw_access) { /* If hw_access is enabled, we mount host's devtmpfs. * WARNING: This is a shared singleton. We MUST be careful. */ if (domount("devtmpfs", dev_path, "devtmpfs", MS_NOSUID | MS_NOEXEC, @@ -141,6 +160,79 @@ int setup_dev(const char *rootfs, int hw_access) { umount2(path, MNT_DETACH); force_unlink(path); } + + /* GPU / Hardware Acceleration Fixes + * Scan for known GPU devices (Mali, Adreno, DMA heaps) and ensure + * they have correct permissions so non-root container users can access them. + * This is critical for Pixel devices (Mali) and others. */ + DIR *dir = opendir(dev_path); + if (dir) { + struct dirent *entry; + int updated_gpu = 0; + while ((entry = readdir(dir)) != NULL) { + int match = 0; + + /* Strict matching logic: + * - Prefix match: mali*, kgsl*, edgetpu*, video* + * - Exact match: dri, dma_heap, genlock, udmabuf + */ + if (strncmp(entry->d_name, "mali", 4) == 0) match = 1; + else if (strncmp(entry->d_name, "kgsl", 4) == 0) match = 1; + else if (strncmp(entry->d_name, "edgetpu", 7) == 0) match = 1; + else if (strncmp(entry->d_name, "video", 5) == 0) match = 1; + else if (strcmp(entry->d_name, "dri") == 0) match = 1; + else if (strcmp(entry->d_name, "dma_heap") == 0) match = 1; + else if (strcmp(entry->d_name, "genlock") == 0) match = 1; + else if (strcmp(entry->d_name, "udmabuf") == 0) match = 1; + + if (match) { + char full_path[PATH_MAX]; + snprintf(full_path, sizeof(full_path), "%s/%s", dev_path, + entry->d_name); + + struct stat st; + /* Use lstat to check for symlinks/types safely */ + if (lstat(full_path, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + /* Recursively chmod directory contents (single-level only). + * We assume GPU device directories like /dev/dri or /dev/dma_heap + * contain flat lists of device nodes. Deeply nested paths are not + * processed. */ + DIR *sub = opendir(full_path); + if (sub) { + struct dirent *sub_e; + while ((sub_e = readdir(sub)) != NULL) { + if (sub_e->d_name[0] == '.') continue; + char sub_p[PATH_MAX]; + snprintf(sub_p, sizeof(sub_p), "%s/%s", full_path, sub_e->d_name); + + struct stat sub_st; + if (lstat(sub_p, &sub_st) == 0) { + /* Only chmod character/block devices */ + if (S_ISCHR(sub_st.st_mode) || S_ISBLK(sub_st.st_mode)) { + if (update_gpu_node_permissions(sub_p, cfg->gpu_mode, + cfg->gpu_group)) + updated_gpu = 1; + } + } + } + closedir(sub); + } + } else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) { + /* Only chmod character/block devices */ + if (update_gpu_node_permissions(full_path, cfg->gpu_mode, + cfg->gpu_group)) + updated_gpu = 1; + } + } + } + } + closedir(dir); + if (updated_gpu) { + ds_log("GPU Access: Enabled permissions for detected GPU devices."); + } + } + } else { ds_warn("Failed to mount devtmpfs, falling back to tmpfs"); if (domount("none", dev_path, "tmpfs", MS_NOSUID | MS_NOEXEC, @@ -155,7 +247,7 @@ int setup_dev(const char *rootfs, int hw_access) { } /* Create minimal set of device nodes (creates secure console/ptmx/etc.) */ - return create_devices(rootfs, hw_access); + return create_devices(rootfs, cfg->hw_access); } int create_devices(const char *rootfs, int hw_access) { @@ -228,13 +320,13 @@ int create_devices(const char *rootfs, int hw_access) { /* Standard symlinks */ char tgt[PATH_MAX]; snprintf(tgt, sizeof(tgt), "%s/dev/fd", rootfs); - symlink("/proc/self/fd", tgt); + if (symlink("/proc/self/fd", tgt) < 0) { /* ignore */ } snprintf(tgt, sizeof(tgt), "%s/dev/stdin", rootfs); - symlink("/proc/self/fd/0", tgt); + if (symlink("/proc/self/fd/0", tgt) < 0) { /* ignore */ } snprintf(tgt, sizeof(tgt), "%s/dev/stdout", rootfs); - symlink("/proc/self/fd/1", tgt); + if (symlink("/proc/self/fd/1", tgt) < 0) { /* ignore */ } snprintf(tgt, sizeof(tgt), "%s/dev/stderr", rootfs); - symlink("/proc/self/fd/2", tgt); + if (symlink("/proc/self/fd/2", tgt) < 0) { /* ignore */ } return 0; } diff --git a/src/network.c b/src/network.c index 6e4d8c6..5d63215 100644 --- a/src/network.c +++ b/src/network.c @@ -6,6 +6,7 @@ */ #include "droidspace.h" +#include /* --------------------------------------------------------------------------- * Host-side networking setup (before container boot) @@ -66,14 +67,369 @@ int fix_networking_host(struct ds_config *cfg) { if (cfg->dns_servers[0]) ds_log("Setting up %d custom DNS servers...", count); - if (is_android()) { - /* Android specific NAT and firewall */ + /* If shared networking (Host mode) on Android, apply basic fixes/optimizations + * but skip iptables to avoid breaking connectivity (as per previous fix). */ + if (cfg->net_mode == DS_NET_HOST && is_android()) { android_configure_iptables(); } return 0; } +/* --------------------------------------------------------------------------- + * Network Namespace Configuration (NAT / Macvlan) + * ---------------------------------------------------------------------------*/ + +static int generate_random_mac(char *buf) { + /* Locally Administered Address (x2, x6, xA, xE) */ + /* We use 02:xx:xx:xx:xx:xx */ + unsigned char mac[6]; + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) return -1; + + ssize_t total_read = 0; + while (total_read < 6) { + ssize_t n = read(fd, mac + total_read, 6 - total_read); + if (n < 0) { + if (errno == EINTR) continue; + close(fd); + return -1; + } + if (n == 0) { /* Unexpected EOF */ + close(fd); + return -1; + } + total_read += n; + } + close(fd); + + mac[0] &= 0xFE; /* Unicast */ + mac[0] |= 0x02; /* Locally Administered */ + + snprintf(buf, 18, "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return 0; +} + +static int get_wlan_interface(char *buf, size_t size) { + DIR *d = opendir("/sys/class/net"); + if (!d) return -1; + + struct dirent *entry; + char best_iface[64] = ""; + int found = 0; + + while ((entry = readdir(d)) != NULL) { + if (entry->d_name[0] == '.') continue; + if (strcmp(entry->d_name, "lo") == 0) continue; + + /* Prioritize wireless */ + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/sys/class/net/%s/wireless", entry->d_name); + if (access(path, F_OK) == 0) { + safe_strncpy(buf, entry->d_name, size); + found = 1; + break; + } + + /* Fallback to any non-loopback interface (e.g. eth0, rev_rmnet) */ + if (best_iface[0] == '\0') { + safe_strncpy(best_iface, entry->d_name, sizeof(best_iface)); + } + } + closedir(d); + + if (found) return 0; + if (best_iface[0] != '\0') { + safe_strncpy(buf, best_iface, size); + return 0; + } + return -1; +} + +/* + * This function runs in the MONITOR process (Host NetNS). + * It expects the container_pid to be a child process that has ALREADY unshared its NetNS. + */ +int ds_configure_network_namespace(pid_t container_pid, struct ds_config *cfg) { + ds_log("Configuring isolated network namespace (PID %d)...", container_pid); + + char veth_host[32], veth_peer[32]; + snprintf(veth_host, sizeof(veth_host), "veth%d", container_pid); + snprintf(veth_peer, sizeof(veth_peer), "vethc%d", container_pid); + + if (cfg->net_mode == DS_NET_NAT) { + /* ----------------------------------------------------------------------- + * NAT Mode: veth pair + iptables MASQUERADE + * ----------------------------------------------------------------------- */ + + /* Allocator Logic: Collision avoidance */ + int subnet_id = (container_pid % 250) + 1; + /* Try to detect if this subnet is taken by scanning host interfaces. + * If taken, increment and retry up to 50 times. */ + int retries = 50; + while (retries-- > 0) { + char check_ip[64]; + snprintf(check_ip, sizeof(check_ip), "ip addr show | grep '10.0.%d.1/24'", subnet_id); + if (run_command_quiet((char*[]){"sh", "-c", check_ip, NULL}) != 0) { + /* Not found, so it is free */ + break; + } + subnet_id = (subnet_id % 250) + 1; + } + + if (retries <= 0) { + ds_error("Failed to allocate free subnet for NAT mode (exhausted retries)"); + return -1; + } + + char host_ip[32], container_ip[32]; + snprintf(host_ip, sizeof(host_ip), "10.0.%d.1/24", subnet_id); + snprintf(container_ip, sizeof(container_ip), "10.0.%d.2/24", subnet_id); + char gateway_ip[32]; + snprintf(gateway_ip, sizeof(gateway_ip), "10.0.%d.1", subnet_id); + + ds_log("Mode: NAT. Subnet: 10.0.%d.0/24", subnet_id); + + /* 1. Create veth pair */ + char *args_link[] = {"ip", "link", "add", veth_host, "type", "veth", "peer", "name", veth_peer, NULL}; + if (run_command_quiet(args_link) != 0) { + ds_error("Failed to create veth pair: ip link add %s type veth peer name %s", veth_host, veth_peer); + return -1; + } + + /* Helper macro for cleanup on failure */ + #define CLEANUP_NAT_AND_RETURN(ret_code) \ + do { \ + char *args_del[] = {"ip", "link", "delete", veth_host, NULL}; \ + run_command_quiet(args_del); \ + return ret_code; \ + } while (0) + + /* 2. Configure Host Side */ + char *args_host_up[] = {"ip", "link", "set", veth_host, "up", NULL}; + if (run_command_quiet(args_host_up) != 0) { + ds_error("Failed to set %s up", veth_host); + CLEANUP_NAT_AND_RETURN(-1); + } + + char *args_host_ip[] = {"ip", "addr", "add", host_ip, "dev", veth_host, NULL}; + if (run_command_quiet(args_host_ip) != 0) { + ds_error("Failed to assign host IP %s to %s", host_ip, veth_host); + CLEANUP_NAT_AND_RETURN(-1); + } + + /* 3. Enable NAT (Masquerade) on Host */ + char subnet_cidr[32]; + snprintf(subnet_cidr, sizeof(subnet_cidr), "10.0.%d.0/24", subnet_id); + + char *args_nat[] = {"iptables", "-t", "nat", "-A", "POSTROUTING", "-s", subnet_cidr, "-j", "MASQUERADE", NULL}; + if (run_command_quiet(args_nat) != 0) { + ds_error("Failed to set up NAT masquerade for %s", subnet_cidr); + CLEANUP_NAT_AND_RETURN(-1); + } + + char *args_fwd[] = {"iptables", "-A", "FORWARD", "-i", veth_host, "-j", "ACCEPT", NULL}; + if (run_command_quiet(args_fwd) != 0) { + ds_error("Failed to allow forwarding in on %s", veth_host); + /* Try to cleanup NAT rule */ + char *args_nat_del[] = {"iptables", "-t", "nat", "-D", "POSTROUTING", "-s", subnet_cidr, "-j", "MASQUERADE", NULL}; + run_command_quiet(args_nat_del); + CLEANUP_NAT_AND_RETURN(-1); + } + + char *args_fwd2[] = {"iptables", "-A", "FORWARD", "-o", veth_host, "-j", "ACCEPT", NULL}; + if (run_command_quiet(args_fwd2) != 0) { + ds_error("Failed to allow forwarding out on %s", veth_host); + char *args_nat_del[] = {"iptables", "-t", "nat", "-D", "POSTROUTING", "-s", subnet_cidr, "-j", "MASQUERADE", NULL}; + run_command_quiet(args_nat_del); + char *args_fwd_del[] = {"iptables", "-D", "FORWARD", "-i", veth_host, "-j", "ACCEPT", NULL}; + run_command_quiet(args_fwd_del); + CLEANUP_NAT_AND_RETURN(-1); + } + + /* 4. Move Peer to Container Namespace */ + char pid_str[16]; + snprintf(pid_str, sizeof(pid_str), "%d", container_pid); + char *args_move[] = {"ip", "link", "set", veth_peer, "netns", pid_str, NULL}; + if (run_command_quiet(args_move) != 0) { + ds_error("Failed to move interface %s to container PID %s", veth_peer, pid_str); + /* Cleanup all rules */ + char *args_nat_del[] = {"iptables", "-t", "nat", "-D", "POSTROUTING", "-s", subnet_cidr, "-j", "MASQUERADE", NULL}; + run_command_quiet(args_nat_del); + char *args_fwd_del[] = {"iptables", "-D", "FORWARD", "-i", veth_host, "-j", "ACCEPT", NULL}; + run_command_quiet(args_fwd_del); + char *args_fwd2_del[] = {"iptables", "-D", "FORWARD", "-o", veth_host, "-j", "ACCEPT", NULL}; + run_command_quiet(args_fwd2_del); + CLEANUP_NAT_AND_RETURN(-1); + } + + /* 5. Configure Container Side (using fork + setns) */ + pid_t worker = fork(); + if (worker < 0) { + ds_error("fork failed during network setup: %s", strerror(errno)); + return -1; + } + if (worker == 0) { + /* Child worker */ + char ns_path[PATH_MAX]; + snprintf(ns_path, sizeof(ns_path), "/proc/%d/ns/net", container_pid); + int fd = open(ns_path, O_RDONLY); + if (fd < 0) { + ds_error("Failed to open netns %s: %s", ns_path, strerror(errno)); + exit(1); + } + if (setns(fd, CLONE_NEWNET) < 0) { + ds_error("Failed to enter netns: %s", strerror(errno)); + exit(1); + } + close(fd); + + /* Inside container namespace now */ + char *cmd_rename[] = {"ip", "link", "set", veth_peer, "name", "eth0", NULL}; + if (run_command_quiet(cmd_rename) != 0) { + ds_error("Failed to rename interface to eth0"); + exit(1); + } + + /* Set Fake MAC */ + char mac[32]; + if (generate_random_mac(mac) < 0) { + ds_error("Failed to generate random MAC"); + exit(1); + } + ds_log("Assigned Virtual MAC: %s", mac); + char *cmd_mac[] = {"ip", "link", "set", "eth0", "address", mac, NULL}; + if (run_command_quiet(cmd_mac) != 0) { + ds_error("Failed to set MAC address"); + exit(1); + } + + char *cmd_ip[] = {"ip", "addr", "add", container_ip, "dev", "eth0", NULL}; + if (run_command_quiet(cmd_ip) != 0) { + ds_error("Failed to assign IP %s", container_ip); + exit(1); + } + + char *cmd_up[] = {"ip", "link", "set", "eth0", "up", NULL}; + if (run_command_quiet(cmd_up) != 0) { + ds_error("Failed to bring up eth0"); + exit(1); + } + + char *cmd_lo[] = {"ip", "link", "set", "lo", "up", NULL}; + if (run_command_quiet(cmd_lo) != 0) { + ds_error("Failed to bring up lo"); + exit(1); + } + + char *cmd_gw[] = {"ip", "route", "add", "default", "via", gateway_ip, NULL}; + if (run_command_quiet(cmd_gw) != 0) { + ds_error("Failed to add default route via %s", gateway_ip); + exit(1); + } + + exit(0); + } + int status; + while (waitpid(worker, &status, 0) < 0) { + if (errno != EINTR) { + ds_error("waitpid failed: %s", strerror(errno)); + return -1; + } + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ds_error("Failed to configure container network interface (worker failed)"); + return -1; + } + + } else if (cfg->net_mode == DS_NET_MACVLAN) { + /* ----------------------------------------------------------------------- + * Macvlan Mode: Bridge to physical interface + * ----------------------------------------------------------------------- */ + ds_log("Mode: Macvlan (Bridge). Trying to get real LAN IP..."); + + char phys_if[32]; + if (get_wlan_interface(phys_if, sizeof(phys_if)) < 0) { + ds_error("Could not find a suitable parent interface (wlan0/eth0) for Macvlan."); + return -1; + } + ds_log("Parent interface: %s", phys_if); + + char mac_if[32]; + snprintf(mac_if, sizeof(mac_if), "mac%d", container_pid); + + char *args_link[] = {"ip", "link", "add", "link", phys_if, "name", mac_if, "type", "macvlan", "mode", "bridge", NULL}; + if (run_command_quiet(args_link) != 0) { + ds_error("Failed to create macvlan interface %s on %s. (Driver might not support it)", mac_if, phys_if); + return -1; + } + + /* Move to container */ + char pid_str[16]; + snprintf(pid_str, sizeof(pid_str), "%d", container_pid); + char *args_move[] = {"ip", "link", "set", mac_if, "netns", pid_str, NULL}; + if (run_command_quiet(args_move) != 0) { + ds_error("Failed to move macvlan interface to container PID %s", pid_str); + char *args_del[] = {"ip", "link", "delete", mac_if, NULL}; + run_command_quiet(args_del); + return -1; + } + + /* Configure inside */ + pid_t worker = fork(); + if (worker < 0) { + ds_error("fork failed during macvlan setup"); + return -1; + } + if (worker == 0) { + char ns_path[PATH_MAX]; + snprintf(ns_path, sizeof(ns_path), "/proc/%d/ns/net", container_pid); + int fd = open(ns_path, O_RDONLY); + if (fd < 0 || setns(fd, CLONE_NEWNET) < 0) { + ds_error("Worker failed to enter netns"); + exit(1); + } + close(fd); + + char *cmd_rename[] = {"ip", "link", "set", mac_if, "name", "eth0", NULL}; + if (run_command_quiet(cmd_rename) != 0) exit(1); + + char mac[32]; + if (generate_random_mac(mac) < 0) { + ds_error("Failed to generate random MAC"); + exit(1); + } + ds_log("Assigned Virtual MAC: %s", mac); + char *cmd_mac[] = {"ip", "link", "set", "eth0", "address", mac, NULL}; + if (run_command_quiet(cmd_mac) != 0) exit(1); + + char *cmd_up[] = {"ip", "link", "set", "eth0", "up", NULL}; + if (run_command_quiet(cmd_up) != 0) exit(1); + + char *cmd_lo[] = {"ip", "link", "set", "lo", "up", NULL}; + if (run_command_quiet(cmd_lo) != 0) exit(1); + + exit(0); + } + int status; + while (waitpid(worker, &status, 0) < 0) { + if (errno != EINTR) { + ds_error("waitpid failed: %s", strerror(errno)); + return -1; + } + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ds_error("Macvlan setup worker returned error"); + return -1; + } else { + ds_warn("Macvlan setup complete. You must run a DHCP client inside the container."); + } + } + + return 0; +} + /* --------------------------------------------------------------------------- * Rootfs-side networking setup (inside container, after pivot_root) * ---------------------------------------------------------------------------*/ @@ -118,7 +474,7 @@ int fix_networking_rootfs(struct ds_config *cfg) { /* Link /etc/resolv.conf */ unlink("/etc/resolv.conf"); - symlink("/run/resolvconf/resolv.conf", "/etc/resolv.conf"); + (void)symlink("/run/resolvconf/resolv.conf", "/etc/resolv.conf"); /* 4. Android Network Groups */ if (is_android()) {