JavaScript DNS解決:Node.js dns.resolveTxt() の代替手法と活用事例

2025-05-16

基本的な機能

  • 非同期処理
    dns.resolveTxt() は非同期的に動作します。これは、DNS サーバーへの問い合わせ処理がバックグラウンドで行われ、Node.js のイベントループをブロックしないことを意味します。処理が完了すると、コールバック関数が呼び出され、結果が渡されます。

関数の形式

dns.resolveTxt(hostname, callback)
  • callback: DNS 問い合わせが完了した後に呼び出される関数です。このコールバック関数は、通常 2 つの引数を受け取ります。
    • err: エラーが発生した場合 (例: ホスト名が存在しない、DNS サーバーに接続できないなど) に、Error オブジェクトが渡されます。成功した場合は null になります。
    • records: DNS 問い合わせの結果として得られた TXT レコードの配列です。各要素は、TXT レコードの内容を表す文字列の配列となります。例えば、['v=spf1 ...', 'k=rsa ...'] のような形式です。
  • hostname: 問い合わせるホスト名 (例: 'example.com', 'mail.google.com') を文字列で指定します。

使用例

const dns = require('node:dns').promises;

async function resolveTXT(hostname) {
  try {
    const records = await dns.resolveTxt(hostname);
    console.log(`TXT レコード (${hostname}):`, records);
    records.forEach(record => {
      console.log('  -', record.join(' ')); // 複数の文字列が連結されている場合があるので join します
    });
  } catch (err) {
    console.error(`TXT レコードの解決に失敗しました (${hostname}):`, err);
  }
}

resolveTXT('example.com');
resolveTXT('mail.google.com');
  1. require('node:dns').promises;: dns モジュールをインポートし、Promise ベースの API を使用しています。これにより、async/await を使って非同期処理をより簡潔に記述できます。
  2. resolveTXT 関数は、指定された hostname の TXT レコードを解決する非同期関数です。
  3. dns.resolveTxt(hostname): 指定されたホスト名の TXT レコードを DNS サーバーに問い合わせます。Promise が返されるため、await で処理の完了を待ちます。
  4. try...catch: DNS 解決に失敗した場合のエラー処理を行います。
  5. console.log(): 取得した TXT レコードの内容をコンソールに出力します。TXT レコードは複数の文字列に分割されている場合があるため、.join(' ') を使って連結しています。
  • Promise ベースの API (dns.promises.resolveTxt()) を使うことで、コールバック地獄を回避し、よりモダンな非同期処理の記述が可能です。
  • エラー処理は重要です。DNS 問い合わせはネットワークに依存するため、様々な理由で失敗する可能性があります。
  • dns.resolveTxt() は、システムの設定に基づいた DNS サーバーを使用します。


一般的なエラー

    • 原因
      指定された hostname が存在しないか、DNS サーバーによって解決できない場合に発生します。スペルミス、存在しないドメイン名、または一時的な DNS サーバーの問題などが考えられます。
    • トラブルシューティング
      • ホスト名のスペルが正しいか再度確認してください。
      • ping コマンドや nslookup コマンドなどを使って、指定されたホスト名が正しく解決できるか確認してください。
      • ネットワーク接続に問題がないか確認してください。
      • 一時的な DNS サーバーの問題である可能性もあるため、しばらく待ってから再度試してみてください。
  1. Error: queryTxt ENODEV (適切なネットワークデバイスが見つかりません)

    • 原因
      ネットワークインターフェースが利用できない状態である場合に発生することがあります。
    • トラブルシューティング
      • ネットワークケーブルが正しく接続されているか確認してください。
      • Wi-Fi 接続が正常に確立されているか確認してください。
      • OS のネットワーク設定を確認し、ネットワークアダプターが有効になっているか確認してください。
  2. Error: queryTxt ECONNREFUSED <address>:<port> (接続が拒否されました)

    • 原因
      DNS サーバーへの接続が拒否された場合に発生します。ファイアウォールが DNS リクエストをブロックしている、DNS サーバーがダウンしている、またはポート (通常は 53番) が閉じているなどが考えられます。
    • トラブルシューティング
      • ファイアウォールの設定を確認し、DNS (UDP/TCP ポート 53) の通信が許可されているか確認してください。
      • 別の DNS サーバー (例: Google Public DNS の 8.8.8.8 や Cloudflare DNS の 1.1.1.1) を使用するように設定を変更して、問題が特定の DNS サーバーにあるかどうかを確認してください。
      • ネットワーク管理者や ISP に連絡し、DNS サーバーの状態を確認してください。
  3. Error: queryTxt ETIMEOUT (タイムアウト)

    • 原因
      DNS サーバーからの応答が指定された時間内に得られなかった場合に発生します。ネットワークの遅延、DNS サーバーの負荷が高い、または DNS サーバーが応答しないなどが考えられます。
    • トラブルシューティング
      • ネットワーク接続の状態を確認してください。
      • 別の DNS サーバーを試してみてください。
      • プログラムのタイムアウト設定が適切かどうか確認してください (Node.js の dns モジュール自体にはタイムアウトを設定する直接的なオプションはありませんが、より高度な DNS クライアントライブラリでは設定できる場合があります)。
  4. コールバックが呼ばれない

    • 原因
      何らかの理由で DNS クエリが完了せず、エラーも発生しない場合に、コールバック関数が永遠に呼ばれないことがあります。ネットワークの問題が一時的に発生し、リトライ処理が実装されていない場合などに起こりえます。
    • トラブルシューティング
      • より堅牢な DNS クライアントライブラリ (例えば dns-over-https など) の使用を検討し、リトライやタイムアウト処理を実装することを検討してください。
      • ネットワークの状態を監視し、不安定な状況が頻繁に発生する場合は、ネットワーク環境の見直しを検討してください。
  5. 解決された TXT レコードが期待される形式と異なる

    • 原因
      DNS サーバーから返された TXT レコードの形式が、プログラムで期待している形式と異なる場合があります。TXT レコードは複数の文字列に分割されていることがあるため、適切に処理する必要があります。
    • トラブルシューティング
      • dns.resolveTxt() から返される records は、文字列の配列の配列であることに注意してください。各 TXT レコードは、さらに複数の文字列に分割されている可能性があります。.join(' ') などを使って適切に連結してから処理するようにしてください。
      • 対象のドメインの TXT レコードの内容を dig コマンドやオンラインの DNS Lookup ツールなどで確認し、プログラムで取得できている内容と比較してください。

