【Node.js】dns.resolveSrv()でのエラー解決ガイド:よくある問題と対処法

2025-05-27

SRVレコードとは?

SRVレコードは、特定のサービスを提供するサーバーのホスト名とポート番号をDNS上で公開するためのレコードタイプです。例えば、IP電話(SIP)やXMPP(Jabber)などのサービスでは、クライアントがサービスに接続する際にどのサーバーとポートを使用すべきかをSRVレコードで発見することが一般的です。

dns.resolveSrv() の使い方

dns.resolveSrv()メソッドの基本的な書式は以下の通りです。

dns.resolveSrv(hostname, callback)
  • callback: DNSクエリが完了したときに呼び出されるコールバック関数。以下の引数を受け取ります。
    • err: エラーが発生した場合にErrorオブジェクトが含まれます。
    • addresses: SRVレコードの情報を格納したオブジェクトの配列。各オブジェクトには以下のプロパティが含まれます。
      • priority: 優先度。値が小さいほど優先度が高い。
      • weight: 同じ優先度のレコード間での重み。
      • port: サービスが提供されているポート番号。
      • name: サービスを提供するターゲットホスト名。
  • hostname: 解決したいホスト名(文字列)。通常、_service._protocol.domain.com のような形式で指定します。例えば、SIPサービスであれば _sip._tcp.example.com のようになります。
const dns = require('dns');

// _sip._tcp.example.com のSRVレコードを解決する例
dns.resolveSrv('_sip._tcp.example.com', (err, addresses) => {
  if (err) {
    console.error('SRVレコードの解決中にエラーが発生しました:', err);
    return;
  }

  console.log('SRVレコード:', addresses);
  // addresses の出力例:
  // [
  //   { priority: 10, weight: 5, port: 5060, name: 'sip-server1.example.com' },
  //   { priority: 20, weight: 5, port: 5060, name: 'sip-server2.example.com' }
  // ]
});

// promises API を使用した例 (Node.js v10.0.0以降)
const { promises: dnsPromises } = require('dns');

async function resolveSrvRecords() {
  try {
    const addresses = await dnsPromises.resolveSrv('_xmpp-client._tcp.jabber.org');
    console.log('XMPP SRVレコード:', addresses);
  } catch (err) {
    console.error('XMPP SRVレコードの解決中にエラーが発生しました:', err);
  }
}

resolveSrvRecords();


一般的なDNS解決エラー

dns.resolveSrv()を含むNode.jsのDNS関連メソッドで発生しうる一般的なエラーは、errオブジェクトのcodeプロパティで識別できます。

エラーコードと意味

  • ネットワーク関連のエラー (例: ECONNREFUSED, EHOSTUNREACH): ネットワーク接続自体に問題がある場合に発生します。DNSサーバーに到達できない可能性があります。
  • dns.FORMERR (EFORMERR): DNSクエリが誤ってフォーマットされている場合に発生します。これはNode.js内部の問題か、非常にまれなケースです。
  • dns.REFUSED (EREFUSED): DNSサーバーがクエリを拒否した場合に発生します。これは通常、DNSサーバーが特定のクエリを許可していない、またはアクセス制限がある場合に起こります。
  • dns.TIMEOUT (ETIMEOUT): DNSクエリがタイムアウトした場合に発生します。DNSサーバーが応答しない、またはネットワーク接続が不安定な場合に起こりえます。
  • dns.SERVFAIL (ESERVFAIL): DNSサーバーがクエリを処理できなかった場合に発生します。これはDNSサーバー自身の一時的な問題や設定ミスが原因であることがあります。
  • dns.NODATA (ENODATA): ドメイン名自体は存在するが、指定されたレコードタイプ(この場合はSRVレコード)が見つからなかった場合に発生します。
    • SRVレコードのホスト名が間違っている可能性があります。(例: _sip._tcp.example.com の代わりに example.com を指定しているなど)
    • そのドメインにSRVレコードが設定されていない可能性があります。
  • dns.NOTFOUND (ENOTFOUND): 指定されたドメイン名(またはSRVレコードのホスト名)が見つからない、つまりDNSに登録されていない場合に発生します。これはタイプミスや、そもそも存在しないドメイン名を指定している可能性が高いです。

