Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bind may incorrectly create a dual-stack socket on some platforms #130668

Open
Zoxc opened this issue Sep 21, 2024 · 10 comments
Open

bind may incorrectly create a dual-stack socket on some platforms #130668

Zoxc opened this issue Sep 21, 2024 · 10 comments
Labels
C-bug Category: This is a bug. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@Zoxc
Copy link
Contributor

Zoxc commented Sep 21, 2024

UdpSocket::bind and TcpListener::bind can on some platforms incorrectly create a dual-stack socket. It seems very unlikely that these are intended to create dual-stack sockets as the API and documentation make no mention of it. There's also no alternate API that allow for the creation of IPv6 wildcard sockets. This means that if these API were intended to create dual-stack sockets, the standard library would be missing essential IPv6 functionality.

Additional there are good reasons to not have or add an API for dual-stack sockets in the standard library:

  • Dual-stack sockets are not portable.
  • They are unneccesary as you can use multiple sockets instead.
  • They are limited in functionality as you cannot bind to a IPv4 / IPv6 pair.

Users not needing portability and wanting dual-stack sockets, may want to use a 3rd party crate (for example socket2) to create dual-stack sockets.

This bug have some rather bad consequences:

  • This means that IPv4 may unexpectedly be accessible over the network which can be a security vulnerability. This is particularly bad if a user tests on a correctly behaving platform, but deploys on another.
  • It's not possible to create portable IPv6 wildcard servers with std.
  • It's not possible to create portable dual-stack wildcard servers with std.

This affects (at least):

  • Linux
  • macOS

Not affected:

  • Windows
  • OpenBSD (does not support dual-stack sockets)
@Zoxc Zoxc added the C-bug Category: This is a bug. label Sep 21, 2024
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Sep 21, 2024
@workingjubilee workingjubilee added the T-libs Relevant to the library team, which will review and decide on the PR/issue. label Sep 21, 2024
@the8472
Copy link
Member

the8472 commented Sep 21, 2024

You should list under which circumstances.

@Zoxc
Copy link
Contributor Author

Zoxc commented Sep 21, 2024

This applies when binding to Ipv6Addr::UNSPECIFIED. For example TcpListener::bind((Ipv6Addr::UNSPECIFIED, port)).

@cuviper
Copy link
Member

cuviper commented Sep 25, 2024

The relevant socket option is IPV6_V6ONLY, which is off (and thus dual-stack) by default on many platforms, as RFC 3493 section 5.3 describes. Linux does have a system wide control for this, sysctl net.ipv6.bindv6only, but the default there is still 0. Windows seems to be the exception in enabling this by default.

Rust did expose this option long ago, but not in a useful way: #33052

@cuviper
Copy link
Member

cuviper commented Sep 25, 2024

Personally, I don't think it's valid to characterize the current behavior as incorrect, but I can understand that it may be surprising. IMO, the way forward would be to design an explicit API like the old set_only_v6, but actually doing its job. Maybe we could add a net::BindOptions type similar to fs::OpenOptions?

@the8472
Copy link
Member

the8472 commented Sep 25, 2024

A general unbound socket type that can be configured before being bound and turned into a more specific type is probably better than an options builder. the socket configuration API is vast and has a lot of niche platform-specific/special-socket-type stuff.

@Zoxc
Copy link
Contributor Author

Zoxc commented Oct 6, 2024

@cuviper What the underlying platform APIs look like is not relevant other than the constraints the place on std's API. They do all support IPv6 wildcard sockets however, so that can't be used as an argument for why std does not always create IPv6 wildcard sockets. What matters here is the intent of the bind API.

I find it rather unlikely that std was intended to not support IPv6 wildcard and dual-stack wildcard servers like you suggest. I expect std to properly support IPv6 and while the API design does, the actual platform implementations are lacking. Adding new APIs instead of actually fixing the implementation is a terrible solution and leaves the current API as a security and correctness trap.

@himikof
Copy link

himikof commented Oct 9, 2024