トラブルシューティングの一般的なヒント

  • より高度な DNS クライアントライブラリを検討する
    より複雑な DNS 処理や、タイムアウト、リトライなどの高度な機能が必要な場合は、サードパーティの DNS クライアントライブラリの利用を検討してください。
  • DNS Lookup ツールを使用する
    dig コマンド (Linux/macOS) や nslookup コマンド (Windows) をターミナルで使用したり、オンラインの DNS Lookup ツールを利用したりして、対象のホスト名の TXT レコードが実際に存在するかどうか、またその内容を確認してください。
  • 別の DNS サーバーを試す
    システムのデフォルト DNS サーバーではなく、Google Public DNS (8.8.8.8, 8.8.4.4) や Cloudflare DNS (1.1.1.1, 1.0.0.1) などを試してみることで、問題が特定の DNS サーバーにあるかどうかを切り分けることができます。
  • ネットワーク接続を確認する
    ping コマンドなどで、ネットワークの基本的な接続性を確認してください。
  • エラーメッセージをよく読む
    エラーメッセージは、問題の原因を特定するための重要な情報を含んでいます。


基本的な TXT レコードの取得 (Promise ベース)

const dns = require('node:dns').promises;

async function getTxtRecords(hostname) {
  try {
    const records = await dns.resolveTxt(hostname);
    console.log(`TXT レコード (${hostname}):`);
    records.forEach(record => {
      console.log('  -', record.join(' ')); // 複数の文字列を連結して表示
    });
    return records;
  } catch (err) {
    console.error(`TXT レコードの解決に失敗しました (${hostname}):`, err);
    return null;
  }
}

async function main() {
  const exampleComRecords = await getTxtRecords('example.com');
  if (exampleComRecords) {
    console.log('\nexample.com の TXT レコード:', exampleComRecords);
  }

  const googleComRecords = await getTxtRecords('google.com');
  if (googleComRecords) {
    console.log('\ngoogle.com の TXT レコード:', googleComRecords);
  }
}

main();

解説

  1. require('node:dns').promises;: dns モジュールを Promise ベースの API でインポートします。
  2. getTxtRecords 関数は、指定された hostname の TXT レコードを非同期で取得します。
  3. dns.resolveTxt(hostname): TXT レコードの解決を試み、Promise を返します。await で結果を待ちます。
  4. 成功した場合、取得した TXT レコードの配列 (records) をコンソールに出力します。各 TXT レコードは文字列の配列である可能性があるため、.join(' ') で連結して表示しています。
  5. エラーが発生した場合は、エラーメッセージをコンソールに出力し、null を返します。
  6. main 関数内で、getTxtRecords をいくつかのホスト名に対して呼び出し、結果を表示しています。

基本的な TXT レコードの取得 (コールバックベース)

