From 7b66b53f934fe611747d7b269834ec30a175b546 Mon Sep 17 00:00:00 2001 From: Gerhard Rieger Date: Sun, 5 Nov 2023 20:46:22 +0100 Subject: [PATCH] Added socks5 client feature for connect and listen (experimental) --- CHANGES | 5 + Makefile.in | 4 +- config.h.in | 1 + configure.ac | 8 + doc/socat.yo | 34 +++ socat.c | 5 + xio-socks5.c | 655 +++++++++++++++++++++++++++++++++++++++++++++++++++ xio-socks5.h | 41 ++++ xiomodes.h | 1 + xioopen.c | 6 + 10 files changed, 758 insertions(+), 2 deletions(-) create mode 100644 xio-socks5.c create mode 100644 xio-socks5.h diff --git a/CHANGES b/CHANGES index 9df8757..63a4362 100644 --- a/CHANGES +++ b/CHANGES @@ -122,6 +122,11 @@ Features: transfer. This can be used with "inetd mode" of systemd. Test: ACCEPT_FD + Added experimental socks5 TCP client support (connect,bind); syntax: + SOCKS5-CONNECT:::: + SOCKS5-LISTEN:::: + Thanks to Charlie Svensson and others for contributions. + Corrections: When a sub process (EXEC, SYSTEM) terminated with exit code other than 0, its last sent data might have been lost depending on timing of read/ diff --git a/Makefile.in b/Makefile.in index 95fcca9..367783a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -49,7 +49,7 @@ XIOSRCS = xioinitialize.c xiohelp.c xioparam.c xiodiag.c xioopen.c xioopts.c \ xio-socket.c xio-interface.c xio-listen.c xio-unix.c xio-vsock.c \ xio-ip.c xio-ip4.c xio-ip6.c xio-ipapp.c xio-tcp.c \ xio-sctp.c xio-rawip.c \ - xio-socks.c xio-proxy.c xio-udp.c \ + xio-socks.c xio-socks5.c xio-proxy.c xio-udp.c \ xio-progcall.c xio-exec.c xio-system.c xio-termios.c xio-readline.c \ xio-pty.c xio-openssl.c xio-streams.c xio-namespaces.c \ xio-ascii.c xiolockfile.c xio-tcpwrap.c xio-fs.c xio-tun.c @@ -67,7 +67,7 @@ HFILES = sycls.h sslcls.h error.h dalan.h procan.h filan.h hostan.h sysincludes. xio-socketpair.h xio-socket.h xio-interface.h xio-listen.h xio-unix.h xio-vsock.h \ xio-ip.h xio-ip4.h xio-ip6.h xio-rawip.h \ xio-ipapp.h xio-tcp.h xio-udp.h xio-sctp.h \ - xio-socks.h xio-proxy.h xio-progcall.h xio-exec.h \ + xio-socks.h xio-socks5.h xio-proxy.h xio-progcall.h xio-exec.h \ xio-system.h xio-termios.h xio-readline.h \ xio-pty.h xio-openssl.h xio-streams.h xio-namespaces.h \ xio-ascii.h xiolockfile.h xio-tcpwrap.h xio-fs.h xio-tun.h diff --git a/config.h.in b/config.h.in index 67e566c..2d5266a 100644 --- a/config.h.in +++ b/config.h.in @@ -706,6 +706,7 @@ #undef WITH_LISTEN #undef WITH_SOCKS4 #undef WITH_SOCKS4A +#undef WITH_SOCKS5 #undef WITH_VSOCK #undef WITH_NAMESPACES #undef WITH_PROXY diff --git a/configure.ac b/configure.ac index 7dced77..0d54292 100644 --- a/configure.ac +++ b/configure.ac @@ -460,6 +460,14 @@ AC_ARG_ENABLE(socks4a, [ --disable-socks4a disable socks4a support], esac], [AC_DEFINE(WITH_SOCKS4A) AC_MSG_RESULT(yes)]) +AC_MSG_CHECKING(whether to include socks5 support) +AC_ARG_ENABLE(socks5, [ --disable-socks5 disable socks5 support], + [case "$enableval" in + no) AC_MSG_RESULT(no);; + *) AC_DEFINE(WITH_SOCKS5) AC_MSG_RESULT(yes);; + esac], + [AC_DEFINE(WITH_SOCKS5) AC_MSG_RESULT(yes)]) + AC_MSG_CHECKING(whether to include proxy connect support) AC_ARG_ENABLE(proxy, [ --disable-proxy disable proxy connect support], [case "$enableval" in diff --git a/doc/socat.yo b/doc/socat.yo index b29e92e..350ef4f 100644 --- a/doc/socat.yo +++ b/doc/socat.yo @@ -971,13 +971,47 @@ label(ADDRESS_SOCKS4)dit(bf(tt(SOCKS4:::))) link(pf)(OPTION_PROTOCOL_FAMILY), link(retry)(OPTION_RETRY)nl() See also: + link(SOCKS5)(ADDRESS_SOCKS5_CONNECT), link(SOCKS4A)(ADDRESS_SOCKS4A), link(PROXY)(ADDRESS_PROXY_CONNECT), link(TCP)(ADDRESS_TCP_CONNECT) + label(ADDRESS_SOCKS4A)dit(bf(tt(SOCKS4A:::))) like link(SOCKS4)(ADDRESS_SOCKS4), but uses socks protocol version 4a, thus leaving host name resolution to the socks server.nl() Option groups: link(FD)(GROUP_FD),link(SOCKET)(GROUP_SOCKET),link(IP4)(GROUP_IP4),link(IP6)(GROUP_IP6),link(TCP)(GROUP_TCP),link(SOCKS4)(GROUP_SOCKS),link(RETRY)(GROUP_RETRY) nl() + +label(ADDRESS_SOCKS5_CONNECT)dit(bf(tt(SOCKS5-CONNECT::::))) + Connects via [link(IP address)(TYPE_IP_ADDRESS)] + to [link(IPv4 address)(TYPE_IPV4_ADDRESS)] + on [link(TCP service)(TYPE_TCP_SERVICE)], + using socks version 5 protocol over TCP. Currently no authentication mechanism is provided.nl() + This address type is experimental.nl() + Option groups: link(FD)(GROUP_FD), link(SOCKET)(GROUP_SOCKET), link(IP4)(GROUP_IP4), link(IP6)(GROUP_IP6), link(TCP)(GROUP_TCP), link(CHILD)(GROUP_CHILD), link(RETRY)(GROUP_RETRY)nl() + Useful options: + link(sourceport)(OPTION_SOURCEPORT), + link(pf)(OPTION_PROTOCOL_FAMILY), + link(retry)(OPTION_RETRY)nl() + See also: + link(SOCKS5-LISTEN)(ADDRESS_SOCKS5_LISTEN), + link(SOCKS4)(ADDRESS_SOCKS4), + link(SOCKS4A)(ADDRESS_SOCKS4A), + link(PROXY)(ADDRESS_PROXY_CONNECT), + link(TCP)(ADDRESS_TCP_CONNECT) + +label(ADDRESS_SOCKS5_LISTEN)dit(bf(tt(SOCKS5-LISTEN::::))) + Connects to [link(IP address)(TYPE_IP_ADDRESS)] + using socks version 5 protocol over TCP + and makes it listen for incoming connections on [link(TCP service)(TYPE_TCP_SERVICE)], binding to <-listen-host> [link(IPv4 address)(TYPE_IPV4_ADDRESS)] + Currently not authentication mechanism is provided. This address type is experimental. + Option groups: link(FD)(GROUP_FD), link(SOCKET)(GROUP_SOCKET), link(IP4)(GROUP_IP4), link(IP6)(GROUP_IP6), link(TCP)(GROUP_TCP), link(CHILD)(GROUP_CHILD), link(RETRY)(GROUP_RETRY)nl() + Useful options: + link(sourceport)(OPTION_SOURCEPORT), + link(pf)(OPTION_PROTOCOL_FAMILY), + link(retry)(OPTION_RETRY)nl() + See also: + link(SOCKS5-CONNECT)(ADDRESS_SOCKS5_CONNECT), + label(ADDRESS_STDERR)dit(bf(tt(STDERR))) Uses file descriptor 2.nl() Option groups: link(FD)(GROUP_FD) (link(TERMIOS)(GROUP_TERMIOS),link(REG)(GROUP_REG),link(SOCKET)(GROUP_SOCKET)) nl() diff --git a/socat.c b/socat.c index e2b1bd8..0c7a464 100644 --- a/socat.c +++ b/socat.c @@ -610,6 +610,11 @@ void socat_version(FILE *fd) { #else fputs(" #undef WITH_SOCKS4A\n", fd); #endif +#ifdef WITH_SOCKS5 + fprintf(fd, " #define WITH_SOCKS5 %d\n", WITH_SOCKS5); +#else + fputs(" #undef WITH_SOCKS5\n", fd); +#endif #ifdef WITH_VSOCK fprintf(fd, " #define WITH_VSOCK %d\n", WITH_VSOCK); #else diff --git a/xio-socks5.c b/xio-socks5.c new file mode 100644 index 0000000..18b7c38 --- /dev/null +++ b/xio-socks5.c @@ -0,0 +1,655 @@ +/* source: xio-socks5.c */ +/* Copyright Gerhard Rieger and contributors (see file CHANGES) */ +/* Published under the GNU General Public License V.2, see file COPYING */ + +/* This file contains the source for opening addresses of socks5 type */ + +/* +* At the moment UDP ASSOCIATE is not supported, but CONNECT and BIND are. +* At the moment no authentication methods are supported (i.e only NO AUTH), +* which is technically not compliant with RFC1928. +*/ + +#include "xiosysincludes.h" + +#if WITH_SOCKS5 + +#include "xioopen.h" +#include "xio-ascii.h" +#include "xio-socket.h" +#include "xio-ip.h" +#include "xio-ipapp.h" + +#include "xio-socks5.h" + + +#define SOCKS5_VERSION 5 + +#define SOCKS5_MAX_REPLY_SIZE (6 + 256) + +#define SOCKS5_AUTH_NONE 0 +#define SOCKS5_AUTH_FAIL 0xff + +#define SOCKS5_COMMAND_CONNECT 1 +#define SOCKS5_COMMAND_BIND 2 +#define SOCKS5_COMMAND_UDP_ASSOCIATE 3 + +#define SOCKS5_ATYPE_IPv4 1 +#define SOCKS5_ATYPE_DOMAINNAME 3 +#define SOCKS5_ATYPE_IPv6 4 + +#define SOCKS5_STATUS_SUCCESS 0 +#define SOCKS5_STATUS_GENERAL_FAILURE 1 +#define SOCKS5_STATUS_CONNECTION_NOT_ALLOWED 2 +#define SOCKS5_STATUS_NETWORK_UNREACHABLE 3 +#define SOCKS5_STATUS_HOST_UNREACHABLE 4 +#define SOCKS5_STATUS_CONNECTION_REFUSED 5 +#define SOCKS5_STATUS_TTL_EXPIRED 6 +#define SOCKS5_STATUS_COMMAND_NOT_SUPPORTED 7 +#define SOCKS5_STATUS_ADDRESS_TYPE_NOT_SUPPORTED 8 + +static int xioopen_socks5(int argc, const char *argv[], struct opt *opts, + int xioflags, xiofile_t *xxfd, + groups_t groups, int dummy1, int dummy2, + int dummy3); + +const struct addrdesc xioaddr_socks5_connect = { "SOCKS5-CONNECT", 1+XIO_RDWR, xioopen_socks5, GROUP_FD|GROUP_SOCKET|GROUP_SOCK_IP4|GROUP_SOCK_IP6|GROUP_IP_TCP|GROUP_CHILD|GROUP_RETRY, SOCKS5_COMMAND_CONNECT, 0, 0 HELP("::::") }; + +const struct addrdesc xioaddr_socks5_listen = { "SOCKS5-LISTEN", 1+XIO_RDWR, xioopen_socks5, GROUP_FD|GROUP_SOCKET|GROUP_SOCK_IP4|GROUP_SOCK_IP6|GROUP_IP_TCP|GROUP_CHILD|GROUP_RETRY, SOCKS5_COMMAND_BIND, 0, 0 HELP("::::") }; + +static const char * _xioopen_socks5_strerror(uint8_t r) +{ + switch(r) { + case SOCKS5_STATUS_SUCCESS: + return "succeeded"; + case SOCKS5_STATUS_GENERAL_FAILURE: + return "general SOCKS server failure"; + case SOCKS5_STATUS_CONNECTION_NOT_ALLOWED: + return "connection not allowed by ruleset"; + case SOCKS5_STATUS_NETWORK_UNREACHABLE: + return "network unreachable"; + case SOCKS5_STATUS_HOST_UNREACHABLE: + return "host unreachable"; + case SOCKS5_STATUS_CONNECTION_REFUSED: + return "connection refused"; + case SOCKS5_STATUS_TTL_EXPIRED: + return "TTL expired"; + case SOCKS5_STATUS_COMMAND_NOT_SUPPORTED: + return "command not supported"; + case SOCKS5_STATUS_ADDRESS_TYPE_NOT_SUPPORTED: + return "address type not supported"; + default: + return "unknown error"; + } +} + +/* +* Performs the SOCKS5 handshake, i.e sends client hello and receives server +* hello back. +* If successful the connection is now ready for sending a SOCKS5 request. +* +* The code is unnecessarily complex right now, for what is essentially +* send(0x050100) followed by "return read() == 0x0500", but will be easier to +* extend for other auth mode support. +*/ +static int _xioopen_socks5_handshake(struct single *xfd, int level) +{ + int result; + ssize_t bytes; + struct socks5_server_hello server_hello; + int nmethods = 1; /* support only 1 auth method - no auth */ + int client_hello_size = + sizeof(struct socks5_client_hello) + + (sizeof(uint8_t) * nmethods); + + struct socks5_client_hello *client_hello = Malloc(client_hello_size); + if (client_hello == NULL) { + Msg2(level, "malloc(%d): %s", + client_hello_size, strerror(errno)); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + + /* malloc failed - could succeed later, so retry then */ + return STAT_RETRYLATER; + } + + unsigned char *server_hello_ptr = (unsigned char *)&server_hello; + + /* SOCKS5 Hello with 1 authentication mechanism - + 0x00 NO AUTHENTICATION */ + client_hello->version = SOCKS5_VERSION; + client_hello->nmethods = 1; + client_hello->methods[0]= SOCKS5_AUTH_NONE; + + /* Send SOCKS5 Client Hello */ + Info2("sending socks5 client hello version=%d nmethods=%d", + client_hello->version, + client_hello->nmethods); +#if WITH_MSGLEVEL <= E_DEBUG + { + char *msgbuf; + if ((msgbuf = Malloc(3 * client_hello_size)) != NULL) { + xiohexdump((unsigned char *)client_hello, + client_hello_size, msgbuf); + Debug1("sending socks5 client hello %s", msgbuf); + free(msgbuf); + } + } +#endif + + if (writefull(xfd->fd, client_hello, client_hello_size) < 0) { + Msg4(level, "write(%d, %p, %d): %s", + xfd->fd, client_hello, client_hello_size, + strerror(errno)); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + free(client_hello); + + /* writefull() failed, but might succeed later, so RETRYLATER */ + return STAT_RETRYLATER; + } + free(client_hello); + + bytes = 0; + Info("waiting for socks5 reply"); + while (bytes >= 0) { + do { + result = Read(xfd->fd, server_hello_ptr + bytes, + sizeof(struct socks5_server_hello)-bytes); + } while (result < 0 && errno == EINTR); + if (result < 0) { + Msg4(level, "read(%d, %p, "F_Zu"): %s", + xfd->fd, server_hello_ptr + bytes, + sizeof(struct socks5_server_hello)-bytes, + strerror(errno)); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + } + if (result == 0) { + Msg(level, "read(): EOF during read of SOCKS5 server hello, peer might not be a SOCKS5 server"); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, + strerror(errno)); + } + + return STAT_RETRYLATER; + } + + bytes += result; + if (bytes == sizeof(struct socks5_server_hello)) { + Debug1("received all "F_Zd" bytes", bytes); + break; + } + Debug2("received %d bytes, waiting for "F_Zu" more bytes", + result, sizeof(struct socks5_server_hello)-bytes); + } + if (result <= 0) { + return STAT_RETRYLATER; + } + + Info2("received SOCKS5 server hello version=%d method=%d", + server_hello.version, + server_hello.method); + + if (server_hello.version != SOCKS5_VERSION) { + Msg2(level, "SOCKS5 Server Hello version was %d, not the expected %d, peer might not be a SOCKS5 server", + server_hello.version, SOCKS5_VERSION); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + return STAT_RETRYLATER; + } + + if (server_hello.method == SOCKS5_AUTH_FAIL) { + Msg(level, "SOCKS5 authentication negotiation failed - client & server have no common supported methods"); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + return STAT_RETRYLATER; + } + + if (server_hello.method != SOCKS5_AUTH_NONE) { + Msg1(level, "SOCKS5 server requested unsupported auth method (%d)", + server_hello.method); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + return STAT_RETRYLATER; + } + + /* Server accepted using no auth */ + return STAT_OK; +} + +/* +* Generates the SOCKS5 request for a given command, host and port +*/ +static struct socks5_request *_xioopen_socks5_prepare_request( + int *bytes, const char *target_name, const char *target_port, + uint8_t socks_command, int level) +{ + struct socks5_request *req; + char ipaddr[16]; + uint16_t *dstport; + + *bytes = 0; + + if (inet_pton(AF_INET, target_name, ipaddr)){ /* if(valid_ipv4) */ + *bytes = sizeof(struct socks5_request) + 4 + sizeof(uint16_t); + req = (struct socks5_request *)Malloc(*bytes); + if (req == NULL){ + Info2("Malloc(%d): %s", *bytes, strerror(errno)); + return NULL; + } + + req->address_type = SOCKS5_ATYPE_IPv4; + memcpy(req->dstdata, ipaddr, 4); + + dstport = (uint16_t *) &req->dstdata[4]; + *dstport = parseport(target_port, IPPROTO_TCP); + } else if (inet_pton(AF_INET6, target_name, ipaddr)) { /* else if(valid_ipv6) */ + *bytes = sizeof(struct socks5_request) + 16 + sizeof(uint16_t); + req = (struct socks5_request *)Malloc(*bytes); + if (req == NULL){ + Info2("Malloc(%d): %s", *bytes, strerror(errno)); + return NULL; + } + + req->address_type = SOCKS5_ATYPE_IPv6; + memcpy(req->dstdata, ipaddr, 16); + + dstport = (uint16_t *) &req->dstdata[16]; + *dstport = parseport(target_port, IPPROTO_TCP); + } else { /* invalid IP, assume hostname */ + int hlen = strlen(target_name); + if (hlen > 255) { + Msg(level, "target hostname too long (>255 bytes), aborting"); + return NULL; + } + + *bytes = sizeof(struct socks5_request) + 1 + hlen + + sizeof(uint16_t); + req = (struct socks5_request *)Malloc(*bytes); + if (req == NULL ){ + Info2("malloc(%d): %s", *bytes, strerror(errno)); + return NULL; + } + + req->address_type = SOCKS5_ATYPE_DOMAINNAME; + req->dstdata[0] = (unsigned char) hlen; + memcpy(&req->dstdata[1], target_name, hlen); + + dstport = (uint16_t *) &req->dstdata[hlen + 1]; + *dstport = parseport(target_port, IPPROTO_TCP); + } + + + if (*dstport == 0){ + free(req); + return NULL; + } + + req->version = SOCKS5_VERSION; + req->command = socks_command; + req->reserved = 0; + + return req; +} + +/* +* Reads a server reply after a request has been sent +*/ +static int _xioopen_socks5_read_reply( + struct single *xfd, struct socks5_reply *reply, int level) +{ + int result = 0; + int bytes_read = 0; + int bytes_to_read = 5; + bool typechecked = false; + + while (bytes_to_read >= 0) { + Info("reading SOCKS5 reply"); + do { + result = Read(xfd->fd, + ((unsigned char *)reply) + bytes_read, + bytes_to_read-bytes_read); + } while (result < 0 && errno == EINTR); + if (result < 0) { + Msg4(level, "read(%d, %p, %d): %s", + xfd->fd, ((unsigned char *)reply) + bytes_read, + bytes_to_read-bytes_read, strerror(errno)); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + return STAT_RETRYLATER; + } + if (result == 0) { + Msg(level, "read(): EOF during read of SOCKS5 reply"); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", + xfd->fd, strerror(errno)); + } + return STAT_RETRYLATER; + } + bytes_read += result; + + /* Once we've read 5 bytes, figure out total message length and + * update bytes_to_read accordingly. */ + if (!typechecked && bytes_read <= 5) { + switch(reply->address_type) { + case SOCKS5_ATYPE_IPv4: + /* 6 fixed bytes, and 4 bytes for v4 address */ + bytes_to_read = 10; + break; + case SOCKS5_ATYPE_IPv6: + /* 6 fixed bytes, and 16 bytes for v6 address */ + bytes_to_read = 22; + break; + case SOCKS5_ATYPE_DOMAINNAME: + /* 6 fixed bytes, 1 byte for strlen, + and 0-255 bytes for domain name */ + bytes_to_read = 7 + reply->dstdata[0]; + break; + default: + Msg1(level, "invalid SOCKS5 reply address type (%d)", + reply->address_type); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", + xfd->fd, strerror(errno)); + } + return STAT_RETRYLATER; + } + typechecked = true; + continue; + } + + if (bytes_to_read == bytes_read) { + Debug1("received all %d bytes", bytes_read); + break; + } + + Debug2("received %d of %d bytes, waiting", + bytes_read, bytes_to_read); + } + + if (result <= 0) { + return STAT_RETRYLATER; + } + + return STAT_OK; +} + + +/* +* Sends a request and receives the reply. +* If command is BIND we receive two replies. +*/ +static int _xioopen_socks5_request( + struct single *xfd, const char *target_name, const char *target_port, + uint8_t socks_command, int level) +{ + struct socks5_request *req; + int bytes, result = 0; + + req = _xioopen_socks5_prepare_request(&bytes, target_name, target_port, + socks_command, level); + if (req == NULL) { + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + + /* Prepare_request could fail due to malloc, but most likely + the destination is invalid, e.g too long hostname, so NORETRY */ + return STAT_NORETRY; + } + + Info4("sending socks5 request version=%d command=%d reserved=%d address_type=%d", + req->version, req->command, req->reserved, req->address_type); + +#if WITH_MSGLEVEL <= E_DEBUG + { + char *msgbuf; + if ((msgbuf = Malloc(3 * bytes)) != NULL) { + xiohexdump((const unsigned char *)req, bytes, msgbuf); + Debug1("sending socks5 request %s", msgbuf); + free(msgbuf); + } + } +#endif + + if (writefull(xfd->fd, req, bytes) < 0) { + Msg4(level, "write(%d, %p, %d): %s", + xfd->fd, req, bytes, strerror(errno)); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + free(req); + return STAT_RETRYLATER; + } + free(req); + req = NULL; + + struct socks5_reply *reply = Malloc(SOCKS5_MAX_REPLY_SIZE); + if (reply == NULL) { + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + + return STAT_RETRYLATER; + } + + result = _xioopen_socks5_read_reply(xfd, reply, level); + if (result != STAT_OK) { + free(reply); + return result; + } + + /* TODO: maybe output nicer debug, like including address */ + Info3("received SOCKS5 reply version=%d reply=%d address_type=%d", + reply->version, reply->reply, reply->address_type); + + if (reply->version != SOCKS5_VERSION) { + Msg2(level, "SOCKS5 reply version was %d, not the expected %d, peer might not be a SOCKS5 server", + reply->version, SOCKS5_VERSION); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + free(reply); + return STAT_RETRYLATER; + } + + if (reply->reply == SOCKS5_STATUS_SUCCESS && + socks_command == SOCKS5_COMMAND_BIND) { + Notice("listening on remote host, waiting for connection"); + /* TODO: nicer debug output */ + /* For BIND, we read two replies */ + result = _xioopen_socks5_read_reply(xfd, reply, level); + if (result != STAT_OK) { + free(reply); + return result; + } + Notice("received connection on remote host"); + /* TODO: maybe output nicer debug, like including address */ + Info3("received second SOCKS5 reply version=%d reply=%d address_type=%d", + reply->version, reply->reply, reply->address_type); + } + + switch (reply->reply) { + case SOCKS5_STATUS_SUCCESS: + break; + default: + Msg2(level, "SOCKS5 server error %d: %s", + reply->reply, + _xioopen_socks5_strerror(reply->reply)); + if (Close(xfd->fd) < 0) { + Info2("close(%d): %s", xfd->fd, strerror(errno)); + } + free(reply); + return STAT_RETRYLATER; + } + + free(reply); + return STAT_OK; +} + +/* Same function for all socks5-modes, determined by argv[0] */ +static int xioopen_socks5( + int argc, const char *argv[], struct opt *opts, int xioflags, + xiofile_t *xxfd, groups_t groups, int socks_command, int dummy2, + int dummy3) +{ + bool dofork = false; + int socktype = SOCK_STREAM; + int pf = PF_UNSPEC; + int ipproto = IPPROTO_TCP; + int level, result; + struct opt *opts0 = NULL; + struct single *xfd = &xxfd->stream; + const char *socks_server, *target_name, *target_port, *socks_port; + union sockaddr_union us_sa, *us = &us_sa; + socklen_t uslen = sizeof(us_sa); + struct addrinfo *themlist, *themp; + bool needbind = false; + bool lowport = false; + char infobuff[256]; + + if (!xioparms.experimental) { + Error1("%s: requires option --experimental", argv[0]); + return STAT_NORETRY; + } + if (argc != 5) { + Error1("%s: 4 parameters required like ::::", + argv[0]); + return STAT_NORETRY; + } + + socks_server = argv[1]; + socks_port = argv[2]; + target_name = argv[3]; + target_port = argv[4]; + + xfd->howtoend = END_SHUTDOWN; + if (applyopts_single(xfd, opts, PH_INIT) < 0) return -1; + applyopts(-1, opts, PH_INIT); + + retropt_int(opts, OPT_SO_TYPE, &socktype); + retropt_bool(opts, OPT_FORK, &dofork); + + result = _xioopen_ipapp_prepare(opts, &opts0, socks_server, socks_port, + &pf, ipproto, + xfd->para.socket.ip.ai_flags, + &themlist, us, &uslen, + &needbind, &lowport, socktype); + + Notice2("connecting to socks5 server %s:%s", + socks_server, socks_port); + + do { +#if WITH_RETRY + if (xfd->forever || xfd->retry) { + level = E_INFO; + } else { + level = E_ERROR; + } +#endif + + /* loop over themlist */ + themp = themlist; + while (themp != NULL) { + Notice1("opening connection to %s", + sockaddr_info(themp->ai_addr, themp->ai_addrlen, + infobuff, sizeof(infobuff))); + result = _xioopen_connect(xfd, needbind?us:NULL, sizeof(*us), + themp->ai_addr, themp->ai_addrlen, + opts, pf?pf:themp->ai_family, socktype, + IPPROTO_TCP, lowport, level); + if (result == STAT_OK) + break; + themp = themp->ai_next; + if (themp == NULL) + result = STAT_RETRYLATER; + + switch(result){ + break; +#if WITH_RETRY + case STAT_RETRYLATER: + case STAT_RETRYNOW: + if (xfd->forever || xfd->retry-- ) { + if (result == STAT_RETRYLATER) Nanosleep(&xfd->intervall, NULL); + continue; + } +#endif + default: + xiofreeaddrinfo(themlist); + return result; + } + } + xiofreeaddrinfo(themlist); + applyopts(xfd->fd, opts, PH_ALL); + + if ((result = _xio_openlate(xfd, opts)) < 0) + return result; + + if ((result = _xioopen_socks5_handshake(xfd, level)) != STAT_OK) { + return result; + } + + result = _xioopen_socks5_request(xfd, target_name, target_port, socks_command, level); + switch (result) { + case STAT_OK: + break; +#if WITH_RETRY + case STAT_RETRYLATER: + case STAT_RETRYNOW: + if ( xfd->forever || xfd->retry-- ) { + if (result == STAT_RETRYLATER) Nanosleep(&xfd->intervall, NULL); + continue; + } +#endif + default: + return result; + } + + if (dofork) { + xiosetchilddied(); + } + +#if WITH_RETRY + if (dofork) { + pid_t pid; + int level = E_ERROR; + if (xfd->forever || xfd->retry) { + level = E_WARN; + } + while ((pid = xio_fork(false, level, xfd->shutup)) < 0) { + if (xfd->forever || --xfd->retry) { + Nanosleep(&xfd->intervall, NULL); + continue; + } + return STAT_RETRYLATER; + } + if ( pid == 0 ) { + xfd->forever = false; + xfd->retry = 0; + break; + } + + Close(xfd->fd); + Nanosleep(&xfd->intervall, NULL); + dropopts(opts, PH_ALL); + opts = copyopts(opts0, GROUP_ALL); + continue; + } else +#endif + { + break; + } + } while (true); + + return 0; +} + +#endif /* WITH_SOCKS5 */ diff --git a/xio-socks5.h b/xio-socks5.h new file mode 100644 index 0000000..4dab76b --- /dev/null +++ b/xio-socks5.h @@ -0,0 +1,41 @@ +/* source: xio-socks5.h */ +/* Copyright Gerhard Rieger and contributors (see file CHANGES) */ +/* Published under the GNU General Public License V.2, see file COPYING */ + +#ifndef __xio_socks5_h_included +#define __xio_socks5_h_included 1 + +#if WITH_SOCKS5 + +struct socks5_client_hello { + uint8_t version; + uint8_t nmethods; + uint8_t methods[]; +}; + +struct socks5_server_hello { + uint8_t version; + uint8_t method; +}; + +struct socks5_request { + uint8_t version; + uint8_t command; + uint8_t reserved; + uint8_t address_type; + char dstdata[]; +}; + +struct socks5_reply { + uint8_t version; + uint8_t reply; + uint8_t reserved; + uint8_t address_type; + char dstdata[]; +}; + +extern const struct addrdesc xioaddr_socks5_connect; +extern const struct addrdesc xioaddr_socks5_listen; + +#endif /* WITH_SOCKS5 */ +#endif /* !defined(__xio_socks5_h_included) */ diff --git a/xiomodes.h b/xiomodes.h index 8ab1b9a..2faa2ae 100644 --- a/xiomodes.h +++ b/xiomodes.h @@ -32,6 +32,7 @@ #include "xio-udp.h" #include "xio-sctp.h" #include "xio-socks.h" +#include "xio-socks5.h" #include "xio-proxy.h" #include "xio-vsock.h" #endif /* _WITH_SOCKET */ diff --git a/xioopen.c b/xioopen.c index b3646ee..ca93f88 100644 --- a/xioopen.c +++ b/xioopen.c @@ -207,6 +207,12 @@ const struct addrname addressnames[] = { #if WITH_SOCKS4A { "SOCKS4A", &xioaddr_socks4a_connect }, #endif +#if WITH_SOCKS5 + { "SOCKS5", &xioaddr_socks5_connect }, + { "SOCKS5-BIND", &xioaddr_socks5_listen }, + { "SOCKS5-CONNECT", &xioaddr_socks5_connect }, + { "SOCKS5-LISTEN", &xioaddr_socks5_listen }, +#endif #if WITH_OPENSSL { "SSL", &xioaddr_openssl }, #if WITH_LISTEN