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
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} = { };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|