• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Пишем backconnect socks5 на C++ с нуля (Win/Linux/MacOS)

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,182
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Дорогие пользователи xss.is, пишу данную статью для вас, чтобы ввести вас немного в курс дела, как можно достаточно просто написать свой backconnect socks5 сервер и клиент.
Это подробный пошаговый гайд, как "с нуля" написать backconnect SOCKS5.
Рассмотрим сервер и клиент по отдельности: начнём с пустого файла, постепенно дополним его нужными методами. Покажу логику и смысл каждой ключевой функции. Затем объединим всё в работающее решение.



Часть 1. Сервер (server_socks5.cpp)

1. Создаём скелет программы

Начинаем с самого простого каркаса main(), который принимает аргументы:

C++:
#include <iostream>
#include <string>

int main(int argc, char* argv[]) {
    // 1) Считать аргументы командной строки (порты, ключ и т.п.)
    // 2) Инициализация сетевых функций (Windows: WSAStartup)
    // 3) Запуск потоков: один для control-порта, один для SOCKS5
    // 4) Ожидать их завершения

    return 0;
}

Разбор
  • int main(int argc, char* argv[]) - обычная точка входа.
  • Мы планируем принимать:
    • -c <control_port> - порт для "управляющих" подключений от клиента (что сидит за NAT).
    • -S <socks_port> - порт, на котором будем принимать SOCKS5-запросы.
    • -x <xor_key> - ключ для XOR (для простенького шифрования).
    • -u <user> -p <pass> - логин/пароль (необязательно).
    • -d - отладочный режим (вывод детальной информации).

2. Подключаем заголовки для сетей и определяем платформозависимые вещи

Чтобы всё работало и на Windows, и на Linux/macOS, нужно аккуратно подключить разные заголовки.
Создадим блок:

C++:
#ifdef _WIN32
  #include <winsock2.h>
  #include <ws2tcpip.h>
  #pragma comment(lib, "ws2_32.lib")
  typedef SOCKET SocketType;
  #define CLOSESOCK closesocket
  #define SOCKERROR WSAGetLastError()
#else
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <arpa/inet.h>
  #include <unistd.h>
  #include <netdb.h>
  typedef int SocketType;
  #define CLOSESOCK close
  #define INVALID_SOCKET -1
  #define SOCKERROR errno
#endif

Разбор
  • #ifdef _WIN32 - компиляция под Windows. Используем WinSock2 (winsock2.h) и т.д.
  • Имена типов и функций в Windows отличаются, поэтому вводим алиасы:
    • typedef SOCKET SocketType;
    • #define CLOSESOCK closesocket
  • В Unix-системах всё проще (сокеты - это целые числа).

3. Функции инициализации сетей

Подготовим функции, которые будут удобными для старта/завершения:

C++:
bool initSockets() {
#ifdef _WIN32
    WSADATA wd;
    int res = WSAStartup(MAKEWORD(2,2), &wd);
    if(res != 0){
        std::cerr << "[Server] WSAStartup error=" << res << "\n";
        return false;
    }
#endif
    return true;
}

void cleanupSockets() {
#ifdef _WIN32
    WSACleanup();
#endif
}

Разбор
  • В Windows надо один раз вызвать WSAStartup(...). В Linux/macOS - ничего не нужно.

4. Создаём слушающий сокет

Нам нужно открыть TCP-сокет, привязать к порту, вызвать listen(...). Сделаем универсальную функцию:

C++:
SocketType createListeningSocket(uint16_t port){
    SocketType sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock == INVALID_SOCKET){
        std::cerr << "[Server] socket() error=" << SOCKERROR << "\n";
        return INVALID_SOCKET;
    }

    // Разрешим переиспользовать адрес
    int opt = 1;
#ifdef _WIN32
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
#else
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#endif

    sockaddr_in addr;
    std::memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0

    // bind
    if(bind(sock, (sockaddr*)&addr, sizeof(addr)) < 0){
        std::cerr << "[Server] bind error=" << SOCKERROR << "\n";
        CLOSESOCK(sock);
        return INVALID_SOCKET;
    }

    // listen
    if(listen(sock, 10) < 0){
        std::cerr << "[Server] listen error=" << SOCKERROR << "\n";
        CLOSESOCK(sock);
        return INVALID_SOCKET;
    }
    return sock;
}
Разбор
  • socket(AF_INET, SOCK_STREAM, 0) создаёт TCP-сокет (Stream).
  • bind(...) связывает сокет с нужным портом.
  • listen(...) переводит сокет в режим "принимать входящие".

5. XOR-функция

Нужно шифровать/дешифровать любой буфер. С помощью XOR всё просто:

C++:
void xorData(char* data, int len, const std::string &key){
    if(key.empty()) return; // если ключ пустой - ничего не делаем
    for(int i = 0; i < len; i++){
        data[i] ^= key[i % key.size()];
    }
}
Разбор
  • data ^= key[i % key.size()]- это и есть "XOR".
  • Один и тот же ключ применяем по кругу.

6. Отправка/приём с учётом "досылки" (sendAll, recvAll)

Часто send или recv возвращают меньше байт, чем запрашивали. Сделаем утилиты:

C++:
bool sendAll(SocketType s, const char* data, int len){
    int total = 0;
    while(total < len){
        int sent = send(s, data + total, len - total, 0);
        if(sent <= 0) return false;
        total += sent;
    }
    return true;
}

bool recvAll(SocketType s, char* buf, int len){
    int total = 0;
    while(total < len){
        int r = recv(s, buf + total, len - total, 0);
        if(r <= 0) return false;
        total += r;
    }
    return true;
}

