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}