init: udp dns to doh forwarder
This commit is contained in:
commit
718f70fc0f
5 changed files with 191 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(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()
|
||||||
30
README.md
Normal file
30
README.md
Normal file
|
|
@ -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-коннект. для личного использования хватает, для нагрузки — нет.
|
||||||
130
src/main.cpp
Normal file
130
src/main.cpp
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <winhttp.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#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<char>(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<uint8_t>& 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<DWORD>(-1), WINHTTP_ADDREQ_FLAG_ADD);
|
||||||
|
|
||||||
|
bool ok = WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||||
|
const_cast<uint8_t*>(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<uint8_t> 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<uint8_t> query,
|
||||||
|
std::wstring doh_host, std::wstring doh_path) {
|
||||||
|
std::vector<uint8_t> answer;
|
||||||
|
if (doh_forward(doh_host, doh_path, query.data(), static_cast<int>(query.size()), answer)) {
|
||||||
|
sendto(sock, reinterpret_cast<const char*>(answer.data()), static_cast<int>(answer.size()), 0,
|
||||||
|
reinterpret_cast<sockaddr*>(&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<uint16_t>(port));
|
||||||
|
|
||||||
|
if (bind(sock, reinterpret_cast<sockaddr*>(&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<uint8_t> buf(512);
|
||||||
|
while (true) {
|
||||||
|
sockaddr_in client_addr{};
|
||||||
|
int client_len = sizeof(client_addr);
|
||||||
|
int n = recvfrom(sock, reinterpret_cast<char*>(buf.data()), static_cast<int>(buf.size()), 0,
|
||||||
|
reinterpret_cast<sockaddr*>(&client_addr), &client_len);
|
||||||
|
if (n <= 0) continue;
|
||||||
|
|
||||||
|
std::vector<uint8_t> 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue