stihl не предоставил(а) никакой дополнительной информации.
Дорогие пользователи xss.is, пишу данную статью для вас, чтобы ввести вас немного в курс дела, как можно достаточно просто написать свой backconnect socks5 сервер и клиент.
Это подробный пошаговый гайд, как "с нуля" написать backconnect SOCKS5.
Рассмотрим сервер и клиент по отдельности: начнём с пустого файла, постепенно дополним его нужными методами. Покажу логику и смысл каждой ключевой функции. Затем объединим всё в работающее решение.
Часть 1. Сервер (server_socks5.cpp)
1. Создаём скелет программы
Начинаем с самого простого каркаса main(), который принимает аргументы:
Разбор
2. Подключаем заголовки для сетей и определяем платформозависимые вещи
Чтобы всё работало и на Windows, и на Linux/macOS, нужно аккуратно подключить разные заголовки.
Создадим блок:
Разбор
3. Функции инициализации сетей
Подготовим функции, которые будут удобными для старта/завершения:
Разбор
4. Создаём слушающий сокет
Нам нужно открыть TCP-сокет, привязать к порту, вызвать listen(...). Сделаем универсальную функцию:
Разбор
5. XOR-функция
Нужно шифровать/дешифровать любой буфер. С помощью XOR всё просто:
Разбор
6. Отправка/приём с учётом "досылки" (sendAll, recvAll)
Часто send или recv возвращают меньше байт, чем запрашивали. Сделаем утилиты:
Разбор
7. sendEnc / recvEnc
Чтобы перед отправкой/приёмом выполнить XOR, сделаем, например, sendEnc (достаточно одной функции для отправки). Для приёма мы можем сразу применять XOR после получения. Ниже пример для отправки:
Разбор
8. Реализация SOCKS5 (в режиме сервера)
8.1 Выбираем метод аутентификации
Когда к нашему SOCKS5-порту подключается клиент, он сначала отправляет:
1. Версию: 0x05
2. Количество поддерживаемых методов: N
3. Список методов.
Мы должны прочитать это, проверить, есть ли 0x00 (No Auth) или 0x02 (User/Pass). Затем ответить, какой метод выбрали.
8.2 Аутентификация по user/pass
Если выбрано useUserPass, клиент отправит:
8.3 Обработка CONNECT
После выбора метода клиент посылает:
8.4 Ответ CONNECT
Когда мы разобрались, нужно послать "ответ" от SOCKS5:
9. Логика "backconnect" на сервере
На сервере есть глобальные переменные:
9.1 Обработка SOCKS-подключения
В отдельном потоке (на каждое подключение к SOCKS-порту) делаем:
Разбор по шагам
10. Обработка control-порта
В отдельном потоке слушаем -c <control_port> и принимаем единственного NAT-клиента:
11. Запуск потока SOCKS5 и control
В main() прописываем логику:
Таким образом, сервер готов.
Часть 2. Клиент (client_socks5.cpp)
Теперь клиент, который сидит за NAT. Он сам коннектится к серверу и держит соединение. Когда сервер просит "создать туннель", мы выполняем команду C.
1. Структура main()
2. Цикл переподключения
Нам нужен вечный цикл:
3. controlChannelLoop(): отправляем "HELLO", ждём "OK"
4. Keep-Alive-поток
5. Обработка команды C: открыть локальное соединение и "сшить" его с ephemeral
Разбор
6. Итоговый main()
Итог
Мы прошлись шаг за шагом, как "с нуля" написать backconnect SOCKS5:
Такую схему можно усложнить или облегчить: добавить более надёжную криптографию, управлять списком доступных ресурсов, реализовать UDP и т.д. Но базовая идея уже есть.
Исходный код проекта из статьи опубликован тут Для просмотра ссылки Войдиили Зарегистрируйся
В ближайшем будущем (наверное) добавлю версию клиента на чистом WINAPI для сборки с /NODEFAULTLIB
---
Автор: Vladislav Tislenko aka keklick1337 (Для просмотра ссылки Войдиили Зарегистрируйся)
Подробный разбор исходного кода (client_socks5.cpp / server_socks5.cpp)
Данный текст создан специально для новичков, максимально подробно, почти "построчно" объясняя, что происходит. Ниже идёт описание client_socks5.cpp и server_socks5.cpp.
Часть 1. client_socks5.cpp - клиентская часть, которая сидит за NAT
Общее назначение:
1. Глобальные переменные и хедеры
2. initSockets / cleanupSockets
В Windows обязательно вызывать WSAStartup. В Unix/Linux/macOS это не нужно:
3. XOR-функция xorData()
4. sendAll / recvAll
5. sendEnc
6. connectLocal / connectServerEphemeral
7. handleCommandC
8. keepAliveThread
9. controlChannelLoop
10. runClientLoop
11. main()
Таким образом, client_socks5.cpp сидит у пользователя за NAT, подключается к серверу, держит канал, обрабатывает команды "C" (т.е. "соединись локально и вернись на серверный ephemeral-порт"), что даёт backconnect-логику.
Часть 2. server_socks5.cpp - серверная часть
Общее назначение:
1. Глобальные переменные, мьютекс
2. initSockets / cleanupSockets, sendAll / recvAll, sendEnc
Все те же, что и в клиенте, но уже для сервера:
3. createListeningSocket
4. SOCKS5-handshake: socks5Handshake_SelectMethod, socks5Handshake_UserPass
5. socks5ParseConnect / socks5SendConnectReply
6. handleSocksClient
Самый главный метод для "большого" цикла SOCKS:
7. controlAcceptLoop
8. main()
Итого:
Таким образом, данный пошаговый разбор позволяет даже "новичку" понять логику backconnect SOCKS5 на основе двух программ (клиент/сервер). Всё, что осталось - это скомпилировать, запустить сервер (с ключом, паролем/логином), затем запустить клиента (с тем же ключом) - и проверить работу "проброса" соединений через NAT в свою домашнюю сеть.
Это подробный пошаговый гайд, как "с нуля" написать 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(...).
- Шлём зашифрованный результат.
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 байт).
- Сам пароль.
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);
}
После выбора метода клиент посылает:
- 1 байт: 0x05 (версия)
- 1 байт: 0x01 (команда CONNECT)
- 1 байт: 0x00 (зарезервировано)
- 1 байт: тип адреса: 0x01 (IPv4) или 0x03 (домен)
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);
}
Разбор по шагам
- Считываем SOCKS5-запрос.
- Создаём временный порт (epSock).
- Шлём через NAT-клиенту команду C.
- Ждём, когда NAT-клиент "зайдёт" в этот epSock.
- После удачного соединения отвечаем SOCKS-клиенту 0x00 (OK).
- Гоним трафик туда-сюда.
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:
- Сервер:
- Слушает control-порт.
- Слушает SOCKS5-порт, обрабатывает CONNECT-запросы.
- Когда приходит CONNECT, создаём ephemeral сокет, шлём команду C клиенту.
- Клиент возвращается на этот ephemeral, и мы пересылаем данные.
- Клиент:
- Постоянно переподключается к серверу (control-порт).
- При подключении шлёт "HELLO", ждёт "OK".
- Слушает команды C: открывает локальное соединение и цепляется к ephemeral порту на сервере.
- Гонит байты туда-сюда.
- XOR:
- Зашифровывает простым 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', клиент:
- Подключается обратно к "вспомогательному" (ephemeral) порту на сервере.
- Подключается к целевому локальному ресурсу (у себя в LAN).
- Сшивает оба соединения (сервер ↔ клиент ↔ локальная цель), давая серверу "достучаться" до локалки за 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).
6. connectLocal / connectServerEphemeral
C++:
SocketType connectLocal(uint32_t ip_n, uint16_t port_n)
SocketType connectServerEphemeral(const std::string &ip, uint16_t port)
- Первая функция connectLocal - подключается к локальному IP
ort внутри домашней сети. Здесь 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)
- Получаем ephemeralPort (порт сервера, к которому надо подключиться).
- Получаем targetIP_n/targetPort_h - адрес внутри домашней сети (в network order/host order), куда клиент должен подключиться.
- Делаем connectServerEphemeral → esock.
- Делаем connectLocal → localSock.
- Если оба успешны - "сшиваем": в отдельном потоке пересылаем esock→localSock, а в текущем - localSock→esock.
- Когда одна сторона отвалилась, закрываем сокеты.
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.
Итого:
- Сервер ждёт NAT-клиента на control_port. При успешном "HELLO" → "OK" мы считаем клиент "подключён".
- Сервер также слушает SOCKS5-порт, принимает команды CONNECT.
- При CONNECT - создаём временный ephemeral порт, отправляем клиенту "C + параметры".
- Клиент, получив "C", подключается назад к ephemeral-порту, одновременно подключаясь к локалке (targetIP/targetPort).
- Таким образом получаем туннель SOCKS-клиент (интернет) ↔ сервер ↔ NAT-клиент ↔ локальный ресурс.
- Все управляющие данные "XOR-ятся" ключом g_xorKey. Это не настоящая криптография, просто маскировка.
Таким образом, данный пошаговый разбор позволяет даже "новичку" понять логику backconnect SOCKS5 на основе двух программ (клиент/сервер). Всё, что осталось - это скомпилировать, запустить сервер (с ключом, паролем/логином), затем запустить клиента (с тем же ключом) - и проверить работу "проброса" соединений через NAT в свою домашнюю сеть.