From 5f815647c136f7fcfb51334bcf4e468d655580e2 Mon Sep 17 00:00:00 2001 From: ImSavsis Date: Sun, 19 Jul 2026 20:25:30 +0500 Subject: [PATCH] init: icmp traceroute --- .github/workflows/ci.yml | 14 +++++ .gitignore | 6 ++ CMakeLists.txt | 11 ++++ README.md | 30 ++++++++++ src/main.cpp | 118 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 src/main.cpp 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..71442e2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) +project(ttlprobe CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(ttlprobe src/main.cpp) + +if(WIN32) + target_link_libraries(ttlprobe ws2_32) +endif() diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bde34b --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# ttl-probe + +свой traceroute на C++ через сырые ICMP-сокеты. шлёт echo-запросы с растущим TTL и смотрит кто отвечает "time exceeded" на каждом хопе. + +```mermaid +sequenceDiagram + ttl-probe->>Hop1: ICMP echo, TTL=1 + Hop1-->>ttl-probe: time exceeded + ttl-probe->>Hop2: ICMP echo, TTL=2 + Hop2-->>ttl-probe: time exceeded + ttl-probe->>Target: ICMP echo, TTL=N + Target-->>ttl-probe: echo reply (дошли) +``` + +изначально планировал сюда честный fake-packet TTL-трюк (как в zapret), но на винде это требует сырых TCP-пакетов, а винда с висты их резать не даёт без отдельного драйвера (WinDivert/Npcap) — это уже не "просто". traceroute зато честно показывает сколько хопов до сервера, полезно прежде чем городить любой TTL-трюк вручную. + +## сборка + +``` +cmake -B build +cmake --build build --config Release +``` + +## юзать + +нужны права администратора (сырые сокеты): + +``` +build\Release\ttlprobe.exe example.com 30 +``` diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c9d3430 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "ws2_32.lib") + +#pragma pack(push, 1) +struct IcmpHeader { + uint8_t type; + uint8_t code; + uint16_t checksum; + uint16_t id; + uint16_t seq; +}; +#pragma pack(pop) + +static uint16_t checksum16(const uint8_t* data, size_t len) { + uint32_t sum = 0; + for (size_t i = 0; i + 1 < len; i += 2) { + sum += (static_cast(data[i]) << 8) | data[i + 1]; + } + if (len % 2 == 1) sum += static_cast(data[len - 1]) << 8; + while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); + return static_cast(~sum); +} + +int main(int argc, char** argv) { + if (argc < 2) { + std::cerr << "usage: ttlprobe [max-hops]" << std::endl; + return 1; + } + std::string target = argv[1]; + int max_hops = argc > 2 ? std::stoi(argv[2]) : 30; + + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { + std::cerr << "WSAStartup failed" << std::endl; + return 1; + } + + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_RAW; + addrinfo* res = nullptr; + if (getaddrinfo(target.c_str(), nullptr, &hints, &res) != 0 || !res) { + std::cerr << "cant resolve " << target << std::endl; + return 1; + } + sockaddr_in dest = *reinterpret_cast(res->ai_addr); + freeaddrinfo(res); + + char dest_ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &dest.sin_addr, dest_ip, sizeof(dest_ip)); + std::cout << "probing " << target << " (" << dest_ip << "), " << max_hops << " hops max" << std::endl; + + SOCKET sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + if (sock == INVALID_SOCKET) { + std::cerr << "raw socket failed, run as administrator" << std::endl; + WSACleanup(); + return 1; + } + + DWORD timeout = 1500; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); + + for (int ttl = 1; ttl <= max_hops; ++ttl) { + setsockopt(sock, IPPROTO_IP, IP_TTL, reinterpret_cast(&ttl), sizeof(ttl)); + + uint8_t packet[64] = {}; + IcmpHeader* icmp = reinterpret_cast(packet); + icmp->type = 8; + icmp->code = 0; + icmp->id = static_cast(GetCurrentProcessId()); + icmp->seq = static_cast(ttl); + icmp->checksum = 0; + icmp->checksum = htons(checksum16(packet, sizeof(packet))); + + auto start = std::chrono::steady_clock::now(); + sendto(sock, reinterpret_cast(packet), sizeof(packet), 0, + reinterpret_cast(&dest), sizeof(dest)); + + uint8_t reply[512]; + sockaddr_in from{}; + int from_len = sizeof(from); + int n = recvfrom(sock, reinterpret_cast(reply), sizeof(reply), 0, + reinterpret_cast(&from), &from_len); + + auto end = std::chrono::steady_clock::now(); + double ms = std::chrono::duration(end - start).count(); + + std::cout << ttl << "\t"; + if (n <= 0) { + std::cout << "*" << std::endl; + continue; + } + + char from_ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &from.sin_addr, from_ip, sizeof(from_ip)); + std::cout << from_ip << "\t" << ms << " ms" << std::endl; + + int ip_header_len = (reply[0] & 0x0F) * 4; + if (n > ip_header_len) { + uint8_t icmp_type = reply[ip_header_len]; + if (icmp_type == 0 || from.sin_addr.S_un.S_addr == dest.sin_addr.S_un.S_addr) { + std::cout << "reached destination" << std::endl; + break; + } + } + } + + closesocket(sock); + WSACleanup(); + return 0; +}