Skip to content

Commit fd13805

Browse files
torarnvcurdbecker
andcommitted
Add BridgedNetworkInterface; make Interface.ipv4Address optional
Adds BridgedNetworkInterface, which uses VZBridgedNetworkDeviceAttachment to place a container on the host's physical network. The IP address is assigned by the upstream DHCP server rather than our allocation pool, so ipv4Address is always nil for this type. Makes Interface.ipv4Address optional (CIDRv4?) to accommodate interfaces whose address is not known at configuration time. Updates all existing conformers (NATInterface, NATNetworkInterface, VmnetNetwork.Interface) and guards the static address/route setup in VirtualMachineAgent+Interface behind an ipv4Address nil-check. Fixes #457 Co-authored-by: Curd Becker <me@curd-becker.de>
1 parent 6b7b42c commit fd13805

10 files changed

Lines changed: 82 additions & 19 deletions
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
#if os(macOS)
18+
19+
import ContainerizationError
20+
import ContainerizationExtras
21+
import Virtualization
22+
23+
/// A network interface that bridges the container onto a host physical interface.
24+
/// The IP address is assigned by the upstream DHCP server; `ipv4Address` is always nil.
25+
@available(macOS 26, *)
26+
public final class BridgedNetworkInterface: Interface, Sendable {
27+
public let hostInterfaceName: String
28+
public let macAddress: MACAddress?
29+
public let ipv4Address: CIDRv4? = nil
30+
public let ipv4Gateway: IPv4Address? = nil
31+
public let mtu: UInt32 = 1500
32+
33+
public init(hostInterfaceName: String, macAddress: MACAddress? = nil) {
34+
self.hostInterfaceName = hostInterfaceName
35+
self.macAddress = macAddress
36+
}
37+
}
38+
39+
@available(macOS 26, *)
40+
extension BridgedNetworkInterface: VZInterface {
41+
public func device() throws -> VZVirtioNetworkDeviceConfiguration {
42+
guard
43+
let vzIface = VZBridgedNetworkInterface.networkInterfaces
44+
.first(where: { $0.identifier == hostInterfaceName })
45+
else {
46+
throw ContainerizationError(
47+
.invalidArgument,
48+
message: "no bridged interface named \(hostInterfaceName)")
49+
}
50+
let config = VZVirtioNetworkDeviceConfiguration()
51+
config.attachment = VZBridgedNetworkDeviceAttachment(interface: vzIface)
52+
if let mac = macAddress, let vzMac = VZMACAddress(string: mac.description) {
53+
config.macAddress = vzMac
54+
}
55+
return config
56+
}
57+
}
58+
59+
#endif

