summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorJörg Thalheim <joerg@thalheim.io>2020-12-31 07:31:38 +0100
committerJörg Thalheim <joerg@thalheim.io>2020-12-31 07:31:38 +0100
commitf19b7b03a03b7f1d5beb44471eb9298de4b9e186 (patch)
tree2bb5fabe03cff0c2058f69921315ab3ff724f9d0 /nixos
parent572a864d024b0c91ac39133f35364362b2376c07 (diff)
parent7a580a12196e63eb665de6eb4db8bb3b6da142f5 (diff)
downloadnixpkgs-f19b7b03a03b7f1d5beb44471eb9298de4b9e186.tar
nixpkgs-f19b7b03a03b7f1d5beb44471eb9298de4b9e186.tar.gz
nixpkgs-f19b7b03a03b7f1d5beb44471eb9298de4b9e186.tar.bz2
nixpkgs-f19b7b03a03b7f1d5beb44471eb9298de4b9e186.tar.lz
nixpkgs-f19b7b03a03b7f1d5beb44471eb9298de4b9e186.tar.xz
nixpkgs-f19b7b03a03b7f1d5beb44471eb9298de4b9e186.tar.zst
nixpkgs-f19b7b03a03b7f1d5beb44471eb9298de4b9e186.zip
Merge branch 'master' into staging-next
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml37
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.nix11
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.pl51
-rw-r--r--nixos/modules/programs/command-not-found/default.nix18
-rw-r--r--nixos/modules/programs/command-not-found/rust/Cargo.lock131
-rw-r--r--nixos/modules/programs/command-not-found/rust/Cargo.toml10
-rw-r--r--nixos/modules/programs/command-not-found/rust/src/main.rs52
-rw-r--r--nixos/modules/services/databases/redis.nix6
-rw-r--r--nixos/modules/services/misc/home-assistant.nix28
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix7
-rw-r--r--nixos/modules/services/networking/privoxy.nix7
-rw-r--r--nixos/modules/services/security/tor.nix1390
-rw-r--r--nixos/modules/services/system/nscd.nix41
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix21
-rw-r--r--nixos/modules/system/activation/switch-to-configuration.pl11
-rw-r--r--nixos/modules/system/boot/stage-1.nix2
-rw-r--r--nixos/modules/tasks/filesystems.nix9
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix2
-rw-r--r--nixos/tests/tor.nix2
19 files changed, 1182 insertions, 654 deletions
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
index d0ff81c1dbb..4510fdd4422 100644
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -113,6 +113,18 @@
     <replaceable>path</replaceable>
    </arg>
    <arg>
+    <option>-L</option>
+   </arg>
+   <arg>
+    <option>--refresh</option>
+   </arg>
+   <arg>
+    <option>--no-net</option>
+   </arg>
+   <arg>
+    <option>--impure</option>
+   </arg>
+   <arg>
     <group choice='req'>
      <arg choice='plain'><option>--verbose</option></arg>
      <arg choice='plain'><option>-v</option></arg>
@@ -131,6 +143,18 @@
     <replaceable>number</replaceable>
    </arg>
    <arg>
+    <option>--fallback</option>
+   </arg>
+   <arg>
+    <option>--repair</option>
+   </arg>
+   <arg>
+    <group choice='req'>
+     <arg choice='plain'><option>--no-build-output</option></arg>
+     <arg choice='plain'><option>-Q</option></arg>
+    </group>
+   </arg>
+   <arg>
     <group choice='req'>
      <arg choice='plain'><option>--keep-failed</option></arg>
      <arg choice='plain'><option>-K</option></arg>
@@ -567,10 +591,19 @@
 
   <para>
    In addition, <command>nixos-rebuild</command> accepts various Nix-related
-   flags, including <option>--max-jobs</option> / <option>-j</option>,
+   flags: <option>-I</option>, <option>--max-jobs</option> / <option>-j</option>,
    <option>--show-trace</option>, <option>--keep-failed</option>,
-   <option>--keep-going</option>, <option>--impure</option>, and <option>--verbose</option> /
+   <option>--keep-going</option>, <option>--impure</option>,  and <option>--verbose</option> /
+   <option>--builders</option>, <option>--show-trace</option>,
+   <option>--fallback</option>, <option>--repair</option>,
+   <option>--no-build-output</option> / <option>-Q</option>,
+   <option>--keep-failed</option> / <option>-K</option>,
+   <option>--keep-going</option> / <option>-k</option> and <option>--verbose</option> /
    <option>-v</option>. See the Nix manual for details.
+
+   The following Nix flags that are support by the upcoming nix 2.4 version:
+   <option>-L</option>, <option>--refresh</option>, <option>--no-net</option>,
+   <option>--no-net</option>, <option>--impure</option>. See <command>nix --help</command> or <command>nix build --help</command> for details.
   </para>
  </refsection>
 
diff --git a/nixos/modules/programs/command-not-found/command-not-found.nix b/nixos/modules/programs/command-not-found/command-not-found.nix
index 656c255fcb1..da09488d82c 100644
--- a/nixos/modules/programs/command-not-found/command-not-found.nix
+++ b/nixos/modules/programs/command-not-found/command-not-found.nix
@@ -9,17 +9,9 @@ with lib;
 
 let
   cfg = config.programs.command-not-found;
-  commandNotFound = pkgs.substituteAll {
-    name = "command-not-found";
-    dir = "bin";
-    src = ./command-not-found.pl;
-    isExecutable = true;
-    inherit (pkgs) perl;
+  commandNotFound = pkgs.callPackage ./. {
     inherit (cfg) dbPath;
-    perlFlags = concatStrings (map (path: "-I ${path}/${pkgs.perl.libPrefix} ")
-      [ pkgs.perlPackages.DBI pkgs.perlPackages.DBDSQLite pkgs.perlPackages.StringShellQuote ]);
   };
-
 in
 
 {
@@ -91,5 +83,4 @@ in
 
     environment.systemPackages = [ commandNotFound ];
   };
-
 }
diff --git a/nixos/modules/programs/command-not-found/command-not-found.pl b/nixos/modules/programs/command-not-found/command-not-found.pl
deleted file mode 100644
index ab7aa204653..00000000000
--- a/nixos/modules/programs/command-not-found/command-not-found.pl
+++ /dev/null
@@ -1,51 +0,0 @@
-#! @perl@/bin/perl -w @perlFlags@
-
-use strict;
-use DBI;
-use DBD::SQLite;
-use String::ShellQuote;
-use Config;
-
-my $program = $ARGV[0];
-
-my $dbPath = "@dbPath@";
-
-my $dbh = DBI->connect("dbi:SQLite:dbname=$dbPath", "", "")
-    or die "cannot open database `$dbPath'";
-$dbh->{RaiseError} = 0;
-$dbh->{PrintError} = 0;
-
-my $system = $ENV{"NIX_SYSTEM"} // $Config{myarchname};
-
-my $res = $dbh->selectall_arrayref(
-    "select package from Programs where system = ? and name = ?",
-    { Slice => {} }, $system, $program);
-
-if (!defined $res || scalar @$res == 0) {
-    print STDERR "$program: command not found\n";
-} elsif (scalar @$res == 1) {
-    my $package = @$res[0]->{package};
-    if ($ENV{"NIX_AUTO_INSTALL"} // "") {
-        print STDERR <<EOF;
-The program ‘$program’ is currently not installed. It is provided by
-the package ‘$package’, which I will now install for you.
-EOF
-        ;
-        exit 126 if system("nix-env", "-iA", "nixos.$package") == 0;
-    } elsif ($ENV{"NIX_AUTO_RUN"} // "") {
-        exec("nix-shell", "-p", $package, "--run", shell_quote("exec", @ARGV));
-    } else {
-        print STDERR <<EOF;
-The program ‘$program’ is currently not installed. You can install it by typing:
-  nix-env -iA nixos.$package
-EOF
-    }
-} else {
-    print STDERR <<EOF;
-The program ‘$program’ is currently not installed. It is provided by
-several packages. You can install it by typing one of the following:
-EOF
-    print STDERR "  nix-env -iA nixos.$_->{package}\n" foreach @$res;
-}
-
-exit 127;
diff --git a/nixos/modules/programs/command-not-found/default.nix b/nixos/modules/programs/command-not-found/default.nix
new file mode 100644
index 00000000000..bbe949fa86a
--- /dev/null
+++ b/nixos/modules/programs/command-not-found/default.nix
@@ -0,0 +1,18 @@
+{ stdenv, rustPlatform, pkgconfig, sqlite
+, dbPath ? "/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite" }:
+
+rustPlatform.buildRustPackage {
+  name = "command-not-found";
+  src = ./rust;
+
+  DB_PATH = dbPath;
+  NIX_SYSTEM = stdenv.system;
+
+  postInstall = ''
+    strip $out/bin/command-not-found
+  '';
+
+  buildInputs = [ sqlite ];
+  nativeBuildInputs = [ pkgconfig ];
+  cargoSha256 = "13q61bb4b1q40g424pbssyp3ln79q1a33vmyz9s9wlqnac34cibd";
+}
diff --git a/nixos/modules/programs/command-not-found/rust/Cargo.lock b/nixos/modules/programs/command-not-found/rust/Cargo.lock
new file mode 100644
index 00000000000..ce3a9358c5c
--- /dev/null
+++ b/nixos/modules/programs/command-not-found/rust/Cargo.lock
@@ -0,0 +1,131 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "command-not-found"
+version = "0.1.0"
+dependencies = [
+ "rusqlite 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libc"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "lru-cache"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rusqlite"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libsqlite3-sys 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[metadata]
+"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+"checksum fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+"checksum fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+"checksum libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)" = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f"
+"checksum libsqlite3-sys 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1e704a02bcaecd4a08b93a23f6be59d0bd79cd161e0963e9499165a0a35df7bd"
+"checksum linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
+"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
+"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
+"checksum rusqlite 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "45d0fd62e1df63d254714e6cb40d0a0e82e7a1623e7a27f679d851af092ae58b"
+"checksum smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
+"checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
+"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
+"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/nixos/modules/programs/command-not-found/rust/Cargo.toml b/nixos/modules/programs/command-not-found/rust/Cargo.toml
new file mode 100644
index 00000000000..f965c7df76a
--- /dev/null
+++ b/nixos/modules/programs/command-not-found/rust/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "command-not-found"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+rusqlite = "0.*.*"
+
+[profile.release]
+lto = true
diff --git a/nixos/modules/programs/command-not-found/rust/src/main.rs b/nixos/modules/programs/command-not-found/rust/src/main.rs
new file mode 100644
index 00000000000..b0af2871cc2
--- /dev/null
+++ b/nixos/modules/programs/command-not-found/rust/src/main.rs
@@ -0,0 +1,52 @@
+use rusqlite::{params, Connection, Result};
+use std::env;
+use std::process::exit;
+
+const NIX_SYSTEM: &str = env!("NIX_SYSTEM");
+const DB_PATH: &str = env!("DB_PATH");
+
+fn query_packages(system: &str, program: &str) -> Result<Vec<String>> {
+    Ok(Connection::open(DB_PATH)?
+        .prepare("select package from Programs where system = ? and name = ?;")?
+        .query_map(params![system, program], |row| row.get("package"))?
+        .collect::<Result<Vec<String>>>()?)
+}
+
+fn run_app() -> i32 {
+    let args: Vec<_> = env::args().collect();
+    if args.len() < 2 {
+        eprintln!("USAGE: {} PROGRAMNAME", args[0]);
+        return 1;
+    }
+    let program = &args[1];
+    let system = env::var("NIX_SYSTEM").unwrap_or_else(|_| NIX_SYSTEM.to_string());
+    let packages = match query_packages(&system, program) {
+        Ok(packages) => packages,
+        Err(err) => {
+            eprintln!("Failed to query package database: {}", err);
+            return 1;
+        }
+    };
+    if packages.is_empty() {
+        eprintln!("{}: command not found", program);
+    } else {
+        let advice = if packages.len() > 1 {
+            "It is provided by several packages. You can install it by typing on of the of following commands:"
+        } else {
+            "You can install it by typing:"
+        };
+        eprintln!(
+            "The program '{}' is currently not installed. {}",
+            program, advice
+        );
+        for pkg in packages {
+            eprintln!("  nix-env -iA nixos.{}", pkg);
+        }
+    }
+
+    127
+}
+
+fn main() {
+    exit(run_app());
+}
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 6b8853ae390..9988f382a1b 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -12,7 +12,7 @@ let
     ${condOption "bind" cfg.bind}
     ${condOption "unixsocket" cfg.unixSocket}
     daemonize no
