mildsunrise / node_netlink

âš’ Use Netlink from Node.js
MIT License
22 stars 2 forks source link

netlink

Node.js interface to Netlink, a Linux-specific socket protocol. Many management kernel APIs (firewall, network, etc.) are accessed through Netlink; as well as listening for related notifications from the kernel, but it also serves as a generic IPC mechanism.

It also implements the Generic Netlink protocol.

Note: This is early stage; API compatibility is not maintained. If you are going to use this, pin to a specific version.

💡 Examples  â€¢  📚 API reference

System APIs

In addition to plain Netlink and Generic Netlink, the following APIs are implemented:

The point is to wrap these interfaces in a 'high-level' way, complete with TypeScript typings. However some fields will be set to Buffer (i.e. unparsed) because its type is not yet known. You can help by improving the type definitions, see Design below.

Warning: Despite writing 'high-level' above, this is a direct exposure of low-level userspace APIs. You need to know what you're doing in many cases, how the API works (independently of the language you use it from), etc.

Usage

There's prebuilds for x86, x64, arm32v7 and arm64v8, so you don't need anything in those cases.

For other archs, you only need a compiler:

sudo apt install build-essential

Then, install this module:

npm install netlink

Netlink APIs are backwards-compatible, but the running kernel may have an older version of the APIs implemented. Thus, not all attributes, fields and commands listed in the typings are necessarily supported. For input, unknown attributes will be collected at the __unparsed field. For output, attempting to use unimplemented features will generally result in EINVAL.

Examples

Sending messages over a Netlink socket

const { createNetlink, Protocol } = require('netlink')
const socket = createNetlink(Protocol.ROUTE)

socket.on('message', (msg, rinfo) => {
  console.log(`Received message from ${rinfo.port}:`, msg)
})

// Send a Netlink message over the socket, to port 1
const data = Buffer.from('...')
socket.send(type, data, { flags: ..., port: 1 })

// Send message with REQUEST and ACK flags set, wait for a reply
const data = Buffer.from('...')
socket.request(type, data, { timeout: 1000 })
    .then(([ reply, rinfo ]) => {
        console.log('Received a reply:', reply)
    }, error => {
        console.error('Request failed:', error)
    })

Managing network configuration (rtnetlink)

const { rt, ifla, createRtNetlink } = require('netlink')
const socket = createRtNetlink()

// List addresses
const addrs = await socket.getAddresses()
console.log('Addresses:', addrs)

// List routes
const routes = await socket.getRoutes()
console.log('Routes:', routes)

// Set eth0 up
const links = await socket.getLinks()
const eth0 = links.filter(x => x.attrs.ifname === 'eth0')[0]
await socket.setLink({
  index: eth0.index,
  change: { up: true },
  flags: { up: true },
})

Subscribing to multicast groups (rtnetlink)

const { createRtNetlink } = require('netlink')

// ref: true causes this socket to keep the event loop alive
const socket = createRtNetlink({ ref: true })

socket.addMembership('IPV4_IFADDR')

socket.on('message', message => {
  for (const part of message) {
    if (part.kind === 'address') {
      const { data, attrs } = part
      const ifaceDesc = `${data.index} (${attrs.label})`
      // we're only subscribed to IPv4, no need to check `data.family`
      const addressDesc = `${attrs.address.join('.')}/${data.prefixlen}`
      console.log(`change in address ${addressDesc} of interface ${ifaceDesc}:`, part)
    }
  }
})

Managing 802.11 (aka wifi) interfaces

const { nl80211: iw, createNl80211 } = require('netlink')

// Prepare a socket (a promise is returned)
const socket = await createNl80211()

// List interfaces
const ifaces = await socket.getInterfaces()
for (const iface of ifaces.values())
  console.log(`Found inferface ${iface.ifindex}: ${iface.ifname} type ${iface.iftype}`)

// Operate on the first interface we find
const { ifindex } = [...ifaces.values()].find(x => 'ifindex' in x)

// Switch to a different frequency
await socket.request(iw.Commands.SET_CHANNEL, {
  ifindex,
  wiphyFreq: 5520,
  wiphyChannelType: 'HT40MINUS',
})

// Trigger a scan
await socket.request(iw.Commands.TRIGGER_SCAN, { ifindex })

Listing Generic Netlink families

const { createGenericNetlink, FlagsGet, genl } = require('netlink')
const socket = createGenericNetlink()

const families = await socket.ctrlRequest(
    genl.Commands.GET_FAMILY, {}, { flags: FlagsGet.DUMP })

console.log(`Listing ${families.length} families:`)
for (const family of families) {
    console.log(` - ${family.familyId}: ${JSON.stringify(family.familyName)}`)
    console.log(`   ${(family.ops || []).length} operations`)
    console.log(`   multicast groups:`)
    for (const mg of family.mcastGroups || []) {
        console.log(`    - ${mg.id}: ${JSON.stringify(mg.name)}`)
    }
}

Communication between sockets

const { createNetlink, Protocol } = require('netlink')

const socket1 = createNetlink(Protocol.ROUTE)
const socket2 = createNetlink(Protocol.ROUTE)
// module automatically generates unique IDs, but
// you can also pass a specific address to bind to
const socket3 = createNetlink(Protocol.ROUTE, { localPort: 5000 })

const port1 = socket1.address().port
const port2 = socket2.address().port
const port3 = socket3.address().port
console.log('Sockets bound to addresses:', port1, port2, port3)

socket1.on('message', (msg, rinfo) => {
  console.log(`${rinfo.port} says: ${msg[0].data}`)
})

// Send message from socket3 to socket1
socket3.send(100, Buffer.from('Hello!'), { port: port1 })

Sending raw data over a Netlink socket

const { createNetlink, RawNetlinkSocket, Protocol } = require('netlink')
const socket = new RawNetlinkSocket(Protocol.ROUTE)

// TODO

Design

For Netlink applications, it's recommended to use libnl. However it's a very small library and its API is very C oriented; exposing it properly would be too much work for the benefits, as well as introduce a native dependency. Instead it has been reimplemented in Javascript, which gives the user much more flexibility.

This makes the native code very thin (read: 500 lines), the rest is done in JavaScript. This introduces a performance penalty, but allows for little need to ever touch the native code.

The module is composed of several layers: