dns.resolve()だけじゃない!Node.jsでDNSを扱う代替手段と選び方

2025-05-16

以下に、その機能と使い方について詳しく説明します。

dns.resolve() の機能

dns.resolve(hostname, [rrtype], callback) は、以下の目的で使用されます。

  • 非同期処理
    DNSクエリはネットワーク操作なので、dns.resolve()は非同期で実行されます。結果はコールバック関数を通じて返されます。
  • 特定のレコードタイプ
    オプションのrrtype(リソースレコードタイプ)を指定することで、取得したいレコードの種類を絞り込むことができます。例えば、IPアドレス(Aレコード)、メールサーバー(MXレコード)、ネームサーバー(NSレコード)などです。
  • DNSレコードの取得
    指定されたhostname(例えば、google.com)に対応するDNSレコードを問い合わせます。

主要な引数

  • callback (Function): DNS解決が完了したときに呼び出されるコールバック関数。以下の引数を受け取ります。
    • err (Error): エラーが発生した場合にエラーオブジェクトが含まれます。
    • addresses (Array): 解決されたアドレスの配列。rrtypeによって配列の要素の形式が異なります。
  • rrtype (String, オプション): 取得したいDNSレコードのタイプを指定します。指定しない場合、デフォルトで 'A' レコード(IPv4アドレス)が解決されます。
    • 主なrrtypeの例:
      • 'A': IPv4アドレス
      • 'AAAA': IPv6アドレス
      • 'MX': メールエクスチェンジャーレコード(メールサーバー)
      • 'NS': ネームサーバーレコード
      • 'TXT': テキストレコード
      • 'SRV': サービスロケーターレコード
      • 'PTR': ポインターレコード(逆引きDNSルックアップ用)
      • 'CNAME': 正規名レコード
  • hostname (String): 解決したいドメイン名またはホスト名。

addresses の形式

rrtypeによってaddresses配列の要素の形式が変わります。

  • 'SRV'
    サービスレコードのオブジェクトの配列。各オブジェクトはpriority(優先度)、weight(重み)、port(ポート番号)、name(ターゲットホスト名)プロパティを持ちます。
    [
      { priority: 10, weight: 5, port: 5269, name: 'xmpp-server.l.google.com' }
    ]
    
  • 'TXT'
    テキストレコードの配列。各要素は文字列の配列で、テキストレコードが複数の文字列に分割されている場合に対応します。
    [
      ['v=spf1 include:_spf.google.com ~all']
    ]
    
  • 'NS', 'CNAME', 'PTR'
    ホスト名の文字列の配列(例: ['ns1.google.com'])。
  • 'MX'
    メールサーバーのオブジェクトの配列。各オブジェクトはpriority(優先度)とexchange(ホスト名)プロパティを持ちます。
    [
      { priority: 10, exchange: 'alt1.aspmx.l.google.com' },
      { priority: 20, exchange: 'alt2.aspmx.l.google.com' }
    ]
    
  • 'A' および 'AAAA'
    IPアドレスの文字列の配列(例: ['172.217.160.142'])。

使用例

例1: デフォルト(Aレコード)でIPアドレスを解決する

const dns = require('dns');

dns.resolve('example.com', (err, addresses) => {
  if (err) {
    console.error('DNS解決エラー:', err);
    return;
  }
  console.log('example.com のIPアドレス (Aレコード):', addresses);
  // 出力例: example.com のIPアドレス (Aレコード): [ '93.184.216.34' ]
});

例2: MXレコード(メールサーバー)を解決する

const dns = require('dns');

dns.resolve('google.com', 'MX', (err, addresses) => {
  if (err) {
    console.error('DNS解決エラー:', err);
    return;
  }
  console.log('google.com のMXレコード:', addresses);
  /* 出力例:
  google.com のMXレコード: [
    { priority: 10, exchange: 'alt1.aspmx.l.google.com' },
    { priority: 20, exchange: 'alt2.aspmx.l.google.com' },
    { priority: 30, exchange: 'alt3.aspmx.l.google.com' },
    { priority: 40, exchange: 'alt4.aspmx.l.google.com' },
    { priority: 50, exchange: 'aspmx.l.google.com' }
  ]
  */
});

例3: Promiseベースでの使用 (Node.js 10以降のdns.promisesを使用)

dnsモジュールはPromiseベースのAPIも提供しており、非同期処理をより現代的に記述できます。

const dns = require('dns');
const { Resolver } = dns.promises; // PromiseベースのAPIをインポート

const resolver = new Resolver();