-    supervised systemd
+    #supervised systemd
     loglevel ${cfg.logLevel}
     logfile ${cfg.logfile}
     syslog-enabled ${redisBool cfg.syslog}
@@ -242,7 +242,9 @@ in
         ExecStart = "${cfg.package}/bin/redis-server /run/redis/redis.conf";
         RuntimeDirectory = "redis";
         StateDirectory = "redis";
-        Type = "notify";
+        TimeoutStartSec = "infinity";
+        TimeoutStopSec = "infinity";
+        Type = "simple";
         User = "redis";
         Group = "redis";
       };
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 1f2e13f3732..16fff215e61 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -62,6 +62,17 @@ let
     lovelace.mode = "yaml";
   };
 
+  #pythonScripts = pkgs.runCommand "python_scripts" {
+  #  nativeBuildInputs = [ pkgs.python3 ];
+  #  scripts = cfg.pythonScripts;
+  #} ''
+  #  mkdir $out
+  #  for s in $scripts; do
+  #    echo "checking syntax of $s"
+  #    python -m py_compile "$s"
+  #    ln -s "$s" "$out/$(basename $s"
+  #  done
+  #'';
 in {
   meta.maintainers = with maintainers; [ dotlambda ];
 
@@ -214,6 +225,17 @@ in {
       '';
     };
 
+    pythonScripts = mkOption {
+      #default = [];
+      #type = types.listOf types.path;
+      default = null;
+      type = types.nullOr types.path;
+      description = ''
+        List of python scripts to use in the <literal>python_scripts</literal> integration.
+        Also see in the <link xlink:href="https://www.home-assistant.io/integrations/python_script">Homeassistant documentation</link>
+      '';
+    };
+
     openFirewall = mkOption {
       default = false;
       type = types.bool;
@@ -224,6 +246,12 @@ in {
   config = mkIf cfg.enable {
     networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
 
+    systemd.tmpfiles.rules = mkIf (cfg.pythonScripts != null) [
+      "L+ ${cfg.configDir}/python_scripts - - - - ${cfg.pythonScripts}"
+    ];
+
+    services.home-assistant.config.python_script = mkIf (cfg.pythonScripts != null) {};
+
     systemd.services.home-assistant = {
       description = "Home Assistant";
       after = [ "network.target" ];
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 0eeff31d6c4..b0763c8d0de 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -11,6 +11,7 @@ let
   nixVersion = getVersion nix;
 
   isNix23 = versionAtLeast nixVersion "2.3pre";
+  isNix24 = versionAtLeast nixVersion "2.4pre";
 
   makeNixBuildUser = nr: {
     name  = "nixbld${toString nr}";
@@ -40,7 +41,11 @@ let
         max-jobs = ${toString (cfg.maxJobs)}
         cores = ${toString (cfg.buildCores)}
         sandbox = ${if (builtins.isBool cfg.useSandbox) then boolToString cfg.useSandbox else cfg.useSandbox}
-        extra-sandbox-paths = ${toString cfg.sandboxPaths}
+
+        ${optionalString (!isNix24) ''
+          extra-sandbox-paths = ${toString cfg.sandboxPaths}
+        ''}
+
         substituters = ${toString cfg.binaryCaches}
         trusted-substituters = ${toString cfg.trustedBinaryCaches}
         trusted-public-keys = ${toString cfg.binaryCachePublicKeys}
diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix
index e3b34cb0c61..7caae328203 100644
--- a/nixos/modules/services/networking/privoxy.nix
+++ b/nixos/modules/services/networking/privoxy.nix
@@ -16,7 +16,7 @@ let
     ${concatMapStrings (f: "actionsfile ${f}\n") cfg.actionsFiles}
     ${concatMapStrings (f: "filterfile ${f}\n") cfg.filterFiles}
   '' + optionalString cfg.enableTor ''
-    forward-socks4a / ${config.services.tor.client.socksListenAddressFaster} .
+    forward-socks5t / 127.0.0.1:9063 .
     toggle 1
     enable-remote-toggle 0
     enable-edit-actions 0
@@ -123,6 +123,11 @@ in
       serviceConfig.ProtectSystem = "full";
     };
 
+    services.tor.settings.SOCKSPort = mkIf cfg.enableTor [
+      # Route HTTP traffic over a faster port (without IsolateDestAddr).
+      { addr = "127.0.0.1"; port = 9063; IsolateDestAddr = false; }
+    ];
+
   };
 
   meta.maintainers = with lib.maintainers; [ rnhmjoj ];
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index 1cceee065b1..ececb633983 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -1,297 +1,300 @@
 { config, lib, pkgs, ... }:
 
+with builtins;
 with lib;
 
 let
   cfg = config.services.tor;
-  torDirectory = "/var/lib/tor";
-  torRunDirectory = "/run/tor";
-
-  opt    = name: value: optionalString (value != null) "${name} ${value}";
-  optint = name: value: optionalString (value != null && value != 0)    "${name} ${toString value}";
-
-  isolationOptions = {
-    type = types.listOf (types.enum [
-      "IsolateClientAddr"
-      "IsolateSOCKSAuth"
-      "IsolateClientProtocol"
-      "IsolateDestPort"
-      "IsolateDestAddr"
+  stateDir = "/var/lib/tor";
+  runDir = "/run/tor";
+  descriptionGeneric = option: ''
+    See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en#${option}">torrc manual</link>.
+  '';
+  bindsPrivilegedPort =
+    any (p0:
+      let p1 = if p0 ? "port" then p0.port else p0; in
+      if p1 == "auto" then false
+      else let p2 = if isInt p1 then p1 else toInt p1; in
+        p1 != null && 0 < p2 && p2 < 1024)
+    (flatten [
+      cfg.settings.ORPort
+      cfg.settings.DirPort
+      cfg.settings.DNSPort
+      cfg.settings.ExtORPort
+      cfg.settings.HTTPTunnelPort
+      cfg.settings.NATDPort
+      cfg.settings.SOCKSPort
+      cfg.settings.TransPort
     ]);
+  optionBool = optionName: mkOption {
+    type = with types; nullOr bool;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionInt = optionName: mkOption {
+    type = with types; nullOr int;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionString = optionName: mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionStrings = optionName: mkOption {
+    type = with types; listOf str;
     default = [];
-    example = [
-      "IsolateClientAddr"
-      "IsolateSOCKSAuth"
-      "IsolateClientProtocol"
-      "IsolateDestPort"
-      "IsolateDestAddr"
+    description = descriptionGeneric optionName;
+  };
+  optionAddress = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    example = "0.0.0.0";
+    description = ''
+      IPv4 or IPv6 (if between brackets) address.
+    '';
+  };
+  optionUnix = mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = ''
+      Unix domain socket path to use.
+    '';
+  };
+  optionPort = mkOption {
+    type = with types; nullOr (oneOf [port (enum ["auto"])]);
+    default = null;
+  };
+  optionPorts = optionName: mkOption {
+    type = with types; listOf port;
+    default = [];
+    description = descriptionGeneric optionName;
+  };
+  optionIsolablePort = with types; oneOf [
+    port (enum ["auto"])
+    (submodule ({config, ...}: {
+      options = {
+        addr = optionAddress;
+        port = optionPort;
+        flags = optionFlags;
+        SessionGroup = mkOption { type = nullOr int; default = null; };
+      } // genAttrs isolateFlags (name: mkOption { type = types.bool; default = false; });
+      config = {
+        flags = filter (name: config.${name} == true) isolateFlags ++
+                optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+      };
+    }))
+  ];
+  optionIsolablePorts = optionName: mkOption {
+    default = [];
+    type = with types; either optionIsolablePort (listOf optionIsolablePort);
+    description = descriptionGeneric optionName;
+  };
+  isolateFlags = [
+    "IsolateClientAddr"
+    "IsolateClientProtocol"
+    "IsolateDestAddr"
+    "IsolateDestPort"
+    "IsolateSOCKSAuth"
+    "KeepAliveIsolateSOCKSAuth"
+  ];
+  optionSOCKSPort = doConfig: let
+    flags = [
+      "CacheDNS" "CacheIPv4DNS" "CacheIPv6DNS" "GroupWritable" "IPv6Traffic"
+      "NoDNSRequest" "NoIPv4Traffic" "NoOnionTraffic" "OnionTrafficOnly"
+      "PreferIPv6" "PreferIPv6Automap" "PreferSOCKSNoAuth" "UseDNSCache"
+      "UseIPv4Cache" "UseIPv6Cache" "WorldWritable"
+    ] ++ isolateFlags;
+    in with types; oneOf [
+      port (submodule ({config, ...}: {
+        options = {
+          unix = optionUnix;
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+          SessionGroup = mkOption { type = nullOr int; default = null; };
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = mkIf doConfig { # Only add flags in SOCKSPort to avoid duplicates
+          flags = filter (name: config.${name} == true) flags ++
+                  optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+        };
+      }))
     ];
-    description = "Tor isolation options";
+  optionFlags = mkOption {
+    type = with types; listOf str;
+    default = [];
+  };
+  optionORPort = optionName: mkOption {
+    default = [];
+    example = 443;
+    type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+      port
+      (enum ["auto"])
+      (submodule ({config, ...}:
+        let flags = [ "IPv4Only" "IPv6Only" "NoAdvertise" "NoListen" ];
+        in {
+        options = {
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = {
+          flags = filter (name: config.${name} == true) flags;
+        };
+      }))
+    ]))];
+    description = descriptionGeneric optionName;
+  };
+  optionBandwith = optionName: mkOption {
+    type = with types; nullOr (either int str);
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionPath = optionName: mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = descriptionGeneric optionName;
   };
 
-
-  torRc = ''
-    User tor
-    DataDirectory ${torDirectory}
-    ${optionalString cfg.enableGeoIP ''
-      GeoIPFile ${cfg.package.geoip}/share/tor/geoip
-      GeoIPv6File ${cfg.package.geoip}/share/tor/geoip6
-    ''}
-
-    ${optint "ControlPort" cfg.controlPort}
-    ${optionalString cfg.controlSocket.enable "ControlPort unix:${torRunDirectory}/control GroupWritable RelaxDirModeCheck"}
-  ''
-  # Client connection config
-  + optionalString cfg.client.enable ''
-    SOCKSPort ${cfg.client.socksListenAddress} ${toString cfg.client.socksIsolationOptions}
-    SOCKSPort ${cfg.client.socksListenAddressFaster}
-    ${opt "SocksPolicy" cfg.client.socksPolicy}
-
-    ${optionalString cfg.client.transparentProxy.enable ''
-    TransPort ${cfg.client.transparentProxy.listenAddress} ${toString cfg.client.transparentProxy.isolationOptions}
-    ''}
-
-    ${optionalString cfg.client.dns.enable ''
-    DNSPort ${cfg.client.dns.listenAddress} ${toString cfg.client.dns.isolationOptions}
-    AutomapHostsOnResolve 1
-    AutomapHostsSuffixes ${concatStringsSep "," cfg.client.dns.automapHostsSuffixes}
-    ''}
-  ''
-  # Explicitly disable the SOCKS server if the client is disabled.  In
-  # particular, this makes non-anonymous hidden services possible.
-  + optionalString (! cfg.client.enable) ''
-  SOCKSPort 0
-  ''
-  # Relay config
-  + optionalString cfg.relay.enable ''
-    ORPort ${toString cfg.relay.port}
-    ${opt "Address" cfg.relay.address}
-    ${opt "Nickname" cfg.relay.nickname}
-    ${opt "ContactInfo" cfg.relay.contactInfo}
-
-    ${optint "RelayBandwidthRate" cfg.relay.bandwidthRate}
-    ${optint "RelayBandwidthBurst" cfg.relay.bandwidthBurst}
-    ${opt "AccountingMax" cfg.relay.accountingMax}
-    ${opt "AccountingStart" cfg.relay.accountingStart}
-
-    ${if (cfg.relay.role == "exit") then
-        opt "ExitPolicy" cfg.relay.exitPolicy
-      else
-        "ExitPolicy reject *:*"}
-
-    ${optionalString (elem cfg.relay.role ["bridge" "private-bridge"]) ''
-      BridgeRelay 1
-      ServerTransportPlugin ${concatStringsSep "," cfg.relay.bridgeTransports} exec ${pkgs.obfs4}/bin/obfs4proxy managed
-      ExtORPort auto
-      ${optionalString (cfg.relay.role == "private-bridge") ''
-        ExtraInfoStatistics 0
-        PublishServerDescriptor 0
-      ''}
-    ''}
-  ''
-  # Hidden services
-  + concatStrings (flip mapAttrsToList cfg.hiddenServices (n: v: ''
-    HiddenServiceDir ${torDirectory}/onion/${v.name}
-    ${optionalString (v.version != null) "HiddenServiceVersion ${toString v.version}"}
-    ${flip concatMapStrings v.map (p: ''
-      HiddenServicePort ${toString p.port} ${p.destination}
-    '')}
-    ${optionalString (v.authorizeClient != null) ''
-      HiddenServiceAuthorizeClient ${v.authorizeClient.authType} ${concatStringsSep "," v.authorizeClient.clientNames}
-    ''}
-  ''))
-  + cfg.extraConfig;
-
-  torRcFile = pkgs.writeText "torrc" torRc;
-
+  mkValueString = k: v:
+    if v == null then ""
+    else if isBool v then
+      (if v then "1" else "0")
+    else if v ? "unix" && v.unix != null then
+      "unix:"+v.unix +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if v ? "port" && v.port != null then
+      optionalString (v ? "addr" && v.addr != null) "${v.addr}:" +
+      toString v.port +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if k == "ServerTransportPlugin" then
+      optionalString (v.transports != []) "${concatStringsSep "," v.transports} exec ${v.exec}"
+    else if k == "HidServAuth" then
+      concatMapStringsSep "\n${k} " (settings: settings.onion + " " settings.auth) v
+    else generators.mkValueStringDefault {} v;
+  genTorrc = settings:
+    generators.toKeyValue {
+      listsAsDuplicateKeys = true;
+      mkKeyValue = k: generators.mkKeyValueDefault { mkValueString = mkValueString k; } " " k;
+    }
+    (lib.mapAttrs (k: v:
+      # Not necesssary, but prettier rendering
+      if elem k [ "AutomapHostsSuffixes" "DirPolicy" "ExitPolicy" "SocksPolicy" ]
+      && v != []
+      then concatStringsSep "," v
+      else v)
+    (lib.filterAttrs (k: v: !(v == null || v == ""))
+    settings));
+  torrc = pkgs.writeText "torrc" (
+    genTorrc cfg.settings +
+    concatStrings (mapAttrsToList (name: onion:
+      "HiddenServiceDir ${onion.path}\n" +
+      genTorrc onion.settings) cfg.relay.onionServices)
+  );
 in
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "tor" "client" "privoxy" "enable" ] ''
-      Use services.privoxy.enable and services.privoxy.enableTor instead.
-    '')
-    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "relay" "port" ])
+    (mkRenamedOptionModule [ "services" "tor" "client" "dns" "automapHostsSuffixes" ] [ "services" "tor" "settings" "AutomapHostsSuffixes" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "isolationOptions" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "listenAddress" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "privoxy" "enable" ] "Use services.privoxy.enable and services.privoxy.enableTor instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksIsolationOptions" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksListenAddressFaster" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "client" "socksPolicy" ] [ "services" "tor" "settings" "SocksPolicy" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "isolationOptions" ] "Use services.tor.settings.TransPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "listenAddress" ] "Use services.tor.settings.TransPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "controlPort" ] [ "services" "tor" "settings" "ControlPort" ])
+    (mkRemovedOptionModule [ "services" "tor" "extraConfig" ] "Plese use services.tor.settings instead.")
+    (mkRenamedOptionModule [ "services" "tor" "hiddenServices" ] [ "services" "tor" "relay" "onionServices" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingMax" ] [ "services" "tor" "settings" "AccountingMax" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingStart" ] [ "services" "tor" "settings" "AccountingStart" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "address" ] [ "services" "tor" "settings" "Address" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthBurst" ] [ "services" "tor" "settings" "BandwidthBurst" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthRate" ] [ "services" "tor" "settings" "BandwidthRate" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bridgeTransports" ] [ "services" "tor" "settings" "ServerTransportPlugin" "transports" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "contactInfo" ] [ "services" "tor" "settings" "ContactInfo" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "exitPolicy" ] [ "services" "tor" "settings" "ExitPolicy" ])
     (mkRemovedOptionModule [ "services" "tor" "relay" "isBridge" ] "Use services.tor.relay.role instead.")
     (mkRemovedOptionModule [ "services" "tor" "relay" "isExit" ] "Use services.tor.relay.role instead.")
+    (mkRenamedOptionModule [ "services" "tor" "relay" "nickname" ] [ "services" "tor" "settings" "Nickname" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "port" ] [ "services" "tor" "settings" "ORPort" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "settings" "ORPort" ])
   ];
 
   options = {
     services.tor = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable the Tor daemon. By default, the daemon is run without
-          relay, exit, bridge or client connectivity.
-        '';
-      };
+      enable = mkEnableOption ''Tor daemon.
+        By default, the daemon is run without
+        relay, exit, bridge or client connectivity'';
+
+      openFirewall = mkEnableOption "opening of the relay port(s) in the firewall";
 
       package = mkOption {
         type = types.package;
         default = pkgs.tor;
         defaultText = "pkgs.tor";
         example = literalExample "pkgs.tor";
-        description = ''
-          Tor package to use
-        '';
+        description = "Tor package to use.";
       };
 
-      enableGeoIP = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whenever to configure Tor daemon to use GeoIP databases.
+      enableGeoIP = mkEnableOption ''use of GeoIP databases.
+        Disabling this will disable by-country statistics for bridges and relays
+        and some client and third-party software functionality'' // { default = true; };
 
-          Disabling this will disable by-country statistics for
-          bridges and relays and some client and third-party software
-          functionality.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Extra configuration. Contents will be added verbatim to the
-          configuration file at the end.
-        '';
-      };
-
-      controlPort = mkOption {
-        type = types.nullOr (types.either types.int types.str);
-        default = null;
-        example = 9051;
-        description = ''
-          If set, Tor will accept connections on the specified port
-          and allow them to control the tor process.
-        '';
-      };
-
-      controlSocket = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable Tor control socket. Control socket is created
-            in <literal>${torRunDirectory}/control</literal>
-          '';
-        };
-      };
+      controlSocket.enable = mkEnableOption ''control socket,
+        created in <literal>${runDir}/control</literal>'';
 
       client = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable Tor daemon to route application
