TCPでDNSリクエスト

概要

TCPDNSリクエストを送ることに成功したので、備忘録として書きます。

目次

  1. DNSリクエストパケット
  2. TCPを使用する際の変更点
  3. 実装
  4. まとめ

1. DNSリクエストパケット

基本的な構成はTCP/UDP共に同じです。

以下ページが大変参考になったのでリンクを貼らせていただきます。

DNSリクエストパケットの構成が分からない方はとりあえず読んでみてください!

pcap.it-mem.info

2. TCPを使用する際の変更点

パケットサイズを先頭につけるだけです。

パケットサイズは16bitで指定します。

TCPDNSリクエストを送る際のパケットの構成は、以下のようになります。

**

f:id:guguru0014:20191223232715p:plain:w300
パケットの構成

3. 実装

DNSリクエストの生成と、レスポンスを解析するプログラムです。

リクエス

#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <string>

uint8_t *makeDNSReqMsg(std::string hostURL)
{
    std::vector<std::string> labels = yn0014::mystring::split(hostURL, ".");
    size_t hd_len = 7 * sizeof(int16_t), qb_len = 0;
    uint16_t header[7] = {0};
    uint8_t *qsecBody = (uint8_t*)calloc(labels.size()*15+5, sizeof(uint8_t));

    // DNS Query Header(TCP)
    header[2] |= RD;   // RDビット
    header[3] = 1;        // QuestionSectionの数

   // DNS Quesition Section
    for(auto part : labels) {
        qsecBody[qb_len++] = part.size();   // ラベルサイズ
        for(char c : part) {
            qsecBody[qb_len++] = c; // ラベル
        }
    }
    qsecBody[qb_len++] = 0;   // ラベル終端
    qsecBody[qb_len++] = 0;  // Aレコード要求
    qsecBody[qb_len++] = 1;   // Aレコード要求
    qsecBody[qb_len++] = 0;
    qsecBody[qb_len++] = 1;
    header[0] = hd_len+qb_len-2;  // パケットサイズ(QuestionSectionの長さが確定してから)

    // 組み立て
    uint8_t *msg = (uint8_t*)malloc(hd_len+qb_len);
    for(int32_t idx = 0; idx < (int32_t)hd_len; idx += 2) { // エンディアンの影響で素直にmemcpyが使えない
        msg[idx] = (header[idx/2] & 0xff00) >> 8;
        msg[idx+1] = header[idx/2] & 0x00ff;
    }
    memcpy(msg+hd_len, qsecBody, qb_len);
    free(qsecBody);
    return msg;
}

(yn0014::mystring::splitは与えられた文字列を区切り文字で区切り、std::vector<std::string>を返します。)

ヘッダでは RD ビットとQuestionSectionの数を指定しています。

ヘッダとQuestionSectionを連結する部分が少し面倒です。

uint16_t で扱っているヘッダをそのまま memcpy するとリトルエンディアン環境であった場合は、意図しない内容のリクエストパケットが生成されてしまいます。

(ネットワークバイトオーダ = ビッグエンディアン)

そのため、リトルエンディアンからビッグエンディアンに変換する処理を挟んでいます。

QuestionSectionについては特に問題なく memcpy できます。

レスポンス解析

#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <string>

#define memcpynum16(dst, src) ((dst |= ((src)[0] << 8) | (src)[1]))
#define memcpynum32(dst, src) ((dst |= ((src)[0] << 24) | ((src)[1] << 16) | ((src)[2] << 8) | (src)[3]))

std::vector<std::string> parseDNSResMsg(uint8_t *msg, size_t qb_len)
{
    uint16_t flag = 0, resp = 0, offset = 0;

    // Header
    memcpynum16(flag, msg+4);        // オプション
    memcpynum16(resp, msg+8);       // AnswerSectionの数           
    if((flag & RCODE) != 0 || (flag & RA) != RA) {          // RCODEが1 => 正常に完了しなかった, RAビットが0 => 名前解決不可能
        cerr << "Received error response" << endl;
        return std::vector<std::string>();
    }
    if(resp == 0) {
        cerr << "AnswerSection is empty" << endl;
        return std::vector<std::string>();
    }
    msg += 14;

    // Question Section
    msg += qb_len;     // 読み飛ばす

    // Answer Section
    std::vector<std::string> ipList;
    for(; resp > 0; -- resp) {
        memcpynum16(offset, msg);
        if((offset & 0xc000) == 0) {   // 先頭2ビットが立っていない => ラベル省略なし、そのままくっついている
            offset = 0;
            while(msg[offset] != 0) ++ offset;
        } else {     // 先頭2ビットが立っている => ラベル省略
            offset = 2;
        }
        msg += offset + 10;
        ipList.emplace_back(yn0014::mystring::format("%d.%d.%d.%d", msg[0], msg[1], msg[2], msg[3]));
        msg += 4;
    }

    return ipList;
}

(yn0014::mystring::formatは内部でsprintfを行い、std::stringを返します。)

QuestionSectionを読み飛ばすために、parseDNSResMsg でそのサイズを受け取るようにしています。

簡単に解析するなら、以上のプログラムで十分だと思います。(多分…)

リクエストの時と同じように、エンディアンの違いが邪魔をするので簡単なマクロを定義しています。

また、AnswerSectionについてくるラベルについては読む必要がないと判断したので、切り捨てられているorラベルがくっついている のを判定したのちに読み飛ばすようにしています。

送信

std::vector<std::string> resolve(std::string hostURL)
{
    uint8_t *reqMsg = makeDNSReqMsg(hostURL), *respMsg;
    size_t reqLen = ((uint16_t*)reqMsg)[0];
    reqLen = ((reqLen & 0xff00) >> 8) | ((reqLen & 0x00ff) << 8);
    reqLen += 2;

    yn0014::net::TCPConnector conn("8.8.8.8", 53);
    conn.sendMsg(reqMsg, reqLen);
    respMsg = conn.getRecv();

    std::vector<std::string> result = parseDNSResMsg(respMsg, reqLen-14);   // QuestionSectionの長さだけ欲しいのでヘッダーサイズを引く
    free(reqMsg);
    free(respMsg);
    return result;
}

(yn0014::net::TCPConnectorはTCP通信を行うための自作クラスです、実装は省略します)

4. まとめ

TCPDNSリクエストを送っている記事が見当たらなかったので書きました。

正直UDPで送る場合とほとんど変わりがないので薄い記事になってしまった感じが…します。

記事中のプログラムについては、実装中のコードからコピーして良い感じに整えたものです。

自己満足のような記事になってしまいましたが、最後まで読んでいただきありがとうございました。

理解や実装に間違いがあれば、ご指摘のほどよろしくお願いします…。