ふ~ん

メモ書き

rubyを使って擬似digコマンドをつくってみる

●まえがき
 暇つぶしにつくりました
 最小限の実装(パラメータ直打)となっています
 需要はないと思いますが載せておきます


●実装及びテスト環境
 Linux Mint 17.2 Rafaela 64-bit
 ruby 2.2.1p85 (rvmで入れました)
 net-dns ver.0.8.0 (モジュールです gem install net-dnsで入れました)


●実際に使った様子
 このブログのURLを名前解決した結果です
 キャッシュサーバにキャッシュがあるため、すぐ応答がきています
f:id:wiasth:20150823042617p:plain
見方がわからない人は、DNS クライアントを作ってみよう (2)を見てみるといいかも


ソースコード

require 'socket'
require 'rubygems'
require 'net/dns'
require 'benchmark'

srcip = "127.0.0.1" #自身のIPアドレス
sport = rand(1024..65535) #自身のポート番号(乱数)
dport = 53 #宛先ポート番号(DNS => 53)
dstip = "127.0.1.1" #DNSのIPアドレス
seturl = "wiasth.hatenablog.com" #名前解決したいURL

@udps = UDPSocket.open
@udps.bind(srcip, sport)

que = Net::DNS::Packet.new(seturl)
@udps.send(que.data, 0, dstip, dport)

time = Benchmark.realtime do
    @r = IO::select([@udps], nil, nil,5)
end

if @r
    res = @udps.recvfrom(1024)
    res = Net::DNS::Packet::parse(res)
    time = time*1000
    time = time.round(10)
    puts "\n#{res}\n"
    puts ";; Query time: #{time} msec\n\n"
else
    time = time*1000
    time = time.round(12)
    puts "\n;; connection timed out: no servers could be reached\n"
    puts ";; Query time: #{time} msec\n\n"
end
@udps.close

 わずか35行

 やっていることは
  1.パラメータのセット
  2.Aレコードを作りDNSサーバへ送る
  3.最大5秒間応答待ち、5秒たっても応答がなければ終了
  4.応答があればそのパケットを表示する
 の4つです
 
  1.パラメータのセット

srcip = "127.0.0.1" #自身のIPアドレス
sport = rand(1024..65535) #自身のポート番号(乱数)
dport = 53 #宛先ポート番号(DNS => 53)
dstip = "127.0.1.1" #DNSのIPアドレス
seturl = "wiasth.hatenablog.com" #名前解決したいURL

   プログラムに直接パラメータを打ち込んでいます
   最低限動作させるだけならこれで十分だと思います


  2.Aレコードを作りDNSサーバに送る

@udps = UDPSocket.open
@udps.bind(srcip, sport)

que = Net::DNS::Packet.new(seturl)
@udps.send(que.data, 0, dstip, dport)

   上の2行で自身のIPとUDPのポート番号をbindしています
   下の2行では、セットしたURLを元にAレコードをつくりDNSサーバへ送信しています

  ※補足
   que = Net::DNS::Packet.new(seturl)とは?
    net-dnsというモジュールをつかうことで1行でレコードをつくっています
    Packet.newのデフォルトはAレコードとなっているみたいです
    他のレコードの作成の仕方として、例えば192.168.1.1を逆引きしたい(PTRレコード)ときは、
    que = Net::DNS::Packet.new("192.168.1.1", Net::DNS::PTR)
    でいけると思います(他のレコードも同じで、パラメータとNet::DNS::レコードの種類でいけるっぽい?)


  3.最大5秒間応答待ち、5秒たっても応答がなければ終了

time = Benchmark.realtime do
    @r = IO::select([@udps], nil, nil,5)
end
・
・
・
else
    time = time*1000
    time = time.round(12)
    puts "\n;; connection timed out: no servers could be reached\n"
    puts ";; Query time: #{time} msec\n\n"
end

   上にあるtimeではbenchmarkという時間をはかるモジュールを使っています
   囲っている部分の時間をはかれるようです
   今回はパケットを送ってから応答が検出されるまでをはかっています(これで大丈夫なのかは分からない)
   ⇒ 追記;応答を受信したときの日時 - パケットを送信したときの日時 で時間を出した方が良かった

   タイムアウトまでの時間はnslookupコマンドのデフォルトと同じ5秒としました
   パケットを送ってから5秒の間@r(応答)が検出されなければif文のelseの方に入り、タイムアウトメッセージと送信から応答までの時間を表示して終わりとなっています


  4.応答があればそのパケットを表示する

if @r
    res = @udps.recvfrom(1024)
    res = Net::DNS::Packet::parse(res)
    time = time*1000
    time = time.round(10)
    puts "\n#{res}\n"
    puts ";; Query time: #{time} msec\n\n"

   @r(応答)が検出されたらif文のifの方に入ります
   ここでは、レスポンスのデータをNet::DNS::Packet::parseに格納することでレスポンス解析しています
   net-dnsの仕様で「puts レスポンス」するだけで詳細表示されるようになっているみたいです
   レスポンスの中身を表示し、送信から応答までの時間を表示して終わりとなっています


応答時間テスト
 digコマンドと比較してみました
 計測方法:
  1.dnsmasq(127.0.1.1)のキャッシュを一度クリア
  2.dnsmasqに対して11回「wiasth.hatenablog.com」の名前解決要求
  3.はじめの1回目をキャッシュ用とみなし破棄、2-11回目の応答時間結果を10で割る
  1〜3の操作を両者で行ないました(こんなやり方で良いのかは疑問ですが)
  digコマンドに合わせてmsecのコンマ以下は切り捨てとしました

 結果:
  digコマンド ⇒ 25.9msec
  つくったやつ ⇒ 25.0msec
  (環境次第な感じ?)


●最後に(補足等)
 結果表示自体はdigコマンドのほうが今回つくったやつより早いです
 理由としては、rubyでは結果表示(puts文等)がかなりのロスになるからです
 応答時間のはかり方が正しいのかあやしい(プログラム的な意味で)ので、上記のテストはあまり参考にしないでください

 間違えてる気がするので、テストは見なかったことにしてください

 今回使用したnet-dnsモジュールを使うとrubyで簡単なDNSサーバを実装できます
 net-dnsの他にDNS関連のモジュールでrubyDNSというものもありますが、個人的には今回使ったnet-dnsが使いやすく感じました

 DNSで既存処理以外のことをさせたいというときにBINDに手を加えるよりはnet-dnsを使って実装したほうが楽な気がします
 しかしながら、net-dnsは情報が少ないみたい(ぐぐってもあんまりでてこない)なので、機会があれば載せてみようと思います

 以上

;(function(document){ var pres = document.getElementsByTagName("pre") for(var i=pres.length; i--; ){  var el = makeOl(pres[i]) pres[i].appendChild(el) } function makeOl(pre){ if (pre.className.indexOf("gist") !== -1) { return } var ol = document.createElement("ol") , li = document.createElement("li") , df = document.createDocumentFragment() , br = pre.innerHTML.match(/\n/g) || 0 ol.className = "preLine" ol.setAttribute("role", "presentation") // no lang, no line-number if( pre.className && ! /lang-./.test(pre.className) ){ br.length += 1 } for(var i=br.length; i--; ){ var li2 = li.cloneNode(true) df.appendChild(li2) } ol.appendChild(df) return ol } })(document)