-            connections.  You might want to disable this if you plan
-            running a dedicated Tor relay.
-          '';
-        };
+        enable = mkEnableOption ''the routing of application connections.
+          You might want to disable this if you plan running a dedicated Tor relay'';
 
-        socksListenAddress = mkOption {
-          type = types.str;
-          default = "127.0.0.1:9050";
-          example = "192.168.0.1:9100";
-          description = ''
-            Bind to this address to listen for connections from
-            Socks-speaking applications. Provides strong circuit
-            isolation, separate circuit per IP address.
-          '';
-        };
+        transparentProxy.enable = mkEnableOption "transparent proxy";
+        dns.enable = mkEnableOption "DNS resolver";
 
-        socksListenAddressFaster = mkOption {
-          type = types.str;
-          default = "127.0.0.1:9063";
-          example = "192.168.0.1:9101";
+        socksListenAddress = mkOption {
+          type = optionSOCKSPort false;
+          default = {addr = "127.0.0.1"; port = 9050; IsolateDestAddr = true;};
+          example = {addr = "192.168.0.1"; port = 9090; IsolateDestAddr = true;};
           description = ''
             Bind to this address to listen for connections from
-            Socks-speaking applications. Same as
-            <option>socksListenAddress</option> but uses weaker
-            circuit isolation to provide performance suitable for a
-            web browser.
-           '';
-         };
-
-        socksPolicy = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "accept 192.168.0.0/16, reject *";
-          description = ''
-            Entry policies to allow/deny SOCKS requests based on IP
-            address. First entry that matches wins. If no SocksPolicy
-            is set, we accept all (and only) requests from
-            <option>socksListenAddress</option>.
+            Socks-speaking applications.
           '';
         };
 
