init: socks5 proxy with tls clienthello fragmentation
This commit is contained in:
commit
a1200d20d1
7 changed files with 349 additions and 0 deletions
14
.github/workflows/ci.yml
vendored
Normal file
14
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
- run: cmake -B build
|
||||||
|
- run: cmake --build build --config Release
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
build/
|
||||||
|
.claude/
|
||||||
|
*.obj
|
||||||
|
*.exe
|
||||||
|
*.pdb
|
||||||
|
*.ilk
|
||||||
11
CMakeLists.txt
Normal file
11
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(dpifrag CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_executable(dpifrag src/main.cpp src/sni_split.cpp)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(dpifrag ws2_32)
|
||||||
|
endif()
|
||||||
28
README.md
Normal file
28
README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# dpi-frag
|
||||||
|
|
||||||
|
маленький локальный socks5 прокси на C++, дробит TLS ClientHello на несколько TCP-пакетов так что DPI не видит SNI одним куском и не режет сайт по домену.
|
||||||
|
|
||||||
|
работает тупо: ты подключаешься через него, он находит имя сайта внутри ClientHello и режет пакет прямо посередине домена перед тем как отправить дальше. большинство DPI смотрит паттерн в одном пакете — а тут его физически нет.
|
||||||
|
|
||||||
|
## сборка
|
||||||
|
|
||||||
|
```
|
||||||
|
cmake -B build
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
нужен cmake и любой C++17 компилятор (проверено на MSVC).
|
||||||
|
|
||||||
|
## юзать
|
||||||
|
|
||||||
|
```
|
||||||
|
build\Release\dpifrag.exe --port 1080
|
||||||
|
```
|
||||||
|
|
||||||
|
и в браузере/системе ставишь socks5 прокси на `127.0.0.1:1080`.
|
||||||
|
|
||||||
|
## что не умеет
|
||||||
|
|
||||||
|
- не шифрует и не скрывает сам факт подключения, это не VPN
|
||||||
|
- не работает если DPI блокирует по IP, а не по SNI
|
||||||
|
- ipv6 таргеты через socks5 не тестировал, теоретически должно работать
|
||||||
219
src/main.cpp
Normal file
219
src/main.cpp
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
#include "sni_split.h"
|
||||||
|
|
||||||
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
|
|
||||||
|
struct Socks5Target {
|
||||||
|
std::string host;
|
||||||
|
uint16_t port = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void close_sock(SOCKET s) {
|
||||||
|
if (s != INVALID_SOCKET) closesocket(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool read_exact(SOCKET s, uint8_t* buf, int len) {
|
||||||
|
int got = 0;
|
||||||
|
while (got < len) {
|
||||||
|
int n = recv(s, reinterpret_cast<char*>(buf + got), len - got, 0);
|
||||||
|
if (n <= 0) return false;
|
||||||
|
got += n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_all(SOCKET s, const uint8_t* data, size_t len) {
|
||||||
|
size_t sent = 0;
|
||||||
|
while (sent < len) {
|
||||||
|
int n = send(s, reinterpret_cast<const char*>(data + sent), static_cast<int>(len - sent), 0);
|
||||||
|
if (n <= 0) return false;
|
||||||
|
sent += static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void relay(SOCKET from, SOCKET to) {
|
||||||
|
std::vector<char> buf(16384);
|
||||||
|
while (true) {
|
||||||
|
int n = recv(from, buf.data(), static_cast<int>(buf.size()), 0);
|
||||||
|
if (n <= 0) break;
|
||||||
|
if (!send_all(to, reinterpret_cast<uint8_t*>(buf.data()), static_cast<size_t>(n))) break;
|
||||||
|
}
|
||||||
|
shutdown(to, SD_SEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool socks5_handshake(SOCKET client, Socks5Target& target) {
|
||||||
|
uint8_t hdr[2];
|
||||||
|
if (!read_exact(client, hdr, 2)) return false;
|
||||||
|
if (hdr[0] != 0x05) return false;
|
||||||
|
|
||||||
|
std::vector<uint8_t> methods(hdr[1]);
|
||||||
|
if (hdr[1] > 0 && !read_exact(client, methods.data(), hdr[1])) return false;
|
||||||
|
|
||||||
|
uint8_t reply[2] = { 0x05, 0x00 };
|
||||||
|
if (send(client, reinterpret_cast<char*>(reply), 2, 0) != 2) return false;
|
||||||
|
|
||||||
|
uint8_t req[4];
|
||||||
|
if (!read_exact(client, req, 4)) return false;
|
||||||
|
if (req[0] != 0x05 || req[1] != 0x01) return false;
|
||||||
|
|
||||||
|
uint8_t atyp = req[3];
|
||||||
|
if (atyp == 0x01) {
|
||||||
|
uint8_t addr[4];
|
||||||
|
if (!read_exact(client, addr, 4)) return false;
|
||||||
|
char ipstr[INET_ADDRSTRLEN];
|
||||||
|
inet_ntop(AF_INET, addr, ipstr, sizeof(ipstr));
|
||||||
|
target.host = ipstr;
|
||||||
|
} else if (atyp == 0x03) {
|
||||||
|
uint8_t len;
|
||||||
|
if (!read_exact(client, &len, 1)) return false;
|
||||||
|
std::vector<uint8_t> domain(len);
|
||||||
|
if (len > 0 && !read_exact(client, domain.data(), len)) return false;
|
||||||
|
target.host.assign(domain.begin(), domain.end());
|
||||||
|
} else if (atyp == 0x04) {
|
||||||
|
uint8_t addr[16];
|
||||||
|
if (!read_exact(client, addr, 16)) return false;
|
||||||
|
char ipstr[INET6_ADDRSTRLEN];
|
||||||
|
inet_ntop(AF_INET6, addr, ipstr, sizeof(ipstr));
|
||||||
|
target.host = ipstr;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t portb[2];
|
||||||
|
if (!read_exact(client, portb, 2)) return false;
|
||||||
|
target.port = (static_cast<uint16_t>(portb[0]) << 8) | portb[1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SOCKET connect_target(const Socks5Target& target) {
|
||||||
|
addrinfo hints{};
|
||||||
|
hints.ai_family = AF_UNSPEC;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
|
||||||
|
addrinfo* res = nullptr;
|
||||||
|
std::string portstr = std::to_string(target.port);
|
||||||
|
if (getaddrinfo(target.host.c_str(), portstr.c_str(), &hints, &res) != 0) return INVALID_SOCKET;
|
||||||
|
|
||||||
|
SOCKET s = INVALID_SOCKET;
|
||||||
|
for (addrinfo* p = res; p != nullptr; p = p->ai_next) {
|
||||||
|
s = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
||||||
|
if (s == INVALID_SOCKET) continue;
|
||||||
|
if (connect(s, p->ai_addr, static_cast<int>(p->ai_addrlen)) == 0) break;
|
||||||
|
closesocket(s);
|
||||||
|
s = INVALID_SOCKET;
|
||||||
|
}
|
||||||
|
freeaddrinfo(res);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_socks5_ok(SOCKET client) {
|
||||||
|
uint8_t reply[10] = { 0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0 };
|
||||||
|
send(client, reinterpret_cast<char*>(reply), sizeof(reply), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_socks5_fail(SOCKET client) {
|
||||||
|
uint8_t reply[10] = { 0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0 };
|
||||||
|
send(client, reinterpret_cast<char*>(reply), sizeof(reply), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_client(SOCKET client) {
|
||||||
|
Socks5Target target;
|
||||||
|
if (!socks5_handshake(client, target)) {
|
||||||
|
close_sock(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET upstream = connect_target(target);
|
||||||
|
if (upstream == INVALID_SOCKET) {
|
||||||
|
send_socks5_fail(client);
|
||||||
|
close_sock(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
send_socks5_ok(client);
|
||||||
|
std::cout << "-> " << target.host << ":" << target.port << std::endl;
|
||||||
|
|
||||||
|
std::vector<uint8_t> first(4096);
|
||||||
|
int n = recv(client, reinterpret_cast<char*>(first.data()), static_cast<int>(first.size()), 0);
|
||||||
|
if (n > 0) {
|
||||||
|
size_t split = find_sni_split_point(first.data(), static_cast<size_t>(n));
|
||||||
|
if (split > 0 && split < static_cast<size_t>(n)) {
|
||||||
|
send_all(upstream, first.data(), split);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||||
|
send_all(upstream, first.data() + split, static_cast<size_t>(n) - split);
|
||||||
|
} else if (n > 0) {
|
||||||
|
send_all(upstream, first.data(), static_cast<size_t>(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread t1(relay, client, upstream);
|
||||||
|
std::thread t2(relay, upstream, client);
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
|
||||||
|
close_sock(client);
|
||||||
|
close_sock(upstream);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
int port = 1080;
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
std::string arg = argv[i];
|
||||||
|
if (arg == "--port" && i + 1 < argc) {
|
||||||
|
port = std::stoi(argv[++i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WSADATA wsa;
|
||||||
|
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
|
||||||
|
std::cerr << "WSAStartup failed" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||||
|
if (listener == INVALID_SOCKET) {
|
||||||
|
std::cerr << "socket() failed" << std::endl;
|
||||||
|
WSACleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sockaddr_in addr{};
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||||
|
addr.sin_port = htons(static_cast<uint16_t>(port));
|
||||||
|
|
||||||
|
if (bind(listener, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR) {
|
||||||
|
std::cerr << "bind() failed on port " << port << std::endl;
|
||||||
|
WSACleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(listener, SOMAXCONN) == SOCKET_ERROR) {
|
||||||
|
std::cerr << "listen() failed" << std::endl;
|
||||||
|
WSACleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "socks5 proxy on 127.0.0.1:" << port << std::endl;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
sockaddr_in client_addr{};
|
||||||
|
int client_len = sizeof(client_addr);
|
||||||
|
SOCKET client = accept(listener, reinterpret_cast<sockaddr*>(&client_addr), &client_len);
|
||||||
|
if (client == INVALID_SOCKET) continue;
|
||||||
|
std::thread(handle_client, client).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
closesocket(listener);
|
||||||
|
WSACleanup();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
66
src/sni_split.cpp
Normal file
66
src/sni_split.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
#include "sni_split.h"
|
||||||
|
|
||||||
|
static bool read_u16(const uint8_t* d, size_t len, size_t pos, uint16_t& out) {
|
||||||
|
if (pos + 2 > len) return false;
|
||||||
|
out = (uint16_t(d[pos]) << 8) | d[pos + 1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t find_sni_split_point(const uint8_t* data, size_t len) {
|
||||||
|
if (len < 6 || data[0] != 0x16) return 0;
|
||||||
|
|
||||||
|
size_t pos = 5;
|
||||||
|
if (pos >= len || data[pos] != 0x01) return 0;
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
if (pos + 2 + 32 > len) return 0;
|
||||||
|
pos += 2 + 32;
|
||||||
|
|
||||||
|
if (pos >= len) return 0;
|
||||||
|
uint8_t session_id_len = data[pos];
|
||||||
|
pos += 1 + session_id_len;
|
||||||
|
|
||||||
|
uint16_t cipher_len;
|
||||||
|
if (!read_u16(data, len, pos, cipher_len)) return 0;
|
||||||
|
pos += 2 + cipher_len;
|
||||||
|
|
||||||
|
if (pos >= len) return 0;
|
||||||
|
uint8_t comp_len = data[pos];
|
||||||
|
pos += 1 + comp_len;
|
||||||
|
|
||||||
|
uint16_t ext_total_len;
|
||||||
|
if (!read_u16(data, len, pos, ext_total_len)) return 0;
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
size_t ext_end = pos + ext_total_len;
|
||||||
|
if (ext_end > len) ext_end = len;
|
||||||
|
|
||||||
|
while (pos + 4 <= ext_end) {
|
||||||
|
uint16_t ext_type, ext_len;
|
||||||
|
if (!read_u16(data, len, pos, ext_type)) break;
|
||||||
|
if (!read_u16(data, len, pos + 2, ext_len)) break;
|
||||||
|
size_t ext_data_start = pos + 4;
|
||||||
|
|
||||||
|
if (ext_type == 0x0000) {
|
||||||
|
size_t p = ext_data_start;
|
||||||
|
uint16_t list_len;
|
||||||
|
if (!read_u16(data, len, p, list_len)) return 0;
|
||||||
|
p += 2;
|
||||||
|
if (p + 3 > len) return 0;
|
||||||
|
|
||||||
|
uint8_t name_type = data[p];
|
||||||
|
uint16_t name_len;
|
||||||
|
if (name_type != 0 || !read_u16(data, len, p + 1, name_len)) return 0;
|
||||||
|
|
||||||
|
size_t name_start = p + 3;
|
||||||
|
size_t name_end = name_start + name_len;
|
||||||
|
if (name_end > len || name_len == 0) return 0;
|
||||||
|
|
||||||
|
return name_start + (name_len / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = ext_data_start + ext_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
5
src/sni_split.h
Normal file
5
src/sni_split.h
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
size_t find_sni_split_point(const uint8_t* data, size_t len);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue