From a5c6c6ea1ab657d83a4d8b064ac9bfa9c16adf63 Mon Sep 17 00:00:00 2001 From: Kirill Isakov Date: Mon, 25 Apr 2022 01:38:50 +0600 Subject: [PATCH] Improve proxy server support - 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 --- src/connection.h | 4 +- src/meson.build | 1 + src/meta.c | 30 +- src/net_socket.c | 5 +- src/protocol_auth.c | 83 +---- src/protocol_misc.c | 4 +- src/proxy.c | 285 ++++++++++++++++ src/proxy.h | 113 +++++++ test/integration/meson.build | 3 +- test/integration/proxy.py | 482 +++++++++++++++++++++++++++ test/integration/testlib/check.py | 12 + test/integration/testlib/path.py | 2 + test/integration/testlib/template.py | 3 +- test/unit/meson.build | 3 + test/unit/test_proxy.c | 430 ++++++++++++++++++++++++ 15 files changed, 1348 insertions(+), 112 deletions(-) create mode 100644 src/proxy.c create mode 100644 src/proxy.h create mode 100755 test/integration/proxy.py create mode 100644 test/unit/test_proxy.c diff --git a/src/connection.h b/src/connection.h index e39d74bb..6489ef4f 100644 --- a/src/connection.h +++ b/src/connection.h @@ -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 */ diff --git a/src/meson.build b/src/meson.build index f84fb4d0..c780ba11 100644 --- a/src/meson.build +++ b/src/meson.build @@ -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', diff --git a/src/meta.c b/src/meta.c index 0ff7bef0..e3f29998 100644 --- a/src/meta.c +++ b/src/meta.c @@ -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 { diff --git a/src/net_socket.c b/src/net_socket.c index 8783b01a..5d72471b 100644 --- a/src/net_socket.c +++ b/src/net_socket.c @@ -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); } diff --git a/src/protocol_auth.c b/src/protocol_auth.c index b13d901a..211d9083 100644 --- a/src/protocol_auth.c +++ b/src/protocol_auth.c @@ -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: diff --git a/src/protocol_misc.c b/src/protocol_misc.c index cef7e3d0..0263d001 100644 --- a/src/protocol_misc.c +++ b/src/protocol_misc.c @@ -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 index 00000000..f7d34d08 --- /dev/null +++ b/src/proxy.c @@ -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 index 00000000..91735667 --- /dev/null +++ b/src/proxy.h @@ -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 diff --git a/test/integration/meson.build b/test/integration/meson.build index 5dc2430a..5e82fe85 100644 --- a/test/integration/meson.build +++ b/test/integration/meson.build @@ -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 index 00000000..93b51f58 --- /dev/null +++ b/test/integration/proxy.py @@ -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) diff --git a/test/integration/testlib/check.py b/test/integration/testlib/check.py index 77865b1a..1d1dfb0f 100755 --- a/test/integration/testlib/check.py +++ b/test/integration/testlib/check.py @@ -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: diff --git a/test/integration/testlib/path.py b/test/integration/testlib/path.py index a33fba55..4a90ac9e 100755 --- a/test/integration/testlib/path.py +++ b/test/integration/testlib/path.py @@ -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.""" diff --git a/test/integration/testlib/template.py b/test/integration/testlib/template.py index b90299c0..83d2f853 100755 --- a/test/integration/testlib/template.py +++ b/test/integration/testlib/template.py @@ -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, diff --git a/test/unit/meson.build b/test/unit/meson.build index eaa9d181..8633c186 100644 --- a/test/unit/meson.build +++ b/test/unit/meson.build @@ -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 index 00000000..a5b30bd1 --- /dev/null +++ b/test/unit/test_proxy.c @@ -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); +} -- 2.20.1