]> code.delx.au - proxy/commitdiff
Initial commit v0.1
authorJames Bunton <jamesbunton@delx.net.au>
Tue, 12 Aug 2014 21:48:49 +0000 (07:48 +1000)
committerJames Bunton <jamesbunton@delx.net.au>
Mon, 21 Sep 2015 13:27:17 +0000 (23:27 +1000)
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
rfc1928.txt [new file with mode: 0644]
socks5server.c [new file with mode: 0644]
test_proxy.py [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..df07823
--- /dev/null
@@ -0,0 +1,2 @@
+socks5server
+.*.swp
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
index 0000000..46bf46e
--- /dev/null
@@ -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]
+\f
+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]
+\f
+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]
+\f
+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]
+\f
+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]
+\f
+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]
+\f
+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]
+\f
+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]
+\f
+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]
+\f
diff --git a/socks5server.c b/socks5server.c
new file mode 100644 (file)
index 0000000..592bef0
--- /dev/null
@@ -0,0 +1,669 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <errno.h>
+
+#include <string.h>
+#include <bsd/string.h>
+#include <sys/types.h>
+
+#include <sys/socket.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+
+
+/***********
+ * 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 (executable)
index 0000000..5f0a70f
--- /dev/null
@@ -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()