Skip to content

Commit 81f2885

Browse files
committed
Start/stop RDNSS monitor.
1 parent 85d13dd commit 81f2885

13 files changed

Lines changed: 213 additions & 25 deletions

File tree

Sources/Containerization/ContainerManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ public struct ContainerManager: Sendable {
483483
message: "missing ipv4 gateway for container \(id)"
484484
)
485485
}
486-
config.dns = .init(nameservers: [gateway.description])
486+
config.dns = .init(nameservers: [gateway.description], enableRDNSSMonitor: true)
487487
}
488488
config.bootLog = BootLog.file(path: self.containerRoot.appendingPathComponent(id).appendingPathComponent("bootlog.log"))
489489
try configuration(&config)

Sources/Containerization/DNSConfiguration.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,22 @@ public struct DNS: Sendable {
2929
public var searchDomains: [String]
3030
/// The DNS options to use.
3131
public var options: [String]
32+
/// When true, vminitd will listen for IPv6 Router Advertisements and
33+
/// merge RDNSS nameservers into this resolv.conf entry.
34+
public var enableRDNSSMonitor: Bool
3235

3336
public init(
3437
nameservers: [String] = defaultNameservers,
3538
domain: String? = nil,
3639
searchDomains: [String] = [],
37-
options: [String] = []
40+
options: [String] = [],
41+
enableRDNSSMonitor: Bool = false
3842
) {
3943
self.nameservers = nameservers
4044
self.domain = domain
4145
self.searchDomains = searchDomains
4246
self.options = options
47+
self.enableRDNSSMonitor = enableRDNSSMonitor
4348
}
4449
}
4550

Sources/Containerization/LinuxContainer.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,18 @@ extension LinuxContainer {
996996
}
997997
}
998998

999+
/// Update the DNS configuration for this container on the running VM.
1000+
/// Replaces the current /etc/resolv.conf content and updates the RDNSS
1001+
/// monitor state to match the new config's `enableRDNSSMonitor` flag.
1002+
public func updateDNS(_ dns: DNS) async throws {
1003+
try await self.state.withLock {
1004+
let state = try $0.startedState("updateDNS")
1005+
try await state.vm.withAgent { agent in
1006+
try await agent.configureDNS(config: dns, location: Self.guestRootfsPath(self.id))
1007+
}
1008+
}
1009+
}
1010+
9991011
/// Get statistics for the container.
10001012
public func statistics(categories: StatCategory = .all) async throws -> ContainerStatistics {
10011013
try await self.state.withLock {

Sources/Containerization/LinuxPod.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,18 @@ extension LinuxPod {
834834
}
835835
}
836836

837+
/// Update the DNS configuration for a container in the pod on the running VM.
838+
/// Replaces the container's /etc/resolv.conf content and updates the RDNSS
839+
/// monitor state to match the new config's `enableRDNSSMonitor` flag.
840+
public func updateDNS(_ dns: DNS, containerID: String) async throws {
841+
try await self.state.withLock { state in
842+
let createdState = try state.phase.createdState("updateDNS")
843+
try await createdState.vm.withAgent { agent in
844+
try await agent.configureDNS(config: dns, location: Self.guestRootfsPath(containerID))
845+
}
846+
}
847+
}
848+
837849
/// Get statistics for containers in the pod.
838850
public func statistics(containerIDs: [String]? = nil, categories: StatCategory = .all) async throws -> [ContainerStatistics] {
839851
let (createdState, ids) = try await self.state.withLock { state in

Sources/Containerization/SandboxContext/SandboxContext.pb.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,8 @@ public struct Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest: Sendabl
10721072

10731073
public var options: [String] = []
10741074

1075+
public var enableRdnssMonitor: Bool = false
1076+
10751077
public var unknownFields = SwiftProtobuf.UnknownStorage()
10761078

10771079
public init() {}
@@ -3046,7 +3048,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultResponse: Swift
30463048

30473049
extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
30483050
public static let protoMessageName: String = _protobuf_package + ".ConfigureDnsRequest"
3049-
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}location\0\u{1}nameservers\0\u{1}domain\0\u{1}searchDomains\0\u{1}options\0")
3051+
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}location\0\u{1}nameservers\0\u{1}domain\0\u{1}searchDomains\0\u{1}options\0\u{3}enable_rdnss_monitor\0")
30503052

30513053
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
30523054
while let fieldNumber = try decoder.nextFieldNumber() {
@@ -3059,6 +3061,7 @@ extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest: SwiftProtob
30593061
case 3: try { try decoder.decodeSingularStringField(value: &self._domain) }()
30603062
case 4: try { try decoder.decodeRepeatedStringField(value: &self.searchDomains) }()
30613063
case 5: try { try decoder.decodeRepeatedStringField(value: &self.options) }()
3064+
case 6: try { try decoder.decodeSingularBoolField(value: &self.enableRdnssMonitor) }()
30623065
default: break
30633066
}
30643067
}
@@ -3084,6 +3087,9 @@ extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest: SwiftProtob
30843087
if !self.options.isEmpty {
30853088
try visitor.visitRepeatedStringField(value: self.options, fieldNumber: 5)
30863089
}
3090+
if self.enableRdnssMonitor != false {
3091+
try visitor.visitSingularBoolField(value: self.enableRdnssMonitor, fieldNumber: 6)
3092+
}
30873093
try unknownFields.traverse(visitor: &visitor)
30883094
}
30893095

