どさにっき 〜2017年2月上旬〜

by やまや
<< = >>

2017年2月2日(木)

金沢帰り

_ えーと、もう2週間前になりますか。インフルにはかかっておりませぬ。あの場にいた人が誰もかれも後でインフルにかかってたのに、自分は何ごともなくピンピンしてるって、バカは風邪をひかないを体現してるかのようでなんかアレ。

Knot Resolver であそぼう

_ cz.nic によるキャッシュ DNS サーバの Knot Resolver(kresd)。インストール手順は省略するけど、依存するものが多いので自前ビルドするとなるとかなりめんどくさい。同じく cz.nic が開発する権威サーバの Knot DNS(knotd) に含まれる libknot が必要なので、knotd を動かすつもりはなくてもインストールしなきゃいけない。knotd もインストールめんどくさいのに、それに輪をかけてめんどくさいということ。

_ knotd は C で書かれてたけど、kresd は C と Lua で書かれてる。はい、Lua です。ということで、設定ファイルは純粋に Lua スクリプト。Lua でできることなら何でもできる。できるからって何でもやろうとするなよ。

_ とりあえず空っぽの設定で動かしてみるとこんな感じ。

_ ちゃんと設定をしていこうとなると、Lua の知識が必要になる。最近は Lua を使うものをちらほら見るけど、たいていは Lua を知らなくても基本的なことにかぎればパラメータに値を代入する程度で使えるものがほとんどだと思う。kresd は、そんな甘くはない。ガチの Lua スクリプトを書くハメになる。

_ つづく。


2017年2月4日(土)

Knot Resolver で ACL

_ デフォで open reosolver なんですよ。いまどきどうなのさ。まずはアクセス制限しなきゃいけないんですよ。

_ 設定例を見ると以下のようなサンプルがある。

-- Block local clients (ACL like)
view:addr('127.0.0.1', function (req, qry) return policy.DENY end))

_ 関数呼び出しの中で無名関数を定義するとか、この時点ですでに Lua の文法を理解できてないとついていけない。とはいえ、acl を設定するだけなら Lua を知らなくてもこれをマネすりゃいいよね。ってことで、こう書いてみる。

view:addr('127.0.0.1', function (req, qry) return policy.PASS end))
view:addr('0.0.0.0/0', function (req, qry) return policy.DENY end))

_ よさげじゃろ? ダメなんだよこれ。なんでだよ。いろいろ試してみたところ、どうやら /0 のマスク指定がダメっぽい。バグかしら? /0 ではなく、2分割して /1 にすれば動いた。ちなみに v6 も ::/0 ではダメで /1 に分割する必要がある。

view:addr('127.0.0.1', function (req, qry) return policy.PASS end))
view:addr('0.0.0.0/1', function (req, qry) return policy.DENY end))
view:addr('128.0.0.0/1', function (req, qry) return policy.DENY end))

_ ところで、DENY っていうぐらいだから拒否応答を返すのかと思ったら NXDOMAIN を返すんだよこれ。実質的には使えないとはいえ、ra フラグ立ってるんだしこれじゃ外から open resolver に見えるのは変わらない。で、かわりに policy.DROP ってのも使える。こちらはクエリを無視して応答を返さない…ではなく SERVFAIL を返す。なんで直感に反する挙動ばっかりすんのかなぁ。SERVFAIL なら NXDOMAIN よりはマシだけど、それもちょっとどうか。

_ ということで、REFUSED を返すポリシーを自前で定義して、こいつを policy.DENY や policy.DROP のかわりに使う。

policy.REFUSED = function(state, req)
    req.answer:rcode(kres.rcode.REFUSED)
    return kres.DONE
end

local acl = {
    ['allow']  = { '127.0.0.0/8', '::1', },
    ['refuse'] = { '0.0.0.0/1', '128.0.0.0/1', '::/1', '8000::/1' }
}

for k, v in pairs(acl.allow) do
    view:addr(v, function(req, query) return policy.PASS end)
end
for k, v in pairs(acl.refuse) do
    view:addr(v, function(req, query) return policy.REFUSED end)
end

_ やっとこれでアクセス制限できました、と。

