10 inherit (lib) mkIf mkMerge;
11 inherit (lib) concatStringsSep optionalString;
13 cfg = config.services.hylafax;
14 mapModems = lib.forEach (lib.attrValues cfg.modems);
18 # creates hylafax config file,
19 # makes sure "Include" is listed *first*
21 mkLines = lib.flip lib.pipe [
22 (lib.mapAttrsToList (key: map (val: "${key}: ${val}")))
25 include = mkLines { Include = conf.Include or [ ]; };
26 other = mkLines (conf // { Include = [ ]; });
28 pkgs.writeText "hylafax-config${name}" (concatStringsSep "\n" (include ++ other));
30 globalConfigPath = mkConfigFile "" cfg.faxqConfig;
35 { config, name, ... }: mkConfigFile ".${name}" (cfg.commonModemConfig // config);
37 { name, type, ... }@modem:
39 # check if modem config file exists:
40 test -f "${pkgs.hylafaxplus}/spool/config/${type}"
43 --no-target-directory \
44 "${mkModemConfigFile modem}" \
48 pkgs.runCommand "hylafax-config-modems" {
49 preferLocalBuild = true;
50 } ''mkdir --parents "$out/" ${concatStringsSep "\n" (mapModems mkLine)}'';
52 setupSpoolScript = pkgs.substituteAll {
53 name = "hylafax-setup-spool.sh";
58 lockPath = "/var/lock";
59 inherit globalConfigPath modemConfigPath;
60 inherit (cfg) sendmailPath spoolAreaPath userAccessFile;
61 inherit (pkgs) hylafaxplus runtimeShell;
64 waitFaxqScript = pkgs.substituteAll {
65 # This script checks the modems status files
66 # and waits until all modems report readiness.
67 name = "hylafax-faxq-wait-start.sh";
70 timeoutSec = toString 10;
71 inherit (cfg) spoolAreaPath;
72 inherit (pkgs) runtimeShell;
75 sockets.hylafax-hfaxd = {
76 description = "HylaFAX server socket";
77 documentation = [ "man:hfaxd(8)" ];
78 wantedBy = [ "multi-user.target" ];
79 listenStreams = [ "127.0.0.1:4559" ];
80 socketConfig.FreeBind = true;
81 socketConfig.Accept = true;
84 paths.hylafax-faxq = {
85 description = "HylaFAX queue manager sendq watch";
90 wantedBy = [ "multi-user.target" ];
91 pathConfig.PathExistsGlob = [ "${cfg.spoolAreaPath}/sendq/q*" ];
95 (mkIf (cfg.faxcron.enable.frequency != null) { hylafax-faxcron.timerConfig.Persistent = true; })
96 (mkIf (cfg.faxqclean.enable.frequency != null) { hylafax-faxqclean.timerConfig.Persistent = true; })
100 # Add some common systemd service hardening settings,
101 # but allow each service (here) to override
102 # settings by explicitly setting those to `null`.
103 # More hardening would be nice but makes
104 # customizing hylafax setups very difficult.
105 # If at all, it should only be added along
106 # with some options to customize it.
109 PrivateDevices = true; # breaks /dev/tty...
110 PrivateNetwork = true;
112 #ProtectClock = true; # breaks /dev/tty... (why?)
113 ProtectControlGroups = true;
114 #ProtectHome = true; # breaks custom spool dirs
115 ProtectKernelLogs = true;
116 ProtectKernelModules = true;
117 ProtectKernelTunables = true;
118 #ProtectSystem = "strict"; # breaks custom spool dirs
119 RestrictNamespaces = true;
120 RestrictRealtime = true;
122 filter = key: value: (value != null) || !(lib.hasAttr key hardening);
123 apply = service: lib.filterAttrs filter (hardening // (service.serviceConfig or { }));
125 service: service // { serviceConfig = apply service; };
127 services.hylafax-spool = {
128 description = "HylaFAX spool area preparation";
129 documentation = [ "man:hylafax-server(4)" ];
132 cd "${cfg.spoolAreaPath}"
133 ${cfg.spoolExtraInit}
134 if ! test -f "${cfg.spoolAreaPath}/etc/hosts.hfaxd"
136 echo hosts.hfaxd is missing
140 serviceConfig.ExecStop = "${setupSpoolScript}";
141 serviceConfig.RemainAfterExit = true;
142 serviceConfig.Type = "oneshot";
143 unitConfig.RequiresMountsFor = [ cfg.spoolAreaPath ];
146 services.hylafax-faxq = {
147 description = "HylaFAX queue manager";
148 documentation = [ "man:faxq(8)" ];
149 requires = [ "hylafax-spool.service" ];
150 after = [ "hylafax-spool.service" ];
151 wants = mapModems ({ name, ... }: "hylafax-faxgetty@${name}.service");
152 wantedBy = mkIf cfg.autostart [ "multi-user.target" ];
153 serviceConfig.Type = "forking";
154 serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/faxq -q "${cfg.spoolAreaPath}"'';
155 # This delays the "readiness" of this service until
156 # all modems are initialized (or a timeout is reached).
157 # Otherwise, sending a fax with the fax service
158 # stopped will always yield a failed send attempt:
159 # The fax service is started when the job is created with
160 # `sendfax`, but modems need some time to initialize.
161 serviceConfig.ExecStartPost = [ "${waitFaxqScript}" ];
162 # faxquit fails if the pipe is already gone
163 # (e.g. the service is already stopping)
164 serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}"'';
165 # disable some systemd hardening settings
166 serviceConfig.PrivateDevices = null;
167 serviceConfig.RestrictRealtime = null;
170 services."hylafax-hfaxd@" = {
171 description = "HylaFAX server";
172 documentation = [ "man:hfaxd(8)" ];
173 after = [ "hylafax-faxq.service" ];
174 requires = [ "hylafax-faxq.service" ];
175 serviceConfig.StandardInput = "socket";
176 serviceConfig.StandardOutput = "socket";
177 serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/hfaxd -q "${cfg.spoolAreaPath}" -d -I'';
178 unitConfig.RequiresMountsFor = [ cfg.userAccessFile ];
179 # disable some systemd hardening settings
180 serviceConfig.PrivateDevices = null;
181 serviceConfig.PrivateNetwork = null;
184 services.hylafax-faxcron = rec {
185 description = "HylaFAX spool area maintenance";
186 documentation = [ "man:faxcron(8)" ];
187 after = [ "hylafax-spool.service" ];
188 requires = [ "hylafax-spool.service" ];
189 wantedBy = mkIf cfg.faxcron.enable.spoolInit requires;
190 startAt = mkIf (cfg.faxcron.enable.frequency != null) cfg.faxcron.enable.frequency;
191 serviceConfig.ExecStart = concatStringsSep " " [
192 "${pkgs.hylafaxplus}/spool/bin/faxcron"
193 ''-q "${cfg.spoolAreaPath}"''
194 ''-info ${toString cfg.faxcron.infoDays}''
195 ''-log ${toString cfg.faxcron.logDays}''
196 ''-rcv ${toString cfg.faxcron.rcvDays}''
200 services.hylafax-faxqclean = rec {
201 description = "HylaFAX spool area queue cleaner";
202 documentation = [ "man:faxqclean(8)" ];
203 after = [ "hylafax-spool.service" ];
204 requires = [ "hylafax-spool.service" ];
205 wantedBy = mkIf cfg.faxqclean.enable.spoolInit requires;
206 startAt = mkIf (cfg.faxqclean.enable.frequency != null) cfg.faxqclean.enable.frequency;
207 serviceConfig.ExecStart = concatStringsSep " " [
208 "${pkgs.hylafaxplus}/spool/bin/faxqclean"
209 ''-q "${cfg.spoolAreaPath}"''
211 (optionalString (cfg.faxqclean.archiving != "never") "-a")
212 (optionalString (cfg.faxqclean.archiving == "always") "-A")
213 ''-j ${toString (cfg.faxqclean.doneqMinutes * 60)}''
214 ''-d ${toString (cfg.faxqclean.docqMinutes * 60)}''
220 lib.nameValuePair "hylafax-faxgetty@${name}" rec {
221 description = "HylaFAX faxgetty for %I";
222 documentation = [ "man:faxgetty(8)" ];
223 bindsTo = [ "dev-%i.device" ];
224 requires = [ "hylafax-spool.service" ];
225 after = bindsTo ++ requires;
227 "hylafax-faxq.service"
230 unitConfig.StopWhenUnneeded = true;
231 unitConfig.AssertFileNotEmpty = "${cfg.spoolAreaPath}/etc/config.%I";
232 serviceConfig.UtmpIdentifier = "%I";
233 serviceConfig.TTYPath = "/dev/%I";
234 serviceConfig.Restart = "always";
235 serviceConfig.KillMode = "process";
236 serviceConfig.IgnoreSIGPIPE = false;
237 serviceConfig.ExecStart = ''-${pkgs.hylafaxplus}/spool/bin/faxgetty -q "${cfg.spoolAreaPath}" /dev/%I'';
238 # faxquit fails if the pipe is already gone
239 # (e.g. the service is already stopping)
240 serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}" %I'';
241 # disable some systemd hardening settings
242 serviceConfig.PrivateDevices = null;
243 serviceConfig.RestrictRealtime = null;
246 modemServices = lib.listToAttrs (mapModems mkFaxgettyService);
251 config.systemd = mkIf cfg.enable {
252 inherit sockets timers paths;
253 services = lib.mapAttrs (lib.const hardenService) (services // modemServices);