Разбор
  • sendAll(...) в цикле шлёт, пока все байты не "утолкнёт" в сокет.
  • recvAll(...) аналогично ждёт, пока не получит все нужные байты (или вернётся с ошибкой).

7. sendEnc / recvEnc

Чтобы перед отправкой/приёмом выполнить XOR, сделаем, например, sendEnc (достаточно одной функции для отправки). Для приёма мы можем сразу применять XOR после получения. Ниже пример для отправки:

C++:
bool sendEnc(SocketType s, const char* data, int len, const std::string &key){
    if(s == INVALID_SOCKET) return false;
    std::vector<char> tmp(data, data + len);
    xorData(tmp.data(), len, key);   // "зашифровать" tmp
    return sendAll(s, tmp.data(), len);
}

Разбор
  • Копируем исходный буфер в tmp.
  • Делаем xorData(...).
  • Шлём зашифрованный результат.
Для приёма можно либо аналогично сделать recvEnc, либо вручную читать recvAll(...) и потом прогонять через xorData().

8. Реализация SOCKS5 (в режиме сервера)

8.1 Выбираем метод аутентификации
Когда к нашему SOCKS5-порту подключается клиент, он сначала отправляет:
1. Версию: 0x05
2. Количество поддерживаемых методов: N
3. Список методов.

Мы должны прочитать это, проверить, есть ли 0x00 (No Auth) или 0x02 (User/Pass). Затем ответить, какой метод выбрали.

C++:
bool socks5Handshake_SelectMethod(SocketType s, bool &useUserPass, const std::string &user, const std::string &pass) {
    unsigned char hdr[2];
    int r = recv(s, (char*)hdr, 2, 0);
    if(r < 2) return false;
    if(hdr[0] != 0x05) return false; // версия SOCKS5

    int nMethods = hdr[1];
    std::vector<unsigned char> methods(nMethods);
    r = recv(s, (char*)methods.data(), nMethods, 0);
    if(r < nMethods) return false;

    // Проверяем, нужно ли нам вообще auth (заданы ли user/pass)
    bool needAuth = (!user.empty() || !pass.empty());

    if(needAuth) {
        // Ищем METHOD_USERPASS (0x02)
        bool found = false;
        for(unsigned char m: methods){
            if(m == 0x02){
                found = true;
                break;
            }
        }
        if(!found) {
            // Отправляем REJECT (0xFF)
            unsigned char resp[2] = {0x05, 0xFF};
            sendAll(s, (char*)resp, 2);
            return false;
        }
        // Сигнализируем, что выбрали user/pass
        unsigned char resp[2] = {0x05, 0x02};
        sendAll(s, (char*)resp, 2);
        useUserPass = true;
    } else {
        // Без аутентификации
        bool found = false;
        for(unsigned char m: methods){
            if(m == 0x00){
                found = true;
                break;
            }
        }
        if(!found) {
            unsigned char resp[2] = {0x05, 0xFF};
            sendAll(s, (char*)resp, 2);
            return false;
        }
        unsigned char resp[2] = {0x05, 0x00};
        sendAll(s, (char*)resp, 2);
        useUserPass = false;
    }
    return true;
}

8.2 Аутентификация по user/pass
Если выбрано useUserPass, клиент отправит:
  • Версию subnegotiation (0x01).
  • Длину имени пользователя (1 байт).
  • Само имя пользователя.
  • Длину пароля (1 байт).
  • Сам пароль.
Мы проверим, совпадает ли с нашим user/pass.

C++:
bool socks5Handshake_UserPass(SocketType s, const std::string &user, const std::string &pass){
    unsigned char ver;
    if(recv(s, (char*)&ver, 1, 0) < 1) return false;
    if(ver != 0x01) return false; // subneg version = 1

    unsigned char ulen;
    if(recv(s, (char*)&ulen, 1, 0) < 1) return false;
    std::vector<char> uname(ulen);
    if(!recvAll(s, uname.data(), ulen)) return false;

    unsigned char plen;
    if(recv(s, (char*)&plen, 1, 0) < 1) return false;
    std::vector<char> upass(plen);
    if(!recvAll(s, upass.data(), plen)) return false;

    std::string su(uname.begin(), uname.end());
    std::string sp(upass.begin(), upass.end());

    // Сравним
    unsigned char status = 0x00;
    if(su != user || sp != pass){
        status = 0x01; // auth fail
    }

    unsigned char resp[2] = {0x01, status};
    sendAll(s, (char*)resp, 2);
    return (status == 0x00);
}
8.3 Обработка CONNECT
После выбора метода клиент посылает:
  • 1 байт: 0x05 (версия)
  • 1 байт: 0x01 (команда CONNECT)
  • 1 байт: 0x00 (зарезервировано)
  • 1 байт: тип адреса: 0x01 (IPv4) или 0x03 (домен)
Далее, если тип 0x01, идут 4 байта IPv4-адреса, потом 2 байта порта. Если 0x03, то 1 байт длины домена, далее сам домен, далее 2 байта порта.

