diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml index c73e8eda074c..a9d1720e94bb 100644 --- a/nixos/doc/manual/release-notes/rl-2105.xml +++ b/nixos/doc/manual/release-notes/rl-2105.xml @@ -100,6 +100,19 @@ Now nginx uses the zlib-ng library by default. + + + Libreswan has been updated + to version 4.4. The package now includes example configurations and manual + pages by default. The NixOS module has been changed to use the upstream + systemd units and write the configuration in the /etc/ipsec.d/ + directory. In addition, two new options have been added to + specify connection policies + () + and disable send/receive redirects + (). + + diff --git a/nixos/modules/services/networking/libreswan.nix b/nixos/modules/services/networking/libreswan.nix index 81bc4e1cf95c..1f0423ac3d84 100644 --- a/nixos/modules/services/networking/libreswan.nix +++ b/nixos/modules/services/networking/libreswan.nix @@ -9,21 +9,22 @@ let libexec = "${pkgs.libreswan}/libexec/ipsec"; ipsec = "${pkgs.libreswan}/sbin/ipsec"; - trim = chars: str: let - nonchars = filter (x : !(elem x.value chars)) - (imap0 (i: v: {ind = i; 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; + trim = chars: str: + let + nonchars = filter (x : !(elem x.value chars)) + (imap0 (i: v: {ind = i; 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" + + configFile = pkgs.writeText "ipsec-nixos.conf" '' config setup ${configText} @@ -31,6 +32,11 @@ let ${connectionText} ''; + policyFiles = mapAttrs' (name: text: + { name = "ipsec.d/policies/${name}"; + value.source = pkgs.writeText "ipsec-policy-${name}" text; + }) cfg.policies; + in { @@ -41,41 +47,71 @@ in services.libreswan = { - enable = mkEnableOption "libreswan ipsec service"; + enable = mkEnableOption "Libreswan IPsec service"; configSetup = mkOption { type = types.lines; default = '' protostack=netkey - nat_traversal=yes virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:25.0.0.0/8,%v4:100.64.0.0/10,%v6:fd00::/8,%v6:fe80::/10 ''; example = '' secretsfile=/root/ipsec.secrets protostack=netkey - nat_traversal=yes virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:25.0.0.0/8,%v4:100.64.0.0/10,%v6:fd00::/8,%v6:fe80::/10 ''; - description = "Options to go in the 'config setup' section of the libreswan ipsec configuration"; + 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 + example = literalExample '' + { myconnection = ''' + auto=add + left=%defaultroute + leftid=@user - right=my.vpn.com + right=my.vpn.com - ikev2=no - ikelifetime=8h - ''; - }; - description = "A set of connections to define for the libreswan ipsec service"; + ikev2=no + ikelifetime=8h + '''; + } + ''; + description = "A set of connections to define for the Libreswan IPsec service"; }; + + policies = mkOption { + type = types.attrsOf types.lines; + default = {}; + example = literalExample '' + { private-or-clear = ''' + # Attempt opportunistic IPsec for the entire Internet + 0.0.0.0/0 + ::/0 + '''; + } + ''; + description = '' + A set of policies to apply to the IPsec connections. + + + The policy name must match the one of connection it needs to apply to. + + ''; + }; + + disableRedirects = mkOption { + type = types.bool; + default = true; + description = '' + Whether to disable send and accept redirects for all nework interfaces. + See the Libreswan + FAQ page for why this is recommended. + ''; + }; + }; }; @@ -85,43 +121,38 @@ in config = mkIf cfg.enable { + # Install package, systemd units, etc. environment.systemPackages = [ pkgs.libreswan pkgs.iproute2 ]; + systemd.packages = [ pkgs.libreswan ]; + systemd.tmpfiles.packages = [ pkgs.libreswan ]; + + # Install configuration files + environment.etc = { + "ipsec.secrets".source = "${pkgs.libreswan}/etc/ipsec.secrets"; + "ipsec.conf".source = "${pkgs.libreswan}/etc/ipsec.conf"; + "ipsec.d/01-nixos.conf".source = configFile; + } // policyFiles; + + # Create NSS database directory + systemd.tmpfiles.rules = [ "d /var/lib/ipsec/nss 755 root root -" ]; systemd.services.ipsec = { description = "Internet Key Exchange (IKE) Protocol Daemon for IPsec"; - path = [ - "${pkgs.libreswan}" - "${pkgs.iproute2}" - "${pkgs.procps}" - "${pkgs.nssTools}" - "${pkgs.iptables}" - "${pkgs.nettools}" - ]; - - 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.iproute2}/bin/ip xfrm policy flush" - "${pkgs.iproute2}/bin/ip xfrm state flush" - "${ipsec} --stopnflog" - ]; - ExecReload = "${libexec}/whack --listen"; - }; - + restartTriggers = [ configFile ] ++ mapAttrsToList (n: v: v.source) policyFiles; + path = with pkgs; [ + libreswan + iproute2 + procps + nssTools + iptables + nettools + ]; + preStart = optionalString cfg.disableRedirects '' + # Disable send/receive redirects + echo 0 | tee /proc/sys/net/ipv4/conf/*/send_redirects + echo 0 | tee /proc/sys/net/ipv{4,6}/conf/*/accept_redirects + ''; }; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index f0b050829234..5ff31ba6834c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -217,6 +217,7 @@ in latestKernel.login = handleTest ./login.nix { latestKernel = true; }; leaps = handleTest ./leaps.nix {}; lidarr = handleTest ./lidarr.nix {}; + libreswan = handleTest ./libreswan.nix {}; lightdm = handleTest ./lightdm.nix {}; limesurvey = handleTest ./limesurvey.nix {}; locate = handleTest ./locate.nix {}; diff --git a/nixos/tests/libreswan.nix b/nixos/tests/libreswan.nix new file mode 100644 index 000000000000..17ae60af8eed --- /dev/null +++ b/nixos/tests/libreswan.nix @@ -0,0 +1,134 @@ +# This test sets up a host-to-host IPsec VPN between Alice and Bob, each on its +# own network and with Eve as the only route between each other. We check that +# Eve can eavesdrop the plaintext traffic between Alice and Bob, but once they +# enable the secure tunnel Eve's spying becomes ineffective. + +import ./make-test-python.nix ({ lib, pkgs, ... }: + +let + + # IPsec tunnel between Alice and Bob + tunnelConfig = { + services.libreswan.enable = true; + services.libreswan.connections.tunnel = + '' + leftid=@alice + left=fd::a + rightid=@bob + right=fd::b + authby=secret + auto=add + ''; + environment.etc."ipsec.d/tunnel.secrets" = + { text = ''@alice @bob : PSK "j1JbIi9WY07rxwcNQ6nbyThKCf9DGxWOyokXIQcAQUnafsNTUJxfsxwk9WYK8fHj"''; + mode = "600"; + }; + }; + + # Common network setup + baseNetwork = { + # shared hosts file + extraHosts = lib.mkVMOverride '' + fd::a alice + fd::b bob + fd::e eve + ''; + # remove all automatic addresses + useDHCP = false; + interfaces.eth1.ipv4.addresses = lib.mkVMOverride []; + interfaces.eth2.ipv4.addresses = lib.mkVMOverride []; + # open a port for testing + firewall.allowedUDPPorts = [ 1234 ]; + }; + + # Adds an address and route from a to b via Eve + addRoute = a: b: { + interfaces.eth1.ipv6.addresses = + [ { address = a; prefixLength = 64; } ]; + interfaces.eth1.ipv6.routes = + [ { address = b; prefixLength = 128; via = "fd::e"; } ]; + }; + +in + +{ + name = "libreswan"; + meta = with lib.maintainers; { + maintainers = [ rnhmjoj ]; + }; + + # Our protagonist + nodes.alice = { ... }: { + virtualisation.vlans = [ 1 ]; + networking = baseNetwork // addRoute "fd::a" "fd::b"; + } // tunnelConfig; + + # Her best friend + nodes.bob = { ... }: { + virtualisation.vlans = [ 2 ]; + networking = baseNetwork // addRoute "fd::b" "fd::a"; + } // tunnelConfig; + + # The malicious network operator + nodes.eve = { ... }: { + virtualisation.vlans = [ 1 2 ]; + networking = lib.mkMerge + [ baseNetwork + { interfaces.br0.ipv6.addresses = + [ { address = "fd::e"; prefixLength = 64; } ]; + bridges.br0.interfaces = [ "eth1" "eth2" ]; + } + ]; + environment.systemPackages = [ pkgs.tcpdump ]; + boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true; + }; + + testScript = + '' + def alice_to_bob(msg: str): + """ + Sends a message as Alice to Bob + """ + bob.execute("nc -lu ::0 1234 >/tmp/msg &") + alice.sleep(1) + alice.succeed(f"echo '{msg}' | nc -uw 0 bob 1234") + bob.succeed(f"grep '{msg}' /tmp/msg") + + + def eavesdrop(): + """ + Starts eavesdropping on Alice and Bob + """ + match = "src host alice and dst host bob" + eve.execute(f"tcpdump -i br0 -c 1 -Avv {match} >/tmp/log &") + + + start_all() + + with subtest("Network is up"): + alice.wait_until_succeeds("ping -c1 bob") + + with subtest("Eve can eavesdrop cleartext traffic"): + eavesdrop() + alice_to_bob("I secretly love turnip") + eve.sleep(1) + eve.succeed("grep turnip /tmp/log") + + with subtest("Libreswan is ready"): + alice.wait_for_unit("ipsec") + bob.wait_for_unit("ipsec") + alice.succeed("ipsec verify 1>&2") + + with subtest("Alice and Bob can start the tunnel"): + alice.execute("ipsec auto --start tunnel &") + bob.succeed("ipsec auto --start tunnel") + # apparently this is needed to "wake" the tunnel + bob.execute("ping -c1 alice") + + with subtest("Eve no longer can eavesdrop"): + eavesdrop() + alice_to_bob("Just kidding, I actually like rhubarb") + eve.sleep(1) + eve.fail("grep rhubarb /tmp/log") + ''; +}) diff --git a/pkgs/tools/networking/libreswan/default.nix b/pkgs/tools/networking/libreswan/default.nix index 1059baf13ee5..24b7176e82b5 100644 --- a/pkgs/tools/networking/libreswan/default.nix +++ b/pkgs/tools/networking/libreswan/default.nix @@ -1,71 +1,114 @@ -{ lib, stdenv, fetchurl, makeWrapper, - pkg-config, systemd, gmp, unbound, bison, flex, pam, libevent, libcap_ng, curl, nspr, - bash, iproute2, iptables, procps, coreutils, gnused, gawk, nss, which, python3, - docs ? false, xmlto, libselinux, ldns - }: +{ lib +, stdenv +, fetchurl +, fetchpatch +, nixosTests +, pkg-config +, systemd +, gmp +, unbound +, bison +, flex +, pam +, libevent +, libcap_ng +, curl +, nspr +, bash +, iproute2 +, iptables +, procps +, coreutils +, gnused +, gawk +, nss +, which +, python3 +, libselinux +, ldns +, xmlto +, docbook_xml_dtd_412 +, docbook_xsl +, findXMLCatalogs +}: let + # Tools needed by ipsec scripts binPath = lib.makeBinPath [ - bash iproute2 iptables procps coreutils gnused gawk nss.tools which python3 + iproute2 iptables procps + coreutils gnused gawk + nss.tools which ]; in -assert docs -> xmlto != null; -assert stdenv.isLinux -> libselinux != null; - stdenv.mkDerivation rec { pname = "libreswan"; - version = "3.32"; + version = "4.4"; src = fetchurl { url = "https://download.libreswan.org/${pname}-${version}.tar.gz"; - sha256 = "0bj3g6qwd3ir3gk6hdl9npy3k44shf56vcgjahn30qpmx3z5fsr3"; + sha256 = "0xj974yc0y1r7235zl4jhvxqz3bpb8js2fy9ic820zq9swh0lgsz"; }; strictDeps = true; - # These flags were added to compile v3.18. Try to lift them when updating. - NIX_CFLAGS_COMPILE = toString [ "-Wno-error=redundant-decls" "-Wno-error=format-nonliteral" - # these flags were added to build with gcc7 - "-Wno-error=implicit-fallthrough" - "-Wno-error=format-truncation" - "-Wno-error=pointer-compare" - "-Wno-error=stringop-truncation" - # The following flag allows libreswan v3.32 to work with NSS 3.22, see - # https://github.com/libreswan/libreswan/issues/334. - # This flag should not be needed for libreswan v3.33 (which is not yet released). - "-DNSS_PKCS11_2_0_COMPAT=1" - ]; - nativeBuildInputs = [ bison flex - makeWrapper pkg-config + xmlto + docbook_xml_dtd_412 + docbook_xsl + findXMLCatalogs ]; - buildInputs = [ bash iproute2 iptables systemd coreutils gnused gawk gmp unbound pam libevent - libcap_ng curl nspr nss python3 ldns ] - ++ lib.optional docs xmlto - ++ lib.optional stdenv.isLinux libselinux; + buildInputs = [ + systemd coreutils + gnused gawk gmp unbound pam libevent + libcap_ng curl nspr nss ldns + # needed to patch shebangs + python3 bash + ] ++ lib.optional stdenv.isLinux libselinux; + + patches = [ + # Fix compilation on aarch64, remove on next update + (fetchpatch { + url = "https://github.com/libreswan/libreswan/commit/ea50d36d2886e44317ba5ba841de1d1bf91aee6c.patch"; + sha256 = "1jp89rm9jp55zmiyimyhg7yadj0fwwxaw7i5gyclrs38w3y1aacj"; + }) + ]; prePatch = '' - # Correct bash path - sed -i -e 's|/bin/bash|/usr/bin/env bash|' mk/config.mk + # Correct iproute2 path + sed -e 's|"/sbin/ip"|"${iproute2}/bin/ip"|' \ + -e 's|"/sbin/iptables"|"${iptables}/bin/iptables"|' \ + -i initsystems/systemd/ipsec.service.in \ + programs/verify/verify.in - # Fix systemd unit directory, and prevent the makefile from trying to reload the - # systemd daemon or create tmpfiles - sed -i -e 's|UNITDIR=.*$|UNITDIR=$\{out}/etc/systemd/system/|g' \ - -e 's|TMPFILESDIR=.*$|TMPFILESDIR=$\{out}/tmpfiles.d/|g' \ - -e 's|systemctl|true|g' \ - -e 's|systemd-tmpfiles|true|g' \ - initsystems/systemd/Makefile + # Prevent the makefile from trying to + # reload the systemd daemon or create tmpfiles + sed -e 's|systemctl|true|g' \ + -e 's|systemd-tmpfiles|true|g' \ + -i initsystems/systemd/Makefile # Fix the ipsec program from crushing the PATH - sed -i -e 's|\(PATH=".*"\):.*$|\1:$PATH|' programs/ipsec/ipsec.in + sed -e 's|\(PATH=".*"\):.*$|\1:$PATH|' -i 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 + sed -e 's/^\(\W*\)installstartcheck()/\1sscmd = "ss"\n\0/' \ + -i programs/verify/verify.in + + # Replace wget with curl to save a dependency + curlArgs='-s --remote-name-all --output-dir' + sed -e "s|wget -q -P|${curl}/bin/curl $curlArgs|g" \ + -i programs/letsencrypt/letsencrypt.in + + # Patch the Makefile: + # 1. correct the pam.d directory install path + # 2. do not create the /var/lib/ directory + sed -e 's|$(DESTDIR)/etc/pam.d|$(out)/etc/pam.d|' \ + -e '/test ! -d $(NSSDIR)/,+3d' \ + -i configs/Makefile ''; # Set appropriate paths for build @@ -73,10 +116,10 @@ stdenv.mkDerivation rec { makeFlags = [ "INITSYSTEM=systemd" - (if docs then "all" else "base") + "UNITDIR=$(out)/etc/systemd/system/" + "TMPFILESDIR=$(out)/lib/tmpfiles.d/" ]; - installTargets = [ (if docs then "install" else "install-base") ]; # Hack to make install work installFlags = [ "FINALVARDIR=\${out}/var" @@ -84,18 +127,23 @@ stdenv.mkDerivation rec { ]; postInstall = '' - for i in $out/bin/* $out/libexec/ipsec/*; do - wrapProgram "$i" --prefix PATH ':' "$out/bin:${binPath}" - done + # Install examples directory (needed for letsencrypt) + cp -r docs/examples $out/share/doc/libreswan/examples ''; - enableParallelBuilding = true; + postFixup = '' + # Add a PATH to the main "ipsec" script + sed -e '0,/^$/{s||export PATH=${binPath}:$PATH|}' \ + -i $out/bin/ipsec + ''; + + passthru.tests.libreswan = nixosTests.libreswan; meta = with lib; { homepage = "https://libreswan.org"; description = "A free software implementation of the VPN protocol based on IPSec and the Internet Key Exchange"; platforms = platforms.linux ++ platforms.freebsd; - license = licenses.gpl2; - maintainers = [ maintainers.afranchuk ]; + license = with licenses; [ gpl2Plus mpl20 ] ; + maintainers = with maintainers; [ afranchuk rnhmjoj ]; }; }