From 80e2813bcfaba3a7b84dbd8b4b361c403f3e6b11 Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Fri, 3 Oct 2025 12:53:35 -0700 Subject: [PATCH] Netlink: Add stats support (#308) Adds support for grabbing stats for an iface. This will be surfaced in a coming change to get container level statistics from the guest, where one field will be network stats. --- .../InterfaceStatistics.swift | 44 +++ .../SandboxContext/SandboxContext.grpc.swift | 100 +++++++ .../SandboxContext/SandboxContext.pb.swift | 181 ++++++++++++ .../SandboxContext/SandboxContext.proto | 16 ++ .../VirtualMachineAgent.swift | 5 + Sources/Containerization/Vminitd.swift | 17 ++ .../NetlinkSession.swift | 10 +- Sources/ContainerizationNetlink/Types.swift | 262 ++++++++++++++++++ vminitd/Sources/vminitd/Server+GRPC.swift | 41 +++ 9 files changed, 673 insertions(+), 3 deletions(-) create mode 100644 Sources/Containerization/InterfaceStatistics.swift diff --git a/Sources/Containerization/InterfaceStatistics.swift b/Sources/Containerization/InterfaceStatistics.swift new file mode 100644 index 00000000..3dd219f1 --- /dev/null +++ b/Sources/Containerization/InterfaceStatistics.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved. +// +// 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. +//===----------------------------------------------------------------------===// + +/// Statistics for a network interface. +public struct InterfaceStatistics { + public var name: String + public var receivedPackets: UInt64? + public var transmittedPackets: UInt64? + public var receivedBytes: UInt64? + public var transmittedBytes: UInt64? + public var receivedErrors: UInt64? + public var transmittedErrors: UInt64? + + public init( + name: String, + receivedPackets: UInt64?, + transmittedPackets: UInt64?, + receivedBytes: UInt64?, + transmittedBytes: UInt64?, + receivedErrors: UInt64?, + transmittedErrors: UInt64? + ) { + self.name = name + self.receivedPackets = receivedPackets + self.transmittedPackets = transmittedPackets + self.receivedBytes = receivedBytes + self.transmittedBytes = transmittedBytes + self.receivedErrors = receivedErrors + self.transmittedErrors = transmittedErrors + } +} diff --git a/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift b/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift index 4896f21f..1e7c5d08 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift @@ -154,6 +154,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtoc callOptions: CallOptions? ) -> UnaryCall + func interfaceStatistics( + _ request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, + callOptions: CallOptions? + ) -> UnaryCall + func sync( _ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, callOptions: CallOptions? @@ -603,6 +608,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtocol { ) } + /// Get statistics about an interface. + /// + /// - Parameters: + /// - request: Request to send to InterfaceStatistics. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func interfaceStatistics( + _ request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.interfaceStatistics.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeInterfaceStatisticsInterceptors() ?? [] + ) + } + /// Perform the sync syscall. /// /// - Parameters: @@ -823,6 +846,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientP callOptions: CallOptions? ) -> GRPCAsyncUnaryCall + func makeInterfaceStatisticsCall( + _ request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + func makeSyncCall( _ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, callOptions: CallOptions? @@ -1132,6 +1160,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco ) } + public func makeInterfaceStatisticsCall( + _ request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.interfaceStatistics.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeInterfaceStatisticsInterceptors() ?? [] + ) + } + public func makeSyncCall( _ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, callOptions: CallOptions? = nil @@ -1447,6 +1487,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco ) } + public func interfaceStatistics( + _ request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, + callOptions: CallOptions? = nil + ) async throws -> Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse { + return try await self.performAsyncUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.interfaceStatistics.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeInterfaceStatisticsInterceptors() ?? [] + ) + } + public func sync( _ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, callOptions: CallOptions? = nil @@ -1563,6 +1615,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientInterc /// - Returns: Interceptors to use when invoking 'configureHosts'. func makeConfigureHostsInterceptors() -> [ClientInterceptor] + /// - Returns: Interceptors to use when invoking 'interfaceStatistics'. + func makeInterfaceStatisticsInterceptors() -> [ClientInterceptor] + /// - Returns: Interceptors to use when invoking 'sync'. func makeSyncInterceptors() -> [ClientInterceptor] @@ -1599,6 +1654,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata { Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.ipRouteAddDefault, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.configureDns, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.configureHosts, + Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.interfaceStatistics, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.sync, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.kill, ] @@ -1749,6 +1805,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata { type: GRPCCallType.unary ) + public static let interfaceStatistics = GRPCMethodDescriptor( + name: "InterfaceStatistics", + path: "/com.apple.containerization.sandbox.v3.SandboxContext/InterfaceStatistics", + type: GRPCCallType.unary + ) + public static let sync = GRPCMethodDescriptor( name: "Sync", path: "/com.apple.containerization.sandbox.v3.SandboxContext/Sync", @@ -1842,6 +1904,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider: Ca /// Configure /etc/hosts. func configureHosts(request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, context: StatusOnlyCallContext) -> EventLoopFuture + /// Get statistics about an interface. + func interfaceStatistics(request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, context: StatusOnlyCallContext) -> EventLoopFuture + /// Perform the sync syscall. func sync(request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, context: StatusOnlyCallContext) -> EventLoopFuture @@ -2077,6 +2142,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider { userFunction: self.configureHosts(request:context:) ) + case "InterfaceStatistics": + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeInterfaceStatisticsInterceptors() ?? [], + userFunction: self.interfaceStatistics(request:context:) + ) + case "Sync": return UnaryServerHandler( context: context, @@ -2254,6 +2328,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvide context: GRPCAsyncServerCallContext ) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse + /// Get statistics about an interface. + func interfaceStatistics( + request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse + /// Perform the sync syscall. func sync( request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, @@ -2502,6 +2582,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvider { wrapping: { try await self.configureHosts(request: $0, context: $1) } ) + case "InterfaceStatistics": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeInterfaceStatisticsInterceptors() ?? [], + wrapping: { try await self.interfaceStatistics(request: $0, context: $1) } + ) + case "Sync": return GRPCAsyncServerHandler( context: context, @@ -2624,6 +2713,10 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextServerInterc /// Defaults to calling `self.makeInterceptors()`. func makeConfigureHostsInterceptors() -> [ServerInterceptor] + /// - Returns: Interceptors to use when handling 'interfaceStatistics'. + /// Defaults to calling `self.makeInterceptors()`. + func makeInterfaceStatisticsInterceptors() -> [ServerInterceptor] + /// - Returns: Interceptors to use when handling 'sync'. /// Defaults to calling `self.makeInterceptors()`. func makeSyncInterceptors() -> [ServerInterceptor] @@ -2662,6 +2755,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata { Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.ipRouteAddDefault, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.configureDns, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.configureHosts, + Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.interfaceStatistics, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.sync, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.kill, ] @@ -2812,6 +2906,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata { type: GRPCCallType.unary ) + public static let interfaceStatistics = GRPCMethodDescriptor( + name: "InterfaceStatistics", + path: "/com.apple.containerization.sandbox.v3.SandboxContext/InterfaceStatistics", + type: GRPCCallType.unary + ) + public static let sync = GRPCMethodDescriptor( name: "Sync", path: "/com.apple.containerization.sandbox.v3.SandboxContext/Sync", diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index 602b5445..e58569af 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -954,6 +954,89 @@ public struct Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse: Send public init() {} } +public struct Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var interface: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var receivedPackets: UInt64 { + get {return _receivedPackets ?? 0} + set {_receivedPackets = newValue} + } + /// Returns true if `receivedPackets` has been explicitly set. + public var hasReceivedPackets: Bool {return self._receivedPackets != nil} + /// Clears the value of `receivedPackets`. Subsequent reads from it will return its default value. + public mutating func clearReceivedPackets() {self._receivedPackets = nil} + + public var transmittedPackets: UInt64 { + get {return _transmittedPackets ?? 0} + set {_transmittedPackets = newValue} + } + /// Returns true if `transmittedPackets` has been explicitly set. + public var hasTransmittedPackets: Bool {return self._transmittedPackets != nil} + /// Clears the value of `transmittedPackets`. Subsequent reads from it will return its default value. + public mutating func clearTransmittedPackets() {self._transmittedPackets = nil} + + public var receivedBytes: UInt64 { + get {return _receivedBytes ?? 0} + set {_receivedBytes = newValue} + } + /// Returns true if `receivedBytes` has been explicitly set. + public var hasReceivedBytes: Bool {return self._receivedBytes != nil} + /// Clears the value of `receivedBytes`. Subsequent reads from it will return its default value. + public mutating func clearReceivedBytes() {self._receivedBytes = nil} + + public var transmittedBytes: UInt64 { + get {return _transmittedBytes ?? 0} + set {_transmittedBytes = newValue} + } + /// Returns true if `transmittedBytes` has been explicitly set. + public var hasTransmittedBytes: Bool {return self._transmittedBytes != nil} + /// Clears the value of `transmittedBytes`. Subsequent reads from it will return its default value. + public mutating func clearTransmittedBytes() {self._transmittedBytes = nil} + + public var receivedErrors: UInt64 { + get {return _receivedErrors ?? 0} + set {_receivedErrors = newValue} + } + /// Returns true if `receivedErrors` has been explicitly set. + public var hasReceivedErrors: Bool {return self._receivedErrors != nil} + /// Clears the value of `receivedErrors`. Subsequent reads from it will return its default value. + public mutating func clearReceivedErrors() {self._receivedErrors = nil} + + public var transmittedErrors: UInt64 { + get {return _transmittedErrors ?? 0} + set {_transmittedErrors = newValue} + } + /// Returns true if `transmittedErrors` has been explicitly set. + public var hasTransmittedErrors: Bool {return self._transmittedErrors != nil} + /// Clears the value of `transmittedErrors`. Subsequent reads from it will return its default value. + public mutating func clearTransmittedErrors() {self._transmittedErrors = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _receivedPackets: UInt64? = nil + fileprivate var _transmittedPackets: UInt64? = nil + fileprivate var _receivedBytes: UInt64? = nil + fileprivate var _transmittedBytes: UInt64? = nil + fileprivate var _receivedErrors: UInt64? = nil + fileprivate var _transmittedErrors: UInt64? = nil +} + public struct Com_Apple_Containerization_Sandbox_V3_SyncRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -2781,6 +2864,104 @@ extension Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse: SwiftPro } } +extension Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".InterfaceStatisticsRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "interface"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.interface.isEmpty { + try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, rhs: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest) -> Bool { + if lhs.interface != rhs.interface {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".InterfaceStatisticsResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "receivedPackets"), + 2: .same(proto: "transmittedPackets"), + 3: .same(proto: "receivedBytes"), + 4: .same(proto: "transmittedBytes"), + 5: .same(proto: "receivedErrors"), + 6: .same(proto: "transmittedErrors"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self._receivedPackets) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self._transmittedPackets) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self._receivedBytes) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self._transmittedBytes) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self._receivedErrors) }() + case 6: try { try decoder.decodeSingularUInt64Field(value: &self._transmittedErrors) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._receivedPackets { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._transmittedPackets { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._receivedBytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 3) + } }() + try { if let v = self._transmittedBytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4) + } }() + try { if let v = self._receivedErrors { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 5) + } }() + try { if let v = self._transmittedErrors { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 6) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse, rhs: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse) -> Bool { + if lhs._receivedPackets != rhs._receivedPackets {return false} + if lhs._transmittedPackets != rhs._transmittedPackets {return false} + if lhs._receivedBytes != rhs._receivedBytes {return false} + if lhs._transmittedBytes != rhs._transmittedBytes {return false} + if lhs._receivedErrors != rhs._receivedErrors {return false} + if lhs._transmittedErrors != rhs._transmittedErrors {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Com_Apple_Containerization_Sandbox_V3_SyncRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SyncRequest" public static let _protobuf_nameMap = SwiftProtobuf._NameMap() diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index c25e8727..91b93bdc 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -58,6 +58,9 @@ service SandboxContext { rpc ConfigureDns(ConfigureDnsRequest) returns (ConfigureDnsResponse); // Configure /etc/hosts. rpc ConfigureHosts(ConfigureHostsRequest) returns (ConfigureHostsResponse); + // Get statistics about an interface. + rpc InterfaceStatistics(InterfaceStatisticsRequest) returns (InterfaceStatisticsResponse); + // Perform the sync syscall. rpc Sync(SyncRequest) returns (SyncResponse); // Send a signal to a process via the PID. @@ -273,6 +276,19 @@ message ConfigureHostsRequest { message ConfigureHostsResponse {} +message InterfaceStatisticsRequest { + string interface = 1; +} + +message InterfaceStatisticsResponse { + optional uint64 receivedPackets = 1; + optional uint64 transmittedPackets = 2; + optional uint64 receivedBytes = 3; + optional uint64 transmittedBytes = 4; + optional uint64 receivedErrors = 5; + optional uint64 transmittedErrors = 6; +} + message SyncRequest {} message SyncResponse {} diff --git a/Sources/Containerization/VirtualMachineAgent.swift b/Sources/Containerization/VirtualMachineAgent.swift index cc2c10be..58798e07 100644 --- a/Sources/Containerization/VirtualMachineAgent.swift +++ b/Sources/Containerization/VirtualMachineAgent.swift @@ -68,6 +68,7 @@ public protocol VirtualMachineAgent: Sendable { func routeAddDefault(name: String, gateway: String) async throws func configureDNS(config: DNS, location: String) async throws func configureHosts(config: Hosts, location: String) async throws + func interfaceStatistics(name: String) async throws -> InterfaceStatistics } extension VirtualMachineAgent { @@ -82,4 +83,8 @@ extension VirtualMachineAgent { public func writeFile(path: String, data: Data, flags: WriteFileFlags, mode: UInt32) async throws { throw ContainerizationError(.unsupported, message: "writeFile") } + + public func interfaceStatistics(name: String) async throws -> InterfaceStatistics { + throw ContainerizationError(.unsupported, message: "interfaceStatistics") + } } diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index ae4901ca..0d06e6fd 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -89,6 +89,23 @@ extension Vminitd: VirtualMachineAgent { }) } + /// Get statistics about an interface. + public func interfaceStatistics(name: String) async throws -> InterfaceStatistics { + let stats = try await client.interfaceStatistics( + .with { + $0.interface = name + }) + return InterfaceStatistics( + name: name, + receivedPackets: stats.hasReceivedPackets ? stats.receivedPackets : nil, + transmittedPackets: stats.hasTransmittedPackets ? stats.transmittedPackets : nil, + receivedBytes: stats.hasReceivedBytes ? stats.receivedBytes : nil, + transmittedBytes: stats.hasTransmittedBytes ? stats.transmittedBytes : nil, + receivedErrors: stats.hasReceivedErrors ? stats.receivedErrors : nil, + transmittedErrors: stats.hasTransmittedErrors ? stats.transmittedErrors : nil + ) + } + /// Mount a filesystem in the sandbox's environment. public func mount(_ mount: ContainerizationOCI.Mount) async throws { _ = try await client.mount( diff --git a/Sources/ContainerizationNetlink/NetlinkSession.swift b/Sources/ContainerizationNetlink/NetlinkSession.swift index 9beb839a..4d19dd75 100644 --- a/Sources/ContainerizationNetlink/NetlinkSession.swift +++ b/Sources/ContainerizationNetlink/NetlinkSession.swift @@ -121,7 +121,7 @@ public struct NetlinkSession { /// Performs a link get command on an interface. /// Returns information about the interface. /// - Parameter interface: The name of the interface to query. - public func linkGet(interface: String? = nil) throws -> [LinkResponse] { + public func linkGet(interface: String? = nil, includeStats: Bool = false) throws -> [LinkResponse] { // ip link ip show let maskAttr = RTAttribute( len: UInt16(RTAttribute.size + MemoryLayout.size), type: LinkAttributeType.IFLA_EXT_MASK) @@ -150,11 +150,16 @@ public struct NetlinkSession { change: InterfaceFlags.DEFAULT_CHANGE) requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset) + var filters = LinkAttributeMaskFilter.RTEXT_FILTER_VF + if !includeStats { + filters |= LinkAttributeMaskFilter.RTEXT_FILTER_SKIP_STATS + } + requestOffset = try maskAttr.appendBuffer(&requestBuffer, offset: requestOffset) guard var requestOffset = requestBuffer.copyIn( as: UInt32.self, - value: LinkAttributeMaskFilter.RTEXT_FILTER_VF | LinkAttributeMaskFilter.RTEXT_FILTER_SKIP_STATS, + value: filters, offset: requestOffset) else { throw NetlinkDataError.sendMarshalFailure @@ -167,7 +172,6 @@ public struct NetlinkSession { else { throw NetlinkDataError.sendMarshalFailure } - requestOffset = updatedRequestOffset } } diff --git a/Sources/ContainerizationNetlink/Types.swift b/Sources/ContainerizationNetlink/Types.swift index 306ae7b9..33e8efba 100644 --- a/Sources/ContainerizationNetlink/Types.swift +++ b/Sources/ContainerizationNetlink/Types.swift @@ -78,6 +78,7 @@ struct InterfaceFlags { struct LinkAttributeType { static let IFLA_EXT_IFNAME: UInt16 = 3 static let IFLA_MTU: UInt16 = 4 + static let IFLA_STATS64: UInt16 = 23 static let IFLA_EXT_MASK: UInt16 = 29 } @@ -568,6 +569,267 @@ public struct RTAttributeData { public struct LinkResponse { public let interfaceIndex: Int32 public let attrDatas: [RTAttributeData] + + /// Extract network interface statistics from the response attributes + public func getStatistics() throws -> LinkStatistics64? { + for attrData in attrDatas { + if attrData.attribute.type == LinkAttributeType.IFLA_STATS64 { + var stats = LinkStatistics64() + var buffer = attrData.data + _ = try stats.bindBuffer(&buffer, offset: 0) + return stats + } + } + return nil + } +} + +/// Network interface statistics (64-bit version) +public struct LinkStatistics64: Bindable { + static let size = 23 * 8 + + public var rxPackets: UInt64 + public var txPackets: UInt64 + public var rxBytes: UInt64 + public var txBytes: UInt64 + public var rxErrors: UInt64 + public var txErrors: UInt64 + public var rxDropped: UInt64 + public var txDropped: UInt64 + public var multicast: UInt64 + public var collisions: UInt64 + public var rxLengthErrors: UInt64 + public var rxOverErrors: UInt64 + public var rxCrcErrors: UInt64 + public var rxFrameErrors: UInt64 + public var rxFifoErrors: UInt64 + public var rxMissedErrors: UInt64 + public var txAbortedErrors: UInt64 + public var txCarrierErrors: UInt64 + public var txFifoErrors: UInt64 + public var txHeartbeatErrors: UInt64 + public var txWindowErrors: UInt64 + public var rxCompressed: UInt64 + public var txCompressed: UInt64 + + public init() { + self.rxPackets = 0 + self.txPackets = 0 + self.rxBytes = 0 + self.txBytes = 0 + self.rxErrors = 0 + self.txErrors = 0 + self.rxDropped = 0 + self.txDropped = 0 + self.multicast = 0 + self.collisions = 0 + self.rxLengthErrors = 0 + self.rxOverErrors = 0 + self.rxCrcErrors = 0 + self.rxFrameErrors = 0 + self.rxFifoErrors = 0 + self.rxMissedErrors = 0 + self.txAbortedErrors = 0 + self.txCarrierErrors = 0 + self.txFifoErrors = 0 + self.txHeartbeatErrors = 0 + self.txWindowErrors = 0 + self.rxCompressed = 0 + self.txCompressed = 0 + } + + func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int { + guard let offset = buffer.copyIn(as: UInt64.self, value: rxPackets, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txPackets, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxBytes, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txBytes, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxDropped, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txDropped, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: multicast, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: collisions, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxLengthErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxOverErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxCrcErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxFrameErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxFifoErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxMissedErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txAbortedErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txCarrierErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txFifoErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txHeartbeatErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txWindowErrors, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: rxCompressed, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + guard let offset = buffer.copyIn(as: UInt64.self, value: txCompressed, offset: offset) else { + throw NetlinkDataError.sendMarshalFailure + } + + return offset + } + + mutating func bindBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int { + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxPackets = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txPackets = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxBytes = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txBytes = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxDropped = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txDropped = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + multicast = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + collisions = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxLengthErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxOverErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxCrcErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxFrameErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxFifoErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxMissedErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txAbortedErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txCarrierErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txFifoErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txHeartbeatErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txWindowErrors = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + rxCompressed = value + + guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: offset) else { + throw NetlinkDataError.recvUnmarshalFailure + } + txCompressed = value + + return offset + } } /// Errors thrown when parsing netlink data. diff --git a/vminitd/Sources/vminitd/Server+GRPC.swift b/vminitd/Sources/vminitd/Server+GRPC.swift index a349fa36..a7d6f267 100644 --- a/vminitd/Sources/vminitd/Server+GRPC.swift +++ b/vminitd/Sources/vminitd/Server+GRPC.swift @@ -907,6 +907,47 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid return .init() } + func interfaceStatistics( + request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, + context: GRPC.GRPCAsyncServerCallContext + ) async throws -> Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse { + log.debug( + "interfaceStatistics", + metadata: [ + "name": "\(request.interface)" + ]) + + do { + let socket = try DefaultNetlinkSocket() + let session = NetlinkSession(socket: socket, log: log) + let responses = try session.linkGet(interface: request.interface, includeStats: true) + guard responses.count == 1 else { + throw ContainerizationError( + .internalError, + message: "linkGet returned invalid number of interfaces: \(responses.count)" + ) + } + let stats = try responses[0].getStatistics() + return .with { + if let stats { + $0.receivedPackets = stats.rxPackets + $0.transmittedPackets = stats.txPackets + $0.receivedBytes = stats.rxBytes + $0.transmittedBytes = stats.txBytes + $0.receivedErrors = stats.rxErrors + $0.transmittedErrors = stats.txErrors + } + } + } catch { + log.error( + "interfaceStatistics", + metadata: [ + "error": "\(error)" + ]) + throw GRPCStatus(code: .internalError, message: "interfaceStatistics: \(error)") + } + } + private func swiftErrno(_ msg: Logger.Message) -> POSIXError { let error = POSIXError(.init(rawValue: errno)!) log.error(