Skip to content

Commit

Permalink
nixos/xandikos: use systemd socket activation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
schnusch committed Dec 16, 2024
1 parent d9d87c5 commit 16525b5
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 31 deletions.
113 changes: 98 additions & 15 deletions nixos/modules/services/networking/xandikos.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,44 @@ 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";

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 {
Expand Down Expand Up @@ -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"
];
};
};

};

};
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -121,21 +180,45 @@ 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}
'';
};
};
}

(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}";
};
};
};
Expand Down
42 changes: 26 additions & 16 deletions nixos/tests/xandikos.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,40 @@ 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;
virtualHosts."xandikos" = {
serverName = "xandikos.local";
basicAuth.xandikos = "snakeOilPassword";
locations."/xandikos/" = {
proxyPass = "http://localhost:8080/xandikos-prefix/";
proxyPass = "http://unix:/run/xandikos/socket:/xandikos-prefix/";
};
};
};
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions pkgs/by-name/xa/xandikos/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ python3Packages.buildPythonApplication rec {
jinja2
multidict
pytz
systemd
vobject
];

Expand Down

0 comments on commit 16525b5

Please sign in to comment.