about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/misc/geoip-updater.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/misc/geoip-updater.nix')
-rw-r--r--nixpkgs/nixos/modules/services/misc/geoip-updater.nix306
1 files changed, 306 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/misc/geoip-updater.nix b/nixpkgs/nixos/modules/services/misc/geoip-updater.nix
new file mode 100644
index 000000000000..baf0a8d73d19
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/misc/geoip-updater.nix
@@ -0,0 +1,306 @@
+{ 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
+          <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.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;
+      };
+    };
+
+  };
+}