プログラマーとDNS (1)

IP通信

 IP(Internet Protocol)通信を行う機器(ホスト)にはIPアドレスという一意の数値が割り振られていて(※1)、内部的にはそのIPアドレスを宛先としてデータを送受信します。IPアドレスは、4オクテットで表されるIPv4アドレスと、16オクテットで表されるIPv6アドレスが主流です。1オクテットは8ビットなので、それぞれ32ビットと128ビットの大きさの数値になります。

※1: IP Anycastという同じIPアドレスを複数の機器で共有する技術もあります。

ホスト名

 しかし、人間には大きな数値は覚えづらい/覚えきれないので、数値に対応した分かり易い文字列の名前を対応(マッピング)させておいて、その名前で通信相手を指定することが多いです。このIPアドレスにマッピングされた名前をホスト名と呼びます。

 インターネットにおいてはよくホスト名は階層化されていて、この階層化された名前をドメイン名と呼びます。階層の区切りにはドット(“.”)が用いられます。例えば、www.microsoft.comというドメイン名は、comに属するmicrosoft、そのmicrosoftに属するwwwと言った具合です。厳密にはcomの上位にルートドメインがあり、そのルートドメインをドットで表します。この例では末尾にドットがあるwww.microsoft.com.がより正確な表記になります。

 ドメイン名の中には、管理・分類などの都合により名前だけあってそれに対応する機器(ホスト)がないものがあります。例えばcom.はドメイン名ですが、対応するホストがないのでホスト名とは言えません。

 また、microsoft.com.ドメインに属するホストにとっては、単にwwwと相対的に指定するだけでwww.microsoft.com.と指定した事になりますが、microsoft.com.ドメイン外のホストは、www.microsoft.com.とフルネームでホスト名を指定しなければなりません。このフルネームのホスト名を完全修飾ドメイン名(fully qualified domain name, FQDN)と呼びます。ルートドメインのドットが省略された(ルートドメインからの相対的な)ホスト名もFQDNと呼ぶことが多いです。

DNS

 IPアドレスとホスト名のマッピングを管理する仕組みの一つがDNS(Domain Name System)です。そして、この仕組みでマッピングを提供してくれるサービスをコンテンツサーバー(contents server)=権威サーバー(authoritative server)と呼びます。他には、コンテンツサーバーへの問い合わせの代行と応答のキャッシュを行い通信コスト(帯域、時間)を軽減させるキャッシュサーバー(cache server)というサービスがあります。一般的に普段使っているDNSサービスは後者のキャッシュサーバーになります。

 ちなみに、IPアドレスとホスト名のマッピングは一対一とは限らず、一つのIPアドレスに複数のホスト名を付けたり、一つのホスト名に複数のIPアドレスを割り当てたりと、多対多に設定することもでき、負荷分散などに役立てられています。

 また、IPアドレスとホスト名のマッピングだけでなく、ホスト名の別名となるホスト名を設定出来たりします。この別名の元の名前を正規名正式名(canonical name, CNAME)と呼びます。正規名に対する別名は複数定義できますが、別名が複数の正規名を持つことはできません。また、正規名は別の1つの正規名を持つ事ができます。

hostsファイル

 DNSの他にもIPアドレスとホスト名のマッピング手段があります。そのうちの一つがhostsファイルです。hostsファイルは端末に置かれているテキストファイルで、その端末でのみ有効なマッピングを設定できます。DNSを使うまでもない場合に利用されます。

 hostsファイルは端末にあることもあってとても便利ですが、www.microsoft.comをApple社のIPアドレスにマッピングしたりなど、自由奔放に好き勝手にマッピング出来てしまい迂闊に編集すると危険なものでもあります。

 WindowsではC:\Windows\System32\drivers\etc、Linuxでは/etcにhostsファイルがあります。他のUNIXでは/etc/hostsが/etc/inet/hostsへのシンボリックリンクだったりするようです。

