npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

node-rtnetlink

v0.0.7

Published

Linux rtnetlink for Node.js: list, create, modify and delete network interfaces

Readme

node-rtnetlink

CI npm

Linux rtnetlink for Node.js, written in TypeScript: list, find, create, modify and delete network interfaces, and assign IP addresses to them.

  • Bring your own socket. node-rtnetlink talks through a node-netlink socket that you pass in. It never opens, binds or closes a socket itself.
  • High-level link API. Links are plain objects with name, mtu, address, flags, linkinfo and more, instead of raw attributes.
  • Low-level access. Requests with struct ifinfomsg and raw rtattrs, and the codecs behind the link API, are available as well.
  • Checked against the C headers. The layouts of struct ifinfomsg and struct rtattr are defined with ya-struct and compared with <linux/rtnetlink.h> in the tests, as are all constants.

Requirements

  • Linux
  • Node.js 24 or newer

Installation

npm install node-rtnetlink node-netlink

To open the socket with po6 as in the examples below, also install po6 with its syscall and memory backends and a poller:

npm install po6 syscall-napi buffer2address @k13engineering/uv-poll

Usage

Setup

Open and bind a NETLINK_ROUTE socket, attach node-netlink to it and node-rtnetlink to node-netlink:

import { createPoller } from "@k13engineering/uv-poll";
import { pinBuffer } from "buffer2address";
import {
  AF_NETLINK,
  NETLINK_ROUTE,
  createNetlinkSocket,
  createPo6NetlinkTransport,
  formatNetlinkAddress,
} from "node-netlink";
import { createKernelAbiFor, createLinuxKernelInterface, createPo6Api, hostAbi } from "po6";
import { syscall, syscallNumbers } from "syscall-napi";
import { createRtnetlink } from "node-rtnetlink";

const kernelAbi = createKernelAbiFor({ machineAbi: hostAbi });
const po6 = createPo6Api({
  kernelInterface: createLinuxKernelInterface({ syscall, syscallNumbers }),
  kernelAbi,
  memory: { pinBuffer },
});

// <linux/net.h> and <asm-generic/fcntl.h>
const SOCK_RAW = 3n;
const SOCK_CLOEXEC = 0o2000000n;

// your socket: opened, bound and closed by your code
const { errno, fd } = po6.socket({ domain: AF_NETLINK, type: SOCK_RAW | SOCK_CLOEXEC, protocol: NETLINK_ROUTE });
if (errno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "socket()", errno });
}

const { errno: bindErrno } = po6.bind({ fd, sockaddr: formatNetlinkAddress({ address: { nl_pid: 0n, nl_groups: 0n } }) });
if (bindErrno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "bind()", errno: bindErrno });
}

const netlink = createNetlinkSocket({ transport: createPo6NetlinkTransport({ fd, po6, kernelAbi, createPoller }) });
const rt = createRtnetlink({ netlink });

When you are done, detach node-netlink and close the socket:

netlink.detach();
po6.close({ fd });

Links

// all links of the network namespace
const links = await rt.link.listAll();
links.forEach(({ ifindex, name, mtu, flags }) => {
  console.log(`${ifindex}: ${name} mtu ${mtu} ${flags.IFF_UP ? "UP" : "DOWN"}`);
});

// look up a link and fetch its current state
const lo = await rt.link.findOneBy({ name: "lo" });
const { address, flags } = await lo.fetch();

Creating and changing links needs CAP_NET_ADMIN:

const bridge = await rt.link.createLink({ name: "br0", linkinfo: { kind: "bridge" } });
const port = await rt.link.createLink({ name: "dummy0", linkinfo: { kind: "dummy" } });

await port.modify({ masterIndex: bridge.ifindex, flags: { IFF_UP: true } });
await bridge.modify({ mtu: 1400, flags: { IFF_UP: true } });

await port.deleteLink();
await bridge.deleteLink();

Errors of the kernel reject with an error that has the errno attached, e.g. creating link failed with EPERM.

Addresses

const eth0 = await rt.link.findOneBy({ name: "eth0" });

// the kernel adds the route to the subnet by itself
await rt.address.add({ ifindex: eth0.ifindex, address: "192.0.2.10", prefixLength: 24 });
await rt.address.add({ ifindex: eth0.ifindex, address: "2001:db8::10", prefixLength: 64, flags: { IFA_F_NODAD: true } });

