Working backplane dyndns client/server

This commit is contained in:
nostoromo root 2020-11-16 12:39:37 -08:00
parent bbf4a90e46
commit 4d6e8cb264
19 changed files with 1242 additions and 133 deletions

View File

@ -0,0 +1,7 @@
{ ... }:
{
imports = [
./dns.nix
];
}

View File

@ -0,0 +1,271 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.fudo.backplane.dns;
dns = import ../../../lib/dns.nix { inherit lib; };
backup-directory = "/var/lib/fudo/backplane/dns";
powerdns-home = "/var/lib/powerdns";
powerdns-conf-dir = "${powerdns-home}/conf.d";
backplaneOpts = { ... }: {
options = {
host = mkOption {
type = types.str;
description = "Hostname of the backplane jabber server.";
};
role = mkOption {
type = types.str;
description = "Backplane XMPP role name for the DNS server.";
default = "service-dns";
};
password-file = mkOption {
type = types.str;
description = "File containing XMPP password for backplane role.";
};
database = mkOption {
type = with types; submodule databaseOpts;
description = "Database settings for backplane server.";
};
};
};
databaseOpts = { ... }: {
options = {
host = mkOption {
type = types.str;
description = "Hostname or IP of the PostgreSQL server.";
};
database = mkOption {
type = types.str;
description = "Database to use for DNS backplane.";
default = "backplane_dns";
};
username = mkOption {
type = types.str;
description = "Database user for DNS backplane.";
default = "backplane_dns";
};
password-file = mkOption {
type = types.str;
description = "File containing password for database user.";
};
};
};
lisp-libs = [];
launchScript = pkgs.writeText "launch-backplane-dns.lisp" ''
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
(ql:quickload :backplane-dns)
(backplane-dns:start-listener-with-env)
(loop (sleep 600))
'';
in {
options.fudo.backplane.dns = {
enable = mkEnableOption "Enable backplane dynamic DNS server.";
port = mkOption {
type = types.port;
description = "Port on which to serve authoritative DNS requests.";
default = 53;
};
listen-addresses = mkOption {
type = with types; listOf str;
description = "IP addresses on which to listen for dns requests.";
default = [ "0.0.0.0" ];
};
required-services = mkOption {
type = with types; listOf str;
description = "A list of services required before the DNS server can start.";
};
user = mkOption {
type = types.str;
description = "User as which to run DNS backplane listener service.";
default = "backplane-dns";
};
group = mkOption {
type = types.str;
description = "Group as which to run DNS backplane listener service.";
default = "backplane-dns";
};
database = mkOption {
type = with types; submodule databaseOpts;
description = "Database settings for the DNS server.";
};
backplane = mkOption {
type = with types; submodule backplaneOpts;
description = "Backplane Jabber settings for the DNS server.";
};
};
config = mkIf cfg.enable {
users = {
users = {
"${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
createHome = true;
home = "/var/home/${cfg.user}";
};
backplane-powerdns = {
isSystemUser = true;
};
};
groups = {
"${cfg.group}" = {
members = [cfg.user];
};
backplane-powerdns = {
members = [ "backplane-powerdns" ];
};
};
};
systemd = {
targets = {
backplane-dns = {
description = "Fudo DNS backplane services.";
wantedBy = [ "multi-user.target" ];
};
};
services = {
backplane-powerdns = let
configDir = pkgs.writeTextDir "pdns.conf" ''
local-address=${lib.concatStringsSep ", " cfg.listen-addresses}
local-port=${toString cfg.port}
launch=
include-dir=${powerdns-conf-dir}/
'';
psql-user = config.services.postgresql.superUser;
in {
unitConfig.Documentation = "man:pdns_server(1) man:pdns_control(1)";
description = "Backplane PowerDNS name server";
requires = [
"postgresql.service"
"backplane-dns-config-generator.service"
"backplane-dns.target"
];
after = [
"network.target"
"postgresql.service"
];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ postgresql ];
serviceConfig = {
Restart="on-failure";
RestartSec="10";
StartLimitInterval="0";
PrivateDevices=true;
# CapabilityBoundingSet="CAP_CHOWN CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID CAP_SYS_CHROOT";
# NoNewPrivileges=true;
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${powerdns-home}";
ExecStart = "${pkgs.powerdns}/bin/pdns_server --setuid=backplane-powerdns --setgid=backplane-powerdns --chroot=${powerdns-home} --socket-dir=/ --daemon=no --guardian=no --disable-syslog --write-pid=no --config-dir=${configDir}";
ProtectSystem="full";
# ProtectHome=true;
RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
};
};
backplane-dns-config-generator = {
description = "Generate postgres configuration for backplane DNS server.";
requiredBy = [ "backplane-powerdns.service" ];
requires = cfg.required-services;
serviceConfig.Type = "oneshot";
restartIfChanged = true;
partOf = [ "backplane-dns.target" ];
# This builds the config in a bash script, to avoid storing the password
# in the nix store at any point
script = ''
if [ ! -d ${powerdns-conf-dir} ]; then
mkdir ${powerdns-conf-dir}
fi
TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t pdns-XXXXXXXXXX)
TMPCONF=$TMPDIR/pdns.local.gpgsql.conf
if [ ! -f ${cfg.database.password-file} ]; then
echo "${cfg.database.password-file} does not exist!"
exit 1
fi
touch $TMPCONF
chown backplane-powerdns:backplane-powerdns $TMPCONF
chmod go-rwx $TMPCONF
PASSWORD=$(cat ${cfg.database.password-file})
echo "launch+=gpgsql" >> $TMPCONF
echo "gpgsql-host=${cfg.database.host}" >> $TMPCONF
echo "gpgsql-dbname=${cfg.database.database}" >> $TMPCONF
echo "gpgsql-user=${cfg.database.username}" >> $TMPCONF
echo "gpgsql-password=$PASSWORD" >> $TMPCONF
echo "gpgsql-dnssec=yes" >> $TMPCONF
mv $TMPCONF ${powerdns-conf-dir}/pdns.local.gpgsql.conf
rm -rf $TMPDIR
exit 0
'';
};
backplane-dns = {
description = "Fudo DNS Backplane Server";
restartIfChanged = true;
serviceConfig = {
ExecStartPre = "${pkgs.lispPackages.quicklisp}/bin/quicklisp init";
ExecStart = "${pkgs.sbcl}/bin/sbcl --load ${launchScript}";
Restart = "on-failure";
PIDFile = "/run/backplane-dns.$USERNAME.pid";
User = cfg.user;
Group = cfg.group;
};
environment = {
LD_LIBRARY_PATH = "${pkgs.openssl_1_1.out}/lib";
FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = cfg.backplane.host;
FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane.role;
FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE = cfg.backplane.password-file;
FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.backplane.database.host;
FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.backplane.database.database;
FUDO_DNS_BACKPLANE_DATABASE_USERNAME = cfg.backplane.database.username;
FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE = cfg.backplane.database.password-file;
CL_SOURCE_REGISTRY = lib.concatStringsSep ":" (map (pkg: "${pkg}//")
(lisp-libs ++ [pkgs.backplane-dns]));
};
requires = cfg.required-services;
partOf = [ "backplane-dns.target" ];
wantedBy = [ "multi-user.target" ];
};
};
};
};
}

