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