윈도우 프로그래밍 시 인터넷 통신사(ISP) 정보를 가져오는 코드

by lizard2019 posted Sep 01, 2025
?

Shortcut

PrevPrev Article

NextNext Article

ESCClose

Larger Font Smaller Font Up Down Go comment Print
윈도우 프로그래밍 시 인터넷 통신사(ISP)를 알아야하는 상황이 있습니다.
이럴때는 아래와 같은 코드를 사용하시면됩니다.
프로젝트는 c++17  이상을 사용해야합니다.

실행 결과

인터넷 통신사(ASN/조직명) 탐지 예제
공인 IPv4: 220.70.82.231
[경고] Team Cymru 조회 실패. 보조 경로(ipapi.co) 시도.

=== 조회 결과 ===
IP     : 220.70.82.231
ASN    : AS4766
운영자 : Korea Telecom
국가   : KR
출처   : ipapi.co

=> 통신사 추정: KT


소스 코드

// DetectISP.cpp
// 현재 인터넷 통신사(ASN/조직명) 추정: 공인 IP → Team Cymru DNS → (실패 시) ipapi.co
// DetectISP.cpp
#define UNICODE
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX

// ★ winsock2 계열을 windows.h 보다 먼저!
#include <winsock2.h>
#include <ws2tcpip.h>

#include <windows.h>
#include <winhttp.h>
#include <windns.h>
#include <cctype> // isdigit

#include <iostream>
#include <string>
#include <vector>
#include <optional>
#include <algorithm>

#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "dnsapi.lib")

// --- 문자열 유틸 ---
static inline std::string Trim(const std::string& s) {
    size_t b = s.find_first_not_of(" \t\r\n");
    size_t e = s.find_last_not_of(" \t\r\n");
    if (b == std::string::npos) return "";
    return s.substr(b, e - b + 1);
}
static inline std::vector<std::string> Split(const std::string& s, char sep) {
    std::vector<std::string> out;
    size_t pos = 0, p = 0;
    while ((p = s.find(sep, pos)) != std::string::npos) {
        out.push_back(s.substr(pos, p - pos));
        pos = p + 1;
    }
    out.push_back(s.substr(pos));
    return out;
}

// --- WinHTTP GET (본문을 그대로 std::string으로) ---
static std::optional<std::string> HttpGet(const wchar_t* host, const wchar_t* path, bool https = true, DWORD timeout_ms = 4000) {
    HINTERNET hSession = WinHttpOpen(L"DetectISP/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
        WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (!hSession) return std::nullopt;

    WinHttpSetTimeouts(hSession, timeout_ms, timeout_ms, timeout_ms, timeout_ms);

    HINTERNET hConnect = WinHttpConnect(hSession, host, https ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT, 0);
    if (!hConnect) { WinHttpCloseHandle(hSession); return std::nullopt; }

    DWORD flags = https ? (WINHTTP_FLAG_ESCAPE_DISABLE | WINHTTP_FLAG_SECURE) : (WINHTTP_FLAG_ESCAPE_DISABLE);
    HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path, nullptr,
        WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
    if (!hRequest) { WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return std::nullopt; }

    BOOL ok = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
        WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
    if (!ok) { WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return std::nullopt; }

    ok = WinHttpReceiveResponse(hRequest, nullptr);
    if (!ok) { WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return std::nullopt; }

    std::string body;
    DWORD avail = 0;
    do {
        if (!WinHttpQueryDataAvailable(hRequest, &avail)) break;
        if (!avail) break;
        std::string chunk;
        chunk.resize(avail);
        DWORD read = 0;
        if (!WinHttpReadData(hRequest, chunk.data(), avail, &read)) break;
        chunk.resize(read);
        body += chunk;
    } while (avail > 0);

    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    return body;
}

// --- 공인 IP 얻기 (IPv4 우선, 필요 시 IPv6 사용) ---
static std::optional<std::string> GetPublicIP(bool v6 = false) {
    auto body = HttpGet(v6 ? L"api64.ipify.org" : L"api.ipify.org", L"/?format=text", true, 4000);
    if (!body) return std::nullopt;
    std::string ip = Trim(*body);

    // 형식 검증
    if (!v6) {
        in_addr a4{};
        if (InetPtonA(AF_INET, ip.c_str(), &a4) == 1) return ip;
    }
    else {
        in6_addr a6{};
        if (InetPtonA(AF_INET6, ip.c_str(), &a6) == 1) return ip;
    }
    return std::nullopt;
}

static std::string W2U8(PCWSTR ws) {
    if (!ws) return {};
    int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, nullptr, 0, nullptr, nullptr);
    if (n <= 1) return {};
    std::string out; out.resize(n - 1);
    WideCharToMultiByte(CP_UTF8, 0, ws, -1, out.data(), n, nullptr, nullptr);
    return out;
}

