//===----------------------------------------------------------------------===//
// 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)
    }
}
