commit a1200d20d11a9e4854bc90ada027337aaf286800 Author: ImSavsis Date: Sun Jul 19 19:14:43 2026 +0500 init: socks5 proxy with tls clienthello fragmentation diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4b2583 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d42690b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +.claude/ +*.obj +*.exe +*.pdb +*.ilk diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8fa0dcf --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/README.md b/README.md new file mode 100644 index 0000000..e218f7a --- /dev/null +++ b/README.md @@ -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 не тестировал, теоретически должно работать diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..23d1202 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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(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(data + sent), static_cast(len - sent), 0); + if (n <= 0) return false; + sent += static_cast(n); + } + return true; +} + +static void relay(SOCKET from, SOCKET to) { + std::vector buf(16384); + while (true) { + int n = recv(from, buf.data(), static_cast(buf.size()), 0); + if (n <= 0) break; + if (!send_all(to, reinterpret_cast(buf.data()), static_cast(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 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(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 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(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(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(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(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 first(4096); + int n = recv(client, reinterpret_cast(first.data()), static_cast(first.size()), 0); + if (n > 0) { + size_t split = find_sni_split_point(first.data(), static_cast(n)); + if (split > 0 && split < static_cast(n)) { + send_all(upstream, first.data(), split); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + send_all(upstream, first.data() + split, static_cast(n) - split); + } else if (n > 0) { + send_all(upstream, first.data(), static_cast(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(port)); + + if (bind(listener, reinterpret_cast(&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(&client_addr), &client_len); + if (client == INVALID_SOCKET) continue; + std::thread(handle_client, client).detach(); + } + + closesocket(listener); + WSACleanup(); + return 0; +} diff --git a/src/sni_split.cpp b/src/sni_split.cpp new file mode 100644 index 0000000..76a1e29 --- /dev/null +++ b/src/sni_split.cpp @@ -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; +} diff --git a/src/sni_split.h b/src/sni_split.h new file mode 100644 index 0000000..6e89776 --- /dev/null +++ b/src/sni_split.h @@ -0,0 +1,5 @@ +#pragma once +#include +#include + +size_t find_sni_split_point(const uint8_t* data, size_t len);