static std::optional<std::string> DnsTxtQuery(const std::string& name) {
    PDNS_RECORD pRec = nullptr; // _DnsRecordW*
    DNS_STATUS st = DnsQuery_UTF8(
        name.c_str(),           // UTF-8 도메인
        DNS_TYPE_TEXT,
        DNS_QUERY_STANDARD,
        nullptr,
        &pRec,                  // PDNS_RECORD*
        nullptr
    );
    if (st != 0 || !pRec) return std::nullopt;

    std::string result;
    for (auto rr = pRec; rr; rr = rr->pNext) {
        if (rr->wType == DNS_TYPE_TEXT && rr->Data.TXT.dwStringCount > 0) {
            for (DWORD i = 0; i < rr->Data.TXT.dwStringCount; ++i) {
                if (rr->Data.TXT.pStringArray[i]) {
                    result += W2U8(rr->Data.TXT.pStringArray[i]); // PWSTR → UTF-8
                }
            }
            break; // 첫 TXT만 사용
        }
    }
    if (pRec) DnsRecordListFree(pRec, DnsFreeRecordList);
    if (result.empty()) return std::nullopt;
    return result;
}

// IPv4: "1.2.3.4" -> "4.3.2.1.origin.asn.cymru.com"
static std::string ReverseIPv4(const std::string& ip) {
    auto parts = Split(ip, '.');
    if (parts.size() != 4) return "";
    std::reverse(parts.begin(), parts.end());
    return parts[0] + "." + parts[1] + "." + parts[2] + "." + parts[3] + ".origin.asn.cymru.com";
}

// Team Cymru 응답 예(대표형): "AS12345 | 1.2.3.0/24 | KR | apnic | 2001-01-01 | ISP NAME"
// 변형에 대비하여 파이프(|) 분리 후 첫 토큰에서 숫자만 뽑음
static std::optional<std::string> ExtractASN(const std::string& txt) {
    auto fields = Split(txt, '|');
    if (fields.empty()) return std::nullopt;
    std::string f0 = Trim(fields[0]);
    // "AS12345" 또는 "12345" 형태
    // 숫자만 추출
    std::string asn;
    for (char c : f0) if (isdigit((unsigned char)c)) asn.push_back(c);
    if (asn.empty()) return std::nullopt;
    return asn;
}

// AS정보 TXT: "AS12345 | KR | apnic | 2001-01-01 | ISP NAME"
// → 마지막 필드를 ISP/조직명으로 사용
static std::optional<std::string> ExtractASName(const std::string& txt) {
    auto fields = Split(txt, '|');
    if (fields.size() < 2) return std::nullopt;
    std::string last = Trim(fields.back());
    return last.empty() ? std::optional<std::string>{} : std::optional<std::string>{ last };
}

struct IspInfo {
    std::string ip;
    std::string asn;     // e.g., "12345"
    std::string as_name; // e.g., "KT Corp" / "SK Broadband" 등
    std::string cc;      // 국가코드(있으면)
    std::string source;  // "Team Cymru DNS" or "ipapi.co"
};

static std::optional<IspInfo> LookupByTeamCymruIPv4(const std::string& ip) {
    IspInfo info; info.ip = ip; info.source = "Team Cymru DNS";

    std::string q1 = ReverseIPv4(ip);
    if (q1.empty()) return std::nullopt;

    auto txt1 = DnsTxtQuery(q1);
    if (!txt1) return std::nullopt;

    auto asn = ExtractASN(*txt1);
    if (!asn) return std::nullopt;
    info.asn = *asn;

    // AS세부 (이름/국가)
    std::string q2 = "AS" + info.asn + ".asn.cymru.com";
    auto txt2 = DnsTxtQuery(q2);
    if (txt2) {
        // 국가코드는 보통 2번째 필드, 이름은 마지막 필드
        auto fields = Split(*txt2, '|');
        if (fields.size() >= 2) info.cc = Trim(fields[1]);
        auto name = ExtractASName(*txt2);
        if (name) info.as_name = *name;
    }

    return info;
}