View File

@ -0,0 +1,94 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.fudo.client.dns;
in {
options.fudo.client.dns = {
enable = mkEnableOption "Enable Fudo DynDNS Client.";
ipv4 = mkOption {
type = types.bool;
default = true;
description = "Report host external IPv4 address to Fudo DynDNS server.";
};
ipv6 = mkOption {
type = types.bool;
default = true;
description = "Report host external IPv6 address to Fudo DynDNS server.";
};
domain = mkOption {
type = types.str;
description = "Domain under which this host is registered.";
default = "fudo.link";
};
server = mkOption {
type = types.str;
description = "Backplane DNS server to which changes will be reported.";
default = "backplane.fudo.org";
};
password-file = mkOption {
type = types.str;
description = "File containing host password for backplane.";
example = "/path/to/secret.passwd";
};
frequency = mkOption {
type = types.str;
description = "Frequency at which to report the local IP(s) to backplane.";
default = "*:0/15";
};
user = mkOption {
type = types.str;
description = "User as which to run the client script (must have access to password file).";
default = "backplane-dns-client";
};
external-interface = mkOption {
type = with types; nullOr str;
description = "Interface with which this host communicates with the larger internet.";
default = null;
};
};
config = mkIf cfg.enable {
users.users = {
"${cfg.user}" = {
isSystemUser = true;
createHome = true;
home = "/var/home/${cfg.user}";
};
};
systemd = {
timers.backplane-dns-client = {
enable = true;
description = "Report local IP addresses to Fudo backplane.";
partOf = [ "backplane-dns-client.service" ];
wantedBy = [ "timers.target" ];
requires = [ "network-online.target" ];
timerConfig = {
OnCalendar = cfg.frequency;
};
};
services.backplane-dns-client = {
enable = true;
serviceConfig = {
Type = "oneshot";
StandardOutput = "journal";
User = cfg.user;
};
script = ''
${pkgs.backplane-dns-client}/bin/backplane-dns-client ${optionalString cfg.ipv4 "-4"} ${optionalString cfg.ipv6 "-6"} ${optionalString (cfg.external-interface != null) "--interface=${cfg.external-interface}"} --domain=${cfg.domain} --server=${cfg.server} --password-file=${cfg.password-file}
'';
};
};
};
}

View File

