Microsoft MVP성태의 닷넷 이야기
.NET Framework: 221. Cache 영향을 받지 않는 DNS 이름 풀이 [링크 복사], [링크+제목 복사],
조회: 20138
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 6개 있습니다.)
.NET Framework: 221. Cache 영향을 받지 않는 DNS 이름 풀이
; https://www.sysnet.pe.kr/2/0/1069

.NET Framework: 264. 다중 LAN 카드 환경에서 Dns.GetHostAddresses(local)가 반환해 주는 IP의 우선순위는 어떻게 될까요?
; https://www.sysnet.pe.kr/2/0/1169

Windows: 154. PowerShell - Zone 별로 DNS 레코드 유형 정보 조회
; https://www.sysnet.pe.kr/2/0/11795

개발 환경 구성: 434. 존재하지 않는 IP 주소에 대한 Dns.GetHostByAddress/gethostbyaddr/GetNameInfoW 실행이 느리다면?
; https://www.sysnet.pe.kr/2/0/11852

개발 환경 구성: 435. 존재하지 않는 IP 주소에 대한 Dns.GetHostByAddress/gethostbyaddr/GetNameInfoW 실행이 느리다면? - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/11853

개발 환경 구성: 646. HOSTS 파일 변경 시 Edge 브라우저에 반영하는 방법
; https://www.sysnet.pe.kr/2/0/13089




Cache 영향을 받지 않는 DNS 이름 풀이


보통, DNS 이름 풀이를 한 번 하게 되면 로컬 컴퓨터에 Cache가 됩니다. 그래서, 다음과 같이 System.Net.Dns 조회를 사용하는 경우 두 번째 호출부터는 캐시된 결과값을 반환받게 됩니다.

string domainName = "www.dns.com";
Stopwatch st = new Stopwatch();

while (5번 호출)
{
    st.Start();
    IPHostEntry entries = System.Net.Dns.GetHostByName(domainName);
    Console.WriteLine(string.Format("{0}. GetHostByName: {1}", i + 1, st.ElapsedMilliseconds));
    st.Reset();
}

===== 실행 결과 =====
1. GetHostByName: 8 <== 처음 한 번 서버 조회
2. GetHostByName: 0 <== 이후 캐시로부터의 결과 반환으로 시간 단축
3. GetHostByName: 0
4. GetHostByName: 0
5. GetHostByName: 0

그렇다면, 캐시된 결과값이 아니라 매번 직접 DNS 서버로부터 조회를 하고 싶다면 어떻게 해야 할까요?

우선, Process.Start 메서드를 "ipconfig /flushdns" 명령어를 내리는 방법이 있습니다. 실제로 테스트를 해보면 다음과 같이 매번 조회하는 것을 확인할 수 있습니다.

string domainName = "www.dns.com";
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = @"c:\\windows\\system32\\ipconfig.exe";
psi.Arguments = "/flushdns";

while (5번 호출)
{
    st.Start();
    IPHostEntry entries = System.Net.Dns.GetHostByName(domainName);
    Console.WriteLine(string.Format("{0}. GetHostByName: {1}", i + 1, st.ElapsedMilliseconds));

    Process process = Process.Start(psi);
    process.WaitForExit();
    st.Reset();
}

===== 실행 결과 =====
1. GetHostByName: 8
2. GetHostByName: 10 <== 첫 번째 호출 이후에 캐시 결과 반환이 아님을 알 수 있음.
3. GetHostByName: 8
4. GetHostByName: 8
5. GetHostByName: 8

결과야 나왔지만, 프로그래머로서 마음에 안 드는 뭔가가 있습니다.

아쉽지만, 그 외에 순수하게 닷넷을 이용하는 방법 내에서는 딱히 떠오르는 것이 없군요. 결국 가장 확실하게 남은 방법이라면, 직접 DNS 프로토콜에 맞게 제작하는 것인데 부담되시는 분들은 아래와 같은 공개된 소스 코드를 사용하는 것도 도움이 될 것입니다. ^^

DNS Client in C#
; http://www.c-sharpcorner.com/UploadFile/ivxivx/DNSClient12122005234612PM/DNSClient.aspx




닷넷 개발자에게는, 윈도우 플랫폼 자체에서 제공되는 풍부한 Win32 API도 활용할 수 있는 영역의 일부입니다. 사실 닷넷에서 P/Invoke라는 공식적인 방법이 제공되고 있으니 Win32 API 사용이 "닷넷만의 해법"에 속하지 않는다고 볼 수는 없습니다.

자, 그럼 한번 찾아볼까요? ^^ 우선 "ipconfig /flushdns"와 동일한 효과를 가지는 Win32 API가 있습니다.

DnsFlushResolverCache - WINXP: Flushing DNS programatically using C# 
; http://brannickdevice.blogspot.com/2006/04/winxp-flushing-dns-programatically.html