async function resolveHost() {
  try {
    const addressesA = await resolver.resolve('nodejs.org', 'A');
    console.log('nodejs.org のIPv4アドレス:', addressesA);

    const addressesNS = await resolver.resolve('nodejs.org', 'NS');
    console.log('nodejs.org のネームサーバー:', addressesNS);
  } catch (err) {
    console.error('DNS解決エラー:', err);
  }
}

resolveHost();

Node.jsにはdns.lookup()という似た機能の関数もありますが、これらには重要な違いがあります。

  • dns.resolve()
    純粋なDNSプロトコルを使用して、ネームサーバーに直接問い合わせを行います。これにより、特定のDNSレコードタイプ(MX, NS, TXTなど)を取得したり、キャッシュや/etc/hostsの影響を受けずに正確なDNS情報を取得したりすることが可能です。

  • dns.lookup()
    オペレーティングシステムが提供する名前解決機能(getaddrinfo)を使用します。主にホスト名をIPアドレスに解決することを目的とし、/etc/hostsファイルやOSのキャッシュなども参照します。DNSレコードタイプを指定することはできません。Webリクエストなどの一般的な目的でホスト名をIPアドレスに解決する場合は、通常dns.lookup()が使われます。



dns.resolve() で発生する一般的なエラーとその意味

dns.resolve()のコールバック関数で返されるerrオブジェクトは、通常、以下のようなエラーコードを持ちます。

    • 意味
      指定されたホスト名に対するDNSエントリが見つからない場合に発生します。これは、ドメイン名が存在しない、タイプミスがある、またはDNSサーバーがそのドメイン名を解決できないことを示します。

    • dns.resolve('nonexistent-domain12345.com', ...)
  1. EDEFENSA (DNS server did not return results)

    • 意味
      DNSサーバーがリクエストに応答しなかった、または無効な応答を返したことを示します。DNSサーバーの問題やネットワークの問題が考えられます。
  2. EFORMERR (DNS server returned a malformed response)

    • 意味
      DNSサーバーからの応答の形式が不正である場合に発生します。これは、DNSサーバーの実装に問題があるか、ネットワークの途中でデータが破損した可能性があります。
  3. ESERVFAIL (DNS server encountered an internal error)

    • 意味
      DNSサーバーが内部エラーを報告した場合に発生します。これは、DNSサーバーが一時的に問題を抱えていることを示します。
  4. ENODATA (No data for the requested record type)

    • 意味
      ホスト名自体は存在するが、指定されたrrtype(レコードタイプ、例: 'MX', 'NS'など)のレコードが見つからない場合に発生します。例えば、Webサイトは存在するが、メールサーバー(MXレコード)が設定されていない場合などです。

    • dns.resolve('example.com', 'SRV', ...)example.comにSRVレコードがない場合。
  5. ETIMEOUT (DNS query timed out)

    • 意味
      DNSクエリが指定された時間内に応答しなかった場合に発生します。ネットワークの遅延、DNSサーバーの負荷、またはファイアウォールによるブロックなどが原因で発生します。
  6. ECONNREFUSED (Connection refused by DNS server)

    • 意味
      DNSサーバーが接続を拒否した場合に発生します。これは、DNSサーバーが停止している、またはファイアウォールによって接続がブロックされている可能性があります。
  7. EAI_AGAIN (Temporary failure in name resolution)

    • 意味
      名前解決の一時的な失敗を示します。通常、DNSサーバーが一時的に過負荷になっているか、ネットワーク接続に一時的な問題がある場合に発生します。再試行することで解決する可能性があります。

