{ 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.runtimeShell} 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 systemd.time 7. 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}/ + the_database. ''; }; }; }; 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.users.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; }; }; }; }