Skip to content

Commit c56a659

Browse files
authored
Use allocate with session to automatically clean up IPs. (apple#1544)
- Part of apple#1318. - Part of apple#1378. - Removes network plugin `deallocate()`, and allocate takes an `XPCServerSession` that registers an `onDisconnect` handler that performs deallocation. - ContainerService now tracks `networkSessions` for allocations.
1 parent 1794afc commit c56a659

6 files changed

Lines changed: 84 additions & 70 deletions

File tree

Sources/Plugins/NetworkVmnet/NetworkVmnetHelper+Start.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ extension NetworkVmnetHelper {
104104
identifier: serviceIdentifier,
105105
routes: [
106106
NetworkRoutes.state.rawValue: XPCServer.route(server.state),
107-
NetworkRoutes.allocate.rawValue: XPCServer.route(server.allocate),
108-
NetworkRoutes.deallocate.rawValue: XPCServer.route(server.deallocate),
107+
NetworkRoutes.allocate.rawValue: server.allocate,
109108
NetworkRoutes.lookup.rawValue: XPCServer.route(server.lookup),
110109
NetworkRoutes.disableAllocator.rawValue: XPCServer.route(server.disableAllocator),
111110
],

Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public actor ContainersService {
3636
var snapshot: ContainerSnapshot
3737
var client: SandboxClient?
3838
var allocatedAttachments: [AllocatedAttachment]
39+
var networkSessions: [XPCClientSession]
3940

4041
func getClient() throws -> SandboxClient {
4142
guard let client else {
@@ -132,7 +133,8 @@ public actor ContainersService {
132133
networks: [],
133134
startedDate: nil
134135
),
135-
allocatedAttachments: []
136+
allocatedAttachments: [],
137+
networkSessions: []
136138
)
137139
results[config.id] = state
138140
guard runtimePlugins.first(where: { $0.name == config.runtimeHandler }) != nil else {
@@ -394,7 +396,7 @@ public actor ContainersService {
394396
networks: [],
395397
startedDate: nil
396398
)
397-
await self.setContainerState(configuration.id, ContainerState(snapshot: snapshot, allocatedAttachments: []), context: context)
399+
await self.setContainerState(configuration.id, ContainerState(snapshot: snapshot, allocatedAttachments: [], networkSessions: []), context: context)
398400
} catch {
399401
throw error
400402
}
@@ -435,20 +437,23 @@ public actor ContainersService {
435437
let (config, _) = try Self.getContainerConfiguration(at: path)
436438

437439
var allocatedAttachments = [AllocatedAttachment]()
440+
var networkSessions = [XPCClientSession]()
438441
do {
439442
for n in config.networks {
440-
let allocatedAttach = try await self.networksService?.allocate(
441-
id: n.network,
442-
hostname: n.options.hostname,
443-
macAddress: n.options.macAddress
444-
)
445-
guard var allocatedAttach = allocatedAttach else {
443+
guard
444+
let (allocatedAttach, session) = try await self.networksService?.allocate(
445+
id: n.network,
446+
hostname: n.options.hostname,
447+
macAddress: n.options.macAddress
448+
)
449+
else {
446450
throw ContainerizationError(.internalError, message: "failed to allocate a network")
447451
}
448452

453+
var finalAttach = allocatedAttach
449454
if let mtu = n.options.mtu {
450455
let a = allocatedAttach.attachment
451-
allocatedAttach = AllocatedAttachment(
456+
finalAttach = AllocatedAttachment(
452457
attachment: Attachment(
453458
network: a.network,
454459
hostname: a.hostname,
@@ -462,7 +467,8 @@ public actor ContainersService {
462467
pluginInfo: allocatedAttach.pluginInfo
463468
)
464469
}
465-
allocatedAttachments.append(allocatedAttach)
470+
allocatedAttachments.append(finalAttach)
471+
networkSessions.append(session)
466472
}
467473

468474
try Self.registerService(
@@ -487,20 +493,11 @@ public actor ContainersService {
487493

488494
state.client = sandboxClient
489495
state.allocatedAttachments = allocatedAttachments
496+
state.networkSessions = networkSessions
490497
await self.setContainerState(id, state, context: context)
491498
} catch {
492-
for allocatedAttach in allocatedAttachments {
493-
do {
494-
try await self.networksService?.deallocate(attachment: allocatedAttach.attachment)
495-
} catch {
496-
self.log.error(
497-
"failed to deallocate network attachment",
498-
metadata: [
499-
"id": "\(id)",
500-
"network": "\(allocatedAttach.attachment.network)",
501-
"error": "\(error)",
502-
])
503-
}
499+
for session in networkSessions {
500+
session.close()
504501
}
505502

506503
let label = Self.fullLaunchdServiceLabel(
@@ -997,27 +994,17 @@ public actor ContainersService {
997994
])
998995
}
999996

1000-
// Best effort deallocate network attachments for the container. Don't throw on
1001-
// failure so we can continue with state cleanup.
1002-
self.log.info("deallocating network attachments", metadata: ["id": "\(id)"])
1003-
for allocatedAttach in state.allocatedAttachments {
1004-
do {
1005-
try await self.networksService?.deallocate(attachment: allocatedAttach.attachment)
1006-
} catch {
1007-
self.log.error(
1008-
"failed to deallocate network attachment",
1009-
metadata: [
1010-
"id": "\(id)",
1011-
"network": "\(allocatedAttach.attachment.network)",
1012-
"error": "\(error)",
1013-
])
1014-
}
997+
// Close network sessions — the network helper auto-releases allocations on disconnect.
998+
self.log.info("closing network sessions", metadata: ["id": "\(id)"])
999+
for session in state.networkSessions {
1000+
session.close()
10151001
}
10161002

10171003
state.snapshot.status = .stopped
10181004
state.snapshot.networks = []
10191005
state.client = nil
10201006
state.allocatedAttachments = []
1007+
state.networkSessions = []
10211008
await self.setContainerState(id, state, context: context)
10221009

10231010
let options = try getContainerCreationOptions(id: id)

Sources/Services/ContainerAPIService/Server/Networks/NetworksService.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -380,26 +380,26 @@ public actor NetworksService {
380380
}
381381
}
382382

383-
public func allocate(id: String, hostname: String, macAddress: MACAddress?) async throws -> AllocatedAttachment {
383+
public func allocate(id: String, hostname: String, macAddress: MACAddress?) async throws -> (AllocatedAttachment, XPCClientSession) {
384384
guard let serviceState = serviceStates[id] else {
385385
throw ContainerizationError(.notFound, message: "no network for id \(id)")
386386
}
387387
guard let pluginInfo = serviceState.networkState.pluginInfo else {
388388
throw ContainerizationError(.internalError, message: "network \(id) missing plugin information")
389389
}
390-
let (attach, additionalData) = try await serviceState.client.allocate(hostname: hostname, macAddress: macAddress)
391-
return AllocatedAttachment(
392-
attachment: attach,
393-
additionalData: additionalData,
394-
pluginInfo: pluginInfo
395-
)
396-
}
397-
398-
public func deallocate(attachment: Attachment) async throws {
399-
guard let serviceState = serviceStates[attachment.network] else {
400-
throw ContainerizationError(.notFound, message: "no network for id \(attachment.network)")
390+
let session = serviceState.client.connect()
391+
do {
392+
let (attach, additionalData) = try await serviceState.client.allocate(hostname: hostname, macAddress: macAddress, on: session)
393+
let alloc = AllocatedAttachment(
394+
attachment: attach,
395+
additionalData: additionalData,
396+
pluginInfo: pluginInfo
397+
)
398+
return (alloc, session)
399+
} catch {
400+
session.close()
401+
throw error
401402
}
402-
return try await serviceState.client.deallocate(hostname: attachment.hostname)
403403
}
404404

405405
private static func getClient(configuration: NetworkConfiguration) throws -> ContainerNetworkServiceClient.NetworkClient {

Sources/Services/ContainerNetworkService/Client/NetworkClient.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,34 @@ extension NetworkClient {
7171
return (attachment, additionalData)
7272
}
7373

74-
public func deallocate(hostname: String) async throws {
75-
let request = XPCMessage(route: NetworkRoutes.deallocate.rawValue)
76-
request.set(key: NetworkKeys.hostname.rawValue, value: hostname)
74+
/// Open a persistent connection to the network helper.
75+
///
76+
/// The returned session should be reused for `allocate(on:)` calls. The
77+
/// network helper automatically releases all allocations made over this
78+
/// session when it closes.
79+
public func connect() -> XPCClientSession {
80+
createClient().openSession()
81+
}
7782

78-
let client = createClient()
79-
try await client.send(request)
83+
/// Allocate a network attachment over an existing session.
84+
///
85+
/// Use `connect()` to obtain a session, then pass it here. The session
86+
/// must remain open for the lifetime of the allocation; closing it
87+
/// releases the allocation on the network helper automatically.
88+
public func allocate(
89+
hostname: String,
90+
macAddress: MACAddress? = nil,
91+
on session: XPCClientSession
92+
) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
93+
let request = XPCMessage(route: NetworkRoutes.allocate.rawValue)
94+
request.set(key: NetworkKeys.hostname.rawValue, value: hostname)
95+
if let macAddress = macAddress {
96+
request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress.description)
97+
}
98+
let response = try await session.send(request)
99+
let attachment = try response.attachment()
100+
let additionalData = response.additionalData()
101+
return (attachment, additionalData)
80102
}
81103

82104
public func lookup(hostname: String) async throws -> Attachment? {

Sources/Services/ContainerNetworkService/Client/NetworkRoutes.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ public enum NetworkRoutes: String {
1919
case state = "com.apple.container.network/state"
2020
/// Allocates parameters for attaching a sandbox to the network.
2121
case allocate = "com.apple.container.network/allocate"
22-
/// Deallocates parameters for attaching a sandbox to the network.
23-
case deallocate = "com.apple.container.network/deallocate"
2422
/// Disables the allocator if no sandboxes are attached.
2523
case disableAllocator = "com.apple.container.network/disableAllocator"
2624
/// Retrieves the allocation for a hostname.

Sources/Services/ContainerNetworkService/Server/NetworkService.swift

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public actor NetworkService: Sendable {
2727
private let log: Logger
2828
private var allocator: AttachmentAllocator
2929
private var macAddresses: [UInt32: MACAddress]
30+
private var allocationsBySession: [XPCServerSession: [(hostname: String, index: UInt32)]] = [:]
3031

3132
/// Set up a network service for the specified network.
3233
public init(
@@ -56,7 +57,7 @@ public actor NetworkService: Sendable {
5657
}
5758

5859
@Sendable
59-
public func allocate(_ message: XPCMessage) async throws -> XPCMessage {
60+
public func allocate(_ message: XPCMessage, _ session: XPCServerSession) async throws -> XPCMessage {
6061
log.debug("enter", metadata: ["func": "\(#function)"])
6162
defer { log.debug("exit", metadata: ["func": "\(#function)"]) }
6263

@@ -99,20 +100,27 @@ public actor NetworkService: Sendable {
99100
}
100101
}
101102
macAddresses[index] = macAddress
103+
104+
if allocationsBySession[session] == nil {
105+
allocationsBySession[session] = []
106+
await session.onDisconnect { [weak self] in
107+
await self?.releaseSession(session)
108+
}
109+
}
110+
allocationsBySession[session]!.append((hostname: hostname, index: index))
111+
102112
return reply
103113
}
104114

105-
@Sendable
106-
public func deallocate(_ message: XPCMessage) async throws -> XPCMessage {
107-
log.debug("enter", metadata: ["func": "\(#function)"])
108-
defer { log.debug("exit", metadata: ["func": "\(#function)"]) }
109-
110-
let hostname = try message.hostname()
111-
if let index = try await allocator.deallocate(hostname: hostname) {
112-
macAddresses.removeValue(forKey: index)
115+
private func releaseSession(_ session: XPCServerSession) async {
116+
guard let allocations = allocationsBySession.removeValue(forKey: session) else {
117+
return
118+
}
119+
for allocation in allocations {
120+
_ = try? await allocator.deallocate(hostname: allocation.hostname)
121+
macAddresses.removeValue(forKey: allocation.index)
113122
}
114-
log.info("released attachments", metadata: ["hostname": "\(hostname)"])
115-
return message.reply()
123+
log.info("released session", metadata: ["allocations": "\(allocations.count)"])
116124
}
117125

118126
@Sendable

0 commit comments

Comments
 (0)