#![allow(clippy::arithmetic_side_effects)]

use {
    libc::{
        AF_INET, AF_INET6, AF_NETLINK, IFLA_INFO_DATA, IFLA_INFO_KIND, IFLA_LINKINFO, MSG_DONTWAIT,
        MSG_TRUNC, NDA_DST, NDA_LLADDR, NETLINK_EXT_ACK, NETLINK_GET_STRICT_CHK, NETLINK_ROUTE,
        NLA_ALIGNTO, NLA_TYPE_MASK, NLM_F_DUMP, NLM_F_DUMP_INTR, NLM_F_MULTI, NLM_F_REQUEST,
        NLMSG_DONE, NLMSG_ERROR, RTA_DST, RTA_GATEWAY, RTA_IIF, RTA_OIF, RTA_PREFSRC, RTA_PRIORITY,
        RTA_TABLE, RTM_GETLINK, RTM_GETNEIGH, RTM_GETROUTE, RTM_NEWLINK, RTM_NEWNEIGH,
        RTM_NEWROUTE, SO_RCVBUF, SOCK_RAW, SOL_NETLINK, SOL_SOCKET, nlattr, nlmsgerr, nlmsghdr,
        recv, send, setsockopt, sockaddr_nl, socket,
    },
    std::{
        collections::HashMap,
        ffi::CStr,
        io, mem,
        net::{IpAddr, Ipv4Addr, Ipv6Addr},
        os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
        ptr, slice,
    },
    thiserror::Error,
};

const NETLINK_RCVBUF_SIZE: i32 = 1 << 16;
const NLA_HDR_LEN: usize = align_to(mem::size_of::<nlattr>(), NLA_ALIGNTO as usize);
// GRE nested attributes (from include/uapi/linux/if_tunnel.h)
const IFLA_GRE_LOCAL: u16 = 6;
const IFLA_GRE_REMOTE: u16 = 7;
const IFLA_GRE_TTL: u16 = 8;
const IFLA_GRE_TOS: u16 = 9;
const IFLA_GRE_PMTUDISC: u16 = 10;

#[repr(C)]
#[allow(non_camel_case_types)]
struct ifinfomsg {
    ifi_family: u8,
    __ifi_pad: u8,
    ifi_type: u16,
    ifi_index: u32,
    ifi_flags: u32,
    ifi_change: u32,
}

pub struct NetlinkSocket {
    sock: OwnedFd,
    _nl_pid: u32,
}

impl NetlinkSocket {
    fn open() -> Result<Self, io::Error> {
        // Safety: libc wrapper
        let sock = unsafe { socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) };
        if sock < 0 {
            return Err(io::Error::last_os_error());
        }
        // SAFETY: `socket` returns a file descriptor.
        let sock = unsafe { OwnedFd::from_raw_fd(sock) };

