詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Goでプライベートネットワークへのアクセスを制限する

Go において、いわゆる SSRF (Server Side Request Forgery) を防ぐような目的で、内部 IP アドレスにアクセスしない HTTP クライアントを作るには hakobe/paranoidhttp が便利だった。ただ、近年ではこれが作られて以降の Go 側のアップデートとして、net.Dialer.Control の登場がある(Go 1.11 より)。

type Dialer struct {
    ...
    // If Control is not nil, it is called after creating the network
    // connection but before actually dialing.
    //
    // Network and address parameters passed to Control method are not
    // necessarily the ones passed to Dial. For example, passing "tcp" to Dial
    // will cause the Control function to be called with "tcp4" or "tcp6".
    Control func(network, address string, c syscall.RawConn) error
}

というわけで、Dialer.Control に関数を設定することで、実際の接続を確立する直前で待ったをかけることができる。とくに、名前解決などをすべておこなったあとで呼ばれるのも安心だ。

この Control に設定する関数の中で、接続先の IP アドレスが内部のものでないかどうかをチェックすることができれば、当初の目的が素直に達成できそうなので、そういうものを書いてみた。

motemen/go-nuts/netutil.NetworkBlocklist というもので、ブロックすべきネットワークを指定すれば、Dialer.Control に渡せる関数を作ってくれる。http.RoundTripper のような高級なものはあえて提供しなくても簡単に構成できるので、API としてはない。Example を見れば一目瞭然:

transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DialContext = (&net.Dialer{
    Timeout:   30 * time.Second,
    KeepAlive: 30 * time.Second,
    Control:   PrivateNetworkBlocklist.Control,
}).DialContext

client := &http.Client{
    Transport: transport,
}

_, err := client.Get("http://[::1]/")
fmt.Println(err)
Get "http://[::1]/": dial tcp [::1]:80: host is blocked (Loopback Address)

この PrivateNetworkBlocklist というのが、内部 IP アドレスをブロックしてくれる定義済みの値になっている。

IPv4、v6 ともに対応していて、とはいえとくに v6 では何がプライベートネットワークなのかよくわからないので、IANA IPv6 Special-Purpose Address Registry より、"Globally Reachable" が True でないものをすべてブロックすることにしている。このページ自体は letsencrypt/boulder で知った。なんか変なことしてたら教えてください。

はてなで一緒に働きませんか?