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