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.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;
 | |
|       };
 | |
|     };
 | |
| 
 | |
|   };
 | |
| }
 | 
