TCPでDNSリクエスト
概要
TCPでDNSリクエストを送ることに成功したので、備忘録として書きます。
目次
1. DNSリクエストパケット
以下ページが大変参考になったのでリンクを貼らせていただきます。
DNSリクエストパケットの構成が分からない方はとりあえず読んでみてください!
2. TCPを使用する際の変更点
パケットサイズを先頭につけるだけです。
パケットサイズは16bitで指定します。
TCPでDNSリクエストを送る際のパケットの構成は、以下のようになります。
**
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. まとめ
TCPでDNSリクエストを送っている記事が見当たらなかったので書きました。
正直UDPで送る場合とほとんど変わりがないので薄い記事になってしまった感じが…します。
記事中のプログラムについては、実装中のコードからコピーして良い感じに整えたものです。
自己満足のような記事になってしまいましたが、最後まで読んでいただきありがとうございました。
理解や実装に間違いがあれば、ご指摘のほどよろしくお願いします…。