        let enable = 1i32;
        for opt in [NETLINK_EXT_ACK, NETLINK_GET_STRICT_CHK] {
            // Safety: libc wrapper
            if unsafe {
                setsockopt(
                    sock.as_raw_fd(),
                    SOL_NETLINK,
                    opt,
                    &enable as *const _ as *const _,
                    mem::size_of::<i32>() as u32,
                )
            } < 0
            {
                return Err(io::Error::last_os_error());
            }
        }
        Ok(Self { sock, _nl_pid: 0 })
    }

    fn send(&self, msg: &[u8]) -> Result<(), io::Error> {
        if unsafe {
            send(
                self.sock.as_raw_fd(),
                msg.as_ptr() as *const _,
                msg.len(),
                0,
            )
        } < 0
        {
            return Err(io::Error::last_os_error());
        }
        Ok(())
    }

    pub(crate) fn recv(&self) -> Result<Vec<NetlinkMessage>, io::Error> {
        self.recv_with_flags(0)
    }

    pub(crate) fn recv_nonblocking(&self) -> Result<Option<Vec<NetlinkMessage>>, io::Error> {
        match self.recv_with_flags(MSG_DONTWAIT) {
            Ok(messages) => Ok(Some(messages)),
            Err(e)
                if e.raw_os_error()
                    .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EWOULDBLOCK) =>
            {
                Ok(None)
            }
            Err(e) => Err(e),
        }
    }

    fn recv_with_flags(&self, flags: i32) -> Result<Vec<NetlinkMessage>, io::Error> {
        // The kernel returns NLMSG_GOODSIZE (8k) as the recommended max allocation for netlink
        // responses. However that is not a hard cap, and netlink code can in theory return larger
        // messages. Out of caution we allocate a larger buffer AND use MSG_TRUNC to detect if that
        // is still not enough.
        let mut buf = [0u8; 8 * 1024]; // 8 KiB
        let mut messages = Vec::new();
        let mut multipart = true;
        'out: while multipart {
            multipart = false;
            // Safety: libc wrapper
            let len = unsafe {
                recv(
                    self.sock.as_raw_fd(),
                    buf.as_mut_ptr() as *mut _,
                    buf.len(),
                    flags | MSG_TRUNC,
                )
            };
            if len < 0 {
                return Err(io::Error::last_os_error());
            }
            if len == 0 {
                break;
            }

            let len = len as usize;
            if len > buf.len() {
                return Err(io::Error::other("netlink datagram truncated"));
            }
            let mut offset = 0;
            while offset < len {
                let message = NetlinkMessage::read(&buf[offset..])?;
                offset += align_to(message.header.nlmsg_len as usize, NLMSG_ALIGNTO as usize);
                multipart = message.header.nlmsg_flags & NLM_F_MULTI as u16 != 0;
                if message.header.nlmsg_flags & NLM_F_DUMP_INTR as u16 != 0 {
                    return Err(io::Error::new(
                        io::ErrorKind::Interrupted,
                        "netlink dump interrupted",
                    ));
                }
                match message.header.nlmsg_type as i32 {
                    NLMSG_ERROR => {
                        let err = message.error.unwrap();
                        if err.error == 0 {
                            // this is an ACK
                            continue;
                        }
                        return Err(io::Error::from_raw_os_error(-err.error));
                    }
                    NLMSG_DONE => break 'out,
                    _ => messages.push(message),
                }
            }
        }

        Ok(messages)
    }

    /// Opens a listener socket for netlink updates
    /// NETLINK_ROUTE socket subscribed to `groups` bitmask
    pub fn bind(groups: u32) -> Result<Self, io::Error> {
        let sock = Self::open()?;

        // Subscribe to multicast groups
        let mut addr: sockaddr_nl = unsafe { mem::zeroed() };
        addr.nl_family = AF_NETLINK as u16;
        addr.nl_groups = groups;
        if unsafe {
            libc::bind(
                sock.as_raw_fd(),
                &addr as *const _ as *const _,
                mem::size_of::<sockaddr_nl>() as u32,
            )
        } < 0
        {
            return Err(io::Error::last_os_error());
        }

        unsafe {
            setsockopt(
                sock.as_raw_fd(),
                SOL_SOCKET,
                SO_RCVBUF,
                &NETLINK_RCVBUF_SIZE as *const _ as *const _,
                mem::size_of::<i32>() as u32,
            );
        }

        Ok(sock)
    }

    #[inline]
    pub fn as_raw_fd(&self) -> RawFd {
        self.sock.as_raw_fd()
    }
}

#[derive(Debug, Clone)]
pub struct NetlinkMessage {
    pub(crate) header: nlmsghdr,
    data: Vec<u8>,
    error: Option<nlmsgerr>,
}

impl NetlinkMessage {
    fn read(buf: &[u8]) -> Result<Self, io::Error> {
        if mem::size_of::<nlmsghdr>() > buf.len() {
            return Err(io::Error::other("buffer smaller than nlmsghdr"));
        }

        // Safety: nlmsghdr is POD so read is safe
        let header = unsafe { ptr::read_unaligned(buf.as_ptr() as *const nlmsghdr) };
        let msg_len = header.nlmsg_len as usize;
        if msg_len < mem::size_of::<nlmsghdr>() || msg_len > buf.len() {
            return Err(io::Error::other("invalid nlmsg_len"));
        }

        let data_offset = align_to(mem::size_of::<nlmsghdr>(), NLMSG_ALIGNTO as usize);
        if data_offset >= buf.len() {
            return Err(io::Error::other("need more data"));
        }

        let (data, error) = if header.nlmsg_type == NLMSG_ERROR as u16 {
            if data_offset + mem::size_of::<nlmsgerr>() > buf.len() {
                return Err(io::Error::other(
                    "NLMSG_ERROR but not enough space for nlmsgerr",
                ));
            }
            (
                Vec::new(),
                // Safety: nlmsgerr is POD so read is safe
                Some(unsafe {
                    ptr::read_unaligned(buf[data_offset..].as_ptr() as *const nlmsgerr)
                }),
            )
        } else {
            (buf[data_offset..msg_len].to_vec(), None)
        };

        Ok(Self {
            header,
            data,
            error,
        })
    }
}