dns.resolve()でエラーが発生した場合、以下の手順でトラブルシューティングを試みてください。

  1. ホスト名の確認

    • 指定したhostnameが正しいか、スペルミスがないかを確認します。
    • もし、内部ネットワークのホスト名を解決しようとしている場合は、そのホスト名が外部DNSではなく、適切な内部DNSサーバーで解決できるかを確認します。
  2. インターネット接続の確認

    • サーバー自体がインターネットに接続されていることを確認します。簡単なping google.comcurl https://example.comなどで確認できます。
  3. DNSサーバーの確認

    • システムが使用しているDNSサーバー
      • Linux/macOS: /etc/resolv.conf ファイルを確認し、設定されているネームサーバー(nameserverエントリ)を確認します。
      • Windows: ネットワークアダプターの設定でDNSサーバーを確認します。
    • DNSサーバーの疎通確認
      • システムが使用しているDNSサーバーのIPアドレスに対してpingを実行し、到達可能か確認します。
      • nslookupdigコマンドを使用して、同じドメイン名をそのDNSサーバーに直接問い合わせてみます。
        • 例: nslookup example.com 8.8.8.8 (Google Public DNSを使用)
        • 例: dig example.com MX @8.8.8.8 (Google Public DNSに対してMXレコードを問い合わせる)
      • もし、dns.resolve()が機能しないのにnslookupdigが機能する場合、Node.jsの環境設定や、OSのDNSキャッシュなどが影響している可能性があります。
  4. rrtypeの確認 (ENODATAの場合)

    • ENODATAエラーの場合、指定したレコードタイプ(rrtype)がそのドメイン名に存在しないことを意味します。
    • 例えば、Webサイトのドメインに対してMXレコードを問い合わせているのにメールサービスを使用していない場合、ENODATAが返されるのは正常です。
    • 本当にそのレコードタイプが必要なのか、または別のレコードタイプを問い合わせるべきかを確認します。
  5. ファイアウォールとセキュリティグループの確認

    • サーバーのファイアウォール(iptables, Windows Firewallなど)や、クラウドプロバイダーのセキュリティグループ(AWS Security Groups, Azure Network Security Groupsなど)が、DNSクエリ(通常、UDP/TCPポート53)をブロックしていないか確認します。
    • アウトバウンドのDNS通信が許可されていることを確認します。
  6. Node.jsのバージョンとDNSキャッシュ

    • Node.jsはデフォルトでOSのDNSキャッシュを利用しますが、独自のDNSキャッシュを持つミドルウェアやライブラリを使用している場合は、そのキャッシュが古い情報を持っている可能性があります。
    • Node.jsのバージョンによっては、DNS解決の挙動が異なる場合があります。最新の安定版にアップデートしてみることも検討します。
    • Node.js自体はDNS結果をキャッシュしませんが、libuvスレッドプールで同期的にDNSルックアップを行うため、大量の同時DNSリクエストはパフォーマンスボトルネックになる可能性があります。
  7. dns.setServers()の使用 (上級者向け)

    • もし、特定のDNSサーバーを使用したい場合、dns.setServers(['8.8.8.8', '8.8.4.4'])のように設定することで、Node.jsが使用するDNSサーバーを明示的に指定できます。これにより、OSのDNS設定とは異なるDNSサーバーを使用できます。ただし、これを設定すると、OSの/etc/resolv.confなどが参照されなくなるため、注意が必要です。
  8. PromiseベースのAPI (dns.promises) の利用

    • コールバック地獄を避けるためにも、Node.js v10以降で導入されたdns.promises APIの使用を検討してください。エラーハンドリングがtry-catchで一元化され、コードの可読性が向上します。

    <!-- end list -->

    const { Resolver } = require('dns').promises;
    const resolver = new Resolver();
    
    async function resolveDomain(domain, type) {
      try {
        const addresses = await resolver.resolve(domain, type);
        console.log(`<span class="math-inline">\{domain\} \(</span>{type}) の解決結果:`, addresses);
        return addresses;
      } catch (err) {
        console.error(`<span class="math-inline">\{domain\} \(</span>{type}) のDNS解決エラー:`, err.code, err.message);
        throw err; // エラーを再スローして上位で処理させる
      }
    }
    
    resolveDomain('example.com', 'A');
    resolveDomain('nonexistent-domain12345.com', 'A'); // ENOTFOUND が期待される
    resolveDomain('google.com', 'MX');
    
  9. 環境固有の問題

    • Dockerコンテナ内で実行している場合、コンテナのDNS設定がホストと異なる場合があります。Dockerの--dnsオプションやdocker-compose.ymlでDNSサーバーを指定できます。
    • VPN接続中は、DNS解決の挙動が変わることがあります。


基本的な使用法: Aレコード(IPv4アドレス)の解決

最も一般的な使用例は、ドメイン名からIPv4アドレス(Aレコード)を取得することです。rrtypeを省略すると、デフォルトでAレコードが解決されます。

// resolve_a_record.js
const dns = require('dns');

const hostname = 'example.com';

dns.resolve(hostname, (err, addresses) => {
  if (err) {
    console.error(`Error resolving ${hostname}:`, err.code, err.message);
    return;
  }
  console.log(`${hostname} のIPv4アドレス (Aレコード):`);
  addresses.forEach(addr => console.log(`- ${addr}`));
});

/*
実行結果例:
example.com のIPv4アドレス (Aレコード):
- 93.184.216.34
*/

特定のレコードタイプの解決

dns.resolve()の第2引数rrtypeに、解決したいDNSレコードのタイプを指定します。