-        socksIsolationOptions = mkOption (isolationOptions // {
-          default = ["IsolateDestAddr"];
-        });
-
-        transparentProxy = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = "Whether to enable tor transparent proxy";
-          };
-
-          listenAddress = mkOption {
-            type = types.str;
-            default = "127.0.0.1:9040";
-            example = "192.168.0.1:9040";
-            description = ''
-              Bind transparent proxy to this address.
-            '';
-          };
-
-          isolationOptions = mkOption isolationOptions;
-        };
-
-        dns = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = "Whether to enable tor dns resolver";
-          };
-
-          listenAddress = mkOption {
-            type = types.str;
-            default = "127.0.0.1:9053";
-            example = "192.168.0.1:9053";
-            description = ''
-              Bind tor dns to this address.
-            '';
-          };
-
-          isolationOptions = mkOption isolationOptions;
-
-          automapHostsSuffixes = mkOption {
-            type = types.listOf types.str;
-            default = [".onion" ".exit"];
-            example = [".onion"];
-            description = "List of suffixes to use with automapHostsOnResolve";
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" = {
+              clientAuthorizations = ["/run/keys/tor/alice.prv.x25519"];
+            };
           };
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.clientAuthorizations = mkOption {
+              description = ''
+                Clients' authorizations for a v3 hidden service,
+                as a list of files containing each one private key, in the format:
+                <screen>descriptor:x25519:&lt;base32-private-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf path;
+              default = [];
+              example = ["/run/keys/tor/alice.prv.x25519"];
+            };
+          }));
         };
       };
 
       relay = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable relaying TOR traffic for others.
+        enable = mkEnableOption ''relaying of Tor traffic for others.
 
-            See <link xlink:href="https://www.torproject.org/docs/tor-doc-relay" />
-            for details.
+          See <link xlink:href="https://www.torproject.org/docs/tor-doc-relay" />
+          for details.
 
-            Setting this to true requires setting
-            <option>services.tor.relay.role</option>
-            and
-            <option>services.tor.relay.port</option>
-            options.
-          '';
-        };
+          Setting this to true requires setting
+          <option>services.tor.relay.role</option>
+          and
+          <option>services.tor.settings.ORPort</option>
+          options'';
 
         role = mkOption {
           type = types.enum [ "exit" "relay" "bridge" "private-bridge" ];
@@ -310,13 +313,13 @@ in
                 <important><para>
                   Running an exit relay may expose you to abuse
                   complaints. See
-                  <link xlink:href="https://www.torproject.org/faq.html.en#ExitPolicies" />
+                  <link xlink:href="https://www.torproject.org/faq.html.en#ExitPolicies"/>
                   for more info.
                 </para></important>
 
                 <para>
                   You can specify which services Tor users may access via
-                  your exit relay using <option>exitPolicy</option> option.
+                  your exit relay using <option>settings.ExitPolicy</option> option.
                 </para>
               </listitem>
             </varlistentry>
@@ -369,15 +372,14 @@ in
                 <important>
                   <para>
                     WARNING: THE FOLLOWING PARAGRAPH IS NOT LEGAL ADVICE.
-                    Consult with your lawer when in doubt.
+                    Consult with your lawyer when in doubt.
                   </para>
 
                   <para>
                     This role should be safe to use in most situations
                     (unless the act of forwarding traffic for others is
                     a punishable offence under your local laws, which
-                    would be pretty insane as it would make ISP
-                    illegal).
+                    would be pretty insane as it would make ISP illegal).
                   </para>
                 </important>
 
@@ -404,7 +406,7 @@ in
 
                 <para>
                   Use this if you want to run a private bridge, for
-                  example because you'll give out your bridge address
+                  example because you'll give out your bridge addr
                   manually to your friends.
                 </para>
 
@@ -426,269 +428,393 @@ in
           '';
         };
 
