mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-25 01:02:24 +00:00
435 lines
15 KiB
C
435 lines
15 KiB
C
|
/*
|
||
|
* 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];
|
||
|
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();
|
||
|
return &s->pn;
|
||
|
}
|
||
|
|
||
|
static void proxy_socks5_free(ProxyNegotiator *pn)
|
||
|
{
|
||
|
Socks5ProxyNegotiator *s = container_of(pn, Socks5ProxyNegotiator, pn);
|
||
|
strbuf_free(s->auth_methods_offered);
|
||
|
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);
|
||
|
|
||
|
put_byte(s->auth_methods_offered, SOCKS5_AUTH_NONE);
|
||
|
|
||
|
{
|
||
|
const char *username = conf_get_str(pn->ps->conf, CONF_proxy_username);
|
||
|
const char *password = conf_get_str(pn->ps->conf, CONF_proxy_password);
|
||
|
if (username[0] || password[0]) {
|
||
|
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];
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Process each auth method. 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_NONE) {
|
||
|
/* 'none' auth needs no further negotiation, so skip ahead
|
||
|
* to actually making the connection */
|
||
|
goto authenticated;
|
||
|
}
|
||
|
|
||
|
if (s->auth_method == SOCKS5_AUTH_PASSWORD) {
|
||
|
/*
|
||
|
* SOCKS 5 password auth packet:
|
||
|
*
|
||
|
* byte version
|
||
|
* pstring username
|
||
|
* pstring password
|
||
|
*/
|
||
|
const char *username = conf_get_str(
|
||
|
pn->ps->conf, CONF_proxy_username);
|
||
|
const char *password = conf_get_str(
|
||
|
pn->ps->conf, CONF_proxy_password);
|
||
|
put_byte(pn->output, SOCKS5_AUTH_PASSWORD_VERSION);
|
||
|
if (!put_pstring(pn->output, username)) {
|
||
|
pn->error = dupstr("SOCKS 5 authentication cannot support "
|
||
|
"usernames longer than 255 chars");
|
||
|
crStopV;
|
||
|
}
|
||
|
if (!put_pstring(pn->output, password)) {
|
||
|
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 */
|
||
|
{
|
||
|
const char *username = conf_get_str(
|
||
|
pn->ps->conf, CONF_proxy_username);
|
||
|
put_byte(pn->output, SOCKS5_AUTH_CHAP_ATTR_USERNAME);
|
||
|
if (!put_pstring(pn->output, username)) {
|
||
|
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 */
|
||
|
const char *password = conf_get_str(
|
||
|
pn->ps->conf, CONF_proxy_password);
|
||
|
strbuf *response = chap_response(
|
||
|
make_ptrlen(s->chap_buf, s->chap_attr_len),
|
||
|
ptrlen_from_asciz(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",
|
||
|
};
|