トラブルシューティング(共通)

  1. ホスト名の確認: まず、dns.resolveSrv()に渡しているホスト名が正しいか、タイプミスがないかを徹底的に確認してください。SRVレコードの場合、_service._protocol.domain.com の形式が非常に重要です。
  2. ネットワーク接続の確認:
    • アプリケーションが動作しているサーバーから外部へのネットワーク接続が正常かを確認します(例: ping google.com)。
    • DNSサーバーが適切に設定されているかを確認します。通常、/etc/resolv.conf (Linux/macOS) やネットワークアダプターの設定 (Windows) を確認します。
  3. DNSサーバーの確認:
    • dignslookup コマンドを使用して、問題のSRVレコードが実際に解決できるかを確認します。
      dig SRV _sip._tcp.example.com @your_dns_server
      nslookup -type=SRV _sip._tcp.example.com your_dns_server
      
      your_dns_server を省略すると、システムのデフォルトDNSサーバーが使われます。
    • もし特定のDNSサーバーで解決できない場合は、別の信頼できるDNSサーバー(例: Google Public DNS 8.8.8.8 や Cloudflare DNS 1.1.1.1)を指定して試してみます。
    • Node.jsのdns.setServers()を使って、プログラム内で一時的にDNSサーバーを変更して試すこともできます。
      const dns = require('dns');
      dns.setServers(['8.8.8.8', '1.1.1.1']); // Google Public DNS を設定
      dns.resolveSrv('_sip._tcp.example.com', (err, addresses) => {
        // ...
      });
      
  4. 対象ドメインのDNS設定を確認: SRVレコードが設定されているはずなのにENODATANOTFOUNDが出る場合は、そのドメインのDNSゾーンファイル自体にSRVレコードが正しく設定されているか、またはDNS伝播が完了しているかを確認してください。DNSの変更は反映に時間がかかる場合があります。
  5. DNSサーバーの負荷/応答遅延: ETIMEOUTが頻繁に発生する場合は、使用しているDNSサーバーが過負荷状態にあるか、地理的に遠いなどの理由で応答が遅れている可能性があります。

dns.resolveSrv()特有の考慮事項とトラブルシューティング

SRVレコードの形式の誤り

  • 解決策:
    • サービスとプロトコルの正しい命名規則を確認します(例: SIPは通常_sip._tcpまたは_sip._udp)。
    • 対象のサービスが使用するSRVレコードのホスト名の正確な形式をドキュメント等で確認してください。
  • 問題: _service._protocol.domain.com の形式を間違えている。よくある間違いは、サービス名やプロトコル名のアンダースコア(_)を忘れる、または存在しないサービス名やプロトコル名を使用することです。

SRVレコードの優先度と重み

  • 解決策: アプリケーション側で、返されたaddresses配列の各要素をpriorityでソートし、同じpriorityの要素間ではweightに基づいて処理ロジックを実装する必要があります。
  • 問題: dns.resolveSrv()はSRVレコードをpriority(優先度)とweight(重み)に基づいてソートして返しません。返された配列の順序は保証されません。

非同期処理と同期的なボトルネック(上級者向け)

Node.jsのDNSモジュールは非同期APIを提供していますが、内部的にはlibuvのスレッドプールで同期的なDNS解決を実行しています。

  • 解決策:
    • DNSクエリを多用する設計を見直し、可能な限りDNSルックアップの結果をキャッシュすることを検討します。カスタムのDNSキャッシング層を実装するか、既存のライブラリを使用します。
    • DNSクエリのレートリミットを設ける。
    • Node.jsのdns.Resolverクラスを使用して、カスタムのDNS設定やより高度な制御を行うことも検討できますが、これは複雑になります。
  • 問題: 短時間に多数のDNSクエリ(dns.resolveSrv()を含む)を並行して実行すると、libuvスレッドプール(デフォルトで4スレッド)を使い果たし、Node.jsアプリケーション全体のI/O処理(ファイルI/Oなど)もブロックされる可能性があります。これにより、アプリケーションが遅くなったり、タイムアウトが発生したりすることがあります。
  • 外部ツールとの比較: dignslookupなどのコマンドラインツールで同じSRVレコードを解決してみて、Node.jsの結果と比較することが非常に有効です。これにより、DNSサーバー側の問題か、Node.jsアプリケーション側の問題かを切り分けることができます。
  • ログ出力: dns.resolveSrv()の呼び出し前後にログを出力し、どのホスト名を解決しようとしているか、エラーが発生したタイミングなどを追跡できるようにします。
  • エラーオブジェクトの詳細確認: errオブジェクトには、エラーコード(err.code)だけでなく、追加情報が含まれている場合があります。console.error(err)で全体を出力し、詳細を確認してください。


基本的な使い方 (Callback-based)

最も基本的な使用例です。指定したホスト名のSRVレコードを解決し、結果をコールバック関数で受け取ります。

const dns = require('dns');

// 例: Google Talk (XMPP) クライアントサービスのSRVレコードを解決
const hostname = '_xmpp-client._tcp.gmail.com';