AAAAレコード(IPv6アドレス)の解決

// resolve_aaaa_record.js
const dns = require('dns');

const hostname = 'ipv6.google.com'; // IPv6アドレスを持つドメイン

dns.resolve(hostname, 'AAAA', (err, addresses) => {
  if (err) {
    console.error(`Error resolving ${hostname} (AAAA):`, err.code, err.message);
    return;
  }
  console.log(`${hostname} のIPv6アドレス (AAAAレコード):`);
  addresses.forEach(addr => console.log(`- ${addr}`));
});

/*
実行結果例:
ipv6.google.com のIPv6アドレス (AAAAレコード):
- 2404:6800:4003:c01::67
*/

MXレコード(メールエクスチェンジャー)の解決

メールの送受信に使用されるメールサーバーの情報を取得します。addresses配列の各要素は、priority(優先度)とexchange(メールサーバーのホスト名)プロパティを持つオブジェクトです。

// resolve_mx_record.js
const dns = require('dns');

const hostname = 'google.com';

dns.resolve(hostname, 'MX', (err, addresses) => {
  if (err) {
    console.error(`Error resolving ${hostname} (MX):`, err.code, err.message);
    return;
  }
  console.log(`${hostname} のMXレコード:`);
  addresses.sort((a, b) => a.priority - b.priority); // 優先度でソート
  addresses.forEach(record => {
    console.log(`- Priority: ${record.priority}, Exchange: ${record.exchange}`);
  });
});

/*
実行結果例:
google.com のMXレコード:
- Priority: 5, Exchange: gmail-smtp-in.l.google.com
- Priority: 10, Exchange: alt1.gmail-smtp-in.l.google.com
- Priority: 20, Exchange: alt2.gmail-smtp-in.l.google.com
- Priority: 30, Exchange: alt3.gmail-smtp-in.l.google.com
- Priority: 40, Exchange: alt4.gmail-smtp-in.l.google.com
*/

NSレコード(ネームサーバー)の解決

ドメインの権威ネームサーバーの情報を取得します。

// resolve_ns_record.js
const dns = require('dns');

const hostname = 'nodejs.org';

dns.resolve(hostname, 'NS', (err, addresses) => {
  if (err) {
    console.error(`Error resolving ${hostname} (NS):`, err.code, err.message);
    return;
  }
  console.log(`${hostname} のNSレコード:`);
  addresses.forEach(ns => console.log(`- ${ns}`));
});

/*
実行結果例:
nodejs.org のNSレコード:
- ns1.p31.dynect.net
- ns2.p31.dynect.net
- ns3.p31.dynect.net
- ns4.p31.dynect.net
*/

TXTレコード(テキスト)の解決

SPFレコードやDKIMレコードなど、任意のテキスト情報を取得します。addresses配列の各要素は、テキストレコードが複数の文字列に分割されている場合に対応するため、文字列の配列となります。

// resolve_txt_record.js
const dns = require('dns');

const hostname = 'google.com';

dns.resolve(hostname, 'TXT', (err, addresses) => {
  if (err) {
    console.error(`Error resolving ${hostname} (TXT):`, err.code, err.message);
    return;
  }
  console.log(`${hostname} のTXTレコード:`);
  addresses.forEach(txtArray => {
    console.log(`- "${txtArray.join('')}"`); // 配列の要素を結合して表示
  });
});

/*
実行結果例(一部抜粋、長いので簡略化):
google.com のTXTレコード:
- "v=spf1 include:_spf.google.com ~all"
- "globalsign-smime-dv=CDGSJtTq01C_X_t2q0c2V_3k213jL"
- "docusign-6735508c-9828-4034-9658-00a8a6582500"
...
*/

SRVレコード(サービスロケーター)の解決

特定のサービスが動作しているホストとポート情報を取得します。各要素は、priority, weight, port, nameプロパティを持つオブジェクトです。

// resolve_srv_record.js
const dns = require('dns');

// 例: Microsoft Lync/Skype for Business の SRV レコード (_sip._tls)
// 実際の環境に合わせてドメイン名を変更してください
const serviceName = '_sip._tls.lync.com'; // 例として、一般的に存在するSRVレコード