C++:
bool socks5ParseConnect(SocketType s, uint32_t &ip, uint16_t &port) {
    unsigned char hdr[4];
    if(!recvAll(s, (char*)hdr, 4)) return false;
    // hdr[0] = 0x05 (версия), hdr[1] = 0x01 (CONNECT), hdr[2]=0x00, hdr[3]=ATYP
    if(hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00) return false;

    unsigned char atyp = hdr[3];
    if(atyp == 0x01){
        // IPv4
        unsigned char a4[4];
        if(!recvAll(s, (char*)a4, 4)) return false;
        // Собираем в 32-битное число (host order)
        ip = ( (uint32_t)a4[0] << 24 )
           | ( (uint32_t)a4[1] << 16 )
           | ( (uint32_t)a4[2] <<  8 )
           | ( (uint32_t)a4[3] );
        unsigned char pbuf[2];
        if(!recvAll(s, (char*)pbuf, 2)) return false;
        port = ((uint16_t)pbuf[0] << 8) | (uint16_t)pbuf[1];
    }
    else if(atyp == 0x03){
        // Доменное имя
        unsigned char dlen;
        if(!recvAll(s, (char*)&dlen, 1)) return false;
        std::vector<char> dom(dlen+1);
        if(!recvAll(s, dom.data(), dlen)) return false;
        dom[dlen] = '\0';

        unsigned char pbuf[2];
        if(!recvAll(s, (char*)pbuf, 2)) return false;
        port = ((uint16_t)pbuf[0] << 8) | (uint16_t)pbuf[1];

        // Резолвим домен
        struct hostent* he = gethostbyname(dom.data());
        if(!he) return false;
        struct in_addr[b] alist = (struct in_addr[/b])he->h_addr_list;
        if(!alist[0]) return false;
        ip = ntohl(alist[0]->s_addr);
    } else {
        return false;
    }
    return true;
}

8.4 Ответ CONNECT
Когда мы разобрались, нужно послать "ответ" от SOCKS5:
  • 0x05 (версия)
  • <rep> (код результата)
  • 0x00 (зарезервировано)
  • 0x01 (тип адреса = IPv4)
  • <4 байта IP>
  • <2 байта порт>
C++:
void socks5SendConnectReply(SocketType s, unsigned char rep, uint32_t ip=0, uint16_t port=0){
    unsigned char buf[10];
    buf[0] = 0x05;
    buf[1] = rep;    // 0x00 = успех, иначе ошибка
    buf[2] = 0x00;
    buf[3] = 0x01;   // IPv4
    uint32_t ip_n = htonl(ip);
    std::memcpy(buf + 4, &ip_n, 4);
    uint16_t p_n = htons(port);
    std::memcpy(buf + 8, &p_n, 2);
    sendAll(s, (char*)buf, 10);
}

9. Логика "backconnect" на сервере

На сервере есть глобальные переменные:

C++:
#include <mutex>
std::mutex g_mutex;
SocketType g_natClientSock = INVALID_SOCKET;  // сокет с NAT-клиентом
bool       g_natClientConnected = false;
std::string g_xorKey; // ключ XOR

9.1 Обработка SOCKS-подключения
В отдельном потоке (на каждое подключение к SOCKS-порту) делаем:

C++:
void handleSocksClient(SocketType sock,
                       const std::string &user,
                       const std::string &pass,
                       bool debug)
{
    bool useUserPass = false;
    // 1) Выбор метода (NoAuth или UserPass)
    if(!socks5Handshake_SelectMethod(sock, useUserPass, user, pass)){
        CLOSESOCK(sock);
        return;
    }

    // 2) Если выбрано user/pass, выполнить subneg
    if(useUserPass) {
        if(!socks5Handshake_UserPass(sock, user, pass)){
            CLOSESOCK(sock);
            return;
        }
    }

    // 3) Достаём IP, порт для CONNECT
    uint32_t tip = 0;
    uint16_t tport = 0;
    if(!socks5ParseConnect(sock, tip, tport)){
        socks5SendConnectReply(sock, 0x01); // ошибка
        CLOSESOCK(sock);
        return;
    }

    // 4) Создаем ephemeral socket (он примет соединение от NAT-клиента)
    SocketType epSock = socket(AF_INET, SOCK_STREAM, 0);
    if(epSock == INVALID_SOCKET){
        socks5SendConnectReply(sock, 0x01);
        CLOSESOCK(sock);
        return;
    }
    sockaddr_in ep;
    std::memset(&ep, 0, sizeof(ep));
    ep.sin_family = AF_INET;
    ep.sin_port   = 0; // сами выберем порт
    ep.sin_addr.s_addr = INADDR_ANY;
    if(bind(epSock, (sockaddr*)&ep, sizeof(ep)) < 0){
        socks5SendConnectReply(sock, 0x01);
        CLOSESOCK(epSock);
        CLOSESOCK(sock);
        return;
    }
    if(listen(epSock, 1) < 0){
        socks5SendConnectReply(sock, 0x01);
        CLOSESOCK(epSock);
        CLOSESOCK(sock);
        return;
    }
    sockaddr_in tmp;
    socklen_t sz = sizeof(tmp);
    getsockname(epSock, (sockaddr*)&tmp, &sz);
    uint16_t ephemeralPort = ntohs(tmp.sin_port);

    // 5) Отправляем команду 'C' нашему NAT-клиенту
    bool okSend = false;
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        if(g_natClientSock != INVALID_SOCKET && g_natClientConnected){
            // Формируем пакет: 1 байт 'C' + 2 байта ephemeralPort + 4 байта IP + 2 байта targetPort
            char cmd[1 + 2 + 4 + 2];
            cmd[0] = 'C';
            uint16_t ep_n = htons(ephemeralPort);
            std::memcpy(cmd+1, &ep_n, 2);
            uint32_t tip_n = htonl(tip);
            std::memcpy(cmd+3, &tip_n, 4);
            uint16_t tpt_n = htons(tport);
            std::memcpy(cmd+7, &tpt_n, 2);

            okSend = sendEnc(g_natClientSock, cmd, sizeof(cmd), g_xorKey);
        }
    }
    if(!okSend){
        socks5SendConnectReply(sock, 0x05);
        CLOSESOCK(epSock);
        CLOSESOCK(sock);
        return;
    }

    // 6) Ждем подключение на epSock (нат-клиент туда зайдет)
    sockaddr_in from;
    socklen_t flen = sizeof(from);
    SocketType esock = accept(epSock, (sockaddr*)&from, &flen);
    CLOSESOCK(epSock); // уже не нужен
    if(esock == INVALID_SOCKET){
        socks5SendConnectReply(sock, 0x05);
        CLOSESOCK(sock);
        return;
    }

    // 7) Сообщаем SOCKS5-клиенту, что всё ок
    socks5SendConnectReply(sock, 0x00, tip, tport);

    // 8) Пересылаем данные в обоих направлениях
    std::thread tFwd([=](){
        char buf[4096];
        while(true){
            int rx = recv(sock, buf, 4096, 0);
            if(rx <= 0) break;
            int tx = send(esock, buf, rx, 0);
            if(tx <= 0) break;
        }
        CLOSESOCK(esock);
    });
    {
        char buf[4096];
        while(true){
            int rx = recv(esock, buf, 4096, 0);
            if(rx <= 0) break;
            int tx = send(sock, buf, rx, 0);
            if(tx <= 0) break;
        }
    }
    CLOSESOCK(esock);
    tFwd.join();
    CLOSESOCK(sock);
}

