diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index f7c66166c5c..f350930f215 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -559,6 +559,7 @@
./services/network-filesystems/yandex-disk.nix
./services/network-filesystems/xtreemfs.nix
./services/network-filesystems/ceph.nix
+ ./services/networking/3proxy.nix
./services/networking/amuled.nix
./services/networking/aria2.nix
./services/networking/asterisk.nix
diff --git a/nixos/modules/services/networking/3proxy.nix b/nixos/modules/services/networking/3proxy.nix
new file mode 100644
index 00000000000..b5f5e707609
--- /dev/null
+++ b/nixos/modules/services/networking/3proxy.nix
@@ -0,0 +1,422 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+ pkg = pkgs._3proxy;
+ cfg = config.services._3proxy;
+ optionalList = list: if list == [ ] then "*" else concatMapStringsSep "," toString list;
+in {
+ options.services._3proxy = {
+ enable = mkEnableOption "3proxy";
+ confFile = mkOption {
+ type = types.path;
+ example = "/var/lib/3proxy/3proxy.conf";
+ description = ''
+ Ignore all other 3proxy options and load configuration from this file.
+ '';
+ };
+ usersFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ example = "/var/lib/3proxy/3proxy.passwd";
+ description = ''
+ Load users and passwords from this file.
+
+ Example users file with plain-text passwords:
+
+
+ test1:CL:password1
+ test2:CL:password2
+
+
+ Example users file with md5-crypted passwords:
+
+
+ "test1:CR:$1$tFkisVd2$1GA8JXkRmTXdLDytM/i3a1"
+ "test2:CR:$1$rkpibm5J$Aq1.9VtYAn0JrqZ8M.1ME."
+
+
+ You can generate md5-crypted passwords via https://unix4lyfe.org/crypt/
+ Consult documentation for more information.
+ '';
+ };
+ services = mkOption {
+ type = types.listOf (types.submodule {
+ options = {
+ type = mkOption {
+ type = types.enum [
+ "proxy"
+ "socks"
+ "pop3p"
+ "ftppr"
+ "admin"
+ "dnspr"
+ "tcppm"
+ "udppm"
+ ];
+ example = "proxy";
+ description = ''
+ Service type. The following values are valid:
+
+
+
+ "proxy": HTTP/HTTPS proxy (default port 3128).
+
+
+ "socks": SOCKS 4/4.5/5 proxy (default port 1080).
+
+
+ "pop3p": POP3 proxy (default port 110).
+
+
+ "ftppr": FTP proxy (default port 21).
+
+
+ "admin": Web interface (default port 80).
+
+
+ "dnspr": Caching DNS proxy (default port 53).
+
+
+ "tcppm": TCP portmapper.
+
+
+ "udppm": UDP portmapper.
+
+
+ '';
+ };
+ bindAddress = mkOption {
+ type = types.str;
+ default = "[::]";
+ example = "127.0.0.1";
+ description = ''
+ Address used for service.
+ '';
+ };
+ bindPort = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ example = 3128;
+ description = ''
+ Override default port used for service.
+ '';
+ };
+ maxConnections = mkOption {
+ type = types.int;
+ default = 100;
+ example = 1000;
+ description = ''
+ Maximum number of simulationeous connections to this service.
+ '';
+ };
+ auth = mkOption {
+ type = types.listOf (types.enum [ "none" "iponly" "strong" ]);
+ example = [ "iponly" "strong" ];
+ description = ''
+ Authentication type. The following values are valid:
+
+
+
+ "none": disables both authentication and authorization. You can not use ACLs.
+
+
+ "iponly": specifies no authentication. ACLs authorization is used.
+
+
+ "strong": authentication by username/password. If user is not registered his access is denied regardless of ACLs.
+
+
+
+ Double authentication is possible, e.g.
+
+ {
+ auth = [ "iponly" "strong" ];
+ acl = [
+ {
+ rule = "allow";
+ targets = [ "192.168.0.0/16" ];
+ }
+ {
+ rule = "allow"
+ users = [ "user1" "user2" ];
+ }
+ ];
+ }
+
+ In this example strong username authentication is not required to access 192.168.0.0/16.
+ '';
+ };
+ acl = mkOption {
+ type = types.listOf (types.submodule {
+ options = {
+ rule = mkOption {
+ type = types.enum [ "allow" "deny" ];
+ example = "allow";
+ description = ''
+ ACL rule. The following values are valid:
+
+
+
+ "allow": connections allowed.
+
+
+ "deny": connections not allowed.
+
+
+ '';
+ };
+ users = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "user1" "user2" "user3" ];
+ description = ''
+ List of users, use empty list for any.
+ '';
+ };
+ sources = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "127.0.0.1" "192.168.1.0/24" ];
+ description = ''
+ List of source IP range, use empty list for any.
+ '';
+ };
+ targets = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "127.0.0.1" "192.168.1.0/24" ];
+ description = ''
+ List of target IP ranges, use empty list for any.
+ May also contain host names instead of addresses.
+ It's possible to use wildmask in the begginning and in the the end of hostname, e.g. *badsite.com or *badcontent*.
+ Hostname is only checked if hostname presents in request.
+ '';
+ };
+ targetPorts = mkOption {
+ type = types.listOf types.int;
+ default = [ ];
+ example = [ 80 443 ];
+ description = ''
+ List of target ports, use empty list for any.
+ '';
+ };
+ };
+ });
+ default = [ ];
+ example = literalExample ''
+ [
+ {
+ rule = "allow";
+ users = [ "user1" ];
+ }
+ {
+ rule = "allow";
+ sources = [ "192.168.1.0/24" ];
+ }
+ {
+ rule = "deny";
+ }
+ ]
+ '';
+ description = ''
+ Use this option to limit user access to resources.
+ '';
+ };
+ extraArguments = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "-46";
+ description = ''
+ Extra arguments for service.
+ Consult "Options" section in documentation for available arguments.
+ '';
+ };
+ extraConfig = mkOption {
+ type = types.nullOr types.lines;
+ default = null;
+ description = ''
+ Extra configuration for service. Use this to configure things like bandwidth limiter or ACL-based redirection.
+ Consult documentation for available options.
+ '';
+ };
+ };
+ });
+ default = [ ];
+ example = literalExample ''
+ [
+ {
+ type = "proxy";
+ bindAddress = "192.168.1.24";
+ bindPort = 3128;
+ auth = [ "none" ];
+ }
+ {
+ type = "proxy";
+ bindAddress = "10.10.1.20";
+ bindPort = 3128;
+ auth = [ "iponly" ];
+ }
+ {
+ type = "socks";
+ bindAddress = "172.17.0.1";
+ bindPort = 1080;
+ auth = [ "strong" ];
+ }
+ ]
+ '';
+ description = ''
+ Use this option to define 3proxy services.
+ '';
+ };
+ denyPrivate = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to deny access to private IP ranges including loopback.
+ '';
+ };
+ privateRanges = mkOption {
+ type = types.listOf types.str;
+ default = [
+ "0.0.0.0/8"
+ "127.0.0.0/8"
+ "10.0.0.0/8"
+ "100.64.0.0/10"
+ "172.16.0.0/12"
+ "192.168.0.0/16"
+ "::"
+ "::1"
+ "fc00::/7"
+ ];
+ example = [
+ "0.0.0.0/8"
+ "127.0.0.0/8"
+ "10.0.0.0/8"
+ "100.64.0.0/10"
+ "172.16.0.0/12"
+ "192.168.0.0/16"
+ "::"
+ "::1"
+ "fc00::/7"
+ ];
+ description = ''
+ What IP ranges to deny access when denyPrivate is set tu true.
+ '';
+ };
+ resolution = mkOption {
+ type = types.submodule {
+ options = {
+ nserver = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "127.0.0.53" "192.168.1.3:5353/tcp" ];
+ description = ''
+ List of nameservers to use.
+
+ Up to 5 nservers may be specified. If no nserver is configured,
+ default system name resolution functions are used.
+ '';
+ };
+ nscache = mkOption {
+ type = types.int;
+ default = 65535;
+ example = 65535;
+ description = "Set name cache size for IPv4.";
+ };
+ nscache6 = mkOption {
+ type = types.int;
+ default = 65535;
+ example = 65535;
+ description = "Set name cache size for IPv6.";
+ };
+ nsrecord = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = {
+ "files.local" = "192.168.1.12";
+ "site.local" = "192.168.1.43";
+ };
+ description = "Adds static nsrecords.";
+ };
+ };
+ };
+ default = { };
+ description = ''
+ Use this option to configure name resolution and DNS caching.
+ '';
+ };
+ extraConfig = mkOption {
+ type = types.nullOr types.lines;
+ default = null;
+ description = ''
+ Extra configuration, appended to the 3proxy configuration file.
+ Consult documentation for available options.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ services._3proxy.confFile = mkDefault (pkgs.writeText "3proxy.conf" ''
+ # log to stdout
+ log
+
+ ${concatMapStringsSep "\n" (x: "nserver " + x) cfg.resolution.nserver}
+
+ nscache ${toString cfg.resolution.nscache}
+ nscache6 ${toString cfg.resolution.nscache6}
+
+ ${concatMapStringsSep "\n" (x: "nsrecord " + x)
+ (mapAttrsToList (name: value: "${name} ${value}")
+ cfg.resolution.nsrecord)}
+
+ ${optionalString (cfg.usersFile != null)
+ ''users $"${cfg.usersFile}"''
+ }
+
+ ${concatMapStringsSep "\n" (service: ''
+ auth ${concatStringsSep " " service.auth}
+
+ ${optionalString (cfg.denyPrivate)
+ "deny * * ${optionalList cfg.privateRanges}"}
+
+ ${concatMapStringsSep "\n" (acl:
+ "${acl.rule} ${
+ concatMapStringsSep " " optionalList [
+ acl.users
+ acl.sources
+ acl.targets
+ acl.targetPorts
+ ]
+ }") service.acl}
+
+ maxconn ${toString service.maxConnections}
+
+ ${optionalString (service.extraConfig != null) service.extraConfig}
+
+ ${service.type} -i${toString service.bindAddress} ${
+ optionalString (service.bindPort != null)
+ "-p${toString service.bindPort}"
+ } ${
+ optionalString (service.extraArguments != null) service.extraArguments
+ }
+
+ flush
+ '') cfg.services}
+ ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+ '');
+ systemd.services."3proxy" = {
+ description = "Tiny free proxy server";
+ documentation = [ "https://github.com/z3APA3A/3proxy/wiki" ];
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ DynamicUser = true;
+ StateDirectory = "3proxy";
+ ExecStart = "${pkg}/bin/3proxy ${cfg.confFile}";
+ Restart = "on-failure";
+ };
+ };
+ };
+
+ meta.maintainers = with maintainers; [ misuzu ];
+}
diff --git a/nixos/tests/3proxy.nix b/nixos/tests/3proxy.nix
new file mode 100644
index 00000000000..b8e1dac0e89
--- /dev/null
+++ b/nixos/tests/3proxy.nix
@@ -0,0 +1,162 @@
+import ./make-test.nix ({ pkgs, ...} : {
+ name = "3proxy";
+ meta = with pkgs.stdenv.lib.maintainers; {
+ maintainers = [ misuzu ];
+ };
+
+ nodes = {
+ peer0 = { lib, ... }: {
+ networking.useDHCP = false;
+ networking.interfaces.eth1 = {
+ ipv4.addresses = [
+ {
+ address = "192.168.0.1";
+ prefixLength = 24;
+ }
+ {
+ address = "216.58.211.111";
+ prefixLength = 24;
+ }
+ ];
+ };
+ };
+
+ peer1 = { lib, ... }: {
+ networking.useDHCP = false;
+ networking.interfaces.eth1 = {
+ ipv4.addresses = [
+ {
+ address = "192.168.0.2";
+ prefixLength = 24;
+ }
+ {
+ address = "216.58.211.112";
+ prefixLength = 24;
+ }
+ ];
+ };
+ # test that binding to [::] is working when ipv6 is disabled
+ networking.enableIPv6 = false;
+ services._3proxy = {
+ enable = true;
+ services = [
+ {
+ type = "admin";
+ bindPort = 9999;
+ auth = [ "none" ];
+ }
+ {
+ type = "proxy";
+ bindPort = 3128;
+ auth = [ "none" ];
+ }
+ ];
+ };
+ networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+ };
+
+ peer2 = { lib, ... }: {
+ networking.useDHCP = false;
+ networking.interfaces.eth1 = {
+ ipv4.addresses = [
+ {
+ address = "192.168.0.3";
+ prefixLength = 24;
+ }
+ {
+ address = "216.58.211.113";
+ prefixLength = 24;
+ }
+ ];
+ };
+ services._3proxy = {
+ enable = true;
+ services = [
+ {
+ type = "admin";
+ bindPort = 9999;
+ auth = [ "none" ];
+ }
+ {
+ type = "proxy";
+ bindPort = 3128;
+ auth = [ "iponly" ];
+ acl = [
+ {
+ rule = "allow";
+ }
+ ];
+ }
+ ];
+ };
+ networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+ };
+
+ peer3 = { lib, ... }: {
+ networking.useDHCP = false;
+ networking.interfaces.eth1 = {
+ ipv4.addresses = [
+ {
+ address = "192.168.0.4";
+ prefixLength = 24;
+ }
+ {
+ address = "216.58.211.114";
+ prefixLength = 24;
+ }
+ ];
+ };
+ services._3proxy = {
+ enable = true;
+ usersFile = pkgs.writeText "3proxy.passwd" ''
+ admin:CR:$1$.GUV4Wvk$WnEVQtaqutD9.beO5ar1W/
+ '';
+ services = [
+ {
+ type = "admin";
+ bindPort = 9999;
+ auth = [ "none" ];
+ }
+ {
+ type = "proxy";
+ bindPort = 3128;
+ auth = [ "strong" ];
+ acl = [
+ {
+ rule = "allow";
+ }
+ ];
+ }
+ ];
+ };
+ networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+ };
+ };
+
+ testScript = ''
+ startAll;
+
+ $peer1->waitForUnit("3proxy.service");
+
+ # test none auth
+ $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://216.58.211.112:9999");
+ $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://192.168.0.2:9999");
+ $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://127.0.0.1:9999");
+
+ $peer2->waitForUnit("3proxy.service");
+
+ # test iponly auth
+ $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://216.58.211.113:9999");
+ $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://192.168.0.3:9999");
+ $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://127.0.0.1:9999");
+
+ $peer3->waitForUnit("3proxy.service");
+
+ # test strong auth
+ $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999");
+ $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999");
+ $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999");
+ $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999");
+ $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://127.0.0.1:9999");
+ '';
+})
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 9db505a27d4..f01c6e71fce 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -21,6 +21,7 @@ let
else {};
in
{
+ _3proxy = handleTest ./3proxy.nix {};
acme = handleTestOn ["x86_64-linux"] ./acme.nix {};
atd = handleTest ./atd.nix {};
automysqlbackup = handleTest ./automysqlbackup.nix {};