diff --git a/Sources/Containerization/ContainerStatistics.swift b/Sources/Containerization/ContainerStatistics.swift index a7e0995ff..7a89b138b 100644 --- a/Sources/Containerization/ContainerStatistics.swift +++ b/Sources/Containerization/ContainerStatistics.swift @@ -182,6 +182,9 @@ public struct ContainerStatistics: Sendable { public var transmittedBytes: UInt64 public var receivedErrors: UInt64 public var transmittedErrors: UInt64 + /// Packets the device dropped (e.g. full rx ring or tx queue). + public var receivedDropped: UInt64 + public var transmittedDropped: UInt64 public init( interface: String, @@ -190,7 +193,9 @@ public struct ContainerStatistics: Sendable { receivedBytes: UInt64, transmittedBytes: UInt64, receivedErrors: UInt64, - transmittedErrors: UInt64 + transmittedErrors: UInt64, + receivedDropped: UInt64, + transmittedDropped: UInt64 ) { self.interface = interface self.receivedPackets = receivedPackets @@ -199,6 +204,8 @@ public struct ContainerStatistics: Sendable { self.transmittedBytes = transmittedBytes self.receivedErrors = receivedErrors self.transmittedErrors = transmittedErrors + self.receivedDropped = receivedDropped + self.transmittedDropped = transmittedDropped } } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index 173d1b11a..e2787b7f4 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -1743,6 +1743,11 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_NetworkStats: Se public var transmittedErrors: UInt64 = 0 + /// Packets the device dropped (e.g. full rx ring or tx queue), from rtnl_link_stats64. + public var receivedDropped: UInt64 = 0 + + public var transmittedDropped: UInt64 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -4670,7 +4675,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_BlockIOEntry: SwiftP nonisolated extension Com_Apple_Containerization_Sandbox_V3_NetworkStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NetworkStats" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}receivedPackets\0\u{1}transmittedPackets\0\u{1}receivedBytes\0\u{1}transmittedBytes\0\u{1}receivedErrors\0\u{1}transmittedErrors\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}receivedPackets\0\u{1}transmittedPackets\0\u{1}receivedBytes\0\u{1}transmittedBytes\0\u{1}receivedErrors\0\u{1}transmittedErrors\0\u{1}receivedDropped\0\u{1}transmittedDropped\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4685,6 +4690,8 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_NetworkStats: SwiftP case 5: try { try decoder.decodeSingularUInt64Field(value: &self.transmittedBytes) }() case 6: try { try decoder.decodeSingularUInt64Field(value: &self.receivedErrors) }() case 7: try { try decoder.decodeSingularUInt64Field(value: &self.transmittedErrors) }() + case 8: try { try decoder.decodeSingularUInt64Field(value: &self.receivedDropped) }() + case 9: try { try decoder.decodeSingularUInt64Field(value: &self.transmittedDropped) }() default: break } } @@ -4712,6 +4719,12 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_NetworkStats: SwiftP if self.transmittedErrors != 0 { try visitor.visitSingularUInt64Field(value: self.transmittedErrors, fieldNumber: 7) } + if self.receivedDropped != 0 { + try visitor.visitSingularUInt64Field(value: self.receivedDropped, fieldNumber: 8) + } + if self.transmittedDropped != 0 { + try visitor.visitSingularUInt64Field(value: self.transmittedDropped, fieldNumber: 9) + } try unknownFields.traverse(visitor: &visitor) } @@ -4723,6 +4736,8 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_NetworkStats: SwiftP if lhs.transmittedBytes != rhs.transmittedBytes {return false} if lhs.receivedErrors != rhs.receivedErrors {return false} if lhs.transmittedErrors != rhs.transmittedErrors {return false} + if lhs.receivedDropped != rhs.receivedDropped {return false} + if lhs.transmittedDropped != rhs.transmittedDropped {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index a7c6cf418..2c6b311a5 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -485,6 +485,9 @@ message NetworkStats { uint64 transmittedBytes = 5; uint64 receivedErrors = 6; uint64 transmittedErrors = 7; + // Packets the device dropped (e.g. full rx ring or tx queue), from rtnl_link_stats64. + uint64 receivedDropped = 8; + uint64 transmittedDropped = 9; } // Memory event counters from cgroup2's memory.events file. diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index 9bf7c080d..f2ab58dcd 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -167,7 +167,9 @@ extension Vminitd: VirtualMachineAgent { receivedBytes: network.receivedBytes, transmittedBytes: network.transmittedBytes, receivedErrors: network.receivedErrors, - transmittedErrors: network.transmittedErrors + transmittedErrors: network.transmittedErrors, + receivedDropped: network.receivedDropped, + transmittedDropped: network.transmittedDropped ) } : nil, memoryEvents: categories.contains(.memoryEvents) && protoStats.hasMemoryEvents diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 6e05ff5ef..bc0c06dcc 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -5146,6 +5146,71 @@ extension IntegrationSuite { } } + @available(macOS 26.0, *) + func testContainerNetworkInterfaceStatistics() async throws { + let id = "test-container-network-interface-statistics" + let bs = try await bootstrap(id) + + let network = try VmnetNetwork() + var manager = try ContainerManager(vmm: bs.vmm, network: network) + defer { + try? manager.delete(id) + } + + let container = try await manager.create( + id, + image: bs.image, + rootfs: bs.rootfs + ) { config in + config.process.arguments = ["sleep", "infinity"] + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + func eth0(_ stats: ContainerStatistics) throws -> ContainerStatistics.NetworkStatistics { + guard let eth0 = stats.networks?.first(where: { $0.interface == "eth0" }) else { + throw IntegrationError.assert(msg: "expected eth0 in network statistics, got \(stats.networks?.map(\.interface) ?? [])") + } + return eth0 + } + + let before = try eth0(try await container.statistics(categories: .network)) + + // Generate some traffic across the interface. The gateway may not answer + // ping, so the exit code is ignored; the packets still go out eth0. + let exec = try await container.exec("ping-gateway") { config in + config.arguments = ["sh", "-c", "ping -c 3 -W 1 $(ip route | awk '/default/ {print $3; exit}'); true"] + } + try await exec.start() + _ = try await exec.wait() + try await exec.delete() + + let after = try eth0(try await container.statistics(categories: .network)) + + guard after.transmittedPackets > before.transmittedPackets else { + throw IntegrationError.assert(msg: "expected eth0 transmittedPackets to grow, got \(before.transmittedPackets) -> \(after.transmittedPackets)") + } + + // Drop counters are cumulative, so they must never go backwards. + guard after.receivedDropped >= before.receivedDropped, after.transmittedDropped >= before.transmittedDropped else { + throw IntegrationError.assert( + msg: + "eth0 dropped counters decreased: rx \(before.receivedDropped)->\(after.receivedDropped) tx \(before.transmittedDropped)->\(after.transmittedDropped)" + ) + } + + try await container.kill(.kill) + try await container.wait() + try await container.stop() + } catch { + try? await container.stop() + throw error + } + } + @available(macOS 26.0, *) func testNetworkingEnabledIPv6() async throws { let id = "test-networking-enabled-ipv6" diff --git a/Sources/Integration/Suite.swift b/Sources/Integration/Suite.swift index 8e628cfee..4fc80ff0e 100644 --- a/Sources/Integration/Suite.swift +++ b/Sources/Integration/Suite.swift @@ -446,6 +446,7 @@ struct IntegrationSuite: AsyncParsableCommand { Test("container interface custom MTU", testInterfaceMTU), Test("container networking disabled", testNetworkingDisabled), Test("container networking enabled", testNetworkingEnabled), + Test("container network interface statistics", testContainerNetworkInterfaceStatistics), Test("container networking enabled ipv6", testNetworkingEnabledIPv6), Test("container IPv6 address", testIPv6AddressAdd), Test("container IPv6 default route", testIPv6DefaultRoute), diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index b3b02927b..ca2aeb283 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -1713,6 +1713,8 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ $0.transmittedBytes = stats.txBytes $0.receivedErrors = stats.rxErrors $0.transmittedErrors = stats.txErrors + $0.receivedDropped = stats.rxDropped + $0.transmittedDropped = stats.txDropped }) } }