Разбор по шагам
  1. Считываем SOCKS5-запрос.
  2. Создаём временный порт (epSock).
  3. Шлём через NAT-клиенту команду C.
  4. Ждём, когда NAT-клиент "зайдёт" в этот epSock.
  5. После удачного соединения отвечаем SOCKS-клиенту 0x00 (OK).
  6. Гоним трафик туда-сюда.

10. Обработка control-порта

В отдельном потоке слушаем -c <control_port> и принимаем единственного NAT-клиента:

C++:
#include <chrono>

void controlAcceptLoop(uint16_t cPort, bool debug){
    SocketType listener = createListeningSocket(cPort);
    if(listener == INVALID_SOCKET){
        std::cerr << "[Server] Failed to listen on controlPort=" << cPort << "\n";
        return;
    }
    std::cout << "[Server] Waiting for NAT client on port " << cPort << "...\n";

    while(true){
        sockaddr_in caddr;
        socklen_t clen = sizeof(caddr);
        SocketType cs = accept(listener, (sockaddr*)&caddr, &clen);
        if(cs == INVALID_SOCKET){
            std::cerr << "[Server] accept() error on control channel\n";
            break;
        }

        // Проверим, не занят ли уже кто-то
        {
            std::lock_guard<std::mutex> lock(g_mutex);
            if(g_natClientConnected){
                // Уже подключен NAT-клиент
                const char msg[] = "OCCUP";
                // зашифруем
                sendEnc(cs, msg, 5, g_xorKey);
                CLOSESOCK(cs);
                continue;
            }
        }

        // Считываем 5 байт "HELLO" (XOR)
        char buf[5];
        bool okHello = false;
        // Хотим таймаут, например 5 секунд
#ifdef _WIN32
        DWORD tmo = 5000;
        setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tmo, sizeof(tmo));
#else
        struct timeval tv;
        tv.tv_sec = 5;
        tv.tv_usec = 0;
        setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
#endif
        if(recvAll(cs, buf, 5)) {
            // XOR
            xorData(buf, 5, g_xorKey);
            if(std::string(buf, 5) == "HELLO"){
                okHello = true;
            }
        }

        if(!okHello){
            CLOSESOCK(cs);
            continue;
        }

        // Отправим "OK"
        const char msgOk[] = "OK";
        sendEnc(cs, msgOk, 2, g_xorKey);

        // Считаем, что клиент подключился
        {
            std::lock_guard<std::mutex> lock(g_mutex);
            g_natClientSock = cs;
            g_natClientConnected = true;
        }
        std::cout << "[Server] NAT client connected!\n";

        // Читаем keep-alive ('K') или EOF
        while(true){
            char c;
            int r = recv(cs, &c, 1, 0);
            if(r <= 0){
                // отключился
                CLOSESOCK(cs);
                std::lock_guard<std::mutex> lock(g_mutex);
                g_natClientSock = INVALID_SOCKET;
                g_natClientConnected = false;
                break;
            }
            c ^= g_xorKey[0];
            if(c == 'K'){
                // keep-alive
            } else {
                // неизвестно, игнорируем
            }
        }
    }
    CLOSESOCK(listener);
}

11. Запуск потока SOCKS5 и control

В main() прописываем логику:

C++:
#include <thread>
#include <cstdlib>

int main(int argc, char* argv[]){
    // 1) Парсим аргументы
    uint16_t controlPort = 0;
    uint16_t socksPort   = 0;
    std::string user, pass;
    bool debug = false;
    // ... (парсим -c, -S, -x, -u, -p, -d и т.д.)

    if(!initSockets()){
        return 1;
    }

    // Стартуем поток, слушающий контрольный порт
    std::thread tCtl(controlAcceptLoop, controlPort, debug);

    // Запустим SOCKS5-listener
    SocketType socksListener = createListeningSocket(socksPort);
    if(socksListener == INVALID_SOCKET){
        std::cerr << "[Server] Unable to listen on " << socksPort << "\n";
        return 1;
    }

    while(true){
        sockaddr_in saddr;
        socklen_t slen = sizeof(saddr);
        SocketType c = accept(socksListener, (sockaddr*)&saddr, &slen);
        if(c == INVALID_SOCKET){
            // ошибка или завершение
            break;
        }
        // Запустим поток handleSocksClient
        std::thread th(handleSocksClient, c, user, pass, debug);
        th.detach();
    }

    CLOSESOCK(socksListener);
    tCtl.join();
    cleanupSockets();
    return 0;
}
Таким образом, сервер готов.


