Improve proxy server support
authorKirill Isakov <bootctl@gmail.com>
Sun, 24 Apr 2022 19:38:50 +0000 (01:38 +0600)
committerKirill Isakov <bootctl@gmail.com>
Sun, 24 Apr 2022 19:38:50 +0000 (01:38 +0600)
- fix authentication with socks5 proxies
- fix crash in forked process with exec proxy and empty node name
- refactor byte fiddling into structs
- add unit and integration tests

15 files changed:
src/connection.h
src/meson.build
src/meta.c
src/net_socket.c
src/protocol_auth.c
src/protocol_misc.c
src/proxy.c [new file with mode: 0644]
src/proxy.h [new file with mode: 0644]
test/integration/meson.build
test/integration/proxy.py [new file with mode: 0755]
test/integration/testlib/check.py
test/integration/testlib/path.py
test/integration/testlib/template.py
test/unit/meson.build
test/unit/test_proxy.c [new file with mode: 0644]

index e39d74b..6489ef4 100644 (file)
@@ -116,8 +116,8 @@ typedef struct connection_t {
        struct buffer_t inbuf;
        struct buffer_t outbuf;
        io_t io;                        /* input/output event on this metadata connection */
-       int tcplen;                     /* length of incoming TCPpacket */
-       int sptpslen;                   /* length of incoming SPTPS packet */
+       uint32_t tcplen;                /* length of incoming TCPpacket */
+       uint32_t sptpslen;              /* length of incoming SPTPS packet */
        int allow_request;              /* defined if there's only one request possible */
 
        time_t last_ping_time;          /* last time we saw some activity from the other end or pinged them */
index f84fb4d..c780ba1 100644 (file)
@@ -156,6 +156,7 @@ src_tincd = [
   'protocol_key.c',
   'protocol_misc.c',
   'protocol_subnet.c',
+  'proxy.c',
   'raw_socket_device.c',
   'route.c',
   'subnet.c',
index 0ff7bef..e3f2999 100644 (file)
@@ -30,6 +30,7 @@
 #include "net.h"
 #include "protocol.h"
 #include "utils.h"
+#include "proxy.h"
 
 #ifndef MIN
 static ssize_t MIN(ssize_t x, ssize_t y) {
@@ -279,33 +280,8 @@ bool receive_meta(connection_t *c) {
                                }
 
                                if(!c->node) {
-                                       if(c->outgoing && proxytype == PROXY_SOCKS4 && c->allow_request == ID) {
-                                               if(tcpbuffer[0] == 0 && tcpbuffer[1] == 0x5a) {
-                                                       logger(DEBUG_CONNECTIONS, LOG_DEBUG, "Proxy request granted");
-                                               } else {
-                                                       logger(DEBUG_CONNECTIONS, LOG_ERR, "Proxy request rejected");
-                                                       return false;
-                                               }
-                                       } else if(c->outgoing && proxytype == PROXY_SOCKS5 && c->allow_request == ID) {
-                                               if(tcpbuffer[0] != 5) {
-                                                       logger(DEBUG_CONNECTIONS, LOG_ERR, "Invalid response from proxy server");
-                                                       return false;
-                                               }
-
-                                               if(tcpbuffer[1] == (char)0xff) {
-                                                       logger(DEBUG_CONNECTIONS, LOG_ERR, "Proxy request rejected: unsuitable authentication method");
-                                                       return false;
-                                               }
-
-                                               if(tcpbuffer[2] != 5) {
-                                                       logger(DEBUG_CONNECTIONS, LOG_ERR, "Invalid response from proxy server");
-                                                       return false;
-                                               }
-
-                                               if(tcpbuffer[3] == 0) {
-                                                       logger(DEBUG_CONNECTIONS, LOG_DEBUG, "Proxy request granted");
-                                               } else {
-                                                       logger(DEBUG_CONNECTIONS, LOG_DEBUG, "Proxy request rejected");
+                                       if(c->outgoing && c->allow_request == ID && (proxytype == PROXY_SOCKS4 || proxytype == PROXY_SOCKS5)) {
+                                               if(!check_socks_resp(proxytype, tcpbuffer, c->tcplen)) {
                                                        return false;
                                                }
                                        } else {
index 8783b01..5d72471 100644 (file)
@@ -456,9 +456,12 @@ static void do_outgoing_pipe(connection_t *c, const char *command) {
        sockaddr2str(&c->address, &host, &port);
        setenv("REMOTEADDRESS", host, true);
        setenv("REMOTEPORT", port, true);
-       setenv("NODE", c->name, true);
        setenv("NAME", myself->name, true);
 
+       if(c->name) {
+               setenv("NODE", c->name, true);
+       }
+
        if(netname) {
                setenv("NETNAME", netname, true);
        }
index b13d901..211d908 100644 (file)
@@ -43,6 +43,7 @@
 #include "xalloc.h"
 #include "random.h"
 #include "compression.h"
+#include "proxy.h"
 
 #include "ed25519/sha512.h"
 #include "keys.h"
@@ -66,84 +67,12 @@ static bool send_proxyrequest(connection_t *c) {
                return true;
        }
 
-       case PROXY_SOCKS4: {
-               if(c->address.sa.sa_family != AF_INET) {
-                       logger(DEBUG_ALWAYS, LOG_ERR, "Cannot connect to an IPv6 host through a SOCKS 4 proxy!");
-                       return false;
-               }
-
-               const size_t s4reqlen = 9 + (proxyuser ? strlen(proxyuser) : 0);
-               uint8_t *s4req = alloca(s4reqlen);
-               s4req[0] = 4;
-               s4req[1] = 1;
-               memcpy(s4req + 2, &c->address.in.sin_port, 2);
-               memcpy(s4req + 4, &c->address.in.sin_addr, 4);
-
-               if(proxyuser) {
-                       memcpy(s4req + 8, proxyuser, strlen(proxyuser));
-               }
-
-               s4req[s4reqlen - 1] = 0;
-               c->tcplen = 8;
-               return send_meta(c, s4req, s4reqlen);
-       }
-
+       case PROXY_SOCKS4:
        case PROXY_SOCKS5: {
-               size_t len = 3 + 6 + (c->address.sa.sa_family == AF_INET ? 4 : 16);
-               c->tcplen = 2;
-
-               if(proxypass) {
-                       len += 3 + strlen(proxyuser) + strlen(proxypass);
-               }
-
-               uint8_t *s5req = alloca(len);
-
-               size_t i = 0;
-               s5req[i++] = 5;
-               s5req[i++] = 1;
-
-               if(proxypass) {
-                       s5req[i++] = 2;
-                       s5req[i++] = 1;
-                       s5req[i++] = strlen(proxyuser);
-                       memcpy(s5req + i, proxyuser, strlen(proxyuser));
-                       i += strlen(proxyuser);
-                       s5req[i++] = strlen(proxypass);
-                       memcpy(s5req + i, proxypass, strlen(proxypass));
-                       i += strlen(proxypass);
-                       c->tcplen += 2;
-               } else {
-                       s5req[i++] = 0;
-               }
-
-               s5req[i++] = 5;
-               s5req[i++] = 1;
-               s5req[i++] = 0;
-
-               if(c->address.sa.sa_family == AF_INET) {
-                       s5req[i++] = 1;
-                       memcpy(s5req + i, &c->address.in.sin_addr, 4);
-                       i += 4;
-                       memcpy(s5req + i, &c->address.in.sin_port, 2);
-                       i += 2;
-                       c->tcplen += 10;
-               } else if(c->address.sa.sa_family == AF_INET6) {
-                       s5req[i++] = 3;
-                       memcpy(s5req + i, &c->address.in6.sin6_addr, 16);
-                       i += 16;
-                       memcpy(s5req + i, &c->address.in6.sin6_port, 2);
-                       i += 2;
-                       c->tcplen += 22;
-               } else {
-                       logger(DEBUG_ALWAYS, LOG_ERR, "Address family %x not supported for SOCKS 5 proxies!", c->address.sa.sa_family);
-                       return false;
-               }
-
-               if(i > len) {
-                       abort();
-               }
-
-               return send_meta(c, s5req, len);
+               size_t reqlen = socks_req_len(proxytype, &c->address);
+               uint8_t *req = alloca(reqlen);
+               c->tcplen = create_socks_req(proxytype, req, &c->address);
+               return c->tcplen ? send_meta(c, req, reqlen) : false;
        }
 
        case PROXY_SOCKS4A:
index cef7e3d..0263d00 100644 (file)
@@ -104,7 +104,7 @@ bool send_tcppacket(connection_t *c, const vpn_packet_t *packet) {
 bool tcppacket_h(connection_t *c, const char *request) {
        short int len;
 
-       if(sscanf(request, "%*d %hd", &len) != 1) {
+       if(sscanf(request, "%*d %hd", &len) != 1 || len < 0) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Got bad %s from %s (%s)", "PACKET", c->name,
                       c->hostname);
                return false;
@@ -136,7 +136,7 @@ bool send_sptps_tcppacket(connection_t *c, const void *packet, size_t len) {
 bool sptps_tcppacket_h(connection_t *c, const char *request) {
        short int len;
 
-       if(sscanf(request, "%*d %hd", &len) != 1) {
+       if(sscanf(request, "%*d %hd", &len) != 1 || len < 0) {
                logger(DEBUG_ALWAYS, LOG_ERR, "Got bad %s from %s (%s)", "SPTPS_PACKET", c->name,
                       c->hostname);
                return false;
diff --git a/src/proxy.c b/src/proxy.c
new file mode 100644 (file)
index 0000000..f7d34d0
--- /dev/null
@@ -0,0 +1,285 @@
+#include "system.h"
+
+#include "logger.h"
+#include "proxy.h"
+
+typedef enum socks5_auth_method_t {
+       AUTH_ANONYMOUS = 0,
+       AUTH_PASSWORD = 2,
+       AUTH_FAILED = 0xFF,
+} socks5_auth_method_t;
+
+// SOCKS 4 constants (https://en.wikipedia.org/wiki/SOCKS#SOCKS4)
+static const uint8_t SOCKS4_CMD_CONN = 1;
+static const uint8_t SOCKS4_REPLY_VERSION = 0;
+static const uint8_t SOCKS4_STATUS_OK = 0x5A;
+static const uint8_t SOCKS4_VERSION = 4;
+
+// SOCKS 5 constants (https://en.wikipedia.org/wiki/SOCKS#SOCKS5)
+typedef enum socks5_addr_type_t {
+       SOCKS5_IPV4 = 1,
+       SOCKS5_IPV6 = 4,
+} socks5_addr_type_t;
+
+static const uint8_t SOCKS5_AUTH_METHOD_NONE = 0;
+static const uint8_t SOCKS5_AUTH_METHOD_PASSWORD = 2;
+static const uint8_t SOCKS5_AUTH_OK = 0;
+static const uint8_t SOCKS5_AUTH_VERSION = 1;
+static const uint8_t SOCKS5_COMMAND_CONN = 1;
+static const uint8_t SOCKS5_STATUS_OK = 0;
+static const uint8_t SOCKS5_VERSION = 5;
+
+static void log_proxy_grant(bool granted) {
+       if(granted) {
+               logger(DEBUG_CONNECTIONS, LOG_DEBUG, "Proxy request granted");
+       } else {
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Proxy request rejected");
+       }
+}
+
+static void log_short_response(void) {
+       logger(DEBUG_CONNECTIONS, LOG_ERR, "Received short response from proxy");
+}
+
+static bool check_socks4_resp(const socks4_response_t *resp, size_t len) {
+       if(len < sizeof(socks4_response_t)) {
+               log_short_response();
+               return false;
+       }
+
+       if(resp->version != SOCKS4_REPLY_VERSION) {
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Bad response from SOCKS4 proxy");
+               return false;
+       }
+
+       bool granted = resp->status == SOCKS4_STATUS_OK;
+       log_proxy_grant(granted);
+       return granted;
+}
+
+static bool socks5_check_result(const socks5_conn_resp_t *re, size_t len) {
+       size_t addrlen;
+
+       switch((socks5_addr_type_t)re->addr_type) {
+       case SOCKS5_IPV4:
+               addrlen = sizeof(socks5_ipv4_t);
+               break;
+
+       case SOCKS5_IPV6:
+               addrlen = sizeof(socks5_ipv6_t);
+               break;
+
+       default:
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Unsupported address type 0x%x from proxy server", re->addr_type);
+               return false;
+       }
+
+       if(len < addrlen) {
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Received short address from proxy server");
+               return false;
+       }
+
+       if(re->socks_version != SOCKS5_VERSION) {
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Invalid response from proxy server");
+               return false;
+       }
+
+       bool granted = re->conn_status == SOCKS5_STATUS_OK;
+       log_proxy_grant(granted);
+       return granted;
+}
+
+static bool check_socks5_resp(const socks5_resp_t *resp, size_t len) {
+       if(len < sizeof(socks5_server_choice_t)) {
+               log_short_response();
+               return false;
+       }
+
+       len -= sizeof(socks5_server_choice_t);
+
+       if(resp->choice.socks_version != SOCKS5_VERSION) {
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Invalid response from proxy server");
+               return false;
+       }
+
+       switch((socks5_auth_method_t) resp->choice.auth_method) {
+       case AUTH_ANONYMOUS:
+               if(len < sizeof(socks5_conn_resp_t)) {
+                       log_short_response();
+                       return false;
+               } else {
+                       return socks5_check_result(&resp->anon, len - sizeof(socks5_conn_resp_t));
+               }
+
+       case AUTH_PASSWORD: {
+               size_t header_len = sizeof(socks5_auth_status_t) + sizeof(socks5_conn_resp_t);
+
+               if(len < header_len) {
+                       log_short_response();
+                       return false;
+               }
+
+               if(resp->pass.status.auth_version != SOCKS5_AUTH_VERSION) {
+                       logger(DEBUG_CONNECTIONS, LOG_ERR, "Invalid proxy authentication protocol version");
+                       return false;
+               }
+
+               if(resp->pass.status.auth_status != SOCKS5_AUTH_OK) {
+                       logger(DEBUG_CONNECTIONS, LOG_ERR, "Proxy authentication failed");
+                       return false;
+               }
+
+               return socks5_check_result(&resp->pass.resp, len - header_len);
+       }
+
+       case AUTH_FAILED:
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Proxy request rejected: unsuitable authentication method");
+               return false;
+
+       default:
+               logger(DEBUG_CONNECTIONS, LOG_ERR, "Unsupported authentication method");
+               return false;
+       }
+}
+
+bool check_socks_resp(proxytype_t type, const void *buf, size_t len) {
+       if(type == PROXY_SOCKS4) {
+               return check_socks4_resp(buf, len);
+       } else if(type == PROXY_SOCKS5) {
+               return check_socks5_resp(buf, len);
+       } else {
+               return false;
+       }
+}
+
+static size_t create_socks4_req(socks4_request_t *req, const sockaddr_t *sa) {
+       if(sa->sa.sa_family != AF_INET) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Cannot connect to an IPv6 host through a SOCKS 4 proxy!");
+               return 0;
+       }
+
+       req->version = SOCKS4_VERSION;
+       req->command = SOCKS4_CMD_CONN;
+       req->dstport = sa->in.sin_port;
+       req->dstip = sa->in.sin_addr;
+
+       if(proxyuser) {
+               strcpy(req->id, proxyuser);
+       } else {
+               req->id[0] = '\0';
+       }
+
+       return sizeof(socks4_response_t);
+}
+
+static size_t create_socks5_req(void *buf, const sockaddr_t *sa) {
+       uint16_t family = sa->sa.sa_family;
+
+       if(family != AF_INET && family != AF_INET6) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Address family %x not supported for SOCKS 5 proxies!", family);
+               return 0;
+       }
+
+       socks5_greet_t *req = buf;
+       req->version = SOCKS5_VERSION;
+       req->nmethods = 1; // only one auth method is supported
+
+       size_t resplen = sizeof(socks5_server_choice_t);
+       uint8_t *auth = (uint8_t *)buf + sizeof(socks5_greet_t);
+
+       if(proxyuser && proxypass) {
+               req->authmethod = SOCKS5_AUTH_METHOD_PASSWORD;
+
+               // field  | VER | IDLEN |  ID   | PWLEN |   PW  |
+               // bytes  |  1  |   1   | 1-255 |   1   | 1-255 |
+
+               // Assign the first field (auth protocol version)
+               *auth++ = SOCKS5_AUTH_VERSION;
+
+               size_t userlen = strlen(proxyuser);
+               size_t passlen = strlen(proxypass);
+
+               // Assign the username length, and copy the username
+               *auth++ = userlen;
+               memcpy(auth, proxyuser, userlen);
+               auth += userlen;
+
+               // Do the same for password
+               *auth++ = passlen;
+               memcpy(auth, proxypass, passlen);
+               auth += passlen;
+
+               resplen += sizeof(socks5_auth_status_t);
+       } else {
+               req->authmethod = SOCKS5_AUTH_METHOD_NONE;
+       }
+
+       socks5_conn_req_t *conn = (socks5_conn_req_t *) auth;
+       conn->header.version = SOCKS5_VERSION;
+       conn->header.command = SOCKS5_COMMAND_CONN;
+       conn->header.reserved = 0;
+
+       resplen += sizeof(socks5_conn_resp_t);
+
+       if(family == AF_INET) {
+               conn->header.addr_type = SOCKS5_IPV4;
+               conn->dst.ipv4.addr = sa->in.sin_addr;
+               conn->dst.ipv4.port = sa->in.sin_port;
+               resplen += sizeof(socks5_ipv4_t);
+       } else {
+               conn->header.addr_type = SOCKS5_IPV6;
+               conn->dst.ipv6.addr = sa->in6.sin6_addr;
+               conn->dst.ipv6.port = sa->in6.sin6_port;
+               resplen += sizeof(socks5_ipv6_t);
+       }
+
+       return resplen;
+}
+
+size_t socks_req_len(proxytype_t type, const sockaddr_t *sa) {
+       uint16_t family = sa->sa.sa_family;
+
+       if(type == PROXY_SOCKS4) {
+               if(family != AF_INET) {
+                       logger(DEBUG_CONNECTIONS, LOG_ERR, "SOCKS 4 only supports IPv4 addresses");
+                       return 0;
+               }
+
+               size_t userlen_size = 1;
+               size_t userlen = proxyuser ? strlen(proxyuser) : 0;
+               return sizeof(socks4_request_t) + userlen_size + userlen;
+       }
+
+       if(type == PROXY_SOCKS5) {
+               if(family != AF_INET && family != AF_INET6) {
+                       logger(DEBUG_CONNECTIONS, LOG_ERR, "SOCKS 5 only supports IPv4 and IPv6");
+                       return 0;
+               }
+
+               size_t len = sizeof(socks5_greet_t) +
+                            sizeof(socks5_conn_hdr_t) +
+                            (family == AF_INET
+                             ? sizeof(socks5_ipv4_t)
+                             : sizeof(socks5_ipv6_t));
+
+               if(proxyuser && proxypass) {
+                       // version, userlen, user, passlen, pass
+                       len += 1 + 1 + strlen(proxyuser) + 1 + strlen(proxypass);
+               }
+
+               return len;
+       }
+
+       logger(DEBUG_CONNECTIONS, LOG_ERR, "Bad proxy type 0x%x", type);
+       return 0;
+}
+
+size_t create_socks_req(proxytype_t type, void *buf, const sockaddr_t *sa) {
+       if(type == PROXY_SOCKS4) {
+               return create_socks4_req(buf, sa);
+       } else if(type == PROXY_SOCKS5) {
+               return create_socks5_req(buf, sa);
+       } else {
+               abort();
+       }
+}
diff --git a/src/proxy.h b/src/proxy.h
new file mode 100644 (file)
index 0000000..9173566
--- /dev/null
@@ -0,0 +1,113 @@
+#ifndef TINC_PROXY_H
+#define TINC_PROXY_H
+
+#include "system.h"
+
+#include "net.h"
+
+PACKED(struct socks4_request_t {
+       uint8_t version;
+       uint8_t command;
+       uint16_t dstport;
+       struct in_addr dstip;
+       char id[];
+});
+
+PACKED(struct socks4_response_t {
+       uint8_t version;
+       uint8_t status;
+       uint16_t dstport;
+       struct in_addr dstip;
+});
+
+typedef struct socks4_request_t socks4_request_t;
+typedef struct socks4_response_t socks4_response_t;
+
+PACKED(struct socks5_greet_t {
+       uint8_t version;
+       uint8_t nmethods;
+       uint8_t authmethod;
+});
+
+typedef struct socks5_greet_t socks5_greet_t;
+
+PACKED(struct socks5_conn_hdr_t {
+       uint8_t version;
+       uint8_t command;
+       uint8_t reserved;
+       uint8_t addr_type;
+});
+
+PACKED(struct socks5_ipv4_t {
+       struct in_addr addr;
+       uint16_t port;
+});
+
+PACKED(struct socks5_ipv6_t {
+       struct in6_addr addr;
+       uint16_t port;
+});
+
+typedef struct socks5_conn_hdr_t socks5_conn_hdr_t;
+typedef struct socks5_ipv4_t socks5_ipv4_t;
+typedef struct socks5_ipv6_t socks5_ipv6_t;
+
+PACKED(struct socks5_conn_req_t {
+       socks5_conn_hdr_t header;
+       union {
+               socks5_ipv4_t ipv4;
+               socks5_ipv6_t ipv6;
+       } dst;
+});
+
+PACKED(struct socks5_server_choice_t {
+       uint8_t socks_version;
+       uint8_t auth_method;
+});
+
+PACKED(struct socks5_auth_status_t {
+       uint8_t auth_version;
+       uint8_t auth_status;
+});
+
+typedef struct socks5_auth_status_t socks5_auth_status_t;
+
+PACKED(struct socks5_conn_resp_t {
+       uint8_t socks_version;
+       uint8_t conn_status;
+       uint8_t reserved;
+       uint8_t addr_type;
+});
+
+typedef struct socks5_conn_req_t socks5_conn_req_t;
+typedef struct socks5_server_choice_t socks5_server_choice_t;
+typedef struct socks5_conn_resp_t socks5_conn_resp_t;
+
+PACKED(struct socks5_resp_t {
+       socks5_server_choice_t choice;
+
+       union {
+               // if choice == password
+               struct {
+                       socks5_auth_status_t status;
+                       socks5_conn_resp_t resp;
+               } pass;
+
+               // if choice == anonymous
+               socks5_conn_resp_t anon;
+       };
+});
+
+typedef struct socks5_resp_t socks5_resp_t;
+
+// Get the length of a connection request to a SOCKS 4 or 5 proxy
+extern size_t socks_req_len(proxytype_t type, const sockaddr_t *sa);
+
+// Create a request to connect to a SOCKS 4 or 5 proxy.
+// Returns the expected response length, or zero on error.
+extern size_t create_socks_req(proxytype_t type, void *req, const sockaddr_t *sa);
+
+// Check that SOCKS server provided a valid response and permitted further requests
+extern bool check_socks_resp(proxytype_t type, const void *buf, size_t len);
+
+#endif // TINC_PROXY_H
index 5dc2430..5e82fe8 100644 (file)
@@ -4,8 +4,9 @@ tests = [
   'commandline.py',
   'executables.py',
   'import_export.py',
-  'invite_tinc_up.py',
   'invite.py',
+  'invite_tinc_up.py',
+  'proxy.py',
   'scripts.py',
   'security.py',
   'splice.py',
diff --git a/test/integration/proxy.py b/test/integration/proxy.py
new file mode 100755 (executable)
index 0000000..93b51f5
--- /dev/null
@@ -0,0 +1,482 @@
+#!/usr/bin/env python3
+
+"""Test that tincd works through proxies."""
+
+import os
+import re
+import tempfile
+import typing as T
+import multiprocessing.connection as mp
+import logging
+import select
+import socket
+import struct
+
+from threading import Thread
+from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler
+from testlib import check, cmd, path
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+from testlib.util import random_string
+from testlib.log import log
+
+USERNAME = random_string(8)
+PASSWORD = random_string(8)
+
+proxy_stats = {"tx": 0}
+
+# socks4
+SOCKS_VERSION_4 = 4
+CMD_STREAM = 1
+REQUEST_GRANTED = 0x5A
+
+# socks5
+SOCKS_VERSION_5 = 5
+METHOD_NONE = 0
+METHOD_USERNAME_PASSWORD = 2
+NO_METHODS = 0xFF
+ADDR_TYPE_IPV4 = 1
+ADDR_TYPE_DOMAIN = 3
+CMD_CONNECT = 1
+REP_SUCCESS = 0
+RESERVED = 0
+AUTH_OK = 0
+AUTH_FAILURE = 0xFF
+
+
+def send_all(sock: socket.socket, data: bytes) -> bool:
+    """Send all data to socket, retrying as necessary."""
+
+    total = 0
+
+    while total < len(data):
+        sent = sock.send(data[total:])
+        if sent <= 0:
+            break
+        total += sent
+
+    return total == len(data)
+
+
+def proxy_data(client: socket.socket, remote: socket.socket) -> None:
+    """Pipe data between the two sockets."""
+
+    while True:
+        read, _, _ = select.select([client, remote], [], [])
+
+        if client in read:
+            data = client.recv(4096)
+            proxy_stats["tx"] += len(data)
+            log.debug("received from client: '%s'", data)
+            if not data or not send_all(remote, data):
+                log.info("remote finished")
+                return
+
+        if remote in read:
+            data = remote.recv(4096)
+            proxy_stats["tx"] += len(data)
+            log.debug("sending to client: '%s'", data)
+            if not data or not send_all(client, data):
+                log.info("client finished")
+                return
+
+
+def error_response(address_type: int, error: int) -> bytes:
+    """Create error response for SOCKS client."""
+    return struct.pack("!BBBBIH", SOCKS_VERSION_5, error, 0, address_type, 0, 0)
+
+
+def read_ipv4(sock: socket.socket) -> str:
+    """Read IPv4 address from socket and convert it into a string."""
+    ip_addr = sock.recv(4)
+    return socket.inet_ntoa(ip_addr)
+
+
+def ip_to_int(addr: str) -> int:
+    """Convert address to integer."""
+    return struct.unpack("!I", socket.inet_aton(addr))[0]
+
+
+def addr_response(address, port: T.Tuple[str, int]) -> bytes:
+    """Create address response. Format:
+    version    rep    rsv    atyp    bind_addr    bind_port
+    """
+    return struct.pack(
+        "!BBBBIH",
+        SOCKS_VERSION_5,
+        REP_SUCCESS,
+        RESERVED,
+        ADDR_TYPE_IPV4,
+        ip_to_int(address),
+        port,
+    )
+
+
+class ProxyServer(StreamRequestHandler):
+    """Parent class for proxy server implementations."""
+
+    name: T.ClassVar[str] = ""
+
+
+class ThreadingTCPServer(ThreadingMixIn, TCPServer):
+    """TCPServer which handles each request in a separate thread."""
+
+
+class HttpProxy(ProxyServer):
+    """HTTP proxy server that handles CONNECT requests."""
+
+    name = "http"
+    _re = re.compile(r"CONNECT ([^:]+):(\d+) HTTP/1\.[01]")
+
+    def handle(self) -> None:
+        try:
+            self._handle_connection()
+        finally:
+            self.server.close_request(self.request)
+
+    def _handle_connection(self) -> None:
+        """Handle a single proxy connection"""
+        data = b""
+        while not data.endswith(b"\r\n\r\n"):
+            data += self.connection.recv(1)
+        log.info("got request: '%s'", data)
+
+        match = self._re.match(data.decode("utf-8"))
+        assert match
+
+        address, port = match.groups()
+        log.info("matched target address %s:%s", address, port)
+
+        with socket.socket() as sock:
+            sock.connect((address, int(port)))
+            log.info("connected to target")
+
+            self.connection.sendall(b"HTTP/1.1 200 OK\r\n\r\n")
+            log.info("sent successful response")
+
+            proxy_data(self.connection, sock)
+
+
+class Socks4Proxy(ProxyServer):
+    """SOCKS 4 proxy server."""
+
+    name = "socks4"
+    username = USERNAME
+
+    def handle(self) -> None:
+        try:
+            self._handle_connection()
+        finally:
+            self.server.close_request(self.request)
+
+    def _handle_connection(self) -> None:
+        """Handle a single proxy connection."""
+
+        version, command, port = struct.unpack("!BBH", self.connection.recv(4))
+        check.equals(SOCKS_VERSION_4, version)
+        check.equals(command, CMD_STREAM)
+        check.port(port)
+
+        addr = read_ipv4(self.connection)
+        log.info("received address %s:%d", addr, port)
+
+        user = ""
+        while True:
+            byte = self.connection.recv(1)
+            if byte == b"\0":
+                break
+            user += byte.decode("utf-8")
+
+        log.info("received username %s", user)
+        self._check_username(user)
+
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as remote:
+            remote.connect((addr, port))
+            logging.info("connected to %s:%s", addr, port)
+            self._process_remote(remote)
+
+    def _check_username(self, user: str) -> bool:
+        """Authenticate by comparing socks4 username."""
+        return user == self.username
+
+    def _process_remote(self, sock: socket.socket) -> None:
+        """Process a single proxy connection."""
+
+        addr, port = sock.getsockname()
+        reply = struct.pack("!BBHI", 0, REQUEST_GRANTED, port, ip_to_int(addr))
+        log.info("sending reply %s", reply)
+        self.connection.sendall(reply)
+
+        proxy_data(self.connection, sock)
+
+
+class AnonymousSocks4Proxy(Socks4Proxy):
+    """socks4 server without any authentication."""
+
+    def _check_username(self, user: str) -> bool:
+        return True
+
+
+class Socks5Proxy(ProxyServer):
+    """SOCKS 5 proxy server."""
+
+    name = "socks5"
+
+    def handle(self) -> None:
+        """Handle a proxy connection."""
+        try:
+            self._process_connection()
+        finally:
+            self.server.close_request(self.request)
+
+    def _process_connection(self) -> None:
+        """Handle a proxy connection."""
+
+        methods = self._read_header()
+        if not self._authenticate(methods):
+            raise RuntimeError("authentication failed")
+
+        command, address_type = self._read_command()
+        address = self._read_address(address_type)
+        port = struct.unpack("!H", self.connection.recv(2))[0]
+        log.info("got address %s:%d", address, port)
+
+        if command != CMD_CONNECT:
+            raise RuntimeError(f"bad command {command}")
+
+        try:
+            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as remote:
+                remote.connect((address, port))
+                bind_address = remote.getsockname()
+                logging.info("connected to %s:%d", address, port)
+
+                reply = addr_response(*bind_address)
+                log.debug("sending address '%s'", reply)
+                self.connection.sendall(reply)
+
+                proxy_data(self.connection, remote)
+        except OSError as ex:
+            log.error("socks server failed", exc_info=ex)
+            reply = error_response(address_type, 5)
+            self.connection.sendall(reply)
+            raise ex
+
+    def _read_address(self, address_type: int) -> str:
+        """Read target address."""
+
+        if address_type == ADDR_TYPE_IPV4:
+            return read_ipv4(self.connection)
+
+        if address_type == ADDR_TYPE_DOMAIN:
+            domain_len = self.connection.recv(1)[0]
+            domain = self.connection.recv(domain_len)
+            return socket.gethostbyname(domain.decode())
+
+        raise RuntimeError(f"unknown address type {address_type}")
+
+    def _read_command(self) -> T.Tuple[int, int]:
+        """Check protocol version and get command code and address type."""
+
+        version, command, _, address_type = struct.unpack(
+            "!BBBB", self.connection.recv(4)
+        )
+        check.equals(SOCKS_VERSION_5, version)
+        return command, address_type
+
+    @property
+    def _method(self) -> int:
+        """Supported authentication method."""
+        return METHOD_USERNAME_PASSWORD
+
+    def _authenticate(self, methods: T.List[int]) -> bool:
+        """Perform client authentication."""
+
+        found = self._method in methods
+        choice = self._method if found else NO_METHODS
+        result = struct.pack("!BB", SOCKS_VERSION_5, choice)
+
+        log.debug("sending authentication result '%s'", result)
+        self.connection.sendall(result)
+
+        if not found:
+            log.error("auth method not found in %s", methods)
+            return False
+
+        if not self._read_creds():
+            log.error("could not verify credentials")
+            return False
+
+        return True
+
+    def _read_header(self) -> T.List[int]:
+        """Get the list of methods supported by the client."""
+
+        version, methods = struct.unpack("!BB", self.connection.recv(2))
+        check.equals(SOCKS_VERSION_5, version)
+        check.greater(methods, 0)
+        return [ord(self.connection.recv(1)) for _ in range(methods)]
+
+    def _read_creds(self) -> bool:
+        """Read and verify auth credentials."""
+
+        version = ord(self.connection.recv(1))
+        check.equals(1, version)
+
+        user_len = ord(self.connection.recv(1))
+        user = self.connection.recv(user_len).decode("utf-8")
+
+        passw_len = ord(self.connection.recv(1))
+        passw = self.connection.recv(passw_len).decode("utf-8")
+
+        log.info("got credentials '%s', '%s'", user, passw)
+        log.info("want credentials '%s', '%s'", USERNAME, PASSWORD)
+
+        passed = user == USERNAME and passw == PASSWORD
+        response = struct.pack("!BB", version, AUTH_OK if passed else AUTH_FAILURE)
+        self.connection.sendall(response)
+
+        return passed
+
+
+class AnonymousSocks5Proxy(Socks5Proxy):
+    """SOCKS 5 server without authentication support."""
+
+    @property
+    def _method(self) -> int:
+        return METHOD_NONE
+
+    def _read_creds(self) -> bool:
+        return True
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
+    """Create a new tinc node."""
+
+    foo, bar = ctx.node(), ctx.node()
+    stdin = f"""
+        init {foo}
+        set Address 127.0.0.1
+        set Port 0
+        set DeviceType dummy
+    """
+    foo.cmd(stdin=stdin)
+
+    stdin = f"""
+        init {bar}
+        set Address 127.0.0.1
+        set Port 0
+        set DeviceType dummy
+    """
+    bar.cmd(stdin=stdin)
+
+    return foo, bar
+
+
+def create_exec_proxy(port: int) -> str:
+    """Create a fake exec proxy program."""
+
+    code = f"""
+import os
+import multiprocessing.connection as mp
+
+with mp.Client(("127.0.0.1", {port}), family="AF_INET") as client:
+    client.send({{ **os.environ }})
+"""
+
+    file = tempfile.mktemp()
+    with open(file, "w", encoding="utf-8") as f:
+        f.write(code)
+
+    return file
+
+
+def test_proxy(ctx: Test, handler: T.Type[ProxyServer], user="", passw="") -> None:
+    """Test socks proxy support."""
+
+    foo, bar = init(ctx)
+
+    bar.add_script(foo.script_up)
+    bar.add_script(Script.TINC_UP)
+    bar.start()
+
+    cmd.exchange(foo, bar)
+    foo.cmd("set", f"{bar}.Port", str(bar.port))
+
+    with ThreadingTCPServer(("127.0.0.1", 0), handler) as server:
+        _, port = server.server_address
+
+        worker = Thread(target=server.serve_forever)
+        worker.start()
+
+        foo.cmd("set", "Proxy", handler.name, f"127.0.0.1 {port} {user} {passw}")
+        foo.cmd("start")
+        bar[foo.script_up].wait()
+
+        foo.cmd("stop")
+        bar.cmd("stop")
+
+        server.shutdown()
+        worker.join()
+
+
+def test_proxy_exec(ctx: Test) -> None:
+    """Test that exec proxies work as expected."""
+    foo, bar = init(ctx)
+
+    log.info("exec proxy without arguments fails")
+    foo.cmd("set", "Proxy", "exec")
+    _, stderr = foo.cmd("start", code=1)
+    check.is_in("Argument expected for proxy type", stderr)
+
+    log.info("exec proxy with correct arguments works")
+    bar.cmd("start")
+    cmd.exchange(foo, bar)
+
+    with mp.Listener(("127.0.0.1", 0), family="AF_INET") as listener:
+        port = int(listener.address[1])
+        proxy = create_exec_proxy(port)
+
+        foo.cmd("set", "Proxy", "exec", f"{path.PYTHON_PATH} {path.PYTHON_CMD} {proxy}")
+        foo.cmd("start")
+
+        with listener.accept() as conn:
+            env: T.Dict[str, str] = conn.recv()
+
+            for var in "NAME", "REMOTEADDRESS", "REMOTEPORT":
+                check.true(env.get(var))
+
+            for var in "NODE", "NETNAME":
+                if var in env:
+                    check.true(env[var])
+
+        os.remove(proxy)
+
+
+if os.name != "nt":
+    with Test("exec proxy") as context:
+        test_proxy_exec(context)
+
+with Test("HTTP CONNECT proxy") as context:
+    proxy_stats["tx"] = 0
+    test_proxy(context, HttpProxy)
+    check.greater(proxy_stats["tx"], 0)
+
+with Test("socks4 proxy with username") as context:
+    proxy_stats["tx"] = 0
+    test_proxy(context, Socks4Proxy, USERNAME)
+    check.greater(proxy_stats["tx"], 0)
+
+with Test("anonymous socks4 proxy") as context:
+    proxy_stats["tx"] = 0
+    test_proxy(context, AnonymousSocks4Proxy)
+    check.greater(proxy_stats["tx"], 0)
+
+with Test("authenticated socks5 proxy") as context:
+    proxy_stats["tx"] = 0
+    test_proxy(context, Socks5Proxy, USERNAME, PASSWORD)
+    check.greater(proxy_stats["tx"], 0)
+
+with Test("anonymous socks5 proxy") as context:
+    proxy_stats["tx"] = 0
+    test_proxy(context, AnonymousSocks5Proxy)
+    check.greater(proxy_stats["tx"], 0)
index 77865b1..1d1dfb0 100755 (executable)
@@ -20,6 +20,12 @@ def true(value: T.Any) -> None:
         raise ValueError(f'expected "{value}" to be truthy', value)
 
 
+def port(value: int) -> None:
+    """Check that value resembles a port."""
+    if not isinstance(value, int) or value < 1 or value > 65535:
+        raise ValueError(f'expected "{value}" to be be a port')
+
+
 def equals(expected: Val, actual: Val) -> None:
     """Check that the two values are equal."""
     if expected != actual:
@@ -32,6 +38,12 @@ def has_prefix(text: T.AnyStr, prefix: T.AnyStr) -> None:
         raise ValueError(f"expected {text!r} to start with {prefix!r}")
 
 
+def greater(value: Num, than: Num) -> None:
+    """Check that value is greater than the other value."""
+    if value <= than:
+        raise ValueError(f"value {value} must be greater than {than}")
+
+
 def in_range(value: Num, gte: Num, lte: Num) -> None:
     """Check that value lies in the range [min, max]."""
     if not gte >= value >= lte:
index a33fba5..4a90ac9 100755 (executable)
@@ -23,6 +23,8 @@ PYTHON_PATH = str(env["PYTHON_PATH"])
 SPTPS_TEST_PATH = str(env["SPTPS_TEST_PATH"])
 SPTPS_KEYPAIR_PATH = str(env["SPTPS_KEYPAIR_PATH"])
 
+PYTHON_CMD = "runpython" if "meson.exe" in PYTHON_PATH.lower() else ""
+
 
 def _check() -> bool:
     """Basic sanity checks on passed environment variables."""
index b90299c..83d2f85 100755 (executable)
@@ -9,7 +9,6 @@ from .notification import notifications
 
 
 _CMD_VARS = os.linesep.join([f"set {var}={val}" for var, val in path.env.items()])
-_CMD_PY = "runpython" if "meson.exe" in path.PYTHON_PATH.lower() else ""
 
 
 def _read_template(tpl_name: str, maps: T.Dict[str, T.Any]) -> str:
@@ -40,7 +39,7 @@ def make_script(node: str, script: str, source: str) -> str:
 def make_cmd_wrap(script: str) -> str:
     """Create a .cmd wrapper for tincd script. Only makes sense on Windows."""
     maps = {
-        "PYTHON_CMD": _CMD_PY,
+        "PYTHON_CMD": path.PYTHON_CMD,
         "PYTHON_PATH": path.PYTHON_PATH,
         "SCRIPT_PATH": script,
         "VARIABLES": _CMD_VARS,
index eaa9d18..8633c18 100644 (file)
@@ -44,6 +44,9 @@ tests = {
   'protocol': {
     'code': 'test_protocol.c',
   },
+  'proxy': {
+    'code': 'test_proxy.c',
+  },
   'utils': {
     'code': 'test_utils.c',
   },
diff --git a/test/unit/test_proxy.c b/test/unit/test_proxy.c
new file mode 100644 (file)
index 0000000..a5b30bd
--- /dev/null
@@ -0,0 +1,430 @@
+#include "unittest.h"
+#include "../../src/net.h"
+#include "../../src/netutl.h"
+#include "../../src/proxy.h"
+#include "../../src/xalloc.h"
+
+static const char *user = "foo";
+static const size_t userlen = sizeof("foo") - 1;
+
+static const char *pass = "bar";
+static const size_t passlen = sizeof("bar") - 1;
+
+static int teardown(void **state) {
+       (void)state;
+
+       free(proxyuser);
+       proxyuser = NULL;
+
+       free(proxypass);
+       proxypass = NULL;
+
+       return 0;
+}
+
+static void test_socks_req_len_socks4_ipv4(void **state) {
+       (void)state;
+
+       const sockaddr_t sa = str2sockaddr("127.0.0.1", "4242");
+
+       size_t len = socks_req_len(PROXY_SOCKS4, &sa);
+       assert_int_equal(9, len);
+
+       proxyuser = xstrdup(user);
+       len = socks_req_len(PROXY_SOCKS4, &sa);
+       assert_int_equal(9 + userlen, len);
+}
+
+static void test_socks_req_len_socks4_ipv6(void **state) {
+       (void)state;
+
+       sockaddr_t sa = str2sockaddr("::1", "4242");
+       size_t len = socks_req_len(PROXY_SOCKS4, &sa);
+       assert_int_equal(0, len);
+}
+
+static void test_socks_req_len_socks5_ipv4(void **state) {
+       (void)state;
+
+       sockaddr_t sa = str2sockaddr("127.0.0.1", "4242");
+       size_t baselen = 13;
+
+       // Base test
+       size_t len = socks_req_len(PROXY_SOCKS5, &sa);
+       assert_int_equal(baselen, len);
+
+       // Setting only password must not change result
+       proxypass = xstrdup(pass);
+       len = socks_req_len(PROXY_SOCKS5, &sa);
+       assert_int_equal(baselen, len);
+
+       // Setting both must
+       proxyuser = xstrdup(user);
+       len = socks_req_len(PROXY_SOCKS5, &sa);
+       assert_int_equal(baselen + 3 + userlen + passlen, len);
+}
+
+static void test_socks_req_len_socks5_ipv6(void **state) {
+       (void)state;
+
+       sockaddr_t sa = str2sockaddr("::1", "4242");
+       size_t baselen = 25;
+
+       // Base test
+       size_t len = socks_req_len(PROXY_SOCKS5, &sa);
+       assert_int_equal(baselen, len);
+
+       // Setting only user must not change result
+       proxyuser = xstrdup(user);
+       len = socks_req_len(PROXY_SOCKS5, &sa);
+       assert_int_equal(baselen, len);
+
+       // Setting both must
+       proxypass = xstrdup(pass);
+       len = socks_req_len(PROXY_SOCKS5, &sa);
+       assert_int_equal(baselen + 3 + userlen + passlen, len);
+}
+
+static void test_socks_req_len_wrong_types(void **state) {
+       (void)state;
+
+       sockaddr_t sa = str2sockaddr("::1", "4242");
+
+       assert_int_equal(0, socks_req_len(PROXY_NONE, &sa));
+       assert_int_equal(0, socks_req_len(PROXY_SOCKS4A, &sa));
+       assert_int_equal(0, socks_req_len(PROXY_HTTP, &sa));
+       assert_int_equal(0, socks_req_len(PROXY_EXEC, &sa));
+}
+
+static void test_socks_req_len_wrong_family(void **state) {
+       (void)state;
+
+       sockaddr_t sa = {.sa.sa_family = AF_UNKNOWN};
+       assert_int_equal(0, socks_req_len(PROXY_SOCKS4, &sa));
+       assert_int_equal(0, socks_req_len(PROXY_SOCKS5, &sa));
+}
+
+static void test_check_socks_resp_wrong_types(void **state) {
+       (void)state;
+
+       uint8_t buf[512] = {0};
+       assert_false(check_socks_resp(PROXY_NONE, buf, sizeof(buf)));
+       assert_false(check_socks_resp(PROXY_SOCKS4A, buf, sizeof(buf)));
+       assert_false(check_socks_resp(PROXY_HTTP, buf, sizeof(buf)));
+       assert_false(check_socks_resp(PROXY_EXEC, buf, sizeof(buf)));
+}
+
+PACKED(struct socks4_response {
+       uint8_t version;
+       uint8_t status;
+       uint16_t port;
+       uint32_t addr;
+});
+
+static const uint32_t localhost_ipv4 = 0x7F000001;
+
+static void test_check_socks_resp_socks4_ok(void **state) {
+       (void)state;
+
+       const struct socks4_response resp = {
+               .version = 0x00,
+               .status = 0x5A,
+               .port = htons(12345),
+               .addr = htonl(localhost_ipv4),
+       };
+       assert_true(check_socks_resp(PROXY_SOCKS4, &resp, sizeof(resp)));
+}
+
+static void test_check_socks_resp_socks4_bad(void **state) {
+       (void)state;
+
+       const uint8_t short_len[] = {0x00, 0x5A};
+       assert_false(check_socks_resp(PROXY_SOCKS4, short_len, sizeof(short_len)));
+
+       const struct socks4_response bad_version = {
+               .version = 0x01,
+               .status = 0x5A,
+               .port = htons(12345),
+               .addr = htonl(0x7F000001),
+       };
+       assert_false(check_socks_resp(PROXY_SOCKS4, &bad_version, sizeof(bad_version)));
+
+       const struct socks4_response status_denied = {
+               .version = 0x00,
+               .status = 0x5B,
+               .port = htons(12345),
+               .addr = htonl(0x7F000001),
+       };
+       assert_false(check_socks_resp(PROXY_SOCKS4, &status_denied, sizeof(status_denied)));
+}
+
+PACKED(struct socks5_response {
+       struct {
+               uint8_t socks_version;
+               uint8_t auth_method;
+       } greet;
+
+       struct {
+               uint8_t version;
+               uint8_t status;
+       } auth;
+
+       struct {
+               uint8_t socks_version;
+               uint8_t status;
+               uint8_t reserved;
+               uint8_t addr_type;
+
+               union {
+                       struct {
+                               uint32_t addr;
+                               uint16_t port;
+                       } ipv4;
+
+                       struct {
+                               uint8_t addr[16];
+                               uint16_t port;
+                       } ipv6;
+               };
+       } resp;
+});
+
+PACKED(struct socks5_test_resp_t {
+       socks5_resp_t resp;
+
+       union {
+               struct {
+                       uint32_t addr;
+                       uint16_t port;
+               } ipv4;
+
+               struct {
+                       uint8_t addr[16];
+                       uint16_t port;
+               } ipv6;
+       };
+});
+
+typedef struct socks5_test_resp_t socks5_test_resp_t;
+
+static socks5_test_resp_t *make_good_socks5_ipv4(void) {
+       static const socks5_test_resp_t reference = {
+               .resp = {
+                       .choice = {.socks_version = 0x05, .auth_method = 0x02},
+                       .pass = {
+                               .status = {.auth_version = 0x01, .auth_status = 0x00},
+                               .resp = {
+                                       .socks_version = 0x05,
+                                       .conn_status = 0x00,
+                                       .reserved = 0x00,
+                                       .addr_type = 0x01,
+                               },
+                       },
+               },
+               .ipv4 = {.addr = 0x01020304, .port = 0x123},
+       };
+
+       socks5_test_resp_t *result = xmalloc(sizeof(socks5_test_resp_t));
+       memcpy(result, &reference, sizeof(reference));
+       return result;
+}
+
+static socks5_test_resp_t *make_good_socks5_ipv6(void) {
+       static const socks5_test_resp_t reference = {
+               .resp = {
+                       .choice = {.socks_version = 0x05, .auth_method = 0x02},
+                       .pass = {
+                               .status = {.auth_version = 0x01, .auth_status = 0x00},
+                               .resp = {
+                                       .socks_version = 0x05,
+                                       .conn_status = 0x00,
+                                       .reserved = 0x00,
+                                       .addr_type = 0x04,
+                               },
+                       },
+               },
+               .ipv6 = {
+                       .addr = {
+                               0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                       },
+                       .port = 0x123,
+               },
+       };
+
+       socks5_test_resp_t *result = xmalloc(sizeof(socks5_test_resp_t));
+       memcpy(result, &reference, sizeof(reference));
+       return result;
+}
+
+static void test_check_socks_resp_socks5_ok_ipv4(void **state) {
+       (void)state;
+
+       socks5_test_resp_t *resp = make_good_socks5_ipv4();
+       assert_true(check_socks_resp(PROXY_SOCKS5, resp, sizeof(*resp)));
+       free(resp);
+}
+
+static void test_check_socks_resp_socks5_ok_ipv6(void **state) {
+       (void)state;
+
+       socks5_test_resp_t *resp = make_good_socks5_ipv6();
+       assert_true(check_socks_resp(PROXY_SOCKS5, resp, sizeof(*resp)));
+       free(resp);
+}
+
+static void test_check_socks_resp_socks5_short(void **state) {
+       (void)state;
+
+       const uint8_t resp[] = {0x05, 0x02};
+       assert_false(check_socks_resp(PROXY_SOCKS5, resp, sizeof(resp)));
+}
+
+// Define a test that assigns a bad value to one of the fields and checks that it fails
+#define BREAK_SOCKS5_FIELD_TEST(proto, name, expr)                                   \
+       static void test_check_socks_resp_socks5_bad_##name##_##proto(void **state) {    \
+               (void)state;                                                                 \
+               socks5_test_resp_t *resp = make_good_socks5_##proto();                       \
+               assert_true(check_socks_resp(PROXY_SOCKS5, resp, sizeof(*resp)));            \
+               expr;                                                                        \
+               assert_false(check_socks_resp(PROXY_SOCKS5, resp, sizeof(*resp)));           \
+               free(resp);                                                                  \
+       }
+
+// Define a test group for IPv4 or IPv6
+#define BREAK_SOCKS5_TEST_GROUP(proto) \
+       BREAK_SOCKS5_FIELD_TEST(proto, resp_socks_version,   resp->resp.pass.resp.socks_version = 0x4)  \
+       BREAK_SOCKS5_FIELD_TEST(proto, resp_conn_status,     resp->resp.pass.resp.conn_status = 0x1)    \
+       BREAK_SOCKS5_FIELD_TEST(proto, resp_addr_type,       resp->resp.pass.resp.addr_type = 0x42)     \
+       BREAK_SOCKS5_FIELD_TEST(proto, choice_socks_version, resp->resp.choice.socks_version = 0x04)    \
+       BREAK_SOCKS5_FIELD_TEST(proto, choice_auth_method,   resp->resp.choice.auth_method = 0x12)      \
+       BREAK_SOCKS5_FIELD_TEST(proto, status_auth_version,  resp->resp.pass.status.auth_version = 0x2) \
+       BREAK_SOCKS5_FIELD_TEST(proto, status_auth_status,   resp->resp.pass.status.auth_status = 0x1)
+
+BREAK_SOCKS5_TEST_GROUP(ipv4)
+BREAK_SOCKS5_TEST_GROUP(ipv6)
+
+static void test_create_socks_req_socks4(void **state) {
+       (void)state;
+
+       const uint8_t ref[8] = {0x04, 0x01, 0x00, 0x7b, 0x01, 0x01, 0x01, 0x01};
+       const sockaddr_t sa = str2sockaddr("1.1.1.1", "123");
+
+       uint8_t buf[512];
+       assert_int_equal(sizeof(ref), create_socks_req(PROXY_SOCKS4, buf, &sa));
+       assert_memory_equal(ref, buf, sizeof(ref));
+}
+
+static void test_create_socks_req_socks5_ipv4_anon(void **state) {
+       (void) state;
+
+       const sockaddr_t sa = str2sockaddr("2.2.2.2", "16962");
+
+       const uint8_t ref[13] = {
+               0x05, 0x01, 0x00,
+               0x05, 0x01, 0x00, 0x01, 0x02, 0x02, 0x02, 0x02, 0x42, 0x42,
+       };
+
+       uint8_t buf[sizeof(ref)];
+       assert_int_equal(12, create_socks_req(PROXY_SOCKS5, buf, &sa));
+       assert_memory_equal(ref, buf, sizeof(ref));
+}
+
+static void test_create_socks_req_socks5_ipv4_password(void **state) {
+       (void)state;
+
+       proxyuser = xstrdup(user);
+       proxypass = xstrdup(pass);
+
+       const sockaddr_t sa = str2sockaddr("2.2.2.2", "16962");
+
+       const uint8_t ref[22] = {
+               0x05, 0x01, 0x02,
+               0x01, (uint8_t)userlen, 'f', 'o', 'o', (uint8_t)passlen, 'b', 'a', 'r',
+               0x05, 0x01, 0x00, 0x01, 0x02, 0x02, 0x02, 0x02, 0x42, 0x42,
+       };
+
+       uint8_t buf[sizeof(ref)];
+       assert_int_equal(14, create_socks_req(PROXY_SOCKS5, buf, &sa));
+       assert_memory_equal(ref, buf, sizeof(ref));
+}
+
+static void test_create_socks_req_socks5_ipv6_anon(void **state) {
+       (void)state;
+
+       const sockaddr_t sa = str2sockaddr("1111:2222::3333:4444:5555", "18504");
+
+       const uint8_t ref[25] = {
+               0x05, 0x01, 0x00,
+               0x05, 0x01, 0x00, 0x04,
+               0x11, 0x11, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x33, 0x44, 0x44, 0x55, 0x55,
+               0x48, 0x48,
+       };
+
+       uint8_t anon_buf[sizeof(ref)];
+       assert_int_equal(24, create_socks_req(PROXY_SOCKS5, anon_buf, &sa));
+       assert_memory_equal(ref, anon_buf, sizeof(ref));
+}
+
+
+static void test_create_socks_req_socks5_ipv6_password(void **state) {
+       (void)state;
+
+       proxyuser = xstrdup(user);
+       proxypass = xstrdup(pass);
+
+       const sockaddr_t sa = str2sockaddr("4444:2222::6666:4444:1212", "12850");
+
+       const uint8_t ref[34] = {
+               0x05, 0x01, 0x02,
+               0x01, (uint8_t)userlen, 'f', 'o', 'o', (uint8_t)passlen, 'b', 'a', 'r',
+               0x05, 0x01, 0x00, 0x04,
+               0x44, 0x44, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x66, 0x44, 0x44, 0x12, 0x12,
+               0x32, 0x32,
+       };
+
+       uint8_t anon_buf[sizeof(ref)];
+       assert_int_equal(26, create_socks_req(PROXY_SOCKS5, anon_buf, &sa));
+       assert_memory_equal(ref, anon_buf, sizeof(ref));
+}
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test_teardown(test_socks_req_len_socks4_ipv4, teardown),
+               cmocka_unit_test_teardown(test_socks_req_len_socks4_ipv6, teardown),
+               cmocka_unit_test_teardown(test_socks_req_len_socks5_ipv4, teardown),
+               cmocka_unit_test_teardown(test_socks_req_len_socks5_ipv6, teardown),
+               cmocka_unit_test_teardown(test_socks_req_len_wrong_types, teardown),
+               cmocka_unit_test_teardown(test_socks_req_len_wrong_family, teardown),
+
+               cmocka_unit_test(test_check_socks_resp_wrong_types),
+               cmocka_unit_test(test_check_socks_resp_socks4_ok),
+               cmocka_unit_test(test_check_socks_resp_socks4_bad),
+               cmocka_unit_test(test_check_socks_resp_socks5_ok_ipv4),
+               cmocka_unit_test(test_check_socks_resp_socks5_ok_ipv6),
+               cmocka_unit_test(test_check_socks_resp_socks5_short),
+
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_resp_socks_version_ipv4),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_resp_conn_status_ipv4),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_resp_addr_type_ipv4),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_choice_socks_version_ipv4),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_choice_auth_method_ipv4),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_status_auth_version_ipv4),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_status_auth_status_ipv4),
+
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_resp_socks_version_ipv6),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_resp_conn_status_ipv6),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_resp_addr_type_ipv6),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_choice_socks_version_ipv6),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_choice_auth_method_ipv6),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_status_auth_version_ipv6),
+               cmocka_unit_test(test_check_socks_resp_socks5_bad_status_auth_status_ipv6),
+
+               cmocka_unit_test_teardown(test_create_socks_req_socks4, teardown),
+               cmocka_unit_test_teardown(test_create_socks_req_socks5_ipv4_anon, teardown),
+               cmocka_unit_test_teardown(test_create_socks_req_socks5_ipv4_password, teardown),
+               cmocka_unit_test_teardown(test_create_socks_req_socks5_ipv6_anon, teardown),
+               cmocka_unit_test_teardown(test_create_socks_req_socks5_ipv6_password, teardown),
+       };
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}