Microsoft MVP성태의 닷넷 이야기
.NET Framework: 221. Cache 영향을 받지 않는 DNS 이름 풀이 [링크 복사], [링크+제목 복사],
조회: 20147
글쓴 사람
정성태 (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)
13339정성태5/6/20235063닷넷: 2113. C# 12 - 기본 생성자(Primary Constructors)
13338정성태5/6/20234422닷넷: 2112. C# 12 - 기본 람다 매개 변수파일 다운로드1
13337정성태5/5/20234941Linux: 59. dockerfile - docker exec로 container에 접속 시 자동으로 실행되는 코드 적용
13336정성태5/4/20234772.NET Framework: 2111. C# - 바이너리 출력 디렉터리와 연관된 csproj 설정
13335정성태4/30/20234801.NET Framework: 2110. C# - FFmpeg.AutoGen 라이브러리를 이용한 기본 프로젝트 구성 - Windows Forms파일 다운로드1
13334정성태4/29/20234449Windows: 250. Win32 C/C++ - Modal 메시지 루프 내에서 SetWindowsHookEx를 이용한 Thread 메시지 처리 방법
13333정성태4/28/20233886Windows: 249. Win32 C/C++ - 대화창 템플릿을 런타임에 코딩해서 사용파일 다운로드1
13332정성태4/27/20233952Windows: 248. Win32 C/C++ - 대화창을 위한 메시지 루프 사용자 정의파일 다운로드1
13331정성태4/27/20233949오류 유형: 856. dockerfile - 구 버전의 .NET Core 이미지 사용 시 apt update 오류
13330정성태4/26/20233609Windows: 247. Win32 C/C++ - CS_GLOBALCLASS 설명
13329정성태4/24/20233846Windows: 246. Win32 C/C++ - 직접 띄운 대화창 템플릿을 위한 Modal 메시지 루프 생성파일 다운로드1
13328정성태4/19/20233479VS.NET IDE: 184. Visual Studio - Fine Code Coverage에서 동작하지 않는 Fake/Shim 테스트
13327정성태4/19/20233878VS.NET IDE: 183. C# - .NET Core/5+ 환경에서 Fakes를 이용한 단위 테스트 방법
13326정성태4/18/20235368.NET Framework: 2109. C# - 닷넷 응용 프로그램에서 SQLite 사용 (System.Data.SQLite) [1]파일 다운로드1
13325정성태4/18/20234640스크립트: 48. 파이썬 - PostgreSQL의 with 문을 사용한 경우 연결 개체 누수
13324정성태4/17/20234463.NET Framework: 2108. C# - Octave의 "save -binary ..."로 생성한 바이너리 파일 분석파일 다운로드1
13323정성태4/16/20234421개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20235225VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20234027개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20233939개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234425개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
13318정성태4/11/20234310개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234364오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233623오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20233875Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20234030개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...