Open5

Erlang/OTP の HTTP クライアントで IPv6

voluntasvoluntas

概要

  • Erlang/OTP の IPv6 で HTTP を送信するためには、明示的に inet6 を指定する必要があり使いづらい
  • 事前に inet_res で AAAA が引けるかどうかを確認するといった仕組みが必要になる
voluntasvoluntas

Gun

  • HTTP クライアントは Gunhttpc を利用する
  • TLS の CaCerts は public_key:cacerts_get() を利用する
  • DNS には AAAA のみ指定する

で、これだと普通にアクセスできない。やっかいすぎる。

gun の tcp_opts で inet6 を指定

gun:open の opts で #{tcp_opts => [inet6]} を指定する

ただしこの設定をすると、ipv4 へのアクセスが微妙な挙動をする。

httpc の set_options で ipfamily に inet6 を指定

httpc:set_options([{ipfamily, inet6}])

voluntasvoluntas

Gun では Hostname からの IPv6 判定はできない

Gun のソースコードを追いかけると domain_lookupinet:tcp_module/2 というのが見つかる。

https://github.com/ninenines/gun/blob/master/src/gun_tcp.erl#L47

この Mod で AAAA レコードのみが設定されたホスト名で inet6_tcp が返ってくるが良いのだが、 inet_tcp が返ってきてしまうのが問題。

OTP 側の inet:tcp_module/2 を見ると、どうやら IPv6 かどうかの判断をしているが、
inet:hostanme() の場合は tcp_opts に依存することがわかった。

https://github.com/erlang/otp/blob/master/lib/kernel/src/inet.erl#L1166

つまり現時点では inet6 を gun:open の Opts の tcp_opts に [inet6] として指定する必要がある。
inet6 を指定すると inet6_tcp が返ってくるのは確認できた。

voluntasvoluntas

inet:tcp_module/2 の挙動

ipv6.example.com というのが AAAA レコードだけ登録されたホスト名とする。

> inet:tcp_module([], "ipv6.example.com").
{inet_tcp,[]}
> inet:tcp_module([inet6], "ipv6.example.com").
{inet6_tcp,[]}

つまりホスト名から inet6_tcp を取得することはできず、ホスト名から inet_res:lookup などで IPv6 が存在するかどうかをチェックして inet6 を渡す必要がある。

だるい。

voluntasvoluntas

httpc:request/1 の挙動

  • ipv6.example.com というのが AAAA レコードだけ登録されたホスト名とする。
  • OTP 標準の httpc も set_options で inet6 を指定しないと正常に動作しない
$ erl
Erlang/OTP 26 [erts-14.1.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit:ns]

Eshell V14.1.1 (press Ctrl+G to abort, type help(). for help)
1> application:ensure_all_started(ssl).
{ok,[crypto,asn1,public_key,ssl]}
2> application:ensure_all_started(inets).
{ok,[inets]}
3> httpc:request("https://ipv6.example.com/").
{error,{failed_connect,[{to_address,{"ipv6.example.com",443}},
                        {inet,[inet],nxdomain}]}}
4> httpc:set_options([{ipfamily, inet6}]).
ok
5> httpc:request("https://ipv6.example.com/").
{ok,{{"HTTP/1.1",200,"OK"},
     [{"connection","keep-alive"},
      {"date","Sat, 25 Nov 2023 04:06:13 GMT"},
      {"server","nginx/1.18.0 (Ubuntu)"},
      {"content-length","2"},
      {"content-type","application/json"}],
     "{}"}}