Blog

Victor is a full stack software engineer who loves travelling and building things. Most recently created Ewolo, a cross-platform workout logger.

Building a VPN kill switch on Ubuntu using Node.js

In this tutorial we will build a VPN kill switch on Ubuntu using Node.js. While there are a few different approaches to doing this, my personal need was as simple as sending a notification message and killing all browser windows whenever the VPN connection is disconnected. Thus, this tutorial will focus simply on the ability to send an action on VPN disconnection. Also note that this tutorial is specific to Ubuntu Gnome due to the technology stack involved.

On a Gnome based system, the networking is handled by NetworkManager which uses the D-Bus to communicate with other processes and send notifications. One of the D-Bus interfaces that the NetworkManager provides is: org.freedesktop.NetworkManager.VPN.Connection. This interface has a PropertiesChanged signal which we can use to investigate the current VPN connection status.

There are many libraries written which allow is to connect to the dbus for various languages, the most notable one being Python's import dbus. On Ubuntu you can install d-feet which will allow you to browse the dbus objects and interfaces. In this tutorial, we will use Node.js and the dbus-native package.

D-Feet in action

So much for the theory, lets look at some code:

const systemBus = dbus.systemBus();

systemBus.connection.on("message", function(msg) {
  if (
    msg.interface === "org.freedesktop.NetworkManager.VPN.Connection" &&
    msg.member === "PropertiesChanged"
  ) {
    const vpnState = getVpnProperty(msg.body, "State");
    if (vpnState === 2) {
      console.log("Vpn connected " + getVpnProperty(msg.body, "Id"));
    } else if (vpnState === 4) {
      console.log("Vpn disconnected!");
    }
  }
});

systemBus.connection.on("error", function(err) {
  console.error("error nativeDbus: " + err);
});

systemBus.addMatch("type='signal'", () => {
  console.dir(systemBus.signals);
});

The above code is actually pretty straightforward:

  • get access to the system bus
  • attach a handler on any message that the bus receives
    • check if the message interface is of the type that we are interested in and that it has the correct member
    • get the VPN connection state from the message body
  • attach an error handler to the system bus connection
  • ask the system bus to forward any signal events to our application

The message body received looks like the following:

const body = [
  [
    ["Default", [[{ type: "b", child: [] }], [true]]],
    ["Dhcp4Config", [[{ type: "o", child: [] }], ["/"]]],
    ["Dhcp6Config", [[{ type: "o", child: [] }], ["/"]]],
    ["Id", [[{ type: "s", child: [] }], ["your.vpn.connection"]]],
    [
      "Ip4Config",
      [
        [{ type: "o", child: [] }],
        ["/org/freedesktop/NetworkManager/IP4Config/106"]
      ]
    ],
    ["Ip6Config", [[{ type: "o", child: [] }], ["/"]]],
    ["State", [[{ type: "u", child: [] }], [2]]],
    ["Type", [[{ type: "s", child: [] }], ["vpn"]]],
    ["VpnState", [[{ type: "u", child: [] }], [5]]]
  ]
];

Thus to easily parse the above, the getVpnProperty function simply returns the property value in the message body if found:

const getVpnProperty = (body, propertyName) => {
  const groups = body[0];

  for (const group of groups) {
    if (group[0] === propertyName) {
      return group[1][1][0];
    }
  }

  return null;
};

In the above sample code, we simply log the current VPN connection state to the console but we could also get a handle to the session bus and send notifications as provided in the dbus-native package readme.

The trickiest part about using the dbus was the fact that the dbus-native package requires you get a dbus object via it's service and interface if you want to directly communicate with it. However, the org.freedesktop.NetworkManager.VPN.Connection interface only pops up on the ActiveConnection object once a VPN connection is established. Therefore, it was much easier to listen to all events and just filter out the one that we are interested in. Moreover, I could not figure out the addMatch rule directly filter out the org.freedesktop.NetworkManager.VPN.Connection interface and had to do it in the code.

Other approaches to building a VPN kill switch involve using iptables to only allow traffic over the VPN network. However, this requires that you have an always-on VPN connection.

This concludes this tutorial and as always, I'm very happy to receive any feedback. Happy VPN'ing!

HackerNews submission / discussion

Back to the article list.