const dns = require('node:dns');

function getTxtRecordsCallback(hostname, callback) {
  dns.resolveTxt(hostname, (err, records) => {
    if (err) {
      console.error(`TXT レコードの解決に失敗しました (${hostname}):`, err);
      callback(err, null);
      return;
    }
    console.log(`TXT レコード (${hostname}):`);
    records.forEach(record => {
      console.log('  -', record.join(' '));
    });
    callback(null, records);
  });
}

getTxtRecordsCallback('example.com', (err, records) => {
  if (!err && records) {
    console.log('\nexample.com の TXT レコード (コールバック):', records);
  }
});

getTxtRecordsCallback('google.com', (err, records) => {
  if (!err && records) {
    console.log('\ngoogle.com の TXT レコード (コールバック):', records);
  }
});

解説

  1. require('node:dns');: dns モジュールをインポートします。
  2. getTxtRecordsCallback 関数は、ホスト名とコールバック関数を引数に取ります。
  3. dns.resolveTxt(hostname, (err, records) => { ... });: 非同期の resolveTxt 関数を呼び出し、結果をコールバック関数で処理します。
  4. コールバック関数は、エラー (err) と TXT レコードの配列 (records) を受け取ります。
  5. エラーがない場合は、取得した TXT レコードをコンソールに出力し、コールバック関数に結果を渡します。エラーがある場合は、エラーメッセージを出力し、コールバック関数にエラーを伝えます。

特定の TXT レコードの内容を検索する例 (SPF レコードの検索)

const dns = require('node:dns').promises;

async function findSpfRecord(hostname) {
  try {
    const records = await dns.resolveTxt(hostname);
    for (const record of records) {
      const joinedRecord = record.join(' ');
      if (joinedRecord.startsWith('v=spf1')) {
        console.log(`SPF レコード (${hostname}):`, joinedRecord);
        return joinedRecord;
      }
    }
    console.log(`SPF レコードが見つかりませんでした (${hostname})`);
    return null;
  } catch (err) {
    console.error(`TXT レコードの解決に失敗しました (${hostname}):`, err);
    return null;
  }
}

async function main() {
  await findSpfRecord('google.com');
  await findSpfRecord('invalid-domain-with-no-spf.example');
}

main();

解説

  1. この例では、取得した TXT レコードの中から、SPF レコード (通常は "v=spf1" で始まる) を検索しています。
  2. 取得した各 TXT レコードを連結し、startsWith('v=spf1') で始まるかどうかを確認します。
  3. SPF レコードが見つかった場合は、その内容を出力します。

エラーハンドリングの例

const dns = require('node:dns').promises;

async function getTxtRecordsWithErrorHandler(hostname) {
  try {
    const records = await dns.resolveTxt(hostname);
    console.log(`TXT レコード (${hostname}):`, records);
    return records;
  } catch (err) {
    if (err.code === 'ENOTFOUND') {
      console.error(`エラー: ホスト名 "${hostname}" は見つかりませんでした。`);
    } else if (err.code === 'ETIMEOUT') {
      console.error(`エラー: DNS サーバーへの問い合わせがタイムアウトしました (${hostname})。`);
    } else {
      console.error(`不明なエラーが発生しました (${hostname}):`, err);
    }
    return null;
  }
}

async function main() {
  await getTxtRecordsWithErrorHandler('nonexistent-domain.invalid');
  await getTxtRecordsWithErrorHandler('example.com');
}

main();
  1. この例では、catch ブロック内でエラーオブジェクトの code プロパティをチェックし、特定のエラー (例: ENOTFOUND, ETIMEOUT) に応じたエラーメッセージを出力しています。
  2. エラーの種類に応じて異なる処理を行うことができます。


サードパーティの DNS クライアントライブラリの利用

Node.js のエコシステムには、dns モジュールよりも多くの機能を提供するサードパーティの DNS クライアントライブラリがいくつか存在します。これらのライブラリは、以下のような利点を提供することがあります。

  • キャッシュ機能
    DNS レスポンスをキャッシュすることで、パフォーマンスを向上させることができます。
  • より使いやすい API
    Promise ベースの API に加えて、さらに高レベルな抽象化を提供し、より簡潔なコードで DNS クエリを実行できる場合があります。
  • 異なる DNS プロトコルのサポート
    標準の UDP/TCP に加えて、DNS over HTTPS (DoH) や DNS over TLS (DoT) など、よりセキュアなプロトコルをサポートしている場合があります。
  • より詳細な制御
    DNS クエリの詳細な設定 (タイムアウト、リトライ、使用する DNS サーバーの指定など) が可能な場合があります。

