Help extending the 'mullvad-vpn' module for NixOS to include more configuration

The Issue

I’m looking to extend the NixOS module for Mullvad VPN to be able to configure the Mullvad daemon settings, which it stores in /etc/mullvad-vpn/settings.json.

It’s pretty straightforward to outline all of the available options in Nix format, and then use something like environment.etc."mullvad-vpn/settings.json".source with pkgs.formats.json.generate to turn that into an appropriate json file, but this approach has two issues:

  1. There is a section of the settings.json file called api_access_methods which mullvad expects to be able to write a unique(?) ID to when logging into a Mullvad account for the first time with mullvad account login, so it’s my impression that this file can’t be a symlink. (It’s worth noting I’m not very familiar with Rust which Mullvad is written in, so I’m not exactly clear on where this comes from.)
  2. The actual account number (a 16-digit number, credit-card style) is stored in /etc/mullvad-vpn/account-history.json, and the device configuration is created at the time of login at /etc/mullvad-vpn/device.json.

Possible Solutions

Based on this and some research, I’ve come up with a couple possible approaches:

  1. Use something like this blog post about declaratively setting up Mullvad with NixOS which would just tack on Mullvad CLI commands to the postStart of the systemd service to change the relevant settings. This feels a little clumsy, but would easily allow for supplying account details and adding mullvad account login to the postStart commands with some kind of check to make sure the user is not already logged in. It also limits configuring fancier stuff like custom endpoint lists which would be much easier to write in Nix/json.
  2. Outline all available configuration options in Nix format as described above, but instead use it to somehow modify the existing file in-place so that the necessary keys are still writable. I know this is definitely possible, but I’m not clear on exactly how to do so. This also still leaves out being able to configure in the account number. The account itself could be done with an activation script(?) or postStart command though.

My Question(s)

So I have a few questions based on my current understanding:

  1. What is the best-practice way to expand the module? Adding commands to the system service, modifying settings.json in-place, or some other approach I may be missing?
  2. If the first approach seems best, is postStart the way to go as detailed in the blog post I linked, or would it be better to use some other on-activation script?
  3. If the json approach seems best, how would I go about modifying the existing settings.json without just supplying the file wholesale?

References

  1. Example Mullvad settings.json
  2. The blog post detailing the first postStart method I discussed
  3. The current mullvad-vpn module
  4. My own sorta-working example module that generates settings.json from Nix
2 Likes

Hello there @SonarMonkey !

I wanted to achieve more or less what you have described here. I followed the Mullvad’s wiki documentation here and was able to only use a subset of the config keys I wanted to edit, I’ve ended up with the below working configuration :

{ pkgs, ... }:
let
  mullvadConfig =
    pkgs.writeText "mullvad-settings"(
      builtins.toJSON {
        allow_lan = true;
        auto_connect = true;
        block_when_disconnected = true;
      }
    );
in
{
  systemd = {
    services."mullvad-daemon".environment.MULLVAD_SETTINGS_DIR = "/var/lib/mullvad-vpn";
    tmpfiles.settings."10-mullvad-settings"."/var/lib/mullvad-vpn/settings.json"."C+" = {
      group = "root";
      mode = "0700";
      user = "root";
      argument = "${mullvadConfig}";
    };
  };
}

In my understanding, according to the docs, these IDs can be manually set as long as you link them correctly as in the given example (not tested, as not needed in my use-case).

As you said, that looks a bit dirty to me as well, even if it probably works. I’d rather configure something close to the daemon, than changing config after the service started.

I think providing a custom config (as per the wiki) to the system service looks to be best practice to me.

Could you let me know how you ended up doing it? Just for curiosity