diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml index 2de32cd09ff..9a1e6b6618d 100644 --- a/nixos/doc/manual/release-notes/rl-2105.xml +++ b/nixos/doc/manual/release-notes/rl-2105.xml @@ -39,6 +39,24 @@ (#7547). + + + Privoxy has been updated + to version 3.0.32 (See announcement). + Compared to the previous release, Privoxy has gained support for HTTPS + inspection (still experimental), Brotli decompression, several new filters + and lots of bug fixes, including security ones. In addition, the package + is now built with compression and external filters support, which were + previously disabled. + + + Regarding the NixOS module, new options for HTTPS inspection have been added + and has been replaced by the new + + (See RFC 0042 + for the motivation). + + diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix index 7caae328203..b8d33916342 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,48 @@ in { addr = "127.0.0.1"; port = 9063; IsolateDestAddr = false; } ]; + services.privoxy.settings = { + user-manual = "${pkgs.privoxy}/share/doc/privoxy/user-manual"; + # This is needed for external filters + temporary-directory = "/tmp"; + 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 ]; } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index fe60b0b83f5..00e84a9df82 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -326,6 +326,7 @@ in predictable-interface-names = handleTest ./predictable-interface-names.nix {}; printing = handleTest ./printing.nix {}; privacyidea = handleTest ./privacyidea.nix {}; + privoxy = handleTest ./privoxy.nix {}; prometheus = handleTest ./prometheus.nix {}; prometheus-exporters = handleTest ./prometheus-exporters.nix {}; prosody = handleTest ./xmpp/prosody.nix {}; diff --git a/nixos/tests/privoxy.nix b/nixos/tests/privoxy.nix new file mode 100644 index 00000000000..d16cc498691 --- /dev/null +++ b/nixos/tests/privoxy.nix @@ -0,0 +1,113 @@ +import ./make-test-python.nix ({ lib, pkgs, ... }: + +let + # Note: For some reason Privoxy can't issue valid + # certificates if the CA is generated using gnutls :( + certs = pkgs.runCommand "example-certs" + { buildInputs = [ pkgs.openssl ]; } + '' + mkdir $out + + # generate CA keypair + openssl req -new -nodes -x509 \ + -extensions v3_ca -keyout $out/ca.key \ + -out $out/ca.crt -days 365 \ + -subj "/O=Privoxy CA/CN=Privoxy CA" + + # generate server key/signing request + openssl genrsa -out $out/server.key 3072 + openssl req -new -key $out/server.key \ + -out server.csr -sha256 \ + -subj "/O=An unhappy server./CN=example.com" + + # sign the request/generate the certificate + openssl x509 -req -in server.csr -CA $out/ca.crt \ + -CAkey $out/ca.key -CAcreateserial -out $out/server.crt \ + -days 500 -sha256 + ''; +in + +{ + name = "privoxy"; + meta = with lib.maintainers; { + maintainers = [ rnhmjoj ]; + }; + + machine = { ... }: { + services.nginx.enable = true; + services.nginx.virtualHosts."example.com" = { + addSSL = true; + sslCertificate = "${certs}/server.crt"; + sslCertificateKey = "${certs}/server.key"; + locations."/".root = pkgs.writeTextFile + { name = "bad-day"; + destination = "/how-are-you/index.html"; + text = "I've had a bad day!\n"; + }; + locations."/ads".extraConfig = '' + return 200 "Hot Nixpkgs PRs in your area. Click here!\n"; + ''; + }; + + services.privoxy = { + enable = true; + inspectHttps = true; + settings = { + ca-cert-file = "${certs}/ca.crt"; + ca-key-file = "${certs}/ca.key"; + debug = 65536; + }; + userActions = '' + {+filter{positive}} + example.com + + {+block{Fake ads}} + example.com/ads + ''; + userFilters = '' + FILTER: positive This is a filter example. + s/bad/great/ig + ''; + }; + + security.pki.certificateFiles = [ "${certs}/ca.crt" ]; + + networking.hosts."::1" = [ "example.com" ]; + networking.proxy.httpProxy = "http://localhost:8118"; + networking.proxy.httpsProxy = "http://localhost:8118"; + }; + + testScript = + '' + with subtest("Privoxy is running"): + machine.wait_for_unit("privoxy") + machine.wait_for_open_port("8118") + machine.succeed("curl -f http://config.privoxy.org") + + with subtest("Privoxy can filter http requests"): + machine.wait_for_open_port("80") + assert "great day" in machine.succeed( + "curl -sfL http://example.com/how-are-you? | tee /dev/stderr" + ) + + with subtest("Privoxy can filter https requests"): + machine.wait_for_open_port("443") + assert "great day" in machine.succeed( + "curl -sfL https://example.com/how-are-you? | tee /dev/stderr" + ) + + with subtest("Blocks are working"): + machine.wait_for_open_port("443") + machine.fail("curl -f https://example.com/ads 1>&2") + machine.succeed("curl -f https://example.com/PRIVOXY-FORCE/ads 1>&2") + + with subtest("Temporary certificates are cleaned"): + # Count current certificates + machine.succeed("test $(ls /run/privoxy/certs | wc -l) -gt 0") + # Forward in time 12 days, trigger the timer.. + machine.succeed("date -s \"$(date --date '12 days')\"") + machine.systemctl("start systemd-tmpfiles-clean") + # ...and count again + machine.succeed("test $(ls /run/privoxy/certs | wc -l) -eq 0") + ''; +}) diff --git a/pkgs/tools/networking/privoxy/default.nix b/pkgs/tools/networking/privoxy/default.nix index 85a8cd5d768..9fce8d7a5f4 100644 --- a/pkgs/tools/networking/privoxy/default.nix +++ b/pkgs/tools/networking/privoxy/default.nix @@ -1,4 +1,9 @@ -{ lib, stdenv, fetchurl, autoreconfHook, zlib, pcre, w3m, man }: +{ lib, stdenv +, nixosTests +, fetchurl, autoreconfHook +, zlib, pcre, w3m, man +, mbedtls, brotli +}: stdenv.mkDerivation rec { @@ -13,18 +18,28 @@ stdenv.mkDerivation rec { hardeningEnable = [ "pie" ]; nativeBuildInputs = [ autoreconfHook w3m man ]; - buildInputs = [ zlib pcre ]; + buildInputs = [ zlib pcre mbedtls brotli ]; - makeFlags = [ "STRIP="]; + makeFlags = [ "STRIP=" ]; + configureFlags = [ + "--with-mbedtls" + "--with-brotli" + "--enable-external-filters" + "--enable-compression" + ]; postInstall = '' - rm -rf $out/var + rm -r $out/var ''; + passthru.tests.privoxy = nixosTests.privoxy; + meta = with lib; { homepage = "https://www.privoxy.org/"; description = "Non-caching web proxy with advanced filtering capabilities"; - license = licenses.gpl2Plus; + # When linked with mbedtls, the license becomes GPLv3 (or later), otherwise + # GPLv2 (or later). See https://www.privoxy.org/user-manual/copyright.html + license = licenses.gpl3Plus; platforms = platforms.all; maintainers = [ maintainers.phreedom ]; };