From f132b603f95ce21d934bb29072567fee3bfb4acd Mon Sep 17 00:00:00 2001 From: schnusch Date: Thu, 24 Feb 2022 18:09:44 +0100 Subject: [PATCH] nixos/xandikos: use systemd socket activation Xandikos' handling of listening addresses is quite buggy. There were multiple bugs regarding socket activation, which caused it to also listen on unwanted default ports. Additionally the metrics port reuses the listen address. To avoid all this unexpected behaviour we run Xandikos in a private network namespace and let systemd handle our *wanted* listening adresses. Optionally the metrics are made accessible in the host's network namespace through `xandikos-metrics.service` using systemd-socket-proxyd. --- .../modules/services/networking/xandikos.nix | 113 +++++++++++++++--- nixos/tests/xandikos.nix | 42 ++++--- pkgs/by-name/xa/xandikos/package.nix | 1 + 3 files changed, 125 insertions(+), 31 deletions(-) diff --git a/nixos/modules/services/networking/xandikos.nix b/nixos/modules/services/networking/xandikos.nix index 1b72cd03ba9cf..4a32f649bc164 100644 --- a/nixos/modules/services/networking/xandikos.nix +++ b/nixos/modules/services/networking/xandikos.nix @@ -9,9 +9,19 @@ with lib; let cfg = config.services.xandikos; + + nginxProxyAddress = + let + first = head (toList cfg.address); + in + if hasInfix "/" first then "unix:${first}" else first; in { + imports = [ + (mkRemovedOptionModule [ "services" "xandikos" "port" ] "Use services.xandikos.address") + ]; + options = { services.xandikos = { enable = mkEnableOption "Xandikos CalDAV and CardDAV server"; @@ -19,18 +29,24 @@ in package = mkPackageOption pkgs "xandikos" { }; address = mkOption { - type = types.str; - default = "localhost"; description = '' - The IP address on which Xandikos will listen. - By default listens on localhost. + systemd ListenStream where Xandikos shall listen, see {manpage}`systemd.socket(5)` ''; - }; - - port = mkOption { - type = types.port; - default = 8080; - description = "The port of the Xandikos web application"; + type = + with types; + oneOf [ + str + (listOf str) + ]; + default = [ + "127.0.0.1:8080" + "[::1]:8080" + ]; + example = [ + "0.0.0.0:8080" + "[::]:8080" + "/run/xandikos/socket" + ]; }; routePrefix = mkOption { @@ -83,6 +99,31 @@ in }; }; + metrics = { + enable = mkEnableOption "Xandikos' metrics"; + + address = mkOption { + description = '' + systemd ListenStream where the Xandikos metrics shall listen, see {manpage}`systemd.socket(5)` + ''; + type = + with types; + oneOf [ + str + (listOf str) + ]; + default = [ + "127.0.0.1:8081" + "[::1]:8081" + ]; + example = [ + "0.0.0.0:8081" + "[::]:8081" + "/run/xandikos/metrics.socket" + ]; + }; + }; + }; }; @@ -91,10 +132,15 @@ in { meta.maintainers = with lib.maintainers; [ _0x4A6F ]; + systemd.sockets.xandikos = { + wantedBy = [ "sockets.target" ]; + socketConfig.ListenStream = cfg.address; + }; + systemd.services.xandikos = { description = "A Simple Calendar and Contact Server"; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; + requires = [ "xandikos.socket" ]; + after = [ "xandikos.socket" ]; serviceConfig = { User = "xandikos"; @@ -104,6 +150,19 @@ in StateDirectory = "xandikos"; StateDirectoryMode = "0700"; PrivateDevices = true; + # Systemd socket activation was broken time and again and bugs with + # default listening addresses introduced. See + # * https://github.com/jelmer/xandikos/issues/134 + # * https://github.com/jelmer/xandikos/pull/133 + # * https://github.com/jelmer/xandikos/pull/136 + # * https://github.com/jelmer/xandikos/pull/155 + # * https://github.com/jelmer/xandikos/issues/260 + # * https://github.com/jelmer/xandikos/pull/262 + # Additionally --metrics-port reuses --listen-address, which + # defaults to 0.0.0.0/::. To avoid all this behaviour having + # unwanted side-effects we run it in a private network namespace. + # The metrics are made accesible through `xandikos-metrics.socket`. + PrivateNetwork = true; # Sandboxing CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN"; ProtectSystem = "strict"; @@ -121,8 +180,6 @@ in ExecStart = '' ${cfg.package}/bin/xandikos \ --directory /var/lib/xandikos \ - --listen-address ${cfg.address} \ - --port ${toString cfg.port} \ --route-prefix ${cfg.routePrefix} \ ${lib.concatStringsSep " " cfg.extraOptions} ''; @@ -130,12 +187,38 @@ in }; } + (mkIf cfg.metrics.enable { + systemd.sockets.xandikos-metrics = { + wantedBy = [ "sockets.target" ]; + socketConfig.ListenStream = cfg.metrics.address; + }; + + systemd.services.xandikos-metrics = { + description = "A proxy to Xandikos' metrics"; + # see https://www.freedesktop.org/software/systemd/man/systemd-socket-proxyd.html#Namespace%20Example + requires = [ + "xandikos.service" + "xandikos-metrics.socket" + ]; + after = [ + "xandikos.service" + "xandikos-metrics.socket" + ]; + unitConfig.JoinsNamespaceOf = "xandikos.service"; + serviceConfig = { + Type = "notify"; + ExecStart = "${config.systemd.package}/lib/systemd/systemd-socket-proxyd localhost:8081"; + PrivateNetwork = true; # required by JoinsNamespaceOf + }; + }; + }) + (mkIf cfg.nginx.enable { services.nginx = { enable = true; virtualHosts."${cfg.nginx.hostName}" = { locations."/" = { - proxyPass = "http://${cfg.address}:${toString cfg.port}/"; + proxyPass = "http://${nginxProxyAddress}"; }; }; }; diff --git a/nixos/tests/xandikos.nix b/nixos/tests/xandikos.nix index 4905a5bc067d5..d5111da39a060 100644 --- a/nixos/tests/xandikos.nix +++ b/nixos/tests/xandikos.nix @@ -9,21 +9,32 @@ import ./make-test-python.nix ( nodes = { xandikos_client = { }; xandikos_default = { - networking.firewall.allowedTCPPorts = [ 8080 ]; - services.xandikos.enable = true; + networking.firewall.allowedTCPPorts = [ + 8080 + 8081 + ]; + services.xandikos = { + enable = true; + metrics.enable = true; + }; }; xandikos_proxy = { networking.firewall.allowedTCPPorts = [ 80 8080 ]; - services.xandikos.enable = true; - services.xandikos.address = "localhost"; - services.xandikos.port = 8080; - services.xandikos.routePrefix = "/xandikos-prefix/"; - services.xandikos.extraOptions = [ - "--defaults" - ]; + services.xandikos = { + enable = true; + address = [ + "127.0.0.1:8080" + "[::1]:8080" + "/run/xandikos/socket" + ]; + routePrefix = "/xandikos-prefix/"; + extraOptions = [ + "--defaults" + ]; + }; services.nginx = { enable = true; recommendedProxySettings = true; @@ -31,7 +42,7 @@ import ./make-test-python.nix ( serverName = "xandikos.local"; basicAuth.xandikos = "snakeOilPassword"; locations."/xandikos/" = { - proxyPass = "http://localhost:8080/xandikos-prefix/"; + proxyPass = "http://unix:/run/xandikos/socket:/xandikos-prefix/"; }; }; }; @@ -42,24 +53,23 @@ import ./make-test-python.nix ( start_all() with subtest("Xandikos default"): - xandikos_default.wait_for_unit("multi-user.target") - xandikos_default.wait_for_unit("xandikos.service") - xandikos_default.wait_for_open_port(8080) + xandikos_default.wait_for_unit("sockets.target") xandikos_default.succeed("curl --fail http://localhost:8080/") xandikos_default.succeed( "curl -s --fail --location http://localhost:8080/ | grep -i Xandikos" ) + xandikos_default.succeed("curl -s --fail --location http://localhost:8081/metrics") xandikos_client.wait_for_unit("network.target") xandikos_client.fail("curl --fail http://xandikos_default:8080/") + xandikos_client.fail("curl --fail http://xandikos_default:8081/metrics") with subtest("Xandikos proxy"): - xandikos_proxy.wait_for_unit("multi-user.target") - xandikos_proxy.wait_for_unit("xandikos.service") - xandikos_proxy.wait_for_open_port(8080) + xandikos_proxy.wait_for_unit("sockets.target") xandikos_proxy.succeed("curl --fail http://localhost:8080/") xandikos_proxy.succeed( "curl -s --fail --location http://localhost:8080/ | grep -i Xandikos" ) + xandikos_client.fail("curl --fail http://xandikos_default:8081/metrics") xandikos_client.wait_for_unit("network.target") xandikos_client.fail("curl --fail http://xandikos_proxy:8080/") xandikos_client.succeed( diff --git a/pkgs/by-name/xa/xandikos/package.nix b/pkgs/by-name/xa/xandikos/package.nix index cfaeef7b13154..ed4f0b8a62c57 100644 --- a/pkgs/by-name/xa/xandikos/package.nix +++ b/pkgs/by-name/xa/xandikos/package.nix @@ -32,6 +32,7 @@ python3Packages.buildPythonApplication rec { jinja2 multidict pytz + systemd vobject ];