Make MTU configurable for Netlink (#204)

Here we add the implementation to make MTU value configurable instead of
hardcoding in `linkSet`.
This commit is contained in:
Raj
2025-07-08 15:54:57 -07:00
committed by GitHub
parent d16893d7b9
commit ad219fecf1
8 changed files with 231 additions and 111 deletions
+2 -1
View File
@@ -198,10 +198,11 @@ extension Vminitd: VirtualMachineAgent {
_ = try await client.deleteProcess(request)
}
public func up(name: String) async throws {
public func up(name: String, mtu: UInt32? = nil) async throws {
let request = Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest.with {
$0.interface = name
$0.up = true
if let mtu { $0.mtu = mtu }
}
_ = try await client.ipLinkSet(request)
}
@@ -504,7 +504,7 @@ extension LinuxContainer {
for (index, i) in self.interfaces.enumerated() {
let name = "eth\(index)"
try await agent.addressAdd(name: name, address: i.address)
try await agent.up(name: name)
try await agent.up(name: name, mtu: 1280)
if let gateway = i.gateway {
try await agent.routeAddDefault(name: name, gateway: gateway)
}
@@ -658,9 +658,20 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: Sendable {
public var up: Bool = false
public var mtu: UInt32 {
get {return _mtu ?? 0}
set {_mtu = newValue}
}
/// Returns true if `mtu` has been explicitly set.
public var hasMtu: Bool {return self._mtu != nil}
/// Clears the value of `mtu`. Subsequent reads from it will return its default value.
public mutating func clearMtu() {self._mtu = nil}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
fileprivate var _mtu: UInt32? = nil
}
public struct Com_Apple_Containerization_Sandbox_V3_IpLinkSetResponse: Sendable {
@@ -1992,6 +2003,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: SwiftProtobuf.
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "interface"),
2: .same(proto: "up"),
3: .same(proto: "mtu"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@@ -2002,24 +2014,33 @@ extension Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: SwiftProtobuf.
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }()
case 2: try { try decoder.decodeSingularBoolField(value: &self.up) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self._mtu) }()
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
if !self.interface.isEmpty {
try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1)
}
if self.up != false {
try visitor.visitSingularBoolField(value: self.up, fieldNumber: 2)
}
try { if let v = self._mtu {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3)
} }()
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest) -> Bool {
if lhs.interface != rhs.interface {return false}
if lhs.up != rhs.up {return false}
if lhs._mtu != rhs._mtu {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@@ -193,6 +193,7 @@ message MkdirResponse {}
message IpLinkSetRequest {
string interface = 1;
bool up = 2;
optional uint32 mtu = 3;
}
message IpLinkSetResponse {}
@@ -53,7 +53,7 @@ public protocol VirtualMachineAgent: Sendable {
func deleteProcess(id: String, containerID: String?) async throws
// Networking
func up(name: String) async throws
func up(name: String, mtu: UInt32?) async throws
func down(name: String) async throws
func addressAdd(name: String, address: String) async throws
func routeAddDefault(name: String, gateway: String) async throws
@@ -67,12 +67,17 @@ public struct NetlinkSession {
/// - Parameters:
/// - interface: The name of the interface.
/// - up: The value to set the interface state to.
public func linkSet(interface: String, up: Bool) throws {
public func linkSet(interface: String, up: Bool, mtu: UInt32? = nil) throws {
// ip link set dev [interface] [up|down]
let interfaceIndex = try getInterfaceIndex(interface)
let mtuAttr = RTAttribute(
len: UInt16(RTAttribute.size + MemoryLayout<UInt32>.size), type: LinkAttributeType.IFLA_MTU)
let requestSize = NetlinkMessageHeader.size + InterfaceInfo.size + mtuAttr.paddedLen
// build the attribute only when mtu is supplied
let attr: RTAttribute? =
(mtu != nil)
? RTAttribute(
len: UInt16(RTAttribute.size + MemoryLayout<UInt32>.size),
type: LinkAttributeType.IFLA_MTU)
: nil
let requestSize = NetlinkMessageHeader.size + InterfaceInfo.size + (attr?.paddedLen ?? 0)
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
@@ -91,16 +96,16 @@ public struct NetlinkSession {
change: InterfaceFlags.DEFAULT_CHANGE)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
requestOffset = try mtuAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
let newRequestOffset = requestBuffer.copyIn(
as: UInt32.self,
value: Self.mtu,
offset: requestOffset)
else {
throw NetlinkDataError.sendMarshalFailure
if let attr = attr, let m = mtu {
requestOffset = try attr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
let newRequestOffset =
requestBuffer.copyIn(as: UInt32.self, value: m, offset: requestOffset)
else {
throw NetlinkDataError.sendMarshalFailure
}
requestOffset = newRequestOffset
}
requestOffset = newRequestOffset
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
@@ -26,23 +26,29 @@ struct NetlinkSessionTest {
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0110000000000000001000000ffffffff08001d00090000000c0003006574683000000000"
mockSocket.responses.append([
0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0c, 0xc0, 0x0c, 0xc0,
0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
0x43, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
])
"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
)
)
// Network down for interface.
let expectedDownRequest = "2800000010000500000000000cc00cc0110000000200000000000000ffffffff0800040000050000"
mockSocket.responses.append([
0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x0c, 0xc0, 0x0c, 0xc0,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x10, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0c, 0x00, 0x00, 0x00,
])
// 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)
@@ -61,26 +67,74 @@ struct NetlinkSessionTest {
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"340000001200010000000000c00cc00c110000000000000001000000ffffffff08001d00090000000c0003006574683000000000"
mockSocket.responses.append([
0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0c, 0xc0, 0x0c,
0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
0x43, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
])
"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 = "280000001000050000000000c00cc00c110000000200000001000000ffffffff0800040000050000"
mockSocket.responses.append([
0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0c, 0xc0, 0x0c,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x10, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
0x11, 0x00, 0x00, 0x00,
])
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)
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)
@@ -96,17 +150,19 @@ struct NetlinkSessionTest {
// Lookup interface by name, truncated response with three attributes.
let expectedLookupRequest =
"34000000120001000000000078563412110000000000000001000000ffffffff08001d00090000000c0003006574683000000000"
mockSocket.responses.append([
0x3c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12,
0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
0x43, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x09, 0x00, 0x03, 0x00, 0x65, 0x74, 0x68, 0x30,
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0d, 0x00,
0xe8, 0x03, 0x00, 0x00, 0x05, 0x00, 0x10, 0x00,
0x06, 0x00, 0x00, 0x00,
])
"34000000120001000000000078563412" // Netlink header (16 B)
+ "110000000000000001000000ffffffff" // struct ifinfomsg (16 B)
+ "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME (eth0)
mockSocket.responses.append(
[UInt8](
hex:
"3c000000100000000000000078563412" // 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)
)
)
let session = NetlinkSession(socket: mockSocket)
let links = try session.linkGet(interface: "eth0")
@@ -135,27 +191,33 @@ struct NetlinkSessionTest {
mockSocket.pid = 0x8765_4321
// Lookup all interfaces, responses with only the interface name attribute.
let expectedLookupRequest = "28000000120001030000000021436587110000000000000001000000ffffffff08001d0009000000"
mockSocket.responses.append([
0x28, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x65, 0x87,
0x00, 0x00, 0x04, 0x03, 0x01, 0x00, 0x00, 0x00,
0x49, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0x00, 0x03, 0x00, 0x6c, 0x6f, 0x00, 0x00,
])
mockSocket.responses.append([
0x2c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x65, 0x87,
0x00, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0a, 0x00, 0x03, 0x00, 0x74, 0x75, 0x6e, 0x6c,
0x30, 0x00, 0x00, 0x00,
])
mockSocket.responses.append([
0x14, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x65, 0x87,
0x00, 0x00, 0x00, 0x00,
])
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()
@@ -185,23 +247,31 @@ struct NetlinkSessionTest {
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0110000000000000001000000ffffffff08001d00090000000c0003006574683000000000"
mockSocket.responses.append([
0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0c, 0xc0, 0x0c, 0xc0,
0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
0x43, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
])
"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 = "2800000014000506000000000cc00cc0021800000200000008000200c0a840fa08000100c0a840fa"
mockSocket.responses.append([
0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x0c, 0xc0, 0x0c, 0xc0,
0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00,
0x14, 0x00, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00,
0x1f, 0x00, 0x00, 0x00,
])
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", address: "192.168.64.250/24")
@@ -219,24 +289,34 @@ struct NetlinkSessionTest {
// Lookup interface by name, truncated response with no attributes (not needed at present).
let expectedLookupRequest =
"3400000012000100000000000cc00cc0110000000000000001000000ffffffff08001d00090000000c0003006574683000000000"
mockSocket.responses.append([
0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0c, 0xc0, 0x0c, 0xc0,
0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
0x43, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
])
"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 =
"3400000018000506000000000cc00cc002180000fe02fd010000000008000100c0a8400008000700c0a840030800040002000000"
mockSocket.responses.append([
0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x0c, 0xc0, 0x0c, 0xc0,
0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00,
0x14, 0x00, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00,
0x1f, 0x00, 0x00, 0x00,
])
"3400000018000506000000000cc00cc0" // Netlink header (16 B)
+ "02180000fe02fd0100000000" // struct rtmsg (12 B): AF_INET, dst/24,
// table=RT_TABLE_MAIN (0xfe), proto=RTPROT_BOOT (0x02),
// scope=RT_SCOPE_UNIVERSE (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(
@@ -253,3 +333,14 @@ struct NetlinkSessionTest {
#expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString())
}
}
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)
}
}
}
+2 -1
View File
@@ -622,7 +622,8 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
try session.linkSet(interface: request.interface, up: request.up)
let mtuValue: UInt32? = request.hasMtu ? request.mtu : nil
try session.linkSet(interface: request.interface, up: request.up, mtu: mtuValue)
} catch {
log.error(
"ip-link-set",