Sources/Containerization/Interface.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import ContainerizationExtras
2020
public protocol Interface: Sendable {
2121
/// The interface IPv4 address and subnet prefix length, as a CIDR address.
2222
/// Example: `192.168.64.3/24`
23-
var ipv4Address: CIDRv4 { get }
23+
var ipv4Address: CIDRv4? { get }
2424

2525
/// The IPv4 gateway address for the default route, or nil for no IPv4 default route.
2626
var ipv4Gateway: IPv4Address? { get }

Sources/Containerization/LinuxContainer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ extension LinuxContainer {
631631
// For every interface asked for:
632632
// 1. Add the address requested
633633
// 2. Online the adapter
634-
// 3. For the first interface, add the default route
634+
// 3. For the first interface with a static address, add the default route
635635
var defaultRouteSet = false
636636
for (index, i) in self.interfaces.enumerated() {
637637
let name = "eth\(index)"

Sources/Containerization/LinuxPod.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ extension LinuxPod {
655655
// For every interface asked for:
656656
// 1. Add the address requested
657657
// 2. Online the adapter
658-
// 3. For the first interface, add the default route
658+
// 3. For the first interface with a static address, add the default route
659659
var defaultRouteSet = false
660660
for (index, i) in self.interfaces.enumerated() {
661661
let name = "eth\(index)"

Sources/Containerization/NATInterface.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import ContainerizationExtras
1818

1919
public struct NATInterface: Interface {
20-
public var ipv4Address: CIDRv4
20+
public var ipv4Address: CIDRv4?
2121
public var ipv4Gateway: IPv4Address?
2222
public var ipv6Address: CIDRv6?
2323
public var ipv6Gateway: IPv6Address?

Sources/Containerization/NATNetworkInterface.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import Synchronization
2626
/// container/virtual machine.
2727
@available(macOS 26, *)
2828
public final class NATNetworkInterface: Interface, Sendable {
29-
public let ipv4Address: CIDRv4
29+
public let ipv4Address: CIDRv4?
3030
public let ipv4Gateway: IPv4Address?
3131
public let macAddress: MACAddress?
3232
public let mtu: UInt32

Sources/Containerization/VirtualMachineAgent+Interface.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,23 @@ extension VirtualMachineAgent {
2626
setDefaultRoute: Bool,
2727
logger: Logger?
2828
) async throws {
29-
logger?.debug("setting up interface \(name) with v4 \(interface.ipv4Address) v6 \(interface.ipv6Address?.description ?? "<none>")")
30-
try await addressAdd(
31-
name: name,
32-
address: .init(ipv4Address: interface.ipv4Address, ipv6Address: interface.ipv6Address)
33-
)
34-
try await up(name: name, mtu: interface.mtu)
35-
36-
guard setDefaultRoute else { return }
37-
3829
let ipv4Address = interface.ipv4Address
3930
let ipv4Gateway = interface.ipv4Gateway
4031
let ipv6Gateway = interface.ipv6Gateway
4132
let ipv6Address = interface.ipv6Address
4233

34+
if let ipv4Address {
35+
logger?.debug("setting up interface \(name) with v4 \(ipv4Address) v6 \(interface.ipv6Address?.description ?? "<none>")")
36+
try await addressAdd(
37+
name: name,
38+
address: .init(ipv4Address: ipv4Address, ipv6Address: interface.ipv6Address)
39+
)
40+
}
41+
try await up(name: name, mtu: interface.mtu)
42+
43+
guard setDefaultRoute else { return }
44+
guard let ipv4Address else { return }
45+
4346
let needsIPv4LinkRoute: Bool
4447
if let ipv4Gateway {
4548
needsIPv4LinkRoute = !ipv4Address.contains(ipv4Gateway)

Sources/Containerization/VmnetNetwork.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public struct VmnetNetwork: Network {
114114

115115
/// A network interface supporting the vmnet_network_ref.
116116
public struct Interface: Containerization.Interface, VZInterface, Sendable {
117-
public let ipv4Address: CIDRv4
117+
public let ipv4Address: CIDRv4?
118118
public let ipv4Gateway: IPv4Address?
119119
public let ipv6Address: CIDRv6?
120120
public let ipv6Gateway: IPv6Address?

Sources/Integration/ContainerTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5144,7 +5144,9 @@ extension IntegrationSuite {
51445144
}
51455145

51465146
// Capture the v4 address vmnet allocated so we can assert it ends up on eth0.
5147-
let expectedV4 = interface.ipv4Address.address.description
5147+
guard let expectedV4 = interface.ipv4Address?.address.description else {
5148+
throw IntegrationError.assert(msg: "network interface needs IPv4 address")
5149+
}
51485150

51495151
let addrBuffer = BufferWriter()
51505152
let routeBuffer = BufferWriter()

Sources/cctl/RunCommand.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,10 @@ extension Application {
143143
}
144144

145145
// Add host entry for the container using just the IP (not CIDR)
146-
if #available(macOS 26, *), !config.interfaces.isEmpty {
147-
let interface = config.interfaces[0]
146+
if #available(macOS 26, *), let addr = config.interfaces.first?.ipv4Address {
148147
hosts.entries.append(
149148
Hosts.Entry(
150-
ipAddress: interface.ipv4Address.address.description,
149+
ipAddress: addr.address.description,
151150
hostnames: [id]
152151
))
153152
}

0 commit comments

Comments
 (0)