윈도우 프로그래밍 시 인터넷 통신사(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;
}