const fn align_to(v: usize, align: usize) -> usize {
    (v + (align - 1)) & !(align - 1)
}

struct NlAttrsIterator<'a> {
    attrs: &'a [u8],
    offset: usize,
}

impl<'a> NlAttrsIterator<'a> {
    fn new(attrs: &'a [u8]) -> Self {
        Self { attrs, offset: 0 }
    }
}

impl<'a> Iterator for NlAttrsIterator<'a> {
    type Item = Result<NlAttr<'a>, NlAttrError>;

    fn next(&mut self) -> Option<Self::Item> {
        let buf = &self.attrs[self.offset..];
        if buf.is_empty() {
            return None;
        }

        if NLA_HDR_LEN > buf.len() {
            self.offset = buf.len();
            return Some(Err(NlAttrError::InvalidBufferLength {
                size: buf.len(),
                expected: NLA_HDR_LEN,
            }));
        }

        let attr = unsafe { ptr::read_unaligned(buf.as_ptr() as *const nlattr) };
        let len = attr.nla_len as usize;
        let align_len = align_to(len, NLA_ALIGNTO as usize);
        if len < NLA_HDR_LEN {
            return Some(Err(NlAttrError::InvalidHeaderLength(len)));
        }
        if align_len > buf.len() {
            return Some(Err(NlAttrError::InvalidBufferLength {
                size: buf.len(),
                expected: align_len,
            }));
        }

        let data = &buf[NLA_HDR_LEN..len];

        self.offset += align_len;
        Some(Ok(NlAttr { header: attr, data }))
    }
}

fn parse_attrs(buf: &[u8]) -> Result<HashMap<u16, NlAttr<'_>>, NlAttrError> {
    let mut attrs = HashMap::new();
    for attr in NlAttrsIterator::new(buf) {
        let attr = attr?;
        attrs.insert(attr.header.nla_type & NLA_TYPE_MASK as u16, attr);
    }
    Ok(attrs)
}

#[derive(Clone)]
struct NlAttr<'a> {
    header: nlattr,
    data: &'a [u8],
}

#[derive(Debug, Error, PartialEq, Eq)]
enum NlAttrError {
    #[error("invalid buffer size `{size}`, expected `{expected}`")]
    InvalidBufferLength { size: usize, expected: usize },

    #[error("invalid nlattr header length `{0}`")]
    InvalidHeaderLength(usize),
}

impl From<NlAttrError> for io::Error {
    fn from(e: NlAttrError) -> Self {
        Self::other(e)
    }
}

fn bytes_of<T>(val: &T) -> &[u8] {
    let size = mem::size_of::<T>();
    unsafe { slice::from_raw_parts(slice::from_ref(val).as_ptr().cast(), size) }
}

const NLMSG_ALIGNTO: u32 = 4;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MacAddress(pub [u8; 6]);

impl MacAddress {
    pub fn new(bytes: [u8; 6]) -> Self {
        MacAddress(bytes)
    }

    pub fn as_bytes(&self) -> &[u8; 6] {
        &self.0
    }
}

impl std::fmt::Display for MacAddress {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
        )
    }
}

/// GRE tunnel information from netlink
///
/// Note: Only supports basic GRE header (no optional fields).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GreTunnelInfo {
    /// Source IP address for the GRE tunnel header
    pub local: IpAddr,
    /// Destination IP address for the GRE tunnel header
    pub remote: IpAddr,
    pub ttl: u8,
    pub tos: u8,
    /// PMTU discovery setting (IFLA_GRE_PMTUDISC)
    pub pmtudisc: u8,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceInfo {
    pub if_index: u32,
    pub gre_tunnel: Option<GreTunnelInfo>,
}

impl InterfaceInfo {
    pub fn is_gre(&self) -> bool {
        self.gre_tunnel.is_some()
    }
}

#[repr(C)]
struct InterfaceRequest {
    header: nlmsghdr,
    ifi: ifinfomsg,
}

