fetch(3): Add SOCKS5 support

This change adds SOCKS5 support to the library fetch(3) and updates the man
page.

Details: Within the fetch_connect() function, fetch(3) checks if the
SOCKS5_PROXY environment variable is set. If so, it connects to this host
rather than the end-host. It then initializes the SOCKS5 connection in
accordance with RFC 1928 and returns the resulting conn_t (file descriptor)
for usage by the regular FTP/HTTP handlers.

Design Decision: This change defaults all DNS resolutions through the proxy
by sending all IPs as hostnames. Going forward, another feature might be to
create another environmental variable to toggle resolutions through the
proxy or not..

One may set the SOCKS5_PROXY environment variable in any of the formats:

SOCKS5_PROXY=proxy.example.com
SOCKS5_PROXY=proxy.example.com:1080
SOCKS5_PROXY=192.0.2.0
SOCKS5_PROXY=198.51.100.0:1080
SOCKS5_PROXY=[2001:db8::1]
SOCKS5_PROXY=[2001:db8::2]:1080

Then perform a request with fetch(1).

(note by kevans)
I've since been informed that Void Linux/xbps has a fork of libfetch that
also implements SOCKS5. I may compare/contrast the two in the mid-to-near
future.

Submitted by:	Farhan Khan <farhan farhan codes>
Differential Revision:	https://reviews.freebsd.org/D18908
This commit is contained in:
Kyle Evans 2020-02-15 18:03:16 +00:00
parent af14123601
commit c44be5aa0a
3 changed files with 351 additions and 11 deletions

View File

