fix(gun): connect to hosts that only have IPv6 addresses
Gun resolves host names over IPv4 only unless :inet6 is in tcp_opts. When the A lookup finds nothing, retry once with :inet6. Gun now runs under its supervisor, so a failed connect no longer kills the pool worker through the link; the worker still links after a successful open.
This commit is contained in:
parent
3e9dc477c9
commit
6f31149d21
3 changed files with 101 additions and 3 deletions
1
changelog.d/gun-ipv6-only-hosts.fix
Normal file
1
changelog.d/gun-ipv6-only-hosts.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Gun adapter: connect to hosts that only have IPv6 (AAAA) addresses
|
||||||
|
|
@ -14,7 +14,9 @@ defmodule Pleroma.Gun.Conn do
|
||||||
opts
|
opts
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
|> Map.put_new(:connect_timeout, pool_opts[:connect_timeout] || 5_000)
|
|> Map.put_new(:connect_timeout, pool_opts[:connect_timeout] || 5_000)
|
||||||
|> Map.put_new(:supervise, false)
|
# Under gun's supervisor, a failed connect does not kill the caller
|
||||||
|
# through the link. Gun still stops when its owner goes down.
|
||||||
|
|> Map.put_new(:supervise, true)
|
||||||
|> maybe_add_tls_opts(uri)
|
|> maybe_add_tls_opts(uri)
|
||||||
|
|
||||||
do_open(uri, opts)
|
do_open(uri, opts)
|
||||||
|
|
@ -117,8 +119,7 @@ defmodule Pleroma.Gun.Conn do
|
||||||
host = Pleroma.HTTP.AdapterHelper.parse_host(host)
|
host = Pleroma.HTTP.AdapterHelper.parse_host(host)
|
||||||
opts = Map.put(opts, :transport, transport(uri.scheme))
|
opts = Map.put(opts, :transport, transport(uri.scheme))
|
||||||
|
|
||||||
with {:ok, conn} <- Gun.open(host, port, opts),
|
with {:ok, conn, protocol} <- open_direct(host, port, opts) do
|
||||||
{:ok, protocol} <- await_up(conn, opts[:connect_timeout]) do
|
|
||||||
{:ok, conn, protocol, nil}
|
{:ok, conn, protocol, nil}
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
|
|
@ -130,6 +131,40 @@ defmodule Pleroma.Gun.Conn do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp open_direct(host, port, opts) do
|
||||||
|
result =
|
||||||
|
with {:ok, conn} <- Gun.open(host, port, opts),
|
||||||
|
{:ok, protocol} <- await_up(conn, opts[:connect_timeout]) do
|
||||||
|
{:ok, conn, protocol}
|
||||||
|
end
|
||||||
|
|
||||||
|
case inet6_fallback(result, host, opts) do
|
||||||
|
{:ok, inet6_opts} -> open_direct(host, port, inet6_opts)
|
||||||
|
:error -> result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gun's default when :tcp_opts is not set.
|
||||||
|
@default_tcp_opts [send_timeout: 15_000, send_timeout_close: true]
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gun resolves hostnames with `inet_tcp` (A records only) unless `:inet6` is
|
||||||
|
in `:tcp_opts`, for both the :tcp and :tls transports. When the A lookup
|
||||||
|
finds nothing, returns the options for one more attempt over IPv6.
|
||||||
|
"""
|
||||||
|
@spec inet6_fallback(term(), Pleroma.HTTP.AdapterHelper.host(), map()) :: {:ok, map()} | :error
|
||||||
|
def inet6_fallback({:error, {:down, {:shutdown, :nxdomain}}}, host, opts) when is_list(host) do
|
||||||
|
tcp_opts = Map.get(opts, :tcp_opts, @default_tcp_opts)
|
||||||
|
|
||||||
|
if :inet6 in tcp_opts do
|
||||||
|
:error
|
||||||
|
else
|
||||||
|
{:ok, Map.put(opts, :tcp_opts, [:inet6 | tcp_opts])}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def inet6_fallback(_result, _host, _opts), do: :error
|
||||||
|
|
||||||
defp destination_opts(%URI{host: host, port: port}) do
|
defp destination_opts(%URI{host: host, port: port}) do
|
||||||
host = Pleroma.HTTP.AdapterHelper.parse_host(host)
|
host = Pleroma.HTTP.AdapterHelper.parse_host(host)
|
||||||
%{host: host, port: port}
|
%{host: host, port: port}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ defmodule Pleroma.Gun.ConnTest do
|
||||||
|
|
||||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8080, opts ->
|
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8080, opts ->
|
||||||
assert opts.transport == :tcp
|
assert opts.transport == :tcp
|
||||||
|
assert opts.supervise
|
||||||
refute Map.has_key?(opts, :tls_opts)
|
refute Map.has_key?(opts, :tls_opts)
|
||||||
{:ok, conn}
|
{:ok, conn}
|
||||||
end)
|
end)
|
||||||
|
|
@ -172,6 +173,67 @@ defmodule Pleroma.Gun.ConnTest do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "IPv6-only hosts" do
|
||||||
|
@nxdomain {:error, {:down, {:shutdown, :nxdomain}}}
|
||||||
|
|
||||||
|
test "retries once over IPv6 when the IPv4 lookup finds no address" do
|
||||||
|
ipv4_conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||||
|
ipv6_conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||||
|
|
||||||
|
expect(Pleroma.GunMock, :open, 2, fn
|
||||||
|
~c"v6only.example", 443, %{tcp_opts: tcp_opts} = opts ->
|
||||||
|
assert :inet6 in tcp_opts
|
||||||
|
assert opts.transport == :tls
|
||||||
|
assert opts.tls_opts[:verify] == :verify_peer
|
||||||
|
{:ok, ipv6_conn}
|
||||||
|
|
||||||
|
~c"v6only.example", 443, opts ->
|
||||||
|
refute Map.has_key?(opts, :tcp_opts)
|
||||||
|
# A linked Gun process would take the caller down with the lookup failure.
|
||||||
|
assert opts.supervise
|
||||||
|
{:ok, ipv4_conn}
|
||||||
|
end)
|
||||||
|
|
||||||
|
stub(Pleroma.GunMock, :await_up, fn
|
||||||
|
^ipv4_conn, _ -> @nxdomain
|
||||||
|
^ipv6_conn, _ -> {:ok, :http2}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, ^ipv6_conn, :http2, nil} =
|
||||||
|
Conn.open(URI.parse("https://v6only.example/inbox"), [])
|
||||||
|
|
||||||
|
stop_conn(ipv4_conn)
|
||||||
|
stop_conn(ipv6_conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not retry IP literals or other errors" do
|
||||||
|
expect(Pleroma.GunMock, :open, 2, fn _, _, _ ->
|
||||||
|
{:ok, spawn(fn -> Process.sleep(:infinity) end)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
stub(Pleroma.GunMock, :await_up, fn _, _ -> @nxdomain end)
|
||||||
|
assert @nxdomain = Conn.open(URI.parse("https://192.0.2.1/inbox"), [])
|
||||||
|
|
||||||
|
stub(Pleroma.GunMock, :await_up, fn _, _ ->
|
||||||
|
{:error, {:down, {:shutdown, :econnrefused}}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, _} = Conn.open(URI.parse("https://v6only.example/inbox"), [])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "inet6_fallback/3 keeps configured TCP options and skips hosts already on IPv6" do
|
||||||
|
assert {:ok, %{tcp_opts: [:inet6, nodelay: true]}} =
|
||||||
|
Conn.inet6_fallback(@nxdomain, ~c"v6only.example", %{tcp_opts: [nodelay: true]})
|
||||||
|
|
||||||
|
assert {:ok, %{tcp_opts: [:inet6, send_timeout: 15_000, send_timeout_close: true]}} =
|
||||||
|
Conn.inet6_fallback(@nxdomain, ~c"v6only.example", %{})
|
||||||
|
|
||||||
|
assert :error = Conn.inet6_fallback(@nxdomain, ~c"v6only.example", %{tcp_opts: [:inet6]})
|
||||||
|
assert :error = Conn.inet6_fallback(@nxdomain, {192, 0, 2, 1}, %{})
|
||||||
|
assert :error = Conn.inet6_fallback({:error, :timeout}, ~c"v6only.example", %{})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp stop_conn(conn) do
|
defp stop_conn(conn) do
|
||||||
Process.unlink(conn)
|
Process.unlink(conn)
|
||||||
Process.exit(conn, :kill)
|
Process.exit(conn, :kill)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue