Re: Goの通信経路選択(net.LookupIP & net.Dial)
September 5, 2016 - golang
先日Goの通信経路選択(net.LookupIP & net.Dial)にて、特定のネットワーク内でのDNSRRが効かないという例と対策について書きました。 いろいろ考えた結果、別の解決策が出てきたのでそれをライブラリ化しました。
前回の解決策
前回は、
- net.LookupIPでアドレスリストを取得
- アドレスリストからランダムに1つIPを選択
- URLにはホスト名ではなくIPを指定する
- Request.Hostに本来のホスト名(とポート)を指定する
という手順で対策を行いました。簡潔にコードを書くとこのようになります。
addrs, _ := net.Lookup(hostname)
addr := addrs[rand.Intn(len(addrs))]
req.Host = req.URL.Host
req.URL.Host = addr
しかし、この手法だとHTTPS接続の際の証明書チェックでコケることになります。 (マスクしてますが実際にはIPは具体的なIPアドレスが入ります)
Get https://***.***.***.***/: x509: cannot validate certificate for ***.***.***.*** because it doesn't contain any IP SANs
さて、この証明書チェックを回避するためにはどうしたら良いでしょう。 どうにもHTTP層では解決できなそうな感じがします。
別案: コネクションだけ切り替える
http.DefaultClientはhttp.DefaultTransport
を利用しており、このhttp.DefaultTransport
はDial
という経路を開く関数への参照を保持しています。
簡単に書くとこんな感じの構造。
http.Client{
Transport: http.Transport{
Dial: (&net.Dialer{}).Dial,
},
}
で、Dial
の定義はこんな感じでnet.Conn
を返します。
type Transport struct {
Dial func(network, addr string) (net.Conn, error)
}
この関数の中で名前解決とコネクション生成を行っているので、 その前にホスト名をIPに変えてやることができればうまくいきそうです。
という発想をもとにライブラリを作りました。
ReSTARTR/go-netutil
良い名前が思いつかなかったのでnetutil
というパッケージのなかに突っ込みました。
このライブラリのなかでnetutil.RRDialer
を定義しています。
詳細はgodocをみてもらうのがいちばんですが、やっていることはざっと以下のとおり。
netutil.RRDialer
のフィールドに実際の接続を行うnet.Dialer{}
を保持Dial(network, address)
で与えられたアドレスをもとにnet.LookupIP
から得られたIP(ipv4限定)のリストを取得- IPリストを任意のルールでソート(デフォルトはランダム)
net.Dialer{}
を使って接続を試みる
実際に使う場合は、
import "github.com/ReSTARTR/go-netutil"
したうえでhttp.Transport.Dial
を入れ替えてやるだけです。
req, _ := http.NewRequest("GET", "https://www.google.co.jp/", nil)
client := http.Client{
Transport: {
// 既存のDialを入れ替えてやるだけ
Dial: netutil.DefaultRRDialer.Dial,
},
}
res, _ := cleint.Do(req)
fmt.Println(res.Status) // "200 OK"
これで、HTTP層は既存の処理を維持したままその下層のコネクションのみ挙動を変えることが出来ます。
ソートアルゴリズムの変更はnetutil.RRDialer.Sort
にソート関数を指定します。
dialer := netutil.RRDialer{
Sort: func(ips []net.IP) []net.IP {
return sort.Sort(ByFoo(ips))
}
}
RFC6724は維持すべきか否か
ただし、アプリケーション(Firefox等のブラウザとか)によっては、上記のような環境においても、アプリケーション側でラウンドロビンにのっとった実装を行ってくれているようだ。
ここで言いたかったのは、上記のような可能性がある以上、DNSラウンドロビンを採用している環境で、各IPアドレスでのアクセス・負荷に偏りが出ることは必然であり、RFC準拠である以上、DNSラウンドロビンは場合によっては使い物にならなくなる可能性があるということ。
この記事は2012年とちょっと古くて触れられているのはRFC3484ですが、そのアップデート版がRFC6724になるので意味合い的には変わりません。 アプリケーション側でRFCを上書きするような対応をしている例もある模様。 ということで今回作成したようなライブラリの実装もアリ、という見方で良いような気がします。 (が、実際どうなんでしょうか…)
さいごに
以上、前回提案したRFC6724対策案の問題点と、その問題を解決するためのライブラリReSTARTR/go-netutilを作ったお話でした。 今回の対策を考えるにあたってnet - The Go Programming Languageの実装をかなり読み込んだのですが、 すべてGoで完結しているしコードも読みやすいし変な黒魔術もないからgrepしやすいな、とあらためて思いました。
※go-netutilではgolang.org/x/net/context
を使っているのでcontext
が同梱される前の1.6でも動くと思います