名前解決とリゾルバー

 様々な目的でIPアドレスとホスト名がマッピングされていますが、先ほど述べた通りIP通信にはIPアドレスが必要になります。そのため、IP通信を行うクライアントプログラムではホスト名からIPアドレスを求める必要があります。

 DNSに限らず、一般にこういったホスト名からIPアドレスなどを求める(引く)ことを名前解決(name resolution)と呼びます。また、名前解決を担うライブラリ(プログラム群)やサービスのことをざっくりとリゾルバー(resolver)やネームリゾルバー(name resolver)と呼びます。

リゾルバーとDNSクライアント

  細かい話ですが厳密にはリゾルバー=DNSクライアントとは言えません。

 一般にリゾルバーは、hostsファイルで名前解決したり、Windowsネットワーク用のNetBIOS名(コンピューター名)を解決したり(※1)、DNSだけ相手にしている訳ではありません。ですので、端的に言うと、DNSメッセージ(プロトコル)を使って問い合わせを行う側がDNSクライアントであり、それに応答する側がDNSサーバーです。

 リゾルバーは、DNSを使う必要があれば、端末内のスタブリゾルバー(stub resolver)に名前解決を依頼し、そのスタブリゾルバーは自分のキャッシュに該当する情報がなければ(※2)、”DNSクライアント“として、予め設定されたフルサービスリゾルバー(full-service resolver)にドメイン名を再帰的問い合わせ(recursive query)を行います。このフルサービスリゾルバーは、前述のキャッシュサーバーのことです。一般にはISPやスマホのキャリアが自社ユーザー向けに提供してくれていますが、自分でキャッシュサーバーを立てることもできます(※3)し、誰でも使えるオープンなキャッシュサーバーを提供してくれている団体があります。

 オープンなキャッシュサーバーで有名な所では、Google、Cloudflare、Quad9、OpenDNSなどが挙げられます。

 そして、キャッシュサーバーは、DNSクライアントの再帰的問い合わせに対してキャッシュを探し、なければコンテンツサーバーに対して”DNSクライアント”として反復問い合わせ(iterative query)を行います。そして、問い合わせされたFQDNに対応するIPアドレスが見つかればそれを、なければ無いという情報をスタブリゾルバーに返し、さらにそれをリゾルバーに返します。こうしてみると、キャッシュサーバーはDNSサーバーでありDNSクライアントでもあることがわかります。

 では、リゾルバーとは一体何なのか。プログラマー目線ではgetaddrinfo()がそれに当たります。すごいヤツです。

※1: WINS(Windows Internet Name Service)という名前解決サービスがあります。
※2: WindowsはOSが標準でDNSキャッシュを行います。
※3: LinuxはOSが標準でDNSキャッシュを行わない様なので、ローカルキャッシュ目的で端末内にDNSキャッシュサーバーを立てることがあるそうです。また、LAN内向けに立てることもあります。

とりあえずgetaddrinfo()で名前解決をし満足している

  IP通信を行うクライアントプログラムを書くプログラマーは、getaddrinfo()という関数を呼び出して名前解決をします。getaddrinfo() は、hostsファイルを解析したり、DNSキャッシュサーバーに問い合わせたり、さくっと名前解決をしてくれるとても便利な関数です。

 getaddrinfo()は今どきはOSがネットワークAPIとして標準で用意してくれている関数なので、アプリケーション層のプログラマならgetaddinfo()の実装を把握する必要はありません。もし、この関数がエラーを返すのなら、問題はこの関数の中ではなくそれ以外の個所にあるはずです。

constexpr auto hostname = "www.microsoft.com";

// FQDNの解決
addrinfo *paddr = nullptr, hint{ 0 };
hint.ai_family = AF_INET;     // IPv4ならAF_INET. IPv6ならAF_INET6. AF_UNSPECも良いよ
hint.ai_socktype = SOCK_STREAM; // 双方向のバイトストリーム
hint.ai_protocol = IPPROTO_TCP; // TCP
auto ret = getaddrinfo(hostname, "http", &hint, &paddr);
if (ret != 0) {
    // エラー処理
    return -1;
}

