DNSに登録する文字列から Resolv::DNS::Resource のインスタンスを作成


ネームサーバーの動作確認をRubyでしたい件の続き。以前の記事で、Rubyのresolvライブラリを用いて名前解決できるようになった。しかし、取得した結果が期待値と一致するか調べるには、手元でも文字列からインスタンスを作成しなければならない。

初めは取得した結果を文字列化して比較しようとしたが、AAAAレコード(IPv6)の比較で早速つまづいた。他のレコードも文字列化すると、空白の数など表記揺れで失敗しかねない。

コンストラクタに引数を与えればいいだけではあるが、文字列のまま与えると適切なクラスに変換されず比較失敗することがあるので、適宜対処しなければならない。

(ちなみにTTLはインスタンス同士の比較に使われない)

コード

扱ったことのあるレコード種別のみ。文字列も規定されたパターンを全て解釈できるわけではなく、よく使うパターンだけ対応させている。

require 'resolv'

def create_resource(type, value)
    type = type.upcase.to_sym
    klass = Resolv::DNS::Resource::IN.const_get(type)

    case type
    when :A, :AAAA
        klass.new(value)
    when :CNAME, :NS, :PTR
        klass.new(Resolv::DNS::Name.create(value))
    when :MX, :SRV
        *num_list, name = value.split
        klass.new(*num_list.map!(&:to_i),
                  Resolv::DNS::Name.create(name))
    when :SOA
        mname, rname, *num_list = value.tr("()", " ").split
        klass.new(Resolv::DNS::Name.create(mname),
                  Resolv::DNS::Name.create(rname),
                  *num_list.map!(&:to_i))
    when :TXT, :SPF
        klass.new(value.gsub(/^"|"$/, ""))
    else
        raise ArgumentError, "not implemented yet"
    end
end
実験
create_resource(:A, '192.0.2.1')
#=> #<Resolv::DNS::Resource::IN::A:0x... @address=#<Resolv::IPv4 192.0.2.1>>
create_resource(:AAAA, '2001:0db8:0000:0000:0000:0000:0000:0001')
#=> #<Resolv::DNS::Resource::IN::AAAA:0x... @address=#<Resolv::IPv6 2001:DB8::1>>

create_resource(:CNAME, 'example.com.')
#=> #<Resolv::DNS::Resource::IN::CNAME:0x... @name=#<Resolv::DNS::Name: example.com.>>
create_resource(:NS, 'example.com.')
#=> #<Resolv::DNS::Resource::IN::NS:0x... @name=#<Resolv::DNS::Name: example.com.>>
create_resource(:PTR, 'example.com.')
#=> #<Resolv::DNS::Resource::IN::PTR:0x... @name=#<Resolv::DNS::Name: example.com.>>

create_resource(:MX, '10 example.com.')
#=> #<Resolv::DNS::Resource::IN::MX:0x... @preference=10, @exchange=#<Resolv::DNS::Name: example.com.>>
create_resource(:SRV, '1 0 21 example.com.')
#=> #<Resolv::DNS::Resource::IN::SRV:0x... @priority=1, @weight=0, @port=21, @target=#<Resolv::DNS::Name: example.com.>>

create_resource(:SOA, <<EOS)
ns1.example.com. root.example.com. (
         1
      7200
       900
   1209600
     86400 )
EOS
#=> #<Resolv::DNS::Resource::IN::SOA:0x... @mname=#<Resolv::DNS::Name: ns1.example.com.>, @rname=#<Resolv::DNS::Name: root.example.com.>, @serial=1, @refresh=7200, @retry=900, @expire=1209600, @minimum=86400>

create_resource(:TXT, '"v=spf1 +ip4:192.168.100.0/24 ~all"')
#=> #<Resolv::DNS::Resource::IN::TXT:0x... @strings=["v=spf1 +ip4:192.168.100.0/24 ~all"]>
create_resource(:SPF, '"v=spf1 +ip4:192.168.100.0/24 ~all"')
#=> NameError: uninitialized constant Resolv::DNS::Resource::IN::SPF

最後のSPFレコードについては次節を参照。

詳細

case でレコード毎に処理を変えているが、それぞれ何のための処理なのかを簡単に記す。

A, AAAA レコード

IPアドレス(IPv4, IPv6)を表すレコード。コンストラクタに文字列のまま与えても、内部で Resolv::IPv[46] に変換してくれる。親切。

IPアドレスくらいなら文字列比較でもいけそうに思えるが、IPv6には様々な省略ルールがあるため文字列比較は非常に難しい。上記の実験では結果が最大限に短縮されている上、アルファベットも大文字に変わっている。

CNAME, NS, PTR レコード

ドメイン名を表すレコード。これらのクラスは Resolv::DNS::Resource::DomainName を親に持つ。コンストラクタに Resolv::DNS::Name を与えないといけない(文字列から自動変換されない)。

MX, SRV レコード

ドメイン名だけでなく数値も持つレコード。これも、ドメイン名は Resolv::DNS::Name で、数値は Integer で与える必要がある。引数の順序はDNSに設定する文字列と同じなので、コードではsplatを利用して簡略化している。

SOA レコード

括弧を用いて複数行で書くことがあるので、事前にスペースに変換してから区切っている。引数の数は多いけれども、これも文字列の順番通りなのでSRVレコードと似た対応でいい。

TXT, SPF レコード

任意の文字列を持てるレコード。DNSに設定するときはダブルクォートで囲むが、 Resolv::DNS の結果ではダブルクォートが含まれないので、事前に取っておく。

なお、resolvライブラリはまだSPFレコードのクラスを持たないので、上の実験ではエラーになった。issueにパッチが書かれているので、それを適用すればひとまず使えるようになる。
https://bugs.ruby-lang.org/issues/11312

参考