Microsoft MVP성태의 닷넷 이야기
.NET Framework: 221. Cache 영향을 받지 않는 DNS 이름 풀이 [링크 복사], [링크+제목 복사],
조회: 20145
글쓴 사람
정성태 (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)
13390정성태7/12/20233532스크립트: 53. 파이썬 - localhost 호출 시의 hang 현상
13389정성태7/5/20233585개발 환경 구성: 684. IIS Express로 호스팅하는 웹을 WSL 환경에서 접근하는 방법
13388정성태7/3/20233765오류 유형: 871. 윈도우 탐색기에서 열리지 않는 zip 파일 - The Compressed (zipped) Folder '[...].zip' is invalid. [1]파일 다운로드1
13387정성태6/28/20233822오류 유형: 870. _mysql - Commands out of sync; you can't run this command now
13386정성태6/27/20233954Linux: 61. docker - 원격 제어를 위한 TCP 바인딩 추가
13385정성태6/27/20234137Linux: 60. Linux - 외부에서의 접속을 허용하기 위한 TCP 포트 여는 방법
13384정성태6/26/20233849.NET Framework: 2131. C# - Source Generator로 해결하는 enum 박싱 문제파일 다운로드1
13383정성태6/26/20233584개발 환경 구성: 683. GPU 런타임을 사용하는 Colab 노트북 설정
13382정성태6/25/20233657.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/20234063오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/20233459스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/20233496스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/20233393오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/20237250오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
13376정성태6/21/20233639.NET Framework: 2129. C# - Polly를 이용한 클라이언트 측의 요청 재시도파일 다운로드1
13375정성태6/20/20233281스크립트: 50. Transformers (신경망 언어모델 라이브러리) 강좌 - 2장 코드 실행 결과
13374정성태6/20/20233395오류 유형: 866. 파이썬 - <class 'AttributeError'> module 'flask.json' has no attribute 'JSONEncoder'
13373정성태6/19/20234681오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/20233288개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/20233373개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
13370정성태6/14/20233531개발 환경 구성: 680. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 - TLS 1.2 지원
13369정성태6/13/20233332개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
13368정성태6/13/20233507개발 환경 구성: 678. openssl로 생성한 인증서를 SQL Server의 암호화 인증서로 설정하는 방법
13367정성태6/10/20233659오류 유형: 864. openssl로 만든 pfx 인증서를 Windows Server 2016 이하에서 등록 시 "The password you entered is incorrect" 오류 발생
13366정성태6/10/20233406.NET Framework: 2128. C# - 윈도우 시스템에서 지원하는 암호화 목록(Cipher Suites) 나열파일 다운로드1
13365정성태6/8/20233140오류 유형: 863. MODIFY FILE encountered operating system error 112(failed to retrieve text for this error. Reason: 15105)
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...