diff options
Diffstat (limited to 'proxy/socks5.c')
-rw-r--r-- | proxy/socks5.c | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/proxy/socks5.c b/proxy/socks5.c new file mode 100644 index 00000000..87a0bbc8 --- /dev/null +++ b/proxy/socks5.c @@ -0,0 +1,498 @@ +/* + * SOCKS 5 proxy negotiation. + */ + +#include "putty.h" +#include "network.h" +#include "proxy.h" +#include "socks.h" +#include "sshcr.h" + +static inline const char *socks5_auth_name(unsigned char m) +{ + switch (m) { + case SOCKS5_AUTH_NONE: return "none"; + case SOCKS5_AUTH_GSSAPI: return "GSSAPI"; + case SOCKS5_AUTH_PASSWORD: return "password"; + case SOCKS5_AUTH_CHAP: return "CHAP"; + default: return "unknown"; + } +} + +static inline const char *socks5_response_text(unsigned char m) +{ + switch (m) { + case SOCKS5_RESP_SUCCESS: return "success"; + case SOCKS5_RESP_FAILURE: return "unspecified failure"; + case SOCKS5_RESP_CONNECTION_NOT_ALLOWED_BY_RULESET: + return "connection not allowed by ruleset"; + case SOCKS5_RESP_NETWORK_UNREACHABLE: return "network unreachable"; + case SOCKS5_RESP_HOST_UNREACHABLE: return "host unreachable"; + case SOCKS5_RESP_CONNECTION_REFUSED: return "connection refused"; + case SOCKS5_RESP_TTL_EXPIRED: return "TTL expired"; + case SOCKS5_RESP_COMMAND_NOT_SUPPORTED: return "command not supported"; + case SOCKS5_RESP_ADDRTYPE_NOT_SUPPORTED: + return "address type not supported"; + default: return "unknown"; + } +} + +typedef struct Socks5ProxyNegotiator { + int crLine; + strbuf *auth_methods_offered; + unsigned char auth_method; + unsigned n_chap_attrs; + unsigned chap_attr, chap_attr_len; + unsigned char chap_buf[256]; + strbuf *username, *password; + prompts_t *prompts; + int username_prompt_index, password_prompt_index; + int response_addr_length; + ProxyNegotiator pn; +} Socks5ProxyNegotiator; + +static ProxyNegotiator *proxy_socks5_new(const ProxyNegotiatorVT *vt) +{ + Socks5ProxyNegotiator *s = snew(Socks5ProxyNegotiator); + memset(s, 0, sizeof(*s)); + s->pn.vt = vt; + s->auth_methods_offered = strbuf_new(); + s->username = strbuf_new(); + s->password = strbuf_new_nm(); + return &s->pn; +} + +static void proxy_socks5_free(ProxyNegotiator *pn) +{ + Socks5ProxyNegotiator *s = container_of(pn, Socks5ProxyNegotiator, pn); + strbuf_free(s->auth_methods_offered); + strbuf_free(s->username); + strbuf_free(s->password); + if (s->prompts) + free_prompts(s->prompts); + smemclr(s, sizeof(*s)); + sfree(s); +} + +static void proxy_socks5_process_queue(ProxyNegotiator *pn) +{ + Socks5ProxyNegotiator *s = container_of(pn, Socks5ProxyNegotiator, pn); + + crBegin(s->crLine); + + /* + * SOCKS 5 initial client packet: + * + * byte version + * byte number of available auth methods + * byte[] that many bytes indicating auth types + */ + + put_byte(pn->output, SOCKS5_REQUEST_VERSION); + + strbuf_clear(s->auth_methods_offered); + + /* + * We have two basic kinds of authentication to offer: none at + * all, and password-based systems (whether the password is sent + * in cleartext or proved via CHAP). + * + * We always offer 'none' as an option. We offer 'password' if we + * either have a username and password already from the Conf, or + * we have a Seat available to ask for them interactively. If + * neither, we don't offer those options in the first place. + */ + put_byte(s->auth_methods_offered, SOCKS5_AUTH_NONE); + + put_dataz(s->username, conf_get_str(pn->ps->conf, CONF_proxy_username)); + put_dataz(s->password, conf_get_str(pn->ps->conf, CONF_proxy_password)); + if (pn->itr || (s->username->len && s->password->len)) { + if (socks5_chap_available) + put_byte(s->auth_methods_offered, SOCKS5_AUTH_CHAP); + + put_byte(s->auth_methods_offered, SOCKS5_AUTH_PASSWORD); + } + + put_byte(pn->output, s->auth_methods_offered->len); + put_datapl(pn->output, ptrlen_from_strbuf(s->auth_methods_offered)); + + crReturnV; + + /* + * SOCKS 5 initial server packet: + * + * byte version + * byte selected auth method, or SOCKS5_AUTH_REJECTED + */ + { + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, data, 2)); + + if (data[0] != SOCKS5_REPLY_VERSION) { + pn->error = dupprintf("SOCKS proxy returned unexpected " + "reply version %d (expected %d)", + (int)data[0], SOCKS5_REPLY_VERSION); + crStopV; + } + + if (data[1] == SOCKS5_AUTH_REJECTED) { + pn->error = dupstr("SOCKS server rejected every authentication " + "method we offered"); + crStopV; + } + + { + bool found = false; + for (size_t i = 0; i < s->auth_methods_offered->len; i++) + if (s->auth_methods_offered->u[i] == data[1]) { + found = true; + break; + } + + if (!found) { + pn->error = dupprintf("SOCKS server asked for auth method %d " + "(%s), which we did not offer", + (int)data[1], socks5_auth_name(data[1])); + crStopV; + } + } + + s->auth_method = data[1]; + } + + /* + * The 'none' auth option requires no further negotiation. If that + * was the one we selected, go straight to making the connection. + */ + if (s->auth_method == SOCKS5_AUTH_NONE) + goto authenticated; + + /* + * Otherwise, we're going to need a username and password, so this + * is the moment to stop and ask for one if we don't already have + * them. + */ + if (pn->itr && (!s->username->len || !s->password->len)) { + s->prompts = proxy_new_prompts(pn->ps); + s->prompts->to_server = true; + s->prompts->from_server = false; + s->prompts->name = dupstr("SOCKS proxy authentication"); + if (!s->username->len) { + s->username_prompt_index = s->prompts->n_prompts; + add_prompt(s->prompts, dupstr("Proxy username: "), true); + } else { + s->username_prompt_index = -1; + } + if (!s->password->len) { + s->password_prompt_index = s->prompts->n_prompts; + add_prompt(s->prompts, dupstr("Proxy password: "), false); + } else { + s->password_prompt_index = -1; + } + + while (true) { + SeatPromptResult spr = seat_get_userpass_input( + interactor_announce(pn->itr), s->prompts); + if (spr.kind == SPRK_OK) { + break; + } else if (spr_is_abort(spr)) { + proxy_spr_abort(pn, spr); + crStopV; + } + crReturnV; + } + + if (s->username_prompt_index != -1) { + strbuf_clear(s->username); + put_dataz(s->username, + prompt_get_result_ref( + s->prompts->prompts[s->username_prompt_index])); + } + + if (s->password_prompt_index != -1) { + strbuf_clear(s->password); + put_dataz(s->password, + prompt_get_result_ref( + s->prompts->prompts[s->password_prompt_index])); + } + + free_prompts(s->prompts); + s->prompts = NULL; + } + + /* + * Now process the different auth methods that will use that + * username and password. Note we can't do this using the natural + * idiom of a switch statement, because there are crReturns inside + * some cases. + */ + if (s->auth_method == SOCKS5_AUTH_PASSWORD) { + /* + * SOCKS 5 password auth packet: + * + * byte version + * pstring username + * pstring password + */ + put_byte(pn->output, SOCKS5_AUTH_PASSWORD_VERSION); + if (!put_pstring(pn->output, s->username->s)) { + pn->error = dupstr("SOCKS 5 authentication cannot support " + "usernames longer than 255 chars"); + crStopV; + } + if (!put_pstring(pn->output, s->password->s)) { + pn->error = dupstr("SOCKS 5 authentication cannot support " + "passwords longer than 255 chars"); + crStopV; + } + + /* + * SOCKS 5 password reply packet: + * + * byte version + * byte 0 for success, >0 for failure + */ + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, data, 2)); + + if (data[0] != SOCKS5_AUTH_PASSWORD_VERSION) { + pn->error = dupprintf( + "SOCKS 5 password reply had version number %d (expected " + "%d)", (int)data[0], SOCKS5_AUTH_PASSWORD_VERSION); + crStopV; + } + + if (data[1] != 0) { + pn->error = dupstr("SOCKS 5 server rejected our password"); + crStopV; + } + } else if (s->auth_method == SOCKS5_AUTH_CHAP) { + assert(socks5_chap_available); + + /* + * All CHAP packets, in both directions, have the same + * overall format: + * + * byte version + * byte number of attributes + * + * and then for each attribute: + * + * byte attribute type + * byte length + * byte[] that many bytes of payload + * + * In the initial outgoing packet we send two attributes: + * the list of supported algorithm names, and the + * username. + * + * (It's possible that we ought to delay sending the + * username until the second packet, in case the proxy + * sent back an attribute indicating which character set + * it would like us to use.) + */ + put_byte(pn->output, SOCKS5_AUTH_CHAP_VERSION); + put_byte(pn->output, 2); /* number of attributes */ + + put_byte(pn->output, SOCKS5_AUTH_CHAP_ATTR_ALGLIST); + put_byte(pn->output, 1); /* string length */ + put_byte(pn->output, SOCKS5_AUTH_CHAP_ALG_HMACMD5); + + /* Second attribute: username */ + { + put_byte(pn->output, SOCKS5_AUTH_CHAP_ATTR_USERNAME); + if (!put_pstring(pn->output, s->username->s)) { + pn->error = dupstr( + "SOCKS 5 CHAP authentication cannot support " + "usernames longer than 255 chars"); + crStopV; + } + } + + while (true) { + /* + * Process a CHAP response packet, which has the same + * overall format as the outgoing packet shown above. + */ + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume( + pn->input, data, 2)); + if (data[0] != SOCKS5_AUTH_CHAP_VERSION) { + pn->error = dupprintf( + "SOCKS 5 CHAP reply had version number %d (expected " + "%d)", (int)data[0], SOCKS5_AUTH_CHAP_VERSION); + crStopV; + } + + s->n_chap_attrs = data[1]; + if (s->n_chap_attrs == 0) { + /* + * If we receive a CHAP packet containing no + * attributes, then we have nothing we didn't have + * before, and can't make further progress. + */ + pn->error = dupprintf( + "SOCKS 5 CHAP reply sent no attributes"); + crStopV; + } + while (s->n_chap_attrs-- > 0) { + unsigned char data[2]; + crMaybeWaitUntilV(bufchain_try_fetch_consume( + pn->input, data, 2)); + s->chap_attr = data[0]; + s->chap_attr_len = data[1]; + crMaybeWaitUntilV(bufchain_try_fetch_consume( + pn->input, s->chap_buf, s->chap_attr_len)); + + if (s->chap_attr == SOCKS5_AUTH_CHAP_ATTR_STATUS) { + if (s->chap_attr_len == 1 && s->chap_buf[0] == 0) { + /* Status 0 means success: we are authenticated! */ + goto authenticated; + } else { + pn->error = dupstr( + "SOCKS 5 CHAP authentication failed"); + crStopV; + } + } else if (s->chap_attr==SOCKS5_AUTH_CHAP_ATTR_CHALLENGE) { + /* The CHAP challenge string. Send the response */ + strbuf *response = chap_response( + make_ptrlen(s->chap_buf, s->chap_attr_len), + ptrlen_from_strbuf(s->password)); + + put_byte(pn->output, SOCKS5_AUTH_CHAP_VERSION); + put_byte(pn->output, 1); /* number of attributes */ + put_byte(pn->output, SOCKS5_AUTH_CHAP_ATTR_RESPONSE); + put_byte(pn->output, response->len); + put_datapl(pn->output, ptrlen_from_strbuf(response)); + + strbuf_free(response); + } else { + /* ignore all other attributes */ + } + } + } + } else { + unreachable("bad auth method in SOCKS 5 negotiation"); + } + + authenticated: + + /* + * SOCKS 5 connection command: + * + * byte version + * byte command + * byte reserved (send as zero) + * byte address type + * byte[] address, with variable size (see below) + * uint16 port + */ + put_byte(pn->output, SOCKS5_REPLY_VERSION); + put_byte(pn->output, SOCKS_CMD_CONNECT); + put_byte(pn->output, 0); /* reserved byte */ + + switch (sk_addrtype(pn->ps->remote_addr)) { + case ADDRTYPE_IPV4: { + /* IPv4: address is 4 raw bytes */ + put_byte(pn->output, SOCKS5_ADDR_IPV4); + char buf[4]; + sk_addrcopy(pn->ps->remote_addr, buf); + put_data(pn->output, buf, sizeof(buf)); + break; + } + case ADDRTYPE_IPV6: { + /* IPv6: address is 16 raw bytes */ + put_byte(pn->output, SOCKS5_ADDR_IPV6); + char buf[16]; + sk_addrcopy(pn->ps->remote_addr, buf); + put_data(pn->output, buf, sizeof(buf)); + break; + } + case ADDRTYPE_NAME: { + /* Hostname: address is a pstring (Pascal-style string, + * unterminated but with a one-byte prefix length) */ + put_byte(pn->output, SOCKS5_ADDR_HOSTNAME); + char hostname[512]; + sk_getaddr(pn->ps->remote_addr, hostname, lenof(hostname)); + if (!put_pstring(pn->output, hostname)) { + pn->error = dupstr( + "SOCKS 5 cannot support host names longer than 255 chars"); + crStopV; + } + break; + } + default: + unreachable("Unexpected addrtype in SOCKS 5 proxy"); + } + + put_uint16(pn->output, pn->ps->remote_port); + crReturnV; + + /* + * SOCKS 5 connection response: + * + * byte version + * byte status + * byte reserved + * byte address type + * byte[] address bound to (same formats as in connection request) + * uint16 port + * + * We read the first four bytes and then decide what to do next. + */ + { + unsigned char data[4]; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, data, 4)); + + if (data[0] != SOCKS5_REPLY_VERSION) { + pn->error = dupprintf("SOCKS proxy returned unexpected " + "reply version %d (expected %d)", + (int)data[0], SOCKS5_REPLY_VERSION); + crStopV; + } + + if (data[1] != SOCKS5_RESP_SUCCESS) { + pn->error = dupprintf("SOCKS proxy failed to connect, error %d " + "(%s)", (int)data[1], + socks5_response_text(data[1])); + crStopV; + } + + /* + * Process each address type to find out the size of the rest + * of the packet. Note we can't do this using the natural + * idiom of a switch statement, because there are crReturns + * inside some cases. + */ + if (data[3] == SOCKS5_ADDR_IPV4) { + s->response_addr_length = 4; + } else if (data[3] == SOCKS5_ADDR_IPV6) { + s->response_addr_length = 16; + } else if (data[3] == SOCKS5_ADDR_HOSTNAME) { + /* Read the hostname length byte to find out how much to read */ + unsigned char len; + crMaybeWaitUntilV(bufchain_try_fetch_consume(pn->input, &len, 1)); + s->response_addr_length = len; + break; + } else { + pn->error = dupprintf("SOCKS proxy response included unknown " + "address type %d", (int)data[3]); + crStopV; + } + + /* Read and ignore the address and port fields */ + crMaybeWaitUntilV(bufchain_try_consume( + pn->input, s->response_addr_length + 2)); + } + + /* And we're done! */ + pn->done = true; + crFinishV; +} + +const struct ProxyNegotiatorVT socks5_proxy_negotiator_vt = { + .new = proxy_socks5_new, + .free = proxy_socks5_free, + .process_queue = proxy_socks5_process_queue, + .type = "SOCKS 5", +}; |