Microsoft MVP성태의 닷넷 이야기
.NET Framework: 221. Cache 영향을 받지 않는 DNS 이름 풀이 [링크 복사], [링크+제목 복사],
조회: 27724
글쓴 사람
정성태 (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
정성태

... 151  152  153  154  155  156  157  158  159  160  [161]  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1021정성태4/18/201127879디버깅 기술: 37. .NET 4.0 응용 프로그램의 Main 함수에 BreakPoint 걸기
1020정성태4/18/201128489오류 유형: 117. Failed to find runtime DLL (mscorwks.dll), 0x80004005
1019정성태4/17/201129127디버깅 기술: 36. Visual Studio의 .NET Disassembly 창의 call 호출에 사용되는 주소의 의미는? [1]파일 다운로드1
1018정성태4/16/201132782오류 유형: 116. 윈도우 업데이트 오류 - 0x8020000E
1017정성태4/14/201127596개발 환경 구성: 115. MSBuild - x86/x64, .NET 2/4, debug/release 빌드에 대한 배치 처리파일 다운로드1
1016정성태4/13/201143602개발 환경 구성: 114. Windows Thin PC 설치 [2]
1015정성태4/9/201128954.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이파일 다운로드1
1014정성태4/7/2011106420오류 유형: 115. ORA-12516: TNS:listener could not find available handler with matching protocol stack [2]
1013정성태4/7/201124228Team Foundation Server: 45. SharePoint 2010 + TFS 2010 환경에서 ProcessGuidance.html 파일 다운로드 문제
1012정성태4/6/201132966.NET Framework: 208. WCF - 접속된 클라이언트의 IP 주소 알아내는 방법 [1]
1011정성태3/31/201135317오류 유형: 114. 인증서 갱신 오류 - The request contains no certificate template information.
1010정성태3/30/201126093개발 환경 구성: 113. 응용 프로그램 디자인 스케치 도구 - SketchFlow [4]
1009정성태3/29/201138414개발 환경 구성: 112. Visual Studio 2010 - .NET Framework 소스 코드 디버깅 [4]
1008정성태3/27/201130794.NET Framework: 207. C# - Right operand가 음수인 Shift 연산 결과 [2]
1007정성태3/16/201131625개발 환경 구성: 111. Excel - XML 파일 연동 [5]파일 다운로드1
1006정성태3/15/201125391.NET Framework: 206. XML/XSD - 외래키처럼 참조 제한 거는 방법파일 다운로드1
1005정성태3/11/201135246개발 환경 구성: 110. 엑셀 매크로 함수 관련 오류 [2]
1004정성태3/3/201124447개발 환경 구성: 109. SharePoint Health Analyzer 디스크 부족 경고 제어
1003정성태3/3/201125475오류 유형: 113. SQL Server - DB Attach 시 Parameter name: nColIndex 오류 발생
1002정성태3/2/201123889Team Foundation Server: 44. TFS 설치 후, Team Portal의 Dashboard를 빠르게 확인하는 방법
1001정성태3/2/201127891Team Foundation Server: 43. TFS 2010 + SharePoint 2010 설치
1000정성태3/1/201132849오류 유형: 112. Remote FX RDP 연결 시 오류 유형 2가지 [5]
999정성태2/28/201146375개발 환경 구성: 108. RemoteFX - Windows 7 가상 머신에서 DirectX 9c 환경을 제공 [5]
998정성태2/27/201120086Team Foundation Server: 42. TFS Application-Tier만 재설치
996정성태2/12/201137543디버깅 기술: 35. windbg - 분석 예: 시작하자마자 비정상 종료하는 프로세스 - NullReferenceException
995정성태2/11/201156119.NET Framework: 205. 코드(C#)를 통한 풀 덤프 만드는 방법 [4]
... 151  152  153  154  155  156  157  158  159  160  [161]  162  163  164  165  ...