diff --git a/Package.resolved b/Package.resolved index 8319ea5d..06d732a8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "f71c8d2a5e74a2c6d11a0fbe324774b5d6084237", - "version" : "2.99.0" + "revision" : "4e8f4b1c9adaa59315c523540c1ff2b38adc20a9", + "version" : "2.87.0" } }, { @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "81cc18264f92cd307ff98430f89372711d4f6fe9", - "version" : "1.43.0" + "revision" : "5e9e99ec96c53bc2c18ddd10c1e25a3cd97c55e5", + "version" : "1.38.0" } }, { diff --git a/Sources/Containerization/BridgedNetworkInterface.swift b/Sources/Containerization/BridgedNetworkInterface.swift new file mode 100644 index 00000000..44f2456a --- /dev/null +++ b/Sources/Containerization/BridgedNetworkInterface.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +#if os(macOS) + +import ContainerizationError +import ContainerizationExtras +import Virtualization + +/// A network interface that bridges the container onto a host physical interface. +/// The IP address is assigned by the upstream DHCP server; `ipv4Address` is always nil. +@available(macOS 26, *) +public final class BridgedNetworkInterface: Interface, Sendable { + public let hostInterfaceName: String + public let macAddress: MACAddress? + public let ipv4Address: CIDRv4? = nil + public let ipv4Gateway: IPv4Address? = nil + + public init(hostInterfaceName: String, macAddress: MACAddress? = nil) { + self.hostInterfaceName = hostInterfaceName + self.macAddress = macAddress + } +} + +@available(macOS 26, *) +extension BridgedNetworkInterface: VZInterface { + public func device() throws -> VZVirtioNetworkDeviceConfiguration { + guard + let vzIface = VZBridgedNetworkInterface.networkInterfaces + .first(where: { $0.identifier == hostInterfaceName }) + else { + throw ContainerizationError( + .invalidArgument, + message: "no bridged interface named \(hostInterfaceName)") + } + let config = VZVirtioNetworkDeviceConfiguration() + config.attachment = VZBridgedNetworkDeviceAttachment(interface: vzIface) + if let mac = macAddress, let vzMac = VZMACAddress(string: mac.description) { + config.macAddress = vzMac + } + return config + } +} + +#endif diff --git a/Sources/Containerization/FileHandleNetworkInterface.swift b/Sources/Containerization/FileHandleNetworkInterface.swift new file mode 100644 index 00000000..ba070410 --- /dev/null +++ b/Sources/Containerization/FileHandleNetworkInterface.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +#if os(macOS) + +import ContainerizationError +import ContainerizationExtras +import Virtualization + +/// A network interface that connects the container to an arbitrary FileHandle-backed +/// network service. The IP address might be assigned by the upstream DHCP server or +/// configured inside the container; `ipv4Address` is always nil. +@available(macOS 26, *) +public final class FileHandleNetworkInterface: Interface, Sendable { + public let macAddress: MACAddress? + public let ipv4Address: CIDRv4? = nil + public let ipv4Gateway: IPv4Address? = nil + public let fileHandle: FileHandle + + public init(fileHandle: FileHandle, macAddress: MACAddress? = nil) { + self.macAddress = macAddress + self.fileHandle = fileHandle + } +} + +@available(macOS 26, *) +extension FileHandleNetworkInterface: VZInterface { + public func device() throws -> VZVirtioNetworkDeviceConfiguration { + let config = VZVirtioNetworkDeviceConfiguration() + config.attachment = VZFileHandleNetworkDeviceAttachment(fileHandle: fileHandle) + if let mac = macAddress, let vzMac = VZMACAddress(string: mac.description) { + config.macAddress = vzMac + } + return config + } +} + +#endif diff --git a/Sources/Containerization/Interface.swift b/Sources/Containerization/Interface.swift index a95c58f9..31b28d14 100644 --- a/Sources/Containerization/Interface.swift +++ b/Sources/Containerization/Interface.swift @@ -19,8 +19,8 @@ import ContainerizationExtras /// A network interface. public protocol Interface: Sendable { /// The interface IPv4 address and subnet prefix length, as a CIDR address. - /// Example: `192.168.64.3/24` - var ipv4Address: CIDRv4 { get } + /// Example: `192.168.64.3/24`. nil when the address is assigned dynamically (e.g. DHCP). + var ipv4Address: CIDRv4? { get } /// The IPv4 gateway address for the default route, or nil for no IPv4 default route. var ipv4Gateway: IPv4Address? { get } diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 58ffa4a3..b19c93f1 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -629,9 +629,9 @@ extension LinuxContainer { } // For every interface asked for: - // 1. Add the address requested + // 1. Add the address requested (skipped for bridge/DHCP interfaces) // 2. Online the adapter - // 3. For the first interface, add the default route + // 3. For the first interface with a static address, add the default route var defaultRouteSet = false for (index, i) in self.interfaces.enumerated() { let name = "eth\(index)" diff --git a/Sources/Containerization/LinuxPod.swift b/Sources/Containerization/LinuxPod.swift index 3e5d58f0..3f14cf7c 100644 --- a/Sources/Containerization/LinuxPod.swift +++ b/Sources/Containerization/LinuxPod.swift @@ -653,9 +653,9 @@ extension LinuxPod { } // For every interface asked for: - // 1. Add the address requested + // 1. Add the address requested (skipped for bridge/DHCP interfaces) // 2. Online the adapter - // 3. For the first interface, add the default route + // 3. For the first interface with a static address, add the default route var defaultRouteSet = false for (index, i) in self.interfaces.enumerated() { let name = "eth\(index)" diff --git a/Sources/Containerization/NATInterface.swift b/Sources/Containerization/NATInterface.swift index c37fbc06..6aeb2793 100644 --- a/Sources/Containerization/NATInterface.swift +++ b/Sources/Containerization/NATInterface.swift @@ -17,7 +17,7 @@ import ContainerizationExtras public struct NATInterface: Interface { - public var ipv4Address: CIDRv4 + public var ipv4Address: CIDRv4? public var ipv4Gateway: IPv4Address? public var ipv6Address: CIDRv6? public var ipv6Gateway: IPv6Address? diff --git a/Sources/Containerization/NATNetworkInterface.swift b/Sources/Containerization/NATNetworkInterface.swift index f4d63922..8737e637 100644 --- a/Sources/Containerization/NATNetworkInterface.swift +++ b/Sources/Containerization/NATNetworkInterface.swift @@ -26,7 +26,7 @@ import Synchronization /// container/virtual machine. @available(macOS 26, *) public final class NATNetworkInterface: Interface, Sendable { - public let ipv4Address: CIDRv4 + public let ipv4Address: CIDRv4? public let ipv4Gateway: IPv4Address? public let macAddress: MACAddress? public let mtu: UInt32 diff --git a/Sources/Containerization/VirtualMachineAgent+Interface.swift b/Sources/Containerization/VirtualMachineAgent+Interface.swift index e2fe7227..4930f753 100644 --- a/Sources/Containerization/VirtualMachineAgent+Interface.swift +++ b/Sources/Containerization/VirtualMachineAgent+Interface.swift @@ -26,20 +26,25 @@ extension VirtualMachineAgent { setDefaultRoute: Bool, logger: Logger? ) async throws { - logger?.debug("setting up interface \(name) with v4 \(interface.ipv4Address) v6 \(interface.ipv6Address?.description ?? "")") - try await addressAdd( - name: name, - address: .init(ipv4Address: interface.ipv4Address, ipv6Address: interface.ipv6Address) - ) - try await up(name: name, mtu: interface.mtu) - - guard setDefaultRoute else { return } - let ipv4Address = interface.ipv4Address let ipv4Gateway = interface.ipv4Gateway let ipv6Gateway = interface.ipv6Gateway let ipv6Address = interface.ipv6Address + if let ipv4Address { + logger?.debug("setting up interface \(name) with v4 \(ipv4Address) v6 \(interface.ipv6Address?.description ?? "")") + try await addressAdd( + name: name, + address: .init(ipv4Address: ipv4Address, ipv6Address: interface.ipv6Address) + ) + } else { + logger?.debug("up interface \(name) (DHCP + SLAAC/PD)") + } + try await up(name: name, mtu: interface.mtu) + + guard setDefaultRoute else { return } + guard let ipv4Address else { return } + let needsIPv4LinkRoute: Bool if let ipv4Gateway { needsIPv4LinkRoute = !ipv4Address.contains(ipv4Gateway) diff --git a/Sources/Containerization/VmnetNetwork.swift b/Sources/Containerization/VmnetNetwork.swift index 86ec92cd..538cd4c7 100644 --- a/Sources/Containerization/VmnetNetwork.swift +++ b/Sources/Containerization/VmnetNetwork.swift @@ -114,7 +114,7 @@ public struct VmnetNetwork: Network { /// A network interface supporting the vmnet_network_ref. public struct Interface: Containerization.Interface, VZInterface, Sendable { - public let ipv4Address: CIDRv4 + public let ipv4Address: CIDRv4? public let ipv4Gateway: IPv4Address? public let ipv6Address: CIDRv6? public let ipv6Gateway: IPv6Address? diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 62c37918..e66b3607 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -4919,7 +4919,9 @@ extension IntegrationSuite { } // Capture the v4 address vmnet allocated so we can assert it ends up on eth0. - let expectedV4 = interface.ipv4Address.address.description + guard let expectedV4 = interface.ipv4Address?.address.description else { + throw IntegrationError.assert(msg: "network interface needs IPv4 address") + } let addrBuffer = BufferWriter() let routeBuffer = BufferWriter() diff --git a/Sources/cctl/RunCommand.swift b/Sources/cctl/RunCommand.swift index 66cf2081..725d8385 100644 --- a/Sources/cctl/RunCommand.swift +++ b/Sources/cctl/RunCommand.swift @@ -143,11 +143,10 @@ extension Application { } // Add host entry for the container using just the IP (not CIDR) - if #available(macOS 26, *), !config.interfaces.isEmpty { - let interface = config.interfaces[0] + if #available(macOS 26, *), let addr = config.interfaces.first?.ipv4Address { hosts.entries.append( Hosts.Entry( - ipAddress: interface.ipv4Address.address.description, + ipAddress: addr.address.description, hostnames: [id] )) } diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index 6bbd7ac8..176f8020 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -1332,13 +1332,12 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ request: Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest, context: GRPCCore.ServerContext ) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse { - let domain = request.hasDomain ? request.domain : nil log.debug( "configureDns", metadata: [ "location": "\(request.location)", "nameservers": "\(request.nameservers)", - "domain": "\(domain ?? "")", + "domain": "\(request.hasDomain ? request.domain : "")", "searchDomains": "\(request.searchDomains)", "options": "\(request.options)", ]) @@ -1347,8 +1346,27 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ let etc = URL(fileURLWithPath: request.location).appendingPathComponent("etc") try FileManager.default.createDirectory(atPath: etc.path, withIntermediateDirectories: true) let resolvConf = etc.appendingPathComponent("resolv.conf") + var nameservers = request.nameservers + var domain = request.hasDomain ? request.domain : nil + if nameservers.isEmpty || domain == nil, + let pnp = try? String(contentsOfFile: "/proc/net/pnp", encoding: .utf8) + { + let lines = pnp.split(separator: "\n") + if nameservers.isEmpty { + nameservers = + lines + .filter { $0.hasPrefix("nameserver") } + .compactMap { $0.split(separator: " ").dropFirst().first.map(String.init) } + } + if domain == nil { + domain = + lines + .first { $0.hasPrefix("domain") } + .flatMap { $0.split(separator: " ").dropFirst().first.map(String.init) } + } + } let config = DNS( - nameservers: request.nameservers, + nameservers: nameservers, domain: domain, searchDomains: request.searchDomains, options: request.options