const addresses = await rt.address.listAll({ ifindex: eth0.ifindex });
addresses.forEach(({ family, address, prefixLength }) => {
  console.log(`${family} ${address}/${prefixLength}`);
});

await rt.address.remove({ ifindex: eth0.ifindex, address: "192.0.2.10", prefixLength: 24 });

Addresses of a link in another network namespace are assigned with a createRtnetlink() instance whose socket was created in that namespace, as RTM_NEWADDR has no attribute for the namespace.

Network namespaces

A NETLINK_ROUTE socket works on the network namespace it was created in. Links can be created in, or moved to, another namespace, given by a file descriptor referring to it (IFLA_NET_NS_FD) or the pid of a process in it (IFLA_NET_NS_PID):

// e.g. the namespace of a container
const netns = { fd: po6.open({ pathname: "/proc/1234/ns/net", flags: O_RDONLY | O_CLOEXEC }).fd };

// a macvtap on eth0 of this namespace, created directly in the other one
const eth0 = await rt.link.findOneBy({ name: "eth0" });
await rt.link.createLinkInNamespace({
  netns,
  name: "macvtap0",
  linkIndex: eth0.ifindex,
  linkinfo: { kind: "macvtap", data: { mode: "bridge" } },
});

// or move an existing link there
await (await rt.link.findOneBy({ name: "dummy0" })).moveToNamespace({ netns });

The kernel picks the index of the link in the other namespace and does not report it, so createLinkInNamespace() needs a name and resolves without a link. To work with the link afterwards, e.g. to bring it up, use a createRtnetlink() instance whose socket was created in that namespace. With po6, enter the namespace with setns(), create the socket and switch back, see the po6 README.

Notifications

Bind the socket to RTMGRP_LINK and parse the messages node-netlink passes to onMessage:

import { hostStructures, parseLinkAttributes, parseLinkMessage, RTMGRP_LINK } from "node-rtnetlink";

// bind with nl_groups: RTMGRP_LINK, then
const netlink = createNetlinkSocket({
  transport,
  onMessage: ({ message }) => {
    const linkMessage = parseLinkMessage({ message });

    if (linkMessage !== undefined) {
      const { attributes } = parseLinkAttributes({ rta: linkMessage.rta, structures: hostStructures });
      console.log(linkMessage.header.nlmsg_type, attributes.name);
    }
  },
});

See examples/ for complete programs that list links, create a bridge, a veth pair and a macvtap link, and monitor link changes.

API

All functions take a single object of arguments.

createRtnetlink({ netlink, structures? })

  • netlink: a node-netlink socket, or anything with its talk() and tryTalk(), e.g. a fake in tests
  • structures: layouts of the kernel structures, hostStructures by default

Returns { link, address, talk, tryTalk }.

rt.link

| Function | Description | | --- | --- | | listAll() | resolves with a TLinkInfo for every link | | findAllBy(criteria) | resolves with all links that match the criteria | | findOneBy(criteria) | resolves with the only matching link, rejects if there is none or more than one | | tryFindOneBy(criteria) | like findOneBy(), but resolves with undefined instead of rejecting | | fromIndex({ ifindex }) | returns the link with this index, without talking to the kernel | | createLink({ flags?, ...attributes }) | creates a link and resolves with it | | createLinkInNamespace({ netns, name, flags?, ...attributes }) | creates a link directly in another network namespace, see Network namespaces |

The criteria are link attributes, type and flags. A link matches if all given values are equal. For linkinfo, only the given fields are compared, and for flags, only the given flags. The links are filtered after dumping all of them, as the kernel can only look up single links by name or index.

createLink() needs to know the index of the new link. With a name, the kernel picks the index: kernels since 6.3 report the new link back (NLM_F_ECHO), older ones are asked for the link with that name afterwards. If the name is taken, it rejects with EEXIST. Without a name, nothing identifies the link on older kernels, so createLink() requests the next free index explicitly and retries up to 3 times if the kernel reports EEXIST or EBUSY, e.g. because another process created a link at the same time, or because a link created along with the new one, like the peer of a veth link, took the index. Give links a name where possible.

A link (TLink) has:

| Member | Description | | --- | --- | | ifindex | the index of the link | | fetch() | resolves with the current TLinkInfo, rejects with ENODEV if the link does not exist | | modify({ flags?, ...attributes }) | changes the given attributes and flags | | deleteLink() | deletes the link | | moveToNamespace({ netns }) | moves the link into another network namespace, where it may get another index. The handle is of no use afterwards |

