{ config, lib, pkgs, ... }: with lib; let cfg = config.services.tor; torDirectory = "/var/lib/tor"; opt = name: value: optionalString (value != null) "${name} ${value}"; optint = name: value: optionalString (value != 0) "${name} ${toString value}"; torRc = '' User tor DataDirectory ${torDirectory} ${optint "ControlPort" cfg.controlPort} '' # Client connection config + optionalString cfg.client.enable '' SOCKSPort ${cfg.client.socksListenAddress} ${opt "SocksPolicy" cfg.client.socksPolicy} '' # Relay config + optionalString cfg.relay.enable '' ORPort ${cfg.relay.portSpec} ${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.isExit then opt "ExitPolicy" cfg.relay.exitPolicy else "ExitPolicy reject *:*"} ${optionalString cfg.relay.isBridge '' BridgeRelay 1 ServerTransportPlugin obfs2,obfs3 exec ${pkgs.pythonPackages.obfsproxy}/bin/obfsproxy managed ''} '' + cfg.extraConfig; torRcFile = pkgs.writeText "torrc" torRc; in { 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. ''; }; 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.int; default = 0; example = 9051; description = '' If set, Tor will accept connections on the specified port and allow them to control the tor process. ''; }; 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. ''; }; 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. ''; }; 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 SocksListenAddress. ''; }; }; relay = { enable = mkOption { type = types.bool; default = false; description = '' Whether to enable relaying TOR traffic for others. See https://www.torproject.org/docs/tor-doc-relay for details. ''; }; isBridge = mkOption { type = types.bool; default = false; description = '' Bridge relays (or "bridges") are Tor relays that aren't listed in the main directory. Since there is no complete public list of them, even if an ISP is filtering connections to all the known Tor relays, they probably won't be able to block all the bridges. A bridge relay can't be an exit relay. You need to set relay.enable to true for this option to take effect. The bridge is set up with an obfuscated transport proxy. See https://www.torproject.org/bridges.html.en for more info. ''; }; isExit = mkOption { type = types.bool; default = false; description = '' An exit relay allows Tor users to access regular Internet services. Unlike running a non-exit relay, running an exit relay may expose you to abuse complaints. See https://www.torproject.org/faq.html.en#ExitPolicies for more info. You can specify which services Tor users may access via your exit relay using exitPolicy option. ''; }; 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 AccountingMax option by looking at the tor manual (man tor) for more. Note this limit applies individually to upload and download; if you specify "500 GBytes" 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 AccountingStart option by looking at the tor manual (man tor) for more. ''; }; bandwidthRate = mkOption { type = types.int; default = 0; 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.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. ''; }; portSpec = mkOption { type = types.str; example = "143"; description = '' What port to advertise for Tor connections. This corresponds to the ORPort section in the Tor manual; see man tor 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 https://www.torproject.org/documentation.html Look at 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. ''; }; }; }; }; config = mkIf cfg.enable { assertions = singleton { message = "Can't be both an exit and a bridge relay at the same time"; assertion = cfg.relay.enable -> !(cfg.relay.isBridge && cfg.relay.isExit); }; users.extraGroups.tor.gid = config.ids.gids.tor; users.extraUsers.tor = { description = "Tor Daemon User"; createHome = true; home = torDirectory; group = "tor"; uid = config.ids.uids.tor; }; systemd.services.tor = { description = "Tor Daemon"; path = [ pkgs.tor ]; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; restartTriggers = [ torRcFile ]; # Translated from the upstream contrib/dist/tor.service.in serviceConfig = { Type = "simple"; ExecStartPre = "${pkgs.tor}/bin/tor -f ${torRcFile} --verify-config"; ExecStart = "${pkgs.tor}/bin/tor -f ${torRcFile} --RunAsDaemon 0"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; KillSignal = "SIGINT"; TimeoutSec = 30; Restart = "on-failure"; LimitNOFILE = 32768; # Hardening # Note: DevicePolicy is set to 'closed', although the # minimal permissions are really: # DeviceAllow /dev/null rw # DeviceAllow /dev/urandom r # .. but we can't specify DeviceAllow multiple times. 'closed' # is close enough. PrivateTmp = "yes"; DevicePolicy = "closed"; InaccessibleDirectories = "/home"; ReadOnlyDirectories = "/"; ReadWriteDirectories = torDirectory; NoNewPrivileges = "yes"; }; }; environment.systemPackages = [ pkgs.tor ]; }; }