1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.redsocks;
11in
12{
13 ##### interface
14 options = {
15 services.redsocks = {
16 enable = mkOption {
17 type = types.bool;
18 default = false;
19 description = "Whether to enable redsocks.";
20 };
21
22 log_debug = mkOption {
23 type = types.bool;
24 default = false;
25 description = "Log connection progress.";
26 };
27
28 log_info = mkOption {
29 type = types.bool;
30 default = false;
31 description = "Log start and end of client sessions.";
32 };
33
34 log = mkOption {
35 type = types.str;
36 default = "stderr";
37 description = ''
38 Where to send logs.
39
40 Possible values are:
41 - stderr
42 - file:/path/to/file
43 - syslog:FACILITY where FACILITY is any of "daemon", "local0",
44 etc.
45 '';
46 };
47
48 chroot = mkOption {
49 type = with types; nullOr str;
50 default = null;
51 description = ''
52 Chroot under which to run redsocks. Log file is opened before
53 chroot, but if logging to syslog /etc/localtime may be required.
54 '';
55 };
56
57 redsocks = mkOption {
58 description = ''
59 Local port to proxy associations to be performed.
60
61 The example shows how to configure a proxy to handle port 80 as HTTP
62 relay, and all other ports as HTTP connect.
63 '';
64 example = [
65 {
66 port = 23456;
67 proxy = "1.2.3.4:8080";
68 type = "http-relay";
69 redirectCondition = "--dport 80";
70 doNotRedirect = [ "-d 1.2.0.0/16" ];
71 }
72 {
73 port = 23457;
74 proxy = "1.2.3.4:8080";
75 type = "http-connect";
76 redirectCondition = true;
77 doNotRedirect = [ "-d 1.2.0.0/16" ];
78 }
79 ];
80 type = types.listOf (
81 types.submodule {
82 options = {
83 ip = mkOption {
84 type = types.str;
85 default = "127.0.0.1";
86 description = ''
87 IP on which redsocks should listen. Defaults to 127.0.0.1 for
88 security reasons.
89 '';
90 };
91
92 port = mkOption {
93 type = types.port;
94 default = 12345;
95 description = "Port on which redsocks should listen.";
96 };
97
98 proxy = mkOption {
99 type = types.str;
100 description = ''
101 Proxy through which redsocks should forward incoming traffic.
102 Example: "example.org:8080"
103 '';
104 };
105
106 type = mkOption {
107 type = types.enum [
108 "socks4"
109 "socks5"
110 "http-connect"
111 "http-relay"
112 ];
113 description = "Type of proxy.";
114 };
115
116 login = mkOption {
117 type = with types; nullOr str;
118 default = null;
119 description = "Login to send to proxy.";
120 };
121
122 password = mkOption {
123 type = with types; nullOr str;
124 default = null;
125 description = ''
126 Password to send to proxy. WARNING, this will end up
127 world-readable in the store! Awaiting
128 https://github.com/NixOS/nix/issues/8 to be able to fix.
129 '';
130 };
131
132 disclose_src = mkOption {
133 type = types.enum [
134 "false"
135 "X-Forwarded-For"
136 "Forwarded_ip"
137 "Forwarded_ipport"
138 ];
139 default = "false";
140 description = ''
141 Way to disclose client IP to the proxy.
142 - "false": do not disclose
143
144 http-connect supports the following ways:
145 - "X-Forwarded-For": add header "X-Forwarded-For: IP"
146 - "Forwarded_ip": add header "Forwarded: for=IP" (see RFC7239)
147 - "Forwarded_ipport": add header 'Forwarded: for="IP:port"'
148 '';
149 };
150
151 redirectInternetOnly = mkOption {
152 type = types.bool;
153 default = true;
154 description = "Exclude all non-globally-routable IPs from redsocks";
155 };
156
157 doNotRedirect = mkOption {
158 type = with types; listOf str;
159 default = [ ];
160 description = ''
161 Iptables filters that if matched will get the packet off of
162 redsocks.
163 '';
164 example = [ "-d 1.2.3.4" ];
165 };
166
167 redirectCondition = mkOption {
168 type = with types; either bool str;
169 default = false;
170 description = ''
171 Conditions to make outbound packets go through this redsocks
172 instance.
173
174 If set to false, no packet will be forwarded. If set to true,
175 all packets will be forwarded (except packets excluded by
176 redirectInternetOnly).
177
178 If set to a string, this is an iptables filter that will be
179 matched against packets before getting them into redsocks. For
180 example, setting it to "--dport 80" will only send
181 packets to port 80 to redsocks. Note "-p tcp" is always
182 implicitly added, as udp can only be proxied through redudp or
183 the like.
184 '';
185 };
186 };
187 }
188 );
189 };
190
191 # TODO: Add support for redudp and dnstc
192 };
193 };
194
195 ##### implementation
196 config =
197 let
198 redsocks_blocks = concatMapStrings (
199 block:
200 let
201 proxy = splitString ":" block.proxy;
202 in
203 ''
204 redsocks {
205 local_ip = ${block.ip};
206 local_port = ${toString block.port};
207
208 ip = ${elemAt proxy 0};
209 port = ${elemAt proxy 1};
210 type = ${block.type};
211
212 ${optionalString (block.login != null) "login = \"${block.login}\";"}
213 ${optionalString (block.password != null) "password = \"${block.password}\";"}
214
215 disclose_src = ${block.disclose_src};
216 }
217 ''
218 ) cfg.redsocks;
219 configfile = pkgs.writeText "redsocks.conf" ''
220 base {
221 log_debug = ${if cfg.log_debug then "on" else "off"};
222 log_info = ${if cfg.log_info then "on" else "off"};
223 log = ${cfg.log};
224
225 daemon = off;
226 redirector = iptables;
227
228 user = redsocks;
229 group = redsocks;
230 ${optionalString (cfg.chroot != null) "chroot = ${cfg.chroot};"}
231 }
232
233 ${redsocks_blocks}
234 '';
235 internetOnly = [
236 # TODO: add ipv6-equivalent
237 "-d 0.0.0.0/8"
238 "-d 10.0.0.0/8"
239 "-d 127.0.0.0/8"
240 "-d 169.254.0.0/16"
241 "-d 172.16.0.0/12"
242 "-d 192.168.0.0/16"
243 "-d 224.168.0.0/4"
244 "-d 240.168.0.0/4"
245 ];
246 redCond = block: optionalString (isString block.redirectCondition) block.redirectCondition;
247 iptables = concatImapStrings (
248 idx: block:
249 let
250 chain = "REDSOCKS${toString idx}";
251 doNotRedirect = concatMapStringsSep "\n" (
252 f: "ip46tables -t nat -A ${chain} ${f} -j RETURN 2>/dev/null || true"
253 ) (block.doNotRedirect ++ (optionals block.redirectInternetOnly internetOnly));
254 in
255 optionalString (block.redirectCondition != false) ''
256 ip46tables -t nat -F ${chain} 2>/dev/null || true
257 ip46tables -t nat -N ${chain} 2>/dev/null || true
258 ${doNotRedirect}
259 ip46tables -t nat -A ${chain} -p tcp -j REDIRECT --to-ports ${toString block.port}
260
261 # TODO: show errors, when it will be easily possible by a switch to
262 # iptables-restore
263 ip46tables -t nat -A OUTPUT -p tcp ${redCond block} -j ${chain} 2>/dev/null || true
264 ''
265 ) cfg.redsocks;
266 in
267 mkIf cfg.enable {
268 users.groups.redsocks = { };
269 users.users.redsocks = {
270 description = "Redsocks daemon";
271 group = "redsocks";
272 isSystemUser = true;
273 };
274
275 systemd.services.redsocks = {
276 description = "Redsocks";
277 after = [ "network.target" ];
278 wantedBy = [ "multi-user.target" ];
279 script = "${pkgs.redsocks}/bin/redsocks -c ${configfile}";
280 };
281
282 networking.firewall.extraCommands = iptables;
283
284 networking.firewall.extraStopCommands = concatImapStringsSep "\n" (
285 idx: block:
286 let
287 chain = "REDSOCKS${toString idx}";
288 in
289 optionalString (
290 block.redirectCondition != false
291 ) "ip46tables -t nat -D OUTPUT -p tcp ${redCond block} -j ${chain} 2>/dev/null || true"
292 ) cfg.redsocks;
293 };
294
295 meta.maintainers = with lib.maintainers; [ ekleog ];
296}