Skip to content

Commit 23b431b

Browse files
authored
Add :inet4 transport option to disable IPv4 fallback (#425)
1 parent 74e0ec6 commit 23b431b

File tree

5 files changed

+301
-12
lines changed

5 files changed

+301
-12
lines changed

lib/mint/core/transport/ssl.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ defmodule Mint.Core.Transport.SSL do
323323

324324
defp connect(address, hostname, port, opts) do
325325
timeout = Keyword.get(opts, :timeout, @default_timeout)
326+
inet4? = Keyword.get(opts, :inet4, true)
326327
inet6? = Keyword.get(opts, :inet6, false)
327328

328329
opts = ssl_opts(String.to_charlist(hostname), opts)
@@ -334,8 +335,11 @@ defmodule Mint.Core.Transport.SSL do
334335
{:ok, sslsocket} ->
335336
{:ok, sslsocket}
336337

337-
_error ->
338+
_error when inet4? ->
338339
wrap_err(:ssl.connect(address, port, opts, timeout))
340+
341+
error ->
342+
wrap_err(error)
339343
end
340344
else
341345
# Use the defaults provided by ssl/gen_tcp.
@@ -428,7 +432,7 @@ defmodule Mint.Core.Transport.SSL do
428432
default_ssl_opts(hostname)
429433
|> Keyword.merge(opts)
430434
|> Keyword.merge(@transport_opts)
431-
|> Keyword.drop([:timeout, :inet6])
435+
|> Keyword.drop([:timeout, :inet4, :inet6])
432436
|> add_verify_opts(hostname)
433437
|> remove_incompatible_ssl_opts()
434438
|> add_ciphers_opt()

lib/mint/core/transport/tcp.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ defmodule Mint.Core.Transport.TCP do
1919
opts = Keyword.delete(opts, :hostname)
2020

2121
timeout = Keyword.get(opts, :timeout, @default_timeout)
22+
inet4? = Keyword.get(opts, :inet4, true)
2223
inet6? = Keyword.get(opts, :inet6, false)
2324

2425
opts =
2526
opts
2627
|> Keyword.merge(@transport_opts)
27-
|> Keyword.drop([:alpn_advertised_protocols, :timeout, :inet6])
28+
|> Keyword.drop([:alpn_advertised_protocols, :timeout, :inet4, :inet6])
2829

2930
if inet6? do
3031
# Try inet6 first, then fall back to the defaults provided by
@@ -33,8 +34,11 @@ defmodule Mint.Core.Transport.TCP do
3334
{:ok, socket} ->
3435
{:ok, socket}
3536

36-
_error ->
37+
_error when inet4? ->
3738
wrap_err(:gen_tcp.connect(address, port, opts, timeout))
39+
40+
error ->
41+
wrap_err(error)
3842
end
3943
else
4044
# Use the defaults provided by gen_tcp.

lib/mint/http.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,12 @@ defmodule Mint.HTTP do
299299
seconds), and may be overridden by the caller. Set to `:infinity` to
300300
disable the connect timeout.
301301
302+
* `:inet6` - if set to `true` enables IPv6 connection. Defaults to `false`
303+
and may be overridden by the caller.
304+
305+
* `:inet4` - if set to `true` falls back to IPv4 if IPv6 connection fails.
306+
Defaults to `true` and may be overridden by the caller.
307+
302308
Options for `:https` only:
303309
304310
* `:alpn_advertised_protocols` - managed by Mint. Cannot be overridden.

test/mint/core/transport/ssl_test.exs

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,15 @@ defmodule Mint.Core.Transport.SSLTest do
186186

187187
task =
188188
Task.async(fn ->
189-
with {:ok, socket} <- :ssl.transport_accept(listen_socket) do
190-
if function_exported?(:ssl, :handshake, 1) do
191-
{:ok, _} = apply(:ssl, :handshake, [socket])
192-
else
193-
:ok = apply(:ssl, :ssl_accept, [socket])
194-
end
195-
196-
{:ok, socket}
189+
{:ok, socket} = :ssl.transport_accept(listen_socket)
190+
191+
if function_exported?(:ssl, :handshake, 1) do
192+
{:ok, _} = apply(:ssl, :handshake, [socket])
193+
else
194+
:ok = apply(:ssl, :ssl_accept, [socket])
197195
end
196+
197+
{:ok, socket}
198198
end)
199199

200200
assert {:ok, _socket} =
@@ -208,6 +208,73 @@ defmodule Mint.Core.Transport.SSLTest do
208208