-        bridgeTransports = mkOption {
-          type = types.listOf types.str;
-          default = ["obfs4"];
-          example = ["obfs2" "obfs3" "obfs4" "scramblesuit"];
-          description = "List of pluggable transports";
-        };
-
-        nickname = mkOption {
-          type = types.str;
-          default = "anonymous";
-          description = ''
-            A unique handle for your TOR relay.
-          '';
-        };
-
-        contactInfo = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "admin@relay.com";
-          description = ''
-            Contact information for the relay owner (e.g. a mail
-            address and GPG key ID).
-          '';
-        };
-
-        accountingMax = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "450 GBytes";
-          description = ''
-            Specify maximum bandwidth allowed during an accounting period. This
-            allows you to limit overall tor bandwidth over some time period.
-            See the <literal>AccountingMax</literal> option by looking at the
-            tor manual <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more.
-
-            Note this limit applies individually to upload and
-            download; if you specify <literal>"500 GBytes"</literal>
-            here, then you may transfer up to 1 TBytes of overall
-            bandwidth (500 GB upload, 500 GB download).
-          '';
-        };
-
-        accountingStart = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "month 1 1:00";
-          description = ''
-            Specify length of an accounting period. This allows you to limit
-            overall tor bandwidth over some time period. See the
-            <literal>AccountingStart</literal> option by looking at the tor
-            manual <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more.
-          '';
-        };
-
-        bandwidthRate = mkOption {
-          type = types.nullOr types.int;
-          default = null;
-          example = 100;
-          description = ''
-            Specify this to limit the bandwidth usage of relayed (server)
-            traffic. Your own traffic is still unthrottled. Units: bytes/second.
-          '';
-        };
-
-        bandwidthBurst = mkOption {
-          type = types.nullOr types.int;
-          default = cfg.relay.bandwidthRate;
-          example = 200;
-          description = ''
-            Specify this to allow bursts of the bandwidth usage of relayed (server)
-            traffic. The average usage will still be as specified in relayBandwidthRate.
-            Your own traffic is still unthrottled. Units: bytes/second.
-          '';
-        };
-
-        address = mkOption {
-          type    = types.nullOr types.str;
-          default = null;
-          example = "noname.example.com";
-          description = ''
-            The IP address or full DNS name for advertised address of your relay.
-            Leave unset and Tor will guess.
-          '';
-        };
-
-        port = mkOption {
-          type    = types.either types.int types.str;
-          example = 143;
-          description = ''
-            What port to advertise for Tor connections. This corresponds to the
-            <literal>ORPort</literal> section in the Tor manual; see
-            <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more details.
-
-            At a minimum, you should just specify the port for the
-            relay to listen on; a common one like 143, 22, 80, or 443
-            to help Tor users who may have very restrictive port-based
-            firewalls.
-          '';
-        };
-
-        exitPolicy = mkOption {
-          type    = types.nullOr types.str;
-          default = null;
-          example = "accept *:6660-6667,reject *:*";
-          description = ''
-            A comma-separated list of exit policies. They're
-            considered first to last, and the first match wins. If you
-            want to _replace_ the default exit policy, end this with
-            either a reject *:* or an accept *:*. Otherwise, you're
-            _augmenting_ (prepending to) the default exit policy.
-            Leave commented to just use the default, which is
-            available in the man page or at
-            <link xlink:href="https://www.torproject.org/documentation.html" />.
-
-            Look at
-            <link xlink:href="https://www.torproject.org/faq-abuse.html#TypicalAbuses" />
-            for issues you might encounter if you use the default
-            exit policy.
-
-            If certain IPs and ports are blocked externally, e.g. by
-            your firewall, you should update your exit policy to
-            reflect this -- otherwise Tor users will be told that
-            those destinations are down.
-          '';
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "example.org/www" = {
+              map = [ 80 ];
+              authorizedClients = [
+                "descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+              ];
+            };
+          };
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.path = mkOption {
+              type = types.path;
+              description = ''
+                Path where to store the data files of the hidden service.
+                If the <option>secretKey</option> is null
+                this defaults to <literal>${stateDir}/onion/$onion</literal>,
+                otherwise to <literal>${runDir}/onion/$onion</literal>.
+              '';
+            };
+            options.secretKey = mkOption {
+              type = with types; nullOr path;
+              default = null;
+              example = "/run/keys/tor/onion/expyuzz4wqqyqhjn/hs_ed25519_secret_key";
+              description = ''
+                Secret key of the onion service.
+                If null, Tor reuses any preexisting secret key (in <option>path</option>)
+                or generates a new one.
+                The associated public key and hostname are deterministically regenerated
+                from this file if they do not exist.
+              '';
+            };
+            options.authorizeClient = mkOption {
+              description = descriptionGeneric "HiddenServiceAuthorizeClient";
+              default = null;
+              type = types.nullOr (types.submodule ({...}: {
+                options = {
+                  authType = mkOption {
+                    type = types.enum [ "basic" "stealth" ];
+                    description = ''
+                      Either <literal>"basic"</literal> for a general-purpose authorization protocol
+                      or <literal>"stealth"</literal> for a less scalable protocol
+                      that also hides service activity from unauthorized clients.
+                    '';
+                  };
+                  clientNames = mkOption {
+                    type = with types; nonEmptyListOf (strMatching "[A-Za-z0-9+-_]+");
+                    description = ''
+                      Only clients that are listed here are authorized to access the hidden service.
+                      Generated authorization data can be found in <filename>${stateDir}/onion/$name/hostname</filename>.
+                      Clients need to put this authorization data in their configuration file using
+                      <xref linkend="opt-services.tor.settings.HidServAuth"/>.
+                    '';
+                  };
+                };
+              }));
+            };
+            options.authorizedClients = mkOption {
+              description = ''
+                Authorized clients for a v3 hidden service,
+                as a list of public key, in the format:
+                <screen>descriptor:x25519:&lt;base32-public-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf str;
+              default = [];
+              example = ["descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"];
+            };
+            options.map = mkOption {
+              description = descriptionGeneric "HiddenServicePort";
+              type = with types; listOf (oneOf [
+                port (submodule ({...}: {
+                  options = {
+                    port = optionPort;
+                    target = mkOption {
+                      default = null;
+                      type = nullOr (submodule ({...}: {
+                        options = {
+                          unix = optionUnix;
+                          addr = optionAddress;
+                          port = optionPort;
+                        };
+                      }));
+                    };
+                  };
+                }))
+              ]);
+              apply = map (v: if isInt v then {port=v; target=null;} else v);
+            };
+            options.version = mkOption {
+              description = descriptionGeneric "HiddenServiceVersion";
+              type = with types; nullOr (enum [2 3]);
+              default = null;
+            };
+            options.settings = mkOption {
+              description = ''
+                Settings of the onion service.
+              '' + descriptionGeneric "_hidden_service_options";
+              default = {};
+              type = types.submodule {
+                freeformType = with types;
+                  (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+                    description = "settings option";
+                  };
+                options.HiddenServiceAllowUnknownPorts = optionBool "HiddenServiceAllowUnknownPorts";
+                options.HiddenServiceDirGroupReadable = optionBool "HiddenServiceDirGroupReadable";
+                options.HiddenServiceExportCircuitID = mkOption {
+                  description = descriptionGeneric "HiddenServiceExportCircuitID";
+                  type = with types; nullOr (enum ["haproxy"]);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreams = mkOption {
+                  description = descriptionGeneric "HiddenServiceMaxStreams";
+                  type = with types; nullOr (ints.between 0 65535);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreamsCloseCircuit = optionBool "HiddenServiceMaxStreamsCloseCircuit";
+                options.HiddenServiceNumIntroductionPoints = mkOption {
+                  description = descriptionGeneric "HiddenServiceNumIntroductionPoints";
+                  type = with types; nullOr (ints.between 0 20);
+                  default = null;
+                };
+                options.HiddenServiceSingleHopMode = optionBool "HiddenServiceSingleHopMode";
+                options.RendPostPeriod = optionString "RendPostPeriod";
+              };
+            };
+            config = {
+              path = mkDefault ((if config.secretKey == null then stateDir else runDir) + "/onion/${name}");
+              settings.HiddenServiceVersion = config.version;
+              settings.HiddenServiceAuthorizeClient =
+                if config.authorizeClient != null then
+                  config.authorizeClient.authType + " " +
+                  concatStringsSep "," config.authorizeClient.clientNames
+                else null;
+              settings.HiddenServicePort = map (p: mkValueString "" p.port + " " + mkValueString "" p.target) config.map;
+            };
+          }));
         };
       };
 
