commit 718f70fc0f0275a801d21fe852fded69e422265e Author: ImSavsis Date: Sun Jul 19 20:21:41 2026 +0500 init: udp dns to doh forwarder 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..10b0198 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) +project(dohstub CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(dohstub src/main.cpp) + +if(WIN32) + target_link_libraries(dohstub ws2_32 winhttp) +endif() diff --git a/README.md b/README.md new file mode 100644 index 0000000..12ff479 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# doh-stub + +локальный DNS-резолвер на C++, который сам ходит наружу по DNS-over-HTTPS. провайдер/DPI видит только HTTPS к cloudflare, а не голые DNS-запросы. + +```mermaid +sequenceDiagram + OS->>doh-stub: обычный DNS-запрос (UDP) + doh-stub->>Cloudflare: тот же запрос, но внутри HTTPS POST + Cloudflare->>doh-stub: ответ внутри HTTPS + doh-stub->>OS: обычный DNS-ответ (UDP) +``` + +## сборка + +``` +cmake -B build +cmake --build build --config Release +``` + +## юзать + +``` +build\Release\dohstub.exe --port 5300 +``` + +по умолчанию слушает `127.0.0.1:5300` и форвардит на `cloudflare-dns.com`. чтобы реально подменить системный DNS — нужен порт 53 и админ, плюс прописать `127.0.0.1` в настройках сети. + +## что не умеет + +не кэширует ответы, каждый запрос — новый HTTPS-коннект. для личного использования хватает, для нагрузки — нет. diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..17e0ec5 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "winhttp.lib") + +static std::wstring widen(const std::string& s) { + return std::wstring(s.begin(), s.end()); +} + +static std::string narrow(const std::wstring& s) { + std::string out(s.size(), '\0'); + for (size_t i = 0; i < s.size(); ++i) out[i] = static_cast(s[i]); + return out; +} + +static bool doh_forward(const std::wstring& doh_host, const std::wstring& doh_path, + const uint8_t* query, int query_len, + std::vector& answer) { + HINTERNET session = WinHttpOpen(L"doh-stub/1.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (!session) return false; + + HINTERNET connect = WinHttpConnect(session, doh_host.c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0); + if (!connect) { WinHttpCloseHandle(session); return false; } + + HINTERNET request = WinHttpOpenRequest(connect, L"POST", doh_path.c_str(), + nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); + if (!request) { WinHttpCloseHandle(connect); WinHttpCloseHandle(session); return false; } + + std::wstring headers = L"Content-Type: application/dns-message\r\nAccept: application/dns-message"; + WinHttpAddRequestHeaders(request, headers.c_str(), static_cast(-1), WINHTTP_ADDREQ_FLAG_ADD); + + bool ok = WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, + const_cast(query), query_len, query_len, 0) != FALSE; + + if (ok) ok = WinHttpReceiveResponse(request, nullptr) != FALSE; + + if (ok) { + DWORD avail = 0; + while (WinHttpQueryDataAvailable(request, &avail) && avail > 0) { + std::vector chunk(avail); + DWORD read = 0; + if (!WinHttpReadData(request, chunk.data(), avail, &read)) break; + answer.insert(answer.end(), chunk.begin(), chunk.begin() + read); + } + } + + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return ok && !answer.empty(); +} + +static void handle_query(SOCKET sock, sockaddr_in client_addr, int client_len, + std::vector query, + std::wstring doh_host, std::wstring doh_path) { + std::vector answer; + if (doh_forward(doh_host, doh_path, query.data(), static_cast(query.size()), answer)) { + sendto(sock, reinterpret_cast(answer.data()), static_cast(answer.size()), 0, + reinterpret_cast(&client_addr), client_len); + std::cout << "resolved, " << answer.size() << " bytes back" << std::endl; + } else { + std::cout << "doh request failed" << std::endl; + } +} + +int main(int argc, char** argv) { + int port = 5300; + std::wstring doh_host = L"cloudflare-dns.com"; + std::wstring doh_path = L"/dns-query"; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--port" && i + 1 < argc) { + port = std::stoi(argv[++i]); + } else if (arg == "--doh-host" && i + 1 < argc) { + doh_host = widen(argv[++i]); + } + } + + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { + std::cerr << "WSAStartup failed" << std::endl; + return 1; + } + + SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock == 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(sock, reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR) { + std::cerr << "bind() failed on port " << port << " (need admin for 53)" << std::endl; + WSACleanup(); + return 1; + } + + std::cout << "doh-stub listening on 127.0.0.1:" << port + << ", forwarding to " << narrow(doh_host) << std::endl; + + std::vector buf(512); + while (true) { + sockaddr_in client_addr{}; + int client_len = sizeof(client_addr); + int n = recvfrom(sock, reinterpret_cast(buf.data()), static_cast(buf.size()), 0, + reinterpret_cast(&client_addr), &client_len); + if (n <= 0) continue; + + std::vector query(buf.begin(), buf.begin() + n); + std::thread(handle_query, sock, client_addr, client_len, query, doh_host, doh_path).detach(); + } + + closesocket(sock); + WSACleanup(); + return 0; +}