_ この例は view と policy というモジュールを使った設定だけど、もうひとつ daf (dns application firewall) というモジュールもある。が、これも結局のところ専用の文法で記述したルールを最終的に view と policy によるルールに変換するだけのものなので、これを使えば解決するというものではない。

daf.add 'src = 127.0.0.1 pass'
daf.add 'src = 0.0.0.0/0 deny'

_ つまり、上の例は /0 だから機能しなくて /1 で2分割する必要があるし、deny は NXDOMAIN を返す(はず。試してない)。そして、refused を自前で定義しようにも隠蔽されててモジュールの外からいじるのは超めんどくさい(できなくはないはずだが)。

_ ちなみに、応答を返さずクエリを捨てる、ということはできないっぽい。ひょっとすると方法があるのかもしれないけど今のところ見つけられてない。

_ いろいろいじってみたけど、kresd 自体ではアクセス制限せず、iptables なり pf なりの OS 側のパケットフィルタを使ってクエリを捨てるのが開発側の想定なのかもしれない。kresd の view が BIND の view と同じような使い方を想定したものであれば /0 というマスク指定はたしかにおかしいわけで、それが動かないのはバグではなく意図した仕様である可能性も捨てきれないんだよね。

_ まだつづく。


2017年2月5日(日)

Knot Resolver に localhost を応答させる

_ kresd はキャッシュサーバなので、自前ではゾーンを持たない。が、localhost の正引き逆引きや、192.168.0.0/16 やら 10.0.0.0/8 やらのプライベートアドレスの逆引きぐらいはキャッシュサーバが自前で応答してくれるとありがたい。

_ これが半分楽ちんで、半分超めんどくさい。トータルすると死ぬほどめんどくさい。

_ とくに何もしなくても、これらの逆引き空間に NXDOMAIN を返すルールが policy モジュールの中に含まれている。楽ちんだね。でも、NXDOMAIN なんだ。使ってないプライベートアドレスならそれでいいんだけど、127.0.0.1 や ::1 の逆引きは NXDOMAIN ではなく localhost. を返してほしい。あるいは、プライベートアドレスにちゃんとした逆引きを設定したい場合もある。ということで、これを修正するルールを書くことになる。

policy.add(policy.pattern(policy.PASS,
    '^' .. todname('1.0.0.127.in-addr.arpa'):gsub('%-', '%%-')))
policy.add(policy.pattern(policy.PASS,
    '^' .. todname('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa')))

_ 127.0.0.1 と ::1 の逆引きに関してはデフォルトの policy.DENY (=NXDOMAIN) ではなく policy.PASS (何もしない)というアクションを実行する。それだけやるのでもずいぶんわかりづらいコードになったけど、正しいはず。正しいはずだけど、ダメなんだよ。

_ ポリシーは定義された順に検査されて、最初にマッチしたものが有効になる。なので、後から自作でのルールを定義したところで、デフォルトで組み込まれている NXDOMAIN を返すルールの方が先にマッチしてしまって発動しないのだ。プライベートアドレスの逆引き問い合わせを他のサーバに forward する場合も同じ罠にひっかかる。つーか、英語の default って本来は初期設定って意味ではなく不履行って意味だぞ。するべき設定をサボって何も設定しない場合にだけ有効なのが default であって、明示的に設定を上書きしてもそれを無視するものはもはや default とは言えん。

_ しかたないので、むりやりルールの順番を入れ替える。念のため、これはキャッシュ DNS の設定です。何かのプログラムではないです。

-- プライベート逆引きゾーンはルールの先頭(policy.rules[1])にあるので、
-- コールバック関数を一時的に退避しておく
local policy_private_zone = policy.rules[1].cb
-- プライベート逆引きルール(id=0)を削除
policy.del(0)
-- ここに自作のルールを追加
-- localhost 逆引き
policy.add(policy.pattern(policy.PASS,
    '^' .. todname('1.0.0.127.in-addr.arpa'):gsub('%-', '%%-')))
policy.add(policy.pattern(policy.PASS,
    '^' .. todname('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa')))
-- 192.168.0.0/24 の逆引きを別ホストに forward
policy.add(policy.suffix(policy.FORWARD('192.168.0.53'),
    {todname('0.168.192.in-addr.arpa')}))
-- 退避しておいたルールを戻す
policy.add(policy_private_zone)

