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.DefaultTransportDialという経路を開く関数への参照を保持しています。 簡単に書くとこんな感じの構造。

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でも動くと思います