209209
assert {:ok, _server_socket} = Task.await(task)
210210
end
211+
212+
test "can fall back to IPv4 if IPv6 fails" do
213+
ssl_opts = [
214+
mode: :binary,
215+
packet: :raw,
216+
active: false,
217+
reuseaddr: true,
218+
nodelay: true,
219+
certfile: Path.expand("../../../support/mint/certificate.pem", __DIR__),
220+
keyfile: Path.expand("../../../support/mint/key.pem", __DIR__)
221+
]
222+
223+
{:ok, listen_socket} = :ssl.listen(0, ssl_opts)
224+
{:ok, {_address, port}} = :ssl.sockname(listen_socket)
225+
226+
task =
227+
Task.async(fn ->
228+
{:ok, socket} = :ssl.transport_accept(listen_socket)
229+
230+
if function_exported?(:ssl, :handshake, 1) do
231+
{:ok, _} = apply(:ssl, :handshake, [socket])
232+
else
233+
:ok = apply(:ssl, :ssl_accept, [socket])
234+
end
235+
236+
{:ok, socket}
237+
end)
238+
239+
assert {:ok, _socket} =
240+
SSL.connect("localhost", port,
241+
active: false,
242+
inet6: true,
243+
timeout: 1000,
244+
verify: :verify_none
245+
)
246+
247+
assert {:ok, _server_socket} = Task.await(task)
248+
end
249+
250+
test "does not fall back to IPv4 if IPv4 is disabled" do
251+
ssl_opts = [
252+
:inet,
253+
mode: :binary,
254+
packet: :raw,
255+
active: false,
256+
reuseaddr: true,
257+
nodelay: true,
258+
certfile: Path.expand("../../../support/mint/certificate.pem", __DIR__),
259+
keyfile: Path.expand("../../../support/mint/key.pem", __DIR__)
260+
]
261+
262+
{:ok, listen_socket} = :ssl.listen(0, ssl_opts)
263+
{:ok, {_address, port}} = :ssl.sockname(listen_socket)
264+
265+
Task.async(fn ->
266+
{:ok, _socket} = :ssl.transport_accept(listen_socket)
267+
end)
268+
269+
assert {:error, %Mint.TransportError{reason: :econnrefused}} =
270+
SSL.connect("localhost", port,
271+
active: false,
272+
inet6: true,
273+
inet4: false,
274+
timeout: 1000,
275+
verify: :verify_none
276+
)
277+
end
211278
end
212279

213280
describe "controlling_process/2" do

