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