dns.resolveSrv(hostname, (err, addresses) => {
  if (err) {
    console.error(`SRVレコード解決エラー (${hostname}):`, err.message);
    // エラーコードによって分岐することも可能
    if (err.code === 'ENOTFOUND') {
      console.error('指定されたホスト名が見つかりません。タイプミスを確認してください。');
    } else if (err.code === 'ENODATA') {
      console.error('ホスト名は存在しますが、SRVレコードが見つかりません。');
    }
    return;
  }

  console.log(`SRVレコード (${hostname}):`);
  if (addresses.length === 0) {
    console.log('  SRVレコードは見つかりませんでした。');
  } else {
    // 取得したSRVレコードをログに出力
    addresses.forEach((addr, index) => {
      console.log(`  [${index + 1}]`);
      console.log(`    ターゲット: ${addr.name}`);
      console.log(`    ポート: ${addr.port}`);
      console.log(`    優先度: ${addr.priority}`);
      console.log(`    重み: ${addr.weight}`);
    });
  }
});

console.log('SRV解決のリクエストを送信しました...');

解説

  • 成功した場合、addresses配列にSRVレコードのオブジェクトが含まれます。各オブジェクトはname, port, priority, weightプロパティを持ちます。
  • エラーが発生した場合、errオブジェクトにエラー情報が含まれます。err.codeでエラーの種類を判断できます。
  • dns.resolveSrv(hostname, callback) の形式で呼び出します。

Promises API を使用した現代的な書き方 (Async/Await)

Node.js v10.0.0以降では、dns.promisesを使用することで、async/await構文を使ったよりクリーンなコードを書くことができます。

const { promises: dnsPromises } = require('dns');

// 例: Microsoft Teams (SIP) サービスのSRVレコードを解決
const hostname = '_sipfederationtls._tcp.lync.com'; // SIPフェデレーション用

async function resolveSrvRecords(host) {
  try {
    console.log(`SRVレコードを解決中: ${host}...`);
    const addresses = await dnsPromises.resolveSrv(host);

    console.log(`SRVレコード (${host}):`);
    if (addresses.length === 0) {
      console.log('  SRVレコードは見つかりませんでした。');
    } else {
      // 取得したSRVレコードをログに出力
      // 優先度と重みでソートすることも可能 (後述)
      addresses.forEach((addr, index) => {
        console.log(`  [${index + 1}]`);
        console.log(`    ターゲット: ${addr.name}`);
        console.log(`    ポート: ${addr.port}`);
        console.log(`    優先度: ${addr.priority}`);
        console.log(`    重み: ${addr.weight}`);
      });
    }
  } catch (error) {
    console.error(`SRVレコード解決エラー (${host}):`, error.message);
    if (error.code === 'ENOTFOUND') {
      console.error('  指定されたホスト名またはSRVレコードが見つかりません。');
    } else if (error.code === 'ENODATA') {
      console.error('  ホスト名は存在しますが、SRVレコードがありません。');
    } else if (error.code === 'ETIMEOUT') {
      console.error('  DNSクエリがタイムアウトしました。ネットワーク接続を確認してください。');
    }
  }
}

resolveSrvRecords(hostname);
resolveSrvRecords('_sip._tcp.example.com'); // 存在しない可能性のあるドメインの例

解説

  • エラーはtry...catchブロックで捕捉します。
  • await dnsPromises.resolveSrv(host) で非同期処理の結果を待ちます。
  • require('dns').promises を使ってdnsPromisesオブジェクトを取得します。

取得したSRVレコードのソートと優先順位付け

SRVレコードはpriority(優先度)とweight(重み)という概念を持ちます。通常、priorityが低いほど優先され、同じpriorityのレコード間ではweightが高いほど優先されます(ただし、クライアントは通常weightを考慮してランダムに選択します)。dns.resolveSrv()はソートされていない配列を返すため、アプリケーション側でソートして利用するのが一般的です。

const { promises: dnsPromises } = require('dns');

const hostname = '_ldap._tcp.gc._msdcs.contoso.com'; // Active Directory LDAPの例

