From 26ef788f9b38d9a4a4de338b6509824eea658274 Mon Sep 17 00:00:00 2001 From: James Bunton Date: Wed, 13 Aug 2014 07:48:49 +1000 Subject: [PATCH] Initial commit v0.1 --- .gitignore | 2 + Makefile | 14 ++ rfc1928.txt | 507 +++++++++++++++++++++++++++++++++++++ socks5server.c | 669 +++++++++++++++++++++++++++++++++++++++++++++++++ test_proxy.py | 401 +++++++++++++++++++++++++++++ 5 files changed, 1593 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 rfc1928.txt create mode 100644 socks5server.c create mode 100755 test_proxy.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df07823 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +socks5server +.*.swp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cf1ab52 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +VERSION := 0.1 +CFLAGS += -Wall -Wextra -Werror -std=c99 -DVERSION='"$(VERSION)"' +LDFLAGS += -lpthread -lbsd + +socks5server: socks5server.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ + +test: socks5server + ./test_proxy.py + +clean: + rm -f socks5server +.PHONY: clean + diff --git a/rfc1928.txt b/rfc1928.txt new file mode 100644 index 0000000..46bf46e --- /dev/null +++ b/rfc1928.txt @@ -0,0 +1,507 @@ + + + + + + +Network Working Group M. Leech +Request for Comments: 1928 Bell-Northern Research Ltd +Category: Standards Track M. Ganis + International Business Machines + Y. Lee + NEC Systems Laboratory + R. Kuris + Unify Corporation + D. Koblas + Independent Consultant + L. Jones + Hewlett-Packard Company + March 1996 + + + SOCKS Protocol Version 5 + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Acknowledgments + + This memo describes a protocol that is an evolution of the previous + version of the protocol, version 4 [1]. This new protocol stems from + active discussions and prototype implementations. The key + contributors are: Marcus Leech: Bell-Northern Research, David Koblas: + Independent Consultant, Ying-Da Lee: NEC Systems Laboratory, LaMont + Jones: Hewlett-Packard Company, Ron Kuris: Unify Corporation, Matt + Ganis: International Business Machines. + +1. Introduction + + The use of network firewalls, systems that effectively isolate an + organizations internal network structure from an exterior network, + such as the INTERNET is becoming increasingly popular. These + firewall systems typically act as application-layer gateways between + networks, usually offering controlled TELNET, FTP, and SMTP access. + With the emergence of more sophisticated application layer protocols + designed to facilitate global information discovery, there exists a + need to provide a general framework for these protocols to + transparently and securely traverse a firewall. + + + + + +Leech, et al Standards Track [Page 1] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + + There exists, also, a need for strong authentication of such + traversal in as fine-grained a manner as is practical. This + requirement stems from the realization that client-server + relationships emerge between the networks of various organizations, + and that such relationships need to be controlled and often strongly + authenticated. + + The protocol described here is designed to provide a framework for + client-server applications in both the TCP and UDP domains to + conveniently and securely use the services of a network firewall. + The protocol is conceptually a "shim-layer" between the application + layer and the transport layer, and as such does not provide network- + layer gateway services, such as forwarding of ICMP messages. + +2. Existing practice + + There currently exists a protocol, SOCKS Version 4, that provides for + unsecured firewall traversal for TCP-based client-server + applications, including TELNET, FTP and the popular information- + discovery protocols such as HTTP, WAIS and GOPHER. + + This new protocol extends the SOCKS Version 4 model to include UDP, + and extends the framework to include provisions for generalized + strong authentication schemes, and extends the addressing scheme to + encompass domain-name and V6 IP addresses. + + The implementation of the SOCKS protocol typically involves the + recompilation or relinking of TCP-based client applications to use + the appropriate encapsulation routines in the SOCKS library. + +Note: + + Unless otherwise noted, the decimal numbers appearing in packet- + format diagrams represent the length of the corresponding field, in + octets. Where a given octet must take on a specific value, the + syntax X'hh' is used to denote the value of the single octet in that + field. When the word 'Variable' is used, it indicates that the + corresponding field has a variable length defined either by an + associated (one or two octet) length field, or by a data type field. + +3. Procedure for TCP-based clients + + When a TCP-based client wishes to establish a connection to an object + that is reachable only via a firewall (such determination is left up + to the implementation), it must open a TCP connection to the + appropriate SOCKS port on the SOCKS server system. The SOCKS service + is conventionally located on TCP port 1080. If the connection + request succeeds, the client enters a negotiation for the + + + +Leech, et al Standards Track [Page 2] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + + authentication method to be used, authenticates with the chosen + method, then sends a relay request. The SOCKS server evaluates the + request, and either establishes the appropriate connection or denies + it. + + Unless otherwise noted, the decimal numbers appearing in packet- + format diagrams represent the length of the corresponding field, in + octets. Where a given octet must take on a specific value, the + syntax X'hh' is used to denote the value of the single octet in that + field. When the word 'Variable' is used, it indicates that the + corresponding field has a variable length defined either by an + associated (one or two octet) length field, or by a data type field. + + The client connects to the server, and sends a version + identifier/method selection message: + + +----+----------+----------+ + |VER | NMETHODS | METHODS | + +----+----------+----------+ + | 1 | 1 | 1 to 255 | + +----+----------+----------+ + + The VER field is set to X'05' for this version of the protocol. The + NMETHODS field contains the number of method identifier octets that + appear in the METHODS field. + + The server selects from one of the methods given in METHODS, and + sends a METHOD selection message: + + +----+--------+ + |VER | METHOD | + +----+--------+ + | 1 | 1 | + +----+--------+ + + If the selected METHOD is X'FF', none of the methods listed by the + client are acceptable, and the client MUST close the connection. + + The values currently defined for METHOD are: + + o X'00' NO AUTHENTICATION REQUIRED + o X'01' GSSAPI + o X'02' USERNAME/PASSWORD + o X'03' to X'7F' IANA ASSIGNED + o X'80' to X'FE' RESERVED FOR PRIVATE METHODS + o X'FF' NO ACCEPTABLE METHODS + + The client and server then enter a method-specific sub-negotiation. + + + +Leech, et al Standards Track [Page 3] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + + Descriptions of the method-dependent sub-negotiations appear in + separate memos. + + Developers of new METHOD support for this protocol should contact + IANA for a METHOD number. The ASSIGNED NUMBERS document should be + referred to for a current list of METHOD numbers and their + corresponding protocols. + + Compliant implementations MUST support GSSAPI and SHOULD support + USERNAME/PASSWORD authentication methods. + +4. Requests + + Once the method-dependent subnegotiation has completed, the client + sends the request details. If the negotiated method includes + encapsulation for purposes of integrity checking and/or + confidentiality, these requests MUST be encapsulated in the method- + dependent encapsulation. + + The SOCKS request is formed as follows: + + +----+-----+-------+------+----------+----------+ + |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + +----+-----+-------+------+----------+----------+ + | 1 | 1 | X'00' | 1 | Variable | 2 | + +----+-----+-------+------+----------+----------+ + + Where: + + o VER protocol version: X'05' + o CMD + o CONNECT X'01' + o BIND X'02' + o UDP ASSOCIATE X'03' + o RSV RESERVED + o ATYP address type of following address + o IP V4 address: X'01' + o DOMAINNAME: X'03' + o IP V6 address: X'04' + o DST.ADDR desired destination address + o DST.PORT desired destination port in network octet + order + + The SOCKS server will typically evaluate the request based on source + and destination addresses, and return one or more reply messages, as + appropriate for the request type. + + + + + +Leech, et al Standards Track [Page 4] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + +5. Addressing + + In an address field (DST.ADDR, BND.ADDR), the ATYP field specifies + the type of address contained within the field: + + o X'01' + + the address is a version-4 IP address, with a length of 4 octets + + o X'03' + + the address field contains a fully-qualified domain name. The first + octet of the address field contains the number of octets of name that + follow, there is no terminating NUL octet. + + o X'04' + + the address is a version-6 IP address, with a length of 16 octets. + +6. Replies + + The SOCKS request information is sent by the client as soon as it has + established a connection to the SOCKS server, and completed the + authentication negotiations. The server evaluates the request, and + returns a reply formed as follows: + + +----+-----+-------+------+----------+----------+ + |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + +----+-----+-------+------+----------+----------+ + | 1 | 1 | X'00' | 1 | Variable | 2 | + +----+-----+-------+------+----------+----------+ + + Where: + + o VER protocol version: X'05' + o REP Reply field: + o X'00' succeeded + o X'01' general SOCKS server failure + o X'02' connection not allowed by ruleset + o X'03' Network unreachable + o X'04' Host unreachable + o X'05' Connection refused + o X'06' TTL expired + o X'07' Command not supported + o X'08' Address type not supported + o X'09' to X'FF' unassigned + o RSV RESERVED + o ATYP address type of following address + + + +Leech, et al Standards Track [Page 5] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + + o IP V4 address: X'01' + o DOMAINNAME: X'03' + o IP V6 address: X'04' + o BND.ADDR server bound address + o BND.PORT server bound port in network octet order + + Fields marked RESERVED (RSV) must be set to X'00'. + + If the chosen method includes encapsulation for purposes of + authentication, integrity and/or confidentiality, the replies are + encapsulated in the method-dependent encapsulation. + +CONNECT + + In the reply to a CONNECT, BND.PORT contains the port number that the + server assigned to connect to the target host, while BND.ADDR + contains the associated IP address. The supplied BND.ADDR is often + different from the IP address that the client uses to reach the SOCKS + server, since such servers are often multi-homed. It is expected + that the SOCKS server will use DST.ADDR and DST.PORT, and the + client-side source address and port in evaluating the CONNECT + request. + +BIND + + The BIND request is used in protocols which require the client to + accept connections from the server. FTP is a well-known example, + which uses the primary client-to-server connection for commands and + status reports, but may use a server-to-client connection for + transferring data on demand (e.g. LS, GET, PUT). + + It is expected that the client side of an application protocol will + use the BIND request only to establish secondary connections after a + primary connection is established using CONNECT. In is expected that + a SOCKS server will use DST.ADDR and DST.PORT in evaluating the BIND + request. + + Two replies are sent from the SOCKS server to the client during a + BIND operation. The first is sent after the server creates and binds + a new socket. The BND.PORT field contains the port number that the + SOCKS server assigned to listen for an incoming connection. The + BND.ADDR field contains the associated IP address. The client will + typically use these pieces of information to notify (via the primary + or control connection) the application server of the rendezvous + address. The second reply occurs only after the anticipated incoming + connection succeeds or fails. + + + + + +Leech, et al Standards Track [Page 6] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + + In the second reply, the BND.PORT and BND.ADDR fields contain the + address and port number of the connecting host. + +UDP ASSOCIATE + + The UDP ASSOCIATE request is used to establish an association within + the UDP relay process to handle UDP datagrams. The DST.ADDR and + DST.PORT fields contain the address and port that the client expects + to use to send UDP datagrams on for the association. The server MAY + use this information to limit access to the association. If the + client is not in possesion of the information at the time of the UDP + ASSOCIATE, the client MUST use a port number and address of all + zeros. + + A UDP association terminates when the TCP connection that the UDP + ASSOCIATE request arrived on terminates. + + In the reply to a UDP ASSOCIATE request, the BND.PORT and BND.ADDR + fields indicate the port number/address where the client MUST send + UDP request messages to be relayed. + +Reply Processing + + When a reply (REP value other than X'00') indicates a failure, the + SOCKS server MUST terminate the TCP connection shortly after sending + the reply. This must be no more than 10 seconds after detecting the + condition that caused a failure. + + If the reply code (REP value of X'00') indicates a success, and the + request was either a BIND or a CONNECT, the client may now start + passing data. If the selected authentication method supports + encapsulation for the purposes of integrity, authentication and/or + confidentiality, the data are encapsulated using the method-dependent + encapsulation. Similarly, when data arrives at the SOCKS server for + the client, the server MUST encapsulate the data as appropriate for + the authentication method in use. + +7. Procedure for UDP-based clients + + A UDP-based client MUST send its datagrams to the UDP relay server at + the UDP port indicated by BND.PORT in the reply to the UDP ASSOCIATE + request. If the selected authentication method provides + encapsulation for the purposes of authenticity, integrity, and/or + confidentiality, the datagram MUST be encapsulated using the + appropriate encapsulation. Each UDP datagram carries a UDP request + header with it: + + + + + +Leech, et al Standards Track [Page 7] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + + +----+------+------+----------+----------+----------+ + |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | + +----+------+------+----------+----------+----------+ + | 2 | 1 | 1 | Variable | 2 | Variable | + +----+------+------+----------+----------+----------+ + + The fields in the UDP request header are: + + o RSV Reserved X'0000' + o FRAG Current fragment number + o ATYP address type of following addresses: + o IP V4 address: X'01' + o DOMAINNAME: X'03' + o IP V6 address: X'04' + o DST.ADDR desired destination address + o DST.PORT desired destination port + o DATA user data + + When a UDP relay server decides to relay a UDP datagram, it does so + silently, without any notification to the requesting client. + Similarly, it will drop datagrams it cannot or will not relay. When + a UDP relay server receives a reply datagram from a remote host, it + MUST encapsulate that datagram using the above UDP request header, + and any authentication-method-dependent encapsulation. + + The UDP relay server MUST acquire from the SOCKS server the expected + IP address of the client that will send datagrams to the BND.PORT + given in the reply to UDP ASSOCIATE. It MUST drop any datagrams + arriving from any source IP address other than the one recorded for + the particular association. + + The FRAG field indicates whether or not this datagram is one of a + number of fragments. If implemented, the high-order bit indicates + end-of-fragment sequence, while a value of X'00' indicates that this + datagram is standalone. Values between 1 and 127 indicate the + fragment position within a fragment sequence. Each receiver will + have a REASSEMBLY QUEUE and a REASSEMBLY TIMER associated with these + fragments. The reassembly queue must be reinitialized and the + associated fragments abandoned whenever the REASSEMBLY TIMER expires, + or a new datagram arrives carrying a FRAG field whose value is less + than the highest FRAG value processed for this fragment sequence. + The reassembly timer MUST be no less than 5 seconds. It is + recommended that fragmentation be avoided by applications wherever + possible. + + Implementation of fragmentation is optional; an implementation that + does not support fragmentation MUST drop any datagram whose FRAG + field is other than X'00'. + + + +Leech, et al Standards Track [Page 8] + +RFC 1928 SOCKS Protocol Version 5 March 1996 + + + The programming interface for a SOCKS-aware UDP MUST report an + available buffer space for UDP datagrams that is smaller than the + actual space provided by the operating system: + + o if ATYP is X'01' - 10+method_dependent octets smaller + o if ATYP is X'03' - 262+method_dependent octets smaller + o if ATYP is X'04' - 20+method_dependent octets smaller + +8. Security Considerations + + This document describes a protocol for the application-layer + traversal of IP network firewalls. The security of such traversal is + highly dependent on the particular authentication and encapsulation + methods provided in a particular implementation, and selected during + negotiation between SOCKS client and SOCKS server. + + Careful consideration should be given by the administrator to the + selection of authentication methods. + +9. References + + [1] Koblas, D., "SOCKS", Proceedings: 1992 Usenix Security Symposium. + +Author's Address + + Marcus Leech + Bell-Northern Research Ltd + P.O. Box 3511, Stn. C, + Ottawa, ON + CANADA K1Y 4H7 + + Phone: (613) 763-9145 + EMail: mleech@bnr.ca + + + + + + + + + + + + + + + + + + +Leech, et al Standards Track [Page 9] + diff --git a/socks5server.c b/socks5server.c new file mode 100644 index 0000000..592bef0 --- /dev/null +++ b/socks5server.c @@ -0,0 +1,669 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + + + +/*********** + * Logging * + ***********/ + +static __thread char* log_context = ""; + +#define log_errno(fmt, ...) \ + fprintf(stderr, "ERROR [%s] " fmt ": %s\n", log_context , ##__VA_ARGS__ , strerror(errno)); + +#define log_error(fmt, ...) \ + fprintf(stderr, "ERROR [%s] " fmt "\n", log_context , ##__VA_ARGS__); + +#define log_warn(fmt, ...) \ + fprintf(stderr, "WARN [%s] " fmt "\n", log_context , ##__VA_ARGS__); + +#define log_info(fmt, ...) \ + fprintf(stderr, "INFO [%s] " fmt "\n", log_context , ##__VA_ARGS__); + +#ifdef DEBUG +#define log_debug(fmt, ...) \ + fprintf(stderr, "DEBUG [%s] " fmt "\n", log_context , ##__VA_ARGS__); +#else +#define log_debug(fmt, ...) +#endif + +void log_context_set(char* new_context) { + log_context = new_context; + log_debug("Thread started"); +} + +void log_context_clear() { + log_debug("Thread finished"); + log_context = ""; +} + + + +/************* + * Utilities * + *************/ + +bool itoa(char* buf, int buflen, int64_t val) { + int result = snprintf(buf, buflen, "%ld", val); + return result > 0 && result < buflen; +} + +void sockaddr_stringify(char* dest, size_t dest_length, struct sockaddr_storage* addr) { + if (addr->ss_family == AF_INET) { + inet_ntop(AF_INET, &((struct sockaddr_in*)addr)->sin_addr, dest, dest_length); + } else if (addr->ss_family == AF_INET6) { + inet_ntop(AF_INET6, &((struct sockaddr_in6*)addr)->sin6_addr, dest, dest_length); + } else { + strlcpy(dest, "Unknown socket family", dest_length); + } +} + +char* sockaddr_tostring(struct sockaddr_storage* addr) { + static char buf[INET6_ADDRSTRLEN]; + sockaddr_stringify(buf, sizeof(buf), addr); + return buf; +} + +void* smalloc(size_t sz) { + void* ptr = calloc(1, sz); + if (ptr == NULL) { + log_errno("Memory allocation failed"); + exit(1); + } + return ptr; +} + + + +/********** + * Config * + **********/ + +extern char** environ; + +int getenv_int(char* key) { + char* value = getenv(key); + if (value == NULL) { + log_error("Missing environment variable: %s", key); + exit(1); + } + + return atoi(value); +} + +char* extract_env_value(char* str) { + char ch; + while ((ch = *str++)) { + if (ch == '=') { + return str; + } + } + return ""; +} + +bool getenv_list_matcher(char* key, bool (*matches)(char*, void*), void* data) { + size_t key_length = strlen(key); + + char** ptr = environ; + + while (*ptr) { + if (strncmp(*ptr, key, key_length) == 0) { + char* value = extract_env_value(*ptr); + if (matches(value, data)) { + return true; + } + } + ++ptr; + } + + return false; +} + + + + +/************** + * Network IO * + **************/ + +bool ssend(int fd, void* buf, size_t len) { + while (len > 0) { + ssize_t sz = send(fd, buf, len, MSG_NOSIGNAL); + if (sz < 0) { + log_errno("Failed to write, closing connection"); + return false; + } + buf += sz; + len -= sz; + } + return true; +} + +ssize_t srecv(int fd, void* buf, size_t len) { + return recv(fd, buf, len, 0); +} + +bool read_bytes(int fd, uint8_t* value, ssize_t sz) { + if (srecv(fd, value, sz) != sz) { + log_errno("Failed to read from client socket"); + return false; + } + for (ssize_t i = 0; i < sz; ++i) { + log_debug("got byte: %x", value[i]); + } + return true; +} + +bool read_byte_check(int fd, uint8_t expected, char* msg) { + uint8_t actual; + if (!read_bytes(fd, &actual, 1)) { + return false; + } + if (actual != expected) { + log_warn("%s", msg); + return false; + } + return true; +} + + + +/********** + * Server * + **********/ + +void proxy_copy_fd(int wfd, int rfd) { + log_debug("proxy_copy_fd %d %d", wfd, rfd); + + char buf[32768]; + + for (;;) { + log_debug("proxy_copy_fd blocking on read %d", rfd); + int bufsz = srecv(rfd, buf, sizeof(buf)); + log_debug("proxy_copy_fd unblocked from read %d", rfd); + if (bufsz < 0) { + log_errno("Failed to read, closing connection"); + break; + } + if (bufsz == 0) { + log_debug("proxy_copy_fd end of file %d %d", wfd, rfd); + break; + } + + log_debug("proxy_copy_fd blocking on write %d", wfd); + if (!ssend(wfd, buf, bufsz)) { + log_errno("Failed to write, closing connection"); + break; + } + log_debug("proxy_copy_fd unblocked from write %d", wfd); + } + + shutdown(wfd, SHUT_WR); + shutdown(rfd, SHUT_RD); +} + + +struct proxy_copy_from_dest_data { + pthread_t tid; + int src_fd; + int dst_fd; + char log_context[INET6_ADDRSTRLEN]; +}; + +void* proxy_copy_from_dest(void* _data) { + struct proxy_copy_from_dest_data* data = (struct proxy_copy_from_dest_data*)_data; + log_context_set(data->log_context); + + proxy_copy_fd(data->dst_fd, data->src_fd); + + log_context_clear(); + free(data); + + return NULL; +} + +bool proxy_create_copy_from_dest_thread( + int dst_fd, + int src_fd +) { + struct proxy_copy_from_dest_data* data = smalloc(sizeof(struct proxy_copy_from_dest_data)); + data->dst_fd = dst_fd; + data->src_fd = src_fd; + strlcpy(data->log_context, log_context, sizeof(data->log_context)); + if (pthread_create(&data->tid, NULL, &proxy_copy_from_dest, data) != 0) { + log_errno("Failed to spawn thread"); + exit(1); + } + return true; +} + +bool proxy_negotiate_auth(int fd) { + if (!read_byte_check(fd, 5, "Client sent invalid version!")) { + return false; + } + + uint8_t nmethods; + if (!read_bytes(fd, &nmethods, 1)) { + return false; + } + if (nmethods < 1) { + log_warn("Client sent invalid authentication method count!"); + return false; + } + + uint8_t methods[nmethods]; + if (!read_bytes(fd, methods, nmethods)) { + return false; + } + bool found_noauth = false; + for (int i = 0; i < nmethods; ++i) { + if (methods[i] == 0) { + found_noauth = true; + break; + } + } + if (!found_noauth) { + ssend(fd, "\x05\xff", 2); + log_warn("Client does not support no-authentication SOCKS5!"); + return false; + } + + if (!ssend(fd, "\x05\x00", 2)) { + return false; + } + + return true; +} + +bool proxy_write_error_response(int fd, uint8_t errcode) { + char response[] = "\x05\xff\x00\x01\x00\x00\x00\x00\x00\x00"; + int length = sizeof(response) - 1; // remove trailing null + response[1] = errcode; + if (!ssend(fd, response, length)) { + return false; + } + return true; +} + +bool proxy_dns_handle_request(int* dst_fd, int fd) { + uint8_t name_length; + if (!read_bytes(fd, (uint8_t*)&name_length, 1)) { + return false; + } + if (name_length < 1) { + log_warn("Client sent invalid name length!"); + return false; + } + + char name[name_length+1]; + if (!read_bytes(fd, (uint8_t*)name, name_length)) { + return false; + } + name[name_length] = '\0'; + + uint16_t port_int; + if (!read_bytes(fd, (uint8_t*)&port_int, 2)) { + return false; + } + port_int = ntohs(port_int); + char port[8]; + if (!itoa(port, sizeof(port), port_int)) { + return false; + } + + + struct addrinfo hints; + struct addrinfo* result; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICSERV; + hints.ai_protocol = 0; + + log_debug("proxy_dns_handle_request getaddrinfo before '%s' '%s'", name, port); + if (getaddrinfo(name, port, &hints, &result) != 0) { + log_warn("proxy_dns_handle_request getaddrinfo failed"); + proxy_write_error_response(fd, 4); + return false; + } + log_debug("proxy_dns_handle_request getaddrinfo success"); + + for (struct addrinfo* rp = result; rp != NULL; rp = rp->ai_next) { + *dst_fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (*dst_fd < 0) { + continue; + } + + if (connect(*dst_fd, rp->ai_addr, rp->ai_addrlen) == 0) { + proxy_write_error_response(fd, 0); + freeaddrinfo(result); + return true; + } + + close(*dst_fd); + } + + proxy_write_error_response(fd, 4); + freeaddrinfo(result); + return false; +} + +bool proxy_ipv6_handle_request(int* dst_fd, int fd) { + struct sockaddr_in6 dst_addr; + memset(&dst_addr, 0, sizeof(dst_addr)); + dst_addr.sin6_family = AF_INET6; + + if (!read_bytes(fd, (uint8_t*)&dst_addr.sin6_addr, 16)) { + return false; + } + + if (!read_bytes(fd, (uint8_t*)&dst_addr.sin6_port, 2)) { + return false; + } + + log_debug("got addr: %s", sockaddr_tostring((struct sockaddr_storage*)&dst_addr)); + + *dst_fd = socket(AF_INET6, SOCK_STREAM, 0); + if (*dst_fd < 0) { + log_errno("Failed to create socket"); + exit(1); + } + + if (connect(*dst_fd, (struct sockaddr*)&dst_addr, sizeof(dst_addr)) != 0) { + proxy_write_error_response(fd, 4); + return false; + } + + proxy_write_error_response(fd, 0); + return true; +} + +bool proxy_ipv4_handle_request(int* dst_fd, int fd) { + struct sockaddr_in dst_addr; + memset(&dst_addr, 0, sizeof(dst_addr)); + dst_addr.sin_family = AF_INET; + + if (!read_bytes(fd, (uint8_t*)&dst_addr.sin_addr, 4)) { + return false; + } + + if (!read_bytes(fd, (uint8_t*)&dst_addr.sin_port, 2)) { + return false; + } + + log_debug("got addr: %s", sockaddr_tostring((struct sockaddr_storage*)&dst_addr)); + + *dst_fd = socket(AF_INET, SOCK_STREAM, 0); + if (*dst_fd < 0) { + log_errno("Failed to create socket"); + exit(1); + } + + if (connect(*dst_fd, (struct sockaddr*)&dst_addr, sizeof(dst_addr)) != 0) { + proxy_write_error_response(fd, 4); + return false; + } + + proxy_write_error_response(fd, 0); + return true; +} + +bool proxy_handle_request(int* dst_fd, int fd) { + if (!read_byte_check(fd, 5, "Client sent invalid version!")) { + proxy_write_error_response(fd, 1); + return false; + } + + if (!read_byte_check(fd, 1, "Client sent invalid command!")) { + proxy_write_error_response(fd, 7); + return false; + } + + if (!read_byte_check(fd, 0, "Client sent invalid reserved section!")) { + proxy_write_error_response(fd, 1); + return false; + } + + uint8_t addr_type; + if (!read_bytes(fd, &addr_type, 1)) { + return false; + } + switch (addr_type) { + case 1: + return proxy_ipv4_handle_request(dst_fd, fd); + case 3: + return proxy_dns_handle_request(dst_fd, fd); + case 4: + return proxy_ipv6_handle_request(dst_fd, fd); + } + + proxy_write_error_response(fd, 8); + return false; +} + +struct proxy_handle_accept_data { + pthread_t tid; + int fd; + char log_context[INET6_ADDRSTRLEN]; +}; + +void* proxy_handle_accept(void* _data) { + struct proxy_handle_accept_data* data = (struct proxy_handle_accept_data*)_data; + log_context_set(data->log_context); + + int dst_fd = -1; + + log_debug("proxy_negotiate_auth before %d", data->fd); + if (!proxy_negotiate_auth(data->fd)) { + log_debug("negotiate_auth fail"); + goto finish; + } + + log_debug("proxy_handle_request before"); + if (!proxy_handle_request(&dst_fd, data->fd)) { + log_debug("proxy_handle_request fail"); + goto finish; + } + + log_debug("proxy_create_copy_from_dest_thread before %d %d", dst_fd, data->fd); + if (!proxy_create_copy_from_dest_thread(dst_fd, data->fd)) { + log_debug("proxy_create_copy_from_dest_thread fail"); + goto finish; + } + + // opposite direction to the thread we just created + log_debug("proxy_copy_fd before %d %d", data->fd, dst_fd); + proxy_copy_fd(data->fd, dst_fd); + log_debug("proxy_copy_fd after"); + +finish: + if (dst_fd >= 0) { + close(dst_fd); + } + close(data->fd); + + log_context_clear(); + + free(data); + + return NULL; +} + +bool sockaddr_equals_addrinfo(struct sockaddr_storage* addr1, struct addrinfo* addr2) { + void* addr1_ptr = NULL; + void* addr2_ptr = NULL; + socklen_t length = 0; + + if (addr1->ss_family == AF_INET && addr2->ai_family == AF_INET) { + addr1_ptr = &(((struct sockaddr_in*)addr1)->sin_addr); + addr2_ptr = &((struct sockaddr_in*)addr2->ai_addr)->sin_addr; + length = sizeof(struct in_addr); + } else if (addr1->ss_family == AF_INET6 && addr2->ai_family == AF_INET6) { + addr1_ptr = &(((struct sockaddr_in6*)addr1)->sin6_addr); + addr2_ptr = &((struct sockaddr_in6*)addr2->ai_addr)->sin6_addr; + length = sizeof(struct in6_addr); + } else { + return false; + } + + return memcmp(addr1_ptr, addr2_ptr, length) == 0; +} + +bool sockaddr_matches_string(char* str, void* _addr) { + struct sockaddr_storage* addr = (struct sockaddr_storage*)_addr; + + struct addrinfo hints; + struct addrinfo* result; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICSERV; + hints.ai_protocol = 0; + + if (getaddrinfo(str, NULL, &hints, &result) != 0) { + log_warn("sockaddr_matches_string getaddrinfo failed: %s", str); + return false; + } + + bool found = false; + for (struct addrinfo* rp = result; rp != NULL; rp = rp->ai_next) { + if (sockaddr_equals_addrinfo(addr, rp)) { + found = true; + break; + } + } + + freeaddrinfo(result); + return found; +} + +bool server_has_permission(struct sockaddr_storage* addr) { + if (getenv("ALLOW_ALL") != NULL) { + log_debug("Accepting connection because ALLOW_ALL is set"); + return true; + } + + if (getenv_list_matcher("ALLOW_HOST", sockaddr_matches_string, addr)) { + log_debug("Accepting connection because it matches ALLOW_HOST entry"); + return true; + } + + log_info("Rejecting connection: %s", sockaddr_tostring(addr)); + + return false; +} + +void rewrite_ipv4_mapped_address(struct sockaddr_storage* addr) { + if (addr->ss_family != AF_INET6) { + return; + } + + struct in6_addr* addr_data = &((struct sockaddr_in6*)addr)->sin6_addr; + if (!( + ((uint32_t*)(addr_data))[0] == 0 && + ((uint32_t*)(addr_data))[1] == 0 && + ((uint32_t*)(addr_data))[2] == htonl(0xffff) + )) { + return; + } + + struct sockaddr_in new_addr; + memset(&new_addr, 0, sizeof(struct sockaddr)); + new_addr.sin_family = AF_INET; + memcpy(&new_addr.sin_addr.s_addr, &((struct sockaddr_in6*)addr)->sin6_addr.s6_addr[12], 4); + memcpy(addr, &new_addr, sizeof(struct sockaddr_in)); +} + +void server_accept_connection(int listen_fd) { + struct sockaddr_storage client_addr; + socklen_t client_addr_length = sizeof(client_addr); + + int fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_length); + if (fd < 0) { + log_errno("Failed to accept connection"); + exit(1); + } + + rewrite_ipv4_mapped_address(&client_addr); + + if (!server_has_permission(&client_addr)) { + close(fd); + return; + } + + log_info("Accepting connection: %s", sockaddr_tostring(&client_addr)); + + struct proxy_handle_accept_data* data = smalloc(sizeof(struct proxy_handle_accept_data)); + data->fd = fd; + sockaddr_stringify(data->log_context, sizeof(data->log_context), &client_addr); + if (pthread_create(&data->tid, NULL, &proxy_handle_accept, data) != 0) { + log_errno("Failed to spawn thread"); + exit(1); + } +} + +int server_create_socket(int listen_port) { + int listen_fd = socket(AF_INET6, SOCK_STREAM, 0); + if (listen_fd < 0) { + log_errno("Failed to create socket"); + exit(1); + } + + int yes = 1; + if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) != 0) { + log_errno("Failed to set SO_REUSEADDR"); + exit(1); + } + + + struct sockaddr_in6 server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin6_family = AF_INET6; + server_addr.sin6_addr = in6addr_any; + server_addr.sin6_port = htons(listen_port); + + if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { + log_errno("Failed to bind to port"); + exit(1); + } + + if (listen(listen_fd, 5) < 0) { + log_errno("Failed to listen on socket"); + exit(1); + } + + return listen_fd; +} + + + +int main() { + printf("socks5server " VERSION "\n"); + log_context = "acceptor"; + + int listen_port = getenv_int("LISTEN_PORT"); + int listen_fd = server_create_socket(listen_port); + log_info("Listening on port %d", listen_port); + + for (;;) { + server_accept_connection(listen_fd); + } + + return 0; +} diff --git a/test_proxy.py b/test_proxy.py new file mode 100755 index 0000000..5f0a70f --- /dev/null +++ b/test_proxy.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 + +import socket +import struct +import subprocess +import time +import threading +import unittest + + +def get_free_port(): + s = socket.socket() + s.bind(("", 0)) + return s.getsockname()[1] + +SOCKS_PORT = get_free_port() +ECHO_PORT = get_free_port() +ECHO_PORT_B = struct.pack(">H", ECHO_PORT) + + +class SocketHelper(object): + def init_socket(self): + self.sock = socket.socket(socket.AF_INET) + self.sock.connect(("localhost", SOCKS_PORT)) + + def init_ipv6_socket(self): + self.sock = socket.socket(socket.AF_INET6) + self.sock.connect(("localhost", SOCKS_PORT)) + + def destroy_socket(self): + self.sock.close() + + def send(self, msg): + l = self.sock.send(msg) + self.assertEqual(len(msg), l) + + def send_proxy_length(self, x): + self.send(("%s" % x).zfill(10).encode("ascii") + b"\n") + + def recv(self, expected_length): + result = self.sock.recv(16384) + self.assertEqual(expected_length, len(result), str(result)) + return result + + def assertEnd(self): + try: + result = self.sock.recv(1) + self.assertEqual(0, len(result), str(result)) + except ConnectionResetError: + return + + def assertAuthSuccess(self): + result = self.recv(2) + self.assertEqual(b"\x05\x00", result) + + def assertAuthFail(self): + result = self.recv(2) + self.assertEqual(b"\x05\xff", result) + + def assertRequestSuccess(self): + self.assertRequestResponse(0) + + def assertRequestFail(self, reply): + self.assertRequestResponse(reply) + self.assertEnd() + + def assertRequestResponse(self, reply): + reply = struct.pack(">B", reply) + expected = b"\x05" + reply + b"\x00\x01\x00\x00\x00\x00\x00\x00" + result = self.recv(10) + self.assertEqual(expected, result) + +class TestAuthNegotiation(SocketHelper, unittest.TestCase): + def run(self, result=None): + with SocksServer(): + unittest.TestCase.run(self, result) + + def setUp(self): + self.init_socket() + + def tearDown(self): + self.destroy_socket() + + def test_one_method_success(self): + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + + def test_two_methods_success_first(self): + self.send(b"\x05\x02\x00\x80") + self.assertAuthSuccess() + + def test_two_methods_success_second(self): + self.send(b"\x05\x02\x80\x00") + self.assertAuthSuccess() + + def test_no_methods_fail(self): + self.send(b"\x05\x00") + self.assertEnd() + + def test_no_matching_methods_fail(self): + self.send(b"\x05\x01\x80") + self.assertAuthFail() + + def test_invalid_version_fail(self): + self.send(b"\x04\x01\x00") + self.assertEnd() + + +class TestRequestNegotiation(SocketHelper, unittest.TestCase): + def run(self, result=None): + with SocksServer(): + unittest.TestCase.run(self, result) + + def setUp(self): + self.init_socket() + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + + def tearDown(self): + self.destroy_socket() + + def test_invalid_version(self): + self.send(b"\x04\x01\x00\x01\x7f\x00\x00\x01\x00\01") + self.assertRequestFail(1) + + def test_invalid_command(self): + self.send(b"\x05\x02\x00\x01\x7f\x00\x00\x01\x00\x01") + self.assertRequestFail(7) + + def test_invalid_reserved_section(self): + self.send(b"\x05\x01\x01\x01\x7f\x00\x00\x01\x00\x01") + self.assertRequestFail(1) + + def test_invalid_address_type(self): + self.send(b"\x05\x01\x00\x09\x7f\x00\x00\x01\x00\x01") + self.assertRequestFail(8) + + def test_ipv4_success(self): + self.send(b"\x05\x01\x00\x01\x7f\x00\x00\x01" + ECHO_PORT_B) + self.assertRequestSuccess() + + def test_ipv4_bad_port(self): + self.send(b"\x05\x01\x00\x01\x7f\x00\x00\x01\xff\xff") + self.assertRequestFail(4) + + def test_ipv4_bad_host(self): + self.send(b"\x05\x01\x00\x01\x7f\x00\x00\x00\xff\xff") + self.assertRequestFail(4) + + def test_dns_success(self): + self.send(b"\x05\x01\x00\x03\x09localhost" + ECHO_PORT_B) + self.assertRequestSuccess() + + def test_dns_bad_port(self): + self.send(b"\x05\x01\x00\x03\x09localhost\xff\xff") + self.assertRequestFail(4) + + def test_dns_bad_host(self): + self.send(b"\x05\x01\x00\x03\x09f.invalid" + ECHO_PORT_B) + self.assertRequestFail(4) + + def test_dns_invalid_host(self): + self.send(b"\x05\x01\x00\x03\x00" + ECHO_PORT_B) + self.assertEnd() + + def test_ipv6_success(self): + self.send(b"\x05\x01\x00\x04" + (b"\x00"*15) + b"\x01" + ECHO_PORT_B) + self.assertRequestSuccess() + + def test_ipv6_bad_port(self): + self.send(b"\x05\x01\x00\x04" + (b"\x00"*15) + b"\x01" + b"\xff\xff") + self.assertRequestFail(4) + + def test_ipv6_bad_host(self): + self.send(b"\x05\x01\x00\x04" + b"\xfe\x80" + (b"\x00"*13) + b"\x01" + ECHO_PORT_B) + self.assertRequestFail(4) + + +class ProxyPacketHelper(object): + def test_one_packet(self): + self.send_proxy_length(3) + self.send(b"foo") + result = self.recv(3) + self.assertEqual(b"foo", result) + self.assertEnd() + + def test_no_received_data(self): + self.send_proxy_length(0) + self.send(b"foo") + self.assertEnd() + + def test_two_packets(self): + self.send_proxy_length(6) + + self.send(b"foo") + result = self.recv(3) + self.assertEqual(b"foo", result) + + self.send(b"bar") + result = self.recv(3) + self.assertEqual(b"bar", result) + + self.assertEnd() + + def test_large_packet(self): + msg = b"1234" * 1024 + self.send_proxy_length(len(msg)) + self.send(msg) + count = len(msg) + while count > 0: + part = self.sock.recv(4) + self.assertEqual(b"1234", part) + count -= 4 + self.assertEnd() + + +class TestIPv4Proxy(SocketHelper, ProxyPacketHelper, unittest.TestCase): + def run(self, result=None): + with SocksServer(): + unittest.TestCase.run(self, result) + + def setUp(self): + self.init_socket() + + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + + self.send(b"\x05\x01\x00\x01\x7f\x00\x00\x01" + ECHO_PORT_B) + self.assertRequestSuccess() + + def tearDown(self): + self.destroy_socket() + +class TestDNSProxy(SocketHelper, ProxyPacketHelper, unittest.TestCase): + def run(self, result=None): + with SocksServer(): + unittest.TestCase.run(self, result) + + def setUp(self): + self.init_socket() + + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + + self.send(b"\x05\x01\x00\x03\x09localhost" + ECHO_PORT_B) + self.assertRequestSuccess() + + def tearDown(self): + self.destroy_socket() + +class TestIPv6Proxy(SocketHelper, ProxyPacketHelper, unittest.TestCase): + def run(self, result=None): + with SocksServer(): + unittest.TestCase.run(self, result) + + def setUp(self): + self.init_socket() + + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + + self.send(b"\x05\x01\x00\x04" + (b"\x00"*15) + b"\x01" + ECHO_PORT_B) + self.assertRequestSuccess() + + def tearDown(self): + self.destroy_socket() + +class TestPermissions(SocketHelper, unittest.TestCase): + def assert_connection_allowed(self): + try: + self.init_socket() + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + finally: + self.destroy_socket() + + def assert_ipv6_connection_allowed(self): + try: + self.init_ipv6_socket() + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + finally: + self.destroy_socket() + + def assert_connection_blocked(self): + try: + self.init_socket() + self.send(b"\x05\x01\x00") + self.assertAuthSuccess() + self.fail("Expected ConnectionResetError") + except ConnectionResetError: + pass + finally: + self.destroy_socket() + + def test_allow_all_connections(self): + with SocksServer({"ALLOW_ALL": "1"}): + self.assert_connection_allowed() + + def test_block_all_connections(self): + with SocksServer({}): + self.assert_connection_blocked() + + def test_allow_ipv4_host(self): + with SocksServer({"ALLOW_HOST1": "127.0.0.1"}): + self.assert_connection_allowed() + + def test_allow_ipv6_host(self): + with SocksServer({"ALLOW_HOST1": "::1"}): + self.assert_ipv6_connection_allowed() + + def test_allow_multiple_hosts(self): + with SocksServer({"ALLOW_HOST1": "foo.invalid", "ALLOW_HOST2": "localhost"}): + self.assert_connection_allowed() + + +class EchoServer(object): + def __enter__(self): + self.run = True + + self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.bind(("", ECHO_PORT)) + self.sock.listen(5) + + self.thread = threading.Thread(target=self.run_echo_server) + self.thread.start() + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.run = False + self.sock.shutdown(socket.SHUT_RDWR) + self.thread.join() + + def run_echo_server(self): + try: + while self.run: + client, addr = self.sock.accept() + try: + self.handle_echo_client(client) + finally: + client.close() + except: + if not self.run: + return + + def handle_echo_client(self, client): + line = client.recv(10+1) + if not line: + return + num_bytes = int(line) + + while num_bytes > 0: + # force the test app to handle many packets by using small ones + data = client.recv(16) + if not data: + break + num_bytes -= len(data) + while data: + l = client.send(data) + data = data[l:] + +class SocksServer(object): + def __init__(self, extra_env={"ALLOW_ALL": "1"}): + self.env = {} + self.env["LISTEN_PORT"] = str(SOCKS_PORT) + self.env.update(extra_env) + self.devnull = open("/dev/null", "w") + + def __enter__(self): + self.process = subprocess.Popen( + args=["./socks5server"], + stdout=self.devnull, + stderr=self.devnull, + env=self.env, + ) + + self.wait_for_port() + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.devnull.close() + self.process.terminate() + self.process.wait() + + def wait_for_port(self): + start_time = time.time() + with socket.socket(socket.AF_INET) as s: + while start_time + 10 > time.time(): + try: + s.connect(("localhost", SOCKS_PORT)) + return + except ConnectionRefusedError: + time.sleep(0.01) + + +if __name__ == "__main__": + with EchoServer(): + unittest.main() -- 2.39.2