+#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;
+}