@ -42,6 +42,7 @@ __FBSDID("$FreeBSD$");
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <netdb.h>
#include <poll.h>
#include <pwd.h>
@ -74,6 +75,64 @@ static struct fetcherr netdb_errlist[] = {
{ -1, FETCH_UNKNOWN, "Unknown resolver error" }
};
/*
* SOCKS5 error enumerations
*/
enum SOCKS5_ERR {
/* Protocol errors */
SOCKS5_ERR_SELECTION,
SOCKS5_ERR_READ_METHOD,
SOCKS5_ERR_VER5_ONLY,
SOCKS5_ERR_NOMETHODS,
SOCKS5_ERR_NOTIMPLEMENTED,
SOCKS5_ERR_HOSTNAME_SIZE,
SOCKS5_ERR_REQUEST,
SOCKS5_ERR_REPLY,
SOCKS5_ERR_NON_VER5_RESP,
SOCKS5_ERR_GENERAL,
SOCKS5_ERR_NOT_ALLOWED,
SOCKS5_ERR_NET_UNREACHABLE,
SOCKS5_ERR_HOST_UNREACHABLE,
SOCKS5_ERR_CONN_REFUSED,
SOCKS5_ERR_TTL_EXPIRED,
SOCKS5_ERR_COM_UNSUPPORTED,
SOCKS5_ERR_ADDR_UNSUPPORTED,
SOCKS5_ERR_UNSPECIFIED,
/* Configuration errors */
SOCKS5_ERR_BAD_HOST,
SOCKS5_ERR_BAD_PROXY_FORMAT,
SOCKS5_ERR_BAD_PORT
};
/*
* Error messages for SOCKS5 errors
*/
static struct fetcherr socks5_errlist[] = {
/* SOCKS5 protocol errors */
{ SOCKS5_ERR_SELECTION, FETCH_ABORT, "SOCKS5: Failed to send selection method" },
{ SOCKS5_ERR_READ_METHOD, FETCH_ABORT, "SOCKS5: Failed to read method" },
{ SOCKS5_ERR_VER5_ONLY, FETCH_PROTO, "SOCKS5: Only version 5 is implemented" },
{ SOCKS5_ERR_NOMETHODS, FETCH_PROTO, "SOCKS5: No acceptable methods" },
{ SOCKS5_ERR_NOTIMPLEMENTED, FETCH_PROTO, "SOCKS5: Method currently not implemented" },
{ SOCKS5_ERR_HOSTNAME_SIZE, FETCH_PROTO, "SOCKS5: Hostname size is above 256 bytes" },
{ SOCKS5_ERR_REQUEST, FETCH_PROTO, "SOCKS5: Failed to request" },
{ SOCKS5_ERR_REPLY, FETCH_PROTO, "SOCKS5: Failed to receive reply" },
{ SOCKS5_ERR_NON_VER5_RESP, FETCH_PROTO, "SOCKS5: Server responded with a non-version 5 response" },
{ SOCKS5_ERR_GENERAL, FETCH_ABORT, "SOCKS5: General server failure" },
{ SOCKS5_ERR_NOT_ALLOWED, FETCH_AUTH, "SOCKS5: Connection not allowed by ruleset" },
{ SOCKS5_ERR_NET_UNREACHABLE, FETCH_NETWORK, "SOCKS5: Network unreachable" },
{ SOCKS5_ERR_HOST_UNREACHABLE, FETCH_ABORT, "SOCKS5: Host unreachable" },
{ SOCKS5_ERR_CONN_REFUSED, FETCH_ABORT, "SOCKS5: Connection refused" },
{ SOCKS5_ERR_TTL_EXPIRED, FETCH_TIMEOUT, "SOCKS5: TTL expired" },
{ SOCKS5_ERR_COM_UNSUPPORTED, FETCH_PROTO, "SOCKS5: Command not supported" },
{ SOCKS5_ERR_ADDR_UNSUPPORTED, FETCH_ABORT, "SOCKS5: Address type not supported" },
{ SOCKS5_ERR_UNSPECIFIED, FETCH_UNKNOWN, "SOCKS5: Unspecified error" },
/* Configuration error */
{ SOCKS5_ERR_BAD_HOST, FETCH_ABORT, "SOCKS5: Bad proxy host" },
{ SOCKS5_ERR_BAD_PROXY_FORMAT, FETCH_ABORT, "SOCKS5: Bad proxy format" },
{ SOCKS5_ERR_BAD_PORT, FETCH_ABORT, "SOCKS5: Bad port" }
};
/* End-of-Line */
static const char ENDL[2] = "\r\n";
@ -314,7 +373,6 @@ fetch_resolve(const char *addr, int port, int af)
}
/*
* Bind a socket to a specific local address
*/
@ -336,6 +394,196 @@ fetch_bind(int sd, int af, const char *addr)
}
/*
* SOCKS5 connection initiation, based on RFC 1928
* Default DNS resolution over SOCKS5
*/
int
fetch_socks5_init(conn_t *conn, const char *host, int port, int verbose)
{
/*
* Size is based on largest packet prefix (4 bytes) +
* Largest FQDN (256) + one byte size (1) +
* Port (2)
*/
unsigned char buf[BUFF_SIZE];
unsigned char *ptr;
int ret = 1;
if (verbose)
fetch_info("Initializing SOCKS5 connection: %s:%d", host, port);
/* Connection initialization */
ptr = buf;
*ptr++ = SOCKS_VERSION_5;
*ptr++ = SOCKS_CONNECTION;
*ptr++ = SOCKS_RSV;
if (fetch_write(conn, buf, 3) != 3) {
ret = SOCKS5_ERR_SELECTION;
goto fail;
}
/* Verify response from SOCKS5 server */
if (fetch_read(conn, buf, 2) != 2) {
ret = SOCKS5_ERR_READ_METHOD;
goto fail;
}
ptr = buf;
if (ptr[0] != SOCKS_VERSION_5) {
ret = SOCKS5_ERR_VER5_ONLY;
goto fail;
}
if (ptr[1] == SOCKS_NOMETHODS) {
ret = SOCKS5_ERR_NOMETHODS;
goto fail;
}
else if (ptr[1] != SOCKS5_NOTIMPLEMENTED) {
ret = SOCKS5_ERR_NOTIMPLEMENTED;
goto fail;
}
/* Send Request */
*ptr++ = SOCKS_VERSION_5;
*ptr++ = SOCKS_CONNECTION;
*ptr++ = SOCKS_RSV;
/* Encode all targets as a hostname to avoid DNS leaks */
*ptr++ = SOCKS_ATYP_DOMAINNAME;
if (strlen(host) > FQDN_SIZE) {
ret = SOCKS5_ERR_HOSTNAME_SIZE;
goto fail;
}
*ptr++ = strlen(host);
strncpy(ptr, host, strlen(host));
ptr = ptr + strlen(host);
port = htons(port);
*ptr++ = port & 0x00ff;
*ptr++ = (port & 0xff00) >> 8;
if (fetch_write(conn, buf, ptr - buf) != ptr - buf) {
ret = SOCKS5_ERR_REQUEST;
goto fail;
}
/* BND.ADDR is variable length, read the largest on non-blocking socket */
if (!fetch_read(conn, buf, BUFF_SIZE)) {
ret = SOCKS5_ERR_REPLY;
goto fail;
}
ptr = buf;
if (*ptr++ != SOCKS_VERSION_5) {
ret = SOCKS5_ERR_NON_VER5_RESP;
goto fail;
}
switch(*ptr++) {
case SOCKS_SUCCESS:
break;
case SOCKS_GENERAL_FAILURE:
ret = SOCKS5_ERR_GENERAL;
goto fail;
case SOCKS_CONNECTION_NOT_ALLOWED:
ret = SOCKS5_ERR_NOT_ALLOWED;
goto fail;
case SOCKS_NETWORK_UNREACHABLE:
ret = SOCKS5_ERR_NET_UNREACHABLE;
goto fail;
case SOCKS_HOST_UNREACHABLE:
ret = SOCKS5_ERR_HOST_UNREACHABLE;
goto fail;
case SOCKS_CONNECTION_REFUSED:
ret = SOCKS5_ERR_CONN_REFUSED;
goto fail;
case SOCKS_TTL_EXPIRED:
ret = SOCKS5_ERR_TTL_EXPIRED;
goto fail;
case SOCKS_COMMAND_NOT_SUPPORTED:
ret = SOCKS5_ERR_COM_UNSUPPORTED;
goto fail;
case SOCKS_ADDRESS_NOT_SUPPORTED:
ret = SOCKS5_ERR_ADDR_UNSUPPORTED;
goto fail;
default:
ret = SOCKS5_ERR_UNSPECIFIED;
goto fail;
}
return (ret);
fail:
socks5_seterr(ret);
return (0);
}
/*
* Perform SOCKS5 initialization
*/
int
fetch_socks5_getenv(char **host, int *port)
{
char *socks5env, *endptr, *ext;
if ((socks5env = getenv("SOCKS5_PROXY")) == NULL || *socks5env == '\0') {
*host = NULL;
*port = -1;
return (-1);
}
/* IPv6 addresses begin and end in brackets */
if (socks5env[0] == '[') {
if (socks5env[strlen(socks5env) - 1] == ']') {
*host = strndup(socks5env, strlen(socks5env));
if (*host == NULL)
goto fail;
*port = 1080; /* Default port as defined in RFC1928 */
} else {
ext = strstr(socks5env, "]:");
if (ext == NULL) {
socks5_seterr(SOCKS5_ERR_BAD_PROXY_FORMAT);
return (0);
}
ext=ext+1;
*host = strndup(socks5env, ext - socks5env);
if (*host == NULL)
goto fail;
errno = 0;
*port = strtoimax(ext + 1, (char **)&endptr, 10);
if (*endptr != '\0' || errno != 0 || *port < 0 ||
*port > 65535) {
socks5_seterr(SOCKS5_ERR_BAD_PORT);
return (0);
}
}
} else {
ext = strrchr(socks5env, ':');
if (ext == NULL) {
*host = strdup(socks5env);
*port = 1080;
} else {
*host = strndup(socks5env, ext-socks5env);
if (*host == NULL)
goto fail;
errno = 0;
*port = strtoimax(ext + 1, (char **)&endptr, 10);
if (*endptr != '\0' || errno != 0 || *port < 0 ||
*port > 65535) {
socks5_seterr(SOCKS5_ERR_BAD_PORT);
return (0);
}
}
}
return (2);
fail:
fprintf(stderr, "Failure to allocate memory, exiting.\n");
return (-1);
}
/*
* Establish a TCP connection to the specified port on the specified host.
*/
@ -346,12 +594,21 @@ fetch_connect(const char *host, int port, int af, int verbose)
const char *bindaddr;
conn_t *conn = NULL;
int err = 0, sd = -1;
char *sockshost;
int socksport;
DEBUGF("---> %s:%d\n", host, port);
/* Check if SOCKS5_PROXY env variable is set */
if (!fetch_socks5_getenv(&sockshost, &socksport))
goto fail;
/* Not using SOCKS5 proxy */
if (sockshost == NULL) {
/* resolve server address */
if (verbose)
fetch_info("resolving server address: %s:%d", host, port);
fetch_info("resolving server address: %s:%d", host,
port);
if ((sais = fetch_resolve(host, port, af)) == NULL)
goto fail;
@ -359,10 +616,21 @@ fetch_connect(const char *host, int port, int af, int verbose)
bindaddr = getenv("FETCH_BIND_ADDRESS");
if (bindaddr != NULL && *bindaddr != '\0') {
if (verbose)
fetch_info("resolving client address: %s", bindaddr);
fetch_info("resolving client address: %s",
bindaddr);
if ((cais = fetch_resolve(bindaddr, 0, af)) == NULL)
goto fail;
}
} else {
/* resolve socks5 proxy address */
if (verbose)
fetch_info("resolving SOCKS5 server address: %s:%d",
sockshost, socksport);
if ((sais = fetch_resolve(sockshost, socksport, af)) == NULL) {
socks5_seterr(SOCKS5_ERR_BAD_HOST);
goto fail;
}
}
/* try each server address in turn */
for (err = 0, sai = sais; sai != NULL; sai = sai->ai_next) {
@ -389,13 +657,26 @@ fetch_connect(const char *host, int port, int af, int verbose)
sd = -1;
}
if (err != 0) {
if (verbose)
if (verbose && sockshost == NULL) {
fetch_info("failed to connect to %s:%d", host, port);
goto syserr;
} else if (sockshost != NULL) {
if (verbose)
fetch_info(
"failed to connect to SOCKS5 server %s:%d",
sockshost, socksport);
socks5_seterr(SOCKS5_ERR_CONN_REFUSED);
goto syserr1;
}
goto syserr;
}
if ((conn = fetch_reopen(sd)) == NULL)
goto syserr;
if (sockshost)
if (!fetch_socks5_init(conn, host, port, verbose))
goto fail;
if (cais != NULL)
freeaddrinfo(cais);
if (sais != NULL)
@ -403,6 +684,7 @@ fetch_connect(const char *host, int port, int af, int verbose)
return (conn);
syserr:
fetch_syserr();
syserr1:
goto fail;
fail:
if (sd >= 0)