위의 글에 쓰인 것처럼, 실제로 MSDN 도움말에서는 위의 API 설명이 누락되어 있으니 언제 바뀌어도 이상하지 않을 'undocumented' API라는 것을 유의해 두어야 겠습니다.

그런데, ipconfig.exe 외부 프로세스를 실행하는 것을 Win32 API로 해결했다고 해서 100% 마음에 들지는 않습니다. 왜냐하면, 이런 식으로 DNS 캐시를 날리게 되면 시스템에 있는 다른 프로그램들까지도 영향을 받기 때문입니다. 생각하기에 따라서 이건 꽤 심각한 부작용일 수 있습니다.

좀 더 매끄럽게 해결할 수 있는 방법은 없을까요? 바로, 오늘의 하이라이트인 ^^ DnsQuery(DnsQuery_A, DnsQuery_W, DnsQuery_UTF8)를 이용하는 방법으로 이 모든 문제를 해결해 보겠습니다.

DnsQuery Function
; https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsquery_a

닷넷에서 사용할 수 있는 P/Invoke 구문은 다음과 같습니다.

dnsquery (dnsapi)
; http://www.pinvoke.net/default.aspx/dnsapi.dnsquery

하지만 아쉽게도 위의 자료에 실린 예제는 MX 레코드를 구하는 것이고, 일반적인 IP 주소를 나타내는 A 레코드를 구하는 것이 없습니다. 음... ^^ 그럼 만들면 되겠지요.

첫 번째 단계로, DnsQuery API의 signature를 볼까요?

DNS_STATUS WINAPI DnsQuery(
  __in         PCTSTR lpstrName,
  __in         WORD wType,
  __in         DWORD Options,
  __inout_opt  PVOID pExtra,
  __out_opt    PDNS_RECORD *ppQueryResultsSet,
  __out_opt    PVOID *pReserved
);

여기서 DNS에 대한 IP를 얻기 위해 우리가 맞춰주어야 할 값은 바로 5번째 DNS_RECORD입니다.

DNS_RECORD Structure
; https://docs.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_recorda

typedef struct _DnsRecord {
  DNS_RECORD *pNext;
  PWSTR      pName;
  WORD       wType;
  WORD       wDataLength;
  union {
    DWORD            DW;
    DNS_RECORD_FLAGS S;
  } Flags;
  DWORD      dwTtl;
  DWORD      dwReserved;
  union {
    DNS_A_DATA      A;
    DNS_SOA_DATA    SOA, Soa;
    DNS_PTR_DATA    PTR, Ptr, NS, Ns, CNAME, Cname, DNAME, Dname, MB, Mb, MD, Md, MF, Mf, MG, Mg, MR, Mr;
    DNS_MINFO_DATA  MINFO, Minfo, RP, Rp;
    ...[생략]...
    DNS_DHCID_DATA    DHCID;
  } Data;
} DNS_RECORD, *PDNS_RECORD;


다소 복잡한 것 같지만, union으로 포함된 것 중에 "DNS_A_DATA A;"만 알아내면 되는데,

DNS_A_DATA Structure
; https://docs.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_a_data

typedef struct {
  IP4_ADDRESS IpAddress;
} DNS_A_DATA, *PDNS_A_DATA;

DNS Data Types
; https://docs.microsoft.com/en-us/windows/win32/dns/dns-data-types

typedef DWORD IP4_ADDRESS;

결국 단순하게 DWORD 값임을 알 수 있습니다. 자... 그럼 다 되었군요. ^^ P/Invoke의 예제에서 위의 부분만을 적용하면 되겠는데, 좀 더 찾아보니 A Record에 대한 조회 예제가 다음과 같이 제공이 되었습니다.

How to use the DnsQuery function to resolve host names and host addresses with Visual C++ .NET
; http://support.microsoft.com/kb/831226/en-us

이에 따라, 최종적으로 소스 코드를 구성해 보면 다음과 같습니다.