// ソケットの作成
auto sock = socket(paddr->ai_family, paddr->ai_socktype, paddr->ai_protocol);
if (sock == INVALID_SOCKET) {
    // エラー処理
    freeaddrinfo(paddr);
    return -1;
}

// 接続
ret = connect(sock, paddr->ai_addr, paddr->ai_addrlen);
if (ret == SOCKET_ERROR) {
    // エラー処理
    freeaddrinfo(paddr);
    return -1;
}

// HTTP送受信(中略)

// 通信の終了とソケットの破棄
shutdown(sock, SD_BOTH);
closesocket(sock);

// アドレス情報の解放
freeaddrinfo(paddr);

 ちなみに、Windows 10のgetaddrinfo()は内部でGetAddrInfoW()を呼び出し、GetAddrInfoW()がWSALookupServiceBegin()/WSALookupServiceNext()/WSALookupServiceEnd()というAPIを使って名前を解決してる様です。この3つのAPIは、Bluetoothデバイスなども列挙できるgetaddrinfo()のさらにすごいヤツの版の様ですが、Bluetooth用途で使った事がないので詳細は知りません。

INT ret;
TCHAR fqdn[] = _T("www.microsoft.com");

AFPROTOCOLS afp[1] = { {AF_INET, IPPROTO_TCP} };
GUID serviceClassId = SVCID_HOSTNAME; // #include <svcguid.h>
WSAQUERYSET querySet{ 0 };
querySet.dwSize = sizeof(WSAQUERYSET);
querySet.lpszServiceInstanceName = fqdn;
querySet.lpServiceClassId = &serviceClassId;
querySet.dwNameSpace = NS_DNS;
querySet.lpafpProtocols = afp;
querySet.dwNumberOfProtocols = sizeof(afp) / sizeof(AFPROTOCOLS);

HANDLE hLookup = INVALID_HANDLE_VALUE;
ret = WSALookupServiceBegin(&querySet, LUP_RETURN_ADDR, &hLookup);
if (ret == SOCKET_ERROR) {
    return -1;
}

DWORD dwLength = sizeof(WSAQUERYSET);
std::vector<char> qs(dwLength);
auto pqs = reinterpret_cast<WSAQUERYSET*>(&qs[0]);
while (true) {
    ret = WSALookupServiceNext(hLookup, 0, &dwLength, pqs);
    if (ret != SOCKET_ERROR) {

        auto paddr = reinterpret_cast<sockaddr_in*>(pqs->lpcsaBuffer->RemoteAddr.lpSockaddr);

        // 解決!

    }
    else {
        auto wsaerr = WSAGetLastError();
        if (wsaerr == WSAEFAULT) {
            qs.resize(dwLength); // バッファが足りないのでリサイズ
            pqs = reinterpret_cast<WSAQUERYSET*>(&qs[0]);
        }
        else if (wsaerr == WSA_E_NO_MORE) {
            break;
        }
        else {
            tcout << "error! " << wsaerr << std::endl;
            break;
        }
    }
} 

WSALookupServiceEnd(hLookup);

 他にもWindowsではWinDNS.hで定義されているDnsQuery()などのAPIでも名前解決できます。しかし、DNSメッセージの生データに近い形での取得になるので、ソケット通信で使う場合には一手間必要です。

auto& tcout = std::wcout;

DNS_RECORD* pResult = nullptr;
DNS_STATUS ret = DnsQuery(_T("www.microsoft.com"), DNS_TYPE_A, DNS_QUERY_STANDARD, nullptr, &pResult, nullptr);
if (ret == 0 && pResult != nullptr) {
    auto p = pResult;
    do {
        if (p->wType == DNS_TYPE_A) {
            // 解決!

            // 文字列に変換して出力
            TCHAR sz[64];
            if (InetNtop(AF_INET, &p->Data.A.IpAddress, sz, _countof(sz)) != nullptr)
                tcout << sz << std::endl;
        }
        p = p->pNext;
    } while (p != nullptr);

    DnsRecordListFree(pResult, DnsFreeRecordList);
}


プログラマーとDNS(1)
プログラマーとDNS(2)
プログラマーとDNS(3)