View File

@ -70,12 +70,47 @@ struct fetcherr {
const char *string;
};
/* For SOCKS header size */
#define HEAD_SIZE 4
#define FQDN_SIZE 256
#define PACK_SIZE 1
#define PORT_SIZE 2
#define BUFF_SIZE HEAD_SIZE + FQDN_SIZE + PACK_SIZE + PORT_SIZE
/* SOCKS5 Request Header */
#define SOCKS_VERSION_5 0x05
/* SOCKS5 CMD */
#define SOCKS_CONNECTION 0x01
#define SOCKS_BIND 0x02
#define SOCKS_UDP 0x03
#define SOCKS_NOMETHODS 0xFF
#define SOCKS5_NOTIMPLEMENTED 0x00
/* SOCKS5 Reserved */
#define SOCKS_RSV 0x00
/* SOCKS5 Address Type */
#define SOCKS_ATYP_IPV4 0x01
#define SOCKS_ATYP_DOMAINNAME 0x03
#define SOCKS_ATYP_IPV6 0x04
/* SOCKS5 Reply Field */
#define SOCKS_SUCCESS 0x00
#define SOCKS_GENERAL_FAILURE 0x01
#define SOCKS_CONNECTION_NOT_ALLOWED 0x02
#define SOCKS_NETWORK_UNREACHABLE 0x03
#define SOCKS_HOST_UNREACHABLE 0x04
#define SOCKS_CONNECTION_REFUSED 0x05
#define SOCKS_TTL_EXPIRED 0x06
#define SOCKS_COMMAND_NOT_SUPPORTED 0x07
#define SOCKS_ADDRESS_NOT_SUPPORTED 0x08
/* for fetch_writev */
struct iovec;
void fetch_seterr(struct fetcherr *, int);
void fetch_syserr(void);
void fetch_info(const char *, ...) __printflike(1, 2);
int fetch_socks5_getenv(char **host, int *port);
int fetch_socks5_init(conn_t *conn, const char *host,
int port, int verbose);
int fetch_default_port(const char *);
int fetch_default_proxy_port(const char *);
struct addrinfo *fetch_resolve(const char *, int, int);
@ -102,6 +137,7 @@ int fetch_no_proxy_match(const char *);
#define http_seterr(n) fetch_seterr(http_errlist, n)
#define netdb_seterr(n) fetch_seterr(netdb_errlist, n)
#define url_seterr(n) fetch_seterr(url_errlist, n)
#define socks5_seterr(n) fetch_seterr(socks5_errlist, n)
#ifndef NDEBUG
#define DEBUGF(...) \