_ これでようやく 127.0.0.1 や ::1 の逆引き問い合わせに対してお仕着せの NXDOMAIN を答えず、外部に反復問い合わせを出す(結果として外から NXDOMAIN をもらってくる)ようになる。一歩前進。あとは外から答をもらってくるのではなく、自分で応答を返すようにすればよい。

_ 自力で答えるには hints モジュールってのがお手軽。その名のとおり root hint を扱うためのものだけど、実は /etc/hosts をデータベースにして正引き逆引きを答えてくれる機能もあったりする。

hints.config('/etc/hosts')

_ これで /etc/hosts に localhost の設定があればおっけー。

_ …だと思うよな。そうは問屋が卸さんぜよ。

_ 手元の FreeBSD11 ではデフォの /etc/hosts がこうなってた。

::1                     localhost localhost.my.domain
127.0.0.1               localhost localhost.my.domain

_ CentOS7 はこう。

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

_ いずれも、ひとつの IP アドレスに対して複数の名前が定義されている。あ、これ、DNS ではやっちゃダメなやつだ。kresd はこういう /etc/hosts を食うと、逆引きに対して複数の名前を答えたりはせず、NXDOMAIN を返す。正引きは複数あっても問題ないので、この状態でも localhost の正引きは答えてくれるようになるが、逆引きが機能しない。以下のように /etc/hosts を修正しておく必要がある。

127.0.0.1       localhost
::1             localhost

_ /etc/hosts による名前解決はもちろん localhost 以外にも使える。

192.0.2.1       hoge hoge.example.com
10.1.2.3        fuga.private.example.com

_ よくある書き方だけど、1行目は hoge.example.com だけでなく hoge. という TLD にも 192.0.2.1 を答えることになってしまうし、複数の名前があるので逆引きは答えてくれない。2行目はそのどちらでもないけど、グローバルアドレスではなくプライベートアドレスなので正引きはいいけど逆引きはお仕着せ NXDOMAIN でブロックされる。

_ つーことで、いろいろ注意が必要。ここでは /etc/hosts を使ったけど、別のファイルを用意した方がいいかも。

_ ちなみに、hosts から答える場合でも、応答に aa フラグは立ちません。unbound で local-data を設定すると aa フラグ立つんだけどね。

_ まあ、localhost なんて DNS に問い合わせてくんなよ、てめーの内部で解決しろよ、で済ませられる話ではあるんだけどね。でも、localhost 以外でもプライベートアドレスまわりを制御するときには必要になることなので、kresd を使いこなすのであれば知っておいたほうがよいかと。

_ おまけ。hints を使わず policy だけで localhost に答えることもできる。aa フラグも立つ(というか、むりやり書き換えている)。繰り返しますが、これはキャッシュ DNS の設定です。何かのプログラムではないです。

policy.add(policy.pattern(
    function (state, req)
        local q = req.answer
        q.wire[2] = bit.bor(q.wire[2], 0x04)    -- set aa flag
        if q:qtype() == kres.type.A or q:qtype() == kres.type.ANY then
            q:put(q:qname(), 86400, kres.class.IN, kres.type.A, '\127\0\0\1')
        end
        if q:qtype() == kres.type.AAAA or q:qtype() == kres.type.ANY then
            q:put(q:qname(), 86400, kres.class.IN, kres.type.AAAA,
                '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1')
        end
        return kres.DONE
    end,
    '^\9localhost\0'
))
for _, z in pairs{'1.0.0.127.in-addr.arpa', '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa'} do
    policy.add(policy.pattern(
        function (state, req)
            local q = req.answer
            q.wire[2] = bit.bor(q.wire[2], 0x04)
            if q:qtype() == kres.type.PTR or q:qtype() == kres.type.ANY then
                q:put(q:qname(), 86400, kres.class.IN, kres.type.PTR, '\9localhost\0')
            end
            return kres.DONE
        end,
        '^'..todname(z):gsub('%-', '%%-')
    ))
end

_ もっとつづく。


2017年2月6日(月)

Knot Resolver 小ネタ

_ カスタマイズ例。

