chore: import upstream snapshot with attribution
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:30 +08:00
commit 680845cb1c
445 changed files with 103779 additions and 0 deletions
@@ -0,0 +1,57 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
//
@testable import ContainerizationNetlink
class MockNetlinkSocket: NetlinkSocket {
static let ENOMEM: Int32 = 12
static let EOVERFLOW: Int32 = 75
var pid: UInt32 = 0
var requests: [[UInt8]] = []
var responses: [[UInt8]] = []
var responseIndex = 0
public init() throws {}
public func send(buf: UnsafeRawPointer!, len: Int, flags: Int32) throws -> Int {
let ptr = buf.bindMemory(to: UInt8.self, capacity: len)
requests.append(Array(UnsafeBufferPointer(start: ptr, count: len)))
return len
}
public func recv(buf: UnsafeMutableRawPointer!, len: Int, flags: Int32) throws -> Int {
guard responseIndex < responses.count else {
throw NetlinkSocketError.recvFailure(rc: Self.ENOMEM)
}
let response = responses[responseIndex]
guard len >= response.count else {
throw NetlinkSocketError.recvFailure(rc: 75)
}
response.withUnsafeBytes { bytes in
buf.copyMemory(from: bytes.baseAddress!, byteCount: response.count)
}
responseIndex += 1
return response.count
}
}
@@ -0,0 +1,665 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
import ContainerizationExtras
import ContainerizationOS
import Testing
@testable import ContainerizationNetlink
struct NetlinkSessionTest {
@Test func testNetworkLinkDown() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no RT attrs
)
)
// Linkdown request 32byte payload, no attributes.
let expectedDownRequest =
"2000000010000500000000000cc00cc0" // Netlink header (16 B)
+ "110000000200000000000000ffffffff" // struct ifinfomsg (16 B) no RT attrs
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000200000001000050000000000" // nlmsg_err payload (16 B)
+ "0c000000" // first 4 B of echoed header
)
)
let session = NetlinkSession(socket: mockSocket)
try session.linkSet(interface: "eth0", up: false)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
#expect(expectedDownRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkLinkUp() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0x0cc0_0cc0
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"340000001200010000000000c00cc00c" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"200000001000000000000000c00cc00c" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Network up for interface.
let expectedUpRequest =
"280000001000050000000000c00cc00c" // Netlink header (16 B)
+ "110000000200000001000000ffffffff" // struct ifinfomsg (16 B)
+ "0800040000050000" // RT attr: IFLA_MTU = 1280 (8 B)
mockSocket.responses.append(
[UInt8](
hex:
"240000000200000100000000c00cc00c" // Netlink header (16 B)
+ "00000000200000001000050000000000" // nlmsg_err payload (16 B)
+ "11000000" // 1st 4 B of echoed offending header
)
)
let session = NetlinkSession(socket: mockSocket)
try session.linkSet(interface: "eth0", up: true, mtu: 1280)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
#expect(expectedUpRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkLinkUpLoopback() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup loopback interface
let expectedLookupRequest =
"3000000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d0009000000080003006c6f0000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME (lo)
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100010000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Link up request for loopback, 32byte payload and no attributes
let expectedUpRequest =
"2000000010000500000000000cc00cc0" // Netlink header (16 B)
+ "110000000100000001000000ffffffff" // struct ifinfomsg (16 B) no RT attrs
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000200000001000050000000000" // nlmsg_err payload (16 B)
+ "0c000000" // first 4 B of echoed offending header
)
)
let session = NetlinkSession(socket: mockSocket)
try session.linkSet(interface: "lo", up: true)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
#expect(expectedUpRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkLinkGetEth0() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0x1234_5678
// Lookup interface by name, truncated response with three attributes.
let expectedLookupRequest =
"34000000120001000000000078563412" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"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)
)
)
let session = NetlinkSession(socket: mockSocket)
let links = try session.linkGet(interface: "eth0")
#expect(mockSocket.requests.count == 1)
#expect(mockSocket.responseIndex == 1)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
try #require(links.count == 1)
#expect(links[0].interfaceIndex == 2)
#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])
#expect(links[0].attrDatas[1].attribute.type == 0x000d)
#expect(links[0].attrDatas[1].attribute.len == 0x0008)
#expect(links[0].attrDatas[1].data == [0xe8, 0x03, 0x00, 0x00])
#expect(links[0].attrDatas[2].attribute.type == 0x0010)
#expect(links[0].attrDatas[2].attribute.len == 0x0005)
#expect(links[0].attrDatas[2].data == [0x06])
}
@Test func testNetworkLinkGet() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0x8765_4321
// Lookup all interfaces, responses with only the interface name attribute.
let expectedLookupRequest =
"28000000120001030000000021436587" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d0009000000" // RT attr: IFLA_EXT_MASK (8 B)
mockSocket.responses.append(
[UInt8](
hex:
"28000000100002000000000021436587" // Netlink header (16 B)
+ "00000403010000004900010000000000" // struct ifinfomsg (16 B)
+ "070003006c6f0000" // IFLA_IFNAME lo (8 B, padded)
)
)
mockSocket.responses.append(
[UInt8](
hex:
"2c000000100002000000000021436587" // Netlink header (16 B)
+ "00000003040000008000000000000000" // struct ifinfomsg (16 B)
+ "0a00030074756e6c30000000" // IFLA_IFNAME tunl0 attr (12 B, padded)
)
)
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 == 3)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
try #require(links.count == 2)
#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])
#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])
}
@Test func testNetworkAddressAdd() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Network down for interface.
let expectedAddRequest =
"2800000014000506000000000cc00cc0" // Netlink header (16 B)
+ "0218000002000000" // ifaddrmsg (8 B): AF_INET, /24, ifindex 2
+ "08000200c0a840fa" // RT attr: IFA_LOCAL 192.168.64.250
+ "08000100c0a840fa" // RT attr: IFA_ADDRESS 192.168.64.250
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000280000001400050600000000" // nlmsg_err payload (16 B)
+ "1f000000" // first 4 B of echoed offending header
)
)
let session = NetlinkSession(socket: mockSocket)
try session.addressAdd(interface: "eth0", ipv4Address: try CIDRv4("192.168.64.250/24"))
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkAddressAddIPv6() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0")
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Add IPv6 address to interface.
let expectedAddRequest =
"2c00000014000506000000000cc00cc0" // Netlink header (16 B): len=44
+ "0a40820002000000" // ifaddrmsg (8 B): AF_INET6, /64, flags=PERMANENT|NODAD, ifindex 2
+ "14000100fd000000000000000000000000000001" // RT attr: IFA_ADDRESS fd00::1
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "0000000040000000140005060000000000000000" // nlmsg_err payload (20 B)
)
)
let session = NetlinkSession(socket: mockSocket)
try session.addressAdd(interface: "eth0", ipv6Address: try CIDRv6("fd00::1/64"))
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkRouteAddIpLink() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0")
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Add link route.
let expectedAddRequest =
"3400000018000506000000000cc00cc0" // Netlink header (16 B)
+ "02180000fe02fd0100000000" // struct rtmsg (12 B): AF_INET, dst/24,
// table=RT_TABLE_MAIN (0xfe), proto=RTPROT_KERNEL (0x02),
// scope=RT_SCOPE_LINK (0xfd), type=RTN_UNICAST (0x01)
+ "08000100c0a84000" // RTA_DST 192.168.64.0
+ "08000700c0a84003" // RTA_PREFSRC 192.168.64.3
+ "0800040002000000" // RTA_OIF ifindex 2 (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000280000001400050600000000" // nlmsg_err payload (16 B)
+ "1f000000" // first 4 B of echoed offending header
)
)
let session = NetlinkSession(socket: mockSocket)
try session.routeAdd(
interface: "eth0",
dstIpv4Addr: try CIDRv4("192.168.64.0/24"),
srcIpv4Addr: try IPv4Address("192.168.64.3")
)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkRouteAddIpLinkWithoutSrc() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0")
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Add link route without RTA_PREFSRC.
let expectedAddRequest =
"2c00000018000506000000000cc00cc0" // Netlink header (16 B)
+ "02180000fe02fd0100000000" // struct rtmsg (12 B): AF_INET, dst/24,
// table=RT_TABLE_MAIN (0xfe), proto=RTPROT_KERNEL (0x02),
// scope=RT_SCOPE_LINK (0xfd), type=RTN_UNICAST (0x01)
+ "08000100c0a84000" // RTA_DST 192.168.64.0
+ "0800040002000000" // RTA_OIF ifindex 2 (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000280000001400050600000000" // nlmsg_err payload (16 B)
+ "1f000000" // first 4 B of echoed offending header
)
)
let session = NetlinkSession(socket: mockSocket)
try session.routeAdd(
interface: "eth0",
dstIpv4Addr: try CIDRv4("192.168.64.0/24"),
srcIpv4Addr: nil
)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkRouteAddIpv6Link() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name.
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0")
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Add IPv6 link route with source.
let expectedAddRequest =
"4c00000018000506000000000cc00cc0" // Netlink header (16 B): len=76
+ "0a400000fe04fd0100000000" // struct rtmsg (12 B): AF_INET6, dst/64,
// table=MAIN(0xfe), proto=STATIC(0x04), scope=LINK(0xfd), type=UNICAST(0x01)
+ "14000100fd000000000000000000000000000000" // RTA_DST fd00::
+ "14000700fd000000000000000000000000000001" // RTA_PREFSRC fd00::1
+ "0800040002000000" // RTA_OIF ifindex 2 (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000280000001400050600000000" // nlmsg_err payload (16 B)
+ "1f000000"
)
)
let session = NetlinkSession(socket: mockSocket)
try session.routeAdd(
interface: "eth0",
dstIpv6Addr: try CIDRv6("fd00::/64"),
srcIpv6Addr: try IPv6Address("fd00::1")
)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkRouteAddIpv6LinkWithoutSrc() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name.
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0")
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Add IPv6 link route without source.
let expectedAddRequest =
"3800000018000506000000000cc00cc0" // Netlink header (16 B): len=56
+ "0a400000fe04fd0100000000" // struct rtmsg (12 B): AF_INET6, dst/64
+ "14000100fd000000000000000000000000000000" // RTA_DST fd00::
+ "0800040002000000" // RTA_OIF ifindex 2 (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000280000001400050600000000" // nlmsg_err payload (16 B)
+ "1f000000"
)
)
let session = NetlinkSession(socket: mockSocket)
try session.routeAdd(
interface: "eth0",
dstIpv6Addr: try CIDRv6("fd00::/64"),
srcIpv6Addr: nil
)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
mockSocket.requests[1][8..<12] = [0, 0, 0, 0]
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
}
@Test func testNetworkRouteAddDefaultIpv6() throws {
let mockSocket = try MockNetlinkSocket()
mockSocket.pid = 0xc00c_c00c
// Lookup interface by name.
let expectedLookupRequest =
"3400000012000100000000000cc00cc0" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0")
mockSocket.responses.append(
[UInt8](
hex:
"2000000010000000000000000cc00cc0" // Netlink header (16 B)
+ "00000100020000004310010000000000" // struct ifinfomsg (16 B) no attributes
)
)
// Add default IPv6 route via gateway.
let expectedAddRequest =
"3800000018000506000000000cc00cc0" // Netlink header (16 B): len=56
+ "0a000000fe03000100000000" // struct rtmsg (12 B): AF_INET6, dst/0,
// table=MAIN(0xfe), proto=BOOT(0x03), scope=UNIVERSE(0x00), type=UNICAST(0x01)
+ "14000500fd000000000000000000000000000001" // RTA_GATEWAY fd00::1
+ "0800040002000000" // RTA_OIF ifindex 2 (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"2400000002000001000000000cc00cc0" // Netlink header (16 B)
+ "00000000280000001400050600000000" // nlmsg_err payload (16 B)
+ "1f000000"
)
)
let session = NetlinkSession(socket: mockSocket)
try session.routeAddDefault(
interface: "eth0",
ipv6Gateway: try IPv6Address("fd00::1")
)
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
mockSocket.requests[0][8..<12] = [0, 0, 0, 0]
#expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString())
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 {
/// Initializes `[UInt8]` from an even-length hex string
init(hex: String) {
self = stride(from: 0, to: hex.count, by: 2).compactMap {
UInt8(
hex[hex.index(hex.startIndex, offsetBy: $0)...]
.prefix(2), radix: 16)
}
}
}
@@ -0,0 +1,221 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
//
import Testing
@testable import ContainerizationNetlink
struct TypesTest {
@Test func testNetlinkMessageHeader() throws {
let expectedValue = NetlinkMessageHeader(
len: 0x1234_5678, type: 0x9abc, flags: 0xdef0, seq: 0x1122_3344, pid: 0x5566_7788)
let expectedBuffer: [UInt8] = [
0x78, 0x56, 0x34, 0x12,
0xbc, 0x9a, 0xf0, 0xde,
0x44, 0x33, 0x22, 0x11,
0x88, 0x77, 0x66, 0x55,
]
var buffer = [UInt8](repeating: 0, count: NetlinkMessageHeader.size)
let offset = try expectedValue.appendBuffer(&buffer, offset: 0)
#expect(NetlinkMessageHeader.size == offset)
#expect(expectedBuffer == buffer)
guard let (offset, value) = buffer.copyOut(as: NetlinkMessageHeader.self) else {
#expect(Bool(false), "could not bind value to buffer")
return
}
#expect(offset == NetlinkMessageHeader.size)
#expect(expectedValue == value)
}
@Test func testInterfaceInfo() throws {
let expectedValue = InterfaceInfo(
family: UInt8(AddressFamily.AF_NETLINK), type: 0x1234, index: 0x1234_5678, flags: 0x9abc_def0,
change: 0x0fed_cba9
)
let expectedBuffer: [UInt8] = [
0x10, 0x00, 0x34, 0x12,
0x78, 0x56, 0x34, 0x12,
0xf0, 0xde, 0xbc, 0x9a,
0xa9, 0xcb, 0xed, 0x0f,
]
var buffer = [UInt8](repeating: 0, count: InterfaceInfo.size)
let offset = try expectedValue.appendBuffer(&buffer, offset: 0)
#expect(InterfaceInfo.size == offset)
#expect(expectedBuffer == buffer)
guard let (offset, value) = buffer.copyOut(as: InterfaceInfo.self) else {
#expect(Bool(false), "could not bind value to buffer")
return
}
#expect(offset == InterfaceInfo.size)
#expect(expectedValue == value)
}
@Test func testAddressInfo() throws {
let expectedValue = AddressInfo(
family: UInt8(AddressFamily.AF_INET), prefixLength: 24, flags: 0x5a, scope: 0xa5, index: 0xdead_beef)
let expectedBuffer: [UInt8] = [
0x02, 0x18, 0x5a, 0xa5,
0xef, 0xbe, 0xad, 0xde,
]
var buffer = [UInt8](repeating: 0, count: AddressInfo.size)
let offset = try expectedValue.appendBuffer(&buffer, offset: 0)
#expect(AddressInfo.size == offset)
#expect(expectedBuffer == buffer)
guard let (offset, value) = buffer.copyOut(as: AddressInfo.self) else {
#expect(Bool(false), "could not bind value to buffer")
return
}
#expect(offset == AddressInfo.size)
#expect(expectedValue == value)
}
@Test func testRTAttribute() throws {
let expectedValue = RTAttribute(len: 0x1234, type: 0x5678)
let expectedBuffer: [UInt8] = [
0x34, 0x12, 0x78, 0x56,
]
var buffer = [UInt8](repeating: 0, count: RTAttribute.size)
let offset = try expectedValue.appendBuffer(&buffer, offset: 0)
#expect(RTAttribute.size == offset)
#expect(expectedBuffer == buffer)
guard let (offset, value) = buffer.copyOut(as: RTAttribute.self) else {
#expect(Bool(false), "could not bind value to buffer")
return
}
#expect(offset == RTAttribute.size)
#expect(expectedValue == value)
}
@Test func testSockaddrNetlink() throws {
let expectedValue = SockaddrNetlink(family: 16, pid: 0x1234_5678, groups: 0x9abc_def0)
let expectedBuffer: [UInt8] = [
0x10, 0x00, 0x00, 0x00,
0x78, 0x56, 0x34, 0x12,
0xf0, 0xde, 0xbc, 0x9a,
]
var buffer = [UInt8](repeating: 0, count: SockaddrNetlink.size)
let offset = try expectedValue.appendBuffer(&buffer, offset: 0)
#expect(SockaddrNetlink.size == offset)
#expect(expectedBuffer == buffer)
var unmarshaledValue = SockaddrNetlink()
let bindOffset = try unmarshaledValue.bindBuffer(&buffer, offset: 0)
#expect(bindOffset == SockaddrNetlink.size)
#expect(expectedValue == unmarshaledValue)
}
@Test func testRouteInfo() throws {
let expectedValue = RouteInfo(
family: UInt8(AddressFamily.AF_INET),
dstLen: 24,
srcLen: 0,
tos: 0,
table: RouteTable.MAIN,
proto: RouteProtocol.KERNEL,
scope: RouteScope.LINK,
type: RouteType.UNICAST,
flags: 0xdead_beef
)
let expectedBuffer: [UInt8] = [
0x02, 0x18, 0x00, 0x00,
0xfe, 0x02, 0xfd, 0x01,
0xef, 0xbe, 0xad, 0xde,
]
var buffer = [UInt8](repeating: 0, count: RouteInfo.size)
let offset = try expectedValue.appendBuffer(&buffer, offset: 0)
#expect(RouteInfo.size == offset)
#expect(expectedBuffer == buffer)
var unmarshaledValue = RouteInfo(
dstLen: 0, srcLen: 0, tos: 0, table: 0, proto: 0, scope: 0, type: 0, flags: 0)
let bindOffset = try unmarshaledValue.bindBuffer(&buffer, offset: 0)
#expect(bindOffset == RouteInfo.size)
#expect(expectedValue == unmarshaledValue)
}
@Test func testLinkStatistics64() throws {
var expectedValue = LinkStatistics64()
expectedValue.rxPackets = 0x0102_0304_0506_0708
expectedValue.txPackets = 0x090a_0b0c_0d0e_0f10
expectedValue.rxBytes = 0x1112_1314_1516_1718
expectedValue.txBytes = 0x191a_1b1c_1d1e_1f20
expectedValue.rxErrors = 0x2122_2324_2526_2728
expectedValue.txErrors = 0x292a_2b2c_2d2e_2f30
expectedValue.rxDropped = 0x3132_3334_3536_3738
expectedValue.txDropped = 0x393a_3b3c_3d3e_3f40
expectedValue.multicast = 0x4142_4344_4546_4748
expectedValue.collisions = 0x494a_4b4c_4d4e_4f50
expectedValue.rxLengthErrors = 0x5152_5354_5556_5758
expectedValue.rxOverErrors = 0x595a_5b5c_5d5e_5f60
expectedValue.rxCrcErrors = 0x6162_6364_6566_6768
expectedValue.rxFrameErrors = 0x696a_6b6c_6d6e_6f70
expectedValue.rxFifoErrors = 0x7172_7374_7576_7778
expectedValue.rxMissedErrors = 0x797a_7b7c_7d7e_7f80
expectedValue.txAbortedErrors = 0x8182_8384_8586_8788
expectedValue.txCarrierErrors = 0x898a_8b8c_8d8e_8f90
expectedValue.txFifoErrors = 0x9192_9394_9596_9798
expectedValue.txHeartbeatErrors = 0x999a_9b9c_9d9e_9fa0
expectedValue.txWindowErrors = 0xa1a2_a3a4_a5a6_a7a8
expectedValue.rxCompressed = 0xa9aa_abac_adae_afb0
expectedValue.txCompressed = 0xb1b2_b3b4_b5b6_b7b8
let expectedBuffer: [UInt8] = [
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09,
0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11,
0x20, 0x1f, 0x1e, 0x1d, 0x1c, 0x1b, 0x1a, 0x19,
0x28, 0x27, 0x26, 0x25, 0x24, 0x23, 0x22, 0x21,
0x30, 0x2f, 0x2e, 0x2d, 0x2c, 0x2b, 0x2a, 0x29,
0x38, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31,
0x40, 0x3f, 0x3e, 0x3d, 0x3c, 0x3b, 0x3a, 0x39,
0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41,
0x50, 0x4f, 0x4e, 0x4d, 0x4c, 0x4b, 0x4a, 0x49,
0x58, 0x57, 0x56, 0x55, 0x54, 0x53, 0x52, 0x51,
0x60, 0x5f, 0x5e, 0x5d, 0x5c, 0x5b, 0x5a, 0x59,
0x68, 0x67, 0x66, 0x65, 0x64, 0x63, 0x62, 0x61,
0x70, 0x6f, 0x6e, 0x6d, 0x6c, 0x6b, 0x6a, 0x69,
0x78, 0x77, 0x76, 0x75, 0x74, 0x73, 0x72, 0x71,
0x80, 0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79,
0x88, 0x87, 0x86, 0x85, 0x84, 0x83, 0x82, 0x81,
0x90, 0x8f, 0x8e, 0x8d, 0x8c, 0x8b, 0x8a, 0x89,
0x98, 0x97, 0x96, 0x95, 0x94, 0x93, 0x92, 0x91,
0xa0, 0x9f, 0x9e, 0x9d, 0x9c, 0x9b, 0x9a, 0x99,
0xa8, 0xa7, 0xa6, 0xa5, 0xa4, 0xa3, 0xa2, 0xa1,
0xb0, 0xaf, 0xae, 0xad, 0xac, 0xab, 0xaa, 0xa9,
0xb8, 0xb7, 0xb6, 0xb5, 0xb4, 0xb3, 0xb2, 0xb1,
]
var buffer = [UInt8](repeating: 0, count: LinkStatistics64.size)
let offset = try expectedValue.appendBuffer(&buffer, offset: 0)
#expect(LinkStatistics64.size == offset)
#expect(expectedBuffer == buffer)
var unmarshaledValue = LinkStatistics64()
let bindOffset = try unmarshaledValue.bindBuffer(&buffer, offset: 0)
#expect(bindOffset == LinkStatistics64.size)
#expect(expectedValue == unmarshaledValue)
}
}