TLinkInfo contains ifindex, type (ARPHRD_*), flags, the link attributes the kernel reported and unknownAttributes, the raw rtattrs node-rtnetlink has no definition for.

Link attributes

| Attribute | rtattr | Type | | --- | --- | --- | | name | IFLA_IFNAME | string | | mtu | IFLA_MTU | number | | address | IFLA_ADDRESS | Uint8Array | | broadcast | IFLA_BROADCAST | Uint8Array | | txqlen | IFLA_TXQLEN | number | | masterIndex | IFLA_MASTER | number, 0 removes the link from its master | | linkIndex | IFLA_LINK | number, the lower link of a virtual link, e.g. the parent of a macvlan link | | linkinfo | IFLA_LINKINFO | { kind?: string, slaveKind?: string, data?: TLinkinfoData } |

linkinfo.data holds the kind specific attributes of IFLA_INFO_DATA. For other kinds than the ones below, data is left out when parsing, and setting it throws.

| Kind | data | | --- | --- | | macvlan, macvtap | { mode?: "private" \| "vepa" \| "bridge" \| "passthru" \| "source" }, the kernel uses "vepa" if the mode is omitted | | veth | { peer?: { name?: string, mtu?: number, address?: Uint8Array } }, the other end, which the kernel creates together with the link and names like veth0 if no name is given. The kernel does not report the peer of existing links |

// a veth pair, like ip link add veth0 type veth peer name veth1
const veth0 = await rt.link.createLink({ name: "veth0", linkinfo: { kind: "veth", data: { peer: { name: "veth1" } } } });

const eth0 = await rt.link.findOneBy({ name: "eth0" });
const macvtap = await rt.link.createLink({
  name: "macvtap0",
  linkIndex: eth0.ifindex,
  linkinfo: { kind: "macvtap", data: { mode: "bridge" } },
});

Link flags

flags is an object with the IFF_* flags of <linux/if.h> as keys, e.g. { IFF_UP: true, IFF_PROMISC: false }. In TLinkInfo, all flags are present. For modify() and createLink(), true sets a flag, false clears it, and flags that are not given stay unchanged.

rt.address

| Function | Description | | --- | --- | | add({ ifindex, address, prefixLength, peer?, broadcast?, label?, scope?, flags? }) | assigns an address to a link, rejects with EEXIST if it has it already | | remove({ ifindex, address, prefixLength }) | removes an address from a link, rejects with EADDRNOTAVAIL if it does not have it | | listAll({ ifindex?, family? }) | resolves with a TAddressInfo for every address, of all links or the given one, and of both or the given family |

Addresses are strings like "192.0.2.1" or "2001:db8::1", the family ("inet" or "inet6") follows from them. add() sends the address as IFA_LOCAL and IFA_ADDRESS, or peer as IFA_ADDRESS for point-to-point links. scope is "universe" (default), "site", "link", "host" or "nowhere". flags is an object with the IFA_F_* flags as keys, e.g. { IFA_F_NODAD: true }, sent in ifa_flags and, as some do not fit there, in IFA_FLAGS.

TAddressInfo contains ifindex, family, address, prefixLength, scope, all flags, and peer, broadcast and label if the kernel reports them, as well as unknownAttributes, e.g. IFA_CACHEINFO. IFA_F_SECONDARY and IFA_F_TEMPORARY are the same bit, which marks temporary addresses for IPv6.

Like rt.talk(), rt.address.talk({ header, ifa, rta?, timeoutMs? }) and rt.address.tryTalk() send address requests (RTM_NEWADDR, RTM_DELADDR or RTM_GETADDR) with a struct ifaddrmsg and resolve with the responses as { header, ifa, rta }. For notifications of RTMGRP_IPV4_IFADDR and RTMGRP_IPV6_IFADDR, parseAddressMessage({ message }) parses node-netlink messages of address types, and addressInfoOf({ message }) turns them into a TAddressInfo.

Low-level API

rt.talk({ header, ifi, rta?, timeoutMs? }) sends a link request (RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK or RTM_SETLINK) and resolves with the responses as { header, ifi, rta }. Missing ifi fields are 0. rt.tryTalk() resolves with { errno, messages } instead of rejecting.

import { NLM_F_DUMP } from "node-netlink";
import { AF_PACKET, RTM_GETLINK } from "node-rtnetlink";