pub fn netlink_get_interfaces(family: u8) -> Result<Vec<InterfaceInfo>, io::Error> {
    let sock = NetlinkSocket::open()?;

    // Safety: ifinfomsg is POD
    let mut req = unsafe { mem::zeroed::<InterfaceRequest>() };

    let nlmsg_len = mem::size_of::<nlmsghdr>() + mem::size_of::<ifinfomsg>();
    req.header = nlmsghdr {
        nlmsg_len: nlmsg_len as u32,
        nlmsg_flags: (NLM_F_REQUEST | NLM_F_DUMP) as u16,
        nlmsg_type: RTM_GETLINK,
        nlmsg_pid: 0,
        nlmsg_seq: 1,
    };

    req.ifi.ifi_family = family;
    sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?;

    let mut interfaces = Vec::new();
    for msg in sock.recv()? {
        if msg.header.nlmsg_type != RTM_NEWLINK {
            continue;
        }

        if let Some(if_info) = parse_rtm_ifinfomsg(&msg) {
            interfaces.push(if_info);
        }
    }

    Ok(interfaces)
}

pub(crate) fn parse_rtm_ifinfomsg(msg: &NetlinkMessage) -> Option<InterfaceInfo> {
    if msg.data.len() < mem::size_of::<ifinfomsg>() {
        return None;
    }

    let ifi = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const ifinfomsg) };
    let Ok(attrs) = parse_attrs(&msg.data[mem::size_of::<ifinfomsg>()..]) else {
        return None;
    };

    // Parse GRE tunnel information if this is a GRE interface
    let gre_tunnel = parse_gre_tunnel_info_from_linkinfo(&attrs);
    Some(InterfaceInfo {
        if_index: ifi.ifi_index,
        gre_tunnel,
    })
}

// Parse GRE tunnel information from netlink
fn parse_gre_tunnel_info_from_linkinfo(attrs: &HashMap<u16, NlAttr>) -> Option<GreTunnelInfo> {
    let gre = parse_linkinfo_data_for_kind(attrs, b"gre")?;

    let u8_from_bytes = |data: &[u8]| -> Option<u8> { data.first().copied() };

    let local = gre
        .get(&IFLA_GRE_LOCAL)
        .and_then(|a| parse_ip_address(a.data, AF_INET as u8))?;
    let remote = gre
        .get(&IFLA_GRE_REMOTE)
        .and_then(|a| parse_ip_address(a.data, AF_INET as u8))?;
    let ttl = gre.get(&IFLA_GRE_TTL).and_then(|a| u8_from_bytes(a.data))?;
    let tos = gre.get(&IFLA_GRE_TOS).and_then(|a| u8_from_bytes(a.data))?;
    let pmtudisc = gre
        .get(&IFLA_GRE_PMTUDISC)
        .and_then(|a| u8_from_bytes(a.data))?;

    Some(GreTunnelInfo {
        local,
        remote,
        ttl,
        tos,
        pmtudisc,
    })
}

fn parse_linkinfo_data_for_kind<'a>(
    attrs: &HashMap<u16, NlAttr<'a>>,
    expected_kind: &[u8],
) -> Option<HashMap<u16, NlAttr<'a>>> {
    let li = attrs.get(&IFLA_LINKINFO)?;
    // IFLA_LINKINFO contains nested attributes
    let info = parse_attrs(li.data).ok()?;
    let kind_attr = info.get(&IFLA_INFO_KIND)?;
    if kind_attr.data.is_empty() {
        return None;
    }
    let kind = CStr::from_bytes_until_nul(kind_attr.data).ok()?;
    if kind.to_bytes() != expected_kind {
        return None;
    }
    // Nested data (GRE attributes) is optional.
    let data_attr = info.get(&IFLA_INFO_DATA)?;
    parse_attrs(data_attr.data).ok()
}

/// Represents an entry in the neighbor table (ARP/NDP cache)
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NeighborEntry {
    // IPv4 or IPv6 address
    pub destination: Option<IpAddr>,
    // MAC address
    pub lladdr: Option<MacAddress>,
    // Interface index
    pub ifindex: i32,
    // NUD_* state
    pub state: u16,
}

impl NeighborEntry {
    #[inline]
    pub fn key(&self) -> Option<(i32, Ipv4Addr)> {
        match self.destination {
            Some(IpAddr::V4(ip)) => Some((self.ifindex, ip)),
            _ => None,
        }
    }
}

#[repr(C)]
#[allow(non_camel_case_types)]
struct ndmsg {
    ndm_family: u8,
    _ndm_pad1: u8,
    _ndm_pad2: u16,
    ndm_ifindex: i32,
    ndm_state: u16,
    _ndm_flags: u8,
    _ndm_type: u8,
}

