1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11
12 cfg = config.services.privoxy;
13
14 serialise =
15 name: val:
16 if isList val then
17 concatMapStrings (serialise name) val
18 else if isBool val then
19 serialise name (if val then "1" else "0")
20 else
21 "${name} ${toString val}\n";
22
23 configType =
24 with types;
25 let
26 atom = oneOf [
27 int
28 bool
29 str
30 path
31 ];
32 in
33 attrsOf (either atom (listOf atom))
34 // {
35 description = ''
36 privoxy configuration type. The format consists of an attribute
37 set of settings. Each setting can be either a value (integer, string,
38 boolean or path) or a list of such values.
39 '';
40 };
41
42 ageType = types.str // {
43 check = x: isString x && (builtins.match "([0-9]+([smhdw]|min|ms|us)*)+" x != null);
44 description = "tmpfiles.d(5) age format";
45 };
46
47 configFile = pkgs.writeText "privoxy.conf" (
48 concatStrings (
49 # Relative paths in some options are relative to confdir. Privoxy seems
50 # to parse the options in order of appearance, so this must come first.
51 # Nix however doesn't preserve the order in attrsets, so we have to
52 # hardcode confdir here.
53 [ "confdir ${pkgs.privoxy}/etc\n" ] ++ mapAttrsToList serialise cfg.settings
54 )
55 );
56
57 inspectAction = pkgs.writeText "inspect-all-https.action" ''
58 # Enable HTTPS inspection for all requests
59 {+https-inspection}
60 /
61 '';
62
63in
64
65{
66
67 ###### interface
68
69 options.services.privoxy = {
70
71 enable = mkEnableOption "Privoxy, non-caching filtering proxy";
72
73 enableTor = mkOption {
74 type = types.bool;
75 default = false;
76 description = ''
77 Whether to configure Privoxy to use Tor's faster SOCKS port,
78 suitable for HTTP.
79 '';
80 };
81
82 inspectHttps = mkOption {
83 type = types.bool;
84 default = false;
85 description = ''
86 Whether to configure Privoxy to inspect HTTPS requests, meaning all
87 encrypted traffic will be filtered as well. This works by decrypting
88 and re-encrypting the requests using a per-domain generated certificate.
89
90 To issue per-domain certificates, Privoxy must be provided with a CA
91 certificate, using the `ca-cert-file`,
92 `ca-key-file` settings.
93
94 ::: {.warning}
95 The CA certificate must also be added to the system trust roots,
96 otherwise browsers will reject all Privoxy certificates as invalid.
97 You can do so by using the option
98 {option}`security.pki.certificateFiles`.
99 :::
100 '';
101 };
102
103 certsLifetime = mkOption {
104 type = ageType;
105 default = "10d";
106 example = "12h";
107 description = ''
108 If `inspectHttps` is enabled, the time generated HTTPS
109 certificates will be stored in a temporary directory for reuse. Once
110 the lifetime has expired the directory will cleared and the certificate
111 will have to be generated again, on-demand.
112
113 Depending on the traffic, you may want to reduce the lifetime to limit
114 the disk usage, since Privoxy itself never deletes the certificates.
115
116 ::: {.note}
117 The format is that of the {manpage}`tmpfiles.d(5)`
118 Age parameter.
119 :::
120 '';
121 };
122
123 userActions = mkOption {
124 type = types.lines;
125 default = "";
126 description = ''
127 Actions to be included in a `user.action` file. This
128 will have a higher priority and can be used to override all other
129 actions.
130 '';
131 };
132
133 userFilters = mkOption {
134 type = types.lines;
135 default = "";
136 description = ''
137 Filters to be included in a `user.filter` file. This
138 will have a higher priority and can be used to override all other
139 filters definitions.
140 '';
141 };
142
143 settings = mkOption {
144 type = types.submodule {
145 freeformType = configType;
146
147 options.listen-address = mkOption {
148 type = types.str;
149 default = "127.0.0.1:8118";
150 description = "Pair of address:port the proxy server is listening to.";
151 };
152
153 options.enable-edit-actions = mkOption {
154 type = types.bool;
155 default = false;
156 description = "Whether the web-based actions file editor may be used.";
157 };
158
159 options.actionsfile = mkOption {
160 type = types.listOf types.str;
161 # This must come after all other entries, in order to override the
162 # other actions/filters installed by Privoxy or the user.
163 apply =
164 x: x ++ optional (cfg.userActions != "") (toString (pkgs.writeText "user.actions" cfg.userActions));
165 default = [
166 "match-all.action"
167 "default.action"
168 ];
169 description = ''
170 List of paths to Privoxy action files. These paths may either be
171 absolute or relative to the privoxy configuration directory.
172 '';
173 };
174
175 options.filterfile = mkOption {
176 type = types.listOf types.str;
177 default = [ "default.filter" ];
178 apply =
179 x: x ++ optional (cfg.userFilters != "") (toString (pkgs.writeText "user.filter" cfg.userFilters));
180 description = ''
181 List of paths to Privoxy filter files. These paths may either be
182 absolute or relative to the privoxy configuration directory.
183 '';
184 };
185 };
186 default = { };
187 example = literalExpression ''
188 { # Listen on IPv6 only
189 listen-address = "[::]:8118";
190
191 # Forward .onion requests to Tor
192 forward-socks5 = ".onion localhost:9050 .";
193
194 # Log redirects and filters
195 debug = [ 128 64 ];
196 # This is equivalent to writing these lines
197 # in the Privoxy configuration file:
198 # debug 128
199 # debug 64
200 }
201 '';
202 description = ''
203 This option is mapped to the main Privoxy configuration file.
204 Check out the Privoxy user manual at
205 <https://www.privoxy.org/user-manual/config.html>
206 for available settings and documentation.
207
208 ::: {.note}
209 Repeated settings can be represented by using a list.
210 :::
211 '';
212 };
213
214 };
215
216 ###### implementation
217
218 config = mkIf cfg.enable {
219
220 users.users.privoxy = {
221 description = "Privoxy daemon user";
222 isSystemUser = true;
223 group = "privoxy";
224 };
225
226 users.groups.privoxy = { };
227
228 systemd.tmpfiles.rules = optional cfg.inspectHttps "d ${cfg.settings.certificate-directory} 0770 privoxy privoxy ${cfg.certsLifetime}";
229
230 systemd.services.privoxy = {
231 description = "Filtering web proxy";
232 documentation = [ "man:privoxy(8)" ];
233 after = [
234 "network.target"
235 "nss-lookup.target"
236 ];
237 wantedBy = [ "multi-user.target" ];
238 serviceConfig = {
239 User = "privoxy";
240 Group = "privoxy";
241 ExecStart = "${pkgs.privoxy}/bin/privoxy --no-daemon ${configFile}";
242 PrivateDevices = true;
243 PrivateTmp = true;
244 ProtectHome = true;
245 ProtectSystem = "full";
246 };
247 unitConfig = mkIf cfg.inspectHttps {
248 ConditionPathExists = with cfg.settings; [
249 ca-cert-file
250 ca-key-file
251 ];
252 };
253 };
254
255 services.tor.settings.SOCKSPort = mkIf cfg.enableTor [
256 # Route HTTP traffic over a faster port (without IsolateDestAddr).
257 {
258 addr = "127.0.0.1";
259 port = 9063;
260 IsolateDestAddr = false;
261 }
262 ];
263
264 services.privoxy.settings =
265 {
266 user-manual = "${pkgs.privoxy}/share/doc/privoxy/user-manual";
267 # This is needed for external filters
268 temporary-directory = "/tmp";
269 filterfile = [ "default.filter" ];
270 actionsfile = [
271 "match-all.action"
272 "default.action"
273 ] ++ optional cfg.inspectHttps (toString inspectAction);
274 }
275 // (optionalAttrs cfg.enableTor {
276 forward-socks5 = "/ 127.0.0.1:9063 .";
277 toggle = true;
278 enable-remote-toggle = false;
279 enable-edit-actions = false;
280 enable-remote-http-toggle = false;
281 })
282 // (optionalAttrs cfg.inspectHttps {
283 # This allows setting absolute key/crt paths
284 ca-directory = "/var/empty";
285 certificate-directory = "/run/privoxy/certs";
286 trusted-cas-file = config.security.pki.caBundle;
287 });
288 };
289
290 imports =
291 let
292 top = x: [
293 "services"
294 "privoxy"
295 x
296 ];
297 setting = x: [
298 "services"
299 "privoxy"
300 "settings"
301 x
302 ];
303 in
304 [
305 (mkRenamedOptionModule (top "enableEditActions") (setting "enable-edit-actions"))
306 (mkRenamedOptionModule (top "listenAddress") (setting "listen-address"))
307 (mkRenamedOptionModule (top "actionsFiles") (setting "actionsfile"))
308 (mkRenamedOptionModule (top "filterFiles") (setting "filterfile"))
309 (mkRemovedOptionModule (top "extraConfig") ''
310 Use services.privoxy.settings instead.
311 This is part of the general move to use structured settings instead of raw
312 text for config as introduced by RFC0042:
313 https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
314 '')
315 ];
316
317 meta.maintainers = with lib.maintainers; [ rnhmjoj ];
318
319}