diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..aee91b87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Shell scripts are executed directly on Android/Linux. CRLF would append a +# carriage return to the shebang interpreter path and make the scripts fail +# with "No such file or directory" when an APK is built from Windows. +*.sh text eol=lf diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index dda9a4da..d3329c4a 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -159,6 +159,12 @@ android { isMinifyEnabled = false isDebuggable = true signingConfig = signingConfigs.getByName("debug") + + // Allow locally signed test builds to coexist with an installed release build. + // Example: -PDROIDSPACES_APP_ID_SUFFIX=.dev + providers.gradleProperty("DROIDSPACES_APP_ID_SUFFIX").orNull + ?.takeIf { it.isNotBlank() } + ?.let { applicationIdSuffix = it } } } diff --git a/Android/app/src/main/assets/post_extract_fixes.sh b/Android/app/src/main/assets/post_extract_fixes.sh index f6f72b45..d8aba1ba 100755 --- a/Android/app/src/main/assets/post_extract_fixes.sh +++ b/Android/app/src/main/assets/post_extract_fixes.sh @@ -184,18 +184,17 @@ EOF $PRINTF "[Unit]\nConditionPathIsReadWrite=\n" > "$ROOTFS_PATH/etc/systemd/system/${unit}.d/99-readonly-fix.conf" done - # 06. Limit specific network services to NAT and gateway modes only - # Both need an in-container DHCP client (NAT: lease from Droidspaces; gateway: - # lease from the gateway container, e.g. OpenWRT). Host/none modes still skip - # them to prevent cellular network breakage. - log "Applying NAT/gateway mode guards to network services..." + # 06. Limit network services to modes where the guest owns DHCP. Direct-L2 + # static mode is configured by the runtime, so DHCP managers must not race + # it. Host/none modes still skip them to protect Android host interfaces. + log "Applying network-mode guards to guest network services..." for unit in NetworkManager.service dhcpcd.service systemd-resolved.service systemd-networkd.service; do if $TEST -f "$ROOTFS_PATH/$GUEST_SYSTEMD_PATH/$unit" || $TEST -f "$ROOTFS_PATH/etc/systemd/system/multi-user.target.wants/$unit"; then $MKDIR -p "$ROOTFS_PATH/etc/systemd/system/${unit}.d" $CAT > "$ROOTFS_PATH/etc/systemd/system/${unit}.d/99-netmode-limit.conf" << 'EOF' [Service] ExecCondition= -ExecCondition=/bin/sh -c "grep -qE 'net_mode=(nat|gateway)' /run/droidspaces/container.config" +ExecCondition=/bin/sh -c "grep -qE 'net_mode=(nat|gateway)' /run/droidspaces/container.config || { grep -qE 'net_mode=(ipvlan|macvlan)' /run/droidspaces/container.config && ! grep -q 'net_ipam=static' /run/droidspaces/container.config; }" EOF fi done @@ -212,8 +211,10 @@ DHCP=yes IPv6AcceptRA=yes [DHCPv4] +ClientIdentifier=duid UseDNS=yes UseDomains=yes +RequestBroadcast=yes RouteMetric=100 EOF @@ -223,13 +224,13 @@ fi # --- 3. Alpine/OpenRC & dhcpcd-Specific Fixes --- -# Replace dhcpcd init script to only start in NAT or gateway network mode +# Replace dhcpcd init script to start only when the guest owns DHCP # This is the OpenRC equivalent of systemd's ExecCondition - if the container # is running in host network mode, dhcpcd is cleanly skipped at boot to prevent # cellular network breakage and kernel panics on Android interfaces. Gateway # mode needs it too: the DHCP lease comes from the gateway container. if $TEST -f "$ROOTFS_PATH/etc/init.d/dhcpcd"; then - log "Alpine/OpenRC dhcpcd service detected, applying NAT/gateway mode limitation..." + log "Alpine/OpenRC dhcpcd service detected, applying network-mode limitation..." $CAT > "$ROOTFS_PATH/etc/init.d/dhcpcd" << 'INITEOF' #!/sbin/openrc-run @@ -249,9 +250,11 @@ depend() { } start_pre() { - # Only start in NAT or gateway mode - prevents cellular network breakage in host network mode - if ! grep -qE 'net_mode=(nat|gateway)' /run/droidspaces/container.config 2>/dev/null; then - einfo "Skipping dhcpcd: not in NAT or gateway network mode" + # Direct-L2 DHCP is guest-owned; direct-L2 static is runtime-owned. + if ! grep -qE 'net_mode=(nat|gateway)' /run/droidspaces/container.config 2>/dev/null && + ! { grep -qE 'net_mode=(ipvlan|macvlan)' /run/droidspaces/container.config 2>/dev/null && + ! grep -q 'net_ipam=static' /run/droidspaces/container.config 2>/dev/null; }; then + einfo "Skipping dhcpcd: network mode is not guest-managed DHCP" return 1 fi checkpath -d /run/dhcpcd diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerCard.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerCard.kt index ba426c3c..2b55f32c 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerCard.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerCard.kt @@ -158,7 +158,7 @@ fun ContainerCard( // Info Rows val displayHostname = container.hostname.takeIf { it.isNotEmpty() } ?: container.name val hasSparseImage = container.useSparseImage && container.sparseImageSizeGB != null - val netModeLabel = when (container.netMode) { "nat" -> context.getString(R.string.network_mode_nat_short); "none" -> context.getString(R.string.network_mode_none_short); "gateway" -> context.getString(R.string.network_mode_gateway_short); else -> context.getString(R.string.network_mode_host_short) } + val netModeLabel = when (container.netMode) { "nat" -> context.getString(R.string.network_mode_nat_short); "none" -> context.getString(R.string.network_mode_none_short); "gateway" -> context.getString(R.string.network_mode_gateway_short); "ipvlan" -> context.getString(R.string.network_mode_ipvlan_short); "macvlan" -> context.getString(R.string.network_mode_macvlan_short); else -> context.getString(R.string.network_mode_host_short) } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { Icon(Icons.Default.Computer, null, modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(context.getString(R.string.hostname_label, displayHostname), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt index 07f0c6ad..e1462a7a 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt @@ -44,11 +44,15 @@ import androidx.compose.material.icons.filled.NetworkCheck import androidx.compose.material.icons.filled.PowerSettingsNew import androidx.compose.material.icons.filled.Public import androidx.compose.material.icons.filled.Security +import androidx.compose.material.icons.filled.SettingsEthernet import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.Terminal import androidx.compose.material.icons.filled.Warning import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -81,8 +85,11 @@ import com.droidspaces.app.util.BindMount import com.droidspaces.app.util.Constants import com.droidspaces.app.util.ContainerConfigState import com.droidspaces.app.util.ContainerInfo +import com.droidspaces.app.util.ContainerManager import com.droidspaces.app.util.GatewayErrors import com.droidspaces.app.util.ValidationUtils +import com.droidspaces.app.util.isDirectNetworkValid +import com.droidspaces.app.util.isNetMacValid /** * The single, shared container-configuration form used by both the Create wizard @@ -117,6 +124,21 @@ fun ContainerConfigForm( var showEnvDialog by remember { mutableStateOf(false) } var showPrivilegedDialog by remember { mutableStateOf(false) } var showHwAccessDialog by remember { mutableStateOf(false) } + var parentInterfaceMenuExpanded by remember { mutableStateOf(false) } + var availableParentInterfaces by remember { mutableStateOf>(emptyList()) } + var directL2Capabilities by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + directL2Capabilities = ContainerManager.getDirectL2Capabilities() + } + + LaunchedEffect(state.netMode) { + if (state.netMode == "ipvlan" || state.netMode == "macvlan") { + availableParentInterfaces = ContainerManager.listUpstreamInterfaces() + } else { + parentInterfaceMenuExpanded = false + } + } val modernFieldShape = RoundedCornerShape(16.dp) val modernFieldColors = DsTextFieldDefaults.colors() @@ -261,13 +283,20 @@ fun ContainerConfigForm( DsDropdown( label = context.getString(R.string.network_mode), selected = state.netMode, - options = listOf("nat", "host", "none", "gateway"), - displayName = { context.getString(when (it) { "nat" -> R.string.network_mode_nat; "none" -> R.string.network_mode_none; "gateway" -> R.string.network_mode_gateway; else -> R.string.network_mode_host }) }, + options = listOf("nat", "host", "none", "gateway", "ipvlan", "macvlan"), + displayName = { context.getString(when (it) { "nat" -> R.string.network_mode_nat; "none" -> R.string.network_mode_none; "gateway" -> R.string.network_mode_gateway; "ipvlan" -> R.string.network_mode_ipvlan; "macvlan" -> R.string.network_mode_macvlan; else -> R.string.network_mode_host }) }, onSelect = { mode -> clearFocus() - onStateChange(state.copy(netMode = mode, disableIPv6 = if (mode != "host") false else state.disableIPv6)) + onStateChange(state.copy(netMode = mode, disableIPv6 = if (mode == "nat" || mode == "none") true else state.disableIPv6)) }, - leadingIcon = Icons.Default.Public + leadingIcon = Icons.Default.Public, + isOptionEnabled = { mode -> + when (mode) { + "ipvlan" -> directL2Capabilities?.ipvlanSupported == true + "macvlan" -> directL2Capabilities?.macvlanSupported == true + else -> true + } + } ) GatewaySettingsSection( @@ -284,6 +313,183 @@ fun ContainerConfigForm( errors = gatewayErrors ) + if (state.netMode == "ipvlan" || state.netMode == "macvlan") { + val parentError = state.netParent.isNotEmpty() && + (state.netParent.length >= 16 || state.netParent.any { it.isWhitespace() || it == '/' }) + val staticError = state.netIpam == "static" && !state.isDirectNetworkValid() + Column( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = context.getString(R.string.direct_l2_settings), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = context.getString(R.string.direct_l2_explain), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ExposedDropdownMenuBox( + expanded = parentInterfaceMenuExpanded, + onExpandedChange = { parentInterfaceMenuExpanded = it }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = state.netParent, + onValueChange = { + onStateChange(state.copy(netParent = it.trim())) + parentInterfaceMenuExpanded = true + }, + label = { Text(context.getString(R.string.net_parent)) }, + placeholder = { Text(context.getString(R.string.net_parent_auto)) }, + supportingText = { + Text(context.getString(if (parentError) R.string.net_parent_error else R.string.net_parent_explain)) + }, + isError = parentError, + modifier = Modifier.menuAnchor().fillMaxWidth(), + singleLine = true, + shape = modernFieldShape, + colors = modernFieldColors, + leadingIcon = { Icon(Icons.Default.Public, contentDescription = null) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = parentInterfaceMenuExpanded + ) + } + ) + ExposedDropdownMenu( + expanded = parentInterfaceMenuExpanded, + onDismissRequest = { parentInterfaceMenuExpanded = false } + ) { + DropdownMenuItem( + text = { Text(context.getString(R.string.net_parent_auto)) }, + onClick = { + onStateChange(state.copy(netParent = "")) + parentInterfaceMenuExpanded = false + clearFocus() + } + ) + availableParentInterfaces + .filter { it.length < 16 && it.none { ch -> ch.isWhitespace() || ch == '/' } } + .distinct() + .forEach { iface -> + DropdownMenuItem( + text = { Text(iface) }, + onClick = { + onStateChange(state.copy(netParent = iface)) + parentInterfaceMenuExpanded = false + clearFocus() + } + ) + } + } + } + if (state.netMode == "macvlan") { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(18.dp) + ) + Text( + text = context.getString(R.string.macvlan_wifi_4addr_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.tertiary + ) + } + } + if (state.netMode == "macvlan") { + val macError = !state.isNetMacValid() + OutlinedTextField( + value = state.netMac, + onValueChange = { onStateChange(state.copy(netMac = it.trim())) }, + label = { Text(context.getString(R.string.net_mac)) }, + placeholder = { Text("02:11:22:33:44:55") }, + supportingText = { + Text(context.getString(if (macError) R.string.net_mac_error else R.string.net_mac_explain)) + }, + isError = macError, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = modernFieldShape, + colors = modernFieldColors, + leadingIcon = { Icon(Icons.Default.SettingsEthernet, contentDescription = null) } + ) + } + DsDropdown( + label = context.getString(R.string.net_ipam), + selected = state.netIpam, + options = listOf("dhcp", "static"), + displayName = { context.getString(if (it == "static") R.string.net_ipam_static else R.string.net_ipam_dhcp) }, + onSelect = { onStateChange(state.copy(netIpam = it)) }, + leadingIcon = Icons.Default.NetworkCheck + ) + DsDropdown( + label = context.getString(R.string.host_access), + selected = state.hostAccess, + options = listOf("none", "ptp", "shim"), + displayName = { + context.getString(when (it) { + "ptp" -> R.string.host_access_ptp + "shim" -> R.string.host_access_shim + else -> R.string.host_access_none + }) + }, + onSelect = { onStateChange(state.copy(hostAccess = it)) }, + leadingIcon = Icons.Default.SettingsEthernet + ) + Text( + text = context.getString(when (state.hostAccess) { + "ptp" -> R.string.host_access_ptp_explain + "shim" -> R.string.host_access_shim_explain + else -> R.string.host_access_none_explain + }), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (state.netIpam == "static") { + OutlinedTextField( + value = state.netAddress, + onValueChange = { onStateChange(state.copy(netAddress = it.trim())) }, + label = { Text(context.getString(R.string.net_address)) }, + placeholder = { Text("192.168.1.50/24") }, + isError = state.netAddress.isNotEmpty() && staticError, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = modernFieldShape, + colors = modernFieldColors + ) + OutlinedTextField( + value = state.netGateway, + onValueChange = { onStateChange(state.copy(netGateway = it.trim())) }, + label = { Text(context.getString(R.string.net_gateway)) }, + placeholder = { Text("192.168.1.1") }, + supportingText = { if (staticError) Text(context.getString(R.string.net_static_error)) }, + isError = state.netGateway.isNotEmpty() && staticError, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = modernFieldShape, + colors = modernFieldColors + ) + } else if (state.netMode == "ipvlan") { + Text( + text = context.getString(R.string.ipvlan_dhcp_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.tertiary + ) + } + } + } + if (state.netMode == "nat") { Column( modifier = Modifier.fillMaxWidth().padding(top = 8.dp), @@ -441,7 +647,7 @@ fun ContainerConfigForm( leadingIcon = { Icon(Icons.Default.Dns, contentDescription = null) } ) - val ipv6IsForced = state.netMode != "host" + val ipv6IsForced = state.netMode == "nat" || state.netMode == "none" ToggleCard( icon = Icons.Default.NetworkCheck, title = context.getString(R.string.disable_ipv6), diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/DsDropdown.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/DsDropdown.kt index bde05567..ec8ac9d4 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/DsDropdown.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/DsDropdown.kt @@ -30,7 +30,8 @@ fun DsDropdown( leadingIcon: ImageVector? = null, isError: Boolean = false, supportingText: String? = null, - enabled: Boolean = true + enabled: Boolean = true, + isOptionEnabled: (T) -> Boolean = { true } ) { var expanded by remember { mutableStateOf(false) } val focusManager = LocalFocusManager.current @@ -95,8 +96,10 @@ fun DsDropdown( ) ) { options.forEach { option -> + val optionEnabled = isOptionEnabled(option) DropdownMenuItem( text = { Text(displayName(option), fontWeight = FontWeight.Medium) }, + enabled = optionEnabled, onClick = { onSelect(option) expanded = false @@ -108,7 +111,8 @@ fun DsDropdown( Icons.Default.Check, contentDescription = null, modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.primary + tint = if (optionEnabled) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) ) } } else null 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 bd8c8888..c36ce8c3 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 @@ -44,6 +44,7 @@ import com.droidspaces.app.ui.util.ClearFocusOnClickOutside import com.droidspaces.app.util.ContainerConfigState import com.droidspaces.app.util.ContainerInfo import com.droidspaces.app.util.ValidationUtils +import com.droidspaces.app.util.isDirectNetworkValid @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -72,7 +73,8 @@ fun ContainerConfigScreen( else installedContainers.find { it.name != containerName && it.staticNatIp == state.staticNatIp } } - val canProceed = (state.netMode != "gateway" || gatewayErrors.isValid) && collisionContainer == null + val canProceed = (state.netMode != "gateway" || gatewayErrors.isValid) && + state.isDirectNetworkValid() && collisionContainer == null Scaffold( topBar = { 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 13774042..45a4eb4c 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 @@ -64,6 +64,7 @@ import com.droidspaces.app.util.SystemInfoManager import com.droidspaces.app.util.ValidationUtils import com.droidspaces.app.util.toConfigState import com.droidspaces.app.util.withConfig +import com.droidspaces.app.util.isDirectNetworkValid import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -170,7 +171,8 @@ fun EditContainerScreen( bottomBar = { val btnShape = RoundedCornerShape(20.dp) val isReadyToSave = !isSaving && !isSaved && hasChanges && hostnameError == null && - (state.netMode != "gateway" || gatewayErrors.isValid) && collisionContainer == null + (state.netMode != "gateway" || gatewayErrors.isValid) && + state.isDirectNetworkValid() && collisionContainer == null val targetBtnColor = when { isSaved -> MaterialTheme.colorScheme.primaryContainer isSaving || isReadyToSave -> MaterialTheme.colorScheme.primary diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/InstallationSummaryScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/InstallationSummaryScreen.kt index 317d7b01..c82ab4fa 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/InstallationSummaryScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/InstallationSummaryScreen.kt @@ -93,12 +93,29 @@ fun InstallationSummaryScreen( SummaryItem(stringResource(R.string.hostname), config.hostname, Icons.Default.Computer) SummaryItem( stringResource(R.string.network_mode), - stringResource(when (config.netMode) { "nat" -> R.string.network_mode_nat; "none" -> R.string.network_mode_none; else -> R.string.network_mode_host }), + stringResource(when (config.netMode) { "nat" -> R.string.network_mode_nat; "none" -> R.string.network_mode_none; "gateway" -> R.string.network_mode_gateway; "ipvlan" -> R.string.network_mode_ipvlan; "macvlan" -> R.string.network_mode_macvlan; else -> R.string.network_mode_host }), Icons.Default.Public ) if (config.netMode == "nat" && config.staticNatIp.isNotEmpty()) { SummaryItem(stringResource(R.string.static_ip_address), config.staticNatIp, Icons.Default.NetworkCheck) } + if (config.netMode == "ipvlan" || config.netMode == "macvlan") { + SummaryItem(stringResource(R.string.net_parent), config.netParent, Icons.Default.Public) + SummaryItem( + stringResource(R.string.net_ipam), + stringResource(if (config.netIpam == "static") R.string.net_ipam_static else R.string.net_ipam_dhcp), + Icons.Default.NetworkCheck + ) + SummaryItem( + stringResource(R.string.host_access), + stringResource(when (config.hostAccess) { + "ptp" -> R.string.host_access_ptp + "shim" -> R.string.host_access_shim + else -> R.string.host_access_none + }), + Icons.Default.SettingsEthernet + ) + } if (config.useSparseImage && config.sparseImageSizeGB != null) { SummaryItem(stringResource(R.string.storage_configuration), "${stringResource(R.string.sparse_image_configuration)} (${config.sparseImageSizeGB}GB)", Icons.Default.Storage) } else { diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt index bfd619c9..62c180f3 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt @@ -188,6 +188,10 @@ CONFIG_IP_NF_NAT=y # Disable this on older kernels to make internet work CONFIG_ANDROID_PARANOID_NETWORK=n +# Optional direct L2 network modes +CONFIG_IPVLAN=y +CONFIG_MACVLAN=y + # Fix for docker unsafe procfs error CONFIG_USER_NS=y""", guideUrl = "https://github.com/ravindu644/Droidspaces-OSS/blob/main/Documentation/Kernel-Configuration.md#non-gki", @@ -222,6 +226,10 @@ CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y # Fix for docker unsafe procfs error CONFIG_USER_NS=y +# Direct L2 network modes +CONFIG_IPVLAN=y +CONFIG_MACVLAN=y + # UFW support CONFIG_NETFILTER_XT_TARGET_REJECT=y CONFIG_NETFILTER_XT_TARGET_LOG=y diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/AppStateViewModel.kt b/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/AppStateViewModel.kt index ec0fd440..f8870ea9 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/AppStateViewModel.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/viewmodel/AppStateViewModel.kt @@ -217,7 +217,15 @@ class AppStateViewModel(application: Application) : AndroidViewModel(application binaryResult.fold( onSuccess = { if (wasDaemon) { - BinaryInstaller.signalDaemon() + val daemonRestart = BinaryInstaller.restartDaemon() + if (daemonRestart.isFailure) { + installErrorMessage = daemonRestart.exceptionOrNull()?.message + ?: context.getString(R.string.binary_installation_failed) + isInstalling = false + resetForPostInstallation() + forceRefresh() + return@fold + } } // Step 3: Install module isInstallingModule = true diff --git a/Android/app/src/main/java/com/droidspaces/app/util/BinaryInstaller.kt b/Android/app/src/main/java/com/droidspaces/app/util/BinaryInstaller.kt index 26e39a29..7d628049 100644 --- a/Android/app/src/main/java/com/droidspaces/app/util/BinaryInstaller.kt +++ b/Android/app/src/main/java/com/droidspaces/app/util/BinaryInstaller.kt @@ -63,9 +63,9 @@ object BinaryInstaller { val droidspacesBinaryName = getDroidspacesBinaryName() val busyboxBinaryName = getBusyboxBinaryName() - // Always install to the canonical path. The daemon's g_self_path fix - // means this is safe even while the daemon is running - the mv is - // atomic and the daemon automatically re-execs the new binary. + // Always install to the canonical path. The move is atomic, so an + // already-running daemon can keep using its old inode until the app + // restarts the daemon after the swap. val droidspacesTargetPath = Constants.DROIDSPACES_BINARY_PATH val busyboxTargetPath = Constants.BUSYBOX_BINARY_PATH @@ -176,16 +176,61 @@ object BinaryInstaller { } /** - * After a live binary swap, send SIGUSR2 to the running daemon - * so it acknowledges the update (used for logging). + * Restart the complete daemon tree after an atomic backend update. + * + * Re-executed CLI sessions already resolve the new canonical binary, but + * the daemon's embedded socketd bridge stays mapped to the old inode. If a + * release changes an internal request/config layout, mixing that old bridge + * with the new CLI can reinterpret unrelated flags. Restarting the parent + * also terminates its bridge through PR_SET_PDEATHSIG, so both processes + * come back from the same binary. */ - suspend fun signalDaemon(): Unit = withContext(Dispatchers.IO) { - val pidResult = Shell.cmd("cat ${Constants.DAEMON_PID_FILE} 2>/dev/null").exec() - if (pidResult.isSuccess && pidResult.out.isNotEmpty()) { - val pid = pidResult.out[0].trim() - if (pid.isNotEmpty()) { - Shell.cmd("kill -USR2 $pid 2>/dev/null").exec() + suspend fun restartDaemon(): Result = withContext(Dispatchers.IO) { + try { + val oldPid = Shell.cmd("cat ${Constants.DAEMON_PID_FILE} 2>/dev/null") + .exec().out.firstOrNull()?.trim()?.toIntOrNull() + + if (oldPid != null && oldPid > 1) { + Shell.cmd("kill -TERM $oldPid 2>/dev/null").exec() + + var alive = true + for (attempt in 0 until 30) { + alive = Shell.cmd("kill -0 $oldPid 2>/dev/null").exec().isSuccess + if (!alive) break + Thread.sleep(100) + } + if (alive) { + Shell.cmd("kill -KILL $oldPid 2>/dev/null").exec() + Thread.sleep(100) + } + } + + // Preserve the daemon SELinux entrypoint label used by the module. + Shell.cmd( + "chcon u:object_r:droidspacesd_exec:s0 ${Constants.DROIDSPACES_BINARY_PATH} 2>/dev/null" + ).exec() + + val launch = Shell.cmd("${Constants.DROIDSPACES_BINARY_PATH} daemon 2>&1").exec() + if (!launch.isSuccess) { + return@withContext Result.failure( + Exception("Failed to restart Droidspaces daemon: ${launch.err.joinToString()}") + ) } + + repeat(30) { + val pid = Shell.cmd("cat ${Constants.DAEMON_PID_FILE} 2>/dev/null") + .exec().out.firstOrNull()?.trim()?.toIntOrNull() + if (pid != null && pid > 1 && + Shell.cmd("kill -0 $pid 2>/dev/null").exec().isSuccess + ) { + return@withContext Result.success(Unit) + } + Thread.sleep(100) + } + + Result.failure(Exception("Droidspaces daemon did not become ready after update")) + } catch (error: Exception) { + Result.failure(error) } } diff --git a/Android/app/src/main/java/com/droidspaces/app/util/ContainerConfigState.kt b/Android/app/src/main/java/com/droidspaces/app/util/ContainerConfigState.kt index 2de58003..01bc9278 100644 --- a/Android/app/src/main/java/com/droidspaces/app/util/ContainerConfigState.kt +++ b/Android/app/src/main/java/com/droidspaces/app/util/ContainerConfigState.kt @@ -16,6 +16,12 @@ package com.droidspaces.app.util */ data class ContainerConfigState( val netMode: String = "nat", + val netParent: String = "", + val netMac: String = "", + val netIpam: String = "dhcp", + val hostAccess: String = "none", + val netAddress: String = "", + val netGateway: String = "", val disableIPv6: Boolean = false, val enableAndroidStorage: Boolean = false, val enableHwAccess: Boolean = false, @@ -48,6 +54,12 @@ data class ContainerConfigState( /** Extract the editable config fields from an existing container. */ fun ContainerInfo.toConfigState(): ContainerConfigState = ContainerConfigState( netMode = netMode, + netParent = netParent, + netMac = netMac, + netIpam = netIpam, + hostAccess = hostAccess, + netAddress = netAddress, + netGateway = netGateway, disableIPv6 = disableIPv6, enableAndroidStorage = enableAndroidStorage, enableHwAccess = enableHwAccess, @@ -84,6 +96,12 @@ fun ContainerInfo.toConfigState(): ContainerConfigState = ContainerConfigState( */ fun ContainerInfo.withConfig(state: ContainerConfigState): ContainerInfo = copy( netMode = state.netMode, + netParent = state.netParent, + netMac = state.netMac, + netIpam = state.netIpam, + hostAccess = state.hostAccess, + netAddress = state.netAddress, + netGateway = state.netGateway, disableIPv6 = state.disableIPv6, enableAndroidStorage = state.enableAndroidStorage, enableHwAccess = state.enableHwAccess, @@ -112,3 +130,37 @@ fun ContainerInfo.withConfig(state: ContainerConfigState): ContainerInfo = copy( gatewayIface = state.gatewayIface, gatewayBridge = state.gatewayBridge, ) + +private fun String.isValidIpv4(): Boolean { + val octets = split('.') + return octets.size == 4 && octets.all { + it.isNotEmpty() && it.length <= 3 && it.all(Char::isDigit) && + (it.toIntOrNull() ?: -1) in 0..255 + } +} + +private fun String.isValidUnicastMac(): Boolean { + val octets = split(':') + if (octets.size != 6 || octets.any { it.length != 2 || it.toIntOrNull(16) == null }) return false + val bytes = octets.map { it.toInt(16) } + return bytes.any { it != 0 } && (bytes[0] and 1) == 0 +} + +fun ContainerConfigState.isNetMacValid(): Boolean = + netMode != "macvlan" || netMac.isBlank() || netMac.isValidUnicastMac() + +/** Validation shared by create/edit action gating and the direct-L2 form. */ +fun ContainerConfigState.isDirectNetworkValid(): Boolean { + if (netMode != "ipvlan" && netMode != "macvlan") return true + // Blank selects the backend's Android-aware active-uplink detection. + if (netParent.isNotBlank() && (netParent.length >= 16 || + netParent.any { it.isWhitespace() || it == '/' })) return false + if (!isNetMacValid()) return false + if (netIpam == "dhcp") return true + if (netIpam != "static") return false + val parts = netAddress.split('/', limit = 2) + val prefix = parts.getOrNull(1)?.toIntOrNull() + return parts.size == 2 && parts[0].isValidIpv4() && prefix != null && + prefix in 0..32 && + netGateway.isValidIpv4() +} 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 029ee473..a57b935e 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,11 +25,23 @@ data class PortForward( val proto: String = "tcp" ) +data class DirectL2Capabilities( + val ipvlanSupported: Boolean = false, + val macvlanSupported: Boolean = false +) + data class ContainerInfo( val name: String, val hostname: String, val rootfsPath: String, val netMode: String = Constants.DEFAULT_NET_MODE, + val netParent: String = "", + val netMac: String = "", + val netIpam: String = "dhcp", + val hostAccess: String = "none", + val hostAccessPtpCidr: String = "", + val netAddress: String = "", + val netGateway: String = "", val disableIPv6: Boolean = false, val enableAndroidStorage: Boolean = false, val enableHwAccess: Boolean = false, @@ -75,6 +87,19 @@ data class ContainerInfo( appendLine("hostname=$hostname") appendLine("rootfs_path=$rootfsPath") appendLine("net_mode=$netMode") + if (netMode == "ipvlan" || netMode == "macvlan") { + appendLine("net_parent=$netParent") + if (netMode == "macvlan" && netMac.isNotBlank()) appendLine("net_mac=$netMac") + appendLine("net_ipam=$netIpam") + appendLine("host_access=$hostAccess") + if (hostAccess == "ptp" && hostAccessPtpCidr.isNotBlank()) { + appendLine("host_access_ptp_cidr=$hostAccessPtpCidr") + } + if (netIpam == "static") { + appendLine("net_address=$netAddress") + appendLine("net_gateway=$netGateway") + } + } appendLine("disable_ipv6=${if (disableIPv6) "1" else "0"}") appendLine("enable_android_storage=${if (enableAndroidStorage) "1" else "0"}") appendLine("enable_hw_access=${if (enableHwAccess) "1" else "0"}") @@ -319,6 +344,13 @@ object ContainerManager { getRootfsPath(containerName) }, netMode = configMap["net_mode"] ?: Constants.DEFAULT_NET_MODE, + netParent = configMap["net_parent"] ?: "", + netMac = configMap["net_mac"] ?: "", + netIpam = configMap["net_ipam"] ?: "dhcp", + hostAccess = configMap["host_access"] ?: "none", + hostAccessPtpCidr = configMap["host_access_ptp_cidr"] ?: "", + netAddress = configMap["net_address"] ?: "", + netGateway = configMap["net_gateway"] ?: "", disableIPv6 = configMap["disable_ipv6"] == "1", enableAndroidStorage = configMap["enable_android_storage"] == "1", enableHwAccess = configMap["enable_hw_access"] == "1", @@ -450,6 +482,24 @@ object ContainerManager { } } + /** Query live kernel support using the backend's temporary Netlink probes. */ + suspend fun getDirectL2Capabilities(): DirectL2Capabilities = withContext(Dispatchers.IO) { + try { + val result = Shell.cmd("\"${Constants.DROIDSPACES_BINARY_PATH}\" --format check 2>/dev/null").exec() + if (!result.isSuccess) return@withContext DirectL2Capabilities() + val values = result.out.mapNotNull { line -> + val parts = line.trim().split("=", limit = 2) + if (parts.size == 2) parts[0] to parts[1] else null + }.toMap() + DirectL2Capabilities( + ipvlanSupported = values["CONFIG_IPVLAN"] == "1", + macvlanSupported = values["CONFIG_MACVLAN"] == "1" + ) + } catch (_: Exception) { + DirectL2Capabilities() + } + } + /** * Update container configuration. * Only updates the configurable options (hostname, flags), not name or rootfsPath. diff --git a/Android/app/src/main/res/values-zh-rCN/strings.xml b/Android/app/src/main/res/values-zh-rCN/strings.xml index 1e9c99f9..dc75541c 100644 --- a/Android/app/src/main/res/values-zh-rCN/strings.xml +++ b/Android/app/src/main/res/values-zh-rCN/strings.xml @@ -475,6 +475,35 @@ 拖动以重新排序 网关(虚拟路由器) 网关 + IPvlan(直连局域网) + IPvlan + Macvlan(直连局域网) + Macvlan + 直连局域网设置 + 在 Android 父接口上创建子链路。DHCP 由容器内的 NetworkManager、networkd 或 dhcpcd 发起,由外部路由器应答;Droidspaces 不启用 NAT,也不运行内置 DHCP 服务。 + 父接口 + 自动检测(活动上行),或例如 wlan0 + 自动检测(活动上行) + 留空时自动检测 Android 当前活动上行,也可明确指定父接口。 + 请输入少于 16 个字符的 Linux 接口名。 + Macvlan 通过 Wi-Fi 使用时,依赖无线路由器/AP 和手机两端都支持且已启用 4addr(WDS)模式。Droidspaces 会尝试在手机端开启 4addr,但无法替你开启路由器端。 + 设备 MAC(可选) + 留空则由内核选择;填写单播 MAC 可配合路由器固定租约。 + 请输入有效的单播 MAC,例如 02:11:22:33:44:55。 + 地址配置 + DHCP(容器网络管理器) + 静态 + IPv4 地址 / 前缀 + 宿主互通 + 不启用 + 专用 PTP(推荐) + 直连局域网地址(shim) + 保持 ipvlan/macvlan 默认的宿主隔离行为。 + 增加专用 dshost0 链路,可靠实现宿主与容器互通,不改变容器的局域网地址。 + 通过共享父接口 shim 路由容器的局域网 IPv4;DHCP 地址会自动检测。 + 默认网关 + 请输入有效的 IPv4 CIDR 和 IPv4 网关。 + IPvlan 与父接口共用 MAC。Droidspaces 会让 systemd-networkd 使用 DUID 客户端标识,但部分路由器只按 MAC 识别租约,可能无法分配独立地址。 网关设置 把这个容器通过另一个充当路由器的容器(例如 OpenWRT)路由。该网关必须先运行起来。 网关容器 diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index bcaf3a50..d6c34798 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -236,6 +236,35 @@ None Gateway (Virtual Router) Gateway + IPvlan (Direct LAN) + IPvlan + Macvlan (Direct LAN) + Macvlan + Direct LAN settings + Creates a child link on an Android host interface. DHCP is handled inside the container by NetworkManager, networkd, or dhcpcd and answered by the external router; Droidspaces does not run NAT or its embedded DHCP server. + Parent interface + Automatic (active uplink), or e.g. wlan0 + Automatic (active uplink) + Leave blank to auto-detect the active Android uplink, or enter a parent interface explicitly. + Enter a Linux interface name shorter than 16 characters. + Macvlan over Wi-Fi depends on both the wireless router/AP and this phone supporting and enabling 4addr (WDS) mode. Droidspaces will try to enable 4addr on the phone, but cannot enable it on the router. + Device MAC (optional) + Leave blank to let the kernel choose; enter a unicast MAC to keep a router reservation. + Enter a valid unicast MAC such as 02:11:22:33:44:55. + Address configuration + DHCP (guest network manager) + Static + IPv4 address / prefix + Host access + None + Private PTP (recommended) + Direct LAN address (shim) + Keep the normal ipvlan/macvlan host isolation. + Adds a private dshost0 link for reliable host-to-container access without changing the LAN address. + Routes the container’s LAN IPv4 through a shared parent shim. DHCP addresses are detected automatically. + Default gateway + Enter a valid IPv4 CIDR and IPv4 gateway. + IPvlan shares the parent MAC. Droidspaces asks systemd-networkd to use a DUID client ID, but some routers identify leases only by MAC and may not issue separate leases. Gateway Settings diff --git a/Documentation/Kernel-Configuration.md b/Documentation/Kernel-Configuration.md index dbe7be14..11793012 100644 --- a/Documentation/Kernel-Configuration.md +++ b/Documentation/Kernel-Configuration.md @@ -121,6 +121,10 @@ CONFIG_IP_NF_NAT=y # Disable this on older kernels to make internet work CONFIG_ANDROID_PARANOID_NETWORK=n +# Optional direct L2 network modes +CONFIG_IPVLAN=y +CONFIG_MACVLAN=y + # Fix for docker unsafe procfs error CONFIG_USER_NS=y ``` @@ -250,6 +254,10 @@ CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y # Fix for docker unsafe procfs error CONFIG_USER_NS=y +# Direct L2 network modes +CONFIG_IPVLAN=y +CONFIG_MACVLAN=y + # UFW support CONFIG_NETFILTER_XT_TARGET_REJECT=y CONFIG_NETFILTER_XT_TARGET_LOG=y @@ -308,6 +316,7 @@ This checks for: - devtmpfs support - OverlayFS support (optional, for volatile mode) - VETH and Bridge support (optional, for NAT mode) +- IPVLAN and MACVLAN support (optional, for direct L2 modes) - PTY/devpts support - Loop device support - ext4 support @@ -333,6 +342,7 @@ This checks for: | OverlayFS | `CONFIG_OVERLAY_FS` | Volatile mode unavailable. | | Network namespace | `CONFIG_NET_NS=y` | NAT and None modes unavailable. | | VETH / Bridge | `CONFIG_VETH` / `CONFIG_BRIDGE` | NAT mode unavailable. | +| IPVLAN / MACVLAN | `CONFIG_IPVLAN` / `CONFIG_MACVLAN` | The corresponding direct L2 mode is unavailable. | | Seccomp | `CONFIG_SECCOMP=y` | Seccomp shield disabled. Security risk. | --- diff --git a/Documentation/zh-CN/Kernel-Configuration.md b/Documentation/zh-CN/Kernel-Configuration.md index c24f6a36..4e097d4b 100644 --- a/Documentation/zh-CN/Kernel-Configuration.md +++ b/Documentation/zh-CN/Kernel-Configuration.md @@ -121,6 +121,10 @@ CONFIG_IP_NF_NAT=y # 在旧内核上禁用此选项以使互联网正常工作 CONFIG_ANDROID_PARANOID_NETWORK=n + +# 可选的直连二层网络模式 +CONFIG_IPVLAN=y +CONFIG_MACVLAN=y ``` @@ -246,6 +250,10 @@ CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y # --- 以下配置为可选但建议开启 --- +# 直连二层网络模式 +CONFIG_IPVLAN=y +CONFIG_MACVLAN=y + # UFW 支持 CONFIG_NETFILTER_XT_TARGET_REJECT=y CONFIG_NETFILTER_XT_TARGET_LOG=y @@ -304,6 +312,7 @@ su -c droidspaces check - devtmpfs 支持 - OverlayFS 支持(可选,用于易失模式) - VETH 和 Bridge 支持(可选,用于 NAT 模式) +- IPVLAN 和 MACVLAN 支持(可选,用于直连二层模式) - PTY/devpts 支持 - Loop 设备支持 - ext4 支持 @@ -329,6 +338,7 @@ su -c droidspaces check | OverlayFS | `CONFIG_OVERLAY_FS` | 易失模式不可用。 | | 网络命名空间 | `CONFIG_NET_NS=y` | NAT 和 None 模式不可用。 | | VETH / Bridge | `CONFIG_VETH` / `CONFIG_BRIDGE` | NAT 模式不可用。 | +| IPVLAN / MACVLAN | `CONFIG_IPVLAN` / `CONFIG_MACVLAN` | 对应的直连二层模式不可用。 | | Seccomp | `CONFIG_SECCOMP=y` | Seccomp 防护盾已禁用。存在安全风险。 | --- diff --git a/Makefile b/Makefile index a92194d3..32a99695 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ SRCS = $(SRC_DIR)/main.c \ $(SRC_DIR)/mount.c \ $(SRC_DIR)/cgroup.c \ $(SRC_DIR)/net/network.c \ + $(SRC_DIR)/net/host_access.c \ $(SRC_DIR)/terminal.c \ $(SRC_DIR)/console.c \ $(SRC_DIR)/pid.c \ @@ -64,6 +65,7 @@ SRCS = $(SRC_DIR)/main.c \ # Compiler flags - hardened warning set, all warnings are errors CFLAGS = -Wall -Wextra -Wpedantic -Werror -O2 -flto=auto -std=gnu99 -I$(SRC_DIR)/include -no-pie -pthread +CFLAGS += -MMD -MP CFLAGS += -Wformat=2 -Wformat-security -Wformat-overflow=2 -Wformat-truncation=2 CFLAGS += -Wnull-dereference -Wcast-qual -Wlogical-op -Wshadow -Wdouble-promotion -Wundef CFLAGS += -Wduplicated-cond -Wduplicated-branches -Wimplicit-fallthrough=3 @@ -84,6 +86,7 @@ ARCH := $(shell $(CC) -dumpmachine 2>/dev/null | cut -d'-' -f1 | \ # Per-arch object directory - prevents collisions when building multiple archs OBJ_DIR = $(OUT_DIR)/.obj/$(ARCH) OBJS = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o) +DEPS = $(OBJS:.o=.d) # Cross-compiler helper HOME_VAR := $(shell echo $$HOME) @@ -142,6 +145,10 @@ $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR) $(Q)mkdir -p $(dir $@) $(Q)$(CC) $(CFLAGS) -c $< -o $@ +# Rebuild every translation unit whose included project header changed. This +# prevents stale objects from silently using an older struct layout. +-include $(DEPS) + # Link step $(BINARY_NAME): $(OBJS) | $(OUT_DIR) $(msg_ld) diff --git a/src/boot.c b/src/boot.c index fca53531..146d1fcf 100644 --- a/src/boot.c +++ b/src/boot.c @@ -9,6 +9,284 @@ #include #include +static int ds_hex_value(unsigned char c) { + if (c >= '0' && c <= '9') + return (int)(c - '0'); + if (c >= 'a' && c <= 'f') + return (int)(c - 'a') + 10; + if (c >= 'A' && c <= 'F') + return (int)(c - 'A') + 10; + return -1; +} + +static int ds_file_contains_bytes(const char *path, const char *needle) { + unsigned char buffer[4096 + 64]; + const size_t needle_len = strlen(needle); + size_t carry = 0; + int found = 0; + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0 || needle_len == 0 || needle_len > 64) { + if (fd >= 0) + close(fd); + return 0; + } + + for (;;) { + ssize_t n = read(fd, buffer + carry, sizeof(buffer) - carry); + if (n < 0 && errno == EINTR) + continue; + if (n <= 0) + break; + + size_t total = carry + (size_t)n; + for (size_t i = 0; i + needle_len <= total; i++) { + if (memcmp(buffer + i, needle, needle_len) == 0) { + found = 1; + break; + } + } + if (found) + break; + + carry = total < needle_len - 1 ? total : needle_len - 1; + memmove(buffer, buffer + total - carry, carry); + } + + close(fd); + return found; +} + +static int ds_networkd_supports_foreign_nexthops(void) { + static const char setting[] = "ManageForeignNextHops"; + return ds_file_contains_bytes("usr/lib/systemd/systemd-networkd", setting) || + ds_file_contains_bytes("lib/systemd/systemd-networkd", setting); +} + +/* Build a stable DHCP identity for direct-L2 links. + * + * ipvlan shares its lower device's MAC, so ClientIdentifier=mac would make + * every ipvlan container (and Android itself) look like the same DHCP client. + * Use the container UUID as a DUID-UUID instead. Legacy configs without a + * valid UUID get a deterministic fallback derived from the container name; + * the identity must never change merely because the container restarts. + * + * The DHCP hostname gets a stable numeric suffix so cloned/common hostnames + * remain distinguishable without causing a fresh lease on every boot. */ +static void ds_build_direct_dhcp_identity(const struct ds_config *cfg, + char duid_raw[48], uint32_t *iaid, + char dhcp_hostname[64], + char ipv6_ra_token[46]) { + uint8_t id[16] = {0}; + int valid_uuid = cfg->uuid[0] != '\0' && strlen(cfg->uuid) == DS_UUID_LEN; + + if (valid_uuid) { + for (size_t i = 0; i < sizeof(id); i++) { + int hi = ds_hex_value((unsigned char)cfg->uuid[i * 2]); + int lo = ds_hex_value((unsigned char)cfg->uuid[i * 2 + 1]); + if (hi < 0 || lo < 0) { + valid_uuid = 0; + break; + } + id[i] = (uint8_t)((hi << 4) | lo); + } + } + + if (!valid_uuid) { + const char *seed = cfg->container_name[0] ? cfg->container_name + : cfg->hostname; + uint64_t h1 = UINT64_C(1469598103934665603); + uint64_t h2 = UINT64_C(1099511628211) ^ UINT64_C(0x9e3779b97f4a7c15); + for (const unsigned char *p = (const unsigned char *)seed; *p; p++) { + h1 ^= *p; + h1 *= UINT64_C(1099511628211); + h2 ^= (uint64_t)(*p + 0x9dU); + h2 *= UINT64_C(14029467366897019727); + } + for (size_t i = 0; i < 8; i++) { + id[i] = (uint8_t)(h1 >> (i * 8)); + id[i + 8] = (uint8_t)(h2 >> (i * 8)); + } + } + + snprintf(duid_raw, 48, + "%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:" + "%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x", + id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], + id[9], id[10], id[11], id[12], id[13], id[14], id[15]); + snprintf(ipv6_ra_token, 46, + "prefixstable," + "%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x", + id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], + id[9], id[10], id[11], id[12], id[13], id[14], id[15]); + + *iaid = ((uint32_t)id[12] << 24) | ((uint32_t)id[13] << 16) | + ((uint32_t)id[14] << 8) | (uint32_t)id[15]; + if (*iaid == 0) + *iaid = 1; + + uint32_t suffix = 2166136261U; + for (size_t i = 0; i < sizeof(id); i++) { + suffix ^= id[i]; + suffix *= 16777619U; + } + suffix %= 100000U; + + const char *source = cfg->hostname[0] ? cfg->hostname : cfg->container_name; + size_t used = 0; + const size_t base_limit = 57; /* '-' + five digits + NUL => max 63 chars */ + for (const unsigned char *p = (const unsigned char *)source; + *p && used < base_limit; p++) { + unsigned char c = (unsigned char)tolower(*p); + if (isalnum(c)) { + dhcp_hostname[used++] = (char)c; + } else if (used > 0 && dhcp_hostname[used - 1] != '-') { + dhcp_hostname[used++] = '-'; + } + } + while (used > 0 && dhcp_hostname[used - 1] == '-') + used--; + if (used == 0) { + memcpy(dhcp_hostname, "droidspace", sizeof("droidspace") - 1); + used = sizeof("droidspace") - 1; + } + snprintf(dhcp_hostname + used, 64 - used, "-%05u", suffix); +} + +/* Per-boot network-service policy. /run overrides stale rootfs drop-ins, so + * old images immediately learn about ipvlan/macvlan without being re-extracted. + * Host/none and direct-L2 static must keep guest managers stopped; NAT, + * gateway, and direct-L2 DHCP let the guest own eth0 configuration. */ +static void ds_write_guest_network_policy(const struct ds_config *cfg) { + if (!cfg) + return; + + int direct_dhcp = + (cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) && + cfg->net_ipam == DS_NET_IPAM_DHCP; + int allow_guest = cfg->net_mode == DS_NET_NAT || + cfg->net_mode == DS_NET_GATEWAY || direct_dhcp; + + mkdir("run/systemd", 0755); + mkdir("run/systemd/system", 0755); + const char *units[] = {"NetworkManager.service", "dhcpcd.service", + "systemd-networkd.service", + "systemd-resolved.service", NULL}; + const char *allow = "[Service]\nExecCondition=\n"; + const char *block = "[Service]\nExecCondition=\nExecCondition=/bin/false\n"; + for (size_t i = 0; units[i]; i++) { + char dir[PATH_MAX]; + char file[PATH_MAX]; + snprintf(dir, sizeof(dir), "run/systemd/system/%s.d", units[i]); + if (mkdir(dir, 0755) < 0 && errno != EEXIST) { + ds_warn("[NET] Cannot create systemd network policy directory %s: %s", + dir, strerror(errno)); + continue; + } + snprintf(file, sizeof(file), "%s/zz-droidspaces-netmode.conf", dir); + if (write_file(file, allow_guest ? allow : block) < 0) + ds_warn("[NET] Cannot write systemd network policy %s", file); + } + + /* dshost0 is a runtime-owned point-to-point control link. The backend + * assigns both ends before init starts; guest managers must leave it alone + * while continuing to manage the direct-L2 eth0 normally. */ + if (cfg->host_access == DS_HOST_ACCESS_PTP) { + mkdir("run/systemd/network", 0755); + if (write_file("run/systemd/network/05-droidspaces-host-access.network", + "[Match]\nName=dshost0\n\n[Link]\nUnmanaged=yes\n") < 0) + ds_warn("[NET] Cannot write dshost0 systemd-networkd policy"); + + mkdir("run/NetworkManager", 0755); + mkdir("run/NetworkManager/conf.d", 0755); + if (write_file("run/NetworkManager/conf.d/" + "05-droidspaces-host-access.conf", + "[keyfile]\nunmanaged-devices=interface-name:dshost0\n") < 0) + ds_warn("[NET] Cannot write dshost0 NetworkManager policy"); + } + + if (direct_dhcp) { + mkdir("run/systemd/network", 0755); + + /* systemd-networkd 257+ represents RA gateways as kernel nexthop objects. + * Android kernels commonly lack the nexthop netlink API (RTM_NEWNEXTHOP), + * which makes IPv6 RA fail with EOPNOTSUPP. Keep the traditional direct + * gateway route representation when networkd advertises the compatibility + * setting; older releases remain untouched. */ + if (cfg->net_mode == DS_NET_IPVLAN && !cfg->disable_ipv6 && + ds_networkd_supports_foreign_nexthops()) { + mkdir("run/systemd/networkd.conf.d", 0755); + if (write_file("run/systemd/networkd.conf.d/" + "zz-droidspaces-kernel-compat.conf", + "[Network]\nManageForeignNextHops=no\n") < 0) + ds_warn("[NET] Cannot write systemd-networkd kernel compatibility " + "configuration"); + } + + char network[2048]; + char duid_raw[48]; + char dhcp_hostname[64]; + char ipv6_ra_token[46]; + char client_identity[256]; + char ipv6_ra[128]; + uint32_t iaid; + ds_build_direct_dhcp_identity(cfg, duid_raw, &iaid, dhcp_hostname, + ipv6_ra_token); + + if (cfg->net_mode == DS_NET_IPVLAN) { + snprintf(client_identity, sizeof(client_identity), + "ClientIdentifier=duid\n" + "DUIDType=uuid\n" + "DUIDRawData=%s\n" + "IAID=%u\n", + duid_raw, iaid); + } else { + snprintf(client_identity, sizeof(client_identity), + "ClientIdentifier=mac\n"); + } + + const char *ipv6_link = cfg->disable_ipv6 + ? "IPv6AcceptRA=no\nLinkLocalAddressing=no\n" + : "IPv6AcceptRA=yes\n" + "LinkLocalAddressing=ipv6\n"; + if (cfg->disable_ipv6) { + ipv6_ra[0] = '\0'; + } else if (cfg->net_mode == DS_NET_IPVLAN) { + snprintf(ipv6_ra, sizeof(ipv6_ra), + "\n[IPv6AcceptRA]\nToken=%s\nUseDNS=yes\n", ipv6_ra_token); + } else { + snprintf(ipv6_ra, sizeof(ipv6_ra), + "\n[IPv6AcceptRA]\nUseDNS=yes\n"); + } + snprintf(network, sizeof(network), + "[Match]\n" + "Name=eth0\n\n" + "[Network]\n" + "DHCP=ipv4\n" + "%s\n" + "[DHCPv4]\n" + "%s" + "SendHostname=yes\n" + "Hostname=%s\n" + "UseDNS=yes\n" + "UseDomains=yes\n" + "RequestBroadcast=yes\n" + "RouteMetric=100\n" + "%s", + ipv6_link, client_identity, dhcp_hostname, ipv6_ra); + if (write_file("run/systemd/network/10-droidspaces-eth0.network", + network) < 0) + ds_warn("[NET] Cannot write direct-L2 systemd-networkd configuration"); + else + ds_log("[NET] DHCP identity: %s (%s)", dhcp_hostname, + cfg->net_mode == DS_NET_IPVLAN ? "DUID-UUID" : "MAC"); + } + + ds_log("[NET] Guest network services: %s (mode=%d, ipam=%s)", + allow_guest ? "enabled" : "blocked", cfg->net_mode, + cfg->net_ipam == DS_NET_IPAM_STATIC ? "static" : "dhcp"); +} + /* * ds_apply_capability_hardening() * @@ -144,9 +422,18 @@ int internal_boot(struct ds_config *cfg) { } } + if ((cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) && + hs.status < 0) + ds_die("Direct L2 network setup failed: %s", strerror(-hs.status)); + /* Configure our side of the veth (or just loopback for DS_NET_NONE) */ if (cfg->net_mode == DS_NET_NAT || cfg->net_mode == DS_NET_GATEWAY) { setup_veth_child_side_named(cfg, hs.peer_name, hs.ip_str); + } else if (cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) { + if (setup_parent_link_child_side(cfg, hs.peer_name) < 0) + ds_die("Failed to configure direct L2 interface inside container"); } else { /* DS_NET_NONE: just bring up loopback */ ds_nl_ctx_t *nlctx = ds_nl_open(); @@ -481,6 +768,8 @@ int internal_boot(struct ds_config *cfg) { ds_warn("Boot: Failed to save internal configuration backup"); } + ds_write_guest_network_policy(cfg); + write_file("run/droidspaces/name", cfg->container_name); if (cfg->img_mount_point[0]) diff --git a/src/check.c b/src/check.c index acd3f141..5f677914 100644 --- a/src/check.c +++ b/src/check.c @@ -145,6 +145,34 @@ static int check_veth_support(void) { return (ret == 0); } +/* Probe direct-L2 link types against real host interfaces. A live NEWLINK + * roundtrip detects both built-in (=y) and loadable (=m) implementations and + * also catches kernels where the feature exists but cannot actually be used + * by the runtime. */ +static int check_direct_l2_support(const char *kind) { + if (!is_root) + return 0; + + char ifaces[64][IFNAMSIZ]; + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) + return 0; + int count = ds_nl_list_ifaces(ctx, ifaces, 64); + ds_nl_close(ctx); + if (count < 0) + return 0; + + for (int i = 0; i < count; i++) { + if (strcmp(ifaces[i], "lo") == 0 || strncmp(ifaces[i], "ds-", 3) == 0) + continue; + char reason[256]; + if (ds_nl_probe_parent_capability(ifaces[i], kind, reason, + sizeof(reason)) == 0) + return 1; + } + return 0; +} + static int check_kernel_version_supported(void) { int major = 0, minor = 0; if (get_kernel_version(&major, &minor) < 0) @@ -400,6 +428,12 @@ int check_requirements_detailed(void) { print_ds_check("Veth pair support", "Required for --net=nat; no fallback exists if absent", check_veth_support(), "OPT"); + print_ds_check("IPvlan support", + "CONFIG_IPVLAN; required for --net=ipvlan", + check_direct_l2_support("ipvlan"), "OPT"); + print_ds_check("Macvlan support", + "CONFIG_MACVLAN; required for --net=macvlan", + check_direct_l2_support("macvlan"), "OPT"); print_ds_check("User namespace", "CONFIG_USER_NS; enable per container with --allow-userns. " "Needed by Docker on some kernels, by sandboxed apps " @@ -428,3 +462,12 @@ int check_requirements_detailed(void) { return 0; } + +int check_requirements_format(void) { + check_root(); + printf("CONFIG_USER_NS=%d\n", + access("/proc/self/ns/user", F_OK) == 0 ? 1 : 0); + printf("CONFIG_IPVLAN=%d\n", check_direct_l2_support("ipvlan")); + printf("CONFIG_MACVLAN=%d\n", check_direct_l2_support("macvlan")); + return 0; +} diff --git a/src/config.c b/src/config.c index 1a5e507a..8d8cfd79 100644 --- a/src/config.c +++ b/src/config.c @@ -369,12 +369,49 @@ int ds_config_load(const char *config_path, struct ds_config *cfg) { } else if (strcmp(val, "gateway") == 0 || strcmp(val, "delegated-gateway") == 0) { cfg->net_mode = DS_NET_GATEWAY; + } else if (strcmp(val, "ipvlan") == 0) { + cfg->net_mode = DS_NET_IPVLAN; + } else if (strcmp(val, "macvlan") == 0) { + cfg->net_mode = DS_NET_MACVLAN; } else { ds_warn( "Unknown network mode '%s' in config file. Defaulting to 'host'.", val); cfg->net_mode = DS_NET_HOST; } + } else if (strcmp(key, "net_ipam") == 0) { + if (strcmp(val, "static") == 0) + cfg->net_ipam = DS_NET_IPAM_STATIC; + else if (strcmp(val, "dhcp") == 0) + cfg->net_ipam = DS_NET_IPAM_DHCP; + else + ds_warn("config: unknown net_ipam '%s'; using dhcp", val); + } else if (strcmp(key, "host_access") == 0) { + if (strcmp(val, "ptp") == 0) + cfg->host_access = DS_HOST_ACCESS_PTP; + else if (strcmp(val, "shim") == 0) + cfg->host_access = DS_HOST_ACCESS_SHIM; + else if (strcmp(val, "none") == 0) + cfg->host_access = DS_HOST_ACCESS_NONE; + else + ds_warn("config: unknown host_access '%s'; using none", val); + } else if (strcmp(key, "host_access_ptp_cidr") == 0) { + safe_strncpy(cfg->host_access_ptp_cidr, val, + sizeof(cfg->host_access_ptp_cidr)); + } else if (strcmp(key, "net_parent") == 0) { + if (strlen(val) < IFNAMSIZ) + safe_strncpy(cfg->net_parent, val, sizeof(cfg->net_parent)); + else + ds_warn("config: ignoring too-long net_parent '%s'", val); + } else if (strcmp(key, "net_mac") == 0) { + if (strlen(val) < sizeof(cfg->net_mac)) + safe_strncpy(cfg->net_mac, val, sizeof(cfg->net_mac)); + else + ds_warn("config: ignoring too-long net_mac '%s'", val); + } else if (strcmp(key, "net_address") == 0) { + safe_strncpy(cfg->net_address, val, sizeof(cfg->net_address)); + } else if (strcmp(key, "net_gateway") == 0) { + safe_strncpy(cfg->net_gateway, val, sizeof(cfg->net_gateway)); } else if (strcmp(key, "gateway_container") == 0) { if (validate_container_name(val)) safe_strncpy(cfg->gateway_container, val, @@ -707,10 +744,37 @@ static void ds_config_serialize_known(FILE *f, struct ds_config *cfg) { fprintf(f, "net_mode=none\n"); } else if (cfg->net_mode == DS_NET_GATEWAY) { fprintf(f, "net_mode=gateway\n"); + } else if (cfg->net_mode == DS_NET_IPVLAN) { + fprintf(f, "net_mode=ipvlan\n"); + } else if (cfg->net_mode == DS_NET_MACVLAN) { + fprintf(f, "net_mode=macvlan\n"); } else { fprintf(f, "net_mode=host\n"); } + if (cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) { + if (cfg->net_parent[0]) + fprintf(f, "net_parent=%s\n", cfg->net_parent); + fprintf(f, "net_ipam=%s\n", + cfg->net_ipam == DS_NET_IPAM_STATIC ? "static" : "dhcp"); + fprintf(f, "host_access=%s\n", + cfg->host_access == DS_HOST_ACCESS_PTP + ? "ptp" + : (cfg->host_access == DS_HOST_ACCESS_SHIM ? "shim" : + "none")); + if (cfg->host_access == DS_HOST_ACCESS_PTP && + cfg->host_access_ptp_cidr[0]) + fprintf(f, "host_access_ptp_cidr=%s\n", cfg->host_access_ptp_cidr); + if (cfg->net_mode == DS_NET_MACVLAN && cfg->net_mac[0]) + fprintf(f, "net_mac=%s\n", cfg->net_mac); + if (cfg->net_ipam == DS_NET_IPAM_STATIC) { + if (cfg->net_address[0]) + fprintf(f, "net_address=%s\n", cfg->net_address); + if (cfg->net_gateway[0]) + fprintf(f, "net_gateway=%s\n", cfg->net_gateway); + } + } + if (cfg->net_mode == DS_NET_GATEWAY) { if (cfg->gateway_container[0]) fprintf(f, "gateway_container=%s\n", cfg->gateway_container); @@ -916,6 +980,38 @@ int ds_config_validate(struct ds_config *cfg) { errors++; } + if (cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) { + /* Empty is valid: the active host uplink is resolved at start time. */ + if (cfg->net_parent[0] && + (strlen(cfg->net_parent) >= IFNAMSIZ || + strchr(cfg->net_parent, '/') || + strpbrk(cfg->net_parent, " \t\r\n"))) + errors++; + if (cfg->net_ipam == DS_NET_IPAM_STATIC) { + char address[sizeof(cfg->net_address)]; + safe_strncpy(address, cfg->net_address, sizeof(address)); + char *slash = strchr(address, '/'); + char *end = NULL; + long prefix = slash ? strtol(slash + 1, &end, 10) : -1; + if (slash) + *slash = '\0'; + struct in_addr parsed; + if (!slash || !end || *end || prefix < 0 || prefix > 32 || + inet_pton(AF_INET, address, &parsed) != 1 || + inet_pton(AF_INET, cfg->net_gateway, &parsed) != 1) + errors++; + } + if ((cfg->net_mode == DS_NET_MACVLAN && cfg->net_mac[0] && + ds_parse_mac_address(cfg->net_mac, (uint8_t[6]){0}) < 0) || + (cfg->net_mode == DS_NET_IPVLAN && cfg->net_mac[0])) + errors++; + if (cfg->host_access < DS_HOST_ACCESS_NONE || + cfg->host_access > DS_HOST_ACCESS_SHIM) + errors++; + } else if (cfg->host_access != DS_HOST_ACCESS_NONE) { + errors++; + } + return (errors > 0) ? -1 : 0; } diff --git a/src/container.c b/src/container.c index 6822836a..343e700d 100644 --- a/src/container.c +++ b/src/container.c @@ -265,7 +265,10 @@ void cleanup_container_resources(struct ds_config *cfg, pid_t pid, } /* Network cleanup: remove host veth and owned network state */ - if (cfg->net_mode == DS_NET_NAT || cfg->net_mode == DS_NET_GATEWAY) { + if (cfg->net_mode == DS_NET_NAT || cfg->net_mode == DS_NET_GATEWAY || + ((cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) && + cfg->host_access != DS_HOST_ACCESS_NONE)) { ds_net_cleanup(cfg, pid > 0 ? pid : cfg->container_pid); } @@ -542,6 +545,10 @@ int start_rootfs(struct ds_config *cfg) { * Only relevant for NAT mode -- host/none modes skip this cleanly. */ if (cfg->net_mode == DS_NET_NAT) ds_net_resolve_static_ip(cfg); + if ((cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) && + cfg->host_access == DS_HOST_ACCESS_PTP) + ds_host_access_resolve_ptp(cfg); /* Persist UUID and resolved static_nat_ip (for NAT) to config immediately * so disk always matches the running container. CLI overrides (e.g. -f) @@ -1593,6 +1600,12 @@ int show_info(struct ds_config *cfg, int trust_cfg_pid) { case DS_NET_GATEWAY: net = "gateway"; break; + case DS_NET_IPVLAN: + net = "ipvlan"; + break; + case DS_NET_MACVLAN: + net = "macvlan"; + break; default: net = "host"; break; @@ -1620,6 +1633,23 @@ int show_info(struct ds_config *cfg, int trust_cfg_pid) { printf("GATEWAY_BRIDGE=%s\n", cfg->gateway_bridge); printf("GATEWAY_IFACE=%s\n", cfg->gateway_lan_ifname[0] ? cfg->gateway_lan_ifname : "eth1"); + } else if (cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) { + printf("NET_PARENT=%s\n", cfg->net_parent); + printf("NET_IPAM=%s\n", + cfg->net_ipam == DS_NET_IPAM_STATIC ? "static" : "dhcp"); + printf("HOST_ACCESS=%s\n", + cfg->host_access == DS_HOST_ACCESS_PTP + ? "ptp" + : (cfg->host_access == DS_HOST_ACCESS_SHIM ? "shim" : + "none")); + if (cfg->host_access == DS_HOST_ACCESS_PTP && + cfg->host_access_ptp_cidr[0]) + printf("HOST_ACCESS_PTP_CIDR=%s\n", cfg->host_access_ptp_cidr); + if (cfg->net_ipam == DS_NET_IPAM_STATIC) { + printf("NET_ADDRESS=%s\n", cfg->net_address); + printf("NET_GATEWAY=%s\n", cfg->net_gateway); + } } printf("DISABLE_IPV6=%d\n", cfg->disable_ipv6); @@ -1754,6 +1784,12 @@ int show_info(struct ds_config *cfg, int trust_cfg_pid) { case DS_NET_GATEWAY: net = "gateway"; break; + case DS_NET_IPVLAN: + net = "ipvlan"; + break; + case DS_NET_MACVLAN: + net = "macvlan"; + break; default: net = "host"; break; @@ -1768,6 +1804,22 @@ int show_info(struct ds_config *cfg, int trust_cfg_pid) { feat_count++; } + if (cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) { + printf(" Parent: %s\n", cfg->net_parent); + printf(" Addressing: %s", cfg->net_ipam == DS_NET_IPAM_STATIC + ? "static" + : "DHCP (guest manager)"); + if (cfg->net_ipam == DS_NET_IPAM_STATIC) + printf(" (%s via %s)", cfg->net_address, cfg->net_gateway); + printf("\n"); + printf(" Host access: %s\n", + cfg->host_access == DS_HOST_ACCESS_PTP + ? "private PTP" + : (cfg->host_access == DS_HOST_ACCESS_SHIM ? "LAN shim" : + "none")); + feat_count += 3; + } + if (cfg->net_mode == DS_NET_NAT) { const char *ip = cfg->static_nat_ip[0] ? cfg->static_nat_ip : cfg->nat_container_ip; diff --git a/src/documentation.c b/src/documentation.c index 1295fc07..3260b5af 100644 --- a/src/documentation.c +++ b/src/documentation.c @@ -288,7 +288,18 @@ static void print_page(int page, const char *bin) { p_printf(" --net=none No network access (air-gapped)\n"); p_printf(" --net=nat Isolated namespace with internet access\n"); p_printf(" --net=gateway LAN delegated to another container " - "(e.g. OpenWRT)\n\n"); + "(e.g. OpenWRT)\n"); + p_printf(" --net=ipvlan Direct L2 child of an Android host interface\n"); + p_printf(" --net=macvlan Direct L2 child with its own MAC address\n\n"); + + p_printf("%sDirect L2 Configuration:%s\n", bold, reset); + p_printf(" --net-parent=IFACE Parent, e.g. wlan0/eth0\n"); + p_printf(" --net-ipam=dhcp Guest network manager uses " + "upstream DHCP\n"); + p_printf(" --host-access=none|ptp|shim Optional host/container " + "reachability\n"); + p_printf(" --net-ipam=static --net-address=A/P --net-gateway=G\n"); + p_printf(" No Droidspaces bridge, NAT, or embedded DHCP server is used.\n\n"); p_printf("%sNAT Mode Configuration:%s\n", bold, reset); p_printf(" %s --name=mycontainer --rootfs=/path/to/rootfs --net=nat " diff --git a/src/include/droidspace.h b/src/include/droidspace.h index a229c994..3ef5206e 100644 --- a/src/include/droidspace.h +++ b/src/include/droidspace.h @@ -205,6 +205,19 @@ enum ds_net_mode { DS_NET_NAT, /* isolated netns + bridge + MASQUERADE */ DS_NET_NONE, /* isolated netns with loopback only */ DS_NET_GATEWAY, /* isolated netns attached to gateway LAN */ + DS_NET_IPVLAN, /* ipvlan L2 child of an Android host link */ + DS_NET_MACVLAN, /* macvlan bridge child of an Android host link */ +}; + +enum ds_net_ipam { + DS_NET_IPAM_DHCP = 0, /* guest network manager requests a lease */ + DS_NET_IPAM_STATIC, /* runtime assigns net_address/gateway */ +}; + +enum ds_host_access { + DS_HOST_ACCESS_NONE = 0, /* direct-L2 keeps normal host isolation */ + DS_HOST_ACCESS_PTP, /* private host<->guest veth control link */ + DS_HOST_ACCESS_SHIM, /* shared parent child + per-guest /32 route */ }; /* Opaque RTNETLINK context - defined in ds_netlink.c */ @@ -212,6 +225,7 @@ typedef struct ds_nl_ctx ds_nl_ctx_t; /* Handshake payload: Monitor → init via net_done_pipe */ struct ds_net_handshake { + int status; /* 0 on success, negative errno on failure */ char peer_name[16]; /* e.g. "ds-p12345" */ char ip_str[32]; /* e.g. "172.28.4.47/16" */ }; @@ -346,7 +360,14 @@ struct ds_config { char container_name[256]; /* --name= (mandatory) */ char hostname[256]; /* --hostname= or container_name */ char dns_servers[1024]; /* --dns= (comma/space separated) */ - enum ds_net_mode net_mode; /* --net=host|nat|none|gateway */ + enum ds_net_mode net_mode; /* --net=host|nat|none|gateway|... */ + enum ds_net_ipam net_ipam; /* --net-ipam=dhcp|static */ + enum ds_host_access host_access; /* --host-access=none|ptp|shim */ + char net_parent[IFNAMSIZ]; /* --net-parent=IFACE */ + char net_mac[18]; /* --net-mac=XX:XX:XX:XX:XX:XX */ + char net_address[32]; /* --net-address=IPv4/PREFIX */ + char net_gateway[INET_ADDRSTRLEN]; /* --net-gateway=IPv4 */ + char host_access_ptp_cidr[32]; /* internal stable 169.254.x.x/30 */ char dns_server_content[1024]; /* In-memory DNS config for boot */ char gateway_container[256]; /* --gateway=NAME for gateway mode */ char gateway_net[64]; /* --gateway-net=NAME (default: lan) */ @@ -709,6 +730,18 @@ int setup_veth_host_side(struct ds_config *cfg, pid_t child_pid); /* Gateway LAN lifecycle: bridge-only veth plumbing, no NAT/DHCP/firewall. */ int setup_gateway_veth_side(struct ds_config *cfg, pid_t child_pid); +/* Direct L2 lifecycle for ipvlan/macvlan modes. */ +int setup_parent_link_host_side(struct ds_config *cfg, pid_t child_pid); +int ds_host_access_setup(struct ds_config *cfg, pid_t child_pid); +void ds_host_access_refresh(struct ds_config *cfg, pid_t child_pid); +void ds_host_access_cleanup(struct ds_config *cfg, pid_t child_pid); +void ds_host_access_resolve_ptp(struct ds_config *cfg); +/* Resolve a blank ipvlan/macvlan parent to the host's active uplink. An + * explicitly configured parent is preserved. */ +int ds_net_resolve_parent(struct ds_config *cfg, char *reason, size_t reason_size); +int ds_parse_mac_address(const char *text, uint8_t mac[6]); +int setup_parent_link_child_side(struct ds_config *cfg, const char *peer_name); + int setup_veth_child_side_named(struct ds_config *cfg, const char *peer_name, const char *ip_str); /* Populate a ds_net_handshake from a container init PID + resolved config. @@ -747,6 +780,8 @@ int ds_nl_link_exists(ds_nl_ctx_t *ctx, const char *ifname); int ds_nl_get_ifindex(ds_nl_ctx_t *ctx, const char *ifname); int ds_nl_create_bridge(ds_nl_ctx_t *ctx, const char *name); int ds_nl_create_veth(ds_nl_ctx_t *ctx, const char *host, const char *peer); +int ds_nl_create_parent_link(ds_nl_ctx_t *ctx, const char *parent, + const char *name, const char *kind); int ds_nl_set_master(ds_nl_ctx_t *ctx, const char *ifname, const char *master); int ds_nl_link_up(ds_nl_ctx_t *ctx, const char *ifname); int ds_nl_link_down(ds_nl_ctx_t *ctx, const char *ifname); @@ -755,8 +790,14 @@ int ds_nl_rename(ds_nl_ctx_t *ctx, const char *ifname, const char *newname); int ds_nl_set_mac(ds_nl_ctx_t *ctx, const char *ifname, const uint8_t mac[6]); int ds_nl_add_addr4(ds_nl_ctx_t *ctx, const char *ifname, uint32_t ip_be, uint8_t prefix); +int ds_nl_del_addr4(ds_nl_ctx_t *ctx, const char *ifname, uint32_t ip_be, + uint8_t prefix); +int ds_nl_get_addr4(ds_nl_ctx_t *ctx, const char *ifname, uint32_t *ip_be, + uint8_t *prefix); int ds_nl_add_route4(ds_nl_ctx_t *ctx, uint32_t dst_be, uint8_t dst_len, uint32_t gw_be, int oif_idx); +int ds_nl_del_route4(ds_nl_ctx_t *ctx, uint32_t dst_be, uint8_t dst_len, + uint32_t gw_be, int oif_idx); int ds_nl_move_to_netns(ds_nl_ctx_t *ctx, const char *ifname, int netns_fd); int ds_nl_move_to_netns_named(ds_nl_ctx_t *ctx, const char *ifname, int netns_fd, const char *newname); @@ -777,6 +818,8 @@ int ds_nl_count_bridge_members_with_prefix(ds_nl_ctx_t *ctx, const char *bridge, int ds_nl_list_ifaces(ds_nl_ctx_t *ctx, char names[][IFNAMSIZ], int max); /* Kernel capability probe - call before any NAT setup */ int ds_nl_probe_nat_capability(char *reason, size_t rsz); +int ds_nl_probe_parent_capability(const char *parent, const char *kind, + char *reason, size_t rsz); /* --------------------------------------------------------------------------- * ds_iptables.c @@ -934,6 +977,7 @@ int is_dangerous_node(const char *name); int check_requirements(void); int check_requirements_hw(int hw_access); int check_requirements_detailed(void); +int check_requirements_format(void); /* --------------------------------------------------------------------------- * daemon.c - daemon, client, and probe entry points diff --git a/src/include/socketd_protocol.h b/src/include/socketd_protocol.h index 0a778a88..8ba7d3c5 100644 --- a/src/include/socketd_protocol.h +++ b/src/include/socketd_protocol.h @@ -111,7 +111,7 @@ struct DS_SOCKETD_PACKED ds_socketd_container_record { char nat_ip[INET_ADDRSTRLEN]; /* empty string if not NAT mode */ char custom_init[DS_SOCKETD_RECORD_PATH_MAX]; /* empty = /sbin/init */ int32_t pid_be; /* host-view PID 1; 0 = stopped */ - uint8_t net_mode; /* 0=host 1=nat 2=none */ + uint8_t net_mode; /* ds_net_mode wire value: host/nat/none/gateway/ipv/mac */ uint8_t port_count; /* entries used in ports[] */ uint8_t _pad[2]; struct ds_socketd_port_record ports[DS_SOCKETD_RECORD_PORTS_MAX]; @@ -171,7 +171,7 @@ struct DS_SOCKETD_PACKED ds_socketd_inspect_container_record_v1 { int64_t pids_limit_be; /* 0 = unlimited */ int32_t privileged_mask_be; - uint8_t net_mode; /* 0=host 1=nat 2=none */ + uint8_t net_mode; /* ds_net_mode wire value */ uint8_t foreground; uint8_t volatile_mode; uint8_t force_cgroupv1; diff --git a/src/main.c b/src/main.c index ed10dd98..433544bd 100644 --- a/src/main.c +++ b/src/main.c @@ -55,7 +55,14 @@ void print_usage(void) { printf( C_BOLD "Options (Networking):" C_RESET "\n" - " --net=MODE Modes: host (default), nat, none, gateway\n" + " --net=MODE Modes: host, nat, none, gateway, ipvlan, " + "macvlan\n" + " --net-parent=IFACE Parent link; blank config auto-detects uplink\n" + " --net-mac=MAC Optional macvlan MAC; blank leaves it unmanaged\n" + " --net-ipam=MODE Addressing: dhcp (default) or static\n" + " --host-access=MODE Host reachability: none, ptp, or shim\n" + " --net-address=CIDR Static IPv4 address, e.g. 192.168.1.50/24\n" + " --net-gateway=IP Static IPv4 default gateway\n" " --gateway=NAME Gateway container for --net=gateway\n" " --gateway-net=NAME Gateway LAN name/bridge suffix (default: " "lan)\n" @@ -244,6 +251,50 @@ static int validate_configuration_cli(struct ds_config *cfg) { } } + if (cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) { + /* A blank parent means auto-detect the active uplink at start time. */ + if (cfg->net_parent[0] && + (strlen(cfg->net_parent) >= IFNAMSIZ || + strchr(cfg->net_parent, '/') || + strpbrk(cfg->net_parent, " \t\r\n"))) { + ds_error("Invalid parent interface name: %s", cfg->net_parent); + errors++; + } + + if (cfg->net_ipam == DS_NET_IPAM_STATIC) { + char address[sizeof(cfg->net_address)]; + safe_strncpy(address, cfg->net_address, sizeof(address)); + char *slash = strchr(address, '/'); + char *end = NULL; + long prefix = slash ? strtol(slash + 1, &end, 10) : -1; + if (slash) + *slash = '\0'; + struct in_addr parsed; + if (!slash || !end || *end || prefix < 0 || prefix > 32 || + inet_pton(AF_INET, address, &parsed) != 1) { + ds_error("--net-address must be a valid IPv4 CIDR."); + errors++; + } + if (inet_pton(AF_INET, cfg->net_gateway, &parsed) != 1) { + ds_error("--net-gateway must be a valid IPv4 address."); + errors++; + } + } + if (cfg->net_mode == DS_NET_MACVLAN && cfg->net_mac[0]) { + uint8_t mac[6]; + if (ds_parse_mac_address(cfg->net_mac, mac) < 0) { + ds_error("--net-mac must be a unicast MAC such as 02:11:22:33:44:55."); + errors++; + } + } else if (cfg->net_mode == DS_NET_IPVLAN && cfg->net_mac[0]) { + ds_error("--net-mac is only valid with --net=macvlan."); + errors++; + } + } else if (cfg->host_access != DS_HOST_ACCESS_NONE) { + ds_error("--host-access is only valid with --net=ipvlan or macvlan."); + errors++; + } + return (errors > 0) ? -1 : 0; } @@ -308,12 +359,13 @@ static void enforce_nat_safety(struct ds_config *cfg, int argc, char **argv) { } if (cfg->net_mode == DS_NET_NAT || cfg->net_mode == DS_NET_NONE || - cfg->net_mode == DS_NET_GATEWAY) { + cfg->net_mode == DS_NET_GATEWAY || cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) { if (!check_ns(CLONE_NEWNET, "net")) { printf("\n" C_RED C_BOLD "[ FATAL: NETWORK NAMESPACE UNSUPPORTED ]" C_RESET "\n\n"); ds_error("Kernel does not support CLONE_NEWNET (network namespaces)."); - ds_log("Cannot use --net=nat, --net=none, or --net=gateway."); + ds_log("The selected isolated network mode cannot be used."); ds_log("Tip: Use --net=host (default) for shared host networking."); exit(EXIT_FAILURE); } @@ -346,6 +398,35 @@ static void enforce_nat_safety(struct ds_config *cfg, int argc, char **argv) { return; } + if (cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) { + char reason[512]; + const char *kind = + cfg->net_mode == DS_NET_IPVLAN ? "ipvlan" : "macvlan"; + if (ds_net_resolve_parent(cfg, reason, sizeof(reason)) < 0) { + printf("\n" C_RED C_BOLD + "[ FATAL: DIRECT L2 PARENT UNAVAILABLE ]" C_RESET "\n\n"); + ds_error("--net=%s could not select a parent interface:\n %s", kind, + reason); + ds_log("Connect a host network or set --net-parent explicitly."); + exit(1); + } + ds_log("[NET] %s", reason); + int probe = ds_nl_probe_parent_capability(cfg->net_parent, kind, reason, + sizeof(reason)); + if (probe < 0) { + printf("\n" C_RED C_BOLD + "[ FATAL: DIRECT L2 NETWORKING UNSUPPORTED ]" C_RESET "\n\n"); + ds_error("--net=%s is unavailable:\n %s", kind, reason); + ds_log("Check that CONFIG_%s=y and that parent '%s' supports this link " + "type.", + cfg->net_mode == DS_NET_IPVLAN ? "IPVLAN" : "MACVLAN", + cfg->net_parent); + exit(1); + } + ds_log("[NET] Kernel capability probe passed: %s", reason); + return; + } + if (cfg->net_mode != DS_NET_NAT) return; @@ -420,6 +501,12 @@ int main(int argc, char **argv) { {"gateway-net", required_argument, 0, 275}, {"gateway-iface", required_argument, 0, 276}, {"gateway-bridge", required_argument, 0, 277}, + {"net-parent", required_argument, 0, 280}, + {"net-ipam", required_argument, 0, 281}, + {"net-address", required_argument, 0, 282}, + {"net-gateway", required_argument, 0, 283}, + {"net-mac", required_argument, 0, 284}, + {"host-access", required_argument, 0, 285}, {"reset", no_argument, 0, 256}, {"format", no_argument, 0, 265}, {"memory", required_argument, 0, 266}, @@ -495,6 +582,10 @@ int main(int argc, char **argv) { else if (strcmp(optarg, "gateway") == 0 || strcmp(optarg, "delegated-gateway") == 0) cfg.net_mode = DS_NET_GATEWAY; + else if (strcmp(optarg, "ipvlan") == 0) + cfg.net_mode = DS_NET_IPVLAN; + else if (strcmp(optarg, "macvlan") == 0) + cfg.net_mode = DS_NET_MACVLAN; else { ds_error("Unknown network mode: '%s'. Valid options: host, nat, none, " "gateway", @@ -693,6 +784,45 @@ int main(int argc, char **argv) { case 277: safe_strncpy(cfg.gateway_bridge, optarg, sizeof(cfg.gateway_bridge)); break; + case 280: + safe_strncpy(cfg.net_parent, optarg, sizeof(cfg.net_parent)); + break; + case 281: + if (strcmp(optarg, "dhcp") == 0) + cfg.net_ipam = DS_NET_IPAM_DHCP; + else if (strcmp(optarg, "static") == 0) + cfg.net_ipam = DS_NET_IPAM_STATIC; + else { + ds_error("Unknown --net-ipam value '%s' (expected dhcp or static)", + optarg); + ret = 1; + goto cleanup; + } + break; + case 282: + safe_strncpy(cfg.net_address, optarg, sizeof(cfg.net_address)); + break; + case 283: + safe_strncpy(cfg.net_gateway, optarg, sizeof(cfg.net_gateway)); + break; + case 284: + safe_strncpy(cfg.net_mac, optarg, sizeof(cfg.net_mac)); + break; + case 285: + if (strcmp(optarg, "none") == 0) + cfg.host_access = DS_HOST_ACCESS_NONE; + else if (strcmp(optarg, "ptp") == 0) + cfg.host_access = DS_HOST_ACCESS_PTP; + else if (strcmp(optarg, "shim") == 0) + cfg.host_access = DS_HOST_ACCESS_SHIM; + else { + ds_error("Unknown --host-access value '%s' (expected none, ptp, or " + "shim)", + optarg); + ret = 1; + goto cleanup; + } + break; case 'I': cfg.disable_ipv6 = 1; break; @@ -766,9 +896,13 @@ int main(int argc, char **argv) { else if (strcmp(optarg, "gateway") == 0 || strcmp(optarg, "delegated-gateway") == 0) cli_net_mode = DS_NET_GATEWAY; + else if (strcmp(optarg, "ipvlan") == 0) + cli_net_mode = DS_NET_IPVLAN; + else if (strcmp(optarg, "macvlan") == 0) + cli_net_mode = DS_NET_MACVLAN; else { ds_error("Unknown network mode: '%s'. Valid options: host, nat, none, " - "gateway", + "gateway, ipvlan, macvlan", optarg); ret = 1; goto cleanup; @@ -1086,7 +1220,8 @@ int main(int argc, char **argv) { /* Basic info commands */ if (strcmp(cmd, "check") == 0) { - ret = check_requirements_detailed(); + ret = cfg.format_output ? check_requirements_format() + : check_requirements_detailed(); goto cleanup; } if (strcmp(cmd, "version") == 0) { diff --git a/src/monitor.c b/src/monitor.c index 4bffd9c3..ffa6dc24 100644 --- a/src/monitor.c +++ b/src/monitor.c @@ -456,6 +456,7 @@ reboot_loop:; } close(cfg->net_ready_pipe[0]); + int net_setup_status = 0; if (cfg->net_mode == DS_NET_NAT) { if (setup_veth_host_side(cfg, netns_pid) < 0) { ds_warn("[NET] Monitor: setup_veth_host_side failed - " @@ -470,6 +471,18 @@ reboot_loop:; ds_warn("[NET] Monitor: setup_gateway_veth_side failed - " "container will remain isolated"); } + } else if (cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) { + net_setup_status = setup_parent_link_host_side(cfg, netns_pid); + if (net_setup_status < 0) + ds_warn("[NET] Monitor: direct L2 link setup failed: %s", + strerror(-net_setup_status)); + else if (cfg->host_access != DS_HOST_ACCESS_NONE) { + int ha_ret = ds_host_access_setup(cfg, netns_pid); + if (ha_ret < 0) + ds_warn("[NET] Monitor: host access setup deferred: %s", + strerror(-ha_ret)); + } } /* Gateway self-heal: if any running client delegates to THIS container as @@ -491,6 +504,7 @@ reboot_loop:; /* Send handshake to init */ struct ds_net_handshake hs; ds_net_derive_handshake(netns_pid, cfg, &hs); + hs.status = net_setup_status; if (cfg->net_mode == DS_NET_GATEWAY) ds_log("[NET] Monitor: sending DONE (gateway mode: eth0 is wired " "host-side, IP comes from the gateway's DHCP)"); @@ -546,6 +560,7 @@ reboot_loop:; sigaddset(&mask, SIGCHLD); sigprocmask(SIG_BLOCK, &mask, NULL); int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); + unsigned int host_access_ticks = 0; while (1) { pid_t r = waitpid(mid_pid, &status, WNOHANG); @@ -570,6 +585,13 @@ reboot_loop:; } ds_virtualize_update(cfg); + if ((cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) && + cfg->host_access != DS_HOST_ACCESS_NONE && + ++host_access_ticks >= 10) { + ds_host_access_refresh(cfg, cfg->container_pid); + host_access_ticks = 0; + } /* Poll the signalfd and, in background mode, the console PTY master. * poll() wakes immediately when the master becomes readable, so draining diff --git a/src/net/host_access.c b/src/net/host_access.c new file mode 100644 index 00000000..4483f5d2 --- /dev/null +++ b/src/net/host_access.c @@ -0,0 +1,699 @@ +/* + * Direct-L2 host access for ipvlan/macvlan containers. + * + * PTP gives every container a private veth /30 and routes the host/container + * primary IPv4 addresses over it. SHIM creates one shared ipvlan/macvlan + * child per parent+kind, borrows the parent's IPv4 as /32, and installs a host + * route for every live container address. + */ +#include "droidspace.h" +#include +#include +#include +#include + +#define DS_HA_PTP_POOL "169.254.240.0" +#define DS_HA_PTP_POOL_PREFIX 20 +#define DS_HA_PTP_LINK_PREFIX 30 +#define DS_HA_RULE_PRIORITY 6080 +#define DS_HA_GUEST_IF "dshost0" + +struct ha_state { + char mode[8]; + char shim[IFNAMSIZ]; + char parent[IFNAMSIZ]; + char guest_ip[INET_ADDRSTRLEN]; + char host_ip[INET_ADDRSTRLEN]; + pid_t pid; +}; + +static uint32_t ha_hash(const char *s) { + uint32_t h = 5381; + if (!s) + return h; + while (*s) + h = ((h << 5) + h) ^ (unsigned char)*s++; + return h; +} + +static const char *ha_key(const struct ds_config *cfg) { + return cfg->uuid[0] ? cfg->uuid : cfg->container_name; +} + +static void ha_ptp_names(const struct ds_config *cfg, char host[IFNAMSIZ], + char peer[IFNAMSIZ]) { + uint32_t h = ha_hash(ha_key(cfg)); + snprintf(host, IFNAMSIZ, "ds-pt%08x", h); + snprintf(peer, IFNAMSIZ, "ds-pg%08x", h); +} + +static void ha_shim_name(const struct ds_config *cfg, char shim[IFNAMSIZ]) { + char key[64]; + snprintf(key, sizeof(key), "%s:%s", + cfg->net_mode == DS_NET_IPVLAN ? "ipvlan" : "macvlan", + cfg->net_parent); + snprintf(shim, IFNAMSIZ, "ds-sh%08x", ha_hash(key)); +} + +static void ha_state_path(const struct ds_config *cfg, char *path, size_t size) { + char safe[256]; + sanitize_container_name(ha_key(cfg), safe, sizeof(safe)); + snprintf(path, size, "%.3800s/ha_%.200s.state", get_net_dir(), safe); +} + +static void ha_stop_path(const struct ds_config *cfg, char *path, size_t size) { + char safe[256]; + sanitize_container_name(ha_key(cfg), safe, sizeof(safe)); + snprintf(path, size, "%.3800s/ha_%.200s.stop", get_net_dir(), safe); +} + +static int ha_is_stopping(const struct ds_config *cfg) { + char path[PATH_MAX]; + ha_stop_path(cfg, path, sizeof(path)); + return access(path, F_OK) == 0; +} + +static int ha_lock(const char *key) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%.3800s/ha_%.32s.lock", get_net_dir(), key); + int fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC, 0600); + if (fd < 0) + return -1; + if (flock(fd, LOCK_EX) < 0) { + close(fd); + return -1; + } + return fd; +} + +static void ha_unlock(int fd) { + if (fd >= 0) { + (void)flock(fd, LOCK_UN); + close(fd); + } +} + +static void ha_write_state(const struct ds_config *cfg, + const struct ha_state *state) { + char path[PATH_MAX]; + char content[512]; + ha_state_path(cfg, path, sizeof(path)); + snprintf(content, sizeof(content), + "mode=%s\npid=%d\nshim=%s\nparent=%s\nguest_ip=%s\nhost_ip=%s\n", + state->mode, (int)state->pid, state->shim, state->parent, + state->guest_ip, state->host_ip); + if (write_file_atomic(path, content) < 0) + ds_warn("[NET] Host access: failed to save runtime state: %s", + strerror(errno)); +} + +static int ha_read_state_path(const char *path, struct ha_state *state) { + FILE *f = fopen(path, "re"); + if (!f) + return -1; + memset(state, 0, sizeof(*state)); + char line[128]; + while (fgets(line, sizeof(line), f)) { + char *nl = strpbrk(line, "\r\n"); + if (nl) + *nl = '\0'; + char *eq = strchr(line, '='); + if (!eq) + continue; + *eq++ = '\0'; + if (strcmp(line, "mode") == 0) + safe_strncpy(state->mode, eq, sizeof(state->mode)); + else if (strcmp(line, "pid") == 0) + state->pid = (pid_t)strtol(eq, NULL, 10); + else if (strcmp(line, "shim") == 0) + safe_strncpy(state->shim, eq, sizeof(state->shim)); + else if (strcmp(line, "parent") == 0) + safe_strncpy(state->parent, eq, sizeof(state->parent)); + else if (strcmp(line, "guest_ip") == 0) + safe_strncpy(state->guest_ip, eq, sizeof(state->guest_ip)); + else if (strcmp(line, "host_ip") == 0) + safe_strncpy(state->host_ip, eq, sizeof(state->host_ip)); + } + fclose(f); + return 0; +} + +static int ha_read_state(const struct ds_config *cfg, struct ha_state *state) { + char path[PATH_MAX]; + ha_state_path(cfg, path, sizeof(path)); + return ha_read_state_path(path, state); +} + +static int ha_state_equal(const struct ha_state *a, + const struct ha_state *b) { + return strcmp(a->mode, b->mode) == 0 && strcmp(a->shim, b->shim) == 0 && + strcmp(a->parent, b->parent) == 0 && + strcmp(a->guest_ip, b->guest_ip) == 0 && + strcmp(a->host_ip, b->host_ip) == 0 && a->pid == b->pid; +} + +static int ha_write_disable_ipv6(const char *ifname) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/proc/sys/net/ipv6/conf/%s/disable_ipv6", + ifname); + int fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) + return errno == ENOENT ? 0 : -errno; + ssize_t n = write(fd, "1\n", 2); + int saved = errno; + close(fd); + return n == 2 ? 0 : -saved; +} + +static int ha_parse_ptp_cidr(const char *cidr, uint32_t *network_be) { + if (!cidr || !cidr[0]) + return -EINVAL; + char copy[32]; + safe_strncpy(copy, cidr, sizeof(copy)); + char *slash = strchr(copy, '/'); + if (!slash || strcmp(slash, "/30") != 0) + return -EINVAL; + *slash = '\0'; + struct in_addr addr; + struct in_addr pool; + if (inet_pton(AF_INET, copy, &addr) != 1 || + inet_pton(AF_INET, DS_HA_PTP_POOL, &pool) != 1) + return -EINVAL; + uint32_t host = ntohl(addr.s_addr); + uint32_t pool_host = ntohl(pool.s_addr); + uint32_t pool_mask = 0xffffffffu << (32 - DS_HA_PTP_POOL_PREFIX); + if ((host & pool_mask) != (pool_host & pool_mask) || (host & 3u) != 0) + return -EINVAL; + if (network_be) + *network_be = addr.s_addr; + return 0; +} + +static int ha_ptp_collision(const char *cidr, const char *exclude_name) { + char dir_path[PATH_MAX]; + snprintf(dir_path, sizeof(dir_path), "%s/Containers", get_workspace_dir()); + DIR *dir = opendir(dir_path); + if (!dir) + return 0; + char safe_exclude[256] = {0}; + if (exclude_name && exclude_name[0]) + sanitize_container_name(exclude_name, safe_exclude, sizeof(safe_exclude)); + int collision = 0; + struct dirent *ent; + while ((ent = readdir(dir)) != NULL && !collision) { + if (ent->d_name[0] == '.' || + (safe_exclude[0] && strcmp(ent->d_name, safe_exclude) == 0)) + continue; + char config_path[PATH_MAX + NAME_MAX + 32]; + snprintf(config_path, sizeof(config_path), "%s/%s/container.config", + dir_path, ent->d_name); + struct ds_config other = {0}; + other.net_ready_pipe[0] = other.net_ready_pipe[1] = -1; + other.net_done_pipe[0] = other.net_done_pipe[1] = -1; + if (ds_config_load(config_path, &other) == 0) { + if (other.host_access == DS_HOST_ACCESS_PTP && + strcmp(other.host_access_ptp_cidr, cidr) == 0) + collision = 1; + ds_config_free(&other); + } + } + closedir(dir); + return collision; +} + +void ds_host_access_resolve_ptp(struct ds_config *cfg) { + if (!cfg || cfg->host_access != DS_HOST_ACCESS_PTP) + return; + int lock = ha_lock("ptp-pool"); + if (ha_parse_ptp_cidr(cfg->host_access_ptp_cidr, NULL) == 0 && + !ha_ptp_collision(cfg->host_access_ptp_cidr, cfg->container_name)) { + ha_unlock(lock); + return; + } + + struct in_addr pool; + if (inet_pton(AF_INET, DS_HA_PTP_POOL, &pool) != 1) { + ha_unlock(lock); + return; + } + uint32_t base = ntohl(pool.s_addr); + unsigned int slots = 1u << (DS_HA_PTP_LINK_PREFIX - DS_HA_PTP_POOL_PREFIX); + unsigned int first = ha_hash(ha_key(cfg)) % slots; + for (unsigned int n = 0; n < slots; n++) { + struct in_addr candidate = {.s_addr = htonl(base + ((first + n) % slots) * 4u)}; + char ip[INET_ADDRSTRLEN]; + char cidr[32]; + if (!inet_ntop(AF_INET, &candidate, ip, sizeof(ip))) + continue; + snprintf(cidr, sizeof(cidr), "%s/30", ip); + if (!ha_ptp_collision(cidr, cfg->container_name)) { + safe_strncpy(cfg->host_access_ptp_cidr, cidr, + sizeof(cfg->host_access_ptp_cidr)); + ds_log("[NET] Host access PTP: reserved %s for %s", cidr, + cfg->container_name); + ha_unlock(lock); + return; + } + } + cfg->host_access_ptp_cidr[0] = '\0'; + ds_warn("[NET] Host access PTP: address pool is exhausted"); + ha_unlock(lock); +} + +static int ha_configure_netns_ptp(pid_t pid, uint32_t guest_be, + uint32_t host_be, uint32_t old_host_be, + uint32_t gateway_be) { + int self_fd = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + if (self_fd < 0) + return -errno; + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/ns/net", (int)pid); + int target_fd = open(path, O_RDONLY | O_CLOEXEC); + if (target_fd < 0) { + int ret = -errno; + close(self_fd); + return ret; + } + int ret = 0; + if (setns(target_fd, CLONE_NEWNET) < 0) { + ret = -errno; + goto restore; + } + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) { + ret = -errno; + goto restore; + } + ret = ds_nl_add_addr4(ctx, DS_HA_GUEST_IF, guest_be, + DS_HA_PTP_LINK_PREFIX); + if (ret == 0) + ret = ds_nl_link_up(ctx, DS_HA_GUEST_IF); + if (ret == 0) + (void)ha_write_disable_ipv6(DS_HA_GUEST_IF); + int guest_idx = ds_nl_get_ifindex(ctx, DS_HA_GUEST_IF); + if (ret == 0 && guest_idx <= 0) + ret = -ENODEV; + if (ret == 0 && old_host_be && old_host_be != host_be) + (void)ds_nl_del_route4(ctx, old_host_be, 32, gateway_be, guest_idx); + if (ret == 0 && host_be) + ret = ds_nl_add_route4(ctx, host_be, 32, gateway_be, guest_idx); + ds_nl_close(ctx); +restore: + if (setns(self_fd, CLONE_NEWNET) < 0 && ret == 0) + ret = -errno; + close(target_fd); + close(self_fd); + return ret; +} + +static int ha_get_guest_ip(pid_t pid, uint32_t *ip_be) { + int self_fd = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + if (self_fd < 0) + return -errno; + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/ns/net", (int)pid); + int target_fd = open(path, O_RDONLY | O_CLOEXEC); + if (target_fd < 0) { + int ret = -errno; + close(self_fd); + return ret; + } + int ret = 0; + if (setns(target_fd, CLONE_NEWNET) < 0) { + ret = -errno; + goto restore; + } + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) + ret = -errno; + else { + ret = ds_nl_get_addr4(ctx, "eth0", ip_be, NULL); + ds_nl_close(ctx); + } +restore: + if (setns(self_fd, CLONE_NEWNET) < 0 && ret == 0) + ret = -errno; + close(target_fd); + close(self_fd); + return ret; +} + +static int ha_setup_ptp(struct ds_config *cfg, pid_t pid, + int discover_guest) { + uint32_t network_be; + if (ha_parse_ptp_cidr(cfg->host_access_ptp_cidr, &network_be) < 0) + return -EINVAL; + uint32_t network = ntohl(network_be); + uint32_t host_be = htonl(network + 1u); + uint32_t guest_be = htonl(network + 2u); + char host[IFNAMSIZ], peer[IFNAMSIZ]; + ha_ptp_names(cfg, host, peer); + int lock = ha_lock(host); + if (ha_is_stopping(cfg)) { + ha_unlock(lock); + return -ECANCELED; + } + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) { + ha_unlock(lock); + return -errno; + } + + struct ha_state old = {0}; + int have_old = ha_read_state(cfg, &old) == 0; + int created = 0; + int ret = 0; + if (!ds_nl_link_exists(ctx, host)) { + ret = ds_nl_create_veth(ctx, host, peer); + if (ret < 0 && ret != -EEXIST) + goto out; + created = 1; + char ns_path[64]; + snprintf(ns_path, sizeof(ns_path), "/proc/%d/ns/net", (int)pid); + int ns_fd = open(ns_path, O_RDONLY | O_CLOEXEC); + if (ns_fd < 0) { + ret = -errno; + goto out; + } + ret = ds_nl_move_to_netns_named(ctx, peer, ns_fd, DS_HA_GUEST_IF); + close(ns_fd); + if (ret < 0) + goto out; + } + ret = ds_nl_add_addr4(ctx, host, host_be, DS_HA_PTP_LINK_PREFIX); + if (ret == 0) + ret = ds_nl_link_up(ctx, host); + if (ret == 0) + (void)ha_write_disable_ipv6(host); + if (ret == 0) + ret = ds_nl_add_rule4(ctx, 0, 0, inet_addr(DS_HA_PTP_POOL), + DS_HA_PTP_POOL_PREFIX, RT_TABLE_MAIN, + DS_HA_RULE_PRIORITY); + + uint32_t host_main_be = 0; + (void)ds_nl_get_addr4(ctx, cfg->net_parent, &host_main_be, NULL); + uint32_t old_host_main_be = 0; + if (have_old && old.host_ip[0]) + (void)inet_pton(AF_INET, old.host_ip, &old_host_main_be); + if (ret == 0) + ret = ha_configure_netns_ptp(pid, guest_be, host_main_be, + old_host_main_be, host_be); + + uint32_t guest_main_be = 0; + if (cfg->net_ipam == DS_NET_IPAM_STATIC && cfg->net_address[0]) { + char cidr[sizeof(cfg->net_address)]; + safe_strncpy(cidr, cfg->net_address, sizeof(cidr)); + char *slash = strchr(cidr, '/'); + if (slash) + *slash = '\0'; + (void)inet_pton(AF_INET, cidr, &guest_main_be); + } else if (discover_guest) { + (void)ha_get_guest_ip(pid, &guest_main_be); + } + + int host_idx = ds_nl_get_ifindex(ctx, host); + if (ret == 0 && host_idx <= 0) + ret = -ENODEV; + char host_ip[INET_ADDRSTRLEN] = {0}; + char guest_ip[INET_ADDRSTRLEN] = {0}; + if (host_main_be) { + struct in_addr addr = {.s_addr = host_main_be}; + (void)inet_ntop(AF_INET, &addr, host_ip, sizeof(host_ip)); + } + if (guest_main_be) { + struct in_addr addr = {.s_addr = guest_main_be}; + (void)inet_ntop(AF_INET, &addr, guest_ip, sizeof(guest_ip)); + } + if (ret == 0 && have_old && old.guest_ip[0] && + strcmp(old.guest_ip, guest_ip) != 0) { + struct in_addr old_guest; + if (inet_pton(AF_INET, old.guest_ip, &old_guest) == 1) { + (void)ds_nl_del_route4(ctx, old_guest.s_addr, 32, guest_be, host_idx); + (void)ds_nl_del_rule4(ctx, 0, 0, old_guest.s_addr, 32, + RT_TABLE_MAIN, DS_HA_RULE_PRIORITY); + } + } + if (ret == 0 && guest_main_be) { + ret = ds_nl_add_route4(ctx, guest_main_be, 32, guest_be, host_idx); + if (ret == 0) + ret = ds_nl_add_rule4(ctx, 0, 0, guest_main_be, 32, RT_TABLE_MAIN, + DS_HA_RULE_PRIORITY); + } + if (ret == 0) { + struct ha_state state = {0}; + safe_strncpy(state.mode, "ptp", sizeof(state.mode)); + safe_strncpy(state.shim, host, sizeof(state.shim)); + safe_strncpy(state.parent, cfg->net_parent, sizeof(state.parent)); + safe_strncpy(state.host_ip, host_ip, sizeof(state.host_ip)); + safe_strncpy(state.guest_ip, guest_ip, sizeof(state.guest_ip)); + state.pid = pid; + if (!have_old || !ha_state_equal(&old, &state)) + ha_write_state(cfg, &state); + if (!have_old || created || strcmp(old.guest_ip, guest_ip) != 0) + ds_log("[NET] Host access PTP ready: host=%s guest=%s (%s), " + "primary=%s<->%s", + host, DS_HA_GUEST_IF, cfg->host_access_ptp_cidr, + host_ip[0] ? host_ip : "unavailable", + guest_ip[0] ? guest_ip : "waiting-for-DHCP"); + } +out: + if (ret < 0) + ds_nl_del_link(ctx, host); + ds_nl_close(ctx); + ha_unlock(lock); + return ret; +} + +static int ha_setup_shim(struct ds_config *cfg, pid_t pid, int discover_guest) { + char shim[IFNAMSIZ]; + ha_shim_name(cfg, shim); + int lock = ha_lock(shim); + if (ha_is_stopping(cfg)) { + ha_unlock(lock); + return -ECANCELED; + } + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) { + ha_unlock(lock); + return -errno; + } + + uint32_t host_be = 0; + int ret = ds_nl_get_addr4(ctx, cfg->net_parent, &host_be, NULL); + if (ret < 0) { + ds_nl_close(ctx); + ha_unlock(lock); + return ret; + } + + struct ha_state old = {0}; + int have_old = ha_read_state(cfg, &old) == 0; + if (!ds_nl_link_exists(ctx, shim)) { + const char *kind = + cfg->net_mode == DS_NET_IPVLAN ? "ipvlan" : "macvlan"; + ret = ds_nl_create_parent_link(ctx, cfg->net_parent, shim, kind); + if (ret < 0 && ret != -EEXIST) + goto out; + } + int shim_idx = ds_nl_get_ifindex(ctx, shim); + if (shim_idx <= 0) { + ret = -ENODEV; + goto out; + } + ret = ds_nl_link_up(ctx, shim); + if (ret < 0) + goto out; + (void)ha_write_disable_ipv6(shim); + + char host_ip[INET_ADDRSTRLEN] = {0}; + struct in_addr host_addr = {.s_addr = host_be}; + if (!inet_ntop(AF_INET, &host_addr, host_ip, sizeof(host_ip))) { + ret = -errno; + goto out; + } + if (have_old && old.host_ip[0] && strcmp(old.host_ip, host_ip) != 0) { + struct in_addr old_host; + if (inet_pton(AF_INET, old.host_ip, &old_host) == 1) + (void)ds_nl_del_addr4(ctx, shim, old_host.s_addr, 32); + } + ret = ds_nl_add_addr4(ctx, shim, host_be, 32); + if (ret < 0) + goto out; + + uint32_t guest_be = 0; + if (cfg->net_ipam == DS_NET_IPAM_STATIC && cfg->net_address[0]) { + char cidr[sizeof(cfg->net_address)]; + safe_strncpy(cidr, cfg->net_address, sizeof(cidr)); + char *slash = strchr(cidr, '/'); + if (slash) + *slash = '\0'; + (void)inet_pton(AF_INET, cidr, &guest_be); + } else if (discover_guest) { + (void)ha_get_guest_ip(pid, &guest_be); + } + + char guest_ip[INET_ADDRSTRLEN] = {0}; + if (guest_be) { + struct in_addr guest_addr = {.s_addr = guest_be}; + (void)inet_ntop(AF_INET, &guest_addr, guest_ip, sizeof(guest_ip)); + } + if (have_old && old.guest_ip[0] && strcmp(old.guest_ip, guest_ip) != 0) { + struct in_addr old_guest; + if (inet_pton(AF_INET, old.guest_ip, &old_guest) == 1) { + (void)ds_nl_del_route4(ctx, old_guest.s_addr, 32, 0, shim_idx); + (void)ds_nl_del_rule4(ctx, 0, 0, old_guest.s_addr, 32, RT_TABLE_MAIN, + DS_HA_RULE_PRIORITY); + } + } + if (guest_be) { + ret = ds_nl_add_route4(ctx, guest_be, 32, 0, shim_idx); + if (ret == 0) + ret = ds_nl_add_rule4(ctx, 0, 0, guest_be, 32, RT_TABLE_MAIN, + DS_HA_RULE_PRIORITY); + if (ret < 0) + goto out; + } + + struct ha_state state = {0}; + safe_strncpy(state.mode, "shim", sizeof(state.mode)); + safe_strncpy(state.shim, shim, sizeof(state.shim)); + safe_strncpy(state.parent, cfg->net_parent, sizeof(state.parent)); + safe_strncpy(state.host_ip, host_ip, sizeof(state.host_ip)); + safe_strncpy(state.guest_ip, guest_ip, sizeof(state.guest_ip)); + state.pid = pid; + if (!have_old || !ha_state_equal(&old, &state)) + ha_write_state(cfg, &state); + if (!have_old || strcmp(old.guest_ip, guest_ip) != 0) + ds_log("[NET] Host access shim ready: %s host=%s/32 guest=%s", + shim, host_ip, guest_ip[0] ? guest_ip : "waiting-for-DHCP"); +out: + ds_nl_close(ctx); + ha_unlock(lock); + return ret; +} + +int ds_host_access_setup(struct ds_config *cfg, pid_t child_pid) { + if (!cfg || child_pid <= 0 || cfg->host_access == DS_HOST_ACCESS_NONE) + return 0; + char stop_path[PATH_MAX]; + ha_stop_path(cfg, stop_path, sizeof(stop_path)); + unlink(stop_path); + if (cfg->host_access == DS_HOST_ACCESS_PTP) + return ha_setup_ptp(cfg, child_pid, 0); + if (cfg->host_access == DS_HOST_ACCESS_SHIM) + return ha_setup_shim(cfg, child_pid, 0); + return -EINVAL; +} + +void ds_host_access_refresh(struct ds_config *cfg, pid_t child_pid) { + if (!cfg || child_pid <= 0) + return; + if (ha_is_stopping(cfg)) + return; + if (cfg->host_access == DS_HOST_ACCESS_PTP) + (void)ha_setup_ptp(cfg, child_pid, 1); + else if (cfg->host_access == DS_HOST_ACCESS_SHIM) + (void)ha_setup_shim(cfg, child_pid, 1); +} + +static int ha_other_live_shim_users(const char *shim, + const char *skip_path) { + DIR *dir = opendir(get_net_dir()); + if (!dir) + return 0; + int count = 0; + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (strncmp(ent->d_name, "ha_", 3) != 0 || + !strstr(ent->d_name, ".state")) + continue; + char path[PATH_MAX + NAME_MAX + 2]; + snprintf(path, sizeof(path), "%s/%s", get_net_dir(), ent->d_name); + if (skip_path && strcmp(path, skip_path) == 0) + continue; + struct ha_state state; + if (ha_read_state_path(path, &state) == 0 && + strcmp(state.mode, "shim") == 0 && strcmp(state.shim, shim) == 0 && + state.pid > 0 && (kill(state.pid, 0) == 0 || errno == EPERM)) { + count++; + break; + } + } + closedir(dir); + return count; +} + +void ds_host_access_cleanup(struct ds_config *cfg, pid_t child_pid) { + (void)child_pid; + if (!cfg || cfg->host_access == DS_HOST_ACCESS_NONE) + return; + /* The CLI can begin cleanup while the monitor is completing its final + * heartbeat. Leave a tombstone before taking any segment lock, and check it + * again inside setup after acquiring that lock. The next genuine start + * removes the marker in ds_host_access_setup(). */ + char stop_path[PATH_MAX]; + ha_stop_path(cfg, stop_path, sizeof(stop_path)); + if (write_file_atomic(stop_path, "1\n") < 0) + ds_warn("[NET] Host access: failed to write stop marker: %s", + strerror(errno)); + char path[PATH_MAX]; + ha_state_path(cfg, path, sizeof(path)); + struct ha_state state = {0}; + (void)ha_read_state_path(path, &state); + + if (cfg->host_access == DS_HOST_ACCESS_PTP) { + char host[IFNAMSIZ], peer[IFNAMSIZ]; + ha_ptp_names(cfg, host, peer); + int lock = ha_lock(host); + ds_nl_ctx_t *ctx = ds_nl_open(); + if (ctx) { + if (state.guest_ip[0]) { + struct in_addr guest; + int idx = ds_nl_get_ifindex(ctx, host); + if (inet_pton(AF_INET, state.guest_ip, &guest) == 1) { + if (idx > 0) + (void)ds_nl_del_route4(ctx, guest.s_addr, 32, 0, idx); + (void)ds_nl_del_rule4(ctx, 0, 0, guest.s_addr, 32, + RT_TABLE_MAIN, DS_HA_RULE_PRIORITY); + } + } + ds_nl_del_link(ctx, host); + if (ds_nl_count_ifaces_with_prefix(ctx, "ds-pt") == 0) + (void)ds_nl_del_rule4(ctx, 0, 0, inet_addr(DS_HA_PTP_POOL), + DS_HA_PTP_POOL_PREFIX, RT_TABLE_MAIN, + DS_HA_RULE_PRIORITY); + ds_nl_close(ctx); + } + unlink(path); + ha_unlock(lock); + return; + } + + if (cfg->host_access == DS_HOST_ACCESS_SHIM) { + char shim[IFNAMSIZ]; + if (state.shim[0]) + safe_strncpy(shim, state.shim, sizeof(shim)); + else + ha_shim_name(cfg, shim); + int lock = ha_lock(shim); + ds_nl_ctx_t *ctx = ds_nl_open(); + if (ctx && state.guest_ip[0]) { + struct in_addr guest; + int idx = ds_nl_get_ifindex(ctx, shim); + if (inet_pton(AF_INET, state.guest_ip, &guest) == 1) { + if (idx > 0) + (void)ds_nl_del_route4(ctx, guest.s_addr, 32, 0, idx); + (void)ds_nl_del_rule4(ctx, 0, 0, guest.s_addr, 32, RT_TABLE_MAIN, + DS_HA_RULE_PRIORITY); + } + } + unlink(path); + if (ctx && !ha_other_live_shim_users(shim, path)) { + ds_nl_del_link(ctx, shim); + ds_log("[NET] Host access shim cleanup: removed idle %s", shim); + } + if (ctx) + ds_nl_close(ctx); + ha_unlock(lock); + } +} diff --git a/src/net/netlink.c b/src/net/netlink.c index 0a2af65d..74c86bf5 100644 --- a/src/net/netlink.c +++ b/src/net/netlink.c @@ -408,6 +408,89 @@ int ds_nl_create_veth(ds_nl_ctx_t *ctx, const char *host, const char *peer) { return ds_nl_talk(ctx, &req.n); } +/* Create an ipvlan L2 or macvlan bridge link attached to a real host link. + * The link is deliberately created in the host namespace first so Android's + * parent ifindex can be referenced, then the caller moves it into the guest. */ +int ds_nl_create_parent_link(ds_nl_ctx_t *ctx, const char *parent, + const char *name, const char *kind) { + int parent_idx = ds_nl_get_ifindex(ctx, parent); + if (parent_idx <= 0) + return -ENODEV; + if (strcmp(kind, "ipvlan") != 0 && strcmp(kind, "macvlan") != 0) + return -EINVAL; + + struct { + struct nlmsghdr n; + struct ifinfomsg i; + char buf[1024]; + } req; + memset(&req, 0, sizeof(req)); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + req.n.nlmsg_type = RTM_NEWLINK; + req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK; + req.i.ifi_family = AF_UNSPEC; + + nl_addattr(&req.n, (int)sizeof(req), IFLA_LINK, &parent_idx, + (int)sizeof(parent_idx)); + nl_addattr(&req.n, (int)sizeof(req), IFLA_IFNAME, name, + (int)strlen(name) + 1); + + struct rtattr *linfo = nl_nest_begin(&req.n, (int)sizeof(req), IFLA_LINKINFO); + nl_addattr(&req.n, (int)sizeof(req), IFLA_INFO_KIND, kind, + (int)strlen(kind) + 1); + struct rtattr *ldata = + nl_nest_begin(&req.n, (int)sizeof(req), IFLA_INFO_DATA); + if (strcmp(kind, "ipvlan") == 0) { + uint16_t mode = IPVLAN_MODE_L2; + nl_addattr(&req.n, (int)sizeof(req), IFLA_IPVLAN_MODE, &mode, + (int)sizeof(mode)); + } else { + uint32_t mode = MACVLAN_MODE_BRIDGE; + nl_addattr(&req.n, (int)sizeof(req), IFLA_MACVLAN_MODE, &mode, + (int)sizeof(mode)); + } + nl_nest_end(&req.n, ldata); + nl_nest_end(&req.n, linfo); + return ds_nl_talk(ctx, &req.n); +} + +int ds_nl_probe_parent_capability(const char *parent, const char *kind, + char *reason, size_t rsz) { + if (!parent || !parent[0] || !kind) { + snprintf(reason, rsz, "A parent interface is required."); + return -EINVAL; + } + + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) { + snprintf(reason, rsz, "Failed to open NETLINK_ROUTE socket: %s", + strerror(errno)); + return -errno; + } + if (ds_nl_get_ifindex(ctx, parent) <= 0) { + snprintf(reason, rsz, "Parent interface '%s' does not exist.", parent); + ds_nl_close(ctx); + return -ENODEV; + } + + char probe[IFNAMSIZ]; + snprintf(probe, sizeof(probe), "%s%x", + strcmp(kind, "ipvlan") == 0 ? "ds-ci" : "ds-cm", + (unsigned int)getpid()); + int ret = ds_nl_create_parent_link(ctx, parent, probe, kind); + if (ret == 0) + ds_nl_del_link(ctx, probe); + ds_nl_close(ctx); + + if (ret < 0) { + snprintf(reason, rsz, "%s on parent '%s' failed: %s", kind, parent, + strerror(-ret)); + return ret; + } + snprintf(reason, rsz, "OK (%s on %s)", kind, parent); + return 0; +} + /* --------------------------------------------------------------------------- * Attach an interface to a bridge (IFLA_MASTER) * ---------------------------------------------------------------------------*/ @@ -589,6 +672,96 @@ int ds_nl_add_addr4(ds_nl_ctx_t *ctx, const char *ifname, uint32_t ip_be, return ds_nl_talk(ctx, &req.n); } +int ds_nl_del_addr4(ds_nl_ctx_t *ctx, const char *ifname, uint32_t ip_be, + uint8_t prefix) { + if (prefix > 32) + return -EINVAL; + int idx = ds_nl_get_ifindex(ctx, ifname); + if (idx <= 0) + return 0; + struct { + struct nlmsghdr n; + struct ifaddrmsg ifa; + char buf[128]; + } req; + memset(&req, 0, sizeof(req)); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg)); + req.n.nlmsg_type = RTM_DELADDR; + req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + req.ifa.ifa_family = AF_INET; + req.ifa.ifa_prefixlen = prefix; + req.ifa.ifa_index = (unsigned int)idx; + nl_addattr(&req.n, (int)sizeof(req), IFA_LOCAL, &ip_be, 4); + nl_addattr(&req.n, (int)sizeof(req), IFA_ADDRESS, &ip_be, 4); + int ret = ds_nl_talk(ctx, &req.n); + return ret == -EADDRNOTAVAIL ? 0 : ret; +} + +int ds_nl_get_addr4(ds_nl_ctx_t *ctx, const char *ifname, uint32_t *ip_be, + uint8_t *prefix) { + int target = ds_nl_get_ifindex(ctx, ifname); + if (target <= 0) + return -ENODEV; + + struct { + struct nlmsghdr n; + struct ifaddrmsg ifa; + } req; + memset(&req, 0, sizeof(req)); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg)); + req.n.nlmsg_type = RTM_GETADDR; + req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + req.ifa.ifa_family = AF_INET; + req.n.nlmsg_seq = ++ctx->seq; + req.n.nlmsg_pid = (uint32_t)ctx->pid; + if (send(ctx->fd, &req, req.n.nlmsg_len, 0) < 0) + return -errno; + + int found = -ENOENT; + uint8_t buf[NL_BUFSIZE]; + for (;;) { + ssize_t n = recv(ctx->fd, buf, sizeof(buf), 0); + if (n < 0) { + if (errno == EINTR) + continue; + return -errno; + } + struct nlmsghdr *h = (struct nlmsghdr *)buf; + for (; NLMSG_OK(h, (uint32_t)n); h = NLMSG_NEXT(h, n)) { + if (h->nlmsg_seq != req.n.nlmsg_seq) + continue; + if (h->nlmsg_type == NLMSG_DONE) + return found; + if (h->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = NLMSG_DATA(h); + return err->error ? err->error : found; + } + if (h->nlmsg_type != RTM_NEWADDR) + continue; + struct ifaddrmsg *ifa = NLMSG_DATA(h); + if (ifa->ifa_family != AF_INET || (int)ifa->ifa_index != target) + continue; + struct rtattr *rta = IFA_RTA(ifa); + int len = (int)IFA_PAYLOAD(h); + uint32_t candidate = 0; + for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { + if (rta->rta_type == IFA_LOCAL || + (rta->rta_type == IFA_ADDRESS && candidate == 0)) + candidate = nl_rta_u32(rta); + } + if (candidate != 0) { + if (ip_be) + *ip_be = candidate; + if (prefix) + *prefix = ifa->ifa_prefixlen; + found = 0; + if (!(ifa->ifa_flags & IFA_F_SECONDARY)) + return 0; + } + } + } +} + /* --------------------------------------------------------------------------- * Add an IPv4 route * dst_be=0 + dst_len=0 → default route. @@ -622,6 +795,33 @@ int ds_nl_add_route4(ds_nl_ctx_t *ctx, uint32_t dst_be, uint8_t dst_len, return ds_nl_talk(ctx, &req.n); } +int ds_nl_del_route4(ds_nl_ctx_t *ctx, uint32_t dst_be, uint8_t dst_len, + uint32_t gw_be, int oif_idx) { + struct { + struct nlmsghdr n; + struct rtmsg r; + char buf[256]; + } req; + memset(&req, 0, sizeof(req)); + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + req.n.nlmsg_type = RTM_DELROUTE; + req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + req.r.rtm_family = AF_INET; + req.r.rtm_dst_len = dst_len; + req.r.rtm_table = RT_TABLE_MAIN; + req.r.rtm_protocol = RTPROT_BOOT; + req.r.rtm_scope = (gw_be == 0) ? RT_SCOPE_LINK : RT_SCOPE_UNIVERSE; + req.r.rtm_type = RTN_UNICAST; + if (dst_len > 0) + nl_addattr(&req.n, (int)sizeof(req), RTA_DST, &dst_be, 4); + if (gw_be) + nl_addattr(&req.n, (int)sizeof(req), RTA_GATEWAY, &gw_be, 4); + if (oif_idx > 0) + nl_addattr(&req.n, (int)sizeof(req), RTA_OIF, &oif_idx, (int)sizeof(int)); + int ret = ds_nl_talk(ctx, &req.n); + return ret == -ESRCH || ret == -ENOENT ? 0 : ret; +} + /* --------------------------------------------------------------------------- * Move an interface into a network namespace (by fd) * ---------------------------------------------------------------------------*/ diff --git a/src/net/network.c b/src/net/network.c index 523919eb..c5a1a22d 100644 --- a/src/net/network.c +++ b/src/net/network.c @@ -46,10 +46,33 @@ static void veth_host_name(const struct ds_config *cfg, pid_t pid, char *buf, * Gateway clients use "ds-q" to match the distinct host-side prefix. */ static void veth_peer_name(const struct ds_config *cfg, pid_t pid, char *buf, size_t sz) { - const char *p = (cfg && cfg->net_mode == DS_NET_GATEWAY) ? "ds-q" : "ds-p"; + const char *p = "ds-p"; + if (cfg && cfg->net_mode == DS_NET_GATEWAY) + p = "ds-q"; + else if (cfg && cfg->net_mode == DS_NET_IPVLAN) + p = "ds-i"; + else if (cfg && cfg->net_mode == DS_NET_MACVLAN) + p = "ds-m"; snprintf(buf, sz, "%s%d", p, (int)pid); } +/* Android devices which expose nl80211 normally ship iw in /system/bin. + * Keep this optional: macvlan still works on Ethernet, while Wi-Fi also + * depends on driver/firmware and AP-side four-address/WDS support. */ +static int try_enable_wifi_4addr(char *ifname) { + static char *const iw_paths[] = { + "/system/bin/iw", "/vendor/bin/iw", "/usr/sbin/iw", "/usr/bin/iw"}; + + for (size_t i = 0; i < sizeof(iw_paths) / sizeof(iw_paths[0]); i++) { + if (access(iw_paths[i], X_OK) != 0) + continue; + char *const argv[] = {iw_paths[i], "dev", ifname, "set", + "4addr", "on", NULL}; + return run_command_log(argv); + } + return 127; +} + /* Derive a deterministic IP from a PID (avoids sequential collisions) */ static void veth_peer_ip(pid_t pid, char *buf, size_t sz) { /* Multiplicative hash to spread sequential PIDs across the /16 subnet. @@ -327,6 +350,7 @@ static int uplink_name_excluded(const char *ifname) { void ds_net_derive_handshake(pid_t init_pid, struct ds_config *cfg, struct ds_net_handshake *hs) { + memset(hs, 0, sizeof(*hs)); veth_peer_name(cfg, init_pid, hs->peer_name, sizeof(hs->peer_name)); /* Use the already-resolved static IP - not the PID-hash fallback. * ip_str is informational on the child side (voided in @@ -897,6 +921,113 @@ int setup_veth_host_side(struct ds_config *cfg, pid_t child_pid) { return 0; } +/* Parse a conventional colon-separated unicast MAC address. All-zero, + * multicast, broadcast, truncated, and trailing-junk forms are rejected. */ +int ds_parse_mac_address(const char *text, uint8_t mac[6]) { + if (!text || !text[0] || !mac) + return -EINVAL; + + unsigned int b[6]; + char extra; + if (sscanf(text, "%2x:%2x:%2x:%2x:%2x:%2x%c", &b[0], &b[1], &b[2], + &b[3], &b[4], &b[5], &extra) != 6) + return -EINVAL; + + int any = 0; + for (size_t i = 0; i < 6; i++) { + if (b[i] > 0xff) + return -EINVAL; + mac[i] = (uint8_t)b[i]; + any |= mac[i]; + } + if (!any || (mac[0] & 0x01)) + return -EINVAL; + return 0; +} + +/* Create an L2 child of an Android interface and move it into the container. + * No bridge, DHCP server, NAT, firewall, address, or route is installed on the + * host. DHCP packets therefore reach the physical LAN unchanged. */ +int setup_parent_link_host_side(struct ds_config *cfg, pid_t child_pid) { + const char *kind = cfg->net_mode == DS_NET_IPVLAN ? "ipvlan" : "macvlan"; + char peer[IFNAMSIZ]; + veth_peer_name(cfg, child_pid, peer, sizeof(peer)); + + /* A Wi-Fi STA normally transports only its own source MAC. macvlan needs + * 802.11 four-address/WDS operation so frames carrying the guest MAC can + * cross the wireless link. This is best-effort: both the phone driver and + * AP must support/enable it, and some Android drivers reject the request. + * ipvlan shares the parent MAC and does not need this. */ + if (cfg->net_mode == DS_NET_MACVLAN && + strncmp(cfg->net_parent, "wlan", 4) == 0) { + int four_addr = try_enable_wifi_4addr(cfg->net_parent); + if (four_addr == 0) + ds_log("[NET] Enabled 4addr/WDS on Wi-Fi parent %s", + cfg->net_parent); + else if (four_addr == 127) + ds_warn("[NET] Could not enable 4addr/WDS on Wi-Fi parent %s: iw " + "is unavailable; macvlan traffic requires phone and AP support", + cfg->net_parent); + else + ds_warn("[NET] Could not enable 4addr/WDS on Wi-Fi parent %s (iw exit " + "%d); macvlan traffic requires phone and AP support", + cfg->net_parent, four_addr); + } + + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) + return -errno; + + ds_nl_del_link(ctx, peer); + int ret = ds_nl_create_parent_link(ctx, cfg->net_parent, peer, kind); + if (ret < 0) { + ds_warn("[NET] Failed to create %s link %s on %s: %s", kind, peer, + cfg->net_parent, strerror(-ret)); + ds_nl_close(ctx); + return ret; + } + + /* A user-specified macvlan identity is optional. When blank, do not touch + * the kernel-assigned address. ipvlan shares its parent's MAC. */ + if (cfg->net_mode == DS_NET_MACVLAN && cfg->net_mac[0]) { + uint8_t mac[6]; + if (ds_parse_mac_address(cfg->net_mac, mac) < 0) { + ds_nl_del_link(ctx, peer); + ds_nl_close(ctx); + return -EINVAL; + } + ret = ds_nl_set_mac(ctx, peer, mac); + if (ret < 0) { + ds_warn("[NET] Could not set requested MAC %s on %s: %s", cfg->net_mac, + peer, strerror(-ret)); + ds_nl_del_link(ctx, peer); + ds_nl_close(ctx); + return ret; + } + ds_log("[NET] Set macvlan MAC to %s", cfg->net_mac); + } + + char netns_path[PATH_MAX]; + snprintf(netns_path, sizeof(netns_path), "/proc/%d/ns/net", child_pid); + int netns_fd = open(netns_path, O_RDONLY | O_CLOEXEC); + if (netns_fd < 0) { + ret = -errno; + ds_nl_del_link(ctx, peer); + ds_nl_close(ctx); + return ret; + } + ret = ds_nl_move_to_netns(ctx, peer, netns_fd); + close(netns_fd); + if (ret < 0) + ds_nl_del_link(ctx, peer); + ds_nl_close(ctx); + + if (ret == 0) + ds_log("[NET] Attached %s to parent %s; guest owns IP configuration", kind, + cfg->net_parent); + return ret; +} + /* --------------------------------------------------------------------------- * Gateway segment lock * @@ -1463,6 +1594,74 @@ int setup_veth_child_side_named(struct ds_config *cfg, const char *peer_name, return 0; } +int setup_parent_link_child_side(struct ds_config *cfg, const char *peer_name) { + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) + return -errno; + + if (peer_name && peer_name[0] && strcmp(peer_name, "eth0") != 0) { + int ret = ds_nl_rename(ctx, peer_name, "eth0"); + if (ret < 0) { + ds_warn("[NET] Child: failed to rename %s to eth0: %s", peer_name, + strerror(-ret)); + ds_nl_close(ctx); + return ret; + } + } + ds_nl_link_up(ctx, "lo"); + int ret = ds_nl_link_up(ctx, "eth0"); + if (ret < 0) { + ds_nl_close(ctx); + return ret; + } + + /* Enforce the configured IPv6 policy on the direct-L2 interface itself. + * all/default may already have the desired value, but a guest network + * manager can otherwise re-enable IPv6 while applying its link profile. */ + if (write_file("/proc/sys/net/ipv6/conf/eth0/disable_ipv6", + cfg->disable_ipv6 ? "1" : "0") < 0) + ds_warn("[NET] Child: failed to apply IPv6 policy on eth0"); + + if (cfg->net_ipam == DS_NET_IPAM_STATIC) { + char cidr[sizeof(cfg->net_address)]; + safe_strncpy(cidr, cfg->net_address, sizeof(cidr)); + char *slash = strchr(cidr, '/'); + if (!slash) { + ds_nl_close(ctx); + return -EINVAL; + } + *slash++ = '\0'; + char *end = NULL; + long prefix = strtol(slash, &end, 10); + struct in_addr addr, gateway; + if (!end || *end || prefix < 0 || prefix > 32 || + inet_pton(AF_INET, cidr, &addr) != 1 || + inet_pton(AF_INET, cfg->net_gateway, &gateway) != 1) { + ds_nl_close(ctx); + return -EINVAL; + } + ret = ds_nl_add_addr4(ctx, "eth0", addr.s_addr, (uint8_t)prefix); + if (ret < 0 && ret != -EEXIST) { + ds_nl_close(ctx); + return ret; + } + int ifindex = ds_nl_get_ifindex(ctx, "eth0"); + ret = ds_nl_add_route4(ctx, 0, 0, gateway.s_addr, ifindex); + if (ret < 0 && ret != -EEXIST) { + ds_nl_close(ctx); + return ret; + } + ds_log("[NET] Child: static address %s via %s configured on eth0", + cfg->net_address, cfg->net_gateway); + } else { + ds_log("[NET] Child: eth0 UP; guest NetworkManager/networkd will request " + "DHCP from the external network"); + } + + ds_nl_close(ctx); + return 0; +} + /* Compatibility wrapper */ /* --------------------------------------------------------------------------- @@ -1491,7 +1690,10 @@ static void setup_resolv_conf(struct ds_config *cfg) { * dnsmasq), advertised in the DHCP lease. Droidspaces must NOT write a * static resolv.conf or it would bypass the gateway's DNS filtering/caching. */ - if (cfg->net_mode == DS_NET_GATEWAY && !cfg->dns_servers[0]) { + if ((cfg->net_mode == DS_NET_GATEWAY || + ((cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) && + cfg->net_ipam == DS_NET_IPAM_DHCP)) && + !cfg->dns_servers[0]) { if (is_systemd_rootfs("/")) { /* systemd-resolved consumes the lease and publishes the real resolver. */ target = "/run/systemd/resolve/resolv.conf"; @@ -1500,8 +1702,7 @@ static void setup_resolv_conf(struct ds_config *cfg) { * which writes the gateway-supplied nameserver from the lease. Writing a * hardcoded 1.1.1.1/8.8.8.8 here would silently defeat the gateway's DNS * (adblock, split-horizon, etc.). Pass --dns to override. */ - ds_log("[NET] Gateway: leaving /etc/resolv.conf to the container's DHCP " - "client (gateway owns DNS)"); + ds_log("[NET] Leaving /etc/resolv.conf to the container's DHCP client"); return; } } else { @@ -1541,7 +1742,8 @@ int fix_networking_rootfs(struct ds_config *cfg) { * Gateway mode is policy-owned by OpenWrt, so IPv6 RA/DHCPv6 should be able * to operate inside the application container netns. */ int ipv6_enabled = - ((cfg->net_mode == DS_NET_HOST || cfg->net_mode == DS_NET_GATEWAY) && + ((cfg->net_mode == DS_NET_HOST || cfg->net_mode == DS_NET_GATEWAY || + cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) && !cfg->disable_ipv6); if (ipv6_enabled) { snprintf(hosts_content, sizeof(hosts_content), @@ -1818,6 +2020,55 @@ static int find_active_uplink(ds_nl_ctx_t *ctx, char *iface_out, return -ENOENT; } +/* Resolve the parent for direct-L2 modes. A configured value is a strict + * override; an empty value follows the same Android-aware active-uplink + * detection used by NAT (netd default rule, main table, then whitelist). + * The resolved name lives only in this start request, so container.config can + * remain blank and be re-evaluated after Wi-Fi/mobile handoffs. */ +int ds_net_resolve_parent(struct ds_config *cfg, char *reason, + size_t reason_size) { + if (!cfg || (cfg->net_mode != DS_NET_IPVLAN && + cfg->net_mode != DS_NET_MACVLAN)) { + if (reason && reason_size) + snprintf(reason, reason_size, "Direct-L2 network mode is not selected."); + return -EINVAL; + } + + if (cfg->net_parent[0]) { + if (reason && reason_size) + snprintf(reason, reason_size, "Using configured parent '%s'.", + cfg->net_parent); + return 0; + } + + ds_nl_ctx_t *ctx = ds_nl_open(); + if (!ctx) { + int err = errno ? errno : EIO; + if (reason && reason_size) + snprintf(reason, reason_size, "Cannot open route netlink: %s", + strerror(err)); + return -err; + } + + char iface[IFNAMSIZ] = {0}; + int table = 0; + int ret = find_active_uplink(ctx, iface, &table); + ds_nl_close(ctx); + if (ret < 0 || !iface[0]) { + if (reason && reason_size) + snprintf(reason, reason_size, + "No active host uplink with an IPv4 default route was found."); + return ret < 0 ? ret : -ENOENT; + } + + safe_strncpy(cfg->net_parent, iface, sizeof(cfg->net_parent)); + if (reason && reason_size) + snprintf(reason, reason_size, + "Auto-detected parent '%s' from active route table %d.", iface, + table); + return 0; +} + /* Re-probe which uplink is active and update the ip rule if needed. */ static void do_uplink_reprobe(void) { ds_nl_ctx_t *ctx = ds_nl_open(); @@ -2172,6 +2423,12 @@ void ds_net_cleanup(struct ds_config *cfg, pid_t container_pid) { * to us, so it is safe to run for every stopping container. */ ds_net_gateway_teardown(cfg->container_name); + if ((cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) && + cfg->host_access != DS_HOST_ACCESS_NONE) { + ds_host_access_cleanup(cfg, container_pid); + return; + } + if (cfg->net_mode == DS_NET_GATEWAY) { ds_nl_ctx_t *ctx = ds_nl_open(); if (!ctx) diff --git a/src/socketd_bridge.c b/src/socketd_bridge.c index 7931c441..de791fa5 100644 --- a/src/socketd_bridge.c +++ b/src/socketd_bridge.c @@ -740,13 +740,37 @@ static int socketd_validate_start_config(struct ds_config *cfg) { if (check_requirements_hw(cfg->hw_access) < 0) return -1; - if ((cfg->net_mode == DS_NET_NAT || cfg->net_mode == DS_NET_NONE) && + if ((cfg->net_mode == DS_NET_NAT || cfg->net_mode == DS_NET_NONE || + cfg->net_mode == DS_NET_GATEWAY || cfg->net_mode == DS_NET_IPVLAN || + cfg->net_mode == DS_NET_MACVLAN) && !check_ns(CLONE_NEWNET, "net")) { ds_error("Container '%s' requires network namespaces for its net mode", cfg->container_name); return -1; } + if (cfg->net_mode == DS_NET_IPVLAN || cfg->net_mode == DS_NET_MACVLAN) { + char reason[256]; + const char *kind = + cfg->net_mode == DS_NET_IPVLAN ? "ipvlan" : "macvlan"; + if (ds_net_resolve_parent(cfg, reason, sizeof(reason)) < 0) { + ds_error("Container '%s' cannot auto-detect a %s parent: %s", + cfg->container_name, kind, reason); + return -1; + } + ds_log("[NET] %s", reason); + if (ds_nl_probe_parent_capability(cfg->net_parent, kind, reason, + sizeof(reason)) < 0) { + ds_error("Container '%s' cannot use %s: %s", cfg->container_name, kind, + reason); + return -1; + } + } else if (cfg->host_access != DS_HOST_ACCESS_NONE) { + ds_error("Container '%s' enables host access outside ipvlan/macvlan mode", + cfg->container_name); + return -1; + } + return 0; }