1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.shadowsocks;
7
8 opts = {
9 server = cfg.localAddress;
10 server_port = cfg.port;
11 method = cfg.encryptionMethod;
12 mode = cfg.mode;
13 user = "nobody";
14 fast_open = cfg.fastOpen;
15 } // optionalAttrs (cfg.plugin != null) {
16 plugin = cfg.plugin;
17 plugin_opts = cfg.pluginOpts;
18 } // optionalAttrs (cfg.password != null) {
19 password = cfg.password;
20 } // cfg.extraConfig;
21
22 configFile = pkgs.writeText "shadowsocks.json" (builtins.toJSON opts);
23
24in
25
26{
27
28 ###### interface
29
30 options = {
31
32 services.shadowsocks = {
33
34 enable = mkOption {
35 type = types.bool;
36 default = false;
37 description = ''
38 Whether to run shadowsocks-libev shadowsocks server.
39 '';
40 };
41
42 localAddress = mkOption {
43 type = types.coercedTo types.str singleton (types.listOf types.str);
44 default = [ "[::0]" "0.0.0.0" ];
45 description = ''
46 Local addresses to which the server binds.
47 '';
48 };
49
50 port = mkOption {
51 type = types.port;
52 default = 8388;
53 description = ''
54 Port which the server uses.
55 '';
56 };
57
58 password = mkOption {
59 type = types.nullOr types.str;
60 default = null;
61 description = ''
62 Password for connecting clients.
63 '';
64 };
65
66 passwordFile = mkOption {
67 type = types.nullOr types.path;
68 default = null;
69 description = ''
70 Password file with a password for connecting clients.
71 '';
72 };
73
74 mode = mkOption {
75 type = types.enum [ "tcp_only" "tcp_and_udp" "udp_only" ];
76 default = "tcp_and_udp";
77 description = ''
78 Relay protocols.
79 '';
80 };
81
82 fastOpen = mkOption {
83 type = types.bool;
84 default = true;
85 description = ''
86 use TCP fast-open
87 '';
88 };
89
90 encryptionMethod = mkOption {
91 type = types.str;
92 default = "chacha20-ietf-poly1305";
93 description = ''
94 Encryption method. See <https://github.com/shadowsocks/shadowsocks-org/wiki/AEAD-Ciphers>.
95 '';
96 };
97
98 plugin = mkOption {
99 type = types.nullOr types.str;
100 default = null;
101 example = literalExpression ''"''${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin"'';
102 description = ''
103 SIP003 plugin for shadowsocks
104 '';
105 };
106
107 pluginOpts = mkOption {
108 type = types.str;
109 default = "";
110 example = "server;host=example.com";
111 description = ''
112 Options to pass to the plugin if one was specified
113 '';
114 };
115
116 extraConfig = mkOption {
117 type = types.attrs;
118 default = {};
119 example = {
120 nameserver = "8.8.8.8";
121 };
122 description = ''
123 Additional configuration for shadowsocks that is not covered by the
124 provided options. The provided attrset will be serialized to JSON and
125 has to contain valid shadowsocks options. Unfortunately most
126 additional options are undocumented but it's easy to find out what is
127 available by looking into the source code of
128 <https://github.com/shadowsocks/shadowsocks-libev/blob/master/src/jconf.c>
129 '';
130 };
131 };
132
133 };
134
135
136 ###### implementation
137
138 config = mkIf cfg.enable {
139 assertions = [
140 {
141 # xor, make sure either password or passwordFile be set.
142 # shadowsocks-libev not support plain/none encryption method
143 # which indicated that password must set.
144 assertion = let noPasswd = cfg.password == null; noPasswdFile = cfg.passwordFile == null;
145 in (noPasswd && !noPasswdFile) || (!noPasswd && noPasswdFile);
146 message = "Option `password` or `passwordFile` must be set and cannot be set simultaneously";
147 }
148 ];
149
150 systemd.services.shadowsocks-libev = {
151 description = "shadowsocks-libev Daemon";
152 after = [ "network.target" ];
153 wantedBy = [ "multi-user.target" ];
154 path = [ pkgs.shadowsocks-libev ] ++ optional (cfg.plugin != null) cfg.plugin ++ optional (cfg.passwordFile != null) pkgs.jq;
155 serviceConfig.PrivateTmp = true;
156 script = ''
157 ${optionalString (cfg.passwordFile != null) ''
158 cat ${configFile} | jq --arg password "$(cat "${cfg.passwordFile}")" '. + { password: $password }' > /tmp/shadowsocks.json
159 ''}
160 exec ss-server -c ${if cfg.passwordFile != null then "/tmp/shadowsocks.json" else configFile}
161 '';
162 };
163 };
164}