Часть 2. Клиент (client_socks5.cpp)

Теперь клиент, который сидит за NAT. Он сам коннектится к серверу и держит соединение. Когда сервер просит "создать туннель", мы выполняем команду C.

1. Структура main()

C++:
int main(int argc, char* argv[]){
    // 1) Парсим -s <server_ip>, -c <control_port>, -x <xor_key>, -d (debug).
    // 2) initSockets()
    // 3) Запускаем цикл connect -> если ок, controlChannelLoop()
    // 4) Если отвалилось, повторяем

    return 0;
}

2. Цикл переподключения
Нам нужен вечный цикл:
C++:
void runClientLoop(const std::string &serverIP,
                   uint16_t controlPort,
                   const std::string &xorKey,
                   bool debug)
{
    while(true){
        // создаём сокет
        SocketType s = socket(AF_INET, SOCK_STREAM, 0);
        if(s == INVALID_SOCKET){
            if(debug) std::cerr << "socket() error\n";
#ifdef _WIN32
            Sleep(5000);
#else
            sleep(5);
#endif
            continue;
        }
        // Подключаемся к serverIP:controlPort
        sockaddr_in addr;
        std::memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port   = htons(controlPort);
        inet_pton(AF_INET, serverIP.c_str(), &addr.sin_addr);

        if(connect(s, (sockaddr*)&addr, sizeof(addr)) < 0){
            if(debug) std::cerr << "Can't connect to server\n";
            CLOSESOCK(s);
#ifdef _WIN32
            Sleep(5000);
#else
            sleep(5);
#endif
            continue;
        }
        std::cout << "[Client] Connected to " << serverIP << ":" << controlPort << "\n";

        // Переходим в функцию, которая ведёт обмен по control-каналу
        controlChannelLoop(s, xorKey, debug);

        // Если вышли - значит соединение отвалилось
        CLOSESOCK(s);
        std::cerr << "[Client] Control connection closed. Retry in 5s...\n";
#ifdef _WIN32
        Sleep(5000);
#else
        sleep(5);
#endif
    }
}

3. controlChannelLoop(): отправляем "HELLO", ждём "OK"

C++:
void controlChannelLoop(SocketType ctrlSock,
                        const std::string &xorKey,
                        bool debug)
{
    // 1) Шлём "HELLO"
    {
        char hello[5] = {'H','E','L','L','O'};
        if(!sendEnc(ctrlSock, hello, 5, xorKey)){
            if(debug) std::cerr << "[Client] fail to send HELLO\n";
            return;
        }
    }
    // 2) Ждём ответ (5 байт, может быть "OK" или "OCCUP")
    char resp[5];
    int r = recv(ctrlSock, resp, 5, 0);
    if(r <= 0){
        if(debug) std::cerr << "[Client] no response\n";
        return;
    }
    // XOR
    for(int i=0; i<r; i++){
        resp[i] ^= xorKey[i % xorKey.size()];
    }
    std::string sresp(resp, r);
    if(sresp == "OCCUP"){
        std::cerr << "[Client] Server is busy.\n";
        return;
    } else if(sresp != "OK"){
        if(debug) std::cerr << "[Client] unknown handshake response\n";
        return;
    }
    std::cout << "[Client] XOR-handshake succeeded (OK)\n";

    // 3) Запускаем keepAlive
    std::thread ka(keepAliveThread, ctrlSock, xorKey, debug);

    // 4) Читаем команды: 'C' ...
    while(true){
        char cmd;
        int rc = recv(ctrlSock, &cmd, 1, 0);
        if(rc <= 0){
            // разрыв
            break;
        }
        // XOR
        cmd ^= xorKey[0];
        if(cmd == 'C'){
            // Читаем 8 байт:
            // ephemeralPort (2 байта), targetIP (4 байта), targetPort (2 байта)
            char buf[8];
            if(!recvAll(ctrlSock, buf, 8)) break;
            for(int i=0; i<8; i++){
                buf[i] ^= xorKey[(1 + i) % xorKey.size()];
            }
            // Разбираем
            uint16_t ep_n;
            std::memcpy(&ep_n, buf, 2);
            uint16_t ephemeralPort = ntohs(ep_n);

            uint32_t tip_n;
            std::memcpy(&tip_n, buf+2, 4);

            uint16_t tpt_n;
            std::memcpy(&tpt_n, buf+6, 2);
            uint16_t targetPort = ntohs(tpt_n);

            // Запустим отдельный поток, который сделает connectLocal() и connectServerEphemeral()
            std::thread th(handleCommandC, ephemeralPort, tip_n, targetPort, xorKey, debug);
            th.detach();
        } else {
            // неизвестно
        }
    }

    ka.join();
}

4. Keep-Alive-поток

C++:
void keepAliveThread(SocketType ctrlSock,
                     const std::string &xorKey,
                     bool debug)
{
    while(true){
#ifdef _WIN32
        Sleep(15000);
#else
        sleep(15);
#endif
        char c = 'K';
        c ^= xorKey[0];
        int rc = send(ctrlSock, &c, 1, 0);
        if(rc <= 0){
            if(debug) std::cerr << "[Client] keepAlive send fail\n";
            break;
        }
    }
}

5. Обработка команды C: открыть локальное соединение и "сшить" его с ephemeral

C++:
SocketType connectLocal(uint32_t ip_n, uint16_t port_h){
    // ip_n - в сетевом порядке
    // port_h - в host-порядке
    SocketType s = socket(AF_INET, SOCK_STREAM, 0);
    if(s == INVALID_SOCKET) return INVALID_SOCKET;
    sockaddr_in addr;
    std::memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = ip_n; // уже network order
    addr.sin_port = htons(port_h);

    if(connect(s, (sockaddr*)&addr, sizeof(addr)) < 0){
        CLOSESOCK(s);
        return INVALID_SOCKET;
    }
    return s;
}