test/mint/core/transport/tcp_test.exs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
defmodule Mint.Core.Transport.TCPTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Mint.Core.Transport.TCP
5+
6+
describe "connect/3" do
7+
test "can connect to IPv6 addresses" do
8+
tcp_opts = [
9+
:inet6,
10+
mode: :binary,
11+
packet: :raw,
12+
active: false,
13+
reuseaddr: true,
14+
nodelay: true
15+
]
16+
17+
{:ok, listen_socket} = :gen_tcp.listen(0, tcp_opts)
18+
{:ok, {_address, port}} = :inet.sockname(listen_socket)
19+
20+
task =
21+
Task.async(fn ->
22+
{:ok, _socket} = :gen_tcp.accept(listen_socket)
23+
end)
24+
25+
assert {:ok, _socket} =
26+
TCP.connect({127, 0, 0, 1}, port,
27+
active: false,
28+
inet6: true,
29+
timeout: 1000
30+
)
31+
32+
assert {:ok, _server_socket} = Task.await(task)
33+
end
34+
35+
test "can fall back to IPv4 if IPv6 fails" do
36+
tcp_opts = [
37+
:inet6,
38+
mode: :binary,
39+
packet: :raw,
40+
active: false,
41+
reuseaddr: true,
42+
nodelay: true
43+
]
44+
45+
{:ok, listen_socket} = :gen_tcp.listen(0, tcp_opts)
46+
{:ok, {_address, port}} = :inet.sockname(listen_socket)
47+
48+
task =
49+
Task.async(fn ->
50+
{:ok, _socket} = :gen_tcp.accept(listen_socket)
51+
end)
52+
53+
assert {:ok, _socket} =
54+
TCP.connect("localhost", port,
55+
active: false,
56+
inet6: true,
57+
timeout: 1000
58+
)
59+
60+
assert {:ok, _server_socket} = Task.await(task)
61+
end
62+
63+
test "does not fall back to IPv4 if IPv4 is disabled" do
64+
tcp_opts = [
65+
:inet,
66+
mode: :binary,
67+
packet: :raw,
68+
active: false,
69+
reuseaddr: true,
70+
nodelay: true
71+
]
72+
73+
{:ok, listen_socket} = :gen_tcp.listen(0, tcp_opts)
74+
{:ok, {_address, port}} = :inet.sockname(listen_socket)
75+
76+
Task.async(fn ->
77+
{:ok, _socket} = :gen_tcp.accept(listen_socket)
78+
end)
79+
80+
assert {:error, %Mint.TransportError{reason: :econnrefused}} =
81+
TCP.connect("localhost", port,
82+
active: false,
83+
inet6: true,
84+
inet4: false,
85+
timeout: 1000
86+
)
87+
end
88+
end
89+
90+
describe "controlling_process/2" do
91+
@describetag :capture_log
92+
93+
setup do
94+
parent = self()
95+
ref = make_ref()
96+
97+
ssl_opts = [
98+
mode: :binary,
99+
packet: :raw,
100+
active: false,
101+
reuseaddr: true,
102+
nodelay: true
103+
]
104+
105+
spawn_link(fn ->
106+
{:ok, listen_socket} = :gen_tcp.listen(0, ssl_opts)
107+
{:ok, {_address, port}} = :inet.sockname(listen_socket)
108+
send(parent, {ref, port})
109+
110+
{:ok, socket} = :gen_tcp.accept(listen_socket)
111+
112+
send(parent, {ref, socket})
113+
114+
# Keep the server alive forever.
115+
:ok = Process.sleep(:infinity)
116+
end)
117+
118+
assert_receive {^ref, port} when is_integer(port), 500
119+
120+
{:ok, socket} = TCP.connect("localhost", port, [])
121+
assert_receive {^ref, server_socket}, 200
122+
123+
{:ok, server_port: port, socket: socket, server_socket: server_socket}
124+
end
125+
126+
test "changing the controlling process of a active: :once socket",
127+
%{socket: socket, server_socket: server_socket} do
128+
parent = self()
129+
ref = make_ref()
130+
131+
# Send two SSL messages (that get translated to Erlang messages right
132+
# away because of "nodelay: true"), but wait after each one so that
133+
# it actually arrives and we can set the socket back to active: :once.
134+
:ok = TCP.setopts(socket, active: :once)
135+
:ok = :gen_tcp.send(server_socket, "some data 1")
136+
Process.sleep(100)
137+
138+
:ok = TCP.setopts(socket, active: :once)
139+
:ok = :gen_tcp.send(server_socket, "some data 2")
140+
141+
wait_until_passes(500, fn ->
142+
{:messages, messages} = Process.info(self(), :messages)
143+
assert {:tcp, socket, "some data 1"} in messages
144+
assert {:tcp, socket, "some data 2"} in messages
145+
end)
146+
147+
other_process = spawn_link(fn -> process_mirror(parent, ref) end)
148+
149+
assert :ok = TCP.controlling_process(socket, other_process)
150+
151+
assert_receive {^ref, {:tcp, ^socket, "some data 1"}}
152+
assert_receive {^ref, {:tcp, ^socket, "some data 2"}}
153+
154+
refute_received _message
155+
end
156+
157+
test "changing the controlling process of a passive socket",
158+
%{socket: socket, server_socket: server_socket} do
159+
parent = self()
160+
ref = make_ref()
161+
162+
:ok = :gen_tcp.send(server_socket, "some data")
163+
164+
other_process =
165+
spawn_link(fn ->
166+
assert_receive message, 500
167+
send(parent, {ref, message})
168+
end)
169+
170+
assert :ok = TCP.controlling_process(socket, other_process)
171+
assert {:ok, [active: false]} = TCP.getopts(socket, [:active])
172+
:ok = TCP.setopts(socket, active: :once)
173+
174+
assert_receive {^ref, {:tcp, ^socket, "some data"}}, 500
175+
176+
refute_received _message
177+
end
178+
179+
test "changing the controlling process of a closed socket",
180+
%{socket: socket} do
181+
other_process = spawn_link(fn -> :ok = Process.sleep(:infinity) end)
182+
183+
:ok = TCP.close(socket)
184+
185+
assert {:error, _error} = TCP.controlling_process(socket, other_process)
186+
end
187+
end
188+
189+
defp process_mirror(parent, ref) do
190+
receive do
191+
message ->
192+
send(parent, {ref, message})
193+
process_mirror(parent, ref)
194+
end
195+
end
196+
197+
defp wait_until_passes(time_left, fun) when time_left <= 0 do
198+
fun.()
199+
end
200+
201+
defp wait_until_passes(time_left, fun) do
202+
fun.()
203+
rescue
204+
_exception ->
205+
Process.sleep(10)
206+
wait_until_passes(time_left - 10, fun)
207+
end
208+
end

0 commit comments

Comments
 (0)