詩と創作・思索のひろば

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

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

LWPで"dh key too small"ってなったときの対処

ひさびさに Perl の話。結構前のことだけど、OS のバージョンを Debian 10 (Buster) にしたら LWP で一部のサイトにアクセスできなくなってしまった。

500 Can't connect to ***.com:443 (SSL connect attempt failed error:141A318A:SSL routines:tls_process_ske_dhe:dh key too small) at ...

こんな感じ。

OpenSSL のグローバルな設定を変更するとこのへんの挙動は変えられるとのことだけど、当然そんなことをしたいわけではないので、Perl (LWP) 側から設定したかった。結局このようになった。

my $ua = LWP::UserAgent->new(
    ssl_opts => {
        SSL_create_ctx_callback => sub {
            my $ctx = shift;
            Net::SSLeay::CTX_set_security_level($ctx, 1);
        },
    }
);

ネストが深いが、こういう流れ。

LWP::UserAgent->new に渡す ssl_opts の値が IO::Socket::SSL に渡されるので、ここでディープな設定を仕込むことができる。

SSL_create_ctx_callback というフックで、Net::SSLeay によって作成された ctx という値を受け取れる。この ctx は OpenSSL 由来のもので、これに対して Net::SSLeay の 低レイヤの API を使って OpenSSL を直接触ることができるようになる。ここでおもむろに SSL_CTX_get_security_level を呼ぶことで、冒頭の問題を解決することができるのです。

ところで SSLeay ってなんやねん! ってずっと思ってたけど OpenSSL の前身がこういう名前だったらしい。なるほどなー。いや eay ってなんなん?

参考にしたもの

ts-nodeで実行中かどうか確認する

TypeScript でユーティリティ的なスクリプトを開発しているとき、毎回 tsc でコンパイルして node で実行するのはあまりに重たいのでやってられない。

ts-node というのでコンパイル作業なしに直接 .ts ファイルを実行できるんだけど、.ts ファイルとコンパイル後の .js ファイルの位置が異なっているので微妙に困ることがある。ファイルからの相対位置で何かを指定するような場合ですね。

なので ts-node 下での実行かどうかを判定したい。process.argv[0] らへんを見ると素の node との違いはわかるんだけど、本質的な情報ではないのでちょっと気持ち悪いところがある。

と思って調べていたらちょうどそういう話があって、結局、以下のようにして調べられるらしい。process を拡張してるんですね。なるほど。

import { REGISTER_INSTANCE } from "ts-node";

const tsService = process[REGISTER_INSTANCE]; // これがあれば ts-node で実行中

ただ、これだと ts-node が成果物の依存に入ってしまうので、REGISTER_INSTANCE の代わりに Symbol.for('ts-node.register.instance') を使ったほうがいいかもしれない。どっちも微妙。

ちなみに process[REGISTER_INSTANCE] には TypeScript コンパイラの設定が入ってるようなので、ここからさらに詳細な設定を知ることも可能そうではある。

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