-      hiddenServices = mkOption {
+      settings = mkOption {
         description = ''
-          A set of static hidden services that terminate their Tor
-          circuits at this node.
-
-          Every element in this set declares a virtual onion host.
-
-          You can specify your onion address by putting corresponding
-          private key to an appropriate place in ${torDirectory}.
-
-          For services without private keys in ${torDirectory} Tor
-          daemon will generate random key pairs (which implies random
-          onion addresses) on restart. The latter could take a while,
-          please be patient.
-
-          <note><para>
-            Hidden services can be useful even if you don't intend to
-            actually <emphasis>hide</emphasis> them, since they can
-            also be seen as a kind of NAT traversal mechanism.
-
-            E.g. the example will make your sshd, whatever runs on
-            "8080" and your mail server available from anywhere where
-            the Tor network is available (which, with the help from
-            bridges, is pretty much everywhere), even if both client
-            and server machines are behind NAT you have no control
-            over.
-          </para></note>
+          See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en">torrc manual</link>
+          for documentation.
         '';
         default = {};
-        example = literalExample ''
-          { "my-hidden-service-example".map = [
-              { port = 22; }                # map ssh port to this machine's ssh
-              { port = 80; toPort = 8080; } # map http port to whatever runs on 8080
-              { port = "sip"; toHost = "mail.example.com"; toPort = "imap"; } # because we can
-            ];
-          }
-        '';
-        type = types.attrsOf (types.submodule ({name, ...}: {
-          options = {
-
-             name = mkOption {
-               type = types.str;
-               description = ''
-                 Name of this tor hidden service.
-
-                 This is purely descriptive.
-
-                 After restarting Tor daemon you should be able to
-                 find your .onion address in
-                 <literal>${torDirectory}/onion/$name/hostname</literal>.
-               '';
-             };
-
-             map = mkOption {
-               default = [];
-               description = "Port mapping for this hidden service.";
-               type = types.listOf (types.submodule ({config, ...}: {
-                 options = {
-
-                   port = mkOption {
-                     type = types.either types.int types.str;
-                     example = 80;
-                     description = ''
-                       Hidden service port to "bind to".
-                     '';
-                   };
-
-                   destination = mkOption {
-                     internal = true;
-                     type = types.str;
-                     description = "Forward these connections where?";
-                   };
-
-                   toHost = mkOption {
-                     type = types.str;
-                     default = "127.0.0.1";
-                     description = "Mapping destination host.";
-                   };
-
-                   toPort = mkOption {
-                     type = types.either types.int types.str;
-                     example = 8080;
-                     description = "Mapping destination port.";
-                   };
-
-                 };
-
-                 config = {
-                   toPort = mkDefault config.port;
-                   destination = mkDefault "${config.toHost}:${toString config.toPort}";
-                 };
-               }));
-             };
-
-             authorizeClient = mkOption {
-               default = null;
-               description = "If configured, the hidden service is accessible for authorized clients only.";
-               type = types.nullOr (types.submodule ({...}: {
-
-                 options = {
-
-                   authType = mkOption {
-                     type = types.enum [ "basic" "stealth" ];
-                     description = ''
-                       Either <literal>"basic"</literal> for a general-purpose authorization protocol
-                       or <literal>"stealth"</literal> for a less scalable protocol
-                       that also hides service activity from unauthorized clients.
-                     '';
-                   };
-
-                   clientNames = mkOption {
-                     type = types.nonEmptyListOf (types.strMatching "[A-Za-z0-9+-_]+");
-                     description = ''
-                       Only clients that are listed here are authorized to access the hidden service.
-                       Generated authorization data can be found in <filename>${torDirectory}/onion/$name/hostname</filename>.
-                       Clients need to put this authorization data in their configuration file using <literal>HidServAuth</literal>.
-                     '';
-                   };
-                 };
-               }));
-             };
-
-             version = mkOption {
-               default = null;
-               description = "Rendezvous service descriptor version to publish for the hidden service. Currently, versions 2 and 3 are supported. (Default: 2)";
-               type = types.nullOr (types.enum [ 2 3 ]);
-             };
+        type = types.submodule {
+          freeformType = with types;
+            (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+              description = "settings option";
+            };
+          options.Address = optionString "Address";
+          options.AssumeReachable = optionBool "AssumeReachable";
+          options.AccountingMax = optionBandwith "AccountingMax";
+          options.AccountingStart = optionString "AccountingStart";
+          options.AuthDirHasIPv6Connectivity = optionBool "AuthDirHasIPv6Connectivity";
+          options.AuthDirListBadExits = optionBool "AuthDirListBadExits";
+          options.AuthDirPinKeys = optionBool "AuthDirPinKeys";
+          options.AuthDirSharedRandomness = optionBool "AuthDirSharedRandomness";
+          options.AuthDirTestEd25519LinkKeys = optionBool "AuthDirTestEd25519LinkKeys";
+          options.AuthoritativeDirectory = optionBool "AuthoritativeDirectory";
+          options.AutomapHostsOnResolve = optionBool "AutomapHostsOnResolve";
+          options.AutomapHostsSuffixes = optionStrings "AutomapHostsSuffixes" // {
+            default = [".onion" ".exit"];
+            example = [".onion"];
           };
-
-          config = {
-            name = mkDefault name;
+          options.BandwidthBurst = optionBandwith "BandwidthBurst";
+          options.BandwidthRate = optionBandwith "BandwidthRate";
+          options.BridgeAuthoritativeDir = optionBool "BridgeAuthoritativeDir";
+          options.BridgeRecordUsageByCountry = optionBool "BridgeRecordUsageByCountry";
+          options.BridgeRelay = optionBool "BridgeRelay" // { default = false; };
+          options.CacheDirectory = optionPath "CacheDirectory";
+          options.CacheDirectoryGroupReadable = optionBool "CacheDirectoryGroupReadable"; # default is null and like "auto"
+          options.CellStatistics = optionBool "CellStatistics";
+          options.ClientAutoIPv6ORPort = optionBool "ClientAutoIPv6ORPort";
+          options.ClientDNSRejectInternalAddresses = optionBool "ClientDNSRejectInternalAddresses";
+          options.ClientOnionAuthDir = mkOption {
+            description = descriptionGeneric "ClientOnionAuthDir";
+            default = null;
+            type = with types; nullOr path;
+          };
+          options.ClientPreferIPv6DirPort = optionBool "ClientPreferIPv6DirPort"; # default is null and like "auto"
+          options.ClientPreferIPv6ORPort = optionBool "ClientPreferIPv6ORPort"; # default is null and like "auto"
+          options.ClientRejectInternalAddresses = optionBool "ClientRejectInternalAddresses";
+          options.ClientUseIPv4 = optionBool "ClientUseIPv4";
+          options.ClientUseIPv6 = optionBool "ClientUseIPv6";
+          options.ConnDirectionStatistics = optionBool "ConnDirectionStatistics";
+          options.ConstrainedSockets = optionBool "ConstrainedSockets";
+          options.ContactInfo = optionString "ContactInfo";
+          options.ControlPort = mkOption rec {
+            description = descriptionGeneric "ControlPort";
+            default = [];
+            example = [{port = 9051;}];
+            type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+              port (enum ["auto"]) (submodule ({config, ...}: let
+                flags = ["GroupWritable" "RelaxDirModeCheck" "WorldWritable"];
+                in {
+                options = {
+                  unix = optionUnix;
+                  flags = optionFlags;
+                  addr = optionAddress;
+                  port = optionPort;
+                } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+                config = {
+                  flags = filter (name: config.${name} == true) flags;
+                };
+              }))
+            ]))];
+          };
+          options.ControlPortFileGroupReadable= optionBool "ControlPortFileGroupReadable";
+          options.ControlPortWriteToFile = optionPath "ControlPortWriteToFile";
+          options.ControlSocket = optionPath "ControlSocket";
+          options.ControlSocketsGroupWritable = optionBool "ControlSocketsGroupWritable";
+          options.CookieAuthFile = optionPath "CookieAuthFile";
+          options.CookieAuthFileGroupReadable = optionBool "CookieAuthFileGroupReadable";
+          options.CookieAuthentication = optionBool "CookieAuthentication";
+          options.DataDirectory = optionPath "DataDirectory" // { default = stateDir; };
+          options.DataDirectoryGroupReadable = optionBool "DataDirectoryGroupReadable";
+          options.DirPortFrontPage = optionPath "DirPortFrontPage";
+          options.DirAllowPrivateAddresses = optionBool "DirAllowPrivateAddresses";
+          options.DormantCanceledByStartup = optionBool "DormantCanceledByStartup";
+          options.DormantOnFirstStartup = optionBool "DormantOnFirstStartup";
+          options.DormantTimeoutDisabledByIdleStreams = optionBool "DormantTimeoutDisabledByIdleStreams";
+          options.DirCache = optionBool "DirCache";
+          options.DirPolicy = mkOption {
+            description = descriptionGeneric "DirPolicy";
+            type = with types; listOf str;
+            default = [];
+            example = ["accept *:*"];
+          };
+          options.DirPort = optionORPort "DirPort";
+          options.DirReqStatistics = optionBool "DirReqStatistics";
+          options.DisableAllSwap = optionBool "DisableAllSwap";
+          options.DisableDebuggerAttachment = optionBool "DisableDebuggerAttachment";
+          options.DisableNetwork = optionBool "DisableNetwork";
+          options.DisableOOSCheck = optionBool "DisableOOSCheck";
+          options.DNSPort = optionIsolablePorts "DNSPort";
+          options.DoSCircuitCreationEnabled = optionBool "DoSCircuitCreationEnabled";
+          options.DoSConnectionEnabled = optionBool "DoSConnectionEnabled"; # default is null and like "auto"
+          options.DoSRefuseSingleHopClientRendezvous = optionBool "DoSRefuseSingleHopClientRendezvous";
+          options.DownloadExtraInfo = optionBool "DownloadExtraInfo";
+          options.EnforceDistinctSubnets = optionBool "EnforceDistinctSubnets";
+          options.EntryStatistics = optionBool "EntryStatistics";
+          options.ExitPolicy = optionStrings "ExitPolicy" // {
+            default = ["reject *:*"];
+            example = ["accept *:*"];
+          };
+          options.ExitPolicyRejectLocalInterfaces = optionBool "ExitPolicyRejectLocalInterfaces";
+          options.ExitPolicyRejectPrivate = optionBool "ExitPolicyRejectPrivate";
+          options.ExitPortStatistics = optionBool "ExitPortStatistics";
+          options.ExitRelay = optionBool "ExitRelay"; # default is null and like "auto"
+          options.ExtORPort = mkOption {
+            description = descriptionGeneric "ExtORPort";
+            default = null;
+            type = with types; nullOr (oneOf [
+              port (enum ["auto"]) (submodule ({...}: {
+                options = {
+                  addr = optionAddress;
+                  port = optionPort;
+                };
+              }))
+            ]);
+            apply = p: if isInt p || isString p then { port = p; } else p;
           };
-        }));
+          options.ExtORPortCookieAuthFile = optionPath "ExtORPortCookieAuthFile";
+          options.ExtORPortCookieAuthFileGroupReadable = optionBool "ExtORPortCookieAuthFileGroupReadable";
+          options.ExtendAllowPrivateAddresses = optionBool "ExtendAllowPrivateAddresses";
+          options.ExtraInfoStatistics = optionBool "ExtraInfoStatistics";
+          options.FascistFirewall = optionBool "FascistFirewall";
+          options.FetchDirInfoEarly = optionBool "FetchDirInfoEarly";
+          options.FetchDirInfoExtraEarly = optionBool "FetchDirInfoExtraEarly";
+          options.FetchHidServDescriptors = optionBool "FetchHidServDescriptors";
+          options.FetchServerDescriptors = optionBool "FetchServerDescriptors";
+          options.FetchUselessDescriptors = optionBool "FetchUselessDescriptors";
+          options.ReachableAddresses = optionStrings "ReachableAddresses";
+          options.ReachableDirAddresses = optionStrings "ReachableDirAddresses";
+          options.ReachableORAddresses = optionStrings "ReachableORAddresses";
+          options.GeoIPFile = optionPath "GeoIPFile";
+          options.GeoIPv6File = optionPath "GeoIPv6File";
+          options.GuardfractionFile = optionPath "GuardfractionFile";
+          options.HidServAuth = mkOption {
+            description = descriptionGeneric "HidServAuth";
+            default = [];
+            type = with types; listOf (oneOf [
+              (submodule {
+                options = {
+                  onion = mkOption {
+                    type = strMatching "[a-z2-7]{16}(\\.onion)?";
+                    description = "Onion address.";
+                    example = "xxxxxxxxxxxxxxxx.onion";
+                  };
+                  auth = mkOption {
+                    type = strMatching "[A-Za-z0-9+/]{22}";
+                    description = "Authentication cookie.";
+                  };
+                };
+              })
+            ]);
+          };
+          options.HiddenServiceNonAnonymousMode = optionBool "HiddenServiceNonAnonymousMode";
+          options.HiddenServiceStatistics = optionBool "HiddenServiceStatistics";
+          options.HSLayer2Nodes = optionStrings "HSLayer2Nodes";
+          options.HSLayer3Nodes = optionStrings "HSLayer3Nodes";
+          options.HTTPTunnelPort = optionIsolablePorts "HTTPTunnelPort";
+          options.IPv6Exit = optionBool "IPv6Exit";
+          options.KeyDirectory = optionPath "KeyDirectory";
+          options.KeyDirectoryGroupReadable = optionBool "KeyDirectoryGroupReadable";
+          options.LogMessageDomains = optionBool "LogMessageDomains";
+          options.LongLivedPorts = optionPorts "LongLivedPorts";
+          options.MainloopStats = optionBool "MainloopStats";
+          options.MaxAdvertisedBandwidth = optionBandwith "MaxAdvertisedBandwidth";
+          options.MaxCircuitDirtiness = optionInt "MaxCircuitDirtiness";
+          options.MaxClientCircuitsPending = optionInt "MaxClientCircuitsPending";
+          options.NATDPort = optionIsolablePorts "NATDPort";
+          options.NewCircuitPeriod = optionInt "NewCircuitPeriod";
+          options.Nickname = optionString "Nickname";
+          options.ORPort = optionORPort "ORPort";
+          options.OfflineMasterKey = optionBool "OfflineMasterKey";
+          options.OptimisticData = optionBool "OptimisticData"; # default is null and like "auto"
+          options.PaddingStatistics = optionBool "PaddingStatistics";
+          options.PerConnBWBurst = optionBandwith "PerConnBWBurst";
+          options.PerConnBWRate = optionBandwith "PerConnBWRate";
+          options.PidFile = optionPath "PidFile";
+          options.ProtocolWarnings = optionBool "ProtocolWarnings";
+          options.PublishHidServDescriptors = optionBool "PublishHidServDescriptors";
+          options.PublishServerDescriptor = mkOption {
+            description = descriptionGeneric "PublishServerDescriptor";
+            type = with types; nullOr (enum [false true 0 1 "0" "1" "v3" "bridge"]);
+            default = null;
+          };
+          options.ReducedExitPolicy = optionBool "ReducedExitPolicy";
+          options.RefuseUnknownExits = optionBool "RefuseUnknownExits"; # default is null and like "auto"
+          options.RejectPlaintextPorts = optionPorts "RejectPlaintextPorts";
+          options.RelayBandwidthBurst = optionBandwith "RelayBandwidthBurst";
+          options.RelayBandwidthRate = optionBandwith "RelayBandwidthRate";
+          #options.RunAsDaemon
+          options.Sandbox = optionBool "Sandbox";
+          options.ServerDNSAllowBrokenConfig = optionBool "ServerDNSAllowBrokenConfig";
+          options.ServerDNSAllowNonRFC953Hostnames = optionBool "ServerDNSAllowNonRFC953Hostnames";
+          options.ServerDNSDetectHijacking = optionBool "ServerDNSDetectHijacking";
+          options.ServerDNSRandomizeCase = optionBool "ServerDNSRandomizeCase";
+          options.ServerDNSResolvConfFile = optionPath "ServerDNSResolvConfFile";
+          options.ServerDNSSearchDomains = optionBool "ServerDNSSearchDomains";
+          options.ServerTransportPlugin = mkOption {
+            description = descriptionGeneric "ServerTransportPlugin";
+            default = null;
+            type = with types; nullOr (submodule ({...}: {
+              options = {
+                transports = mkOption {
+                  description = "List of pluggable transports.";
+                  type = listOf str;
+                  example = ["obfs2" "obfs3" "obfs4" "scramblesuit"];
+                };
+                exec = mkOption {
+                  type = types.str;
+                  description = "Command of pluggable transport.";
+                };
+              };
+            }));
+          };
+          options.SocksPolicy = optionStrings "SocksPolicy" // {
+            example = ["accept *:*"];
+          };
+          options.SOCKSPort = mkOption {
+            description = descriptionGeneric "SOCKSPort";
+            default = if cfg.settings.HiddenServiceNonAnonymousMode == true then [{port = 0;}] else [];
+            example = [{port = 9090;}];
+            type = types.listOf (optionSOCKSPort true);
+          };
+          options.TestingTorNetwork = optionBool "TestingTorNetwork";
+          options.TransPort = optionIsolablePorts "TransPort";
+          options.TransProxyType = mkOption {
+            description = descriptionGeneric "TransProxyType";
+            type = with types; nullOr (enum ["default" "TPROXY" "ipfw" "pf-divert"]);
+            default = null;
+          };
+          #options.TruncateLogFile
+          options.UnixSocksGroupWritable = optionBool "UnixSocksGroupWritable";
+          options.UseDefaultFallbackDirs = optionBool "UseDefaultFallbackDirs";
+          options.UseMicrodescriptors = optionBool "UseMicrodescriptors";
+          options.V3AuthUseLegacyKey = optionBool "V3AuthUseLegacyKey";
+          options.V3AuthoritativeDirectory = optionBool "V3AuthoritativeDirectory";
+          options.VersioningAuthoritativeDirectory = optionBool "VersioningAuthoritativeDirectory";
+          options.VirtualAddrNetworkIPv4 = optionString "VirtualAddrNetworkIPv4";
+          options.VirtualAddrNetworkIPv6 = optionString "VirtualAddrNetworkIPv6";
+          options.WarnPlaintextPorts = optionPorts "WarnPlaintextPorts";
+        };
       };
     };
   };
