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