Skip to content

Datagrams sent to an IP literal, and a v6only switch for UDP sockets - #9

Open
neutrino38 wants to merge 6 commits into
witchtails:masterfrom
neutrino38:fix/udp-ipv6
Open

neutrino38 wants to merge 6 commits into
witchtails:masterfrom
neutrino38:fix/udp-ipv6

Conversation

@neutrino38

Copy link
Copy Markdown

Base. This branch sits on the TCP/SSL literal PR #8,
which itself sits on #7.
Take the three in that order.

Two changes, one commit each.

Socket.Datagram.send/3 takes a destination in every form
Socket.Address.t() covers
: a tuple, a literal such as "::1", the bracketed
"[::1]" that a URI authority or a Host header carries, or a host name. A
literal is sent as a tuple. Only a real host name is resolved.

Socket.UDP.open/2 takes v6only: true | false, which sets the socket's
ipv6_v6only. One open/2 call now expresses the family, the bind address and
this switch.

Tests

Two tests in test/ipv6_test.exs are added

Elixir 1.18.3 / OTP 26: mix format --check-formatted passes, mix test is 16
tests / 0 failures over four runs, and mix compile --warnings-as-errors fails
with exactly the nine warnings master already has, at the same sites.

A URI authority and a Host header carry an IPv6 literal inside square
brackets (RFC 3986 §3.2.2), and that is the form a caller holding an
address taken from a URI has in hand. parse/1 answered nil on it, so
every such caller had to strip the brackets itself, and the one who
forgot dialed the wrong address family with no diagnostic.

Only a v6 address is written that way: "[127.0.0.1]" stays nil, and so
does a bracket that never closes.
connect/3 turned every binary into a charlist and handed it to
:gen_tcp.connect/4, which resolves it as a name in the socket's family.
"::1" therefore came back :nxdomain on a socket nobody had declared
:inet6, while a tuple carries its own family. Parse first, and fall back
to a charlist only for what really is a host name.

The @SPEC already announced :inet.ip_address(), which the code could not
handle. It now says Socket.Address.t(), which it does.
connect/3 carried the same charlist conversion as Socket.TCP.connect/3,
with the same effect: the literal went to the resolver instead of to the
socket.

Its test reads a failure rather than a success — no TLS server answers on
the other end — because :nxdomain is what the resolution path returns and
a handshake timeout is not.
Socket.Datagram.send/3 takes a destination address in every form
Socket.Address.t() covers, and a literal goes to :gen_udp as a tuple.

Two things follow. The bracketed form a URI or a Host header carries,
"[::1]", now reaches its peer. And a destination given as a literal costs
no name resolution, on any datagram.
open/2 takes `v6only: true | false` and maps it to the socket's
ipv6_v6only. It is the last IPv6 setting the wrapper could not spell, and
an option it does not know raises rather than passes through.

It matters beyond the socket: an IPv6 socket that also accepts IPv4
receives from those peers under ::ffff:a.b.c.d, and every protocol
message that writes a peer address then carries that form. Only the
socket can be told otherwise.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant