mirror of
https://github.com/apple/container.git
synced 2026-09-26 01:25:52 +00:00
Handle multiple messages in netlink responses. (#460)
- Closes #459. - Loop in `parseResponse()` until all data is consumed. - Add properties to LinkResponse for `isEthernet`, `isLoopback`, `address`.
This commit is contained in:
@@ -127,7 +127,7 @@ public struct NetlinkSession {
|
||||
len: UInt16(RTAttribute.size + MemoryLayout<UInt32>.size), type: LinkAttributeType.IFLA_EXT_MASK)
|
||||
let interfaceName = try interface.map { try getInterfaceName($0) }
|
||||
let interfaceNameAttr = interfaceName.map {
|
||||
RTAttribute(len: UInt16(RTAttribute.size + $0.count), type: LinkAttributeType.IFLA_EXT_IFNAME)
|
||||
RTAttribute(len: UInt16(RTAttribute.size + $0.count), type: LinkAttributeType.IFLA_IFNAME)
|
||||
}
|
||||
let requestSize =
|
||||
NetlinkMessageHeader.size + InterfaceInfo.size + maskAttr.paddedLen + (interfaceNameAttr?.paddedLen ?? 0)
|
||||
@@ -184,7 +184,13 @@ public struct NetlinkSession {
|
||||
let (infos, attrDataLists) = try parseResponse(infoType: NetlinkType.RTM_NEWLINK) { InterfaceInfo() }
|
||||
var linkResponses: [LinkResponse] = []
|
||||
for i in 0..<infos.count {
|
||||
linkResponses.append(LinkResponse(interfaceIndex: infos[i].index, attrDatas: attrDataLists[i]))
|
||||
linkResponses.append(
|
||||
LinkResponse(
|
||||
interfaceIndex: infos[i].index,
|
||||
interfaceFlags: infos[i].flags,
|
||||
interfaceType: infos[i].type,
|
||||
attrDatas: attrDataLists[i])
|
||||
)
|
||||
}
|
||||
|
||||
return linkResponses
|
||||
@@ -446,38 +452,50 @@ public struct NetlinkSession {
|
||||
var moreResponses = false
|
||||
repeat {
|
||||
var (buffer, size) = try receiveResponse()
|
||||
let header: NetlinkMessageHeader
|
||||
var offset = 0
|
||||
|
||||
(header, offset) = try parseHeader(buffer: &buffer, offset: offset)
|
||||
if let infoType {
|
||||
if header.type == infoType {
|
||||
log.trace(
|
||||
"RECV-INFO-DUMP: dump = \(buffer[offset..<offset + InterfaceInfo.size].hexEncodedString())")
|
||||
var info = infoProvider()
|
||||
offset = try info.bindBuffer(&buffer, offset: offset)
|
||||
log.trace("RECV-INFO: \(info)")
|
||||
// A single buffer may contain multiple netlink messages
|
||||
while offset < size {
|
||||
let messageStart = offset
|
||||
let header: NetlinkMessageHeader
|
||||
(header, offset) = try parseHeader(buffer: &buffer, offset: offset)
|
||||
|
||||
let attrDatas: [RTAttributeData]
|
||||
(attrDatas, offset) = try parseAttributes(
|
||||
buffer: &buffer,
|
||||
offset: offset,
|
||||
residualCount: size - offset)
|
||||
if let infoType {
|
||||
if header.type == infoType {
|
||||
log.trace(
|
||||
"RECV-INFO-DUMP: dump = \(buffer[offset..<offset + InterfaceInfo.size].hexEncodedString())")
|
||||
var info = infoProvider()
|
||||
offset = try info.bindBuffer(&buffer, offset: offset)
|
||||
log.trace("RECV-INFO: \(info)")
|
||||
|
||||
infos.append(info)
|
||||
attrDataLists.append(attrDatas)
|
||||
// Calculate the number of bytes remaining in THIS message
|
||||
let messageEnd = messageStart + Int(header.len)
|
||||
let attributeBytes = messageEnd - offset
|
||||
|
||||
let attrDatas: [RTAttributeData]
|
||||
(attrDatas, offset) = try parseAttributes(
|
||||
buffer: &buffer,
|
||||
offset: offset,
|
||||
residualCount: attributeBytes)
|
||||
|
||||
infos.append(info)
|
||||
attrDataLists.append(attrDatas)
|
||||
} else {
|
||||
// Skip this message - advance offset to the end of the message
|
||||
offset = messageStart + Int(header.len)
|
||||
}
|
||||
} else if header.type != NetlinkType.NLMSG_DONE && header.type != NetlinkType.NLMSG_ERROR
|
||||
&& header.type != NetlinkType.NLMSG_NOOP
|
||||
{
|
||||
throw Error.unexpectedInfo(type: header.type)
|
||||
}
|
||||
} else if header.type != NetlinkType.NLMSG_DONE && header.type != NetlinkType.NLMSG_ERROR
|
||||
&& header.type != NetlinkType.NLMSG_NOOP
|
||||
{
|
||||
throw Error.unexpectedInfo(type: header.type)
|
||||
|
||||
moreResponses = header.moreResponses
|
||||
}
|
||||
|
||||
guard offset == size else {
|
||||
throw Error.unexpectedOffset(offset: offset, size: size)
|
||||
}
|
||||
|
||||
moreResponses = header.moreResponses
|
||||
} while moreResponses
|
||||
|
||||
return (infos, attrDataLists)
|
||||
|
||||
@@ -29,6 +29,10 @@ struct AddressFamily {
|
||||
static let AF_PACKET: UInt16 = 17
|
||||
}
|
||||
|
||||
struct ArpHardware {
|
||||
static let ARPHRD_ETHER: UInt16 = 1
|
||||
}
|
||||
|
||||
struct NetlinkProtocol {
|
||||
static let NETLINK_ROUTE: Int32 = 0
|
||||
}
|
||||
@@ -72,11 +76,15 @@ struct NetlinkScope {
|
||||
|
||||
struct InterfaceFlags {
|
||||
static let IFF_UP: UInt32 = 1 << 0
|
||||
static let IFF_LOOPBACK: UInt32 = 1 << 3
|
||||
static let IFF_POINTOPOINT: UInt32 = 1 << 4
|
||||
static let DEFAULT_CHANGE: UInt32 = 0xffff_ffff
|
||||
}
|
||||
|
||||
struct LinkAttributeType {
|
||||
static let IFLA_EXT_IFNAME: UInt16 = 3
|
||||
static let IFLA_ADDRESS: UInt16 = 1
|
||||
static let IFLA_BROADCAST: UInt16 = 2
|
||||
static let IFLA_IFNAME: UInt16 = 3
|
||||
static let IFLA_MTU: UInt16 = 4
|
||||
static let IFLA_STATS64: UInt16 = 23
|
||||
static let IFLA_EXT_MASK: UInt16 = 29
|
||||
@@ -568,8 +576,26 @@ public struct RTAttributeData {
|
||||
/// A response from the get link command.
|
||||
public struct LinkResponse {
|
||||
public let interfaceIndex: Int32
|
||||
public let interfaceFlags: UInt32
|
||||
public let interfaceType: UInt16
|
||||
public let attrDatas: [RTAttributeData]
|
||||
|
||||
public var isLoopback: Bool {
|
||||
(interfaceFlags & InterfaceFlags.IFF_LOOPBACK) != 0
|
||||
}
|
||||
|
||||
public var isEthernet: Bool {
|
||||
interfaceType == ArpHardware.ARPHRD_ETHER
|
||||
&& (interfaceFlags & (InterfaceFlags.IFF_LOOPBACK | InterfaceFlags.IFF_POINTOPOINT)) == 0
|
||||
}
|
||||
|
||||
public var address: [UInt8]? {
|
||||
attrDatas
|
||||
.filter { $0.attribute.type == LinkAttributeType.IFLA_ADDRESS }
|
||||
.first
|
||||
.map { $0.data }
|
||||
}
|
||||
|
||||
/// Extract network interface statistics from the response attributes
|
||||
public func getStatistics() throws -> LinkStatistics64? {
|
||||
for attrData in attrDatas {
|
||||
|
||||
@@ -157,11 +157,12 @@ struct NetlinkSessionTest {
|
||||
mockSocket.responses.append(
|
||||
[UInt8](
|
||||
hex:
|
||||
"3c000000100000000000000078563412" // Netlink header (16 B)
|
||||
"48000000100000000000000078563412" // Netlink header (16 B)
|
||||
+ "00000100020000004300010000000000" // struct ifinfomsg (16 B)
|
||||
+ "090003006574683000000000" // IFLA_IFNAME (“eth0”) attr (12 B)
|
||||
+ "08000d00e8030000" // IFLA_MTU = 1000 attr (8 B)
|
||||
+ "0500100006000000" // attr type 0x0010 (8 B)
|
||||
+ "0a000100825524c244030000" // IFLA_ADDRESS = 82:55:24:c2:44:03 (12 B)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -175,7 +176,12 @@ struct NetlinkSessionTest {
|
||||
try #require(links.count == 1)
|
||||
|
||||
#expect(links[0].interfaceIndex == 2)
|
||||
try #require(links[0].attrDatas.count == 3)
|
||||
#expect(links[0].interfaceFlags == 0x0001_0043)
|
||||
#expect(links[0].interfaceType == 1)
|
||||
#expect(links[0].isEthernet)
|
||||
#expect(!links[0].isLoopback)
|
||||
#expect(links[0].address == [0x82, 0x55, 0x24, 0xc2, 0x44, 0x03])
|
||||
try #require(links[0].attrDatas.count == 4)
|
||||
#expect(links[0].attrDatas[0].attribute.type == 0x0003)
|
||||
#expect(links[0].attrDatas[0].attribute.len == 0x0009)
|
||||
#expect(links[0].attrDatas[0].data == [0x65, 0x74, 0x68, 0x30, 0x00])
|
||||
@@ -292,7 +298,7 @@ struct NetlinkSessionTest {
|
||||
let expectedLookupRequest =
|
||||
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
|
||||
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
|
||||
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME (“eth0”)
|
||||
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0")
|
||||
mockSocket.responses.append(
|
||||
[UInt8](
|
||||
hex:
|
||||
@@ -333,6 +339,85 @@ struct NetlinkSessionTest {
|
||||
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
|
||||
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
|
||||
}
|
||||
|
||||
@Test func testNetworkLinkGetMultipleMessagesInSingleBuffer() throws {
|
||||
let mockSocket = try MockNetlinkSocket()
|
||||
mockSocket.pid = 0x8765_4321
|
||||
|
||||
// Lookup all interfaces, with multiple messages packed into a single buffer.
|
||||
// This tests the fix for parsing multiple netlink messages that arrive in one recv() call.
|
||||
let expectedLookupRequest =
|
||||
"28000000120001030000000021436587" // Netlink header (16 B)
|
||||
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
|
||||
+ "08001d0009000000" // RT attr: IFLA_EXT_MASK (8 B)
|
||||
|
||||
// Pack three messages into a single response buffer:
|
||||
// Message 1: loopback interface with one attribute
|
||||
let msg1 =
|
||||
"28000000100002000000000021436587" // Netlink header (16 B), len=40
|
||||
+ "00000403010000004900010000000000" // struct ifinfomsg (16 B)
|
||||
+ "070003006c6f0000" // IFLA_IFNAME "lo" (8 B, padded)
|
||||
|
||||
// Message 2: tunl0 interface with one attribute
|
||||
let msg2 =
|
||||
"2c000000100002000000000021436587" // Netlink header (16 B), len=44
|
||||
+ "00000003040000008000000000000000" // struct ifinfomsg (16 B)
|
||||
+ "0a00030074756e6c30000000" // IFLA_IFNAME "tunl0" attr (12 B, padded)
|
||||
|
||||
// Message 3: eth0 interface with two attributes
|
||||
let msg3 =
|
||||
"34000000100002000000000021436587" // Netlink header (16 B), len=52
|
||||
+ "00000100020000004300010000000000" // struct ifinfomsg (16 B)
|
||||
+ "090003006574683000000000" // IFLA_IFNAME "eth0" attr (12 B)
|
||||
+ "08000d00e8030000" // IFLA_MTU = 1000 attr (8 B)
|
||||
|
||||
// Combine all three messages into a single buffer
|
||||
mockSocket.responses.append([UInt8](hex: msg1 + msg2 + msg3))
|
||||
|
||||
// Final NLMSG_DONE message in separate buffer
|
||||
mockSocket.responses.append(
|
||||
[UInt8](
|
||||
hex:
|
||||
"14000000030002000000000021436587" // Netlink header (16 B) – NLMSG_DONE
|
||||
+ "00000000" // 4-byte payload
|
||||
)
|
||||
)
|
||||
|
||||
let session = NetlinkSession(socket: mockSocket)
|
||||
let links = try session.linkGet()
|
||||
|
||||
#expect(mockSocket.requests.count == 1)
|
||||
#expect(mockSocket.responseIndex == 2)
|
||||
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
|
||||
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
|
||||
|
||||
// Verify we got all three interfaces
|
||||
try #require(links.count == 3)
|
||||
|
||||
// Verify loopback interface
|
||||
#expect(links[0].interfaceIndex == 1)
|
||||
try #require(links[0].attrDatas.count == 1)
|
||||
#expect(links[0].attrDatas[0].attribute.type == 0x0003)
|
||||
#expect(links[0].attrDatas[0].attribute.len == 0x0007)
|
||||
#expect(links[0].attrDatas[0].data == [0x6c, 0x6f, 0x00])
|
||||
|
||||
// Verify tunl0 interface
|
||||
#expect(links[1].interfaceIndex == 4)
|
||||
try #require(links[1].attrDatas.count == 1)
|
||||
#expect(links[1].attrDatas[0].attribute.type == 0x0003)
|
||||
#expect(links[1].attrDatas[0].attribute.len == 0x000a)
|
||||
#expect(links[1].attrDatas[0].data == [0x74, 0x75, 0x6e, 0x6c, 0x30, 0x00])
|
||||
|
||||
// Verify eth0 interface
|
||||
#expect(links[2].interfaceIndex == 2)
|
||||
try #require(links[2].attrDatas.count == 2)
|
||||
#expect(links[2].attrDatas[0].attribute.type == 0x0003)
|
||||
#expect(links[2].attrDatas[0].attribute.len == 0x0009)
|
||||
#expect(links[2].attrDatas[0].data == [0x65, 0x74, 0x68, 0x30, 0x00])
|
||||
#expect(links[2].attrDatas[1].attribute.type == 0x000d)
|
||||
#expect(links[2].attrDatas[1].attribute.len == 0x0008)
|
||||
#expect(links[2].attrDatas[1].data == [0xe8, 0x03, 0x00, 0x00])
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == UInt8 {
|
||||
|
||||
Reference in New Issue
Block a user