You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

483 lines
20 KiB
Nix

{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/master";
utils.url = "github:numtide/flake-utils";
};
description = "A very basic flake";
outputs = { self, nixpkgs, utils }:
utils.lib.eachDefaultSystem(system: let
pkgs = import nixpkgs {
inherit system;
};
pname = "pounce";
version = "3.1";
in {
packages = {
default = pkgs.stdenv.mkDerivation {
inherit pname version;
src = pkgs.fetchzip {
url = "https://git.causal.agency/pounce/snapshot/pounce-${version}.tar.gz";
sha256 = "sha256-6PGiaU5sOwqO4V2PKJgIi3kI2jXsBOldEH51D7Sx9tg=";
};
buildInputs = with pkgs;
[ libressl libxcrypt curl sqlite ];
nativeBuildInputs = [ pkgs.pkg-config ];
configureFlags = [ "--enable-notify" "--enable-palaver" ];
buildFlags = [ "all" ];
makeFlags = [
"PREFIX=$(out)"
];
meta = with nixpkgs.lib; {
homepage = "https://code.causal.agency/june/pounce";
description = "Simple multi-client TLS-only IRC bouncer";
license = licenses.gpl3;
};
};
};
}) // {
nixosModule = {config, lib, pkgs, ...}:
with lib;
let
cfg = config.services.pounce;
pkg = self.packages.${pkgs.system}.default;
defaultUser = "pounce";
settingsFormat = {
type = types.attrsOf (types.nullOr
(types.oneOf [ types.bool types.int types.str ]));
generate = name: value:
let
mkKeyValue = k: v:
if true == v then k
else if false == v then "#${k}"
else lib.generators.mkKeyValueDefault {} " = " k v;
mkKeyValueList = values:
lib.generators.toKeyValue { inherit mkKeyValue; } values;
in pkgs.writeText name (mkKeyValueList value);
};
sharedNotifyOpts = {
insecure = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
Disable certificate validation for connecting to the Pounce
instance. Overrides
{option}`services.pounce.networks.<name>.notify.trust-cert`.
'';
};
trust-cert = mkOption {
type = types.nullOr types.str;
default = "";
example = "/etc/letsencrypt/live/libera.irc.example.org/fullchain.pem";
description = lib.mdDoc ''
Pounce certificate for the pounce-notify client to trust.
This is required if Pounce is using a self-signed
certificate. If left blank, pounce-notify will use the
appropriate certificate in
{option}`services.pounce.fullChain`. Set to `null` to disable
certificate pinning.
'';
};
client-cert = mkOption {
type = types.str;
default = "";
description = lib.mdDoc ''
Client certificate to use if Pounce is configured to require
certificate authentication. If the relevant private key is
stored in a separate file, load it with
{option}`services.pounce.networks.<name>.notify.client-priv`.
'';
};
client-priv = mkOption {
type = types.str;
default = "";
description = lib.mdDoc ''
Private key to use if Pounce is configured to require
certificate authentication. If the certificate provided in
{option}`services.pounce.networks.<name>.notify.client-cert`
has an embedded private key, this option can be left
empty.
'';
};
user = mkOption {
type = types.str;
default = "pounce-notify";
description = lib.mdDoc ''
Username to present to Pounce when connecting.
'';
};
};
hardeningFlags = {
CapabilityBoundingSet = [ "" ];
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
RestrictNamespaces = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
};
in {
options.services.pounce = {
enable = mkEnableOption
(lib.mdDoc "the Pounce IRC bouncer and Calico dispatcher");
user = mkOption {
type = types.str;
default = defaultUser;
description = lib.mdDoc ''
User account under which Pounce runs. If not specified, a default user
will be created.
'';
};
socketDir = mkOption {
type = types.str;
default = "/run/pounce";
description = lib.mdDoc ''
Directory where each Pounce instance's UNIX-domain socket is stored for
Calico to route to.
'';
};
bindHost = mkOption {
type = types.str;
default = "localhost";
description = lib.mdDoc ''
The IP or host for Calico to bind to.
'';
};
port = mkOption {
type = types.port;
default = 6697;
description = lib.mdDoc "Port for Calico to listen on.";
};
timeout = mkOption {
type = types.ints.positive;
default = 1000;
description = lib.mdDoc ''
Timeout parameter (in milliseconds) for Calico to close a connection
if no `ClientHello` message is sent.
'';
};
networks = mkOption {
type = types.attrsOf (types.submodule {
options = {
fullChain = mkOption {
type = types.str;
description = lib.mdDoc ''
Certificate chain for TLS connections.
'';
};
privKey = mkOption {
type = types.str;
description = lib.mdDoc ''
Private key for TLS connections.
'';
};
config = mkOption {
type = settingsFormat.type;
default = {};
example = {
irc.libera.chat = {
host = "irc.libera.chat";
port = 6697;
sasl-plain = "testname:password";
join = "#nixos,#nixos-dev";
};
};
description = lib.mdDoc ''
Pounce configuration to user for the network. For information on
what options Pounce accepts, see the
[pounce(1)](https://git.causal.agency/pounce/about/pounce.1)
manual page.
'';
};
notify = mkOption {
type = types.submodule {
options = sharedNotifyOpts // {
script = mkOption {
type = types.lines;
default = "";
example = ''
# Pushover example
if [ -z "$NOTIFY_CHANNEL" ]; then
TITLE="Private Message"
CONTEXT="$NOTIFY_NICK"
else
TITLE="Mention"
CONTEXT="$NOTIFY_CHANNEL"
fi
$${pkgs.curl}/bin/curl \
-X POST \
--form-string token="API_TOKEN" \
--form-string user="USER_KEY" \
--form-string title="(libera/$CONTEXT) $TITLE" \
--form-string timestamp="$NOTIFY_TIME" \
--form-string message="$NOTIFY_NICK: $NOTIFY_MESSAGE" \
https://api.pushover.net/1/messages.json
'';
description = lib.mdDoc ''
Commands to run when a private message or a mention
occurs. See
[pounce-notify(1)](https://git.causal.agency/pounce/about/extra/notify/pounce-notify.1)
for a list of environment variables containing information
about the notification event.
'';
};
command = mkOption {
type = types.str;
default = "";
example = "/var/lib/pounce/notify.sh";
description = lib.mdDoc ''
Command to run when a private message or a mention occurs.
Overrides
{option}`services.pounce.notify.networks.<name>.notify.script`.
See
[pounce-notify(1)](https://git.causal.agency/pounce/about/extra/notify/pounce-notify.1)
for a list of environment variables containing information
about the notification event.
'';
};
};
};
default = {};
example = { script = "/var/lib/pounce/notify.sh"; };
description = lib.mdDoc ''
Configuration for pounce-notify. A notification client will be
spawned if either
{option}`services.pounce.networks.<name>.notify.script` or
{option}`services.pounce.networks.<name>.notify.command` is set.
'';
};
palaver = mkOption {
type = types.submodule {
options = sharedNotifyOpts // {
enable = mkEnableOption
(lib.mdDoc "the palaver notification client");
noPreviews = mkOption {
type = types.bool;
default = false;
description = "Never send message previews, regardless of app preferences.";
};
noPrivateMessagePreviews = mkOption {
type = types.bool;
default = false;
description = "Never send message previews for private messages.";
};
dbPath = mkOption {
type = types.string;
default = "";
description = "Set the path to the database file used to store notification preferences.";
};
caseSensitive = mkOption {
type = types.bool;
default = false;
description = "Match nick and keywords case-sensitively, despite the specification.";
};
verbose = mkOption {
type = types.bool;
default = false;
description = "Log IRC protocol, SQL and HTTP to standard error.";
};
};
};
default = {
enable = false;
};
example = { script = "/var/lib/pounce/notify.sh"; };
description = lib.mdDoc ''
Configuration for pounce-palaver. A notification client will be
spawned if
{option}`services.pounce.networks.<name>.palaver.enable` is true.
'';
};
};
});
default = {};
description = lib.mdDoc "Attribute set of IRC networks to connect to.";
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [ "d ${cfg.socketDir} 0700 ${cfg.user} ${cfg.user} -" ];
systemd.services = mkMerge (
[
{
calico = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
description = "Calico dispatcher for Pounce IRC bouncer.";
serviceConfig = {
User = cfg.user;
Group = cfg.user;
ExecStart = ''
${pkg}/bin/calico \
-H ${cfg.bindHost} -P ${toString cfg.port} \
-t ${toString cfg.timeout} ${cfg.socketDir}
'';
Restart = "on-failure";
} // hardeningFlags;
};
}
] ++ (mapAttrsToList (name: value: mkMerge [
{
"pounce-${name}" = {
wantedBy = [ "calico.service" ];
after = [ "network.target" ];
before = [ "calico.service" ];
description = "Pounce IRC bouncer for the ${name} network.";
serviceConfig = {
User = cfg.user;
Group = cfg.user;
ExecStart = ''
${pkg}/bin/pounce \
-C ${value.fullChain} \
-K ${value.privKey} \
-U ${cfg.socketDir} -H ${name} \
${settingsFormat.generate "${name}.cfg" value.config}
'';
Restart = "on-failure";
} // hardeningFlags;
};
}
(mkIf (value.notify.command != "" || value.notify.script != "") {
"pounce-${name}-notify" = {
wantedBy = [ "multi-user.target" ];
requires = [ "calico.service" "pounce-${name}.service" ];
after = [ "calico.service" "pounce-${name}.service" ];
description = "Pounce notification client for the ${name} network.";
serviceConfig = {
User = cfg.user;
Group = cfg.user;
Environment = "SHELL=${pkgs.bash}/bin/bash";
ExecStart = ''
${pkg}/bin/pounce-notify \
${if value.notify.insecure then "-!" else
if value.notify.trust-cert == "" then
"-t ${value.fullChain}"
else if value.notify.trust-cert != null then
"-t ${value.notify.trust-cert}" else ""} \
${if value.notify.client-cert != "" then "-c ${value.notify.client-cert}" else ""} \
${if value.notify.client-priv != "" then "-k ${value.notify.client-priv}" else ""} \
-p ${toString cfg.port} \
-u ${value.notify.user} \
${if cfg.networks.${name}.config ? local-pass then
"-w ${cfg.networks.${name}.config.local-pass}" else ""} \
${name} \
${if value.notify.command != "" then "\"${value.notify.command}\"" else
pkgs.writeShellScript "pounce-${name}-notify-script" value.notify.script}
'';
Restart = "on-failure";
# pounce will refuse all connections before it's connected to the
# IRC network, but there's no easy way for systemd to know when
# that's happened. The best I've come up with is starting
# pounce-notify anyways and retrying with a fairly long delay.
# This value works for me, hopefully it works for you too.
RestartSec = "15s";
} // hardeningFlags;
};
})
(mkIf (value.palaver.enable) {
"pounce-${name}-palaver" = {
wantedBy = [ "multi-user.target" ];
requires = [ "calico.service" "pounce-${name}.service" ];
after = [ "calico.service" "pounce-${name}.service" ];
description = "Pounce palaver notification client for the ${name} network.";
serviceConfig = {
User = cfg.user;
Group = cfg.user;
Environment = "SHELL=${pkgs.bash}/bin/bash";
ExecStart = ''
${pkg}/bin/pounce-palaver \
${if value.palaver.insecure then "-!" else
if value.palaver.trust-cert == "" then
"-t ${value.fullChain}"
else if value.palaver.trust-cert != null then
"-t ${value.palaver.trust-cert}" else ""} \
${if value.palaver.client-cert != "" then "-c ${value.notify.client-cert}" else ""} \
${if value.palaver.client-priv != "" then "-k ${value.notify.client-priv}" else ""} \
-p ${toString cfg.port} \
-u ${value.palaver.user} \
${if cfg.networks.${name}.config ? local-pass then
"-w ${cfg.networks.${name}.config.local-pass}" else ""} \
${name} \
${if value.palaver.noPreviews then "-N" else ""} \
${if value.palaver.noPrivateMessagePreviews then "-N" else ""} \
${if value.palaver.dbPath != "" then "-d ${value.palaver.dbPath}" else ""} \
${if value.palaver.caseSensitive then "-s" else "" } \
${if value.palaver.verbose then "-v" else ""}
'';
Restart = "on-failure";
# pounce will refuse all connections before it's connected to the
# IRC network, but there's no easy way for systemd to know when
# that's happened. The best I've come up with is starting
# pounce-palaver anyways and retrying with a fairly long delay.
# This value works for me, hopefully it works for you too.
RestartSec = "15s";
} // hardeningFlags;
};
})
]) cfg.networks)
);
users = optionalAttrs (cfg.user == defaultUser) {
users.${defaultUser} = {
group = defaultUser;
isSystemUser = true;
};
groups.${defaultUser} = { };
};
};
};
};
}