성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>Cache 영향을 받지 않는 DNS 이름 풀이</h1> <p> <br /> 보통, DNS 이름 풀이를 한 번 하게 되면 로컬 컴퓨터에 Cache가 됩니다. 그래서, 다음과 같이 System.Net.Dns 조회를 사용하는 경우 두 번째 호출부터는 캐시된 결과값을 반환받게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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(); } ===== 실행 결과 ===== <span style='color: blue; font-weight: bold'>1. GetHostByName: 8 <== 처음 한 번 서버 조회 2. GetHostByName: 0 <== 이후 캐시로부터의 결과 반환으로 시간 단축</span> 3. GetHostByName: 0 4. GetHostByName: 0 5. GetHostByName: 0 </pre> <br /> 그렇다면, 캐시된 결과값이 아니라 매번 직접 DNS 서버로부터 조회를 하고 싶다면 어떻게 해야 할까요?<br /> <br /> 우선, Process.Start 메서드를 "ipconfig /flushdns" 명령어를 내리는 방법이 있습니다. 실제로 테스트를 해보면 다음과 같이 매번 조회하는 것을 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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(); } ===== 실행 결과 ===== <span style='color: blue; font-weight: bold'>1. GetHostByName: 8 2. GetHostByName: 10 <== 첫 번째 호출 이후에 캐시 결과 반환이 아님을 알 수 있음.</span> 3. GetHostByName: 8 4. GetHostByName: 8 5. GetHostByName: 8 </pre> <br /> 결과야 나왔지만, 프로그래머로서 마음에 안 드는 뭔가가 있습니다. <br /> <br /> 아쉽지만, 그 외에 순수하게 닷넷을 이용하는 방법 내에서는 딱히 떠오르는 것이 없군요. 결국 가장 확실하게 남은 방법이라면, 직접 DNS 프로토콜에 맞게 제작하는 것인데 부담되시는 분들은 아래와 같은 공개된 소스 코드를 사용하는 것도 도움이 될 것입니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DNS Client in C# ; <a target='tab' href='http://www.c-sharpcorner.com/UploadFile/ivxivx/DNSClient12122005234612PM/DNSClient.aspx'>http://www.c-sharpcorner.com/UploadFile/ivxivx/DNSClient12122005234612PM/DNSClient.aspx</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> 닷넷 개발자에게는, 윈도우 플랫폼 자체에서 제공되는 풍부한 Win32 API도 활용할 수 있는 영역의 일부입니다. 사실 닷넷에서 P/Invoke라는 공식적인 방법이 제공되고 있으니 Win32 API 사용이 "닷넷만의 해법"에 속하지 않는다고 볼 수는 없습니다.<br /> <br /> 자, 그럼 한번 찾아볼까요? ^^ 우선 "ipconfig /flushdns"와 동일한 효과를 가지는 Win32 API가 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DnsFlushResolverCache - WINXP: Flushing DNS programatically using C# ; <a target='tab' href='http://brannickdevice.blogspot.com/2006/04/winxp-flushing-dns-programatically.html'>http://brannickdevice.blogspot.com/2006/04/winxp-flushing-dns-programatically.html</a> </pre> <br /> 위의 글에 쓰인 것처럼, 실제로 MSDN 도움말에서는 위의 API 설명이 누락되어 있으니 언제 바뀌어도 이상하지 않을 'undocumented' API라는 것을 유의해 두어야 겠습니다. <br /> <br /> 그런데, ipconfig.exe 외부 프로세스를 실행하는 것을 Win32 API로 해결했다고 해서 100% 마음에 들지는 않습니다. 왜냐하면, 이런 식으로 DNS 캐시를 날리게 되면 시스템에 있는 다른 프로그램들까지도 영향을 받기 때문입니다. 생각하기에 따라서 이건 꽤 심각한 부작용일 수 있습니다.<br /> <br /> 좀 더 매끄럽게 해결할 수 있는 방법은 없을까요? 바로, 오늘의 하이라이트인 ^^ DnsQuery(DnsQuery_A, DnsQuery_W, DnsQuery_UTF8)를 이용하는 방법으로 이 모든 문제를 해결해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DnsQuery Function ; <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsquery_a'>https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsquery_a</a> </pre> <br /> 닷넷에서 사용할 수 있는 P/Invoke 구문은 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > dnsquery (dnsapi) ; <a target='tab' href='http://www.pinvoke.net/default.aspx/dnsapi.dnsquery'>http://www.pinvoke.net/default.aspx/dnsapi.dnsquery</a> </pre> <br /> 하지만 아쉽게도 위의 자료에 실린 예제는 MX 레코드를 구하는 것이고, 일반적인 IP 주소를 나타내는 A 레코드를 구하는 것이 없습니다. 음... ^^ 그럼 만들면 되겠지요.<br /> <br /> 첫 번째 단계로, DnsQuery API의 signature를 볼까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DNS_STATUS WINAPI DnsQuery( __in PCTSTR lpstrName, __in WORD wType, __in DWORD Options, __inout_opt PVOID pExtra, <span style='color: blue; font-weight: bold'>__out_opt PDNS_RECORD *ppQueryResultsSet,</span> __out_opt PVOID *pReserved ); </pre> <br /> 여기서 DNS에 대한 IP를 얻기 위해 우리가 맞춰주어야 할 값은 바로 5번째 DNS_RECORD입니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DNS_RECORD Structure ; <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_recorda'>https://docs.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_recorda</a> 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 { <span style='color: blue; font-weight: bold'>DNS_A_DATA A;</span> 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; </pre> <br /> 다소 복잡한 것 같지만, union으로 포함된 것 중에 "DNS_A_DATA A;"만 알아내면 되는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DNS_A_DATA Structure ; <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_a_data'>https://docs.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_a_data</a> <span style='color: blue; font-weight: bold'>typedef struct { IP4_ADDRESS IpAddress; } DNS_A_DATA, *PDNS_A_DATA; </span> DNS Data Types ; <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/dns/dns-data-types'>https://docs.microsoft.com/en-us/windows/win32/dns/dns-data-types</a> <span style='color: blue; font-weight: bold'>typedef DWORD IP4_ADDRESS;</span> </pre> <br /> 결국 단순하게 DWORD 값임을 알 수 있습니다. 자... 그럼 다 되었군요. ^^ <a target='tab' href='http://www.pinvoke.net/default.aspx/dnsapi.dnsquery'>P/Invoke의 예제</a>에서 위의 부분만을 적용하면 되겠는데, 좀 더 찾아보니 A Record에 대한 조회 예제가 다음과 같이 제공이 되었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > How to use the DnsQuery function to resolve host names and host addresses with Visual C++ .NET ; <a target='tab' href='http://support.microsoft.com/kb/831226/en-us'>http://support.microsoft.com/kb/831226/en-us</a> </pre> <br /> 이에 따라, 최종적으로 소스 코드를 구성해 보면 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 <mark>GetARecords</mark>(string domain) { IntPtr pDnsRecord = IntPtr.Zero; try { int status = DnsRecordQuery.DnsQuery(ref domain, QueryTypes.DNS_TYPE_A, <mark>QueryOptions.DNS_QUERY_BYPASS_CACHE</mark>, 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; } } </pre> <br /> 위의 소스 코드에서 핵심적인 내용은 DnsQuery 호출에 DNS_QUERY_BYPASS_CACHE 인자를 넘기는 것입니다. 그 덕분에 DNS Resolver Cache에서 값을 조회하지 않고 직접 서버로 구하는 작업을 하기 때문입니다. 물론, 테스트를 해봐도 다음과 같이 시간이 매번 출력되는 것을 확인할 수 있습니다. (참고로, 네트워크를 끊어놓고 해보면 DnsQuery API는 123이라는 반환값이 나오고 300ms의 수행 시간값이 고르게 나오는 반면, GetHostByName .NET 메서드에서는 예외가 발생합니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [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 </pre> <br /> <hr style='width: 50%' /><br /> <br /> 여기까지 하면 끝일 줄 알았는데, 테스트를 하다 보니 이상한 점이 발견되었습니다.<br /> <br /> 예를 들어, "www.microsoft.com"을 조회하는데, DnsQuery로는 "144.11.232.5"와 같은 IP가 나오고, GetHostByName으로는 65.55.12.249와 같은 IP가 나왔습니다. 물론 DNS를 통한 부하 분산을 하기 때문에 IP가 다르게 나올 수는 있겠지만, 문제는 웹 브라우저를 통해서 접속해 보면 DnsQuery로 반환받은 IP로는 정상적으로 웹 사이트에 연결이 안된다는 점입니다.<br /> <br /> <a target='tab' href='http://www.pinvoke.net/default.aspx/dnsapi.dnsquery'>MXRecord를 구하는 DnsQuery 예제</a>에서 보니, DNS_RECORD의 pNext를 통해서 열람하는 것을 볼 수 있는데, 이를 DNS_TYPE_A에도 적용해서 출력을 해보았습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>www.microsoft.com == 80.17.15.6</span> toggle.www.ms.akadns.net == 208.16.15.6 g.www.ms.akadns.net == 48.16.15.6 <span style='color: blue; font-weight: bold'>lb1.www.ms.akadns.net == 207.46.19.190</span> 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 </pre> <br /> 음... 이거 만만치가 않군요. ^^ 위의 DNS 이름 풀이에서 실제로 웹 브라우저를 통해서 접근할 수 있었던 IP는 "lb1.www.ms.akadns.net"에 해당하는 값이었습니다. 즉, "www.microsoft.com"으로 반환받은 80.17.15.6 값은 웹 브라우저 입장에서는 의미가 없는 값인데... 그럼 어떤 기준으로 "lb1.www.ms.akadns.net"에 해당하는 IP를 구할 수 있을까요?<br /> <br /> 이를 위해 DNS_RECORD의 값 중에서 wType과 Flags 값을 같이 출력해 보았습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Console.WriteLine(string.Format("{0}<span style='color: blue; font-weight: bold'>[{1}, {2}]</span> == {3}", item.HostName, <span style='color: blue; font-weight: bold'>item.Type, item.Flags</span>, 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 <span style='color: blue; font-weight: bold'>lb1.www.ms.akadns.net[1, 8201] == 65.55.12.249</span> 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 </pre> <br /> 아하,,, 뭔가 규칙이 있는 것 같습니다. 일단 wType의 열람값을 보면, 다음에 해당합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DNS_TYPE_A == 0x0001 DNS_TYPE_CNAME == 0x0005 </pre> <br /> 그렇다면, wType == 1인 값 중에서 Flags의 값에 따라 필터링을 해야 할 것 같은데, Flags 값의 의미를 좀 더 자세히 알아보기 위해 도움말을 찾아보았습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > typedef struct _DnsRecordFlags { <span style='color: blue; font-weight: bold'>DWORD Section :2;</span> 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. */ <mark>DnsSectionAnswer</mark>, /* 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; </pre> <br /> 설명되어 있는 것과 실험값의 조합에 따라 종합해서 판단해 보면, DnsQuery를 통해 원하는 IP 주소를 구해오려면 wType == DNS_TYPE_A, Flags == DnsSectionAnswer인 조합을 반환받으면 될 것 같습니다.<br /> <br /> 자, 실제로 이제 다양한 DNS 이름을 통해 확인해 보니 정상적으로 구해지는 것을 알 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ==== 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 </pre> <br /> 이걸로 일단 DnsQuery 검사는 끝~~~!<br /> <br /> <a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=602&boardid=331301885'>첨부한 파일은 위의 결과를 확인할 수 있는 소스코드</a>를 담고 있습니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1276
(왼쪽의 숫자를 입력해야 합니다.)