const messages = await rt.talk({
  header: { nlmsg_type: RTM_GETLINK, nlmsg_flags: NLM_F_DUMP },
  ifi: { ifi_family: AF_PACKET },
});

| Function | Description | | --- | --- | | parseLinkMessage({ message, structures? }) | parses a node-netlink message of a link type, undefined for other types | | formatIfinfoPayload({ ifi, rta?, structures }), parseIfinfoPayload({ payload, structures }) | struct ifinfomsg followed by attributes | | formatIfaddrPayload({ ifa, rta?, structures }), parseIfaddrPayload({ payload, structures }) | struct ifaddrmsg followed by attributes | | formatAddressFlags({ flags }), parseAddressFlags({ bits }) | address flags from and to the bits of IFA_FLAGS | | formatAttributes({ attributes, structures }), parseAttributes({ data, structures }) | lists of rtattr | | formatLinkAttributes({ attributes, structures }), parseLinkAttributes({ rta, structures }) | link attributes from and to rtattrs | | formatLinkFlags({ flags }), parseLinkFlags({ ifi_flags }) | link flags from and to ifi_flags and ifi_change | | parseIpAddress({ address }), formatIpAddress({ bytes }) | IPv4 and IPv6 addresses from and to their bytes in network byte order, formatted as recommended by RFC 5952 | | typeOfAttribute({ attribute }) | the type of an rtattr without NLA_F_NESTED and NLA_F_NET_BYTEORDER | | stringCodec, u32Codec, bytesCodec | codecs of attribute values |

Pass hostStructures as structures, or createRtnetlinkStructuresFor({ abi }) for another byte order. The ya-struct definitions of the structures are exported as ifinfomsgDefinition, rtattrDefinition and ifaddrmsgDefinition.

Constants

AF_UNSPEC, AF_PACKET, AF_INET, AF_INET6, RTM_*LINK, RTM_*ADDR, RTMGRP_LINK, RTMGRP_IPV4_IFADDR, RTMGRP_IPV6_IFADDR, the address attributes IFA_*, the address flags IFA_F_*, the scopes RT_SCOPE_*, the attribute types IFLA_*, IFLA_INFO_* and IFLA_MACVLAN_*, the macvlan modes MACVLAN_MODE_*, NLA_F_*, NLA_TYPE_MASK and the link flags IFF_* are exported as bigints.

Migrating from 0.0.x

  • node-rtnetlink no longer opens sockets. Replace rtnetlink.open() with your own socket and node-netlink socket, see Setup, and createRtnetlink({ netlink }). close() is replaced by detach() of node-netlink and closing the socket yourself.
  • The package is an ES module with named exports, there is no default export anymore.
  • ifindex, mtu and masterIndex are numbers instead of bigints, address is a Uint8Array instead of an array of numbers.
  • fetch() no longer takes provideUnknown, unknown attributes are always returned as unknownAttributes. It also returns type and flags.
  • findAllBy() matches all given criteria on the client side, family is no criterion anymore.
  • tryTalk() resolves with { errno, messages } instead of { errorCode, packets }.
  • on() and once() are replaced by onMessage of node-netlink and parseLinkMessage().
  • The license is LGPL-2.1, like node-netlink.

Development

npm ci
npm run build       # transpile to dist/
npm run type-check
npm run test        # mocha with c8, 100% coverage required
npm run lint

The sources are in lib/, tests are next to them as *.spec.ts:

  • lib/rtnetlink.ts: link requests on top of an injected node-netlink socket
  • lib/link.ts: the high-level link API
  • lib/link-attributes.ts, lib/linkinfo.ts, lib/link-flags.ts: link attributes, linkinfo and flags
  • lib/address.ts, lib/ip-address.ts: the address API and IP addresses
  • lib/ifinfo.ts, lib/ifaddr.ts, lib/rtattr.ts, lib/structures.ts, lib/constants.ts: message formats, structure layouts and constants

The unit tests run against the fake kernel in lib/test-support/fake-kernel.ts. lib/index.spec.ts talks to the kernel of the host, which needs no privileges for reading. If sudo works without a password, it also creates, modifies and deletes links in a fresh network namespace, creates a macvtap in a second one and assigns addresses to it there. The structure layouts and constants are compared with the C headers by compiling C programs, so gcc and the Linux headers are required.

Releases are published by pushing a tag like v0.1.0, which builds the package, merges package.npm.json into package.json and sets the version.

License

LGPL-2.1, see LICENSE.