#[repr(C)]
struct NeighRequest {
    header: nlmsghdr,
    ndm: ndmsg,
}

/// fetch the kernel's neighbor table (ARP/NDP cache)
pub fn netlink_get_neighbors(
    if_index: Option<u32>,
    family: u8,
) -> Result<Vec<NeighborEntry>, io::Error> {
    let sock = NetlinkSocket::open()?;

    // Safety: NeighRequest is POD
    let mut req = unsafe { mem::zeroed::<NeighRequest>() };

    let nlmsg_len = mem::size_of::<nlmsghdr>() + mem::size_of::<ndmsg>();
    req.header = nlmsghdr {
        nlmsg_len: nlmsg_len as u32,
        nlmsg_flags: (NLM_F_REQUEST | NLM_F_DUMP) as u16,
        nlmsg_type: RTM_GETNEIGH,
        nlmsg_pid: 0,
        nlmsg_seq: 1,
    };

    req.ndm.ndm_family = family;
    if let Some(idx) = if_index {
        req.ndm.ndm_ifindex = idx as i32;
    }

    sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?;

    let mut neighbors = Vec::new();

    for msg in sock.recv()? {
        if msg.header.nlmsg_type != RTM_NEWNEIGH {
            continue;
        }

        if msg.data.len() < mem::size_of::<ndmsg>() {
            continue;
        }

        if let Some(neighbor) = parse_rtm_newneigh(&msg, if_index) {
            neighbors.push(neighbor);
        }
    }

    Ok(neighbors)
}

pub fn parse_rtm_newneigh(msg: &NetlinkMessage, if_index: Option<u32>) -> Option<NeighborEntry> {
    if msg.data.len() < mem::size_of::<ndmsg>() {
        return None;
    }
    let nd_msg = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const ndmsg) };
    if let Some(idx) = if_index {
        if nd_msg.ndm_ifindex != idx as i32 {
            return None;
        }
    }
    let Ok(attrs) = parse_attrs(&msg.data[mem::size_of::<ndmsg>()..]) else {
        return None;
    };
    let mut neighbor = NeighborEntry {
        destination: None,
        lladdr: None,
        ifindex: nd_msg.ndm_ifindex,
        state: nd_msg.ndm_state,
    };
    if let Some(dst_attr) = attrs.get(&NDA_DST) {
        neighbor.destination = parse_ip_address(dst_attr.data, nd_msg.ndm_family);
    }
    if let Some(lladdr_attr) = attrs.get(&NDA_LLADDR) {
        if lladdr_attr.data.len() >= 6 {
            let mut mac = [0u8; 6];
            mac.copy_from_slice(&lladdr_attr.data[0..6]);
            neighbor.lladdr = Some(MacAddress(mac));
        }
    }
    Some(neighbor)
}

#[derive(Clone)]
pub struct RouteEntry {
    pub destination: Option<IpAddr>,
    pub gateway: Option<IpAddr>,
    pub pref_src: Option<IpAddr>,
    pub out_if_index: Option<i32>,
    pub in_if_index: Option<i32>,
    pub priority: Option<u32>,
    pub table: Option<u32>,
    pub protocol: u8,
    pub scope: u8,
    pub type_: u8,
    pub family: u8,
    pub dst_len: u8,
    pub flags: u32,
}

#[repr(C)]
#[allow(non_camel_case_types)]
struct rtmsg {
    rtm_family: u8,
    rtm_dst_len: u8,
    rtm_src_len: u8,
    rtm_tos: u8,
    rtm_table: u8,
    rtm_protocol: u8,
    rtm_scope: u8,
    rtm_type: u8,
    rtm_flags: u32,
}

#[repr(C)]
struct RouteRequest {
    header: nlmsghdr,
    rtm: rtmsg,
}

fn parse_ip_address(data: &[u8], family: u8) -> Option<IpAddr> {
    match family as i32 {
        AF_INET if data.len() == 4 => Some(IpAddr::V4(Ipv4Addr::new(
            data[0], data[1], data[2], data[3],
        ))),
        AF_INET6 if data.len() == 16 => {
            let mut segments = [0u16; 8];
            for i in 0..8 {
                segments[i] = ((data[i * 2] as u16) << 8) | (data[i * 2 + 1] as u16);
            }
            Some(IpAddr::V6(Ipv6Addr::from(segments)))
        }
        _ => None,
    }
}