@@ -696,79 +822,217 @@ in
   config = mkIf cfg.enable {
     # Not sure if `cfg.relay.role == "private-bridge"` helps as tor
     # sends a lot of stats
-    warnings = optional (cfg.relay.enable && cfg.hiddenServices != {})
+    warnings = optional (cfg.settings.BridgeRelay &&
+      flatten (mapAttrsToList (n: o: o.map) cfg.relay.onionServices) != [])
       ''
         Running Tor hidden services on a public relay makes the
         presence of hidden services visible through simple statistical
         analysis of publicly available data.
+        See https://trac.torproject.org/projects/tor/ticket/8742
 
         You can safely ignore this warning if you don't intend to
         actually hide your hidden services. In either case, you can
         always create a container/VM with a separate Tor daemon instance.
-      '';
+      '' ++
+      flatten (mapAttrsToList (n: o:
+        optional (o.settings.HiddenServiceVersion == 2) [
+          (optional (o.settings.HiddenServiceExportCircuitID != null) ''
+            HiddenServiceExportCircuitID is used in the HiddenService: ${n}
+            but this option is only for v3 hidden services.
+          '')
+        ] ++
+        optional (o.settings.HiddenServiceVersion != 2) [
+          (optional (o.settings.HiddenServiceAuthorizeClient != null) ''
+            HiddenServiceAuthorizeClient is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+          (optional (o.settings.RendPostPeriod != null) ''
+            RendPostPeriod is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+        ]
+      ) cfg.relay.onionServices);
 
     users.groups.tor.gid = config.ids.gids.tor;
     users.users.tor =
       { description = "Tor Daemon User";
         createHome  = true;
-        home        = torDirectory;
+        home        = stateDir;
         group       = "tor";
         uid         = config.ids.uids.tor;
       };
 
-    # We have to do this instead of using RuntimeDirectory option in
-    # the service below because systemd has no way to set owners of
-    # RuntimeDirectory and putting this into the service below
-    # requires that service to relax it's sandbox since this needs
-    # writable /run
-    systemd.services.tor-init =
-      { description = "Tor Daemon Init";
-        wantedBy = [ "tor.service" ];
-        script = ''
-          install -m 0700 -o tor -g tor -d ${torDirectory} ${torDirectory}/onion
-          install -m 0750 -o tor -g tor -d ${torRunDirectory}
-        '';
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-        };
-      };
+    services.tor.settings = mkMerge [
+      (mkIf cfg.enableGeoIP {
+        GeoIPFile = "${cfg.package.geoip}/share/tor/geoip";
+        GeoIPv6File = "${cfg.package.geoip}/share/tor/geoip6";
+      })
+      (mkIf cfg.controlSocket.enable {
+        ControlPort = [ { unix = runDir + "/control"; GroupWritable=true; RelaxDirModeCheck=true; } ];
+      })
+      (mkIf cfg.relay.enable (
+        optionalAttrs (cfg.relay.role != "exit") {
+          ExitPolicy = mkForce ["reject *:*"];
+        } //
+        optionalAttrs (elem cfg.relay.role ["bridge" "private-bridge"]) {
+          BridgeRelay = true;
+          ExtORPort.port = mkDefault "auto";
+          ServerTransportPlugin.transports = mkDefault ["obfs4"];
+          ServerTransportPlugin.exec = mkDefault "${pkgs.obfs4}/bin/obfs4proxy managed";
+        } // optionalAttrs (cfg.relay.role == "private-bridge") {
+          ExtraInfoStatistics = false;
+          PublishServerDescriptor = false;
+        }
+      ))
+      (mkIf (!cfg.relay.enable) {
+        # Avoid surprises when leaving ORPort/DirPort configurations in cfg.settings,
+        # because it would still enable Tor as a relay,
+        # which can trigger all sort of problems when not carefully done,
+        # like the blocklisting of the machine's IP addresses
+        # by some hosting providers...
+        DirPort = mkForce [];
+        ORPort = mkForce [];
+        PublishServerDescriptor = mkForce false;
+      })
+      (mkIf cfg.client.enable (
+        { SOCKSPort = [ cfg.client.socksListenAddress ];
+        } // optionalAttrs cfg.client.transparentProxy.enable {
+          TransPort = [{ addr = "127.0.0.1"; port = 9040; }];
+        } // optionalAttrs cfg.client.dns.enable {
+          DNSPort = [{ addr = "127.0.0.1"; port = 9053; }];
+          AutomapHostsOnResolve = true;
+          AutomapHostsSuffixes = cfg.client.dns.automapHostsSuffixes;
+        } // optionalAttrs (flatten (mapAttrsToList (n: o: o.clientAuthorizations) cfg.client.onionServices) != []) {
+          ClientOnionAuthDir = runDir + "/ClientOnionAuthDir";
+        }
+      ))
+    ];
 
