at 25.11-pre 9.2 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 10 inherit (lib) 11 concatLines 12 escapeShellArgs 13 mkIf 14 mkMerge 15 optional 16 ; 17 inherit (lib.cli) toGNUCommandLine; 18 19 cfg = config.services.hylafax; 20 mapModems = lib.forEach (lib.attrValues cfg.modems); 21 22 mkSpoolCmd = 23 prefix: program: posArg: options: 24 let 25 start = "${prefix}${cfg.package}/spool/bin/${program}"; 26 optionsList = toGNUCommandLine { mkOptionName = k: "-${k}"; } ( 27 { q = cfg.spoolAreaPath; } // options 28 ); 29 posArgList = optional (posArg != null) posArg; 30 in 31 "${start} ${escapeShellArgs (optionsList ++ posArgList)}"; 32 33 mkConfigFile = 34 name: conf: 35 # creates hylafax config file, 36 # makes sure "Include" is listed *first* 37 let 38 mkLines = lib.flip lib.pipe [ 39 (lib.mapAttrsToList (key: map (val: "${key}: ${val}"))) 40 lib.concatLists 41 ]; 42 include = mkLines { Include = conf.Include or [ ]; }; 43 other = mkLines (conf // { Include = [ ]; }); 44 in 45 pkgs.writeText "hylafax-config${name}" (concatLines (include ++ other)); 46 47 globalConfigPath = mkConfigFile "" cfg.faxqConfig; 48 49 modemConfigPath = 50 let 51 mkModemConfigFile = 52 { config, name, ... }: mkConfigFile ".${name}" (cfg.commonModemConfig // config); 53 mkLine = 54 { name, type, ... }@modem: 55 '' 56 # check if modem config file exists: 57 test -f "${cfg.package}/spool/config/${type}" 58 ln \ 59 --symbolic \ 60 --no-target-directory \ 61 "${mkModemConfigFile modem}" \ 62 "$out/config.${name}" 63 ''; 64 in 65 pkgs.runCommand "hylafax-config-modems" { 66 preferLocalBuild = true; 67 } ''mkdir --parents "$out/" ${concatLines (mapModems mkLine)}''; 68 69 setupSpoolScript = pkgs.replaceVarsWith { 70 name = "hylafax-setup-spool.sh"; 71 src = ./spool.sh; 72 isExecutable = true; 73 replacements = { 74 faxuser = "uucp"; 75 faxgroup = "uucp"; 76 lockPath = "/var/lock"; 77 inherit globalConfigPath modemConfigPath; 78 inherit (cfg) package spoolAreaPath userAccessFile; 79 inherit (pkgs) runtimeShell; 80 }; 81 }; 82 83 waitFaxqScript = pkgs.replaceVarsWith { 84 # This script checks the modems status files 85 # and waits until all modems report readiness. 86 name = "hylafax-faxq-wait-start.sh"; 87 src = ./faxq-wait.sh; 88 isExecutable = true; 89 replacements = { 90 timeoutSec = toString 10; 91 inherit (cfg) spoolAreaPath; 92 inherit (pkgs) runtimeShell; 93 }; 94 }; 95 96 sockets.hylafax-hfaxd = { 97 description = "HylaFAX server socket"; 98 documentation = [ "man:hfaxd(8)" ]; 99 wantedBy = [ "multi-user.target" ]; 100 listenStreams = [ "127.0.0.1:4559" ]; 101 socketConfig.FreeBind = true; 102 socketConfig.Accept = true; 103 }; 104 105 paths.hylafax-faxq = { 106 description = "HylaFAX queue manager sendq watch"; 107 documentation = [ 108 "man:faxq(8)" 109 "man:sendq(5)" 110 ]; 111 wantedBy = [ "multi-user.target" ]; 112 pathConfig.PathExistsGlob = [ "${cfg.spoolAreaPath}/sendq/q*" ]; 113 }; 114 115 timers = mkMerge [ 116 (mkIf (cfg.faxcron.enable.frequency != null) { hylafax-faxcron.timerConfig.Persistent = true; }) 117 (mkIf (cfg.faxqclean.enable.frequency != null) { hylafax-faxqclean.timerConfig.Persistent = true; }) 118 ]; 119 120 hardenService = 121 # Add some common systemd service hardening settings, 122 # but allow each service (here) to override 123 # settings by explicitly setting those to `null`. 124 # More hardening would be nice but makes 125 # customizing hylafax setups very difficult. 126 # If at all, it should only be added along 127 # with some options to customize it. 128 let 129 hardening = { 130 PrivateDevices = true; # breaks /dev/tty... 131 PrivateNetwork = true; 132 PrivateTmp = true; 133 #ProtectClock = true; # breaks /dev/tty... (why?) 134 ProtectControlGroups = true; 135 #ProtectHome = true; # breaks custom spool dirs 136 ProtectKernelLogs = true; 137 ProtectKernelModules = true; 138 ProtectKernelTunables = true; 139 #ProtectSystem = "strict"; # breaks custom spool dirs 140 RestrictNamespaces = true; 141 RestrictRealtime = true; 142 }; 143 filter = key: value: (value != null) || !(lib.hasAttr key hardening); 144 apply = service: lib.filterAttrs filter (hardening // (service.serviceConfig or { })); 145 in 146 service: service // { serviceConfig = apply service; }; 147 148 services.hylafax-spool = { 149 description = "HylaFAX spool area preparation"; 150 documentation = [ "man:hylafax-server(4)" ]; 151 script = '' 152 ${setupSpoolScript} 153 cd "${cfg.spoolAreaPath}" 154 ${cfg.spoolExtraInit} 155 if ! test -f "${cfg.spoolAreaPath}/etc/hosts.hfaxd" 156 then 157 echo hosts.hfaxd is missing 158 exit 1 159 fi 160 ''; 161 serviceConfig.ExecStop = "${setupSpoolScript}"; 162 serviceConfig.RemainAfterExit = true; 163 serviceConfig.Type = "oneshot"; 164 unitConfig.RequiresMountsFor = [ cfg.spoolAreaPath ]; 165 }; 166 167 services.hylafax-faxq = { 168 description = "HylaFAX queue manager"; 169 documentation = [ "man:faxq(8)" ]; 170 requires = [ "hylafax-spool.service" ]; 171 after = [ "hylafax-spool.service" ]; 172 wants = mapModems ({ name, ... }: "hylafax-faxgetty@${name}.service"); 173 wantedBy = mkIf cfg.autostart [ "multi-user.target" ]; 174 serviceConfig.Type = "forking"; 175 serviceConfig.ExecStart = mkSpoolCmd "" "faxq" null { }; 176 # This delays the "readiness" of this service until 177 # all modems are initialized (or a timeout is reached). 178 # Otherwise, sending a fax with the fax service 179 # stopped will always yield a failed send attempt: 180 # The fax service is started when the job is created with 181 # `sendfax`, but modems need some time to initialize. 182 serviceConfig.ExecStartPost = [ "${waitFaxqScript}" ]; 183 # faxquit fails if the pipe is already gone 184 # (e.g. the service is already stopping) 185 serviceConfig.ExecStop = mkSpoolCmd "-" "faxquit" null { }; 186 # disable some systemd hardening settings 187 serviceConfig.PrivateDevices = null; 188 serviceConfig.RestrictRealtime = null; 189 }; 190 191 services."hylafax-hfaxd@" = { 192 description = "HylaFAX server"; 193 documentation = [ "man:hfaxd(8)" ]; 194 after = [ "hylafax-faxq.service" ]; 195 requires = [ "hylafax-faxq.service" ]; 196 serviceConfig.StandardInput = "socket"; 197 serviceConfig.StandardOutput = "socket"; 198 serviceConfig.ExecStart = mkSpoolCmd "" "hfaxd" null { 199 d = true; 200 I = true; 201 }; 202 unitConfig.RequiresMountsFor = [ cfg.userAccessFile ]; 203 # disable some systemd hardening settings 204 serviceConfig.PrivateDevices = null; 205 serviceConfig.PrivateNetwork = null; 206 }; 207 208 services.hylafax-faxcron = rec { 209 description = "HylaFAX spool area maintenance"; 210 documentation = [ "man:faxcron(8)" ]; 211 after = [ "hylafax-spool.service" ]; 212 requires = [ "hylafax-spool.service" ]; 213 wantedBy = mkIf cfg.faxcron.enable.spoolInit requires; 214 startAt = mkIf (cfg.faxcron.enable.frequency != null) cfg.faxcron.enable.frequency; 215 serviceConfig.ExecStart = mkSpoolCmd "" "faxcron" null { 216 info = cfg.faxcron.infoDays; 217 log = cfg.faxcron.logDays; 218 rcv = cfg.faxcron.rcvDays; 219 }; 220 }; 221 222 services.hylafax-faxqclean = rec { 223 description = "HylaFAX spool area queue cleaner"; 224 documentation = [ "man:faxqclean(8)" ]; 225 after = [ "hylafax-spool.service" ]; 226 requires = [ "hylafax-spool.service" ]; 227 wantedBy = mkIf cfg.faxqclean.enable.spoolInit requires; 228 startAt = mkIf (cfg.faxqclean.enable.frequency != null) cfg.faxqclean.enable.frequency; 229 serviceConfig.ExecStart = mkSpoolCmd "" "faxqclean" null { 230 v = true; 231 a = cfg.faxqclean.archiving != "never"; 232 A = cfg.faxqclean.archiving == "always"; 233 j = 60 * cfg.faxqclean.doneqMinutes; 234 d = 60 * cfg.faxqclean.docqMinutes; 235 }; 236 }; 237 238 mkFaxgettyService = 239 { name, ... }: 240 lib.nameValuePair "hylafax-faxgetty@${name}" rec { 241 description = "HylaFAX faxgetty for %I"; 242 documentation = [ "man:faxgetty(8)" ]; 243 bindsTo = [ "dev-%i.device" ]; 244 requires = [ "hylafax-spool.service" ]; 245 after = bindsTo ++ requires; 246 before = [ 247 "hylafax-faxq.service" 248 "getty.target" 249 ]; 250 unitConfig.StopWhenUnneeded = true; 251 unitConfig.AssertFileNotEmpty = "${cfg.spoolAreaPath}/etc/config.%I"; 252 serviceConfig.UtmpIdentifier = "%I"; 253 serviceConfig.TTYPath = "/dev/%I"; 254 serviceConfig.Restart = "always"; 255 serviceConfig.KillMode = "process"; 256 serviceConfig.IgnoreSIGPIPE = false; 257 serviceConfig.ExecStart = mkSpoolCmd "-" "faxgetty" "/dev/%I" { }; 258 # faxquit fails if the pipe is already gone 259 # (e.g. the service is already stopping) 260 serviceConfig.ExecStop = mkSpoolCmd "-" "faxquit" "%I" { }; 261 # disable some systemd hardening settings 262 serviceConfig.PrivateDevices = null; 263 serviceConfig.RestrictRealtime = null; 264 }; 265 266 modemServices = lib.listToAttrs (mapModems mkFaxgettyService); 267 268in 269 270{ 271 config.systemd = mkIf cfg.enable { 272 inherit sockets timers paths; 273 services = lib.mapAttrs (lib.const hardenService) (services // modemServices); 274 }; 275}