Commit | Line | Data |
---|---|---|
7eb6c37c JH |
1 | /************************************************* |
2 | * Exim - an Internet mail transport agent * | |
3 | *************************************************/ | |
4 | ||
f9ba5e22 | 5 | /* Copyright (c) Jeremy Harris 2015 - 2018 */ |
7eb6c37c JH |
6 | /* See the file NOTICE for conditions of use and distribution. */ |
7 | ||
8 | /* SOCKS version 5 proxy, client-mode */ | |
9 | ||
10 | #include "../exim.h" | |
11 | #include "smtp.h" | |
12 | ||
f0989ec0 | 13 | #ifdef SUPPORT_SOCKS /* entire file */ |
7eb6c37c JH |
14 | |
15 | #ifndef nelem | |
16 | # define nelem(arr) (sizeof(arr)/sizeof(*arr)) | |
17 | #endif | |
18 | ||
19 | ||
20 | /* Defaults */ | |
21 | #define SOCKS_PORT 1080 | |
22 | #define SOCKS_TIMEOUT 5 | |
7f06582c JH |
23 | #define SOCKS_WEIGHT 1 |
24 | #define SOCKS_PRIORITY 1 | |
7eb6c37c JH |
25 | |
26 | #define AUTH_NONE 0 | |
27 | #define AUTH_NAME 2 /* user/password per RFC 1929 */ | |
28 | #define AUTH_NAME_VER 1 | |
29 | ||
30 | struct socks_err | |
31 | { | |
32 | uschar * reason; | |
33 | int errcode; | |
34 | } socks_errs[] = | |
35 | { | |
36 | {NULL, 0}, | |
37 | {US"general SOCKS server failure", EIO}, | |
38 | {US"connection not allowed by ruleset", EACCES}, | |
39 | {US"Network unreachable", ENETUNREACH}, | |
40 | {US"Host unreachable", EHOSTUNREACH}, | |
41 | {US"Connection refused", ECONNREFUSED}, | |
42 | {US"TTL expired", ECANCELED}, | |
43 | {US"Command not supported", EOPNOTSUPP}, | |
44 | {US"Address type not supported", EAFNOSUPPORT} | |
45 | }; | |
46 | ||
47 | typedef struct | |
48 | { | |
7f06582c | 49 | const uschar * proxy_host; |
7eb6c37c JH |
50 | uschar auth_type; /* RFC 1928 encoding */ |
51 | const uschar * auth_name; | |
52 | const uschar * auth_pwd; | |
53 | short port; | |
7f06582c | 54 | BOOL is_failed; |
7eb6c37c | 55 | unsigned timeout; |
7f06582c JH |
56 | unsigned weight; |
57 | unsigned priority; | |
7eb6c37c JH |
58 | } socks_opts; |
59 | ||
60 | static void | |
61 | socks_option_defaults(socks_opts * sob) | |
62 | { | |
7f06582c JH |
63 | sob->proxy_host = NULL; |
64 | sob->auth_type = AUTH_NONE; | |
65 | sob->auth_name = US""; | |
66 | sob->auth_pwd = US""; | |
67 | sob->is_failed = FALSE; | |
68 | sob->port = SOCKS_PORT; | |
69 | sob->timeout = SOCKS_TIMEOUT; | |
70 | sob->weight = SOCKS_WEIGHT; | |
71 | sob->priority = SOCKS_PRIORITY; | |
7eb6c37c JH |
72 | } |
73 | ||
74 | static void | |
75 | socks_option(socks_opts * sob, const uschar * opt) | |
76 | { | |
7eb6c37c JH |
77 | if (Ustrncmp(opt, "auth=", 5) == 0) |
78 | { | |
79 | opt += 5; | |
80 | if (Ustrcmp(opt, "none") == 0) sob->auth_type = AUTH_NONE; | |
81 | else if (Ustrcmp(opt, "name") == 0) sob->auth_type = AUTH_NAME; | |
82 | } | |
83 | else if (Ustrncmp(opt, "name=", 5) == 0) | |
84 | sob->auth_name = opt + 5; | |
85 | else if (Ustrncmp(opt, "pass=", 5) == 0) | |
86 | sob->auth_pwd = opt + 5; | |
87 | else if (Ustrncmp(opt, "port=", 5) == 0) | |
059f2ace | 88 | sob->port = atoi(CCS opt + 5); |
7eb6c37c | 89 | else if (Ustrncmp(opt, "tmo=", 4) == 0) |
2b01e535 | 90 | sob->timeout = atoi(CCS opt + 4); |
7f06582c | 91 | else if (Ustrncmp(opt, "pri=", 4) == 0) |
2b01e535 | 92 | sob->priority = atoi(CCS opt + 4); |
7f06582c | 93 | else if (Ustrncmp(opt, "weight=", 7) == 0) |
2b01e535 | 94 | sob->weight = atoi(CCS opt + 7); |
7eb6c37c JH |
95 | return; |
96 | } | |
97 | ||
98 | static int | |
99 | socks_auth(int fd, int method, socks_opts * sob, time_t tmo) | |
100 | { | |
101 | uschar * s; | |
102 | int len, i, j; | |
103 | ||
104 | switch(method) | |
105 | { | |
106 | default: | |
107 | log_write(0, LOG_MAIN|LOG_PANIC, | |
108 | "Unrecognised socks auth method %d", method); | |
109 | return FAIL; | |
110 | case AUTH_NONE: | |
111 | return OK; | |
112 | case AUTH_NAME: | |
e1d04f48 | 113 | HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" socks auth NAME '%s' '%s'\n", |
7eb6c37c JH |
114 | sob->auth_name, sob->auth_pwd); |
115 | i = Ustrlen(sob->auth_name); | |
116 | j = Ustrlen(sob->auth_pwd); | |
117 | s = string_sprintf("%c%c%.255s%c%.255s", AUTH_NAME_VER, | |
118 | i, sob->auth_name, j, sob->auth_pwd); | |
119 | len = i + j + 3; | |
120 | HDEBUG(D_transport|D_acl|D_v) | |
121 | { | |
e1d04f48 | 122 | debug_printf_indent(" SOCKS>>"); |
d7978c0f | 123 | for (int i = 0; i<len; i++) debug_printf(" %02x", s[i]); |
7eb6c37c JH |
124 | debug_printf("\n"); |
125 | } | |
1ccd5f67 JH |
126 | if (send(fd, s, len, 0) < 0) |
127 | return FAIL; | |
128 | #ifdef TCP_QUICKACK | |
129 | (void) setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off)); | |
130 | #endif | |
0a5441fc | 131 | if (!fd_ready(fd, tmo) || read(fd, s, 2) != 2) |
7eb6c37c JH |
132 | return FAIL; |
133 | HDEBUG(D_transport|D_acl|D_v) | |
e1d04f48 | 134 | debug_printf_indent(" SOCKS<< %02x %02x\n", s[0], s[1]); |
7eb6c37c JH |
135 | if (s[0] == AUTH_NAME_VER && s[1] == 0) |
136 | { | |
e1d04f48 | 137 | HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" socks auth OK\n"); |
7eb6c37c JH |
138 | return OK; |
139 | } | |
140 | ||
141 | log_write(0, LOG_MAIN|LOG_PANIC, "socks auth failed"); | |
142 | errno = EPROTO; | |
143 | return FAIL; | |
144 | } | |
145 | } | |
146 | ||
147 | ||
148 | ||
7f06582c JH |
149 | /* Find a suitable proxy to use from the list. |
150 | Possible common code with spamd_get_server() ? | |
151 | ||
152 | Return: index into proxy spec array, or -1 | |
153 | */ | |
154 | ||
155 | static int | |
156 | socks_get_proxy(socks_opts * proxies, unsigned nproxies) | |
157 | { | |
158 | unsigned int i; | |
159 | socks_opts * sd; | |
160 | socks_opts * lim = &proxies[nproxies]; | |
161 | long rnd, weights; | |
162 | unsigned pri; | |
163 | static BOOL srandomed = FALSE; | |
164 | ||
165 | if (nproxies == 1) /* shortcut, if we have only 1 server */ | |
166 | return (proxies[0].is_failed ? -1 : 0); | |
167 | ||
168 | /* init random */ | |
169 | if (!srandomed) | |
170 | { | |
171 | struct timeval tv; | |
172 | gettimeofday(&tv, NULL); | |
173 | srandom((unsigned int)(tv.tv_usec/1000)); | |
174 | srandomed = TRUE; | |
175 | } | |
176 | ||
177 | /* scan for highest pri */ | |
178 | for (pri = 0, sd = proxies; sd < lim; sd++) | |
179 | if (!sd->is_failed && sd->priority > pri) | |
180 | pri = sd->priority; | |
181 | ||
182 | /* get sum of weights at this pri */ | |
183 | for (weights = 0, sd = proxies; sd < lim; sd++) | |
184 | if (!sd->is_failed && sd->priority == pri) | |
185 | weights += sd->weight; | |
186 | if (weights == 0) /* all servers failed */ | |
187 | return -1; | |
188 | ||
189 | for (rnd = random() % weights, i = 0; i < nproxies; i++) | |
190 | { | |
191 | sd = &proxies[i]; | |
192 | if (!sd->is_failed && sd->priority == pri) | |
193 | if ((rnd -= sd->weight) <= 0) | |
194 | return i; | |
195 | } | |
196 | ||
197 | log_write(0, LOG_MAIN|LOG_PANIC, | |
198 | "%s unknown error (memory/cpu corruption?)", __FUNCTION__); | |
199 | return -1; | |
200 | } | |
201 | ||
202 | ||
203 | ||
7eb6c37c JH |
204 | /* Make a connection via a socks proxy |
205 | ||
206 | Arguments: | |
207 | host smtp target host | |
208 | host_af address family | |
209 | port remote tcp port number | |
210 | interface local interface | |
211 | tb transport | |
212 | timeout connection timeout (zero for indefinite) | |
213 | ||
214 | Return value: | |
215 | 0 on success; -1 on failure, with errno set | |
216 | */ | |
217 | ||
218 | int | |
219 | socks_sock_connect(host_item * host, int host_af, int port, uschar * interface, | |
220 | transport_instance * tb, int timeout) | |
7eb6c37c JH |
221 | { |
222 | smtp_transport_options_block * ob = | |
223 | (smtp_transport_options_block *)tb->options_block; | |
224 | const uschar * proxy_list; | |
225 | const uschar * proxy_spec; | |
226 | int sep = 0; | |
227 | int fd; | |
228 | time_t tmo; | |
229 | const uschar * state; | |
230 | uschar buf[24]; | |
7f06582c JH |
231 | socks_opts proxies[32]; /* max #proxies handled */ |
232 | unsigned nproxies; | |
233 | socks_opts * sob; | |
234 | unsigned size; | |
0ab63f3d | 235 | blob early_data; |
7eb6c37c JH |
236 | |
237 | if (!timeout) timeout = 24*60*60; /* use 1 day for "indefinite" */ | |
238 | tmo = time(NULL) + timeout; | |
239 | ||
240 | if (!(proxy_list = expand_string(ob->socks_proxy))) | |
241 | { | |
242 | log_write(0, LOG_MAIN|LOG_PANIC, "Bad expansion for socks_proxy in %s", | |
243 | tb->name); | |
244 | return -1; | |
245 | } | |
246 | ||
7f06582c JH |
247 | /* Read proxy list */ |
248 | ||
249 | for (nproxies = 0; | |
250 | nproxies < nelem(proxies) | |
251 | && (proxy_spec = string_nextinlist(&proxy_list, &sep, NULL, 0)); | |
252 | nproxies++) | |
7eb6c37c | 253 | { |
7eb6c37c | 254 | int subsep = -' '; |
7eb6c37c JH |
255 | const uschar * option; |
256 | ||
7f06582c JH |
257 | socks_option_defaults(sob = &proxies[nproxies]); |
258 | ||
259 | if (!(sob->proxy_host = string_nextinlist(&proxy_spec, &subsep, NULL, 0))) | |
7eb6c37c JH |
260 | { |
261 | /* paniclog config error */ | |
262 | return -1; | |
263 | } | |
264 | ||
265 | /*XXX consider global options eg. "hide socks_password = wibble" on the tpt */ | |
7eb6c37c JH |
266 | /* extract any further per-proxy options */ |
267 | while ((option = string_nextinlist(&proxy_spec, &subsep, NULL, 0))) | |
7f06582c JH |
268 | socks_option(sob, option); |
269 | } | |
270 | ||
0ab63f3d JH |
271 | /* Set up the socks protocol method-selection message, |
272 | for sending on connection */ | |
273 | ||
274 | state = US"method select"; | |
275 | buf[0] = 5; buf[1] = 1; buf[2] = sob->auth_type; | |
276 | early_data.data = buf; | |
277 | early_data.len = 3; | |
278 | ||
7f06582c JH |
279 | /* Try proxies until a connection succeeds */ |
280 | ||
281 | for(;;) | |
282 | { | |
283 | int idx; | |
284 | host_item proxy; | |
285 | int proxy_af; | |
286 | ||
287 | if ((idx = socks_get_proxy(proxies, nproxies)) < 0) | |
288 | { | |
e1d04f48 | 289 | HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" no proxies left\n"); |
7f06582c JH |
290 | errno = EBUSY; |
291 | return -1; | |
292 | } | |
293 | sob = &proxies[idx]; | |
7eb6c37c JH |
294 | |
295 | /* bodge up a host struct for the proxy */ | |
848214f7 | 296 | proxy.address = proxy.name = sob->proxy_host; |
7f06582c | 297 | proxy_af = Ustrchr(sob->proxy_host, ':') ? AF_INET6 : AF_INET; |
7eb6c37c | 298 | |
b536a578 | 299 | /*XXX we trust that the method-select command is idempotent */ |
7f06582c | 300 | if ((fd = smtp_sock_connect(&proxy, proxy_af, sob->port, |
0ab63f3d | 301 | interface, tb, sob->timeout, &early_data)) >= 0) |
e6d2a989 JH |
302 | { |
303 | proxy_local_address = string_copy(proxy.address); | |
304 | proxy_local_port = sob->port; | |
7f06582c | 305 | break; |
e6d2a989 | 306 | } |
7f06582c JH |
307 | |
308 | log_write(0, LOG_MAIN, "%s: %s", __FUNCTION__, strerror(errno)); | |
309 | sob->is_failed = TRUE; | |
310 | } | |
311 | ||
312 | /* Do the socks protocol stuff */ | |
7f06582c | 313 | |
e1d04f48 | 314 | HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SOCKS>> 05 01 %02x\n", sob->auth_type); |
7f06582c JH |
315 | |
316 | /* expect method response */ | |
317 | ||
1ccd5f67 JH |
318 | #ifdef TCP_QUICKACK |
319 | (void) setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off)); | |
320 | #endif | |
321 | ||
0a5441fc | 322 | if ( !fd_ready(fd, tmo) |
7f06582c JH |
323 | || read(fd, buf, 2) != 2 |
324 | ) | |
325 | goto rcv_err; | |
326 | HDEBUG(D_transport|D_acl|D_v) | |
e1d04f48 | 327 | debug_printf_indent(" SOCKS<< %02x %02x\n", buf[0], buf[1]); |
7f06582c JH |
328 | if ( buf[0] != 5 |
329 | || socks_auth(fd, buf[1], sob, tmo) != OK | |
330 | ) | |
331 | goto proxy_err; | |
332 | ||
333 | { | |
334 | union sockaddr_46 sin; | |
7eb6c37c JH |
335 | (void) ip_addr(&sin, host_af, host->address, port); |
336 | ||
337 | /* send connect (ipver, ipaddr, port) */ | |
7f06582c | 338 | |
7eb6c37c | 339 | buf[0] = 5; buf[1] = 1; buf[2] = 0; buf[3] = host_af == AF_INET6 ? 4 : 1; |
7f06582c | 340 | #if HAVE_IPV6 |
7eb6c37c JH |
341 | if (host_af == AF_INET6) |
342 | { | |
343 | memcpy(buf+4, &sin.v6.sin6_addr, sizeof(sin.v6.sin6_addr)); | |
344 | memcpy(buf+4+sizeof(sin.v6.sin6_addr), | |
345 | &sin.v6.sin6_port, sizeof(sin.v6.sin6_port)); | |
346 | size = 4+sizeof(sin.v6.sin6_addr)+sizeof(sin.v6.sin6_port); | |
347 | } | |
348 | else | |
7f06582c | 349 | #endif |
7eb6c37c JH |
350 | { |
351 | memcpy(buf+4, &sin.v4.sin_addr.s_addr, sizeof(sin.v4.sin_addr.s_addr)); | |
352 | memcpy(buf+4+sizeof(sin.v4.sin_addr.s_addr), | |
353 | &sin.v4.sin_port, sizeof(sin.v4.sin_port)); | |
354 | size = 4+sizeof(sin.v4.sin_addr.s_addr)+sizeof(sin.v4.sin_port); | |
355 | } | |
7f06582c | 356 | } |
7eb6c37c | 357 | |
7f06582c JH |
358 | state = US"connect"; |
359 | HDEBUG(D_transport|D_acl|D_v) | |
360 | { | |
e1d04f48 | 361 | debug_printf_indent(" SOCKS>>"); |
d7978c0f | 362 | for (int i = 0; i<size; i++) debug_printf(" %02x", buf[i]); |
7f06582c JH |
363 | debug_printf("\n"); |
364 | } | |
365 | if (send(fd, buf, size, 0) < 0) | |
366 | goto snd_err; | |
7eb6c37c | 367 | |
7f06582c JH |
368 | /* expect conn-reply (success, local(ipver, addr, port)) |
369 | of same length as conn-request, or non-success fail code */ | |
7eb6c37c | 370 | |
0a5441fc | 371 | if ( !fd_ready(fd, tmo) |
7f06582c JH |
372 | || (size = read(fd, buf, size)) < 2 |
373 | ) | |
374 | goto rcv_err; | |
375 | HDEBUG(D_transport|D_acl|D_v) | |
376 | { | |
e1d04f48 | 377 | debug_printf_indent(" SOCKS>>"); |
d7978c0f | 378 | for (int i = 0; i<size; i++) debug_printf(" %02x", buf[i]); |
7f06582c | 379 | debug_printf("\n"); |
7eb6c37c | 380 | } |
7f06582c JH |
381 | if ( buf[0] != 5 |
382 | || buf[1] != 0 | |
383 | ) | |
384 | goto proxy_err; | |
385 | ||
e6d2a989 JH |
386 | proxy_external_address = string_copy( |
387 | host_ntoa(buf[3] == 4 ? AF_INET6 : AF_INET, buf+4, NULL, NULL)); | |
388 | proxy_external_port = ntohs(*((uint16_t *)(buf + (buf[3] == 4 ? 20 : 8)))); | |
389 | proxy_session = TRUE; | |
390 | ||
7f06582c | 391 | HDEBUG(D_transport|D_acl|D_v) |
e1d04f48 | 392 | debug_printf_indent(" proxy farside: [%s]:%d\n", proxy_external_address, proxy_external_port); |
7f06582c JH |
393 | |
394 | return fd; | |
7eb6c37c JH |
395 | |
396 | snd_err: | |
e1d04f48 | 397 | HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" proxy snd_err %s: %s\n", state, strerror(errno)); |
7eb6c37c JH |
398 | return -1; |
399 | ||
400 | proxy_err: | |
401 | { | |
402 | struct socks_err * se = | |
403 | buf[1] > nelem(socks_errs) ? NULL : socks_errs + buf[1]; | |
404 | HDEBUG(D_transport|D_acl|D_v) | |
e1d04f48 | 405 | debug_printf_indent(" proxy %s: %s\n", state, se ? se->reason : US"unknown error code received"); |
7eb6c37c JH |
406 | errno = se ? se->errcode : EPROTO; |
407 | } | |
408 | ||
409 | rcv_err: | |
e1d04f48 | 410 | HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" proxy rcv_err %s: %s\n", state, strerror(errno)); |
7eb6c37c JH |
411 | if (!errno) errno = EPROTO; |
412 | else if (errno == ENOENT) errno = ECONNABORTED; | |
413 | return -1; | |
414 | } | |
415 | ||
416 | #endif /* entire file */ | |
417 | /* vi: aw ai sw=2 | |
418 | */ |