async function resolveAndSortSrvRecords(host) {
  try {
    console.log(`SRVレコードを解決中 (ソートあり): ${host}...`);
    const addresses = await dnsPromises.resolveSrv(host);

    if (addresses.length === 0) {
      console.log('  SRVレコードは見つかりませんでした。');
      return;
    }

    // 優先度 (priority) で昇順、次に重み (weight) で降順にソート
    // priorityが低いほど優先度が高い
    // 同じpriorityの場合、weightが高いほど優先される (クライアントがランダム選択する際の重み)
    addresses.sort((a, b) => {
      if (a.priority !== b.priority) {
        return a.priority - b.priority;
      }
      return b.weight - a.weight; // 同じ優先度の場合は重みで降順
    });

    console.log(`ソートされたSRVレコード (${host}):`);
    addresses.forEach((addr, index) => {
      console.log(`  [${index + 1}]`);
      console.log(`    ターゲット: ${addr.name}`);
      console.log(`    ポート: ${addr.port}`);
      console.log(`    優先度: ${addr.priority}`);
      console.log(`    重み: ${addr.weight}`);
    });

    // 最も優先されるサーバーに接続する例
    if (addresses.length > 0) {
      const primaryServer = addresses[0];
      console.log(`\n最も優先されるサーバー: ${primaryServer.name}:${primaryServer.port}`);
      // ここで、例えばソケット接続を試みるなどの処理に進む
    }

  } catch (error) {
    console.error(`SRVレコード解決エラー (${host}):`, error.message);
  }
}

resolveAndSortSrvRecords(hostname);

解説

  • priorityが同じ場合は、weightが大きい方が前に来るように降順でソートします。これは、クライアントが複数のサーバーから選択する際に、重み付けされたロードバランシングを行うためのものです。
  • priorityが異なる場合は、小さい方が前に来るように昇順でソートします。
  • addresses.sort()メソッドを使って、カスタムの比較関数を渡しています。

デフォルトのシステムDNSサーバーではなく、特定のDNSサーバー(例: Google Public DNSやISPのDNS)を使用して解決したい場合。

const dns = require('dns');
const { Resolver } = dns;

const hostname = '_sip._tcp.example.com'; // 例のドメイン

// 新しいResolverインスタンスを作成
const resolver = new Resolver();
// 任意のDNSサーバーを設定
resolver.setServers(['8.8.8.8', '8.8.4.4']); // Google Public DNS

resolver.resolveSrv(hostname, (err, addresses) => {
  if (err) {
    console.error(`カスタムDNSサーバーでのSRVレコード解決エラー (${hostname}):`, err.message);
    return;
  }
  console.log(`カスタムDNSサーバー (${resolver.getServers().join(', ')}) で解決したSRVレコード (${hostname}):`);
  addresses.forEach((addr, index) => {
    console.log(`  [${index + 1}] ${addr.name}:${addr.port} (Priority: ${addr.priority}, Weight: ${addr.weight})`);
  });
});

// デフォルトのDNSサーバー設定を元に戻すことも可能
// dns.setServers(originalServers);
  • このresolverインスタンスのresolveSrv()メソッドを使用します。
  • resolver.setServers()でDNSサーバーのIPアドレスを配列で指定します。
  • dns.Resolverクラスのインスタンスを作成することで、独自のDNSサーバー設定を持つリゾルバを作成できます。


外部DNSクライアントライブラリの使用

Node.jsの標準dnsモジュールは比較的シンプルな機能を提供しますが、より高度なDNS機能(例えば、特定のDNSSEC検証、DANE TLSAレコード、より詳細なDNSプロトコル制御、または非標準的なDNSレコードタイプ)が必要な場合、専用のDNSクライアントライブラリを検討することがあります。

例: dns-packetnative-dns (あまり活発ではない)

厳密にはresolveSrvの直接的な代替というよりは、より低レベルでDNSクエリを構築・パースするためのライブラリです。これらを使うことで、dns.resolveSrv()が提供しないようなカスタムなSRVレコードクエリや、他のレコードタイプとの組み合わせ、DNSサーバーへの直接的なUDP/TCP接続など、高度な処理を実装できます。

ただし、これらのライブラリは通常、より多くのコードとDNSプロトコルに関する深い知識を必要とします。

メリット

  • Node.jsの標準ライブラリではサポートされていない、より高度なDNS機能を利用できる可能性がある。
  • DNSプロトコルに対する低レベルな制御が可能。

デメリット

  • 多くの場合、SRVレコードを解決するだけであればオーバーキル。
  • ライブラリによってはメンテナンスが活発でない場合がある。
  • 実装が複雑になりがちで、DNSプロトコルの知識が必須。

使用場面

  • カスタムのDNSサーバー実装や、DNSプロトコルに関する研究開発を行う場合。
  • Node.jsのdnsモジュールが提供しない特定のDNSレコードタイプを解決する必要がある場合。

厳密にはdns.resolveSrv()の代替ではありませんが、ごく一部のレガシーなシステムでは、SRVレコードを直接解決する代わりに、SRVレコードが示すサービスのエンドポイント(ホスト名とポート)が別の方法で提供される場合があります。