View File

@ -668,6 +668,13 @@ which proxies should not be used.
Same as
.Ev NO_PROXY ,
for compatibility.
.It Ev SOCKS5_PROXY
Uses SOCKS version 5 to make connection.
The format must be the IP or hostname followed by a colon for the port.
IPv6 addresses must enclose the address in brackets.
If no port is specified, the default is 1080.
This setting will supercede a connection to an
.Ev HTTP_PROXY .
.It Ev SSL_ALLOW_SSL3
Allow SSL version 3 when negotiating the connection (not recommended).
.It Ev SSL_CA_CERT_FILE
@ -726,6 +733,21 @@ as follows:
NO_PROXY=localhost,127.0.0.1
.Ed
.Pp
To use a SOCKS5 proxy, set the
.Ev SOCKS5_PROXY
environment variable to a
valid host or IP followed by an optional colon and the port.
IPv6 addresses must be enclosed in brackets.
The following are examples of valid settings:
.Bd -literal -offset indent
SOCKS5_PROXY=proxy.example.com
SOCKS5_PROXY=proxy.example.com:1080
SOCKS5_PROXY=192.0.2.0
SOCKS5_PROXY=198.51.100.0:1080
SOCKS5_PROXY=[2001:db8::1]
SOCKS5_PROXY=[2001:db8::2]:1080
.Ed
.Pp
Access HTTPS website without any certificate verification whatsoever:
.Bd -literal -offset indent
SSL_NO_VERIFY_PEER=1