Skip to content

Commit 23c8a66

Browse files
committed
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 LinuxContainer and LinuxPod behind an ipv4Address nil-check. Fixes #457
1 parent b770cd2 commit 23c8a66

8 files changed

Lines changed: 87 additions & 23 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 let vzIface = VZBridgedNetworkInterface.networkInterfaces
43+
.first(where: { $0.identifier == hostInterfaceName }) else {
44+
throw ContainerizationError(
45+
.invalidArgument,
46+
message: "no bridged interface named \(hostInterfaceName)")
47+
}
48+
let config = VZVirtioNetworkDeviceConfiguration()
49+
config.attachment = VZBridgedNetworkDeviceAttachment(interface: vzIface)
50+
if let mac = macAddress, let vzMac = VZMACAddress(string: mac.description) {
51+
config.macAddress = vzMac
52+
}
53+
return config
54+
}
55+
}
56+
57+
#endif

Sources/Containerization/Interface.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import ContainerizationExtras
1919
/// A network interface.
2020
public protocol Interface: Sendable {
2121
/// The interface IPv4 address and subnet prefix length, as a CIDR address.
22-
/// Example: `192.168.64.3/24`
23-
var ipv4Address: CIDRv4 { get }
22+
/// Example: `192.168.64.3/24`. nil when the address is assigned dynamically (e.g. DHCP).
23+
var ipv4Address: CIDRv4? { get }
2424

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

Sources/Containerization/LinuxContainer.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -606,22 +606,26 @@ extension LinuxContainer {
606606
}
607607

608608
// For every interface asked for:
609-
// 1. Add the address requested
609+
// 1. Add the address requested (skipped for bridge/DHCP interfaces)
610610
// 2. Online the adapter
611-
// 3. For the first interface, add the default route
611+
// 3. For the first interface with a static address, add the default route
612612
var defaultRouteSet = false
613613
for (index, i) in self.interfaces.enumerated() {
614614
let name = "eth\(index)"
615-
self.logger?.debug("setting up interface \(name) with address \(i.ipv4Address)")
616-
try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address)
615+
if let addr = i.ipv4Address {
616+
self.logger?.debug("setting up interface \(name) with address \(addr)")
617+
try await agent.addressAdd(name: name, ipv4Address: addr)
618+
} else {
619+
self.logger?.debug("bringing up interface \(name) (got address via DHCP)")
620+
}
617621
try await agent.up(name: name, mtu: i.mtu)
618-
if defaultRouteSet {
622+
guard !defaultRouteSet, let addr = i.ipv4Address else {
619623
continue
620624
}
621625
if let ipv4Gateway = i.ipv4Gateway {
622-
if !i.ipv4Address.contains(ipv4Gateway) {
623-
self.logger?.debug("gateway \(ipv4Gateway) is outside subnet \(i.ipv4Address), adding a route first")
624-
try await agent.routeAddLink(name: name, dstIPv4Addr: ipv4Gateway, srcIPv4Addr: i.ipv4Address.address)
626+
if !addr.contains(ipv4Gateway) {
627+
self.logger?.debug("gateway \(ipv4Gateway) is outside subnet \(addr), adding a route first")
628+
try await agent.routeAddLink(name: name, dstIPv4Addr: ipv4Gateway, srcIPv4Addr: addr.address)
625629
}
626630
try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway)
627631
} else {

Sources/Containerization/LinuxPod.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -442,21 +442,25 @@ extension LinuxPod {
442442
}
443443

444444
// For every interface asked for:
445-
// 1. Add the address requested
445+
// 1. Add the address requested (skipped for bridge/DHCP interfaces)
446446
// 2. Online the adapter
447-
// 3. For the first interface, add the default route
447+
// 3. For the first interface with a static address, add the default route
448448
var defaultRouteSet = false
449449
for (index, i) in self.interfaces.enumerated() {
450450
let name = "eth\(index)"
451-
self.logger?.debug("setting up interface \(name) with address \(i.ipv4Address)")
452-
try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address)
451+
if let addr = i.ipv4Address {
452+
self.logger?.debug("setting up interface \(name) with address \(addr)")
453+
try await agent.addressAdd(name: name, ipv4Address: addr)
454+
} else {
455+
self.logger?.debug("bringing up interface \(name) (got address via DHCP)")
456+
}
453457
try await agent.up(name: name, mtu: i.mtu)
454-
if defaultRouteSet {
458+
guard !defaultRouteSet, let addr = i.ipv4Address else {
455459
continue
456460
}
457461
if let ipv4Gateway = i.ipv4Gateway {
458-
if !i.ipv4Address.contains(ipv4Gateway) {
459-
self.logger?.debug("gateway \(ipv4Gateway) is outside subnet \(i.ipv4Address), adding a route first")
462+
if !addr.contains(ipv4Gateway) {
463+
self.logger?.debug("gateway \(ipv4Gateway) is outside subnet \(addr), adding a route first")
460464
try await agent.routeAddLink(name: name, dstIPv4Addr: ipv4Gateway, srcIPv4Addr: nil)
461465
}
462466
try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway)

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 macAddress: MACAddress?
2323
public var mtu: UInt32

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/VmnetNetwork.swift

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

7272
/// A network interface supporting the vmnet_network_ref.
7373
public struct Interface: Containerization.Interface, VZInterface, Sendable {
74-
public let ipv4Address: CIDRv4
74+
public let ipv4Address: CIDRv4?
7575
public let ipv4Gateway: IPv4Address?
7676
public let macAddress: MACAddress?
7777
public let mtu: UInt32

Sources/cctl/RunCommand.swift

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

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

0 commit comments

Comments
 (0)