Files
apple--containerization/Sources/ContainerizationNetlink/NetlinkSession.swift
T
wehub-resource-sync 680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

1007 lines
44 KiB
Swift

//===----------------------------------------------------------------------===//
// 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 Logging
/// `NetlinkSession` facilitates interacting with netlink via a provided `NetlinkSocket`. This is
/// the core high-level type offered to perform actions to the netlink surface in the kernel.
public struct NetlinkSession {
private static let receiveDataLength = 65536
private static let mtu: UInt32 = 1280
private let socket: any NetlinkSocket
private let log: Logger
/// Creates a new `NetlinkSession`.
/// - Parameters:
/// - socket: The `NetlinkSocket` to use for netlink interaction.
/// - log: The logger to use. The default value is `nil`.
public init(socket: any NetlinkSocket, log: Logger? = nil) {
self.socket = socket
self.log = log ?? Logger(label: "com.apple.containerization.netlink")
}
/// Errors that may occur during netlink interaction.
public enum Error: Swift.Error, CustomStringConvertible, Equatable {
case invalidIpAddress
case invalidPrefixLength
case unexpectedInfo(type: UInt16)
case unexpectedOffset(offset: Int, size: Int)
case unexpectedResidualPackets
case unexpectedResultSet(count: Int, expected: Int)
/// The description of the errors.
public var description: String {
switch self {
case .invalidIpAddress:
return "invalid IP address"
case .invalidPrefixLength:
return "invalid prefix length"
case .unexpectedInfo(let type):
return "unexpected response information, type = \(type)"
case .unexpectedOffset(let offset, let size):
return "unexpected buffer state, offset = \(offset), size = \(size)"
case .unexpectedResidualPackets:
return "unexpected residual response packets"
case .unexpectedResultSet(let count, let expected):
return "unexpected result set size, count = \(count), expected = \(expected)"
}
}
}
/// Performs a link set command on an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - up: The value to set the interface state to.
public func linkSet(interface: String, up: Bool, mtu: UInt32? = nil) throws {
// ip link set dev [interface] [up|down]
let interfaceIndex = try getInterfaceIndex(interface)
// 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
let requestHeader = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWLINK,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK,
pid: socket.pid)
requestOffset = try requestHeader.appendBuffer(&requestBuffer, offset: requestOffset)
let flags = up ? InterfaceFlags.IFF_UP : 0
let requestInfo = InterfaceInfo(
family: UInt8(AddressFamily.AF_PACKET),
index: interfaceIndex,
flags: flags,
change: InterfaceFlags.DEFAULT_CHANGE)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
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 BindError.sendMarshalFailure(type: "RTAttribute", field: "IFLA_MTU")
}
requestOffset = newRequestOffset
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWLINK) { InterfaceInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Set link attributes (MAC and/or bridge master) on an existing interface.
/// Either argument may be omitted; if both are nil this is a no-op.
///
/// Sends a single `RTM_NEWLINK` carrying any of:
/// - `IFLA_ADDRESS` — the new hardware address (6 bytes for an Ethernet MAC).
/// - `IFLA_MASTER` — the index of the bridge to enslave the link to. The
/// bridge is identified by name; the index is resolved internally.
///
/// - Parameters:
/// - interface: The name of the interface to update.
/// - macAddress: If non-nil, the new MAC address.
/// - master: If non-nil, the name of a bridge to enslave the interface to.
public func linkSetAttributes(
interface: String,
macAddress: MACAddress? = nil,
master: String? = nil
) throws {
if macAddress == nil && master == nil {
return
}
let interfaceIndex = try getInterfaceIndex(interface)
var masterIndex: Int32? = nil
if let master {
masterIndex = try getInterfaceIndex(master)
}
// Build the attribute list. MAC is 6 raw bytes; master is a 4-byte
// integer holding the bridge's interface index.
let macAttr: RTAttribute? =
(macAddress != nil)
? RTAttribute(
len: UInt16(RTAttribute.size + 6),
type: LinkAttributeType.IFLA_ADDRESS)
: nil
let masterAttr: RTAttribute? =
(masterIndex != nil)
? RTAttribute(
len: UInt16(RTAttribute.size + MemoryLayout<Int32>.size),
type: LinkAttributeType.IFLA_MASTER)
: nil
let requestSize =
NetlinkMessageHeader.size
+ InterfaceInfo.size
+ (macAttr?.paddedLen ?? 0)
+ (masterAttr?.paddedLen ?? 0)
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let requestHeader = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWLINK,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK,
pid: socket.pid)
requestOffset = try requestHeader.appendBuffer(&requestBuffer, offset: requestOffset)
// No flag changes — passing 0/0 means "do not modify IFF_* flags".
let requestInfo = InterfaceInfo(
family: UInt8(AddressFamily.AF_PACKET),
index: interfaceIndex,
flags: 0,
change: 0)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
if let macAttr, let macAddress {
requestOffset = try macAttr.appendBuffer(&requestBuffer, offset: requestOffset)
for byte in macAddress.bytes {
guard let next = requestBuffer.copyIn(as: UInt8.self, value: byte, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFLA_ADDRESS")
}
requestOffset = next
}
// Pad attribute payload to 4-byte boundary (NLA_ALIGN).
let payloadLen = 6
let padded = ((payloadLen + 3) >> 2) << 2
requestOffset += padded - payloadLen
}
if let masterAttr, let masterIndex {
requestOffset = try masterAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
let next = requestBuffer.copyIn(as: Int32.self, value: masterIndex, offset: requestOffset)
else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFLA_MASTER")
}
requestOffset = next
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWLINK) { InterfaceInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Create a Linux bridge link via `RTM_NEWLINK` carrying
/// `IFLA_LINKINFO/IFLA_INFO_KIND="bridge"`.
///
/// Sends `NLM_F_CREATE | NLM_F_EXCL`, so the kernel returns `EEXIST` if a
/// link with the same name already exists. Callers wanting idempotent
/// creation should catch and inspect the thrown error.
public func linkAddBridge(name: String) throws {
let nameBytes = Array(name.utf8) + [0]
let ifnameAttr = RTAttribute(
len: UInt16(RTAttribute.size + nameBytes.count),
type: LinkAttributeType.IFLA_IFNAME)
let kindBytes = Array("bridge".utf8) + [0]
let kindAttr = RTAttribute(
len: UInt16(RTAttribute.size + kindBytes.count),
type: LinkInfoAttributeType.IFLA_INFO_KIND)
// IFLA_LINKINFO is a nest containing IFLA_INFO_KIND.
let linkInfoAttr = RTAttribute(
len: UInt16(RTAttribute.size + kindAttr.paddedLen),
type: LinkAttributeType.IFLA_LINKINFO)
let requestSize =
NetlinkMessageHeader.size
+ InterfaceInfo.size
+ ifnameAttr.paddedLen
+ linkInfoAttr.paddedLen
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWLINK,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK
| NetlinkFlags.NLM_F_CREATE | NetlinkFlags.NLM_F_EXCL,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let info = InterfaceInfo(
family: UInt8(AddressFamily.AF_UNSPEC),
index: 0,
flags: 0,
change: 0)
requestOffset = try info.appendBuffer(&requestBuffer, offset: requestOffset)
// IFLA_IFNAME
requestOffset = try ifnameAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let next = requestBuffer.copyIn(buffer: nameBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFLA_IFNAME")
}
// Pad NUL-terminated name to NLA 4-byte boundary.
requestOffset = next + (ifnameAttr.paddedLen - RTAttribute.size - nameBytes.count)
// IFLA_LINKINFO -> IFLA_INFO_KIND
requestOffset = try linkInfoAttr.appendBuffer(&requestBuffer, offset: requestOffset)
requestOffset = try kindAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let after = requestBuffer.copyIn(buffer: kindBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFLA_INFO_KIND")
}
requestOffset = after + (kindAttr.paddedLen - RTAttribute.size - kindBytes.count)
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWLINK) { InterfaceInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Remove a link by name via `RTM_DELLINK`.
///
/// Throws on netlink error. Callers wanting idempotent removal should
/// catch and inspect the thrown error (e.g. `ENODEV` ⇒ already gone).
public func linkDel(name: String) throws {
let interfaceIndex = try getInterfaceIndex(name)
let requestSize = NetlinkMessageHeader.size + InterfaceInfo.size
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_DELLINK,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let info = InterfaceInfo(
family: UInt8(AddressFamily.AF_UNSPEC),
index: interfaceIndex,
flags: 0,
change: 0)
requestOffset = try info.appendBuffer(&requestBuffer, offset: requestOffset)
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_DELLINK) { InterfaceInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Performs a link get command on an interface.
/// Returns information about the interface.
/// - Parameter interface: The name of the interface to query.
public func linkGet(interface: String? = nil, includeStats: Bool = false) throws -> [LinkResponse] {
// ip link ip show
let maskAttr = RTAttribute(
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_IFNAME)
}
let requestSize =
NetlinkMessageHeader.size + InterfaceInfo.size + maskAttr.paddedLen + (interfaceNameAttr?.paddedLen ?? 0)
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let flags =
interface != nil ? NetlinkFlags.NLM_F_REQUEST : (NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_DUMP)
let requestHeader = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_GETLINK,
flags: flags,
pid: socket.pid)
requestOffset = try requestHeader.appendBuffer(&requestBuffer, offset: requestOffset)
let requestInfo = InterfaceInfo(
family: UInt8(AddressFamily.AF_PACKET),
index: 0,
flags: InterfaceFlags.IFF_UP,
change: InterfaceFlags.DEFAULT_CHANGE)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
var filters = LinkAttributeMaskFilter.RTEXT_FILTER_VF
if !includeStats {
filters |= LinkAttributeMaskFilter.RTEXT_FILTER_SKIP_STATS
}
requestOffset = try maskAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
var requestOffset = requestBuffer.copyIn(
as: UInt32.self,
value: filters,
offset: requestOffset)
else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFLA_EXT_MASK")
}
if let interfaceNameAttr {
if let interfaceName {
requestOffset = try interfaceNameAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let updatedRequestOffset = requestBuffer.copyIn(buffer: interfaceName, offset: requestOffset)
else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFLA_IFNAME")
}
requestOffset = updatedRequestOffset
}
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
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,
interfaceFlags: infos[i].flags,
interfaceType: infos[i].type,
attrDatas: attrDataLists[i])
)
}
return linkResponses
}
/// Adds an IPv4 address to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - ipv4Address: The CIDRv4 address describing the interface IP and subnet prefix length.
public func addressAdd(interface: String, ipv4Address: CIDRv4) throws {
// ip addr add [addr] dev [interface]
// ip address {add|change|replace} IFADDR dev IFNAME [ LIFETIME ] [ CONFFLAG-LIST ]
// IFADDR := PREFIX | ADDR peer PREFIX
// [ broadcast ADDR ] [ anycast ADDR ]
// [ label IFNAME ] [ scope SCOPE-ID ] [ metric METRIC ]
// SCOPE-ID := [ host | link | global | NUMBER ]
// CONFFLAG-LIST := [ CONFFLAG-LIST ] CONFFLAG
// CONFFLAG := [ home | nodad | mngtmpaddr | noprefixroute | autojoin ]
// LIFETIME := [ valid_lft LFT ] [ preferred_lft LFT ]
// LFT := forever | SECONDS
let interfaceIndex = try getInterfaceIndex(interface)
let ipAddressBytes = ipv4Address.address.bytes
let addressAttrSize = RTAttribute.size + MemoryLayout<UInt8>.size * ipAddressBytes.count
let requestSize = NetlinkMessageHeader.size + AddressInfo.size + 2 * addressAttrSize
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWADDR,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL
| NetlinkFlags.NLM_F_CREATE,
seq: 0,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let requestInfo = AddressInfo(
family: UInt8(AddressFamily.AF_INET),
prefixLength: ipv4Address.prefix.length,
flags: 0,
scope: NetlinkScope.RT_SCOPE_UNIVERSE,
index: UInt32(interfaceIndex))
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
let ipLocalAttr = RTAttribute(len: UInt16(addressAttrSize), type: AddressAttributeType.IFA_LOCAL)
requestOffset = try ipLocalAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard var requestOffset = requestBuffer.copyIn(buffer: ipAddressBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFA_LOCAL")
}
let ipAddressAttr = RTAttribute(len: UInt16(addressAttrSize), type: AddressAttributeType.IFA_ADDRESS)
requestOffset = try ipAddressAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let requestOffset = requestBuffer.copyIn(buffer: ipAddressBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFA_ADDRESS")
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWADDR) { AddressInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Adds an IPv6 address to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - ipv6Address: The CIDRv6 address describing the interface IP and subnet prefix length.
public func addressAdd(interface: String, ipv6Address: CIDRv6) throws {
let interfaceIndex = try getInterfaceIndex(interface)
let ipAddressBytes = ipv6Address.address.bytes
let addressAttrSize = RTAttribute.size + MemoryLayout<UInt8>.size * ipAddressBytes.count
let requestSize = NetlinkMessageHeader.size + AddressInfo.size + addressAttrSize
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWADDR,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL
| NetlinkFlags.NLM_F_CREATE,
seq: 0,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let requestInfo = AddressInfo(
family: UInt8(AddressFamily.AF_INET6),
prefixLength: ipv6Address.prefix.length,
flags: AddressFlags.IFA_F_PERMANENT | AddressFlags.IFA_F_NODAD,
scope: NetlinkScope.RT_SCOPE_UNIVERSE,
index: UInt32(interfaceIndex))
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
let ipAddressAttr = RTAttribute(len: UInt16(addressAttrSize), type: AddressAttributeType.IFA_ADDRESS)
requestOffset = try ipAddressAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let requestOffset = requestBuffer.copyIn(buffer: ipAddressBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFA_ADDRESS")
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWADDR) { AddressInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Adds an IPv4 route to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - dstIpv4Addr: The CIDRv4 address describing the gateway IP and subnet prefix length.
/// - srcIpv4Addr: The source IPv4 address to route from.
public func routeAdd(
interface: String,
dstIpv4Addr: CIDRv4,
srcIpv4Addr: IPv4Address?
) throws {
// ip route add [dest-cidr] dev [interface] [src [src-addr]] proto kernel
let interfaceIndex = try getInterfaceIndex(interface)
let dstAddrBytes = dstIpv4Addr.address.bytes
let dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count
let srcAddrAttrSize: Int
if let srcIpv4Addr {
let srcAddrBytes = srcIpv4Addr.bytes
srcAddrAttrSize = RTAttribute.size + srcAddrBytes.count
} else {
srcAddrAttrSize = 0
}
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
let requestSize =
NetlinkMessageHeader.size + RouteInfo.size + dstAddrAttrSize + srcAddrAttrSize + interfaceAttrSize
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWROUTE,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL
| NetlinkFlags.NLM_F_CREATE,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let requestInfo = RouteInfo(
family: UInt8(AddressFamily.AF_INET),
dstLen: dstIpv4Addr.prefix.length,
srcLen: 0,
tos: 0,
table: RouteTable.MAIN,
proto: RouteProtocol.KERNEL,
scope: RouteScope.LINK,
type: RouteType.UNICAST,
flags: 0)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
let dstAddrAttr = RTAttribute(len: UInt16(dstAddrAttrSize), type: RouteAttributeType.DST)
requestOffset = try dstAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard var requestOffset = requestBuffer.copyIn(buffer: dstAddrBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_DST")
}
if let srcIpv4Addr {
let srcAddrBytes = srcIpv4Addr.bytes
let srcAddrAttr = RTAttribute(len: UInt16(srcAddrAttrSize), type: RouteAttributeType.PREFSRC)
requestOffset = try srcAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let newOffset = requestBuffer.copyIn(buffer: srcAddrBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_PREFSRC")
}
requestOffset = newOffset
}
let interfaceAttr = RTAttribute(len: UInt16(interfaceAttrSize), type: RouteAttributeType.OIF)
requestOffset = try interfaceAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
let requestOffset = requestBuffer.copyIn(
as: UInt32.self,
value: UInt32(interfaceIndex),
offset: requestOffset)
else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_OIF")
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWROUTE) { AddressInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Adds a default IPv4 route to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - ipv4Gateway: The gateway address, or nil.
public func routeAddDefault(
interface: String,
ipv4Gateway: IPv4Address?
) throws {
// ip route add default via [gateway] dev [interface] or
// ip route add default dev [interface]
let dstAddrBytes = ipv4Gateway?.bytes
let dstAddrAttrSize: Int
if let dstAddrBytes {
dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count
} else {
dstAddrAttrSize = 0
}
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
let interfaceIndex = try getInterfaceIndex(interface)
let requestSize = NetlinkMessageHeader.size + RouteInfo.size + dstAddrAttrSize + interfaceAttrSize
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWROUTE,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL
| NetlinkFlags.NLM_F_CREATE,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let requestInfo = RouteInfo(
family: UInt8(AddressFamily.AF_INET),
dstLen: 0,
srcLen: 0,
tos: 0,
table: RouteTable.MAIN,
proto: RouteProtocol.BOOT,
scope: ipv4Gateway != nil ? RouteScope.UNIVERSE : RouteScope.LINK,
type: RouteType.UNICAST,
flags: 0)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
if let dstAddrBytes {
let dstAddrAttr = RTAttribute(len: UInt16(dstAddrAttrSize), type: RouteAttributeType.GATEWAY)
requestOffset = try dstAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let newOffset = requestBuffer.copyIn(buffer: dstAddrBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_GATEWAY")
}
requestOffset = newOffset
}
let interfaceAttr = RTAttribute(len: UInt16(interfaceAttrSize), type: RouteAttributeType.OIF)
requestOffset = try interfaceAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
let requestOffset = requestBuffer.copyIn(
as: UInt32.self,
value: UInt32(interfaceIndex),
offset: requestOffset)
else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_OIF")
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWROUTE) { AddressInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Adds an IPv6 route to an interface. Used to install an on-link host
/// route (typically a /128) to a gateway that lives outside the interface's
/// subnet, so the kernel will accept the v6 default route. The chosen
/// `proto STATIC, scope LINK` matches what `iproute2` emits for explicit
/// `ip -6 route add <addr>/128 dev <iface>`.
/// - Parameters:
/// - interface: The name of the interface.
/// - dstIpv6Addr: The CIDRv6 address describing the destination network and prefix length.
/// - srcIpv6Addr: The source IPv6 address to route from.
public func routeAdd(
interface: String,
dstIpv6Addr: CIDRv6,
srcIpv6Addr: IPv6Address?
) throws {
let interfaceIndex = try getInterfaceIndex(interface)
let dstAddrBytes = dstIpv6Addr.address.bytes
let dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count
let srcAddrAttrSize: Int
if let srcIpv6Addr {
let srcAddrBytes = srcIpv6Addr.bytes
srcAddrAttrSize = RTAttribute.size + srcAddrBytes.count
} else {
srcAddrAttrSize = 0
}
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
let requestSize =
NetlinkMessageHeader.size + RouteInfo.size + dstAddrAttrSize + srcAddrAttrSize + interfaceAttrSize
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWROUTE,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL
| NetlinkFlags.NLM_F_CREATE,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let requestInfo = RouteInfo(
family: UInt8(AddressFamily.AF_INET6),
dstLen: dstIpv6Addr.prefix.length,
srcLen: 0,
tos: 0,
table: RouteTable.MAIN,
proto: RouteProtocol.STATIC,
scope: RouteScope.LINK,
type: RouteType.UNICAST,
flags: 0)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
let dstAddrAttr = RTAttribute(len: UInt16(dstAddrAttrSize), type: RouteAttributeType.DST)
requestOffset = try dstAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard var requestOffset = requestBuffer.copyIn(buffer: dstAddrBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_DST")
}
if let srcIpv6Addr {
let srcAddrBytes = srcIpv6Addr.bytes
let srcAddrAttr = RTAttribute(len: UInt16(srcAddrAttrSize), type: RouteAttributeType.PREFSRC)
requestOffset = try srcAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard let newOffset = requestBuffer.copyIn(buffer: srcAddrBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_PREFSRC")
}
requestOffset = newOffset
}
let interfaceAttr = RTAttribute(len: UInt16(interfaceAttrSize), type: RouteAttributeType.OIF)
requestOffset = try interfaceAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
let requestOffset = requestBuffer.copyIn(
as: UInt32.self,
value: UInt32(interfaceIndex),
offset: requestOffset)
else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_OIF")
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWROUTE) { AddressInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
/// Adds a default IPv6 route to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - ipv6Gateway: The gateway address.
public func routeAddDefault(
interface: String,
ipv6Gateway: IPv6Address
) throws {
let gatewayBytes = ipv6Gateway.bytes
let gatewaySize = RTAttribute.size + gatewayBytes.count
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
let interfaceIndex = try getInterfaceIndex(interface)
let requestSize = NetlinkMessageHeader.size + RouteInfo.size + gatewaySize + interfaceAttrSize
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
var requestOffset = 0
let header = NetlinkMessageHeader(
len: UInt32(requestBuffer.count),
type: NetlinkType.RTM_NEWROUTE,
flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL
| NetlinkFlags.NLM_F_CREATE,
pid: socket.pid)
requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset)
let requestInfo = RouteInfo(
family: UInt8(AddressFamily.AF_INET6),
dstLen: 0,
srcLen: 0,
tos: 0,
table: RouteTable.MAIN,
proto: RouteProtocol.BOOT,
scope: RouteScope.UNIVERSE,
type: RouteType.UNICAST,
flags: 0)
requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset)
let dstAddrAttr = RTAttribute(len: UInt16(gatewaySize), type: RouteAttributeType.GATEWAY)
requestOffset = try dstAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard var requestOffset = requestBuffer.copyIn(buffer: gatewayBytes, offset: requestOffset) else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_GATEWAY")
}
let interfaceAttr = RTAttribute(len: UInt16(interfaceAttrSize), type: RouteAttributeType.OIF)
requestOffset = try interfaceAttr.appendBuffer(&requestBuffer, offset: requestOffset)
guard
let requestOffset = requestBuffer.copyIn(
as: UInt32.self,
value: UInt32(interfaceIndex),
offset: requestOffset)
else {
throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_OIF")
}
guard requestOffset == requestSize else {
throw Error.unexpectedOffset(offset: requestOffset, size: requestSize)
}
try sendRequest(buffer: &requestBuffer)
let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWROUTE) { AddressInfo() }
guard infos.count == 0 else {
throw Error.unexpectedResultSet(count: infos.count, expected: 0)
}
}
private func getInterfaceName(_ interface: String) throws -> [UInt8] {
guard let interfaceNameData = interface.data(using: .utf8) else {
throw BindError.sendMarshalFailure(type: "String", field: "interface")
}
var interfaceName = [UInt8](interfaceNameData)
interfaceName.append(0)
while interfaceName.count % MemoryLayout<UInt32>.size != 0 {
interfaceName.append(0)
}
return interfaceName
}
private func getInterfaceIndex(_ interface: String) throws -> Int32 {
let linkResponses = try linkGet(interface: interface)
guard linkResponses.count == 1 else {
throw Error.unexpectedResultSet(count: linkResponses.count, expected: 1)
}
return linkResponses[0].interfaceIndex
}
private func sendRequest(buffer: inout [UInt8]) throws {
log.trace("SEND-LENGTH: \(buffer.count)")
log.trace("SEND-DUMP: \(buffer[0..<buffer.count].hexEncodedString())")
let sendLength = try socket.send(buf: &buffer, len: buffer.count, flags: 0)
if sendLength != buffer.count {
log.warning("sent length \(sendLength) not equal to packet length \(buffer.count)")
}
}
private func receiveResponse() throws -> ([UInt8], Int) {
var buffer = [UInt8](repeating: 0, count: Self.receiveDataLength)
let size = try socket.recv(buf: &buffer, len: Self.receiveDataLength, flags: 0)
log.trace("RECV-LENGTH: \(size)")
log.trace("RECV-DUMP: \(buffer[0..<size].hexEncodedString())")
return (buffer, size)
}
private func parseResponse<T: Bindable>(infoType: UInt16? = nil, _ infoProvider: () -> T) throws -> (
[T], [[RTAttributeData]]
) {
var infos: [T] = []
var attrDataLists: [[RTAttributeData]] = []
var moreResponses = false
repeat {
var (buffer, size) = try receiveResponse()
var offset = 0
// 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)
}
moreResponses = header.moreResponses
}
guard offset == size else {
throw Error.unexpectedOffset(offset: offset, size: size)
}
} while moreResponses
return (infos, attrDataLists)
}
private func parseErrorCode(buffer: inout [UInt8], offset: Int) throws -> (Int32, Int) {
guard let errorPtr = buffer.bind(as: Int32.self, offset: offset) else {
throw BindError.recvMarshalFailure(type: "NetlinkErrorMessage", field: "error")
}
let rc = errorPtr.pointee
log.trace("RECV-ERR-CODE: \(rc)")
return (rc, offset + MemoryLayout<Int32>.size)
}
private func parseErrorResponse(buffer: inout [UInt8], offset: Int) throws -> Int {
var (rc, offset) = try parseErrorCode(buffer: &buffer, offset: offset)
log.trace(
"RECV-ERR-HEADER-DUMP: dump = \(buffer[offset..<offset + NetlinkMessageHeader.size].hexEncodedString())")
var header = NetlinkMessageHeader()
offset = try header.bindBuffer(&buffer, offset: offset)
log.trace("RECV-ERR-HEADER: \(header))")
guard rc == 0 else {
throw NetlinkDataError.responseError(rc: rc)
}
return offset
}
private func parseHeader(buffer: inout [UInt8], offset: Int) throws -> (NetlinkMessageHeader, Int) {
log.trace("RECV-HEADER-DUMP: dump = \(buffer[offset..<offset + NetlinkMessageHeader.size].hexEncodedString())")
var header = NetlinkMessageHeader()
var offset = try header.bindBuffer(&buffer, offset: offset)
log.trace("RECV-HEADER: \(header)")
switch header.type {
case NetlinkType.NLMSG_ERROR:
offset = try parseErrorResponse(buffer: &buffer, offset: offset)
case NetlinkType.NLMSG_DONE:
let rc: Int32
(rc, offset) = try parseErrorCode(buffer: &buffer, offset: offset)
guard rc == 0 else {
throw NetlinkDataError.responseError(rc: rc)
}
default:
break
}
return (header, offset)
}
private func parseAttributes(buffer: inout [UInt8], offset: Int, residualCount: Int) throws -> (
[RTAttributeData], Int
) {
var attrDatas: [RTAttributeData] = []
var offset = offset
var residualCount = residualCount
log.trace("RECV-RESIDUAL: \(residualCount)")
while residualCount > 0 {
var attr = RTAttribute()
log.trace(" RECV-ATTR-DUMP: dump = \(buffer[offset..<offset + RTAttribute.size].hexEncodedString())")
offset = try attr.bindBuffer(&buffer, offset: offset)
log.trace(" RECV-ATTR: len = \(attr.len) type = \(attr.type)")
let dataLen = Int(attr.len) - RTAttribute.size
if dataLen >= 0 {
log.trace(" RECV-ATTR-DATA-DUMP: dump = \(buffer[offset..<offset + dataLen].hexEncodedString())")
attrDatas.append(RTAttributeData(attribute: attr, data: Array(buffer[offset..<offset + dataLen])))
} else {
attrDatas.append(RTAttributeData(attribute: attr, data: []))
}
residualCount -= Int(attr.paddedLen)
offset += attr.paddedLen - RTAttribute.size
log.trace("RECV-RESIDUAL: \(residualCount)")
}
return (attrDatas, offset)
}
}