at 25.11-pre 13 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 inherit (lib.trivial) isFloat isInt isBool; 10 inherit (lib.modules) mkIf; 11 inherit (lib.options) 12 literalExpression 13 mkOption 14 mkPackageOption 15 mkEnableOption 16 ; 17 inherit (lib.strings) 18 isString 19 escapeShellArg 20 escapeShellArgs 21 concatMapStringsSep 22 concatMapAttrsStringSep 23 replaceStrings 24 substring 25 stringLength 26 hasInfix 27 hasSuffix 28 typeOf 29 match 30 ; 31 inherit (lib.lists) all isList flatten; 32 inherit (lib.attrsets) 33 attrsToList 34 filterAttrs 35 optionalAttrs 36 mapAttrs' 37 mapAttrsToList 38 nameValuePair 39 ; 40 inherit (lib.generators) toKeyValue; 41 inherit (lib) types; 42 43 # Deeply checks types for a given type function. Calls `override` with type and value. 44 deep = 45 func: override: type: 46 let 47 prev = func type; 48 in 49 prev 50 // { 51 check = value: prev.check value && (override type value); 52 }; 53 54 # Deep listOf. 55 listOf' = deep types.listOf (type: value: all type.check value); 56 57 # Deep attrsOf. 58 attrsOf' = deep types.attrsOf (type: value: all (item: type.check item.value) (attrsToList value)); 59 60 # Kismet config atoms. 61 atom = 62 with types; 63 oneOf [ 64 number 65 bool 66 str 67 ]; 68 69 # Composite types. 70 listOfAtom = listOf' atom; 71 atomOrList = with types; either atom listOfAtom; 72 lists = listOf' atomOrList; 73 kvPair = attrsOf' atomOrList; 74 kvPairs = listOf' kvPair; 75 76 # Options that eval to a string with a header (foo:key=value) 77 headerKvPair = attrsOf' (attrsOf' atomOrList); 78 headerKvPairs = attrsOf' (listOf' (attrsOf' atomOrList)); 79 80 # Toplevel config type. 81 topLevel = 82 let 83 topLevel' = 84 with types; 85 oneOf [ 86 headerKvPairs 87 headerKvPair 88 kvPairs 89 kvPair 90 listOfAtom 91 lists 92 atom 93 ]; 94 in 95 topLevel' 96 // { 97 description = "Kismet config stanza"; 98 }; 99 100 # Throws invalid. 101 invalid = atom: throw "invalid value '${toString atom}' of type '${typeOf atom}'"; 102 103 # Converts an atom. 104 mkAtom = 105 atom: 106 if isString atom then 107 if hasInfix "\"" atom || hasInfix "," atom then 108 ''"${replaceStrings [ ''"'' ] [ ''\"'' ] atom}"'' 109 else 110 atom 111 else if isFloat atom || isInt atom || isBool atom then 112 toString atom 113 else 114 invalid atom; 115 116 # Converts an inline atom or list to a string. 117 mkAtomOrListInline = 118 atomOrList: 119 if isList atomOrList then 120 mkAtom "${concatMapStringsSep "," mkAtom atomOrList}" 121 else 122 mkAtom atomOrList; 123 124 # Converts an out of line atom or list to a string. 125 mkAtomOrList = 126 atomOrList: 127 if isList atomOrList then 128 "${concatMapStringsSep "," mkAtomOrListInline atomOrList}" 129 else 130 mkAtom atomOrList; 131 132 # Throws if the string matches the given regex. 133 deny = 134 regex: str: 135 assert (match regex str) == null; 136 str; 137 138 # Converts a set of k/v pairs. 139 convertKv = concatMapAttrsStringSep "," ( 140 name: value: "${mkAtom (deny "=" name)}=${mkAtomOrListInline value}" 141 ); 142 143 # Converts k/v pairs with a header. 144 convertKvWithHeader = header: attrs: "${mkAtom (deny ":" header)}:${convertKv attrs}"; 145 146 # Converts the entire config. 147 convertConfig = mapAttrs' ( 148 name: value: 149 let 150 # Convert foo' into 'foo+' for support for '+=' syntax. 151 newName = if hasSuffix "'" name then substring 0 (stringLength name - 1) name + "+" else name; 152 153 # Get the stringified value. 154 newValue = 155 if headerKvPairs.check value then 156 flatten ( 157 mapAttrsToList (header: values: (map (value: convertKvWithHeader header value) values)) value 158 ) 159 else if headerKvPair.check value then 160 mapAttrsToList convertKvWithHeader value 161 else if kvPairs.check value then 162 map convertKv value 163 else if kvPair.check value then 164 convertKv value 165 else if listOfAtom.check value then 166 mkAtomOrList value 167 else if lists.check value then 168 map mkAtomOrList value 169 else if atom.check value then 170 mkAtom value 171 else 172 invalid value; 173 in 174 nameValuePair newName newValue 175 ); 176 177 mkKismetConf = 178 options: 179 (toKeyValue { listsAsDuplicateKeys = true; }) ( 180 filterAttrs (_: value: value != null) (convertConfig options) 181 ); 182 183 cfg = config.services.kismet; 184in 185{ 186 options.services.kismet = { 187 enable = mkEnableOption "kismet"; 188 package = mkPackageOption pkgs "kismet" { }; 189 user = mkOption { 190 description = "The user to run Kismet as."; 191 type = types.str; 192 default = "kismet"; 193 }; 194 group = mkOption { 195 description = "The group to run Kismet as."; 196 type = types.str; 197 default = "kismet"; 198 }; 199 serverName = mkOption { 200 description = "The name of the server."; 201 type = types.str; 202 default = "Kismet"; 203 }; 204 serverDescription = mkOption { 205 description = "The description of the server."; 206 type = types.str; 207 default = "NixOS Kismet server"; 208 }; 209 logTypes = mkOption { 210 description = "The log types."; 211 type = with types; listOf str; 212 default = [ "kismet" ]; 213 }; 214 dataDir = mkOption { 215 description = "The Kismet data directory."; 216 type = types.path; 217 default = "/var/lib/kismet"; 218 }; 219 httpd = { 220 enable = mkOption { 221 description = "True to enable the HTTP server."; 222 type = types.bool; 223 default = false; 224 }; 225 address = mkOption { 226 description = "The address to listen on. Note that this cannot be a hostname or Kismet will not start."; 227 type = types.str; 228 default = "127.0.0.1"; 229 }; 230 port = mkOption { 231 description = "The port to listen on."; 232 type = types.port; 233 default = 2501; 234 }; 235 }; 236 settings = mkOption { 237 description = '' 238 Options for Kismet. See: 239 https://www.kismetwireless.net/docs/readme/configuring/configfiles/ 240 ''; 241 default = { }; 242 type = with types; attrsOf topLevel; 243 example = literalExpression '' 244 { 245 /* Examples for atoms */ 246 # dot11_link_bssts=false 247 dot11_link_bssts = false; # Boolean 248 249 # dot11_related_bss_window=10000000 250 dot11_related_bss_window = 10000000; # Integer 251 252 # devicefound=00:11:22:33:44:55 253 devicefound = "00:11:22:33:44:55"; # String 254 255 # log_types+=wiglecsv 256 log_types' = "wiglecsv"; 257 258 /* Examples for lists of atoms */ 259 # wepkey=00:DE:AD:C0:DE:00,FEEDFACE42 260 wepkey = [ "00:DE:AD:C0:DE:00" "FEEDFACE42" ]; 261 262 # alert=ADHOCCONFLICT,5/min,1/sec 263 # alert=ADVCRYPTCHANGE,5/min,1/sec 264 alert = [ 265 [ "ADHOCCONFLICT" "5/min" "1/sec" ] 266 [ "ADVCRYPTCHANGE" "5/min" "1/sec" ] 267 ]; 268 269 /* Examples for sets of atoms */ 270 # source=wlan0:name=ath11k 271 source.wlan0 = { name = "ath11k"; }; 272 273 /* Examples with colon-suffixed headers */ 274 # gps=gpsd:host=localhost,port=2947 275 gps.gpsd = { 276 host = "localhost"; 277 port = 2947; 278 }; 279 280 # apspoof=Foo1:ssid=Bar1,validmacs="00:11:22:33:44:55,aa:bb:cc:dd:ee:ff" 281 # apspoof=Foo1:ssid=Bar2,validmacs="01:12:23:34:45:56,ab:bc:cd:de:ef:f0" 282 # apspoof=Foo2:ssid=Baz1,validmacs="11:22:33:44:55:66,bb:cc:dd:ee:ff:00" 283 apspoof.Foo1 = [ 284 { ssid = "Bar1"; validmacs = [ "00:11:22:33:44:55" "aa:bb:cc:dd:ee:ff" ]; } 285 { ssid = "Bar2"; validmacs = [ "01:12:23:34:45:56" "ab:bc:cd:de:ef:f0" ]; } 286 ]; 287 288 # because Foo1 is a list, Foo2 needs to be as well 289 apspoof.Foo2 = [ 290 { 291 ssid = "Bar2"; 292 validmacs = [ "00:11:22:33:44:55" "aa:bb:cc:dd:ee:ff" ]; 293 }; 294 ]; 295 } 296 ''; 297 }; 298 extraConfig = mkOption { 299 description = '' 300 Literal Kismet config lines appended to the site config. 301 Note that `services.kismet.settings` allows you to define 302 all options here using Nix attribute sets. 303 ''; 304 default = ""; 305 type = types.str; 306 example = '' 307 # Looks like the following in `services.kismet.settings`: 308 # wepkey = [ "00:DE:AD:C0:DE:00" "FEEDFACE42" ]; 309 wepkey=00:DE:AD:C0:DE:00,FEEDFACE42 310 ''; 311 }; 312 }; 313 314 config = 315 let 316 configDir = "${cfg.dataDir}/.kismet"; 317 settings = 318 cfg.settings 319 // { 320 server_name = cfg.serverName; 321 server_description = cfg.serverDescription; 322 logging_enabled = cfg.logTypes != [ ]; 323 log_types = cfg.logTypes; 324 } 325 // optionalAttrs cfg.httpd.enable { 326 httpd_bind_address = cfg.httpd.address; 327 httpd_port = cfg.httpd.port; 328 httpd_auth_file = "${configDir}/kismet_httpd.conf"; 329 httpd_home = "${cfg.package}/share/kismet/httpd"; 330 }; 331 in 332 mkIf cfg.enable { 333 systemd.tmpfiles.settings = { 334 "10-kismet" = { 335 ${cfg.dataDir} = { 336 d = { 337 inherit (cfg) user group; 338 mode = "0750"; 339 }; 340 }; 341 ${configDir} = { 342 d = { 343 inherit (cfg) user group; 344 mode = "0750"; 345 }; 346 }; 347 }; 348 }; 349 systemd.services.kismet = 350 let 351 kismetConf = pkgs.writeText "kismet.conf" '' 352 ${mkKismetConf settings} 353 ${cfg.extraConfig} 354 ''; 355 in 356 { 357 description = "Kismet monitoring service"; 358 wants = [ "basic.target" ]; 359 after = [ 360 "basic.target" 361 "network.target" 362 ]; 363 wantedBy = [ "multi-user.target" ]; 364 serviceConfig = 365 let 366 capabilities = [ 367 "CAP_NET_ADMIN" 368 "CAP_NET_RAW" 369 ]; 370 kismetPreStart = pkgs.writeShellScript "kismet-pre-start" '' 371 owner=${escapeShellArg "${cfg.user}:${cfg.group}"} 372 mkdir -p ~/.kismet 373 374 # Ensure permissions on directories Kismet uses. 375 chown "$owner" ~/ ~/.kismet 376 cd ~/.kismet 377 378 package=${cfg.package} 379 if [ -d "$package/etc" ]; then 380 for file in "$package/etc"/*.conf; do 381 # Symlink the config files if they exist or are already a link. 382 base="''${file##*/}" 383 if [ ! -f "$base" ] || [ -L "$base" ]; then 384 ln -sf "$file" "$base" 385 fi 386 done 387 fi 388 389 for file in kismet_httpd.conf; do 390 # Un-symlink these files. 391 if [ -L "$file" ]; then 392 cp "$file" ".$file" 393 rm -f "$file" 394 mv ".$file" "$file" 395 chmod 0640 "$file" 396 chown "$owner" "$file" 397 fi 398 done 399 400 # Link the site config. 401 ln -sf ${kismetConf} kismet_site.conf 402 ''; 403 in 404 { 405 Type = "simple"; 406 ExecStart = escapeShellArgs [ 407 "${cfg.package}/bin/kismet" 408 "--homedir" 409 cfg.dataDir 410 "--confdir" 411 configDir 412 "--datadir" 413 "${cfg.package}/share" 414 "--no-ncurses" 415 "-f" 416 "${configDir}/kismet.conf" 417 ]; 418 WorkingDirectory = cfg.dataDir; 419 ExecStartPre = "+${kismetPreStart}"; 420 Restart = "always"; 421 KillMode = "control-group"; 422 CapabilityBoundingSet = capabilities; 423 AmbientCapabilities = capabilities; 424 LockPersonality = true; 425 NoNewPrivileges = true; 426 PrivateDevices = false; 427 PrivateTmp = true; 428 PrivateUsers = false; 429 ProtectClock = true; 430 ProtectControlGroups = true; 431 ProtectHome = true; 432 ProtectHostname = true; 433 ProtectKernelLogs = true; 434 ProtectKernelModules = true; 435 ProtectKernelTunables = true; 436 ProtectProc = "invisible"; 437 ProtectSystem = "full"; 438 RestrictNamespaces = true; 439 RestrictSUIDSGID = true; 440 User = cfg.user; 441 Group = cfg.group; 442 UMask = "0007"; 443 TimeoutStopSec = 30; 444 }; 445 446 # Allow it to restart if the wifi interface is not up 447 unitConfig.StartLimitIntervalSec = 5; 448 }; 449 users.groups.${cfg.group} = { }; 450 users.users.${cfg.user} = { 451 inherit (cfg) group; 452 description = "User for running Kismet"; 453 isSystemUser = true; 454 home = cfg.dataDir; 455 }; 456 }; 457 458 meta.maintainers = with lib.maintainers; [ numinit ]; 459}