This is a detailed step-by-step guide on how to write a SOCKS5 backconnect from scratch.
Let's look at the server and client separately: let's start with an empty file, and gradually add the necessary methods to it. I'll show the logic and meaning of each key function. Then we'll combine everything into a working solution.
Analysis
int main(int argc, char* argv[]) is the usual entry point. We plan to accept:
Analysis
The #ifdef _WIN32 block compiles under Windows. We use WinSock2 (winsock2.h) and related functions. Note that names of types and functions in Windows differ, so we define aliases:
typedef SOCKET SocketType;
#define CLOSESOCK closesocket
In Unix systems, it is simpler (sockets are just integers).
Analysis
On Windows, you must call WSAStartup(...) once. On Linux/macOS, nothing is needed.
Analysis
Analysis
data ^= key[i % key.size()] is the "XOR" operation, applying the same key repeatedly in a cycle.
Analysis
Analysis
We copy the original buffer into tmp, perform xorData(...), then send the encrypted result. For receiving, you can either create a similar recvEnc or manually call recvAll(...) then pass the result through xorData().
Analysis
We form the reply packet with version 0x05, reply code <rep> (0x00 for success), reserved 0x00, address type 0x01 (IPv4), then 4 bytes for IP and 2 bytes for port.
Step-by-step Analysis
Thus, the server is complete.
Analysis
Server:
The source code for the project from the article is published here: https://github.com/keklick1337/backconnect_socks5
Let's look at the server and client separately: let's start with an empty file, and gradually add the necessary methods to it. I'll show the logic and meaning of each key function. Then we'll combine everything into a working solution.
Part 1. Server (server_socks5.cpp)
1. Create a program skeleton
We start with the simplest skeleton main(), which takes arguments:
1. Create a program skeleton
We start with the simplest skeleton main(), which takes arguments:
Part 1. Server (server_socks5.cpp)
1. Creating the Program Skeleton
We start with the simplest main() skeleton that accepts command-line arguments:
C:
#include <iostream>#include <string>
int main(int argc, char* argv[]) {// 1) Read command-line arguments (ports, key, etc.)// 2) Initialize network functions (Windows: WSAStartup)// 3) Launch threads: one for the control port, one for SOCKS5// 4) Wait for their completion
kotlin
Копировать
return 0;
}
Analysis
int main(int argc, char* argv[]) is the usual entry point. We plan to accept:
- -c <control_port> – the port for "control" connections from a client (located behind NAT).
- -S <socks_port> – the port on which we will accept SOCKS5 requests.
- -x <xor_key> – key for XOR (for simple encryption).
- -u <user> -p <pass> – login/password (optional).
- -d – debug mode (for detailed logging).
2. Including Network Headers and Defining Platform-Dependent Items
To ensure compatibility on Windows, Linux, and macOS, we carefully include different headers. Create the following block:
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
Analysis
The #ifdef _WIN32 block compiles under Windows. We use WinSock2 (winsock2.h) and related functions. Note that names of types and functions in Windows differ, so we define aliases:
typedef SOCKET SocketType;
#define CLOSESOCK closesocket
In Unix systems, it is simpler (sockets are just integers).
3. Network Initialization Functions
Prepare functions that will be handy for starting and cleaning up:
C:
bool initSockets() {#ifdef _WIN32WSADATA wd;int res = WSAStartup(MAKEWORD(2,2), &wd);if(res != 0){std::cerr << "[Server] WSAStartup error=" << res << "\n";return false;}#endifreturn true;}
void cleanupSockets() {#ifdef _WIN32WSACleanup();#endif}
Analysis
On Windows, you must call WSAStartup(...) once. On Linux/macOS, nothing is needed.
4. Creating a Listening Socket
We need to open a TCP socket, bind it to a port, and call listen(...). Let’s create a universal function:
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;}
cpp
Копировать
// Allow address reuse
int opt = 1;
#ifdef _WIN32setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));#elsesetsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));#endif
cpp
Копировать
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;
}
Analysis
- socket(AF_INET, SOCK_STREAM, 0) creates a TCP (stream) socket.
- bind(...) associates the socket with the desired port.
- listen(...) puts the socket into a state to accept incoming connections.
5. XOR Function
We need to encrypt/decrypt any buffer using XOR. With XOR, it’s simple:
C:
void xorData(char* data, int len, const std::string &key){if(key.empty()) return; // if the key is empty - do nothingfor(int i = 0; i < len; i++){data[i] ^= key[i % key.size()];}}
Analysis
data ^= key[i % key.size()] is the "XOR" operation, applying the same key repeatedly in a cycle.
6. Sending/Receiving with "Completeness" (sendAll, recvAll)
Often, send or recv return fewer bytes than requested. Let’s create utility functions:
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;}
Analysis
- sendAll(...) loops until all bytes are sent.
- recvAll(...) similarly waits until it has received all required bytes (or fails).
7. sendEnc / recvEnc
To perform XOR before sending/receiving, we can create, for example, a sendEnc function (one function for sending is enough; for receiving, you can either make a similar recvEnc function or apply XOR after recvAll). Here’s an example for sending:
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); // "encrypt" tmpreturn sendAll(s, tmp.data(), len);}
Analysis
We copy the original buffer into tmp, perform xorData(...), then send the encrypted result. For receiving, you can either create a similar recvEnc or manually call recvAll(...) then pass the result through xorData().
8. Implementing SOCKS5 (Server Mode)
8.1 Selecting an Authentication Method
When a client connects to our SOCKS5 port, it first sends:- Version: 0x05
- Number of supported methods: N
- List of methods.
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 version
cpp
Копировать
int nMethods = hdr[1];
std::vector<unsigned char> methods(nMethods);
r = recv(s, (char*)methods.data(), nMethods, 0);
if(r < nMethods) return false;
// Check if authentication is required (if user/pass are provided)
bool needAuth = (!user.empty() || !pass.empty());
if(needAuth) {
// Look for METHOD_USERPASS (0x02)
bool found = false;
for(unsigned char m: methods){
if(m == 0x02){
found = true;
break;
}
}
if(!found) {
// Send REJECT (0xFF)
unsigned char resp[2] = {0x05, 0xFF};
sendAll(s, (char*)resp, 2);
return false;
}
// Indicate that we choose user/pass
unsigned char resp[2] = {0x05, 0x02};
sendAll(s, (char*)resp, 2);
useUserPass = true;
} else {
// No authentication required
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 Authentication
If useUserPass is selected, the client sends:- Subnegotiation version (0x01).
- 1 byte: length of username.
- The username.
- 1 byte: length of password.
- The password.
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; // subnegotiation version = 1
arduino
Копировать
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());
// Compare credentials
unsigned char status = 0x00;
if(su != user || sp != pass){
status = 0x01; // authentication failed
}
unsigned char resp[2] = {0x01, status};
sendAll(s, (char*)resp, 2);
return (status == 0x00);
}
8.3 Processing CONNECT
After the method is chosen, the client sends:- 1 byte: 0x05 (version)
- 1 byte: 0x01 (CONNECT command)
- 1 byte: 0x00 (reserved)
- 1 byte: address type: 0x01 (IPv4) or 0x03 (domain)
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 (version), hdr[1] = 0x01 (CONNECT), hdr[2]=0x00, hdr[3]=ATYPif(hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00) return false;
cpp
Копировать
unsigned char atyp = hdr[3];
if(atyp == 0x01){
// IPv4
unsigned char a4[4];
if(!recvAll(s, (char*)a4, 4)) return false;
// Assemble into a 32-bit number (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){
// Domain name
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];
// Resolve the domain
struct hostent* he = gethostbyname(dom.data());
if(!he) return false;
struct in_addr* const* alist = (struct in_addr* const*)he->h_addr_list;
if(!alist[0]) return false;
ip = ntohl(alist[0]->s_addr);
} else {
return false;
}
return true;
}
8.4 Sending CONNECT Reply
Once the request is parsed, we need to send a SOCKS5 "reply":- 0x05 (version)
- <rep> (reply code)
- 0x00 (reserved)
- 0x01 (address type = IPv4)
- <4 bytes of IP>
- <2 bytes of port>
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 = success, otherwise errorbuf[2] = 0x00;buf[3] = 0x01; // IPv4uint32_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);}
Analysis
We form the reply packet with version 0x05, reply code <rep> (0x00 for success), reserved 0x00, address type 0x01 (IPv4), then 4 bytes for IP and 2 bytes for port.
9. Server "Backconnect" Logic
On the server, we have global variables:
C:
#include <mutex>std::mutex g_mutex;SocketType g_natClientSock = INVALID_SOCKET; // socket with the NAT clientbool g_natClientConnected = false;std::string g_xorKey; // XOR key
9.1 Handling a SOCKS Connection
In a separate thread (one per connection to the SOCKS port), do the following:
C:
void handleSocksClient(SocketType sock,const std::string &user,const std::string &pass,bool debug){bool useUserPass = false;// 1) Select method (NoAuth or UserPass)if(!socks5Handshake_SelectMethod(sock, useUserPass, user, pass)){CLOSESOCK(sock);return;}
cpp
Копировать
// 2) If user/pass is selected, perform subnegotiation
if(useUserPass) {
if(!socks5Handshake_UserPass(sock, user, pass)){
CLOSESOCK(sock);
return;
}
}
// 3) Extract IP and port for CONNECT
uint32_t tip = 0;
uint16_t tport = 0;
if(!socks5ParseConnect(sock, tip, tport)){
socks5SendConnectReply(sock, 0x01); // error
CLOSESOCK(sock);
return;
}
// 4) Create an ephemeral socket (it will accept connection from the NAT client)
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; // let the system choose the port
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) Send command 'C' to our NAT client
bool okSend = false;
{
std::lock_guard<std::mutex> lock(g_mutex);
if(g_natClientSock != INVALID_SOCKET && g_natClientConnected){
// Form the packet: 1 byte 'C' + 2 bytes ephemeralPort + 4 bytes IP + 2 bytes 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) Wait for a connection on epSock (the NAT client will connect here)
sockaddr_in from;
socklen_t flen = sizeof(from);
SocketType esock = accept(epSock, (sockaddr*)&from, &flen);
CLOSESOCK(epSock); // no longer needed
if(esock == INVALID_SOCKET){
socks5SendConnectReply(sock, 0x05);
CLOSESOCK(sock);
return;
}
// 7) Inform the SOCKS5 client that everything is OK
socks5SendConnectReply(sock, 0x00, tip, tport);
// 8) Forward data in both directions
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);
}
Step-by-step Analysis
- We read the SOCKS5 request.
- Create a temporary (ephemeral) port (epSock).
- Send the 'C' command to the client through the NAT channel.
- Wait for the NAT client to "connect" to this epSock.
- After successful connection, reply to the SOCKS client with 0x00 (OK).
- Tunnel the traffic back and forth.
10. Handling the Control Port
In a separate thread, listen on the control port (specified with -c <control_port>) and accept a single NAT client:
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";
pgsql
Копировать
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;
}
// Check if a NAT client is already connected
{
std::lock_guard<std::mutex> lock(g_mutex);
if(g_natClientConnected){
// A NAT client is already connected
const char msg[] = "OCCUP";
// encrypt
sendEnc(cs, msg, 5, g_xorKey);
CLOSESOCK(cs);
continue;
}
}
// Read 5 bytes "HELLO" (encrypted with XOR)
char buf[5];
bool okHello = false;
// Set a timeout, for example 5 seconds
#ifdef _WIN32DWORD tmo = 5000;setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tmo, sizeof(tmo));#elsestruct timeval tv;tv.tv_sec = 5;tv.tv_usec = 0;setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));#endifif(recvAll(cs, buf, 5)) {// Apply XORxorData(buf, 5, g_xorKey);if(std::string(buf, 5) == "HELLO"){okHello = true;}}
cpp
Копировать
if(!okHello){
CLOSESOCK(cs);
continue;
}
// Send "OK"
const char msgOk[] = "OK";
sendEnc(cs, msgOk, 2, g_xorKey);
// Mark client as connected
{
std::lock_guard<std::mutex> lock(g_mutex);
g_natClientSock = cs;
g_natClientConnected = true;
}
std::cout << "[Server] NAT client connected!\n";
// Read keep-alive ('K') or EOF
while(true){
char c;
int r = recv(cs, &c, 1, 0);
if(r <= 0){
// disconnected
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 {
// unknown, ignore
}
}
}
CLOSESOCK(listener);
}
11. Starting the SOCKS5 and Control Threads
In main(), implement the following logic:
C:
#include <thread>#include <cstdlib>
int main(int argc, char* argv[]){// 1) Parse arguments (e.g., -c, -S, -x, -u, -p, -d, etc.)uint16_t controlPort = 0;uint16_t socksPort = 0;std::string user, pass;bool debug = false;// ... (parse the options)
cpp
Копировать
if(!initSockets()){
return 1;
}
// Start the control port listener thread
std::thread tCtl(controlAcceptLoop, controlPort, debug);
// Start the 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){
// error or termination
break;
}
// Launch a thread for handleSocksClient
std::thread th(handleSocksClient, c, user, pass, debug);
th.detach();
}
CLOSESOCK(socksListener);
tCtl.join();
cleanupSockets();
return 0;
}
Thus, the server is complete.
Part 2. Client (client_socks5.cpp)
Now for the client that sits behind NAT. It connects to the server and maintains the connection. When the server requests a "tunnel" creation, we execute command C.1. The main() Structure
C:
int main(int argc, char* argv[]){// 1) Parse arguments (for example):// -s <server_ip>, -c <control_port>, -x <xor_key>, -d (debug).// 2) initSockets()// 3) Start a loop to connect -> if successful, call controlChannelLoop()// 4) If disconnected, retry
kotlin
Копировать
return 0;
}
2. Reconnection Loop
We need an infinite loop:
C:
void runClientLoop(const std::string &serverIP,uint16_t controlPort,const std::string &xorKey,bool debug){while(true){// Create socketSocketType s = socket(AF_INET, SOCK_STREAM, 0);if(s == INVALID_SOCKET){if(debug) std::cerr << "socket() error\n";#ifdef _WIN32Sleep(5000);#elsesleep(5);#endifcontinue;}// Connect to serverIP:controlPortsockaddr_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);
scss
Копировать
if(connect(s, (sockaddr*)&addr, sizeof(addr)) < 0){
if(debug) std::cerr << "Can't connect to server\n";
CLOSESOCK(s);
#ifdef _WIN32Sleep(5000);#elsesleep(5);#endifcontinue;}std::cout << "[Client] Connected to " << serverIP << ":" << controlPort << "\n";
scss
Копировать
// Switch to the function that handles communication over the control channel
controlChannelLoop(s, xorKey, debug);
// If we exit, the connection has dropped
CLOSESOCK(s);
std::cerr << "[Client] Control connection closed. Retry in 5s...\n";
#ifdef _WIN32Sleep(5000);#elsesleep(5);#endif}}
3. controlChannelLoop(): Send "HELLO" and Wait for "OK"
C:
void controlChannelLoop(SocketType ctrlSock,const std::string &xorKey,bool debug){// 1) Send "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) Wait for a response (5 bytes, could be "OK" or "OCCUP")char resp[5];int r = recv(ctrlSock, resp, 5, 0);if(r <= 0){if(debug) std::cerr << "[Client] no response\n";return;}// Apply XORfor(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";
cpp
Копировать
// 3) Launch keepAlive thread
std::thread ka(keepAliveThread, ctrlSock, xorKey, debug);
// 4) Read commands: 'C' ...
while(true){
char cmd;
int rc = recv(ctrlSock, &cmd, 1, 0);
if(rc <= 0){
// disconnected
break;
}
// Apply XOR
cmd ^= xorKey[0];
if(cmd == 'C'){
// Read 8 bytes:
// ephemeralPort (2 bytes), targetIP (4 bytes), targetPort (2 bytes)
char buf[8];
if(!recvAll(ctrlSock, buf, 8)) break;
for(int i=0; i<8; i++){
buf[i] ^= xorKey[(1 + i) % xorKey.size()];
}
// Parse
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);
// Launch a separate thread to perform connectLocal() and connectServerEphemeral()
std::thread th(handleCommandC, ephemeralPort, tip_n, targetPort, xorKey, debug);
th.detach();
} else {
// Unknown command; ignore
}
}
ka.join();
}
4. Keep-Alive Thread
C:
void keepAliveThread(SocketType ctrlSock,const std::string &xorKey,bool debug){while(true){#ifdef _WIN32Sleep(15000);#elsesleep(15);#endifchar 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. Handling Command 'C': Open Local Connection and "Bridge" it with the Ephemeral Connection
C:
SocketType connectLocal(uint32_t ip_n, uint16_t port_h){// ip_n is in network order// port_h is in host orderSocketType 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; // already in network orderaddr.sin_port = htons(port_h);
scss
Копировать
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);
scss
Копировать
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){// Step 1: Connect to the server's ephemeral portSocketType esock = connectServerEphemeral(g_serverIP, ephemeralPort);if(esock == INVALID_SOCKET){if(debug) std::cerr << "[Client] can't connect ephemeral\n";return;}// Step 2: Establish a local connectionSocketType localSock = connectLocal(targetIP_n, targetPort_h);if(localSock == INVALID_SOCKET){if(debug) std::cerr << "[Client] can't connect local\n";CLOSESOCK(esock);return;}// Step 3: Forward data between the connectionsstd::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();}
Analysis
- connectLocal(...) connects within the local network (to the target requested by the server).
- connectServerEphemeral(...) connects to the server's ephemeral port.
- We "bridge" the two sockets, relaying bytes in both directions.
6. Final main()
C:
int main(int argc, char* argv[]){// 1) Parse arguments (for example):// -s <server_ip>, -c <control_port>, -x <xor_key>, -d// 2) initSockets()// 3) runClientLoop(serverIP, controlPort, xorKey, debug)// 4) cleanupSockets()return 0;}
Summary
We have walked through step by step how to write a backconnect SOCKS5 program from scratch:Server:
- Listens on a control port.
- Listens on a SOCKS5 port and handles CONNECT requests.
- When a CONNECT request comes, creates an ephemeral socket and sends command 'C' to the client.
- The client connects to this ephemeral port, and data is relayed between the client and the SOCKS5 connection.
- Continuously reconnects to the server (control port).
- Upon connection, sends "HELLO" and waits for "OK".
- Listens for command 'C': opens a local connection and attaches to the ephemeral port on the server.
- Relays bytes back and forth.
- Uses simple XOR to encrypt all service data ("HELLO", "OK", "C", etc.).
The source code for the project from the article is published here: https://github.com/keklick1337/backconnect_socks5