SocketType connectServerEphemeral(const std::string &serverIP,
                                  uint16_t ephemeralPort)
{
    SocketType s = socket(AF_INET, SOCK_STREAM, 0);
    if(s == INVALID_SOCKET) return INVALID_SOCKET;
    sockaddr_in addr;
    std::memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(ephemeralPort);
    inet_pton(AF_INET, serverIP.c_str(), &addr.sin_addr);

    if(connect(s, (sockaddr*)&addr, sizeof(addr)) < 0){
        CLOSESOCK(s);
        return INVALID_SOCKET;
    }
    return s;
}

void handleCommandC(uint16_t ephemeralPort,
                    uint32_t targetIP_n,
                    uint16_t targetPort_h,
                    const std::string &xorKey,
                    bool debug)
{
    // Шаг 1: подключиться к ephemeral-порту на сервере
    SocketType esock = connectServerEphemeral(g_serverIP, ephemeralPort);
    if(esock == INVALID_SOCKET){
        if(debug) std::cerr << "[Client] can't connect ephemeral\n";
        return;
    }
    // Шаг 2: локальное соединение
    SocketType localSock = connectLocal(targetIP_n, targetPort_h);
    if(localSock == INVALID_SOCKET){
        if(debug) std::cerr << "[Client] can't connect local\n";
        CLOSESOCK(esock);
        return;
    }
    // Шаг 3: пересылка
    std::thread tFwd([=](){
        char b[4096];
        while(true){
            int rx = recv(esock, b, 4096, 0);
            if(rx <= 0) break;
            int tx = send(localSock, b, rx, 0);
            if(tx <= 0) break;
        }
        CLOSESOCK(localSock);
    });
    {
        char b[4096];
        while(true){
            int rx = recv(localSock, b, 4096, 0);
            if(rx <= 0) break;
            int tx = send(esock, b, rx, 0);
            if(tx <= 0) break;
        }
    }
    CLOSESOCK(esock);
    tFwd.join();
}

Разбор
  • connectLocal(...) подключается внутри домашней сети (куда попросил сервер).
  • connectServerEphemeral(...) идёт на сервер, который слушает ephemeralPort.
  • "Сшиваем" оба сокета, гоняя байты в обоих направлениях.

6. Итоговый main()

C++:
int main(int argc, char* argv[]){
    // 1) Парсим (примерно):
    // -s <server_ip>, -c <control_port>, -x <xor_key>, -d
    // 2) initSockets()
    // 3) runClientLoop(serverIP, controlPort, xorKey, debug)
    // 4) cleanupSockets()
    return 0;
}

Итог

Мы прошлись шаг за шагом, как "с нуля" написать backconnect SOCKS5:
  1. Сервер:
    1. Слушает control-порт.
    2. Слушает SOCKS5-порт, обрабатывает CONNECT-запросы.
    3. Когда приходит CONNECT, создаём ephemeral сокет, шлём команду C клиенту.
    4. Клиент возвращается на этот ephemeral, и мы пересылаем данные.
  2. Клиент:
    1. Постоянно переподключается к серверу (control-порт).
    2. При подключении шлёт "HELLO", ждёт "OK".
    3. Слушает команды C: открывает локальное соединение и цепляется к ephemeral порту на сервере.
    4. Гонит байты туда-сюда.
  3. XOR:
    1. Зашифровывает простым XOR все служебные данные ("HELLO", "OK", "C" и т.д.).

Такую схему можно усложнить или облегчить: добавить более надёжную криптографию, управлять списком доступных ресурсов, реализовать UDP и т.д. Но базовая идея уже есть.

Исходный код проекта из статьи опубликован тут Для просмотра ссылки Войди или Зарегистрируйся

В ближайшем будущем (наверное) добавлю версию клиента на чистом WINAPI для сборки с /NODEFAULTLIB
---

Автор: Vladislav Tislenko aka keklick1337 (Для просмотра ссылки Войди или Зарегистрируйся)


Подробный разбор исходного кода (client_socks5.cpp / server_socks5.cpp)
Данный текст создан специально для новичков, максимально подробно, почти "построчно" объясняя, что происходит. Ниже идёт описание client_socks5.cpp и server_socks5.cpp.

Часть 1. client_socks5.cpp - клиентская часть, которая сидит за NAT

Общее назначение:
  • Программа подключается к серверу (по IP + control_port) и проходит XOR-хендшейк (отправляет "HELLO", получает "OK").
  • Поддерживает keepalive ("K" каждые 15 секунд), чтобы сервер знал, что клиент на связи.
  • Когда сервер даёт команду 'C', клиент:
    1. Подключается обратно к "вспомогательному" (ephemeral) порту на сервере.
    2. Подключается к целевому локальному ресурсу (у себя в LAN).
    3. Сшивает оба соединения (сервер ↔ клиент ↔ локальная цель), давая серверу "достучаться" до локалки за NAT.

1. Глобальные переменные и хедеры
  • Подключаются заголовки: для сокетов (Cross-platform), для string, vector, thread и т.д.
  • Объявляются: g_debug (режим отладки), g_xorKey (строка-ключ XOR), g_serverIP (адрес сервера), g_controlPort (порт для контролей).

2. initSockets / cleanupSockets
В Windows обязательно вызывать WSAStartup. В Unix/Linux/macOS это не нужно:
  • initSockets() - возвращает true, если всё ок.
  • cleanupSockets() - завершает (Windows: WSACleanup()).