_ (ランダム8文字以上).example.com にマッチするクエリに SERVFAIL を返す。www.example.com のような8文字以下のクエリはふつーに処理する。つまり、たったこの1行だけで簡易的な水責め攻撃への対策になる。誤爆が怖い場合、policy.DROP ではなく policy.TC にすると tc フラグつきの応答を返すので、TCP フォールバックをちゃんと実装してるクライアントならば名前解決にコケることはない。

policy.add(policy.pattern(policy.DROP, '^[\8-\63]%w+\7example\3com\0'))

_ ちゃんとした水責め対策にはレート制御とかのしくみも欲しいところだけど、このあたりの機能は kresd には用意されていない。が、その気になれば Lua で十分書けそう。クエリ処理だけでなく、一定時間ごとに実行されるイベントなんてのも書けるので、単位時間内に一定以上のクエリを投げてきたクライアントを一時的にブロックし、しばらくしたら自動で開放する、なんて処理も実現できるはず。

_ ちょっと長くなるけど AAAA フィルタ。BIND の filter-aaaa-on-v4 は、AAAA を聞かれたら自前で A を調べて、見つからなければ AAAA をフィルタせずにそのまま返すというキモい動作をしてたはず。たしか。この例ではそこまではせず、単純に応答から AAAA (と RRSIG)を削るだけ。kresd も自前で名前解決させることはできるので、やろうと思えば同じようなこともできるはず。引数で RRSIG がある場合にフィルタしない/するを選べる。署名があっても AAAA を削る場合は ad フラグも落とした方がいいのかもしれないけど、そこまではしてない。

function policy.filter_aaaa(break_dnssec)
    return function (state, req)
        if state == kres.FAIL then return state end
        -- do nothing if not ipv4
        -- if req.qsource.addr:family() ~= 2 then return state end -- AF_INET==2???
        if req.qsource.addr:len() ~= 4 then return state end
        -- do nothing if ANCOUNT==0
        local pkt = kres.request_t(req).answer
        local rrset = pkt:section(kres.section.ANSWER)
        if #rrset == 0 then return state end
        -- find AAAA and RRSIG
        local has_aaaa = false
        local has_rrsig = false
        for i, rr in ipairs(rrset) do
            if rr.type == kres.type.AAAA then
                has_aaaa = true
            elseif rr.type == kres.type.RRSIG then
                has_rrsig = true
            end
        end
        if not has_aaaa then return state end
        if has_rrsig and not break_dnssec then return state end
        -- filter AAAA and RRSIG
        local qname = pkt:qname()
        local qclass = pkt:qclass()
        local qtype = pkt:qtype()
        pkt:clear()
        pkt:question(qname, qclass, qtype)
        for i, rr in ipairs(rrset) do
            if rr.type ~= kres.type.AAAA and rr.type ~= kres.type.RRSIG then
                pkt:put(rr.owner, rr.ttl, rr.class, rr.type, rr.rdata)
            end
        end
        return state
    end
end

policy.add(policy.all(policy.filter_aaaa(false)), true)    -- DNSSEC 署名されていたら AAAA フィルタしない
-- policy.add(policy.all(policy.filter_aaaa(true)), true)  -- 署名されていてもかまわず AAAA フィルタする

_ 特定のクエリにポリシーを適用させるのに後方一致とかパターンマッチとかさせる関数はあるけど、なぜか完全一致で発動させるものがないので作った。localhost の逆引きに NXDOMAIN を返すルールを修正した例ではパターンマッチさせてたけど、以下のように完全一致させた方がわかりやすい。

-- クエリが指定した名前と一致した場合のみ action を実行する
function policy.qname(action, zone_list)
    return function(req, query)
        local qname = query:name()
        if type(zone_list) == 'table' then
            for k, v in pairs(zone_list) do
                if v == qname then
                    return action
                end
            end
        elseif zone_list == qname then
            return action
        end
        return nil
    end
end

