at 18.03-beta 9.0 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.geoip-updater; 7 8 dbBaseUrl = "https://geolite.maxmind.com/download/geoip/database"; 9 10 randomizedTimerDelaySec = "3600"; 11 12 # Use writeScriptBin instead of writeScript, so that argv[0] (logged to the 13 # journal) doesn't include the long nix store path hash. (Prefixing the 14 # ExecStart= command with '@' doesn't work because we start a shell (new 15 # process) that creates a new argv[0].) 16 geoip-updater = pkgs.writeScriptBin "geoip-updater" '' 17 #!${pkgs.stdenv.shell} 18 skipExisting=0 19 debug() 20 { 21 echo "<7>$@" 22 } 23 info() 24 { 25 echo "<6>$@" 26 } 27 error() 28 { 29 echo "<3>$@" 30 } 31 die() 32 { 33 error "$@" 34 exit 1 35 } 36 waitNetworkOnline() 37 { 38 ret=1 39 for i in $(seq 6); do 40 curl_out=$("${pkgs.curl.bin}/bin/curl" \ 41 --silent --fail --show-error --max-time 60 "${dbBaseUrl}" 2>&1) 42 if [ $? -eq 0 ]; then 43 debug "Server is reachable (try $i)" 44 ret=0 45 break 46 else 47 debug "Server is unreachable (try $i): $curl_out" 48 sleep 10 49 fi 50 done 51 return $ret 52 } 53 dbFnameTmp() 54 { 55 dburl=$1 56 echo "${cfg.databaseDir}/.$(basename "$dburl")" 57 } 58 dbFnameTmpDecompressed() 59 { 60 dburl=$1 61 echo "${cfg.databaseDir}/.$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//' 62 } 63 dbFname() 64 { 65 dburl=$1 66 echo "${cfg.databaseDir}/$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//' 67 } 68 downloadDb() 69 { 70 dburl=$1 71 curl_out=$("${pkgs.curl.bin}/bin/curl" \ 72 --silent --fail --show-error --max-time 900 -L -o "$(dbFnameTmp "$dburl")" "$dburl" 2>&1) 73 if [ $? -ne 0 ]; then 74 error "Failed to download $dburl: $curl_out" 75 return 1 76 fi 77 } 78 decompressDb() 79 { 80 fn=$(dbFnameTmp "$1") 81 ret=0 82 case "$fn" in 83 *.gz) 84 cmd_out=$("${pkgs.gzip}/bin/gzip" --decompress --force "$fn" 2>&1) 85 ;; 86 *.xz) 87 cmd_out=$("${pkgs.xz.bin}/bin/xz" --decompress --force "$fn" 2>&1) 88 ;; 89 *) 90 cmd_out=$(echo "File \"$fn\" is neither a .gz nor .xz file") 91 false 92 ;; 93 esac 94 if [ $? -ne 0 ]; then 95 error "$cmd_out" 96 ret=1 97 fi 98 } 99 atomicRename() 100 { 101 dburl=$1 102 mv "$(dbFnameTmpDecompressed "$dburl")" "$(dbFname "$dburl")" 103 } 104 removeIfNotInConfig() 105 { 106 # Arg 1 is the full path of an installed DB. 107 # If the corresponding database is not specified in the NixOS config we 108 # remove it. 109 db=$1 110 for cdb in ${lib.concatStringsSep " " cfg.databases}; do 111 confDb=$(echo "$cdb" | sed 's/\.\(gz\|xz\)$//') 112 if [ "$(basename "$db")" = "$(basename "$confDb")" ]; then 113 return 0 114 fi 115 done 116 rm "$db" 117 if [ $? -eq 0 ]; then 118 debug "Removed $(basename "$db") (not listed in services.geoip-updater.databases)" 119 else 120 error "Failed to remove $db" 121 fi 122 } 123 removeUnspecifiedDbs() 124 { 125 for f in "${cfg.databaseDir}/"*; do 126 test -f "$f" || continue 127 case "$f" in 128 *.dat|*.mmdb|*.csv) 129 removeIfNotInConfig "$f" 130 ;; 131 *) 132 debug "Not removing \"$f\" (unknown file extension)" 133 ;; 134 esac 135 done 136 } 137 downloadAndInstall() 138 { 139 dburl=$1 140 if [ "$skipExisting" -eq 1 -a -f "$(dbFname "$dburl")" ]; then 141 debug "Skipping existing file: $(dbFname "$dburl")" 142 return 0 143 fi 144 downloadDb "$dburl" || return 1 145 decompressDb "$dburl" || return 1 146 atomicRename "$dburl" || return 1 147 info "Updated $(basename "$(dbFname "$dburl")")" 148 } 149 for arg in "$@"; do 150 case "$arg" in 151 --skip-existing) 152 skipExisting=1 153 info "Option --skip-existing is set: not updating existing databases" 154 ;; 155 *) 156 error "Unknown argument: $arg";; 157 esac 158 done 159 waitNetworkOnline || die "Network is down (${dbBaseUrl} is unreachable)" 160 test -d "${cfg.databaseDir}" || die "Database directory (${cfg.databaseDir}) doesn't exist" 161 debug "Starting update of GeoIP databases in ${cfg.databaseDir}" 162 all_ret=0 163 for db in ${lib.concatStringsSep " \\\n " cfg.databases}; do 164 downloadAndInstall "${dbBaseUrl}/$db" || all_ret=1 165 done 166 removeUnspecifiedDbs || all_ret=1 167 if [ $all_ret -eq 0 ]; then 168 info "Completed GeoIP database update in ${cfg.databaseDir}" 169 else 170 error "Completed GeoIP database update in ${cfg.databaseDir}, with error(s)" 171 fi 172 # Hack to work around systemd journal race: 173 # https://github.com/systemd/systemd/issues/2913 174 sleep 2 175 exit $all_ret 176 ''; 177 178in 179 180{ 181 options = { 182 services.geoip-updater = { 183 enable = mkOption { 184 default = false; 185 type = types.bool; 186 description = '' 187 Whether to enable periodic downloading of GeoIP databases from 188 maxmind.com. You might want to enable this if you, for instance, use 189 ntopng or Wireshark. 190 ''; 191 }; 192 193 interval = mkOption { 194 type = types.str; 195 default = "weekly"; 196 description = '' 197 Update the GeoIP databases at this time / interval. 198 The format is described in 199 <citerefentry><refentrytitle>systemd.time</refentrytitle> 200 <manvolnum>7</manvolnum></citerefentry>. 201 To prevent load spikes on maxmind.com, the timer interval is 202 randomized by an additional delay of ${randomizedTimerDelaySec} 203 seconds. Setting a shorter interval than this is not recommended. 204 ''; 205 }; 206 207 databaseDir = mkOption { 208 type = types.path; 209 default = "/var/lib/geoip-databases"; 210 description = '' 211 Directory that will contain GeoIP databases. 212 ''; 213 }; 214 215 databases = mkOption { 216 type = types.listOf types.str; 217 default = [ 218 "GeoLiteCountry/GeoIP.dat.gz" 219 "GeoIPv6.dat.gz" 220 "GeoLiteCity.dat.xz" 221 "GeoLiteCityv6-beta/GeoLiteCityv6.dat.gz" 222 "asnum/GeoIPASNum.dat.gz" 223 "asnum/GeoIPASNumv6.dat.gz" 224 "GeoLite2-Country.mmdb.gz" 225 "GeoLite2-City.mmdb.gz" 226 ]; 227 description = '' 228 Which GeoIP databases to update. The full URL is ${dbBaseUrl}/ + 229 <literal>the_database</literal>. 230 ''; 231 }; 232 233 }; 234 235 }; 236 237 config = mkIf cfg.enable { 238 239 assertions = [ 240 { assertion = (builtins.filter 241 (x: builtins.match ".*\\.(gz|xz)$" x == null) cfg.databases) == []; 242 message = '' 243 services.geoip-updater.databases supports only .gz and .xz databases. 244 245 Current value: 246 ${toString cfg.databases} 247 248 Offending element(s): 249 ${toString (builtins.filter (x: builtins.match ".*\\.(gz|xz)$" x == null) cfg.databases)}; 250 ''; 251 } 252 ]; 253 254 users.extraUsers.geoip = { 255 group = "root"; 256 description = "GeoIP database updater"; 257 uid = config.ids.uids.geoip; 258 }; 259 260 systemd.timers.geoip-updater = 261 { description = "GeoIP Updater Timer"; 262 partOf = [ "geoip-updater.service" ]; 263 wantedBy = [ "timers.target" ]; 264 timerConfig.OnCalendar = cfg.interval; 265 timerConfig.Persistent = "true"; 266 timerConfig.RandomizedDelaySec = randomizedTimerDelaySec; 267 }; 268 269 systemd.services.geoip-updater = { 270 description = "GeoIP Updater"; 271 after = [ "network-online.target" "nss-lookup.target" ]; 272 wants = [ "network-online.target" ]; 273 preStart = '' 274 mkdir -p "${cfg.databaseDir}" 275 chmod 755 "${cfg.databaseDir}" 276 chown geoip:root "${cfg.databaseDir}" 277 ''; 278 serviceConfig = { 279 ExecStart = "${geoip-updater}/bin/geoip-updater"; 280 User = "geoip"; 281 PermissionsStartOnly = true; 282 }; 283 }; 284 285 systemd.services.geoip-updater-setup = { 286 description = "GeoIP Updater Setup"; 287 after = [ "network-online.target" "nss-lookup.target" ]; 288 wants = [ "network-online.target" ]; 289 wantedBy = [ "multi-user.target" ]; 290 conflicts = [ "geoip-updater.service" ]; 291 preStart = '' 292 mkdir -p "${cfg.databaseDir}" 293 chmod 755 "${cfg.databaseDir}" 294 chown geoip:root "${cfg.databaseDir}" 295 ''; 296 serviceConfig = { 297 ExecStart = "${geoip-updater}/bin/geoip-updater --skip-existing"; 298 User = "geoip"; 299 PermissionsStartOnly = true; 300 # So it won't be (needlessly) restarted: 301 RemainAfterExit = true; 302 }; 303 }; 304 305 }; 306}