3. XOR-функция xorData()
C++:
void xorData(char* data, int len, const std::string &key){
    if(key.empty()) return;
    for(int i=0; i<len; i++){
        data[i]^= key[i % key.size()];
    }
}
  • Берёт буфер data, длину len, "пробегается" по каждому байту и "XOR-ит" его с соответствующим байтом ключа (key).
  • Если ключ пустой - ничего не делается.

4. sendAll / recvAll
C++:
bool sendAll(SocketType s, const char* data, int len) {
   // ...
}
bool recvAll(SocketType s, char* buf, int len) {
   // ...
}
  • Утилиты, которые в цикле стараются "дослать" или "дочитать" ровно нужное количество байт, поскольку системные вызовы send и recv могут вернуть меньше.

5. sendEnc
C++:
bool sendEnc(SocketType s, const char* data, int len){
    std::vector<char> tmp(data,data+len);
    xorData(tmp.data(), len, g_xorKey);
    return sendAll(s, tmp.data(), len);
}
  • Сначала копирует исходные данные в tmp.
  • Прогоняет xorData, "зашифровывает".
  • Отправляет "зашифрованные" данные по сокету (через sendAll).
Таким образом, сервер (или клиент) на другом конце должен "раскодировать" их, тоже применив xorData.

6. connectLocal / connectServerEphemeral
C++:
SocketType connectLocal(uint32_t ip_n, uint16_t port_n)
SocketType connectServerEphemeral(const std::string &ip, uint16_t port)
  • Первая функция connectLocal - подключается к локальному IPort внутри домашней сети. Здесь ip_n - уже в сетевом порядке байт, порт - в host-порядке (поэтому вызывается htons).
  • Вторая connectServerEphemeral - подключается к серверу на "временный" (ephemeral) порт, который сервер назначил.
  • Если connect не удаётся, возвращаем INVALID_SOCKET.

7. handleCommandC
C++:
void handleCommandC(uint16_t ephemeralPort, uint32_t targetIP_n, uint16_t targetPort_h)
  1. Получаем ephemeralPort (порт сервера, к которому надо подключиться).
  2. Получаем targetIP_n/targetPort_h - адрес внутри домашней сети (в network order/host order), куда клиент должен подключиться.
  3. Делаем connectServerEphemeral → esock.
  4. Делаем connectLocal → localSock.
  5. Если оба успешны - "сшиваем": в отдельном потоке пересылаем esock→localSock, а в текущем - localSock→esock.
  6. Когда одна сторона отвалилась, закрываем сокеты.

8. keepAliveThread
C++:
void keepAliveThread(SocketType ctrlSock){
    ...
    // каждые 15 секунд отправляет 'K'
}
  • Каждые 15 секунд отправляет символ 'K', за-XOR-енный первым байтом ключа.
  • Если отправка не удалась - считаем, что соединение закрылось.

9. controlChannelLoop
C++:
void controlChannelLoop(SocketType ctrl) {
   // 1) Отправляем "HELLO"
   // 2) Ждём ответ ("OK"/"OCCUP"), декодируем XOR
   // 3) Запускаем keepAliveThread
   // 4) В цикле читаем команды ('C'), парсим 8 байт параметров, запускаем handleCommandC
}
  • Это "сердце" клиента. При старте мы шлём "HELLO" (XOR), ожидаем "OK" или "OCCUP" от сервера.
  • Если "OCCUP" - значит, сервер "занят" (другой NAT-клиент уже подключён).
  • Если "OK" - значит всё ок, запускаем keepAliveThread.
  • Затем в бесконечном цикле ждём команду 'C'. Если пришла - читаем доп. 8 байт (порт, IP, порт), XOR-раскодируем, запускаем handleCommandC в новом потоке.
  • Если сервер разрывает соединение (recv <= 0) - завершаемся.

10. runClientLoop
C++:
void runClientLoop(){
   while(true){
       // 1) socket()
       // 2) connect(...) к g_serverIP:g_controlPort
       // 3) Если ок -> controlChannelLoop(s)
       // 4) Закрываем, спим 5с, повторяем
   }
}
  • Бесконечный цикл переподключения. Если связь рвётся - через 5 секунд пробуем снова.

11. main()
C++:
int main(int argc, char* argv[]){
   // 1) Парсим аргументы (-s, -c, -x, -d)
   // 2) initSockets()
   // 3) runClientLoop()
   // 4) cleanupSockets()
}
  • Считываем g_serverIP, g_controlPort, g_xorKey.
  • Запускаем runClientLoop, пока программа не завершится.

Таким образом, client_socks5.cpp сидит у пользователя за NAT, подключается к серверу, держит канал, обрабатывает команды "C" (т.е. "соединись локально и вернись на серверный ephemeral-порт"), что даёт backconnect-логику.


Часть 2. server_socks5.cpp - серверная часть

Общее назначение:
  • Слушает control_port, ждёт единственного NAT-клиента. Как только тот присоединился и сказал "HELLO" (XOR) - считаем, что g_natClientConnected = true.
  • Слушает SOCKS5-порт. Когда приходит обычный SOCKS5-клиент (из интернета), он может выполнить команду CONNECT.
  • При CONNECT, сервер создаёт ephemeral сокет, потом шлёт NAT-клиенту команду 'C'. Тот вернётся на ephemeral-порт, и мы "сшиваем" оба канала. Таким образом, пользователь "за NAT" предоставляет "туннель".

1. Глобальные переменные, мьютекс
C++:
std::mutex g_mutex;
SocketType g_natClientSock = INVALID_SOCKET;
bool       g_natClientConnected = false;
std::string g_xorKey;
...
  • Мьютекс (g_mutex) нужен, чтобы защищать доступ к g_natClientSock.
  • Если g_natClientConnected == true, значит клиент "обитает" на g_natClientSock.