A general unbound socket type that can be configured before being bound and turned into a more specific type is probably better than an options builder. the socket configuration API is vast and has a lot of niche platform-specific/special-socket-type stuff.

The socket2 crate which follows this paradigm was already mentioned. This is literally the first example in its documentation:

use std::net::{SocketAddr, TcpListener};
use socket2::{Socket, Domain, Type};

// Create a TCP listener bound to two addresses.
let socket = Socket::new(Domain::IPV6, Type::STREAM, None)?;

socket.set_only_v6(false)?;
let address: SocketAddr = "[::1]:12345".parse().unwrap();
socket.bind(&address.into())?;
socket.listen(128)?;

let listener: TcpListener = socket.into();

But I think that having a simple way to explicitly set only_v6 without resorting to such low-level code would be an appropriate addition to the standard library regardless of having a flexible socket2-like type in it (which I personally think is very useful, too).

Maybe we could add a net::BindOptions type similar to fs::OpenOptions?

Actually, the TcpStream::connect/TcpStream::connect_timeout have the same fundamental problem. So maybe net::BindOptions/net::ConnectOptions? Or net::SocketCreationOptions?

What matters here is the intent of the bind API.

Yes, and the intent/semantics of the common bind API are (in this part) described in RFC 3493, sections 3.7 and 5.3. The RFC authors were very explicit that they intend the dual-stack sockets to be both mandatory and the default in the implementations:

Applications may use AF_INET6 sockets to open TCP connections to IPv4
nodes, or send UDP packets to IPv4 nodes, by simply encoding the
destination's IPv4 address as an IPv4-mapped IPv6 address, and
passing that address, within a sockaddr_in6 structure, in the
connect() or sendto() call.

As stated in section <3.7 Compatibility with IPv4 Nodes>,
AF_INET6 sockets may be used for both IPv4 and IPv6 communications.
Some applications may want to restrict their use of an AF_INET6
socket to IPv6 communications only. For these applications the
IPV6_V6ONLY socket option is defined.

Unfortunately, the current situation is indeed not consistent between platforms, and the default cannot be relied on. So an explicit toggle is needed both to ensure dual-stack sockets are used and are not used.

If such only_v6 toggle is available in the standard library, a separate question arises: what the default value should be if the user did specify nothing. There are 3 obvious options: off (following the RFC), on (as suggested in this issue), or following the platform defaults (as it is currently). But I'm also pretty sure that changing the existing TcpListener::bind API would be prohibited by the API stability requirements, so any change here would result in creation of new APIs anyway.

@Zoxc
Copy link
Contributor Author

Zoxc commented Oct 9, 2024

Yes, and the intent/semantics of the common bind API are (in this part) described in RFC 3493

RFC 3493 is irrelevant. It does not specify std. It does not even specify any Rust API. std does not try to follow it either, otherwise some variant of IPV6_V6ONLY would have been in the initial design.

This is simply an implementation bug in std.

@cuviper
Copy link
Member

cuviper commented Oct 9, 2024

RFC 3493 is irrelevant. It does not specify std.

IPv6 is not an invention of Rust std -- the RFC is relevant to expectations and interoperability at large.

If such only_v6 toggle is available in the standard library, a separate question arises: what the default value should be if the user did specify nothing. There are 3 obvious options: off (following the RFC), on (as suggested in this issue), or following the platform defaults (as it is currently).

I think following the platform default makes sense, not only because it's the status quo, but it's also hard to achieve that otherwise -- especially with local system choices like the Linux sysctl that you would have to check at runtime.

@Zoxc
Copy link
Contributor Author

Zoxc commented Oct 9, 2024

IPv6 is not an invention of Rust std -- the RFC is relevant to expectations and interoperability at large.

RFC 3493 also not specify IPv6. C users of Windows and OpenBSD would also have different expectations.

I think following the platform default makes sense

Only Linux seems to have a concept of a platform default. Regardless std's bind does not aim to follow your definition of a platform default. I don't see a strong use case for adding such an API, but it should diverge from bind by returning the actual bound IP protocols.

@Noratrieb Noratrieb removed the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Nov 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

7 participants