@@ -3093,6 +3099,7 @@ extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest: SwiftProtob
30933099
if lhs._domain != rhs._domain {return false}
30943100
if lhs.searchDomains != rhs.searchDomains {return false}
30953101
if lhs.options != rhs.options {return false}
3102+
if lhs.enableRdnssMonitor != rhs.enableRdnssMonitor {return false}
30963103
if lhs.unknownFields != rhs.unknownFields {return false}
30973104
return true
30983105
}

Sources/Containerization/SandboxContext/SandboxContext.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ message ConfigureDnsRequest {
303303
optional string domain = 3;
304304
repeated string searchDomains = 4;
305305
repeated string options = 5;
306+
bool enable_rdnss_monitor = 6;
306307
}
307308

308309
message ConfigureDnsResponse {}

Sources/Containerization/Vminitd.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ extension Vminitd {
496496
}
497497
$0.searchDomains = config.searchDomains
498498
$0.options = config.options
499+
$0.enableRdnssMonitor = config.enableRDNSSMonitor
499500
})
500501
}
501502

Sources/Integration/ContainerTests.swift

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4176,9 +4176,10 @@ extension IntegrationSuite {
41764176
try await container.create()
41774177
try await container.start()
41784178

4179-
// The vmnet router sends Router Advertisements with RDNSS options.
4179+
// ContainerManager sets enableRDNSSMonitor on the DNS config, so vminitd
4180+
// starts the monitor automatically when configureDNS is called.
41804181
// Poll resolv.conf until the DNSMonitor has received an RA and merged
4181-
// an IPv6 nameserver into the file (identified by a colon in the address).
4182+
// an IPv6 nameserver into the file.
41824183
var found = false
41834184
let deadline = Date.now.addingTimeInterval(15)
41844185
while Date.now < deadline {
@@ -4196,22 +4197,53 @@ extension IntegrationSuite {
41964197
if status.exitCode == 0,
41974198
let output = String(data: buffer.data, encoding: .utf8),
41984199
output.split(separator: "\n")
4199-
.contains(where: { $0.hasPrefix("nameserver") && $0.contains(":") })
4200+
.contains(where: {
4201+
guard $0.hasPrefix("nameserver ") else { return false }
4202+
let addr = $0.dropFirst("nameserver ".count).trimmingCharacters(in: .whitespaces)
4203+
return (try? IPv6Address(addr)) != nil
4204+
})
42004205
{
42014206
found = true
42024207
break
42034208
}
42044209
}
42054210

4206-
try await container.kill(SIGKILL)
4207-
try await container.wait()
4208-
try await container.stop()
4209-
42104211
guard found else {
42114212
throw IntegrationError.assert(
42124213
msg: "resolv.conf was not updated with an IPv6 nameserver from RDNSS within timeout"
42134214
)
42144215
}
4216+
4217+
// Disable the RDNSS monitor by updating DNS config with the flag off.
4218+
// The monitor should stop and purge IPv6 nameservers from resolv.conf.
4219+
let staticDNS = DNS(nameservers: container.config.dns?.nameservers ?? [], enableRDNSSMonitor: false)
4220+
try await container.updateDNS(staticDNS)
4221+
4222+
let cleanBuffer = BufferWriter()
4223+
let cleanExec = try await container.exec("check-resolv-clean") { config in
4224+
config.arguments = ["cat", "/etc/resolv.conf"]
4225+
config.stdout = cleanBuffer
4226+
}
4227+
try await cleanExec.start()
4228+
let cleanStatus = try await cleanExec.wait()
4229+
try await cleanExec.delete()
4230+
if cleanStatus.exitCode == 0,
4231+
let cleanOutput = String(data: cleanBuffer.data, encoding: .utf8),
4232+
cleanOutput.split(separator: "\n")
4233+
.contains(where: {
4234+
guard $0.hasPrefix("nameserver ") else { return false }
4235+
let addr = $0.dropFirst("nameserver ".count).trimmingCharacters(in: .whitespaces)
4236+
return (try? IPv6Address(addr)) != nil
4237+
})
4238+
{
4239+
throw IntegrationError.assert(
4240+
msg: "resolv.conf still contains an IPv6 nameserver after disabling the RDNSS monitor"
4241+
)
4242+
}
4243+
4244+
try await container.kill(SIGKILL)
4245+
try await container.wait()
4246+
try await container.stop()
42154247
} catch {
42164248
try? await container.stop()
42174249
throw error

Sources/Integration/PodTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import ArgumentParser
1818
import Containerization
1919
import ContainerizationError
20+
import ContainerizationExtras
2021
import ContainerizationOCI
2122
import ContainerizationOS
2223
import Foundation
@@ -2032,4 +2033,84 @@ extension IntegrationSuite {
20322033
throw error
20332034
}
20342035
}
2036+
2037+
@available(macOS 26.0, *)
2038+
func testPodRDNSSUpdatesResolvConf() async throws {
2039+
let id = "test-pod-rdnss-updates-resolv-conf"
2040+
let bs = try await bootstrap(id)
2041+
2042+
var network = try ContainerManager.VmnetNetwork()
2043+
2044+
guard let interface = try network.create("\(id)-c1"),
2045+
let gateway = interface.ipv4Gateway
2046+
else {
2047+
throw IntegrationError.assert(msg: "failed to get vmnet interface or gateway")
2048+
}
2049+
2050+
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
2051+
config.cpus = 4
2052+
config.memoryInBytes = 1024.mib()
2053+
config.bootLog = bs.bootLog
2054+
config.interfaces = [interface]
2055+
config.dns = DNS(nameservers: [gateway.description], enableRDNSSMonitor: true)
2056+
}
2057+
2058+
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
2059+
config.process.arguments = ["sleep", "30"]
2060+
}
2061+
2062+
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
2063+
config.process.arguments = ["sleep", "30"]
2064+
}
2065+
2066+
do {
2067+
try await pod.create()
2068+
try await pod.startContainer("container1")
2069+
try await pod.startContainer("container2")
2070+
2071+
// Both containers share the pod's DNS config with enableRDNSSMonitor = true,
2072+
// so vminitd starts the RDNSS monitor automatically. Poll each container's
2073+
// resolv.conf until an IPv6 nameserver appears.
2074+
for containerID in ["container1", "container2"] {
2075+
var found = false
2076+
let deadline = Date.now.addingTimeInterval(15)
2077+
while Date.now < deadline {
2078+
try await Task.sleep(for: .seconds(1))
2079+
2080+
let buffer = BufferWriter()
2081+
let exec = try await pod.execInContainer(containerID, processID: "check-resolv-\(containerID)") { config in
2082+
config.arguments = ["cat", "/etc/resolv.conf"]
2083+
config.stdout = buffer
2084+
}
2085+
try await exec.start()
2086+
let status = try await exec.wait()
2087+
try await exec.delete()
2088+
2089+
if status.exitCode == 0,
2090+
let output = String(data: buffer.data, encoding: .utf8),
2091+
output.split(separator: "\n")
2092+
.contains(where: {
2093+
guard $0.hasPrefix("nameserver ") else { return false }
2094+
let addr = $0.dropFirst("nameserver ".count).trimmingCharacters(in: .whitespaces)
2095+
return (try? IPv6Address(addr)) != nil
2096+
})
2097+
{
2098+
found = true
2099+
break
2100+
}
2101+
}
2102+
2103+
guard found else {
2104+
throw IntegrationError.assert(
2105+
msg: "\(containerID): resolv.conf was not updated with an IPv6 nameserver from RDNSS within timeout"
2106+
)
2107+
}
2108+
}
2109+
2110+
try await pod.stop()
2111+
} catch {
2112+
try? await pod.stop()
2113+
throw error
2114+
}
2115+
}
20352116
}

Sources/Integration/Suite.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ struct IntegrationSuite: AsyncParsableCommand {
265265
Test("container networking disabled", testNetworkingDisabled),
266266
Test("container networking enabled", testNetworkingEnabled),
267267
Test("container RDNSS updates resolv.conf", testRDNSSUpdatesResolvConf),
268+
Test("pod RDNSS updates resolv.conf", testPodRDNSSUpdatesResolvConf),
268269
]
269270
}
270271
return []

0 commit comments

Comments
 (0)