@ -7,7 +7,8 @@ let
join-lines = concatStringsSep "\n";
ip = import ../../lib/ip.nix { lib = lib; };
ip = import ../../lib/ip.nix { inherit lib; };
dns = import ../../lib/dns.nix { inherit lib; };
hostOpts = { hostname, ... }: {
options = {
@ -35,33 +36,6 @@ let
traceout = out: builtins.trace out out;
srvRecordOpts = with types; {
options = {
weight = mkOption {
type = int;
description = "Weight relative to other records.";
default = 1;
};
priority = mkOption {
type = int;
description = "Priority to give this record.";
default = 0;
};
port = mkOption {
type = port;
description = "Port to use when connecting.";
};
host = mkOption {
type = str;
description = "Host to contact for this service.";
example = "my-host.my-domain.com.";
};
};
};
in {
options.fudo.local-network = {
@ -143,7 +117,7 @@ in {
};
srv-records = mkOption {
type = with types; attrsOf (attrsOf (listOf (submodule srvRecordOpts)));
type = dns.srvRecords;
description = "Map of traffic type to srv records.";
default = {};
example = {
@ -231,12 +205,6 @@ in {
hostSshFpRecords = host: data: join-lines (map (sshfp: "${host} IN SSHFP ${sshfp}") data.ssh-fingerprints);
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
makeSrvRecords = protocol: type: records:
join-lines (map (record: "_${type}._${protocol} IN SRV ${toString record.priority} ${toString record.weight} ${toString record.port} ${record.host}.")
records);
makeSrvProtocolRecords = protocol: types: join-lines (mapAttrsToList (makeSrvRecords protocol) types);
in {
enable = true;
cacheNetworks = [ cfg.network "localhost" "localnets" ];
@ -267,7 +235,7 @@ in {
${join-lines (mapAttrsToList hostSshFpRecords cfg.hosts)}
${join-lines (mapAttrsToList cnameRecord cfg.aliases)}
${join-lines cfg.extra-dns-records}
${join-lines (mapAttrsToList makeSrvProtocolRecords cfg.srv-records)}
${dns.srvRecordsToBindZone cfg.srv-records}
'';
}
] ++ blockZones;

114
config/fudo/password.nix Normal file
View File

@ -0,0 +1,114 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.password;
genOpts = {
options = {
file = mkOption {
type = types.str;
description = "Password file in which to store a generated password.";
};
user = mkOption {
type = types.str;
description = "User to which the file should belong.";
};
group = mkOption {
type = with types; nullOr str;
description = "Group to which the file should belong.";
default = "nogroup";
};
restart-services = mkOption {
type = with types; listOf str;
description = "List of services to restart when the password file is generated.";
default = [];
};
};
};
generate-passwd-file = file: user: group: pkgs.writeShellScriptBin "generate-passwd-file.sh" ''
if touch ${file}; then
${pkgs.pwgen}/bin/pwgen 30 1 > ${file}
else
echo "cannot write to ${file}"
exit 2
fi
if [ ! -f ${file} ]; then
echo "Failed to create file ${file}"
exit 3
fi
chown ${user}${optionalString (group != null) ":${group}"} ${file}
if [ $? -ne 0 ]; then
rm ${file}
echo "failed to set permissions on ${file}"
exit 4
fi
${if (group != null) then
"chmod 640 ${file}"
else
"chmod 600 ${file}"}
echo "created password file ${file}"
exit 0
'';
restart-script = service-name: ''
SYSCTL=${pkgs.systemd}/bin/systemctl
JOBTYPE=$(${pkgs.systemd}/bin/systemctl show ${service-name} -p Type)
if $SYSCTL is-active --quiet ${service-name} ||
[ $JOBTYPE == "Type=simple" ] ||
[ $JOBTYPE == "Type=oneshot" ] ; then
echo "restarting service ${service-name} because password has changed."
$SYSCTL restart ${service-name}
fi
'';
filterForRestarts = filterAttrs (name: opts: opts.restart-services != []);
in {
options.fudo.password = {
file-generator = mkOption {
type = with types; loaOf (submodule genOpts);
description = "List of password files to generate.";
default = {};
};
};
config = {
systemd.targets.fudo-passwords = {
description = "Target indicating that all Fudo passwords have been generated.";
wantedBy = [ "default.target" ];
};
systemd.services = fold (a: b: a // b) {} (mapAttrsToList (name: opts: {
"file-generator-${name}" = {
partOf = [ "fudo-passwords.target" ];
serviceConfig.Type = "oneshot";
description = "Generate password file for ${name}.";
script = "${generate-passwd-file opts.file opts.user opts.group}/bin/generate-passwd-file.sh";
};
"file-generator-watcher-${name}" = mkIf (! (opts.restart-services == [])) {
description = "Restart services upon regenerating password for ${name}";
after = [ "file-generator-${name}.service" ];
partOf = [ "fudo-passwords.target" ];
serviceConfig.Type = "oneshot";
script = concatStringsSep "\n" (map restart-script opts.restart-services);
};
}) cfg.file-generator);
systemd.paths = mapAttrs' (name: opts:
nameValuePair "file-generator-watcher-${name}" {
partOf = [ "fudo-passwords.target"];
pathConfig.PathChanged = opts.file;
}) (filterForRestarts cfg.file-generator);
};
}

View File

@ -2,23 +2,51 @@
with lib;
let
cfg = config.fudo.postgresql;
utils = import ../../lib/utils.nix { inherit lib; };
join-lines = lib.concatStringsSep "\n";
userDatabaseOpts = { database, ... }: {
options = {
access = mkOption {
type = types.str;
description = "Privileges for user on this database.";
default = "CONNECT";
};
entity-access = mkOption {
type = with types; attrsOf str;
description = "A list of entities mapped to the access this user should have.";
default = {};
example = {
"TABLE users" = "SELECT,DELETE";
"ALL SEQUENCES IN public" = "SELECT";
};
};
};
};
userOpts = { username, ... }: {
options = {
password = mkOption {
password-file = mkOption {
type = with types; nullOr str;
description = "The user's (plaintext) password.";
description = "A file containing the user's (plaintext) password.";
default = null;
};
databases = mkOption {
type = with types; loaOf str;
description = "Map of databases to which this user has access, to the required perms.";
type = with types; attrsOf (submodule userDatabaseOpts);
description = "Map of databases to required database/table perms.";
default = {};
example = {
my_database = "ALL PRIVILEGES";
my_database = {
access = "ALL PRIVILEGES";
entity-access = {
"ALL TABLES" = "SELECT";
};
};
};
};
};
@ -34,43 +62,68 @@ let
};
};
userDatabaseAccess = user: databases:
mapAttrs' (database: perms:
nameValuePair "DATABASE ${database}" perms)
databases;
filterPasswordedUsers = filterAttrs (user: opts: opts.password-file != null);
stringJoin = joiner: els:
if (length els) == 0 then
""
else
foldr(lel: rel: "${lel}${joiner}${rel}") (last els) (init els);
password-setter-script = user: password-file: sql-file: ''
unset PASSWORD
if [ ! -f ${password-file} ]; then
echo "file does not exist: ${password-file}"
exit 1
fi
PASSWORD=$(cat ${password-file})
echo "setting password for user ${user}"
echo "ALTER USER ${user} ENCRYPTED PASSWORD '$PASSWORD';" >> ${sql-file}
'';
passwords-setter-script = users:
pkgs.writeScriptBin "postgres-set-passwords.sh" ''
#!${pkgs.bash}/bin/bash
if [ $# -ne 1 ]; then
echo "usage: $0 output-file.sql"
exit 1
fi
OUTPUT_FILE=$1
if [ ! -f $OUTPUT_FILE ]; then
echo "file doesn't exist: $OUTPUT_FILE"
exit 2
fi
${join-lines
(mapAttrsToList
(user: opts: password-setter-script user opts.password-file "$OUTPUT_FILE")
(filterPasswordedUsers users))}
'';
userDatabaseAccess = user: databases:
mapAttrs' (database: databaseOpts:
nameValuePair "DATABASE ${database}" databaseOpts.access)
databases;
makeEntry = nw:
"host all all ${nw} gss include_realm=0 krb_realm=FUDO.ORG";
makeNetworksEntry = networks:
stringJoin "\n" (map makeEntry networks);
setPasswordSql = username: attrs:
"ALTER USER ${username} ENCRYPTED PASSWORD '${attrs.password}';";
setPasswordsSql = users:
(stringJoin "\n"
(mapAttrsToList (username: attrs: setPasswordSql username attrs)
(filterAttrs (user: attrs: attrs.password != null) users))) + "\n";
makeNetworksEntry = networks: join-lines (map makeEntry networks);
makeLocalUserPasswordEntries = users:
stringJoin "\n"
(mapAttrsToList
(username: attrs:
stringJoin "\n"
join-lines (mapAttrsToList
(user: opts: join-lines
(map (db: ''
local ${db} ${username} md5
host ${db} ${username} 127.0.0.1/16 md5
host ${db} ${username} ::1/128 md5
'') (attrNames attrs.databases)))
users);
local ${db} ${user} md5
host ${db} ${user} 127.0.0.1/16 md5
host ${db} ${user} ::1/128 md5
'') (attrNames opts.databases)))
(filterPasswordedUsers users));
userTableAccessSql = user: entity: access: "GRANT ${access} ON ${entity} TO ${user};";
userDatabaseAccessSql = user: database: dbOpts: ''
\c ${database}
${join-lines (mapAttrsToList (userTableAccessSql user) dbOpts.entity-access)}
'';
userAccessSql = user: userOpts: join-lines (mapAttrsToList (userDatabaseAccessSql user) userOpts.databases);
usersAccessSql = users: join-lines (mapAttrsToList userAccessSql users);
in {
@ -106,8 +159,15 @@ in {
description = "A map of users to user attributes.";
example = {
sampleUser = {
password = "some-password";
databases = [ "sample_user_db" ];
password-file = "/path/to/password/file";
databases = {
some_database = {
access = "CONNECT";
entity-access = {
"TABLE some_table" = "SELECT,UPDATE";
};
};
};
};
};
default = {};
@ -136,6 +196,13 @@ in {
description = "Users able to access the server via local socket.";
default = [];
};
required-services = mkOption {
type = with types; listOf str;
description = "List of services that should run before postgresql.";
default = [];
example = [ "password-generator.service" ];
};
};
config = mkIf cfg.enable {
@ -166,13 +233,6 @@ in {
group = "postgres";
source = cfg.keytab;
};
"postgresql/private/user-script.sql" = {
mode = "0400";
user = "postgres";
group = "postgres";
text = setPasswordsSql cfg.users;
};
};
};
@ -187,15 +247,20 @@ in {
package = pkgs.postgresql_11_gssapi;
enableTCPIP = true;
ensureDatabases = mapAttrsToList (name: value: name) cfg.databases;
ensureUsers = mapAttrsToList
ensureUsers = ((mapAttrsToList
(username: attrs:
{
name = username;
ensurePermissions =
#{ "DATABASE ${username}" = "ALL PRIVILEGES"; };
(userDatabaseAccess username attrs.databases);
ensurePermissions = userDatabaseAccess username attrs.databases;
})
cfg.users;
cfg.users) ++ (flatten (mapAttrsToList
(database: opts:
(map (username: {
name = username;
ensurePermissions = {
"DATABASE ${database}" = "ALL PRIVILEGES";
};
}) opts.users)) cfg.databases)));
extraConfig = ''
krb_server_keyfile = '/etc/postgresql/private/postgres.keytab'
@ -221,15 +286,54 @@ in {
# local networks
${makeNetworksEntry cfg.local-networks}
'';
# initialScript = pkgs.writeText "database-init.sql" ''
# ${setPasswordsSql cfg.users}
# '';
};
systemd.services.postgresql.postStart = ''
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -f /etc/postgresql/private/user-script.sql -d postgres
systemd = {
services = {
postgresql-password-setter = let
passwords-script = passwords-setter-script cfg.users;
password-wrapper-script = pkgs.writeScriptBin "password-script-wrapper.sh" ''
#!${pkgs.bash}/bin/bash
TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t postgres-XXXXXXXXXX)
echo "using temp dir $TMPDIR"
PASSWORD_SQL_FILE=$TMPDIR/user-passwords.sql
echo "password file $PASSWORD_SQL_FILE"
touch $PASSWORD_SQL_FILE
chown ${config.services.postgresql.superUser} $PASSWORD_SQL_FILE
chmod go-rwx $PASSWORD_SQL_FILE
${passwords-script}/bin/postgres-set-passwords.sh $PASSWORD_SQL_FILE
echo "executing $PASSWORD_SQL_FILE"
${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -d postgres -f $PASSWORD_SQL_FILE
echo rm $PASSWORD_SQL_FILE
echo "Postgresql user passwords set.";
exit 0
'';
in {
description = "A service to set postgresql user passwords after the server has started.";
after = [ "postgresql.service" ] ++ cfg.required-services;
requires = [ "postgresql.service" ] ++ cfg.required-services;
serviceConfig = {
Type = "oneshot";
User = config.services.postgresql.superUser;
};
script = "${password-wrapper-script}/bin/password-script-wrapper.sh";
};
postgresql.postStart = let
allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;";
extra-settings-sql = pkgs.writeText "settings.sql" ''
${concatStringsSep "\n" (map allow-user-login (mapAttrsToList (key: val: key) cfg.users))}
${usersAccessSql cfg.users}
'';
in ''
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -d postgres -f ${extra-settings-sql}
${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL*
'';
};
};
};
}

View File

@ -5,7 +5,9 @@ with lib;
imports = [
./fudo/acme-for-hostname.nix
./fudo/authentication.nix
./fudo/backplane
./fudo/chat.nix
./fudo/client/dns.nix
./fudo/common.nix
./fudo/dns.nix
./fudo/git.nix
@ -18,6 +20,7 @@ with lib;
./fudo/minecraft-server.nix
./fudo/netinfo-email.nix
./fudo/node-exporter.nix
./fudo/password.nix
./fudo/postgres.nix
./fudo/prometheus.nix
./fudo/secure-dns-proxy.nix

View File

@ -59,6 +59,7 @@
lshw
mkpasswd
ncurses5
nixfmt
nix-index
nix-prefetch-git
nmap

View File

@ -32,7 +32,7 @@ let
if [ $# -gt 1 ]; then
echo "usage: $0 [timeout]"
exit 1
elif [ $# -eq 1 ]; then
elif [ $# -eq 1 ]; the
TIMEOUT=$1
else
TIMEOUT=15m

View File

@ -1,12 +1,21 @@
{ lib, config, pkgs, ... }:
with lib;
let
hostname = "nostromo";
host-internal-ip = "10.0.0.1";
inherit (lib.strings) concatStringsSep;
in {
environment.systemPackages = with pkgs; [
dnsproxy
google-photos-uploader
libguestfs-with-appliance
libvirt
powerdns
virtmanager
];
boot.kernelModules = [ "kvm-amd" ];
boot.loader.grub.enable = true;
@ -34,7 +43,6 @@ in {
dns-serve-ips = [ host-internal-ip "127.0.0.1" "127.0.1.1" ];
# Using a pihole running in docker, see below
recursive-resolver = "${host-internal-ip} port 5353";
# recursive-resolver = "1.1.1.1";
server-ip = host-internal-ip;
};
@ -86,21 +94,150 @@ in {
nat = {
enable = true;
externalInterface = "eno2";
internalInterfaces = ["intif0"];
internalInterfaces = [ "intif0" ];
};
};
users = {
users = {
backplane-powerdns = {
isSystemUser = true;
};
backplane-dns = {
isSystemUser = true;
};
fudo-client = {
isSystemUser = true;
};
};
groups = {
backplane-powerdns = {
members = [ "backplane-powerdns" ];
};
backplane-dns = {
members = [ "backplane-dns" ];
};
};
};
fudo = {
password.file-generator = {
dns_backplane_powerdns = {
file = "/srv/backplane/dns/secure/db_powerdns.passwd";
user = config.services.postgresql.superUser;
group = "backplane-powerdns";
restart-services = [
"backplane-dns-config-generator.service"
"postgresql-password-setter.service"
"backplane-powerdns.service"
];
};
dns_backplane_database = {
file = "/srv/backplane/dns/secure/db_backplane.passwd";
user = config.services.postgresql.superUser;
group = "backplane-dns";
restart-services = [
"backplane-dns.service"
"postgresql-password-setter.service"
];
};
};
backplane.dns = {
enable = true;
port = 353;
listen-addresses = [ "10.0.0.1" ];
required-services = [ "fudo-passwords.target" ];
user = "backplane-dns";
group = "backplane-dns";
database = {
username = "backplane_powerdns";
database = "backplane_dns";
# Uses an IP to avoid cyclical dependency...not really relevant, but
# whatever
host = "127.0.0.1";
password-file = "/srv/backplane/dns/secure/db_powerdns.passwd";
};
backplane = {
host = "backplane.fudo.org";
role = "service-dns";
password-file = "/srv/backplane/dns/secure/backplane.passwd";
database = {
username = "backplane_dns";
database = "backplane_dns";
host = "127.0.0.1";
password-file = "/srv/backplane/dns/secure/db_backplane.passwd";
};
};
};
client.dns = {
enable = true;
ipv4 = true;
ipv6 = true;
domain = "dyn.fudo.org";
user = "fudo-client";
external-interface = "eno2";
password-file = "/srv/client/secure/client.passwd";
};
postgresql = {
enable = true;
ssl-private-key = "/srv/nostromo/certs/private/privkey.pem";
ssl-certificate = "/srv/nostromo/certs/cert.pem";
keytab = "/srv/nostromo/keytabs/postgres.keytab";
required-services = [ "fudo-passwords.target" ];
local-networks = [
"10.0.0.1/24"
"127.0.0.1/8"
];
users = {
backplane_powerdns = {
password-file = "/srv/backplane/dns/secure/db_powerdns.passwd";
databases = {
backplane_dns = {
access = "CONNECT";
entity-access = {
"ALL TABLES IN SCHEMA public" = "SELECT";
};
};
};
};
backplane_dns = {
password-file = "/srv/backplane/dns/secure/db_backplane.passwd";
databases = {
backplane_dns = {
access = "CONNECT";
entity-access = {
"ALL TABLES IN SCHEMA public" = "SELECT,INSERT,UPDATE";
"ALL SEQUENCES IN SCHEMA public" = "SELECT,UPDATE";
};
};
};
};
niten = {
databases = {
backplane_dns = {
access = "ALL PRIVILEGES";
entity-access = {
"ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
"ALL SEQUENCES IN SCHEMA public" = "ALL PRIVILEGES";
};
};
};
};
};
local-users = ["niten"];
databases = {
backplane_dns = {
users = ["niten"];
};
};
};
secure-dns-proxy = {
@ -119,13 +256,6 @@ in {
};
};
environment.systemPackages = with pkgs; [
dnsproxy
libguestfs-with-appliance
libvirt
virtmanager
];
virtualisation = {
docker = {
enable = true;
@ -166,7 +296,7 @@ in {
};
services = {
dhcpd6.enable = false;
# dhcpd6.enable = true;
nginx = {
enable = true;
@ -192,33 +322,5 @@ in {
};
};
};
# ceph = {
# enable = true;
# global = {
# clusterName = "sea-data";
# clusterNetwork = "10.0.10.0/24";
# fsid = "d443e192-896d-4102-a60f-f8f0777eb2a3";
# monHost = "10.0.10.2";
# monInitialMembers = "mon-1";
# publicNetwork = "10.0.0.0/22";
# };
# mds = {
# enable = true;
# daemons = ["srv-2"];
# };
# mgr = {
# enable = true;
# daemons = ["srv-2"];
# };
# mon = {
# enable = true;
# daemons = ["srv-2"];
# };
# };
};
}

58
lib/dns.nix Normal file
View File

@ -0,0 +1,58 @@
{ lib }:
with lib;
let
join-lines = concatStringsSep "\n";
makeSrvRecords = protocol: type: records:
join-lines (map (record:
"_${type}._${protocol} IN SRV ${toString record.priority} ${toString record.weight} ${toString record.port} ${record.host}.")
records);
makeSrvProtocolRecords = protocol: types: join-lines (mapAttrsToList (makeSrvRecords protocol) types);
srvRecordOpts = with types; {
options = {
weight = mkOption {
type = int;
description = "Weight relative to other records.";
default = 1;
};
priority = mkOption {
type = int;
description = "Priority to give this record.";
default = 0;
};
port = mkOption {
type = port;
description = "Port to use when connecting.";
};
host = mkOption {
type = str;
description = "Host to contact for this service.";
example = "my-host.my-domain.com.";
};
};
};
srvRecordPair = domain: protocol: type: record: {
"_${type}._${protocol}.${domain}" = "${toString record.priority} ${toString record.weight} ${toString record.port} ${record.host}.";
};
in rec {
srvRecords = with types; attrsOf (attrsOf (listOf (submodule srvRecordOpts)));
srvRecordsToBindZone = srvRecords: join-lines (mapAttrsToList makeSrvProtocolRecords srvRecords);
concatMapAttrs = f: attrs: concatMap (x: x) (mapAttrsToList (key: val: f key val) attrs);
srvRecordsToPairs = domain: srvRecords:
listToAttrs
(concatMapAttrs (protocol: types:
concatMapAttrs (type: records: map (srvRecordPair domain protocol type) records) types)
srvRecords);
}

14
lib/utils.nix Normal file
View File

@ -0,0 +1,14 @@
{ lib }:
with lib;
{
recursiveMergeAttrs = a: b: let
commonAttrs = intersectLists (attrNames a) (attrNames b);
aAttrs = subtractLists (attrNames a) commonAttrs;
bAttrs = subtractLists (attrNames b) commonAttrs;
aSide = (filterAttrs (k: v: elem k aAttrs) a);
bSide = (filterAttrs (k: v: elem k bAttrs) b);
common = (foldr (a: b: a // b) {}
(map (k: { ${k} = a.${k} // b.${k}; }) commonAttrs));
in aSide // bSide // common;
}

View File

@ -0,0 +1,35 @@
{ stdenv, fetchgit, pkgs, bundlerEnv }:
let
url = "https://git.fudo.org/fudo-public/backplane-dns-client.git";
version = "0.1";
srcdir = ../static/backplane-dns-client;
gems = bundlerEnv {
name = "backplane-dns-client-env";
ruby = pkgs.ruby;
gemdir = srcdir;
};
in stdenv.mkDerivation {
name = "backplane-dns-client-${version}";
src = srcdir;
buildInputs = [gems pkgs.ruby];
phases = ["installPhase"];
installPhase = ''
mkdir -p "$out/bin" "$out/lib"
cp "$src/dns-client.rb" "$out/lib"
BIN="$out/bin/backplane-dns-client"
cat > $BIN <<EOF
#!${pkgs.bash}/bin/bash -e
exec ${gems}/bin/bundle exec ${pkgs.ruby}/bin/ruby $out/lib/dns-client.rb "\$@"
EOF
chmod +x $BIN
'';
}

View File

@ -0,0 +1,24 @@
{ stdenv, fetchgit, pkgs }:
let
url = "https://git.fudo.org/fudo-public/backplane-dns.git";
version = "0.1";
in stdenv.mkDerivation {
name = "backplane-dns-${version}";
src = fetchgit {
url = url;
rev = "bfad36c9d223c7c8949fab50424c32a11164cd3a";
sha256 = "0s8g5cm9mdjr9wb8w6a8lc1dv5cg85hxp8bdcgr1xd6hs4fnr745";
fetchSubmodules = false;
};
phases = ["installPhase"];
installPhase = ''
mkdir -p "$out/lib/common-lisp/backplane-dns"
cp "$src/backplane-dns.asd" "$out/lib/common-lisp/backplane-dns"
cp -R $src/*.lisp "$out/lib/common-lisp/backplane-dns"
'';
}

View File

@ -61,6 +61,19 @@
inherit (pkgs) stdenv fetchurl makeWrapper cups dpkg a2ps ghostscript gnugrep gnused coreutils file perl which;
};
backplane-dns = import ./backplane-dns.nix {
pkgs = pkgs;
stdenv = pkgs.stdenv;
fetchgit = pkgs.fetchgit;
};
backplane-dns-client = import ./backplane-dns-client.nix {
pkgs = pkgs;
stdenv = pkgs.stdenv;
fetchgit = pkgs.fetchgit;
bundlerEnv = pkgs.bundlerEnv;
};
cl-gemini = import ./cl-gemini.nix {
pkgs = pkgs;
stdenv = pkgs.stdenv;
@ -71,5 +84,28 @@
fetchgit = pkgs.fetchgit;
pkgs = pkgs;
};
google-photos-uploader = pkgs.buildGoModule rec {
pname = "google-photos-uploader";
version = "1.6.1";
src = pkgs.fetchFromGitHub {
owner = "int128";
repo = "gpup";
rev = "${version}";
sha256 = "0zdkd5iwkp270p0810dijg25djkzrsdyqiqaqv6rzzgzj5d5pwhm";
};
modSha256 = "15ndc6jq51f9mz1v089416x2lxrifp3wglbxpff8b055jj07hbkw";
subPackages = [ "." ];
meta = with pkgs.lib; {
description = "Google photos uploader, written in Go.";
homepage = https://github.com/int128/gpup;
license = licenses.asl20;
platforms = platforms.linux ++ platforms.darwin;
};
};
};
}

View File

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem "xmpp4r"

View File

@ -0,0 +1,13 @@
GEM
remote: https://rubygems.org/
specs:
xmpp4r (0.5.6)
PLATFORMS
ruby
DEPENDENCIES
xmpp4r
BUNDLED WITH
1.17.2

View File

@ -0,0 +1,250 @@
require "ipaddr"
require "socket"
require "optparse"
require "json"
require "securerandom"
require "xmpp4r"
puts ARGV
options = {}
OptionParser.new do |opts|
opts.banner = "usage: ${$0} [opts]"
opts.on("-i", "--interface=INTERFACE",
"Publicly-accessible interface") do |interface|
options[:interface] = interface
end
opts.on("-d", "--domain=DOMAIN",
"Domain on which we wish to set the new ip") do |domain|
options[:domain] = domain
end
opts.on("-s", "--server=SERVER",
"Backplane DNS XMPP server") do |server|
options[:server] = server
end
opts.on("-p", "--password-file=/path/to/file",
"File containing password for XMPP server") do |pw_file|
options[:pw_file] = pw_file
end
opts.on("-4", "--ipv4",
"Check for a public IPv4 and register with the backplane.") do
options[:ipv4] = true
end
opts.on("-6", "--ipv6",
"Check for a public IPv6 and register with the backplane.") do
options[:ipv6] = true
end
end.parse!
def error(msg)
puts msg
throw msg
end
error("domain is required") if not options[:domain]
error("server is required") if not options[:server]
error("password file is required") if not options[:pw_file]
error("at least one of -4 or -6 required") if not (options[:ipv4] or options[:ipv6])
if not File::readable?(options[:pw_file])
error("file does not exist or is not readable")
end
password = File::open(options[:pw_file]) { |f| f.gets.strip }
class XMPPClient
def initialize(domain, hostname, server, password)
@jid = "host-#{hostname}@#{server}"
@service_jid = "service-dns@#{server}"
@server = server
@domain = domain
@password = password
@responses = Queue.new
@responses_lock = Mutex.new
end
def connect
disconnect if connected?
@client = Jabber::Client::new(@jid)
@client.connect # will use SRV records
error("failed to initialize TLS connection") if not @client.is_tls?
@client.auth(@password)
register_response_callback
end
def connected?
@client ||= nil
@client.respond_to?(:is_connected?) and @client.is_connected?
end
def disconnect
if @client.respond_to?(:is_connected?) && @client.is_connected?
begin
@client.close
rescue Errno::EPIPE, IOError => e
nil
end
end
@client = nil
end
def send(msg_content)
msg_id = SecureRandom::uuid
encoded_payload = payload(msg_content, msg_id).to_json
msg = Jabber::Message.new(@service_jid, encoded_payload)
msg.type = :chat
@client.send(msg)
response = receive_response(msg_id)
response and response["status"] == "OK"
end
def send_ip(ip)
send(ip_payload(ip))
end
def payload(req, msg_id)
{
version: 1,
service: :dns,
msgid: msg_id,
payload: req
}
end
def ip_payload(ip)
{
request: ip.ipv4? ? :change_ipv4 : :change_ipv6,
domain: @domain,
ip: ip.to_s
}
end
def register_response_callback
@client.add_message_callback do |msg|
enqueue_message(JSON.parse(msg.body))
end
end
def enqueue_message(msg)
@responses << msg
end
def receive_response(msg_id)
msg = @responses.pop
return msg if (msg and (msg["msgid"] == msg_id.to_s))
raise "failed to receive message: #{msg}"
end
end
RESERVED_V4_NETWORKS = [
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/4",
"240.0.0.0/4",
"255.255.255.255/32"
].map { |ip| IPAddr.new(ip) }
def public_ip?(ip)
if (ip.ipv4?)
not RESERVED_V4_NETWORKS.any? { |network| network.include? ip }
elsif (ip.ipv6?)
not (ip.link_local? or ip.loopback? or ip.private?)
else
false
end
end
def to_ipaddr(addrinfo)
if addrinfo.ipv4?
IPAddr.new addrinfo.ip_address
else
IPAddr.new(addrinfo.ip_address.split("%")[0])
end
end
def local_addresses
Socket::ip_address_list.map do |addrinfo|
to_ipaddr(addrinfo)
end.select { |ip| public_ip?(ip) }
end
def interface_addresses(interface)
Socket::getifaddrs.select do |ifaddr|
ifaddr.name == interface
end.select do |ifaddr|
ifaddr.addr.ip? and (ifaddr.flags & Socket::IFF_MULTICAST != 0)
end.map do |ifaddr|
to_ipaddr(ifaddr.addr)
end.filter do |ip|
public_ip? ip
end
end
client = XMPPClient::new(options[:domain],
Socket::gethostname,
options[:server],
password)
success = true
begin
client.connect
addrs = if options[:interface]
interface_addresses(options[:interface])
else
local_addresses
end
if options[:ipv4]
ipv4 = addrs.find { |ip| ip.ipv4? }
if ipv4
puts "#{options[:server]}: #{Socket::gethostname}.#{options[:domain]} IN A => #{ipv4.to_s}"
if client.send_ip(ipv4)
puts "OK"
else
puts "ERROR"
success = false
end
else
puts "#{options[:server]}: no valid public IPv4 found on the local host"
end
end
if options[:ipv6]
ipv6 = addrs.find { |ip| ip.ipv6? }
if ipv6
puts "#{options[:server]}: #{Socket::gethostname}.#{options[:domain]} IN AAAA => #{ipv6.to_s}"
if client.send_ip(ipv6)
puts "OK"
else
puts "ERROR"
success = false
end
else
puts "#{options[:server]}: no valid public IPv6 found on the local host"
end
end
ensure
client.disconnect
end
exit success ? 0 : 1

View File

@ -0,0 +1,12 @@
{
xmpp4r = {
groups = ["default"];
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "15ls2yqjvflxrc8chv5pcdh2p1p9fjsky74yc8y7wvw90wz0izrb";
type = "gem";
};
version = "0.5.6";
};
}