-- 利用例
-- localhost の逆引きに対して何もさせない
policy.add(policy.qname(policy.PASS, policy.todnames{
    '1.0.0.127.in-addr.arpa',
    '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa'
}))
-- now.example.com の問い合わせに対して TXT レコードで現在時刻を返す
policy.add(policy.qname(
    function (state, req)
        local q = req.answer
        local now = os.date()
        q.wire[2] = bit.bor(q.wire[2], 0x04)    -- set aa flag
        q:put(q:qname(), 0, kres.class.IN, kres.type.TXT, string.char(#now)..now)
        return kres.DONE
    end,
    '\3now\7example\3com\0'
))

_ もちょっとだけつづくんじゃ。


2017年2月7日(火)

Knot Resolver まとめ

_ ここまでの例ではすげー使いづらそうに感じるかもしれないけど、細かい部分までスクリプトで制御できてお手軽に機能を拡張できるというのはたいへんすばらしい。いろいろ機能はあるけどかゆいところに手が届かない、というものは世の中に多いけど、kresd はそうではなく、かゆいところがあるなら孫の手もセットしてるんで自分でかいてね、というものなので、これができるあれができない、と既存のものと単純に比較するのは間違っている。逆に、既存の bind やら unbound やらの機能で十分間に合っている、かゆいところはない、というのであれば、kresd は単純にめんどくさいだけでわざわざ使うメリットはないと思う。

_ kresd を使いこなすにはまず孫の手(Lua)の使い方を習得する必要があるのでハードルは高いけど、今どきはいろんなところで Lua は使われてるので覚えておいて損はないはず。拡張モジュールに関しては C や go でも書けるみたいだけど、設定はどっちにしろ Lua になる。ちなみに、unbound も python モジュールを使って拡張できるけど、あちらよりいじれる範囲も広く、使いやすさもずっと上という印象。何か妙なことをやりたい要件があるなら、dnsdist か kresd のどっちか、ということになりそう。ちなみに dnsdist も Lua。ISP のような不特定多数が使う環境ではなく、ちょっと特殊なカスタマイズ要件がある組織内部のキャッシュサーバ用途、あるいは BIND にしかない機能が必要だけどもう BIND なんて使いたくない、なんてときに候補に上がりそう。複数ホストでキャッシュを共有できる機能はあるけど、そんな機能が欲しくなるような大規模な用途で使うのは、少なくとも現時点ではおすすめできない。

_ なんでもかんでも Lua でやっちゃうのでオーバーヘッドが気になるところだけど、LuaJIT はスクリプト言語最速と言われコンパイラ言語と遜色ない速度を出すので、それほど心配しなくていいんじゃないかな、たぶん。ベンチマークとかしてないけど。キャッシュサーバって、キャッシュ済みかどうかで速度がぜんぜん違うから、条件合わせて計測するのがめんどくさいんだよね。

_ 最初に書いたように、現状では ANY を問い合わせると NOTIMP を返すので、クライアントに qmail が存在する可能性がある場合に使うのは危険。ソースの該当部分を見ると修正する意思はありそうなので、このへんは将来に期待。とにかく bind や unbound とは方向性がまったく違うので(あえて触れなかったけど DNS サーバなのに HTTP/2 喋れるんだぜこいつ)、現状の延長線上で置き替えようとすると間違いなく失敗する。

_ ということでおしまい。

蛇足

_ ベンチマークではない。N.fib.example.com に問い合わせると N 番目のフィボナッチ数を返す。

policy.add(policy.suffix(
    function (state, req)
        local function fib(n, a, b)
            return n<=1 and a or fib(n-1, a+b, a)
        end
        local q = req.answer
        local n = tonumber(kres.dname2str(q:qname()):match('(%d+)') or 0)
        local rdata = n<=1000 and tostring(fib(n,1,0)) or "too large number"
        rdata = string.char(#rdata) .. rdata
        q:put(q:qname(), 0, kres.class.IN, kres.type.TXT, rdata)
        return kres.DONE
     end,
     {'\3fib\7example\3com\0'}
))

_ 結果。末尾最適化があるとはいえ、さすがに LuaJIT 速すぎないかこれ。

> dig @::1 +noall +ans +stats 38.fib.example.com txt
38.fib.example.com.     0    IN TXT "39088169"
;; Query time: 0 msec
...
> dig @::1 +noall +ans +stats 50.fib.example.com txt
50.fib.example.com.     0    IN TXT "12586269025"
;; Query time: 0 msec
...
> dig @::1 +noall +ans +stats 1000.fib.example.com txt
1000.fib.example.com.   0    IN TXT "4.3466557686937e+208"
;; Query time: 0 msec
...

<< = >>
やまや