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.
This commit is contained in:
Danny Canter
2025-10-03 12:53:35 -07:00
committed by GitHub
parent f9ae8f7801
commit 80e2813bcf
9 changed files with 673 additions and 3 deletions
@@ -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
}
}
@@ -154,6 +154,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtoc
callOptions: CallOptions?
) -> UnaryCall<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>
func interfaceStatistics(
_ request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest,
callOptions: CallOptions?
) -> UnaryCall<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse>
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<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse> {
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<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>
func makeInterfaceStatisticsCall(
_ request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse>
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<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse> {
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<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>]
/// - Returns: Interceptors to use when invoking 'interfaceStatistics'.
func makeInterfaceStatisticsInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse>]
/// - Returns: Interceptors to use when invoking 'sync'.
func makeSyncInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_SyncRequest, Com_Apple_Containerization_Sandbox_V3_SyncResponse>]
@@ -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<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>
/// Get statistics about an interface.
func interfaceStatistics(request: Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse>
/// Perform the sync syscall.
func sync(request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_SyncResponse>
@@ -2077,6 +2142,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider {
userFunction: self.configureHosts(request:context:)
)
case "InterfaceStatistics":
return UnaryServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse>(),
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<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse>(),
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<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>]
/// - Returns: Interceptors to use when handling 'interfaceStatistics'.
/// Defaults to calling `self.makeInterceptors()`.
func makeInterfaceStatisticsInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsRequest, Com_Apple_Containerization_Sandbox_V3_InterfaceStatisticsResponse>]
/// - Returns: Interceptors to use when handling 'sync'.
/// Defaults to calling `self.makeInterceptors()`.
func makeSyncInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_SyncRequest, Com_Apple_Containerization_Sandbox_V3_SyncResponse>]
@@ -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",
@@ -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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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()
@@ -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 {}
@@ -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")
}
}
+17
View File
@@ -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(
@@ -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<UInt32>.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
}
}
+262
View File
@@ -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.
+41
View File
@@ -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(