diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix index 7caae3282032..8ab9a6171fc7 100644 --- a/nixos/modules/services/networking/privoxy.nix +++ b/nixos/modules/services/networking/privoxy.nix @@ -4,26 +4,46 @@ with lib; let - inherit (pkgs) privoxy; - cfg = config.services.privoxy; - confFile = pkgs.writeText "privoxy.conf" ('' - user-manual ${privoxy}/share/doc/privoxy/user-manual - confdir ${privoxy}/etc/ - listen-address ${cfg.listenAddress} - enable-edit-actions ${if (cfg.enableEditActions == true) then "1" else "0"} - ${concatMapStrings (f: "actionsfile ${f}\n") cfg.actionsFiles} - ${concatMapStrings (f: "filterfile ${f}\n") cfg.filterFiles} - '' + optionalString cfg.enableTor '' - forward-socks5t / 127.0.0.1:9063 . - toggle 1 - enable-remote-toggle 0 - enable-edit-actions 0 - enable-remote-http-toggle 0 - '' + '' - ${cfg.extraConfig} - ''); + serialise = name: val: + if isList val then concatMapStrings (serialise name) val + else if isBool val then serialise name (if val then "1" else "0") + else "${name} ${toString val}\n"; + + configType = with types; + let atom = oneOf [ int bool string path ]; + in attrsOf (either atom (listOf atom)) + // { description = '' + privoxy configuration type. The format consists of an attribute + set of settings. Each setting can be either a value (integer, string, + boolean or path) or a list of such values. + ''; + }; + + ageType = types.str // { + check = x: + isString x && + (builtins.match "([0-9]+([smhdw]|min|ms|us)*)+" x != null); + description = "tmpfiles.d(5) age format"; + }; + + configFile = pkgs.writeText "privoxy.conf" + (concatStrings ( + # Relative paths in some options are relative to confdir. Privoxy seems + # to parse the options in order of appearance, so this must come first. + # Nix however doesn't preserve the order in attrsets, so we have to + # hardcode confdir here. + [ "confdir ${pkgs.privoxy}/etc\n" ] + ++ mapAttrsToList serialise cfg.settings + )); + + inspectAction = pkgs.writeText "inspect-all-https.action" + '' + # Enable HTTPS inspection for all requests + {+https-inspection} + / + ''; in @@ -31,70 +51,130 @@ in ###### interface - options = { + options.services.privoxy = { - services.privoxy = { + enable = mkEnableOption "Privoxy, non-caching filtering proxy"; - enable = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable the Privoxy non-caching filtering proxy. - ''; - }; - - listenAddress = mkOption { - type = types.str; - default = "127.0.0.1:8118"; - description = '' - Address the proxy server is listening to. - ''; - }; - - actionsFiles = mkOption { - type = types.listOf types.str; - example = [ "match-all.action" "default.action" "/etc/privoxy/user.action" ]; - default = [ "match-all.action" "default.action" ]; - description = '' - List of paths to Privoxy action files. - These paths may either be absolute or relative to the privoxy configuration directory. - ''; - }; - - filterFiles = mkOption { - type = types.listOf types.str; - example = [ "default.filter" "/etc/privoxy/user.filter" ]; - default = [ "default.filter" ]; - description = '' - List of paths to Privoxy filter files. - These paths may either be absolute or relative to the privoxy configuration directory. - ''; - }; - - enableEditActions = mkOption { - type = types.bool; - default = false; - description = '' - Whether or not the web-based actions file editor may be used. - ''; - }; - - enableTor = mkOption { - type = types.bool; - default = false; - description = '' - Whether to configure Privoxy to use Tor's faster SOCKS port, - suitable for HTTP. - ''; - }; - - extraConfig = mkOption { - type = types.lines; - default = "" ; - description = '' - Extra configuration. Contents will be added verbatim to the configuration file. - ''; + enableTor = mkOption { + type = types.bool; + default = false; + description = '' + Whether to configure Privoxy to use Tor's faster SOCKS port, + suitable for HTTP. + ''; + }; + + inspectHttps = mkOption { + type = types.bool; + default = false; + description = '' + Whether to configure Privoxy to inspect HTTPS requests, meaning all + encrypted traffic will be filtered as well. This works by decrypting + and re-encrypting the requests using a per-domain generated certificate. + + To issue per-domain certificates, Privoxy must be provided with a CA + certificate, using the ca-cert-file, + ca-key-file settings. + + + The CA certificate must also be added to the system trust roots, + otherwise browsers will reject all Privoxy certificates as invalid. + You can do so by using the option + . + + ''; + }; + + certsLifetime = mkOption { + type = ageType; + default = "10d"; + example = "12h"; + description = '' + If inspectHttps is enabled, the time generated HTTPS + certificates will be stored in a temporary directory for reuse. Once + the lifetime has expired the directory will cleared and the certificate + will have to be generated again, on-demand. + + Depending on the traffic, you may want to reduce the lifetime to limit + the disk usage, since Privoxy itself never deletes the certificates. + + The format is that of the tmpfiles.d(5) + Age parameter. + ''; + }; + + userActions = mkOption { + type = types.lines; + default = ""; + description = '' + Actions to be included in a user.action file. This + will have a higher priority and can be used to override all other + actions. + ''; + }; + + userFilters = mkOption { + type = types.lines; + default = ""; + description = '' + Filters to be included in a user.filter file. This + will have a higher priority and can be used to override all other + filters definitions. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = configType; + + options.listen-address = mkOption { + type = types.str; + default = "127.0.0.1:8118"; + description = "Pair of address:port the proxy server is listening to."; + }; + + options.enable-edit-actions = mkOption { + type = types.bool; + default = false; + description = "Whether the web-based actions file editor may be used."; + }; + + options.actionsfile = mkOption { + type = types.listOf types.str; + # This must come after all other entries, in order to override the + # other actions/filters installed by Privoxy or the user. + apply = x: x ++ optional (cfg.userActions != "") + (toString (pkgs.writeText "user.actions" cfg.userActions)); + default = [ "match-all.action" "default.action" ]; + description = '' + List of paths to Privoxy action files. These paths may either be + absolute or relative to the privoxy configuration directory. + ''; + }; + + options.filterfile = mkOption { + type = types.listOf types.str; + default = [ "default.filter" ]; + apply = x: x ++ optional (cfg.userFilters != "") + (toString (pkgs.writeText "user.filter" cfg.userFilters)); + description = '' + List of paths to Privoxy filter files. These paths may either be + absolute or relative to the privoxy configuration directory. + ''; + }; }; + default = {}; + example = literalExample '' + { listen-address = "[::]:8118"; # listen on IPv6 only + forward-socks5 = ".onion localhost:9050 ."; # forward .onion requests to Tor + } + ''; + description = '' + This option is mapped to the main Privoxy configuration file. + Check out the Privoxy user manual at + + for available settings and documentation. + ''; }; }; @@ -104,23 +184,34 @@ in config = mkIf cfg.enable { users.users.privoxy = { + description = "Privoxy daemon user"; isSystemUser = true; - home = "/var/empty"; group = "privoxy"; }; users.groups.privoxy = {}; + systemd.tmpfiles.rules = with cfg.settings; [ + "d ${certificate-directory} 0770 privoxy privoxy ${cfg.certsLifetime}" + ]; + systemd.services.privoxy = { description = "Filtering web proxy"; after = [ "network.target" "nss-lookup.target" ]; wantedBy = [ "multi-user.target" ]; - serviceConfig.ExecStart = "${privoxy}/bin/privoxy --no-daemon --user privoxy ${confFile}"; - - serviceConfig.PrivateDevices = true; - serviceConfig.PrivateTmp = true; - serviceConfig.ProtectHome = true; - serviceConfig.ProtectSystem = "full"; + serviceConfig = { + User = "privoxy"; + Group = "privoxy"; + ExecStart = "${pkgs.privoxy}/bin/privoxy --no-daemon ${configFile}"; + PrivateDevices = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "full"; + }; + unitConfig = mkIf cfg.inspectHttps { + ConditionPathExists = with cfg.settings; + [ ca-cert-file ca-key-file ]; + }; }; services.tor.settings.SOCKSPort = mkIf cfg.enableTor [ @@ -128,8 +219,46 @@ in { addr = "127.0.0.1"; port = 9063; IsolateDestAddr = false; } ]; + services.privoxy.settings = { + user-manual = "${pkgs.privoxy}/share/doc/privoxy/user-manual"; + filterfile = [ "default.filter" ]; + actionsfile = + [ "match-all.action" + "default.action" + ] ++ optional cfg.inspectHttps (toString inspectAction); + } // (optionalAttrs cfg.enableTor { + forward-socks5 = "127.0.0.1:9063 ."; + toggle = true; + enable-remote-toggle = false; + enable-edit-actions = false; + enable-remote-http-toggle = false; + }) // (optionalAttrs cfg.inspectHttps { + # This allows setting absolute key/crt paths + ca-directory = "/var/empty"; + certificate-directory = "/run/privoxy/certs"; + trusted-cas-file = "/etc/ssl/certs/ca-certificates.crt"; + }); + }; + imports = + let + top = x: [ "services" "privoxy" x ]; + setting = x: [ "services" "privoxy" "settings" x ]; + in + [ (mkRenamedOptionModule (top "enableEditActions") (setting "enable-edit-actions")) + (mkRenamedOptionModule (top "listenAddress") (setting "listen-address")) + (mkRenamedOptionModule (top "actionsFiles") (setting "actionsfile")) + (mkRenamedOptionModule (top "filterFiles") (setting "filterfile")) + (mkRemovedOptionModule (top "extraConfig") + '' + Use services.privoxy.settings instead. + This is part of the general move to use structured settings instead of raw + text for config as introduced by RFC0042: + https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md + '') + ]; + meta.maintainers = with lib.maintainers; [ rnhmjoj ]; }