pub fn netlink_get_routes(family: u8, table: u32) -> Result<Vec<RouteEntry>, io::Error> {
    let sock = NetlinkSocket::open()?;

    // Safety: RouteRequest is POD
    let mut req = unsafe { mem::zeroed::<RouteRequest>() };

    let nlmsg_len = mem::size_of::<nlmsghdr>() + mem::size_of::<rtmsg>();
    let table_attr_len = align_to(NLA_HDR_LEN + mem::size_of::<u32>(), NLA_ALIGNTO as usize);
    req.header = nlmsghdr {
        nlmsg_len: (nlmsg_len + table_attr_len) as u32,
        nlmsg_flags: (NLM_F_REQUEST | NLM_F_DUMP) as u16,
        nlmsg_type: RTM_GETROUTE,
        nlmsg_pid: 0,
        nlmsg_seq: 1,
    };

    req.rtm.rtm_family = family;

    let mut req_buf = bytes_of(&req)[..nlmsg_len].to_vec();
    push_nlattr(&mut req_buf, RTA_TABLE, &table);
    sock.send(&req_buf)?;

    let mut routes = Vec::new();

    for msg in sock.recv()? {
        if msg.header.nlmsg_type != RTM_NEWROUTE {
            continue;
        }
        if let Some(route) = parse_rtm_newroute(&msg) {
            // with strict checking, the kernel should return routes for the requested table only
            debug_assert!(route.table == Some(table));
            if route.table == Some(table) {
                routes.push(route);
            }
        }
    }

    Ok(routes)
}

pub fn parse_rtm_newroute(msg: &NetlinkMessage) -> Option<RouteEntry> {
    if msg.data.len() < mem::size_of::<rtmsg>() {
        return None;
    }

    let rt_msg = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const rtmsg) };
    let Ok(attrs) = parse_attrs(&msg.data[mem::size_of::<rtmsg>()..]) else {
        return None;
    };
    let mut route = RouteEntry {
        destination: None,
        gateway: None,
        pref_src: None,
        out_if_index: None,
        in_if_index: None,
        priority: None,
        table: Some(u32::from(rt_msg.rtm_table)),
        protocol: rt_msg.rtm_protocol,
        scope: rt_msg.rtm_scope,
        type_: rt_msg.rtm_type,
        family: rt_msg.rtm_family,
        dst_len: rt_msg.rtm_dst_len,
        flags: rt_msg.rtm_flags,
    };
    if let Some(dst_attr) = attrs.get(&RTA_DST) {
        route.destination = parse_ip_address(dst_attr.data, rt_msg.rtm_family);
    }
    if let Some(gateway_attr) = attrs.get(&RTA_GATEWAY) {
        route.gateway = parse_ip_address(gateway_attr.data, rt_msg.rtm_family);
    }

    let u32_from_ne_bytes = |data: &[u8]| -> Option<u32> {
        data.get(..4)
            .map(|data| u32::from_ne_bytes([data[0], data[1], data[2], data[3]]))
    };

    if let Some(oif_attr) = attrs.get(&RTA_OIF) {
        route.out_if_index = u32_from_ne_bytes(oif_attr.data).map(|i| i as i32);
    }
    if let Some(iif_attr) = attrs.get(&RTA_IIF) {
        route.in_if_index = u32_from_ne_bytes(iif_attr.data).map(|i| i as i32);
    }
    if let Some(priority_attr) = attrs.get(&RTA_PRIORITY) {
        route.priority = u32_from_ne_bytes(priority_attr.data);
    }
    if let Some(table_attr) = attrs.get(&RTA_TABLE) {
        route.table = u32_from_ne_bytes(table_attr.data);
    }
    if let Some(prefsrc_attr) = attrs.get(&RTA_PREFSRC) {
        route.pref_src = parse_ip_address(prefsrc_attr.data, rt_msg.rtm_family);
    }
    Some(route)
}

fn push_nlattr<T>(buf: &mut Vec<u8>, attr_type: u16, value: &T) {
    let attr_len = NLA_HDR_LEN + mem::size_of::<T>();
    let aligned_len = align_to(attr_len, NLA_ALIGNTO as usize);
    let attr = nlattr {
        nla_len: attr_len as u16,
        nla_type: attr_type,
    };
    buf.extend_from_slice(bytes_of(&attr));
    buf.extend_from_slice(bytes_of(value));
    buf.resize(buf.len() + (aligned_len - attr_len), 0);
}