2. initSockets / cleanupSockets, sendAll / recvAll, sendEnc
Все те же, что и в клиенте, но уже для сервера:
  • initSockets()/cleanupSockets() - инициализация/зачистка.
  • sendAll/recvAll - "досылки".
  • sendEnc - отправка с XOR.

3. createListeningSocket
C++:
SocketType createListeningSocket(uint16_t port){
   // socket -> bind -> listen
}
  • Создаём TCP-сокет, биндим на port, делаем listen.
  • Возвращаем дескриптор или INVALID_SOCKET при ошибке.

4. SOCKS5-handshake: socks5Handshake_SelectMethod, socks5Handshake_UserPass
  • socks5Handshake_SelectMethod:
    • Читает байты: 0x05 (версия), N (число методов), затем N методов.
    • Если g_socksUser/g_socksPass не пусты - значит, нужно USER/PASS (0x02). Иначе 0x00 (NOAUTH).
    • Возвращаем useUserPass = true/false.
  • socks5Handshake_UserPass:
    - Читает версию (0x01), длину логина, логин, длину пароля, пароль.
    - Сравнивает с g_socksUser/g_socksPass.
    - Возвращает true, если всё совпало.

5. socks5ParseConnect / socks5SendConnectReply
  • socks5ParseConnect читает: 0x05 (версия), 0x01 (команда CONNECT), 0x00, ATYP.
    • Если ATYP=0x01 (IPv4), то считываем 4 байта адреса и 2 байта порта.
    • Если ATYP=0x03 (домен), то 1 байт длины домена, далее сам домен, далее порт. Резолвим через gethostbyname.
    • Заполняем ip (host-порядок) и port.
  • socks5SendConnectReply отправляет 10 байт: 0x05, rep, 0x00, 0x01, 4 байта IP, 2 байта PORT.
    - rep=0x00, если успех; rep=0x01 - любая ошибка.

6. handleSocksClient
Самый главный метод для "большого" цикла SOCKS:
C++:
void handleSocksClient(SocketType sock){
   // 1) SOCKS5-handshake, optional user/pass
   // 2) parse CONNECT (получаем tip, tport)
   // 3) Создаём ephemeral socket epSock (bind=0, listen=1, узнаём ephemeralPort)
   // 4) Лочим mutex, шлём команду 'C' nat-клиенту (cmd[0]='C', ...)
   // 5) Ждём accept(epSock)
   // 6) Отправляем socks5SendConnectReply(sock, 0x00)
   // 7) Пересылка в отдельном потоке sock->esock и в текущем esock->sock
}
  • Если g_natClientConnected=false, или отправка команды "C" не удалась - отвечаем SOCKS5-клиенту ошибкой (rep=0x05).
  • Если всё ок, клиент за NAT вернётся в ephemeralPort, мы сделаем accept -> получим esock.
  • Отправим SOCKS5-клиенту 0x00 (успех), и начнём гонять трафик туда-сюда.

7. controlAcceptLoop
C++:
void controlAcceptLoop(uint16_t cPort){
   // 1) createListeningSocket(cPort)
   // 2) В цикле accept()
   // 3) Если g_natClientConnected=true, отправляем "OCCUP"
   // 4) Иначе ждем 5 байт "HELLO" (с таймаутом 5с)
   // 5) Если "HELLO" ок, шлем "OK"
   // 6) Сохраняем g_natClientSock=cs, g_natClientConnected=true
   // 7) Ждем 'K' (keepalive) или EOF
}
  • Таким образом, сервер ждёт одного клиента NAT. Когда тот отваливается - слушаем следующего.
  • Если уже кто-то сидит, отвечаем "OCCUP".
  • При успешном "HELLO" → "OK" (XOR), выставляем g_natClientConnected=true.

8. main()
C++:
int main(int argc, char* argv[]){
   // 1) Парсим -c <control_port>, -S <socks_port>, -x <xor_key>, ...
   // 2) initSockets()
   // 3) std::thread tCtl(controlAcceptLoop, controlPort)
   // 4) Создаём socksListener = createListeningSocket(socksPort)
   // 5) В цикле accept(), для каждого клиента -> std::thread(handleSocksClient, c).detach()
   // 6) tCtl.join()
}
  • Основная точка входа. Запускаем controlAcceptLoop (в отдельном потоке) и SOCKS5 accept (в основном).
  • Параметры user/pass (опционально) включают аутентификацию для SOCKS5.

Итого:
  1. Сервер ждёт NAT-клиента на control_port. При успешном "HELLO" → "OK" мы считаем клиент "подключён".
  2. Сервер также слушает SOCKS5-порт, принимает команды CONNECT.
  3. При CONNECT - создаём временный ephemeral порт, отправляем клиенту "C + параметры".
  4. Клиент, получив "C", подключается назад к ephemeral-порту, одновременно подключаясь к локалке (targetIP/targetPort).
  5. Таким образом получаем туннель SOCKS-клиент (интернет) ↔ сервер ↔ NAT-клиент ↔ локальный ресурс.
  6. Все управляющие данные "XOR-ятся" ключом g_xorKey. Это не настоящая криптография, просто маскировка.

Таким образом, данный пошаговый разбор позволяет даже "новичку" понять логику backconnect SOCKS5 на основе двух программ (клиент/сервер). Всё, что осталось - это скомпилировать, запустить сервер (с ключом, паролем/логином), затем запустить клиента (с тем же ключом) - и проверить работу "проброса" соединений через NAT в свою домашнюю сеть.
 
Activity
So far there's no one here