Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 44 additions & 26 deletions Sources/ContainerizationNetlink/NetlinkSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)")

let attrDatas: [RTAttributeData]
(attrDatas, offset) = try parseAttributes(
buffer: &buffer,
offset: offset,
residualCount: size - offset)

infos.append(info)
attrDataLists.append(attrDatas)
// 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)

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)")

// 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)
Expand Down
28 changes: 27 additions & 1 deletion Sources/ContainerizationNetlink/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
91 changes: 88 additions & 3 deletions Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)

Expand All @@ -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])
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down