1{ config, lib, pkgs, ... }:
2
3let
4 cfg = config.services.geoipupdate;
5in
6{
7 imports = [
8 (lib.mkRemovedOptionModule [ "services" "geoip-updater" ] "services.geoip-updater has been removed, use services.geoipupdate instead.")
9 ];
10
11 options = {
12 services.geoipupdate = {
13 enable = lib.mkEnableOption ''
14 periodic downloading of GeoIP databases using
15 <productname>geoipupdate</productname>.
16 '';
17
18 interval = lib.mkOption {
19 type = lib.types.str;
20 default = "weekly";
21 description = ''
22 Update the GeoIP databases at this time / interval.
23 The format is described in
24 <citerefentry><refentrytitle>systemd.time</refentrytitle>
25 <manvolnum>7</manvolnum></citerefentry>.
26 '';
27 };
28
29 settings = lib.mkOption {
30 description = ''
31 <productname>geoipupdate</productname> configuration
32 options. See
33 <link xlink:href="https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md" />
34 for a full list of available options.
35 '';
36 type = lib.types.submodule {
37 freeformType =
38 with lib.types;
39 let
40 type = oneOf [str int bool];
41 in
42 attrsOf (either type (listOf type));
43
44 options = {
45
46 AccountID = lib.mkOption {
47 type = lib.types.int;
48 description = ''
49 Your MaxMind account ID.
50 '';
51 };
52
53 EditionIDs = lib.mkOption {
54 type = with lib.types; listOf (either str int);
55 example = [
56 "GeoLite2-ASN"
57 "GeoLite2-City"
58 "GeoLite2-Country"
59 ];
60 description = ''
61 List of database edition IDs. This includes new string
62 IDs like <literal>GeoIP2-City</literal> and old
63 numeric IDs like <literal>106</literal>.
64 '';
65 };
66
67 LicenseKey = lib.mkOption {
68 type = lib.types.path;
69 description = ''
70 A file containing the <productname>MaxMind</productname>
71 license key.
72 '';
73 };
74
75 DatabaseDirectory = lib.mkOption {
76 type = lib.types.path;
77 default = "/var/lib/GeoIP";
78 example = "/run/GeoIP";
79 description = ''
80 The directory to store the database files in. The
81 directory will be automatically created, the owner
82 changed to <literal>geoip</literal> and permissions
83 set to world readable. This applies if the directory
84 already exists as well, so don't use a directory with
85 sensitive contents.
86 '';
87 };
88
89 };
90 };
91 };
92 };
93
94 };
95
96 config = lib.mkIf cfg.enable {
97
98 services.geoipupdate.settings = {
99 LockFile = "/run/geoipupdate/.lock";
100 };
101
102 systemd.services.geoipupdate-create-db-dir = {
103 serviceConfig.Type = "oneshot";
104 script = ''
105 mkdir -p ${cfg.settings.DatabaseDirectory}
106 chmod 0755 ${cfg.settings.DatabaseDirectory}
107 '';
108 };
109
110 systemd.services.geoipupdate = {
111 description = "GeoIP Updater";
112 requires = [ "geoipupdate-create-db-dir.service" ];
113 after = [
114 "geoipupdate-create-db-dir.service"
115 "network-online.target"
116 "nss-lookup.target"
117 ];
118 wants = [ "network-online.target" ];
119 startAt = cfg.interval;
120 serviceConfig = {
121 ExecStartPre =
122 let
123 geoipupdateKeyValue = lib.generators.toKeyValue {
124 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
125 mkValueString = v: with builtins;
126 if isInt v then toString v
127 else if isString v then v
128 else if true == v then "1"
129 else if false == v then "0"
130 else if isList v then lib.concatMapStringsSep " " mkValueString v
131 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
132 };
133 };
134
135 geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
136
137 script = ''
138 chown geoip "${cfg.settings.DatabaseDirectory}"
139
140 cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
141 ${pkgs.replace-secret}/bin/replace-secret '${cfg.settings.LicenseKey}' \
142 '${cfg.settings.LicenseKey}' \
143 /run/geoipupdate/GeoIP.conf
144 '';
145 in
146 "+${pkgs.writeShellScript "start-pre-full-privileges" script}";
147 ExecStart = "${pkgs.geoipupdate}/bin/geoipupdate -f /run/geoipupdate/GeoIP.conf";
148 User = "geoip";
149 DynamicUser = true;
150 ReadWritePaths = cfg.settings.DatabaseDirectory;
151 RuntimeDirectory = "geoipupdate";
152 RuntimeDirectoryMode = 0700;
153 CapabilityBoundingSet = "";
154 PrivateDevices = true;
155 PrivateMounts = true;
156 PrivateUsers = true;
157 ProtectClock = true;
158 ProtectControlGroups = true;
159 ProtectHome = true;
160 ProtectHostname = true;
161 ProtectKernelLogs = true;
162 ProtectKernelModules = true;
163 ProtectKernelTunables = true;
164 ProtectProc = "invisible";
165 ProcSubset = "pid";
166 SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
167 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
168 RestrictRealtime = true;
169 RestrictNamespaces = true;
170 MemoryDenyWriteExecute = true;
171 LockPersonality = true;
172 SystemCallArchitectures = "native";
173 };
174 };
175
176 systemd.timers.geoipupdate-initial-run = {
177 wantedBy = [ "timers.target" ];
178 unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}";
179 timerConfig = {
180 Unit = "geoipupdate.service";
181 OnActiveSec = 0;
182 };
183 };
184 };
185
186 meta.maintainers = [ lib.maintainers.talyz ];
187}