代表的なライブラリ

  • native-dns
    より低レベルな DNS プロトコルへのアクセスを提供し、高度な DNS 処理を必要とする場合に利用できます。
  • node-resolve
    Node.js の require.resolve() アルゴリズムと同様の機能を提供しますが、DNS の解決にも利用できる場合があります。ただし、TXT レコードの直接的な解決に特化しているわけではありません。
  • dnscache
    DNS クエリの結果をキャッシュすることで、パフォーマンスを改善します。標準の dns モジュールのメソッドをラップして使用できます。
  • dns-over-https
    DNS over HTTPS プロトコルをサポートしており、プライバシーとセキュリティを向上させることができます。resolveTxt 関数も提供しています。

dns-over-https の使用例

const doh = require('dns-over-https');

async function resolveTxtWithDoH(hostname) {
  try {
    const records = await doh({
      hostname: hostname,
      resource: 'TXT' // TXT レコードを指定
    });
    console.log(`(DoH) TXT レコード (${hostname}):`);
    records.forEach(record => {
      console.log('  -', record.data.join(' ')); // データ構造が標準モジュールと異なる場合あり
    });
    return records;
  } catch (err) {
    console.error(`(DoH) TXT レコードの解決に失敗しました (${hostname}):`, err);
    return null;
  }
}

async function main() {
  await resolveTxtWithDoH('example.com');
}

main();

注意点
サードパーティのライブラリを使用する場合は、そのライブラリのドキュメントをよく読み、API の使い方やエラー処理の方法を理解する必要があります。

外部の DNS 解決 API の利用

Node.js アプリケーションから、HTTP などのプロトコルを通じて外部の DNS 解決 API を呼び出すことも可能です。これらの API は、多くの場合、JSON 形式などで DNS レコードの情報を提供します。

利点

  • 追加機能
    一部の API は、DNSSEC 検証などの追加機能を提供している場合があります。
  • 柔軟性
    さまざまな DNS 解決サービスを利用でき、特定の要件に合ったものを選択できます。

例 (Google Public DNS の JSON API を利用)

const https = require('node:https');

async function resolveTxtWithExternalApi(hostname) {
  return new Promise((resolve, reject) => {
    const url = `https://dns.google/resolve?name=${hostname}&type=TXT`;

    https.get(url, (res) => {
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        try {
          const result = JSON.parse(data);
          if (result.Answer) {
            const txtRecords = result.Answer
              .filter(record => record.type === 16) // TXT レコードのタイプは 16
              .map(record => record.data);
            console.log(`(外部 API) TXT レコード (${hostname}):`, txtRecords);
            resolve(txtRecords);
          } else {
            console.log(`(外部 API) TXT レコードが見つかりませんでした (${hostname})`);
            resolve([]);
          }
        } catch (error) {
          reject(error);
        }
      });
    }).on('error', (err) => {
      reject(err);
    });
  });
}

async function main() {
  await resolveTxtWithExternalApi('example.com');
}

main();

注意点

  • API のレスポンス形式はサービスによって異なるため、適切にパースする必要があります。
  • API の利用規約やレート制限などを確認する必要があります。
  • 外部 API の利用には、ネットワークアクセスが必要です。

子プロセスを利用して DNS 解決コマンドを実行

child_process モジュールを利用して、システムにインストールされている dignslookup などの DNS 解決コマンドを実行し、その出力を解析する方法もあります。

利点

  • システムの DNS 解決機能を利用
    OS が持つ DNS 解決機能を利用できるため、特別なライブラリのインストールが不要な場合があります。

欠点

  • セキュリティ上の注意
    外部コマンドを実行するため、入力のサニタイズなどに注意が必要です。
  • 出力の解析が必要
    コマンドの出力をプログラムで解析する必要があり、複雑になる場合があります。
  • プラットフォーム依存
    コマンドの存在や出力形式が OS によって異なるため、移植性が低くなる可能性があります。
const { exec } = require('node:child_process');

function resolveTxtWithDig(hostname) {
  return new Promise((resolve, reject) => {
    exec(`dig +short txt ${hostname}`, (error, stdout, stderr) => {
      if (error) {
        console.error(`(dig) エラーが発生しました (${hostname}):`, error);
        reject(error);
        return;
      }
      if (stderr) {
        console.error(`(dig) 標準エラー出力 (${hostname}):`, stderr);
        reject(new Error(stderr));
        return;
      }
      const records = stdout.trim().split('\n').map(line => line.replace(/"/g, '').split(' '));
      console.log(`(dig) TXT レコード (${hostname}):`, records);
      resolve(records);
    });
  });
}

async function main() {
  await resolveTxtWithDig('example.com');
}

main();