-    systemd.services.tor =
-      { description = "Tor Daemon";
-        path = [ pkgs.tor ];
-
-        wantedBy = [ "multi-user.target" ];
-        after    = [ "tor-init.service" "network.target" ];
-        restartTriggers = [ torRcFile ];
-
-        serviceConfig =
-          { Type         = "simple";
-            # Translated from the upstream contrib/dist/tor.service.in
-            ExecStartPre = "${cfg.package}/bin/tor -f ${torRcFile} --verify-config";
-            ExecStart    = "${cfg.package}/bin/tor -f ${torRcFile}";
-            ExecReload   = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-            KillSignal   = "SIGINT";
-            TimeoutSec   = 30;
-            Restart      = "on-failure";
-            LimitNOFILE  = 32768;
-
-            # Hardening
-            # this seems to unshare /run despite what systemd.exec(5) says
-            PrivateTmp              = mkIf (!cfg.controlSocket.enable) "yes";
-            PrivateDevices          = "yes";
-            ProtectHome             = "yes";
-            ProtectSystem           = "strict";
-            InaccessiblePaths       = "/home";
-            ReadOnlyPaths           = "/";
-            ReadWritePaths          = [ torDirectory torRunDirectory ];
-            NoNewPrivileges         = "yes";
-
-            # tor.service.in has this in, but this line it fails to spawn a namespace when using hidden services
-            #CapabilityBoundingSet   = "CAP_SETUID CAP_SETGID CAP_NET_BIND_SERVICE";
-          };
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts =
+        concatMap (o: optional (isInt o && o > 0 || o ? "port" && isInt o.port && o.port > 0) o.port)
+        (flatten [
+          cfg.settings.ORPort
+          cfg.settings.DirPort
+        ]);
+    };
+
+    systemd.services.tor = {
+      description = "Tor Daemon";
+      path = [ pkgs.tor ];
+
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+      restartTriggers = [ torrc ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "tor";
+        Group = "tor";
+        ExecStartPre = [
+          "${cfg.package}/bin/tor -f ${torrc} --verify-config"
+          # DOC: Appendix G of https://spec.torproject.org/rend-spec-v3
+          ("+" + pkgs.writeShellScript "ExecStartPre" (concatStringsSep "\n" (flatten (["set -eu"] ++
+            mapAttrsToList (name: onion:
+              optional (onion.authorizedClients != []) ''
+                rm -rf ${escapeShellArg onion.path}/authorized_clients
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path} ${escapeShellArg onion.path}/authorized_clients
+              '' ++
+              imap0 (i: pubKey: ''
+                echo ${pubKey} |
+                install -o tor -g tor -m 0400 /dev/stdin ${escapeShellArg onion.path}/authorized_clients/${toString i}.auth
+              '') onion.authorizedClients ++
+              optional (onion.secretKey != null) ''
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path}
+                key="$(cut -f1 -d: ${escapeShellArg onion.secretKey})"
+                case "$key" in
+                 ("== ed25519v"*"-secret")
+                  install -o tor -g tor -m 0400 ${escapeShellArg onion.secretKey} ${escapeShellArg onion.path}/hs_ed25519_secret_key;;
+                 (*) echo >&2 "NixOS does not (yet) support secret key type for onion: ${name}"; exit 1;;
+                esac
+              ''
+            ) cfg.relay.onionServices ++
+            mapAttrsToList (name: onion: imap0 (i: prvKeyPath:
+              let hostname = removeSuffix ".onion" name; in ''
+              printf "%s:" ${escapeShellArg hostname} | cat - ${escapeShellArg prvKeyPath} |
+              install -o tor -g tor -m 0700 /dev/stdin \
+               ${runDir}/ClientOnionAuthDir/${escapeShellArg hostname}.${toString i}.auth_private
+            '') onion.clientAuthorizations)
+            cfg.client.onionServices
+          ))))
+        ];
+        ExecStart = "${cfg.package}/bin/tor -f ${torrc}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        KillSignal = "SIGINT";
+        TimeoutSec = 30;
+        Restart = "on-failure";
+        LimitNOFILE = 32768;
+        RuntimeDirectory = [
+          # g+x allows access to the control socket
+          "tor"
+          "tor/root"
+          # g+x can't be removed in ExecStart=, but will be removed by Tor
+          "tor/ClientOnionAuthDir"
+        ];
+        RuntimeDirectoryMode = "0710";
+        StateDirectoryMode = "0700";
+        StateDirectory = [
+            "tor"
+            "tor/onion"
+          ] ++
+          flatten (mapAttrsToList (name: onion:
+            optional (onion.secretKey == null) "tor/onion/${name}"
+          ) cfg.relay.onionServices);
+        # The following options are only to optimize:
+        # systemd-analyze security tor
+        RootDirectory = runDir + "/root";
+        RootDirectoryStartOnly = true;
+        #InaccessiblePaths = [ "-+${runDir}/root" ];
+        UMask = "0066";
+        BindPaths = [ stateDir ];
+        BindReadOnlyPaths = [ storeDir "/etc" ];
+        AmbientCapabilities   = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        # Tor cannot currently bind privileged port when PrivateUsers=true,
+        # see https://gitlab.torproject.org/legacy/trac/-/issues/20930
+        PrivateUsers = !bindsPrivilegedPort;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        # See also the finer but experimental option settings.Sandbox
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall listed by:
+          # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' tor
+          # in tests, and seem likely not necessary for tor.
+          "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
       };
+    };
 
     environment.systemPackages = [ cfg.package ];
   };
+
+  meta.maintainers = with lib.maintainers; [ julm ];
 }
diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix
index d720f254b81..43b05c5b14d 100644
--- a/nixos/modules/services/system/nscd.nix
+++ b/nixos/modules/services/system/nscd.nix
@@ -50,10 +50,20 @@ in
     systemd.services.nscd =
       { description = "Name Service Cache Daemon";
 
-        wantedBy = [ "nss-lookup.target" "nss-user-lookup.target" ];
-
         environment = { LD_LIBRARY_PATH = nssModulesPath; };
 
+        # We need system users to be resolveable in late-boot.  nscd is the proxy between
+        # nss-modules in NixOS and thus if you have nss-modules providing system users
+        # (e.g. when using DynamicUser) then nscd needs to be available before late-boot is ready
+        # We add a dependency of sysinit.target to nscd to ensure
+        # these units are started after nscd is fully started.
+        unitConfig.DefaultDependencies = false;
+        wantedBy = [ "sysinit.target" ];
+        before = [ "sysinit.target" "shutdown.target" ];
+        conflicts = [ "shutdown.target" ];
+        wants = [ "local-fs.target" ];
+        after = [ "local-fs.target" ];
+
         restartTriggers = [
           config.environment.etc.hosts.source
           config.environment.etc."nsswitch.conf".source
@@ -66,20 +76,19 @@ in
         # privileges after all the NSS modules have read their configuration
         # files. So prefix the ExecStart command with "!" to prevent systemd
         # from dropping privileges early. See ExecStart in systemd.service(5).
-        serviceConfig =
-          { ExecStart = "!@${nscd}/sbin/nscd nscd";
-            Type = "forking";
-            DynamicUser = true;
-            RuntimeDirectory = "nscd";
-            PIDFile = "/run/nscd/nscd.pid";
-            Restart = "always";
-            ExecReload =
-              [ "${nscd}/sbin/nscd --invalidate passwd"
-                "${nscd}/sbin/nscd --invalidate group"
-                "${nscd}/sbin/nscd --invalidate hosts"
-              ];
-          };
+        serviceConfig = {
+          ExecStart = "!@${nscd}/sbin/nscd nscd";
+          Type = "forking";
+          DynamicUser = true;
+          RuntimeDirectory = "nscd";
+          PIDFile = "/run/nscd/nscd.pid";
+          Restart = "always";
+          ExecReload = [
+            "${nscd}/sbin/nscd --invalidate passwd"
+            "${nscd}/sbin/nscd --invalidate group"
+            "${nscd}/sbin/nscd --invalidate hosts"
+          ];
+        };
       };
-
   };
 }
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 62671e9d748..2bde2a68a6b 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -179,6 +179,12 @@ let
       ${cfg.httpConfig}
     }''}
 
+    ${optionalString (cfg.streamConfig != "") ''
+    stream {
+      ${cfg.streamConfig}
+    }
+    ''}
+
     ${cfg.appendConfig}
   '';
 
@@ -452,6 +458,21 @@ in
         ";
       };
 
+      streamConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          server {
+            listen 127.0.0.1:53 udp reuseport;
+            proxy_timeout 20s;
+            proxy_pass 192.168.0.1:53535;
+          }
+        '';
+        description = "
+          Configuration lines to be set inside the stream block.
+        ";
+      };
+
       eventsConfig = mkOption {
         type = types.lines;
         default = "";
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index b82d69b3bb8..c774be2ec54 100644
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -346,10 +346,11 @@ sub filterUnits {
     return @res;
 }
 
+my $startNscd = delete $unitsToStart{"nscd.service"};
+
 my @unitsToStopFiltered = filterUnits(\%unitsToStop);
 my @unitsToStartFiltered = filterUnits(\%unitsToStart);
 
-
 # Show dry-run actions.
 if ($action eq "dry-activate") {
     print STDERR "would stop the following units: ", join(", ", @unitsToStopFiltered), "\n"
@@ -359,6 +360,7 @@ if ($action eq "dry-activate") {
     print STDERR "would restart systemd\n" if $restartSystemd;
     print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"
         if scalar(keys %unitsToRestart) > 0;
+    print STDERR "would start nscd\n" if $startNscd;
     print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n"
         if scalar @unitsToStartFiltered;
     print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n"
@@ -418,6 +420,13 @@ close $listActiveUsers;
 print STDERR "setting up tmpfiles\n";
 system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3;
 
+# We need to start nscd before any other service, since they might need
+# to resolve users/groups only exposed by nss modules (i.e. DynamicUser via nss_systemd)
+if ($startNscd) {
+    print STDERR "starting nscd\n";
+    system("@systemd@/bin/systemctl", "start", "nscd.service") == 0 or $res = 4;
+}
+
 # Reload units that need it. This includes remounting changed mount
 # units.
 if (scalar(keys %unitsToReload) > 0) {
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 86bfde6349c..e133a357bb7 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -366,7 +366,7 @@ let
         }
         trap cleanup EXIT
 
-        tmp=$(mktemp -d initrd-secrets.XXXXXXXXXX)
+        tmp=$(mktemp -d ''${TMPDIR:-/tmp}/initrd-secrets.XXXXXXXXXX)
 
         ${lib.concatStringsSep "\n" (mapAttrsToList (dest: source:
             let source' = if source == null then dest else toString source; in
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index a055072f9c9..5388fc738e3 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -7,8 +7,9 @@ let
 
   addCheckDesc = desc: elemType: check: types.addCheck elemType check
     // { description = "${elemType.description} (with check: ${desc})"; };
-  nonEmptyStr = addCheckDesc "non-empty" types.str
-    (x: x != "" && ! (all (c: c == " " || c == "\t") (stringToCharacters x)));
+
+  isNonEmpty = s: (builtins.match ".*[^ \t]+.*" s) != null;
+  nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty;
 
   fileSystems' = toposort fsBefore (attrValues config.fileSystems);
 
@@ -28,10 +29,10 @@ let
   coreFileSystemOpts = { name, config, ... }: {
 
     options = {
-
       mountPoint = mkOption {
         example = "/mnt/usb";
-        type = nonEmptyStr;
+        type = addCheckDesc "non-empty without trailing slash" types.str
+          (s: isNonEmpty s && (builtins.match "(/|/.*[^/])" s) != null);
         description = "Location of the mounted the file system.";
       };
 
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index 26297a7d0f1..4f83d72901c 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -137,7 +137,7 @@ in
     services.openssh.permitRootLogin = "prohibit-password";
 
     # Creates symlinks for block device names.
-    services.udev.packages = [ pkgs.ec2-utils ];
+    services.udev.packages = [ pkgs.amazon-ec2-utils ];
 
     # Force getting the hostname from EC2.
     networking.hostName = mkDefault "";
diff --git a/nixos/tests/tor.nix b/nixos/tests/tor.nix
index ad07231557c..c061f59226c 100644
--- a/nixos/tests/tor.nix
+++ b/nixos/tests/tor.nix
@@ -17,7 +17,7 @@ rec {
       environment.systemPackages = with pkgs; [ netcat ];
       services.tor.enable = true;
       services.tor.client.enable = true;
-      services.tor.controlPort = 9051;
+      services.tor.settings.ControlPort = 9051;
     };
 
   testScript = ''