libnetlink

Recently I wanted to refactor several functions, each function opened the special linux file /proc/net/route, searched for the passed interface-name and returned the queried value in human-readable format.

the file looks like:

Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask		MTU	Window	IRTT
enp4s0	00000000	0100A8C0	0003	0	0	1024	00000000	0	0	0

The old functions read the file line-by-line “parsed” each line with sscanf, checked if the line is for the interface and returned the corresponding column. The values of interest are IFace (NIC interface name) Gateway, and Metric. In most of the code-path all functions were called one after the other with the same interface-name.

The plan was to use the libnetlink to query the Gateway and Metric. I found the blog post getting linux routing table using netlink but I couldn’t get it to work, the metric was never returned.

The next step then was to try the libnetlink from iproute2. The command ip route list showed all the needed information. But trying to find some example code was not that easy. The github repository libnetlink example had some examples but nothing with routes.

but for the rescue the readme has a “More info:” section

If you need information about what other parameters/commands are available, you can always browse the iproute2 repo: https://github.com/shemminger/iproute2

searching the internet for examples or documentation for some hours and i still ended up in reading the original c-source code :(

so here we go with some example

Example in C++

Each entry in the linux routing table is represented as a struct RoutingEntry and the whole routing table is a std::vector<RoutingEntry>

struct RoutingEntry
{
    using IpAddress = boost::asio::ip::address_v4;
    struct
    {
        IpAddress address;
        unsigned char netmask{0};
    } destination;

    std::optional<IpAddress> gateway;
    std::uint32_t interfaceIdx{0};
    std::optional<std::uint32_t> metric;
};
using RoutingTable = std::vector<RoutingEntry>;

the next code is based on the original iproute2 sourcecode starting in the function iproute_list_flush_or_save:

extern "C"
{
#include <libnetlink.h>
#include <resolv.h>
}

// no filtering needed
int iproute_dump_filter(nlmsghdr*, int)
{
  return 0;
}

__u32 rtm_get_table(rtmsg* r, rtattr** tb)
{
  __u32 table = r->rtm_table;

  if (tb[RTA_TABLE])
    table = rta_getattr_u32(tb[RTA_TABLE]);
  return table;
}

// callback function used by libnetlink for each routing table entry
int append_route(nlmsghdr* n, void* arg)
{
  if (!arg)
  {
    return -1;
  }
  auto& arguments = *static_cast<RoutingTable*>(arg);

  auto* r = reinterpret_cast<rtmsg*>(NLMSG_DATA(n));
  struct rtattr* tb[RTA_MAX + 1];

  int len = static_cast<int>(n->nlmsg_len - NLMSG_LENGTH(sizeof(*r)));
  if (len < 0)
  {
    fmt::print("Wrong message length\n");
    return -1;
  }

  parse_rtattr(tb, RTA_MAX, RTM_RTA(r), len);
  auto table = rtm_get_table(r, tb);

   // currently only IPv4 is supported, but IPv6 support can easily be added
  if (r->rtm_family != AF_INET && table != RT_TABLE_MAIN)
  {
    return 0;
  }

  RoutingEntry entry;

  if (tb[RTA_DST] && r->rtm_dst_len)
  {
    boost::asio::ip::address_v4::bytes_type bytes;
    memcpy(bytes.data(), RTA_DATA(tb[RTA_DST]), RTA_PAYLOAD(tb[RTA_DST]));
    entry.destination.address = boost::asio::ip::make_address_v4(bytes);
    entry.destination.netmask = r->rtm_dst_len;
  }

  if (tb[RTA_GATEWAY])
  {
    boost::asio::ip::address_v4::bytes_type bytes;
    memcpy(bytes.data(), RTA_DATA(tb[RTA_GATEWAY]), RTA_PAYLOAD(tb[RTA_GATEWAY]));
    entry.gateway = boost::asio::ip::make_address_v4(bytes);
  }

  if (tb[RTA_OIF])
  {
    entry.interfaceIdx = rta_getattr_u32(tb[RTA_OIF]);
  }

  if (tb[RTA_PRIORITY])
  {
    entry.metric = rta_getattr_u32(tb[RTA_PRIORITY]);
  }

  arguments.push_back(std::move(entry));

  return 0;
}

std::vector<RoutingEntry> load_routingtable()
{
  rtnl_handle rth{};
  rth.fd = -1;

  if (rtnl_open(&rth, 0) < 0)
    exit(1);

  rtnl_set_strict_dump(&rth);

  if (rtnl_routedump_req(&rth, AF_INET, &iproute_dump_filter) < 0)
  {
    rtnl_close(&rth);
    return {};
  }

  std::vector<RoutingEntry> routingTable;
  if (rtnl_dump_filter(&rth, append_route, &routingTable) < 0)
  {
    rtnl_close(&rth);
    return {};
  }

  rtnl_close(&rth);

  routingTable.shrink_to_fit();
  return routingTable;
}

example usage:

int main()
{
    const auto table = load_routingtable();

    for (const auto& entry : table)
    {
        if (entry.destination.address.is_unspecified() && entry.destination.netmask == 0)
        {
            fmt::print("default");
        }
        else
        {
            fmt::print("{}/{}", entry.destination.address.to_string(), entry.destination.netmask);
        }
        if (entry.gateway)
        {
            fmt::print(" via {}", entry.gateway->to_string());
        }
        fmt::print(" deviceIndex {}", entry.interfaceIdx);
        if (entry.metric)
        {
            fmt::print(" metric {}", *entry.metric);
        }
        fmt::print("\n");
    }
}

and compile it with:

g++ -std=c++20 libnetlink_example.cpp -o route -lnetlink -lfmt -lmnl

when the program is executed on a system with above /proc/net/route file, the output is:

default via 192.168.0.1 deviceIndex 2 metric 1024

source-code: libnetlink_example.cpp