詩と創作・思索のひろば

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

Fork me on GitHub

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 で知った。なんか変なことしてたら教えてください。

Go の関数を context 対応するツール

最初から完璧な設計と実装ができているなら苦労はないわけだけど、実際にはそうもいかない。具体的にはある程度の規模になってくると「あーこの関数 context.Context 対応したい!」みたいな気持ちが湧いてくるわけです。context 対応ってのは、第一引数に ctx context.Context を追加することですね。

そういうことをやるツールを書きました。

GitHub - motemen/go-ctxize: Rewrite functions to have "Context"s

go get github.com/motemen/go-ctxize/cmd/goctxize で、goctxize というバイナリが手に入ります。

サンプル

README やテストにある例だけど、

// $GOPATH/src/example.com/foo/foo.go
package foo

func F() {
}

// $GOPATH/src/example.com/foo/foo_test.go
package foo

import "testing"

func TestF(t *testing.T) {
    F()
}

// $GOPATH/src/example.com/bar/bar.go
package bar

import (
    "example.com/foo"
)

func bar() {
    foo.F()
}

という感じのコードにおいて、foo.F() のシグネチャが foo.F(ctx context.Context) であってほしい! という場合。

goctxize example.com/foo.F

としてやると foo.go が以下のように書き換わります。

// $GOPATH/src/example.com/foo/foo.go
package foo

import "context"

func F(ctx context.Context) {
}

// $GOPATH/src/example.com/foo/foo_test.go
package foo

import (
    "context"
    "testing"
)

func TestF(t *testing.T) {
    ctx := context.TODO()

    F(ctx)
}

ほんとは example.com/foo.F を呼び出しているほかのパッケージも書き換えたいわけなので、第2引数以降に追加のパッケージを指定してやると:

goctxize example.com/foo.F example.com/bar

bar.go もこうなります。

// $GOPATH/src/example.com/bar/bar.go
package bar

import (
    "context"
    "example.com/foo"
)

func bar() {
    ctx := context.TODO()

    foo.F(ctx)
}

コンテキストの推測

呼び出し側のファイルを書き換える際、*gin.Context みたいに context.Context を実装している変数がすでに呼び出し側のスコープに存在しているときはその変数を使ってくれます。かしこいですね。スコープ周辺の判定はけっこう雑。

// before
package web

import (
    "example.com/foo"
    "github.com/gin-gonic/gin"
)

func web(c *gin.Context) {
    foo.F()
}

// after
package web

import (
    "example.com/foo"
    "github.com/gin-gonic/gin"
)

func web(c *gin.Context) {
    foo.F(c)
}

また、コンテキスト化される関数の中に context.TODO() で初期化されている変数があったら、その変数を消し去ることもしてくれます。

// before
package bar

import (
        "context"

        "example.com/foo"
)

func alreadyHasCtxInside() { // これを ctx 化する
        ctx := context.TODO()
        _ = ctx
}

// after
package bar

import (
        "context"

        "example.com/foo"
)

func alreadyHasCtxInside(ctx context.Context) {

        _ = ctx
}

パッケージの指定

フルパスでパッケージを指定するのは煩雑なので、相対パスによる指定もできます:

# PWD=$GOPATH/src/example.com
goctxize ./foo.F ./bar ./baz

とか。go list と同じ解釈をするので、./... もいけます。

これは x/tools/go/packages パッケージによって実現されていて、作ってる途中で浦島太郎的にこの存在を知ったんだけど(Go 1.11 からの登場)、これがすげー便利。これまで静的解析をおこなうツールを書くとき、複数のパッケージにまたがって型情報を読み込みたいときには x/tools/go/loader を使うのが定石だった(と思う)のだけど、これは ./... 的な引数をうまく解釈することができない不都合があって、やや物足りなかったのです。Go モジュールに対応していることもあり、今後はこれを使うのが標準となる予定みたいですね。

どうぞご利用ください。

パッケージ指定で "go run" する

ちょっとしたコマンドラインツールを作ってるときはよく go run main.go するんだけど、作業ディレクトリが main.go から離れてしまうと go run $GOPATH/src/... みたいなことをする羽目になり、ちょっとありがたくない。ファイルが増えてくると go run *.go することになるが、テストコード(*_test.go)が入ってくると go run できないので、そいつを除いてやる必要もあり面倒。

そこで go list を使ってパッケージ名からソースコードを一覧し、それを go run に渡してやる簡単なシェルスクリプトを書いた。

GitHub - motemen/gorun: Run Go programs by their package path

使い方はこんな感じ。

usage: gorun [-l] [-tags tags] packages [arguments...]

gorun github.com/motemen/ghq みたいに、main パッケージへのパスを指定すると go run 的なことをしてくれます。手元のソースコードを動かしたいなら、go run main.go の代わりに gorun . すればよい。

-l フラグをつけると go run する代わりにソースコードをリストアップしてくれるので、entr みたいなのに渡すのに便利。

実際に go run するところは

go run "${flags[@]}" -exec "bash -c 'shift; exec \"\$0\" \"\$@\"'" "${files[@]}" -- "$@"

となっているんだけど、この -exec がミソで、go run するプログラムの引数に *.go なファイルを渡したいようなときに、プログラムのソースコードとプログラムへの引数を -- によって分割する、というようなことをおこなっている。

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