d4e5bb34b7
That way 'nobody' is prevented from messing with the databases.
307 lines
8.8 KiB
Nix
307 lines
8.8 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.geoip-updater;
|
|
|
|
dbBaseUrl = "https://geolite.maxmind.com/download/geoip/database";
|
|
|
|
randomizedTimerDelaySec = "3600";
|
|
|
|
# Use writeScriptBin instead of writeScript, so that argv[0] (logged to the
|
|
# journal) doesn't include the long nix store path hash. (Prefixing the
|
|
# ExecStart= command with '@' doesn't work because we start a shell (new
|
|
# process) that creates a new argv[0].)
|
|
geoip-updater = pkgs.writeScriptBin "geoip-updater" ''
|
|
#!${pkgs.stdenv.shell}
|
|
skipExisting=0
|
|
debug()
|
|
{
|
|
echo "<7>$@"
|
|
}
|
|
info()
|
|
{
|
|
echo "<6>$@"
|
|
}
|
|
error()
|
|
{
|
|
echo "<3>$@"
|
|
}
|
|
die()
|
|
{
|
|
error "$@"
|
|
exit 1
|
|
}
|
|
waitNetworkOnline()
|
|
{
|
|
ret=1
|
|
for i in $(seq 6); do
|
|
curl_out=$("${pkgs.curl.bin}/bin/curl" \
|
|
--silent --fail --show-error --max-time 60 "${dbBaseUrl}" 2>&1)
|
|
if [ $? -eq 0 ]; then
|
|
debug "Server is reachable (try $i)"
|
|
ret=0
|
|
break
|
|
else
|
|
debug "Server is unreachable (try $i): $curl_out"
|
|
sleep 10
|
|
fi
|
|
done
|
|
return $ret
|
|
}
|
|
dbFnameTmp()
|
|
{
|
|
dburl=$1
|
|
echo "${cfg.databaseDir}/.$(basename "$dburl")"
|
|
}
|
|
dbFnameTmpDecompressed()
|
|
{
|
|
dburl=$1
|
|
echo "${cfg.databaseDir}/.$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//'
|
|
}
|
|
dbFname()
|
|
{
|
|
dburl=$1
|
|
echo "${cfg.databaseDir}/$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//'
|
|
}
|
|
downloadDb()
|
|
{
|
|
dburl=$1
|
|
curl_out=$("${pkgs.curl.bin}/bin/curl" \
|
|
--silent --fail --show-error --max-time 900 -L -o "$(dbFnameTmp "$dburl")" "$dburl" 2>&1)
|
|
if [ $? -ne 0 ]; then
|
|
error "Failed to download $dburl: $curl_out"
|
|
return 1
|
|
fi
|
|
}
|
|
decompressDb()
|
|
{
|
|
fn=$(dbFnameTmp "$1")
|
|
ret=0
|
|
case "$fn" in
|
|
*.gz)
|
|
cmd_out=$("${pkgs.gzip}/bin/gzip" --decompress --force "$fn" 2>&1)
|
|
;;
|
|
*.xz)
|
|
cmd_out=$("${pkgs.xz.bin}/bin/xz" --decompress --force "$fn" 2>&1)
|
|
;;
|
|
*)
|
|
cmd_out=$(echo "File \"$fn\" is neither a .gz nor .xz file")
|
|
false
|
|
;;
|
|
esac
|
|
if [ $? -ne 0 ]; then
|
|
error "$cmd_out"
|
|
ret=1
|
|
fi
|
|
}
|
|
atomicRename()
|
|
{
|
|
dburl=$1
|
|
mv "$(dbFnameTmpDecompressed "$dburl")" "$(dbFname "$dburl")"
|
|
}
|
|
removeIfNotInConfig()
|
|
{
|
|
# Arg 1 is the full path of an installed DB.
|
|
# If the corresponding database is not specified in the NixOS config we
|
|
# remove it.
|
|
db=$1
|
|
for cdb in ${lib.concatStringsSep " " cfg.databases}; do
|
|
confDb=$(echo "$cdb" | sed 's/\.\(gz\|xz\)$//')
|
|
if [ "$(basename "$db")" = "$(basename "$confDb")" ]; then
|
|
return 0
|
|
fi
|
|
done
|
|
rm "$db"
|
|
if [ $? -eq 0 ]; then
|
|
debug "Removed $(basename "$db") (not listed in services.geoip-updater.databases)"
|
|
else
|
|
error "Failed to remove $db"
|
|
fi
|
|
}
|
|
removeUnspecifiedDbs()
|
|
{
|
|
for f in "${cfg.databaseDir}/"*; do
|
|
test -f "$f" || continue
|
|
case "$f" in
|
|
*.dat|*.mmdb|*.csv)
|
|
removeIfNotInConfig "$f"
|
|
;;
|
|
*)
|
|
debug "Not removing \"$f\" (unknown file extension)"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
downloadAndInstall()
|
|
{
|
|
dburl=$1
|
|
if [ "$skipExisting" -eq 1 -a -f "$(dbFname "$dburl")" ]; then
|
|
debug "Skipping existing file: $(dbFname "$dburl")"
|
|
return 0
|
|
fi
|
|
downloadDb "$dburl" || return 1
|
|
decompressDb "$dburl" || return 1
|
|
atomicRename "$dburl" || return 1
|
|
info "Updated $(basename "$(dbFname "$dburl")")"
|
|
}
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--skip-existing)
|
|
skipExisting=1
|
|
info "Option --skip-existing is set: not updating existing databases"
|
|
;;
|
|
*)
|
|
error "Unknown argument: $arg";;
|
|
esac
|
|
done
|
|
waitNetworkOnline || die "Network is down (${dbBaseUrl} is unreachable)"
|
|
test -d "${cfg.databaseDir}" || die "Database directory (${cfg.databaseDir}) doesn't exist"
|
|
debug "Starting update of GeoIP databases in ${cfg.databaseDir}"
|
|
all_ret=0
|
|
for db in ${lib.concatStringsSep " \\\n " cfg.databases}; do
|
|
downloadAndInstall "${dbBaseUrl}/$db" || all_ret=1
|
|
done
|
|
removeUnspecifiedDbs || all_ret=1
|
|
if [ $all_ret -eq 0 ]; then
|
|
info "Completed GeoIP database update in ${cfg.databaseDir}"
|
|
else
|
|
error "Completed GeoIP database update in ${cfg.databaseDir}, with error(s)"
|
|
fi
|
|
# Hack to work around systemd journal race:
|
|
# https://github.com/systemd/systemd/issues/2913
|
|
sleep 2
|
|
exit $all_ret
|
|
'';
|
|
|
|
in
|
|
|
|
{
|
|
options = {
|
|
services.geoip-updater = {
|
|
enable = mkOption {
|
|
default = false;
|
|
type = types.bool;
|
|
description = ''
|
|
Whether to enable periodic downloading of GeoIP databases from
|
|
maxmind.com. You might want to enable this if you, for instance, use
|
|
ntopng or Wireshark.
|
|
'';
|
|
};
|
|
|
|
interval = mkOption {
|
|
type = types.str;
|
|
default = "weekly";
|
|
description = ''
|
|
Update the GeoIP databases at this time / interval.
|
|
The format is described in
|
|
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
|
<manvolnum>7</manvolnum></citerefentry>.
|
|
To prevent load spikes on maxmind.com, the timer interval is
|
|
randomized by an additional delay of ${randomizedTimerDelaySec}
|
|
seconds. Setting a shorter interval than this is not recommended.
|
|
'';
|
|
};
|
|
|
|
databaseDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/geoip-databases";
|
|
description = ''
|
|
Directory that will contain GeoIP databases.
|
|
'';
|
|
};
|
|
|
|
databases = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [
|
|
"GeoLiteCountry/GeoIP.dat.gz"
|
|
"GeoIPv6.dat.gz"
|
|
"GeoLiteCity.dat.xz"
|
|
"GeoLiteCityv6-beta/GeoLiteCityv6.dat.gz"
|
|
"asnum/GeoIPASNum.dat.gz"
|
|
"asnum/GeoIPASNumv6.dat.gz"
|
|
"GeoLite2-Country.mmdb.gz"
|
|
"GeoLite2-City.mmdb.gz"
|
|
];
|
|
description = ''
|
|
Which GeoIP databases to update. The full URL is ${dbBaseUrl}/ +
|
|
<literal>the_database</literal>.
|
|
'';
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
|
|
assertions = [
|
|
{ assertion = (builtins.filter
|
|
(x: builtins.match ".*\.(gz|xz)$" x == null) cfg.databases) == [];
|
|
message = ''
|
|
services.geoip-updater.databases supports only .gz and .xz databases.
|
|
|
|
Current value:
|
|
${toString cfg.databases}
|
|
|
|
Offending element(s):
|
|
${toString (builtins.filter (x: builtins.match ".*\.(gz|xz)$" x == null) cfg.databases)};
|
|
'';
|
|
}
|
|
];
|
|
|
|
users.extraUsers.geoip = {
|
|
group = "root";
|
|
description = "GeoIP database updater";
|
|
uid = config.ids.uids.geoip;
|
|
};
|
|
|
|
systemd.timers.geoip-updater =
|
|
{ description = "GeoIP Updater Timer";
|
|
partOf = [ "geoip-updater.service" ];
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig.OnCalendar = cfg.interval;
|
|
timerConfig.Persistent = "true";
|
|
timerConfig.RandomizedDelaySec = randomizedTimerDelaySec;
|
|
};
|
|
|
|
systemd.services.geoip-updater = {
|
|
description = "GeoIP Updater";
|
|
after = [ "network-online.target" "nss-lookup.target" ];
|
|
wants = [ "network-online.target" ];
|
|
preStart = ''
|
|
mkdir -p "${cfg.databaseDir}"
|
|
chmod 755 "${cfg.databaseDir}"
|
|
chown geoip:root "${cfg.databaseDir}"
|
|
'';
|
|
serviceConfig = {
|
|
ExecStart = "${geoip-updater}/bin/geoip-updater";
|
|
User = "geoip";
|
|
PermissionsStartOnly = true;
|
|
};
|
|
};
|
|
|
|
systemd.services.geoip-updater-setup = {
|
|
description = "GeoIP Updater Setup";
|
|
after = [ "network-online.target" "nss-lookup.target" ];
|
|
wants = [ "network-online.target" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
conflicts = [ "geoip-updater.service" ];
|
|
preStart = ''
|
|
mkdir -p "${cfg.databaseDir}"
|
|
chmod 755 "${cfg.databaseDir}"
|
|
chown geoip:root "${cfg.databaseDir}"
|
|
'';
|
|
serviceConfig = {
|
|
ExecStart = "${geoip-updater}/bin/geoip-updater --skip-existing";
|
|
User = "geoip";
|
|
PermissionsStartOnly = true;
|
|
# So it won't be (needlessly) restarted:
|
|
RemainAfterExit = true;
|
|
};
|
|
};
|
|
|
|
};
|
|
}
|