dns.resolve(serviceName, 'SRV', (err, addresses) => {
  if (err) {
    console.error(`Error resolving ${serviceName} (SRV):`, err.code, err.message);
    // ENODATA がよく出るレコードタイプなので、その場合は特にメッセージを出す
    if (err.code === 'ENODATA') {
      console.log(`Note: ${serviceName} はSRVレコードを持っていないか、存在しません。`);
    }
    return;
  }
  console.log(`${serviceName} のSRVレコード:`);
  addresses.sort((a, b) => a.priority - b.priority || a.weight - b.weight); // 優先度と重みでソート
  addresses.forEach(record => {
    console.log(`- Priority: ${record.priority}, Weight: ${record.weight}, Port: ${record.port}, Name: ${record.name}`);
  });
});

/*
実行結果例(サービスが存在する場合):
_sip._tls.lync.com のSRVレコード:
- Priority: 0, Weight: 0, Port: 443, Name: sipfed.online.lync.com
*/

エラーハンドリングの重要性

dns.resolve()は非同期であるため、エラーハンドリングはコールバック関数内で行う必要があります。特に、ドメインが存在しない、ネットワークの問題、DNSサーバーの問題など、様々な理由でエラーが発生する可能性があります。

// resolve_error_handling.js
const dns = require('dns');

const nonexistentDomain = 'this-domain-does-not-exist-123456789.com';
const existingDomainNoMx = 'example.com'; // MXレコードがない可能性があるドメイン

// 存在しないドメインのAレコードを解決
dns.resolve(nonexistentDomain, 'A', (err, addresses) => {
  if (err) {
    console.error(`--- ${nonexistentDomain} の解決エラー ---`);
    console.error(`エラーコード: ${err.code}`);
    console.error(`メッセージ: ${err.message}`);
    // ENOTFOUND は一般的なエラー
    if (err.code === 'ENOTFOUND') {
      console.error('このドメインは存在しないか、スペルミスがあります。');
    }
    return;
  }
  console.log(`${nonexistentDomain} のIPアドレス:`, addresses);
});

// 存在するが特定のレコードタイプがないドメイン
dns.resolve(existingDomainNoMx, 'SRV', (err, addresses) => {
  if (err) {
    console.error(`\n--- ${existingDomainNoMx} のSRV解決エラー ---`);
    console.error(`エラーコード: ${err.code}`);
    console.error(`メッセージ: ${err.message}`);
    // ENODATA は指定したレコードタイプが見つからない場合に発生
    if (err.code === 'ENODATA') {
      console.error(`Note: ${existingDomainNoMx} にはSRVレコードが設定されていないようです。`);
    }
    return;
  }
  console.log(`${existingDomainNoMx} のSRVレコード:`, addresses);
});

Node.js v10以降では、dnsモジュールにPromiseベースのAPIが追加され、非同期コードをより扱いやすくなりました。async/awaitと組み合わせることで、コールバック関数よりも直感的にコードを記述できます。

// resolve_promises.js
const dns = require('dns');
const { Resolver } = dns.promises; // PromiseベースのAPIをインポート

const resolver = new Resolver(); // Resolverインスタンスを作成

async function performDnsResolutions() {
  const domains = [
    { hostname: 'google.com', type: 'A' },
    { hostname: 'bing.com', type: 'MX' },
    { hostname: 'github.com', type: 'NS' },
    { hostname: 'nonexistent.example.com', type: 'A' } // エラーを発生させる例
  ];

  for (const domain of domains) {
    try {
      console.log(`\n--- ${domain.hostname} (${domain.type}) の解決 ---`);
      const addresses = await resolver.resolve(domain.hostname, domain.type);
      console.log('解決結果:', addresses);
    } catch (err) {
      console.error(`エラー発生: ${domain.hostname} (${domain.type})`);
      console.error(`  コード: ${err.code}`);
      console.error(`  メッセージ: ${err.message}`);
    }
  }
}

performDnsResolutions();

/*
実行結果例(一部抜粋):

--- google.com (A) の解決 ---
解決結果: [ '142.250.199.14', '142.250.199.46' ]

--- bing.com (MX) の解決 ---
解決結果: [
  { priority: 10, exchange: 'mx3.hotmail.com' },
  { priority: 10, exchange: 'mx4.hotmail.com' },
  { priority: 10, exchange: 'mx1.hotmail.com' },
  { priority: 10, exchange: 'mx2.hotmail.com' }
]

--- github.com (NS) の解決 ---
解決結果: [ 'ns1.p31.dynect.net', 'ns2.p31.dynect.net', 'ns3.p31.dynect.net', 'ns4.p31.dynect.net' ]

--- nonexistent.example.com (A) の解決 ---
エラー発生: nonexistent.example.com (A)
  コード: ENOTFOUND
  メッセージ: getaddrinfo ENOTFOUND nonexistent.example.com
*/


これは dns.resolve() と混同されがちですが、機能が異なります。