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

... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12875정성태12/13/20216822스크립트: 35. python - time.sleep(...) 호출 시 hang이 걸리는 듯한 문제
12874정성태12/13/20216862오류 유형: 773. shell script 실행 시 "$'\r': command not found" 오류
12873정성태12/12/20217995오류 유형: 772. 리눅스 - PATH에 등록했는데도 "command not found"가 나온다면?
12872정성태12/12/20217838개발 환경 구성: 615. GoLang과 Python 빌드가 모두 가능한 docker 이미지 만들기
12871정성태12/12/20217869오류 유형: 771. docker: Error response from daemon: OCI runtime create failed
12870정성태12/9/20216422개발 환경 구성: 614. 파이썬 - PyPI 패키지 만들기 (4) package_data 옵션
12869정성태12/8/20218733개발 환경 구성: 613. git clone 실행 시 fingerprint 묻는 단계를 생략하는 방법
12868정성태12/7/20217349오류 유형: 770. twine 업로드 시 "HTTPError: 400 Bad Request ..." 오류 [1]
12867정성태12/7/20216997개발 환경 구성: 612. 파이썬 - PyPI 패키지 만들기 (3) entry_points 옵션
12866정성태12/7/202114388오류 유형: 769. "docker build ..." 시 "failed to solve with frontend dockerfile.v0: failed to read dockerfile ..." 오류
12865정성태12/6/20217135개발 환경 구성: 611. 파이썬 - PyPI 패키지 만들기 (2) long_description, cmdclass 옵션
12864정성태12/6/20215527Linux: 46. WSL 환경에서 find 명령을 사용해 파일을 찾는 방법
12863정성태12/4/20217465개발 환경 구성: 610. 파이썬 - PyPI 패키지 만들기
12862정성태12/3/20216163오류 유형: 768. Golang - 빌드 시 "cmd/go: unsupported GOOS/GOARCH pair linux /amd64" 오류
12861정성태12/3/20218421개발 환경 구성: 609. 파이썬 - "Windows embeddable package"로 개발 환경 구성하는 방법
12860정성태12/1/20216453오류 유형: 767. SQL Server - 127.0.0.1로 접속하는 경우 "Access is denied"가 발생한다면?
12859정성태12/1/202112693개발 환경 구성: 608. Hyper-V 가상 머신에 Console 모드로 로그인하는 방법
12858정성태11/30/20219949개발 환경 구성: 607. 로컬의 USB 장치를 원격 머신에 제공하는 방법 - usbip-win
12857정성태11/24/20217280개발 환경 구성: 606. WSL Ubuntu 20.04에서 파이썬을 위한 uwsgi 설치 방법
12856정성태11/23/20219206.NET Framework: 1121. C# - 동일한 IP:Port로 바인딩 가능한 서버 소켓 [2]
12855정성태11/13/20216410개발 환경 구성: 605. Azure App Service - Kudu SSH 환경에서 FTP를 이용한 파일 전송
12854정성태11/13/20218086개발 환경 구성: 604. Azure - 윈도우 VM에서 FTP 여는 방법
12853정성태11/10/20216350오류 유형: 766. Azure App Service - JBoss 호스팅 생성 시 "This region has quota of 0 PremiumV3 instances for your subscription. Try selecting different region or SKU."
12851정성태11/1/20217868스크립트: 34. 파이썬 - MySQLdb 기본 예제 코드
12850정성태10/27/20219027오류 유형: 765. 우분투에서 pip install mysqlclient 실행 시 "OSError: mysql_config not found" 오류
12849정성태10/17/20218139스크립트: 33. JavaScript와 C#의 시간 변환 [1]
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...