Skip to main content

Contributing to NixOS

I needed a VPN client in NixOS (the operating system I've installed and have been using on my work machine) that would work with my company's VPN. Unfortunately, neither openfortivpn nor strongswan worked, but I was told by some coworkers that libreswan was working well. The libreswan package didn't yet exist in the main NixOS nixpkgs repository, so I took it upon myself to add it in! Along the way I also learned about the nix expression language and the community behind both nix and NixOS.

Our IT has some special stuff set up for the VPN connection, and no matter what I tried I couldn't get openfortivpn nor strongswan to play nicely with it. I was especially surprised that strongswan didn't work, since it's from the same original codebase as libreswan and many have said it is more featureful. However, since I knew that libreswan could work with our VPN, I was determined to get it working on NixOS.

libreswan

A typical libreswan install includes multiple binaries and shell scripts, and at runtime it normally should be run as either a sysv or systemd service. Since NixOS uses systemd as an integral part of its service management, I figured that libreswan would work well in NixOS (eventually).

Nix expression language

I had been using NixOS for about 3-4 months, but had minimal exposure to the nix expression language. I had written a few simple package modifications, but mostly I was just copy and pasting from online resources to get things to work. I hadn't taken the time to fully read up on the language or resources, however I had a loose understanding of what was going on in the language from what I had written and seen. Still, I learned a lot about the language from this experience. It is a pretty standard functional language, with somewhat different syntax than others, and I think it was this different syntax that can be blamed for slowing down my comprehension. This isn't to say it has bad syntax; it's just not quite as obvious as other functional languages at first glance. I would have been a bit more at home with something like Guix, but one of the reasons I installed NixOS was so I would get some second-hand learning :)

For anyone looking for an introduction to the nix language, I advise you to read the manual, which covers the basics of the language. Beyond that, looking at other packages and how they structure their code is useful too. I couldn't find much documentation of the standard library functions in general; this is the one area in which I think NixOS is seriously lacking. Many of the library functions I saw being used in other packages had no easy documentation that I could find by googling around, so I simply resorted to reading the actual implementation to figure things out. I also browsed through the standard library functions to find what was available, for instance what string operations were already there (sadly, no trim function). The "standard" library functions are available here. Nothing too fancy going on there.

NixOS package development

The typical way that one goes about sanely developing a new package for NixOS is by forking the nixpkgs repository, adding your package into the package tree in some place that makes sense, and then submitting a pull request of your fork. More details of this process can by found on the NixOS wiki. The trick here is that you usually want to develop and test against a stationary target, so after forking the repository it's best to make a branch that is at the same commit that your NixOS system is on (which can be found by running nixos-version). Then, as you add your new package and install it, there won't be unnecessary building of all the updated packages in the latest NixOS repo.

To use your forked repository, simply run

$ nixos-rebuild -I nixpkgs=/path/to/clone/of/your/repository switch

or if you're building the package as a local (not system-wide) package, run

$ nix-env -f /path/to/clone/of/your/repository [nix-env install arguments]

Adding libreswan to nixpkgs

I will detail two parts; first, adding the nix expression to build the libreswan suite of binaries and shell scripts, and second, adding the nix expression to expose the libreswan ipsec service as a system service in NixOS.

Building libreswan from nixpkgs

The standard libraries/functions of NixOS includes stdenv, which for most GNU autotools packages will do the Right Thing™. Usually you can simply give it a source to download from, and some extra meta information like the package name, homepage, description, and packages it relies on, and it will handle the rest.

stdenv.mkDerivation {
  name = "libsomething-1.0";
  src = fetchurl {
    url = http://something.com/libsomething-1.0.tar.bz2;
    md5 = "45d90a9881e68d89aabff994d7e0b2d4";
  };
}

Notice that the various fetch* functions all require some sort of hash value to be provided, so NixOS always verifies the integrity of the download.

Unfortunately, libreswan isn't quite autotooled, although it still uses a GNU Makefile to build. I had to manually look through the Makefile(s) to figure out which variables should be set to which values for the NixOS build to work. I also found that much of libreswan had hardcoded paths to the typical /usr/bin install directory; I'm not sure how well the package would work if you wanted to install it elsewhere. However, since NixOS puts nothing in /usr/bin (besides env), I had to introduce some hooks to performs simple sed find/replaces.

The stdenv.mkDerivation expression works in phases, where each phase occurs at each step of the regular autotools build process. So there's a patch phase, a configuration phase, a make phase, and many other phases for the in-between points of the build. So for instance, to fix the hard-coded paths to point to those where nix would be putting the output of the build (among other things), I had

prePatch = ''
  # Correct bash path
  sed -i -e 's|/bin/bash|/usr/bin/env bash|' mk/config.mk

  # Fix systemd unit directory, and prevent the makefile from trying to reload the systemd daemon
  sed -i -e 's|UNITDIR=.*$|UNITDIR=$\{out}/etc/systemd/system/|' -e 's|systemctl --system daemon-reload|true|' initsystems/systemd/Makefile

  # Fix the ipsec program from crushing the PATH
  sed -i -e 's|\(PATH=".*"\):.*$|\1:$PATH|' programs/ipsec/ipsec.in

  # Fix python script to use the correct python
  sed -i -e 's|#!/usr/bin/python|#!/usr/bin/env python|' -e 's/^\(\W*\)installstartcheck()/\1sscmd = "ss"\n\0/' programs/verify/verify.in
'';

as part of the stdenv.mkDerivation expression.

This expression also allows you to define the flags that will be passed to make (both at compile and install), so I had to add a flag to the build make step to force the libreswan build to use systemd (otherwise it wouldn't automatically detect it since it wasn't residing in /usr/bin), and I had to set the install flags to override the output directories to the directory that nix wanted to put the output in, which is conveniently available through the variable ${out}.

Finally with the help of abbradar I added a post install hook to "wrap" the resulting binaries. Essentially, the binaries built by NixOS are sandboxed, in that they can only access other packages that they depend on. Since the libreswan package has some extra runtime dependencies (as there are a lot of bash scripts it uses), I needed to have a way for the resulting binaries to automatically have other system packages available on their $PATH. This is a need that many packages have, so NixOS has a function defined in the bash environment used to run the phase hooks to wrap binaries with a script that extends their $PATH.

postInstall = ''
  for i in $out/bin/* $out/libexec/ipsec/*; do
    wrapProgram "$i" --prefix PATH ':' "$out/bin:${binPath}"
  done
'';

where ${binPath} is defined as

binPath = stdenv.lib.makeBinPath [
  bash iproute iptables procps coreutils gnused gawk nssTools which python
];

and stdenv.lib.makeBinPath simply takes each package and gets the directory to which its binaries are installed in the nix store. This was all a bit foreign to me at first since I couldn't find much documentation or examples of usage, but after reading through what each function did it became very clear.

The libreswan package is placed in an appropriate location of the nixpkgs heirarchy (pkgs/tools/networking/libreswan) and adding the following line to pkgs/top-level/all-packages.nix is what allows it be "found" and installed by nix:

libreswan = callPackage ../tools/networking/libreswan { };

With a bit of trial and error and iteration, I eventually got the libreswan package reproducibly building on my machine!

Adding the ipsec service to nixpkgs

At this point libreswan was building fine, but I actually couldn't run it because it relied on the ipsec binary to be run as a system service, but NixOS doesn't allow services to be added to systemd without being tied into the configuration of the system. Again, this is for the sake of reproducible systems. All of the systemd unit files in NixOS are read-only, generated from the NixOS configuration file. So to add the ipsec service of the libreswan package, I would need to add the service to NixOS.

Adding packages had pretty good examples and documentation, but I found much less about adding services. Luckily, there are plenty of services in NixOS already, so I perused the existing files and started out by copying from a handful of them.

To add a service is pretty simple, but to add appropriate configuration options for the service takes a little more time. My first iteration of the service file relied on using files that were dropped into /etc, which I knew wasn't the NixOS way of doing things, and sure enough the feedback I received strongly recommended changing the configuration to be done through the service. I wanted it to be this way anyway, but I was also on a deadline to get my VPN working, so for my own personal use I was able to get away with creating files in /etc. However, I did go back and move the configuration to the service definition.

The service definition returns what's called an attribute set in the nix expression language; it's similar to Python's dictionaries or Javascript's objects in that you have a set of names which map to values (although their implementation and properties are different). Here's an example:

{
  value1 = 1;
  hello = "world";
  nested = {
    a = "a";
    b = "c";
  };
};

The attribute set returned by the service definition should have two values (or at least, these are the two I see all over the place): options and config.

Options

Options serves as an interface for the configuration of the package, defining the possible configurable values, what types they should be, their default values, etc. The type checking is done at runtime, and there's an excellent set of functions and definition for all the usual types and composite types in lib.types; I recommend you take a look at them. Note that the value of options is merged into the top-level configuration namespace, so you should set options to be an attribute set with an attribute being the full name that will be used in the configuration file to set options for the service. This is a mouthful, but here's my options value setting, which will make my meaning a bit clearer:

options = {
  services.libreswan = {
    enable = mkEnableOption "libreswan ipsec service";

    configSetup = mkOption {
      type = types.lines;
      default = ''
        [omitted]
      '';
      example = ''
        [omitted]
      '';
      description = "Options to go in the 'config setup' section of the libreswan ipsec configuration";
    };

    connections = mkOption {
      type = types.attrsOf types.lines;
      default = {};
      example = {
        myconnection = ''
          auto=add
          left=%defaultroute
          leftid=@user

          right=my.vpn.com

          ikev2=no
          ikelifetime=8h
        '';
      };
      description = "A set of connections to define for the libreswan ipsec service";
    };
  };
};

I've omitted the default and example values for brevity. But as you may be able to see now, the ipsec service nix expression is exposing services.libreswan.enable, services.libreswan.configSetup, and services.libreswan.connections as configuration values to be set in the NixOS configuration file. Note also that the connections value is meant to be set as a attribute set of strings of lines. What that means is that (as shown in the example) each connection is named by an attribute, and the value of the attribute is the libreswan configuration lines defining that connection, as a string.

Config

The config attribute, on the other hand, is the actual implementation that goes along with the options. The options are evaluated, and if the types all match up, the configuration, which consists of the options with the values assigned in the NixOS configuration file, is exposed through config.services.libreswan. This is what is actually being set in the NixOS configuration file. So the config attribute defines the business logic that will be performed when the service is enabled (in my case).

To integrate with the enable option, one can simply use the mkIf function, which takes a boolean and an attribute set, and only returns the attribute set if the boolean is true. So if config.services.libreswan.enable is false, config is set to an empty list, whereas if it's true, config is set to a list containing the attribute set given, and that is evaluated accordingly.

So, here's the definition of config for libreswan:

config = mkIf cfg.enable {
  environment.systemPackages = [ pkgs.libreswan pkgs.iproute ];

  systemd.services.ipsec = {
    description = "Internet Key Exchange (IKE) Protocol Daemon for IPsec";
    path = [
      "${pkgs.libreswan}"
      "${pkgs.iproute}"
      "${pkgs.procps}"
    ];

    wants = [ "network-online.target" ];
    after = [ "network-online.target" ];
    wantedBy = [ "multi-user.target" ];

    serviceConfig = {
      Type = "simple";
      Restart = "always";
      EnvironmentFile = "${pkgs.libreswan}/etc/sysconfig/pluto";
      ExecStartPre = [
        "${libexec}/addconn --config ${configFile} --checkconfig"
        "${libexec}/_stackmanager start"
        "${ipsec} --checknss"
        "${ipsec} --checknflog"
      ];
      ExecStart = "${libexec}/pluto --config ${configFile} --nofork \$PLUTO_OPTIONS";
      ExecStop = "${libexec}/whack --shutdown";
      ExecStopPost = [
        "${pkgs.iproute}/bin/ip xfrm policy flush"
        "${pkgs.iproute}/bin/ip xfrm state flush"
        "${ipsec} --stopnflog"
      ];
      ExecReload = "${libexec}/whack --listen";
    };
  };
};

Notice that environment.systemPackages is being set to ensure that if the service is enabled, those packages are installed (iproute is required at runtime by the ipsec service scripts). Naturally we need our libreswan package here; this is the way that the service is linked to the package definition above, otherwise the service wouldn't have access to the libreswan executables. It's worth noting that this would cause the libreswan package to be installed automatically if it wasn't previously installed. This is one of the many niceties of NixOS :)

The next line, setting the value of systemd.services.ipsec, is what actually defines the ipsec service. Setting this value is adding an attribute to the systemd.services attribute set, which is later in the configuration used to generate all of the systemd service unit files. In the attribute set of the service, we set path to add packages to the service's runtime $PATH, and set wants, after, and wantedBy as we would for a normal systemd service. I actually just copied these values from the systemd unit file that libreswan normally installs. Unfortunately due to its nature, NixOS doesn't support adding arbitrary systemd unit files -- or at least as far as I could tell -- so I had to recreate the file that the libreswan installs in the NixOS format.

The serviceConfig value and all of the values set in its attribute set are just as they are in typical systemd unit files as well.

One thing I haven't discussed yet, which you may have noticed, is the ${configFile} mentioned in the above code. This is the file that is generated from the values set in config.services.libreswan. I define this earlier in the file:

trim = chars: str: let
    nonchars = filter (x : !(elem x.value chars))
                (imap (i: v: {ind = (sub i 1); value = v;}) (stringToCharacters str));
  in
    if length nonchars == 0 then ""
    else substring (head nonchars).ind (add 1 (sub (last nonchars).ind (head nonchars).ind)) str;
indent = str: concatStrings (concatMap (s: ["  " (trim [" " "\t"] s) "\n"]) (splitString "\n" str));
configText = indent (toString cfg.configSetup);
connectionText = concatStrings (mapAttrsToList (n: v: 
  ''
    conn ${n}
    ${indent v}

  '') cfg.connections);
configFile = pkgs.writeText "ipsec.conf"
  ''
    config setup
    ${configText}

    ${connectionText}
  '';

I had to write my own string trimming and indenting functions because lib/strings.nix didn't seem to have them, but maybe I'll move them over into that file at some point. You can see here that I take the value from the configuration (abbreviated to cfg earlier in the file) and do some string manipulation and iteration across the attribute set in the case of cfg.connections to generate the ipsec configuration file based on the user configuration. The standard library functions have good support for iterating and manipulating the data types, as one would hope since nix expressions are a functional language after all.

The pkgs.writeText call is a convenience function that writes a file as its own nix store entry (just like packages have nix store entries), with the given text and returns the name of the file. So that last call defining configFile is what is actually creating the ipsec.conf file in the nix store, which the ipsec service can then use.

Service install

The service file is added at the most logical place in the nixos hierarchy, in this case nixos/modules/services/networking/libreswan.nix, and to allow the service module to be loaded and its configuration checked and used, this (relative) path needs to be added to nixos/modules/module-list.nix:

./services/networking/libreswan.nix

After that was done, I was able to specify the service options in my NixOS configuration file and the libreswan package worked just as it does on other distributions!

Community feedback

This is where NixOS really stood out. Shortly after submitting the pull request of my code, I got immediate feedback on it. Not only were NixOS maintainers looking for conformance to contributing rules, but also they were looking at coding style, and offered tips on how I could simplify the code (especially using standard functions I didn't know about). For about a week we iterated on the code, improving it so that the libreswan configuration was done through the NixOS configuration file.

This is an important part of NixOS; when all packages are only able to be configured through a set of nix expressions, the builds become reproducible: the configuration file completely describes the environment of the operating system. This is enforced mainly through the fact that all files installed and generated by nix are mounted read-only in the nix store, so you couldn't change them directly if you wanted to do so. The only way to make a change is by changing the system configuration file(s)!

I have to thank abbradar; he put a lot of effort into reviewing my code and ensuring the final product was reasonable for the NixOS ecosystem and ideology. Once we settled on a final version, the pull request was accepted, and now my port of the libreswan package is available in NixOS!

Final thoughts

I really enjoyed contributing to NixOS. The community is great and the operating system's core beliefs are noteworthy to say the least. They have very difficult goals set for the operating system, especially since all existing software packages out there originally needed nix expressions written just so they could be used in NixOS. But with continuing community contributions, they've achieved quite a bit; nixpkgs has TONS of the standard packages available on any POSIX/GNU *nix system, and is now very comparable to the package managers of other popular Linux distributions.

If there's one area that was an issue, it was the aforementioned lack of good documentation for the standard functions. It's one thing to have documentation for each function, but even just a list of functions with a short description of what each does (if the name doesn't make it obvious) would have been very helpful in my early development. There are a few tutorials out there that do mention what you should "normally" use, but that wasn't sufficient for my needs, especially when it came to implementing the nix expression for the ipsec libreswan service.

I'm glad I was able to contribute so soon and be a part of the operating system's growth; I installed the operating system thinking that at some point I'd be contributing to it, and now that prediction has come to light!

The complete pull request, adding the libreswan package and service to NixOS: GitHub

Comments