class DnsRecordQuery
{
    [DllImport("dnsapi", EntryPoint = "DnsQuery_W", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
    private static extern int DnsQuery([MarshalAs(UnmanagedType.VBByRefStr)]ref string pszName, QueryTypes wType, QueryOptions options, int aipServers, ref IntPtr ppQueryResults, int pReserved);

    [DllImport("dnsapi", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern void DnsRecordListFree(IntPtr pRecordList, int FreeType);

    public static IPAddress GetARecords(string domain)
    {
        IntPtr pDnsRecord = IntPtr.Zero;

        try
        {
            int status = DnsRecordQuery.DnsQuery(ref domain, QueryTypes.DNS_TYPE_A, QueryOptions.DNS_QUERY_BYPASS_CACHE, 0, ref pDnsRecord, 0);
            if (status != 0)
            {
                throw new Win32Exception(status);
            }

            ARecord recordA = (ARecord)Marshal.PtrToStructure(pDnsRecord, typeof(ARecord));
            return new IPAddress((long)recordA.ip4Address);
        }
        finally
        {
            DnsRecordQuery.DnsRecordListFree(pDnsRecord, 0);
        }
    }

    private enum QueryOptions
    {
        DNS_QUERY_BYPASS_CACHE = 8,
    }

    private enum QueryTypes
    {
        DNS_TYPE_A = 1,
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct ARecord
    {
        public IntPtr pNext;
        public string pName;
        public short wType;
        public short wDataLength;
        public int flags;
        public int dwTtl;
        public int dwReserved;

        public int ip4Address;
    }
}

위의 소스 코드에서 핵심적인 내용은 DnsQuery 호출에 DNS_QUERY_BYPASS_CACHE 인자를 넘기는 것입니다. 그 덕분에 DNS Resolver Cache에서 값을 조회하지 않고 직접 서버로 구하는 작업을 하기 때문입니다. 물론, 테스트를 해봐도 다음과 같이 시간이 매번 출력되는 것을 확인할 수 있습니다. (참고로, 네트워크를 끊어놓고 해보면 DnsQuery API는 123이라는 반환값이 나오고 300ms의 수행 시간값이 고르게 나오는 반면, GetHostByName .NET 메서드에서는 예외가 발생합니다.)

[DNS_QUERY_BYPASS_CACHE 옵션으로 DnsQuery를 사용한 경우]
1. GetARecords: 8
2. GetARecords: 8
3. GetARecords: 9
4. GetARecords: 8
5. GetARecords: 9

[ipconfig /flushdns와 GetHostByName을 사용한 경우]
1. GetHostByName: 10
2. GetHostByName: 9
3. GetHostByName: 9
4. GetHostByName: 9
5. GetHostByName: 9




여기까지 하면 끝일 줄 알았는데, 테스트를 하다 보니 이상한 점이 발견되었습니다.

예를 들어, "www.microsoft.com"을 조회하는데, DnsQuery로는 "144.11.232.5"와 같은 IP가 나오고, GetHostByName으로는 65.55.12.249와 같은 IP가 나왔습니다. 물론 DNS를 통한 부하 분산을 하기 때문에 IP가 다르게 나올 수는 있겠지만, 문제는 웹 브라우저를 통해서 접속해 보면 DnsQuery로 반환받은 IP로는 정상적으로 웹 사이트에 연결이 안된다는 점입니다.

MXRecord를 구하는 DnsQuery 예제에서 보니, DNS_RECORD의 pNext를 통해서 열람하는 것을 볼 수 있는데, 이를 DNS_TYPE_A에도 적용해서 출력을 해보았습니다.

www.microsoft.com == 80.17.15.6
toggle.www.ms.akadns.net == 208.16.15.6
g.www.ms.akadns.net == 48.16.15.6
lb1.www.ms.akadns.net == 207.46.19.190
za.akadns.org == 96.6.112.198
zb.akadns.org == 64.211.42.194
zc.akadns.org == 124.40.52.133
zd.akadns.org == 72.246.46.4
ze.akadns.net == 193.108.88.129
zf.akadns.net == 193.108.88.130
eur1.akadns.net == 195.59.44.134
use3.akadns.net == 72.246.46.4
use4.akadns.net == 208.44.108.137
usw2.akadns.net == 64.211.42.194
asia9.akadns.net == 222.122.64.133

음... 이거 만만치가 않군요. ^^ 위의 DNS 이름 풀이에서 실제로 웹 브라우저를 통해서 접근할 수 있었던 IP는 "lb1.www.ms.akadns.net"에 해당하는 값이었습니다. 즉, "www.microsoft.com"으로 반환받은 80.17.15.6 값은 웹 브라우저 입장에서는 의미가 없는 값인데... 그럼 어떤 기준으로 "lb1.www.ms.akadns.net"에 해당하는 IP를 구할 수 있을까요?

이를 위해 DNS_RECORD의 값 중에서 wType과 Flags 값을 같이 출력해 보았습니다.

Console.WriteLine(string.Format("{0}[{1}, {2}] == {3}", item.HostName, item.Type, item.Flags, item.IPAddress));

www.microsoft.com[5, 12297] == 80.17.46.6
toggle.www.ms.akadns.net[5, 12297] == 208.16.46.6
g.www.ms.akadns.net[5, 12297] == 48.16.46.6
lb1.www.ms.akadns.net[1, 8201] == 65.55.12.249
za.akadns.org[1, 8203] == 96.6.112.198
zb.akadns.org[1, 8203] == 64.211.42.194
zc.akadns.org[1, 8203] == 124.40.52.133
zd.akadns.org[1, 8203] == 72.246.46.4
ze.akadns.net[1, 8203] == 193.108.88.129
zf.akadns.net[1, 8203] == 193.108.88.130
eur1.akadns.net[1, 8203] == 195.59.44.134
use3.akadns.net[1, 8203] == 72.246.46.4
use4.akadns.net[1, 8203] == 208.44.108.137
usw2.akadns.net[1, 8203] == 64.211.42.194
asia9.akadns.net[1, 8203] == 222.122.64.133

아하,,, 뭔가 규칙이 있는 것 같습니다. 일단 wType의 열람값을 보면, 다음에 해당합니다.

DNS_TYPE_A == 0x0001
DNS_TYPE_CNAME == 0x0005

그렇다면, wType == 1인 값 중에서 Flags의 값에 따라 필터링을 해야 할 것 같은데, Flags 값의 의미를 좀 더 자세히 알아보기 위해 도움말을 찾아보았습니다.

typedef struct _DnsRecordFlags {
  DWORD Section  :2;
  DWORD Delete  :1;
  DWORD CharSet  :2;
  DWORD Unused  :3;
  DWORD Reserved  :24;
} DNS_RECORD_FLAGS;

typedef enum  {
  DnsSectionQuestion, /* The DNS section specified is a DNS question. */
  DnsSectionAnswer, /* The DNS section specified is a DNS answer. */
  DnsSectionAuthority, /* The DNS section specified indicates a DNS authority. */
  DnsSectionAddtional /* The DNS section specified is additional DNS information. */
} DNS_SECTION;

설명되어 있는 것과 실험값의 조합에 따라 종합해서 판단해 보면, DnsQuery를 통해 원하는 IP 주소를 구해오려면 wType == DNS_TYPE_A, Flags == DnsSectionAnswer인 조합을 반환받으면 될 것 같습니다.

자, 실제로 이제 다양한 DNS 이름을 통해 확인해 보니 정상적으로 구해지는 것을 알 수 있습니다.

==== www.microsoft.com ====
[DnsQuery인 경우]
lb1.www.ms.akadns.net[1, 8201] == 207.46.19.254, Section == 1

[GetHostByName인 경우]
207.46.19.190

==== www.naver.com ====
[DnsQuery인 경우]
www.g.naver.com[1, 8201] == 202.131.29.70, Section == 1
www.g.naver.com[1, 9] == 222.122.195.6, Section == 1

[GetHostByName인 경우]
202.131.29.71
222.122.195.5

==== www.daum.net ====
[DnsQuery인 경우]
www.g.daum.net[1, 8201] == 180.70.134.9, Section == 1
www.g.daum.net[1, 9] == 180.70.93.57, Section == 1

[GetHostByName인 경우]
180.70.93.57
180.70.134.9

==== 특히 유명한 www.sysnet.pe.kr =====
[DnsQuery인 경우]
www.sysnet.pe.kr[1, 8201] == 121.139.108.88, Section == 1

[GetHostByName인 경우]
121.139.108.88

이걸로 일단 DnsQuery 검사는 끝~~~!

첨부한 파일은 위의 결과를 확인할 수 있는 소스코드를 담고 있습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 7/17/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2012-04-16 11시29분
What is the real maximum length of a DNS name?
; https://devblogs.microsoft.com/oldnewthing/20120412-00/?p=7873
정성태
2019-03-20 01시18분
ipconfig /flushdns
ipconfig /displaydns
정성태

1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13314정성태4/9/20234023개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20234045개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20234107Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234563C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20234226C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/20234356.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/20234249스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
13307정성태4/4/20234007.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/20233794Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/20234168Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)파일 다운로드1
13304정성태3/31/20234498VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
13303정성태3/30/20233840Windows: 241. 환경 변수 %PATH%에 DLL을 찾는 규칙
13302정성태3/30/20234472Windows: 240. RDP 환경에서 바뀌는 %TEMP% 디렉터리 경로
13301정성태3/29/20234591Windows: 239. C/C++ - Windows 10 Version 1607부터 지원하는 /DEPENDENTLOADFLAG 옵션파일 다운로드1
13300정성태3/28/20234261Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유
13299정성태3/27/20234003Windows: 237. Win32 - 모든 메시지 루프를 탈출하는 WM_QUIT 메시지
13298정성태3/27/20233987Windows: 236. Win32 - MessageBeep 소리가 안 들린다면?
13297정성태3/26/20234654Windows: 235. Win32 - Code Modal과 UI Modal
13296정성태3/25/20234011Windows: 234. IsDialogMessage와 협업하는 WM_GETDLGCODE Win32 메시지 [1]파일 다운로드1
13295정성태3/24/20234247Windows: 233. Win32 - modeless 대화창을 modal처럼 동작하게 만드는 방법파일 다운로드1
13294정성태3/22/20234428.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234491오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/20234616Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/20234974.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/20234490.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/20233682Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...