// 보조: ipapi.co로 ASN/ORG 가져오기 (무료/제한적; 실패해도 프로그램 계속)
static std::optional<IspInfo> FallbackByIpapi(const std::string& ip) {
    IspInfo info; info.ip = ip; info.source = "ipapi.co";
    std::wstring host = L"ipapi.co";
    std::wstring path = L"/";
    path += std::wstring(ip.begin(), ip.end());
    path += L"/json";

    auto body = HttpGet(host.c_str(), path.c_str(), true, 4000);
    if (!body) return std::nullopt;

    // 매우 단순한 파싱: "asn": "ASXXXX", "org": "ORG NAME"
    std::string s = *body;
    auto find_json_value = [&](const char* key) -> std::optional<std::string> {
        std::string k = std::string("\"") + key + "\"";
        size_t p = s.find(k);
        if (p == std::string::npos) return std::nullopt;
        p = s.find(':', p);
        if (p == std::string::npos) return std::nullopt;
        p = s.find('"', p);
        if (p == std::string::npos) return std::nullopt;
        size_t q = s.find('"', p + 1);
        if (q == std::string::npos) return std::nullopt;
        return s.substr(p + 1, q - (p + 1));
        };

    auto asn = find_json_value("asn");
    auto org = find_json_value("org");
    auto cc = find_json_value("country");

    if (!asn && !org) return std::nullopt;
    if (asn) {
        // "AS12345"에서 숫자만 추출
        std::string onlynum; for (char c : *asn) if (isdigit((unsigned char)c)) onlynum.push_back(c);
        info.asn = onlynum;
    }
    if (org) info.as_name = *org;
    if (cc) info.cc = *cc;
    return info;
}

int main() {
    std::cout << "인터넷 통신사(ASN/조직명) 탐지 예제\n";

    // 1) 우선 IPv4 공인 IP 시도
    auto ip4 = GetPublicIP(false);
    bool usedV6 = false;
    if (!ip4) {
        // IPv4 불가하면 IPv6 시도 (Team Cymru IPv6 처리까지 구현하려면 추가 코드 필요)
        auto ip6 = GetPublicIP(true);
        if (!ip6) {
            std::cerr << "[오류] 공인 IP를 가져오지 못했습니다.\n";
            return 1;
        }
        usedV6 = true;
        std::cout << "공인 IPv6: " << std::string(ip6->begin(), ip6->end()) << "\n";
        std::cerr << "[알림] 본 샘플은 IPv4 기반 Team Cymru 조회를 중심으로 합니다. IPv6 ASN 조회는 추가 구현이 필요합니다.\n";
        return 0;
    }

    std::cout << "공인 IPv4: " << std::string(ip4->begin(), ip4->end()) << "\n";

    // 2) Team Cymru로 ASN/이름 조회 (권장 루트)
    auto info = LookupByTeamCymruIPv4(*ip4);
    if (!info) {
        std::cerr << "[경고] Team Cymru 조회 실패. 보조 경로(ipapi.co) 시도.\n";
        info = FallbackByIpapi(*ip4);
    }

    if (!info) {
        std::cerr << "[오류] 통신사 정보를 확인하지 못했습니다.\n";
        return 2;
    }

    std::cout << "\n=== 조회 결과 ===\n";
    std::cout << "IP     : " << std::string(info->ip.begin(), info->ip.end()) << "\n";
    if (!info->asn.empty())     std::cout << "ASN    : AS" << std::string(info->asn.begin(), info->asn.end()) << "\n";
    if (!info->as_name.empty()) std::cout << "운영자 : " << std::string(info->as_name.begin(), info->as_name.end()) << "\n";
    if (!info->cc.empty())      std::cout << "국가   : " << std::string(info->cc.begin(), info->cc.end()) << "\n";
    std::cout << "출처   : " << std::string(info->source.begin(), info->source.end()) << "\n";

    // 간단한 통신사 추정(한국 예시) - 문자열 포함 여부로 라벨링
    std::string nameUp = info->as_name;
    std::transform(nameUp.begin(), nameUp.end(), nameUp.begin(), ::toupper);
    if (nameUp.find("KOREA TELECOM") != std::string::npos || nameUp.find("KT") != std::string::npos) {
        std::cout << "\n=> 통신사 추정: KT\n";
    }
    else if (nameUp.find("SK BROADBAND") != std::string::npos || nameUp.find("SK") != std::string::npos) {
        std::cout << "\n=> 통신사 추정: SK브로드밴드\n";
    }
    else if (nameUp.find("LG") != std::string::npos || nameUp.find("LG U") != std::string::npos || nameUp.find("LGD") != std::string::npos) {
        std::cout << "\n=> 통신사 추정: LG U+\n";
    }

    return 0;
}