diff --git a/text/0000-io-net-2.1.md b/text/0000-io-net-2.1.md new file mode 100644 index 00000000000..377bb34d7a1 --- /dev/null +++ b/text/0000-io-net-2.1.md @@ -0,0 +1,527 @@ +- Feature Name: `net2` +- Start Date: 2015-05-07 +- RFC PR: (leave this empty) +- Rust Issue: (leave this empty) + +# Summary + +Expand the surface area of `std::net` to bind more low-level interfaces and +provide more advanced customization and configuration of sockets. + +# Motivation + +Right now the API of `std::net` is fairly conservative in what it exposes, and +there's quite a bit more functionality that can be exposed in a cross-platform +manner. Some examples of tasks which cannot be accomplished through `std::net` +APIs today are: + +* The high-level functions `TcpStream::connect`, `TcpListener::bind`, and + `UdpSocket::bind` actually encompass a few discrete steps as part of each + operation. Each step can possibly have configuration specified inbetween as + well as having extra parameters not specified to the top-level function. These + operations should be decomposable into independent operations. + +* There are a number of existing methods for setting the various options for a + socket, but they are far from comprehensive and haven't quite yet been + prepared for expansion. Many new socket options will likely be added to the + standard library over time so there needs to be room and design space for + these options. + +* A few remaining clarification issues remain around cross-platform + compatibility and behavior of APIs in various corner cases, and this RFC + attempts to clarify these areas. + +# Detailed design + +This RFC proposes APIs for each of the points listed in the motivation above, +split into relevant sections. + +### std::net::TcpBuilder + +The following API is proposed to supplement the existing types in the `std::net` +module: + +```rust +pub struct TcpBuilder { ... } + +impl TcpBuilder { + /// Constructs a new TcpBuilder with either the `AF_INET` or `AF_INET6` + /// domain, the `SOCK_STREAM` type, and with a protocol argument of 0. + /// + /// Note that passing other kinds of flags or arguments can be done through + /// the `FromRaw{Fd,Socket}` implementation. + pub fn new_v4() -> io::Result; + pub fn new_v6() -> io::Result; + + /// Binds this socket to the specified address. + /// + /// This function directly corresponds to the bind(2) function on Windows + /// and Unix. + pub fn bind(&self, addr: T) -> io::Result<&TcpBuilder>; + + /// Mark a socket as ready to accept incoming connection requests using + /// accept() + /// + /// This function directly corresponds to the listen(2) function on Windows + /// and Unix. + /// + /// An error will be returned if `listen` or `connect` has already been + /// called on this builder. + pub fn listen(&self, backlog: i32) -> io::Result; + + /// Initiate a connection on this socket to the specified address. + /// + /// This function directly corresponds to the connect(2) function on Windows + /// and Unix. + /// + /// An error will be returned if `listen` or `connect` has already been + /// called on this builder. + pub fn connect(&self, addr: T) -> io::Result; + + // socket options (specified below). +} + +impl FromRaw{Fd,Socket} for TcpBuilder { ... } +impl AsRaw{Fd,Socket} for TcpBuilder { ... } +``` + +The purpose of this API is to provide fine-grained control over the construction +of a TCP socket. It teases apart the convenience methods of `TcpStream::connect` +and `TcpListener::bind` by allowing configuration inbetween and tweaking the +options here and there. + +### std::net::UdpBuilder + +Similar to the `TcpBuilder` above, a `UdpBuilder` struct will also be added: + +```rust +pub struct UdpBuilder { ... } + +impl UdpBuilder { + /// Constructs a new UdpBuilder with either the `AF_INET` or `AF_INET6` + /// domain, the `SOCK_DGRAM` type, and with a protocol argument of 0. + /// + /// Note that passing other kinds of flags or arguments can be done through + /// the `FromRaw{Fd,Socket}` implementation. + pub fn new_v4() -> io::Result; + pub fn new_v6() -> io::Result; + + /// Binds this socket to the specified address. + /// + /// This function directly corresponds to the bind(2) function on Windows + /// and Unix. + pub fn bind(&self, addr: T) -> io::Result; + + // socket options (specified below). +} + +impl FromRaw{Fd,Socket} for UdpBuilder { ... } +impl AsRaw{Fd,Socket} for UdpBuilder { ... } +``` + +### Socket Options + +Currently there are no stable APIs to set the socket options for a socket, but +there are a number of unstable APIs exposed. Additionally, there is quite an +array of socket options that can both be platform specific or not available at +all on some platforms. This RFC proposes a set of conventions for binding socket +options in the standard library, and the existing set of options will be audited +for this convention and then new ones can be exposed over time. + +Each socket option is assigned an identifier `foo` and an associated value for +the option `T`. For each appropriate type of `TcpListener`, `TcpStream`, +`UdpSocket`, and `Socket`, the following inherent methods will be implemented: + +```rust +pub fn foo(&self) -> io::Result; +pub fn set_foo(&self, t: T) -> io::Result<()>; +``` + +The `TcpBuilder` and `UdpBuilder` types will only have the "setter" variants of +the functions for now and will return `&Self` to enable chaining. + +```rust +pub fn foo(&self, t: T) -> io::Result<&Self>; +``` + +This mirrors the getter/setter conventions in the rest of Rust today, but one +notable difference is that `set_foo` takes `&self`. This aspect mirrors the +ability of the underlying system to deal with concurrent modification of socket +options. Note that some socket options do not have a "get" variant as it doesn't +always make sense. + +The existing options today will be stabilized as follows: + +* `nodelay: bool` - the `TCP_NODELAY` option, and this is only available on + `TcpStream`. +* `keepalive: Option` - same as today's `set_keepalive`, but this will + differ slightly in terms of implementation with the rest of the socket options + here. On Unix `None` will simply set `SO_KEEPALIVE` to `false`, while + `Some(dur)` will set `SO_KEEPALIVE` to `true` and `TCP_KEEP{ALIVE,IDLE}` to + the specified `dur`. On Windows the implementation will start being wired up + to one call to modifying the `SIO_KEEPALIVE_VALS` option (this is + unimplemented today). This will be available on `TcpStream`. +* `read_timeout: Option`, `write_timeout: Option` - these + are covered by [RFC 1047][socket-timeouts] however and already fit within + these conventions. This option will be available on all sockets. +* `broadcast: bool` - the `SO_BROADCAST` option, only available on `UdpSocket`. +* `multicast_loop_v4: bool` - the `IP_MULTICAST_LOOP` option, available on + `UdpSocket`. +* `multicast_ttl_v4: u32` - the `IP_MULTICAST_TTL` option, available on + `UdpSocket`. +* `ttl: u32` - the `IP_TTL` option, available on all sockets. +* `multicast_loop_v6: bool` - the `IPV6_MULTICAST_LOOP` option, available on + `UdpSocket`. +* `multicast_ttl_v6: u32` - the `IPV6_MULTICAST_TTL` option, available on + `UdpSocket`. +* `only_v6: bool` - the `IPV6_V6ONLY` option, available on all sockets. + +Some socket options with only setters will be exposed as methods: + +* `fn join_multicast_v4(&self, multiaddr: &Ipv4Addr, interface: &Ipv4Addr)` - + corresponding to the `IP_ADD_MEMBERSHIP` option. +* `fn join_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32)` - + corresponding to the `IPV6_ADD_MEMBERSHIP` option. +* `fn leave_multicast_v4(&self, multiaddr: &Ipv4Addr, interface: &Ipv4Addr)` - + corresponding to the `IP_DROP_MEMBERSHIP` option. +* `fn leave_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32)` - + corresponding to the `IPV6_DROP_MEMBERSHIP` option. + +[socket-timeouts]: https://github.com/rust-lang/rfcs/pull/1047 + +### Cross-platform behavior and `IPV6_V6ONLY` + +It is pointed out by @stepancheg in [the std::net expansion issue][issue] that +an IPv6 socket created on Windows will by default not be able to accept IPv4 +connections. This is a cross-platform hazard and may end up causing some code to +work on Linux (which has the opposite behavior by default) but not elsewhere. + +[issue]: https://github.com/rust-lang/rfcs/issues/957 + +As pointed out [in the man +page](http://man7.org/linux/man-pages/man7/ipv6.7.html), however, it's possible +to configure the default at a global level for Linux as well. Despite this, to +increase cross-platform interoperability, sockets created by `TcpListener::bind` +and `TcpStream::connect` will have this option set to `false` by default on *all +platforms*. + +### `ToSocketAddrs` and multiple addresses + +APIs today that consume an argument of the type `ToSocketAddrs` are somewhat +ambiguous on how they use the list of addresses returned. Some APIs will attempt +each address while some will only use one address. The new guidelines will be: + +* The `Tcp{Stream,Builder}::connect` function will attempt each address in-order + of what's returned from the associated iterator until one succeeds. This helps + the "default usage" of `TcpStream::connect("rust-lang.org:80")` to work a + little more easily. +* All other methods will return an error if 0 or more than 1 address is resolved + to. This includes `UdpSocket::{bind, send_to}` and `TcpListener::bind`. This + change is primarily a tweak in semantics to requiring precisely one address + for these operations instead of ignoring other addresses or trying to use + them. + +# Drawbacks + +The builder-style API of `{Tcp,Udp}Builder` is not following the conventional +builder guidelines due to the ability that each intermediate step being able to +fail and the desire to understand at precisely what point each operation failed +at. This hinders the ergonomics of the API, however, as the `try!` macro would +need to be leveraged quited a bit or some other error-handling mechanism would +be required. This means that usage today would look like: + +```rust +try!(try!(try!(try!(TcpBuilder::new_v6()) + .nodelay(true)) + .keepalive(Some(Duration::new(0, 500)))) + .read_timeout(None)) +``` + +If, however, the [`?` operator][trait-exns] is implemented the builder API +could be used quite fluently: + +```rust +TcpBuilder::new_v6()? + .nodelay(true)? + .keepalive(Some(Duration::new(0, 500))? + .read_timeout(None); +``` + +[trait-exns]: https://github.com/rust-lang/rfcs/pull/243 + +All-in-all having a builder-style API today to prepare for this in the future +shouldn't be a hindrance, and it just means that today's usage will probably +avoid chaining of API calls. + +This API also does not cover all the use cases of using sockets today. For +example a Unix socket-style connection would not be covered by any of these +builders and would not benefit from the APIs in the standard library. +Additionally, other kinds of sockets (such as raw sockets) would also not +benefit as they would have to have many of the same bindings out-of-std as well. + +# Alternatives + +## An "all powerful" std::net::Socket + +An alternative approach to the above builder API would be one of an "all +powerful" `std::net::Socket` which is the thinnest layer over the OS as +possible. For example: + +```rust +// A raw OS socket which can be configured and then transformed into a higher +// level abstraction such as a `TcpListener`, `TcpStream`, or `UdpSocket`. +pub struct Socket { ... } + +// Arguments to the `Socket::new` function +pub struct Domain { val: c_int } +pub struct Type { val: c_int } +pub struct Protocol { val: c_int } + +// Representation of a raw socket address that is generally passed to other +// low-level functions. This structure is generally generated via `.into()` on a +// reference to a normal address. +pub struct RawSocketAddr { + pub addr: *const sockaddr, + pub len: socklen_t, +} + +impl Socket { + /// Creates a new socket with the specified arguments. + /// + /// This function directly corresponds to the socket(2) function on Unix, + /// and translates to a call to `WSASocket` on Windows with the + /// `WSA_FLAG_OVERLAPPED` and `WSA_FLAG_NO_HANDLE_INHERIT` flags specified. + pub fn new(domain: Domain, + type_: Type, + protocol: Option) -> io::Result; + + /// Binds this socket to the specified address. + /// + /// This function directly corresponds to the bind(2) function on Windows + /// and Unix. + pub fn bind>(&self, addr: &T) -> io::Result<()>; + + /// Mark a socket as ready to accept incoming connection requests using + /// accept() + /// + /// This function directly corresponds to the listen(2) function on Windows + /// and Unix. + pub fn listen(&self, backlog: i32) -> io::Result<()>; + + /// Initiate a connection on this socket to the specified address. + /// + /// This function directly corresponds to the connect(2) function on Windows + /// and Unix. + pub fn connect>(&self, addr: &T) -> io::Result<()>; + + /// Accept a new socket from this socket. + /// + /// This function directly corresponds to the accept(2) function on Windows + /// and Unix. + pub fn accept(&self) -> io::Result<(Socket, SocketAddr)>; + + /// Same as `try_clone` on other primitives. + pub fn try_clone(&self) -> io::Result; + + /// Same as `shutdown` on other primitives. + pub fn shutdown(&self, how: Shutdown) -> io::Result<()> + + // socket options (specified in the next section) .. + + /// Same as `UdpSocket::send_to`, the `flags` parameter to the underlying + /// syscall is set to 0. + pub fn send_to(&self, data: &[u8], addr: A) + -> io::Result; + + /// Same as `UdpSocket::recv_from`, the `flags` parameter to the underlying + /// syscall is set to 0. + pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)>; +} + +// Standard I/O operations +impl Read for Socket { ... } +impl Read for &Socket { ... } +impl Write for Socket { ... } +impl Write for &Socket { ... } + +// Conversion traits/types for `Socket` +impl AsRaw{Fd,Socket} for Socket { ... } +impl From for TcpListener { ... } +impl From for TcpStream { ... } +impl From for UdpSocket { ... } +impl<'a> Into for &'a SocketAddr { ... } + +// Common constructors, conversions, and inspectors for the various arguments to +// `Socket::new`. Platform-specific constructors can be provided through +// platform specific extension traits +impl Domain { + pub fn ipv4() -> Domain; + pub fn ipv6() -> Domain; + pub fn value(&self) -> c_int; +} +impl From for Domain { ... } + +impl Type { + pub fn stream() -> Type; + pub fn datagram() -> Type; + pub fn raw() -> Type; + pub fn value(&self) -> c_int; +} +impl From for Type { ... } + +impl From for Protocol { ... } +``` + +The upside of this approach is that it is maximally flexible in terms of usage. +All interactions with C APIs can be directly translated to Rust itself with ease +and there is a clear layer of abstraction for what's going on here. The +downsides of this approach, however, are: + +* It's unclear why the `TcpStream` and other refined primitives still exist. + This API is a union of the `TcpStream`, `TcpListener`, and `UdpSocket` APIs + and poses a new question of "should I use a Socket or TcpStream?". +* The scope of this API is somewhat unclear, for example is `Socket` intended to + encompass Unix sockets as well? It attempts to via the `Into` + trait bound, but it's arguably not super ergonomic. +* Many mis-usages of the API which would statically be prevented (such as + calling `accept` on a UDP socket) are allowed with a single `Socket` API. + Other examples of this are setting TCP options for a datagram socket. + +Overall the builder-based approach seemed to give the same level of flexibility +while maintaining a clearly defined purpose of solely operating as builders for +new sockets. + +### A note on typestate + +Note that there are many alternatives within this alternative itself (of having +a unifying notion of a "socket"), such as using typestate to solve the mis-usage +bullet point above. Sockets are often a classical use case of a state-transition +diagram, so this can be a natural conclusion to come to. + +There are, however, not that many usages of typestate throughout the rest of the +standard library today, and there are a number of ergonomic and API concerns +which would be difficult to overcome. + +## Socket Options + +The detailed design section above outlined a convention for adding a pair of +methods per socket option, but this has the drawback of leading to quite a large +API surface area and it's tough to view the set of "options related methods" in +a group. An alternative to the scheme proposed above would be something along +the lines of the following: + +```rust +mod net::opt { + pub trait Sockopt { + type Value; + fn get(socket: &Socket) -> io::Result; + fn set(socket: &Socket, value: Self::Value) -> io::Result<()>; + } + + // socket options + pub enum KeepAlive {} // SO_KEEPALIVE, Value = bool + pub enum ReuseAddress {} // SO_REUSEADDR, Value = bool + pub enum Broadcast {} // SO_BROADCAST, Value = bool + pub enum ReadTimeout {} // SO_RCVTIMEO, Value = Option + pub enum WriteTimeout {} // SO_SNDTIMEO, Value = Option + + // tcp options + pub enum TcpNodelay {} // TCP_NODELAY, Value = bool + + // ipv4 options + pub enum AddMembershipV4 {} // IP_ADD_MEMBERSHIP, Value = MulticastRequestV4 + pub enum DropMembershipV4 {} // IP_DROP_MEMBERSHIP, Value = MulticastRequestV4 + pub enum MulticastLoopV4 {} // IP_MULTICAST_LOOP, Value = bool + pub enum MulticastTtlV4 {} // IP_MULTICAST_TTL, Value = u32 + pub enum TtlV4 {} // IP_TTL, Value = u32 + + // ipv6 options + pub enum AddMembershipV6 {} // IPV6_ADD_MEMBERSHIP, Value = MulticastRequestV6 + pub enum DropMembershipV6 {} // IPV6_DROP_MEMBERSHIP, Value = MulticastRequestV6 + pub enum MulticastLoopV6 {} // IPV6_MULTICAST_LOOP, Value = bool + pub enum OnlyV6 {} // IPV6_V6ONLY, Value = bool + + pub struct MulticastRequestV6 { + pub multiaddr: Ipv6Addr, + pub interface: u32, + } +} + +mod os::unix::net { + pub enum TcpKeepAlive {} // TCP_KEEP{ALIVE,IDLE}, Value = Duration + // others as necessary +} + +impl {Socket,TcpListener,TcpStream,UdpSocket} { + pub fn set(&self, val: T::Value) -> io::Result<()> { + T::set(self, val) + } + + pub fn get(&self) -> io::Result { + T::get(self) + } +} + +// example usage +use std::net::opt::{KeepAlive, TcpKeepAlive}; + +socket.set::(true); +socket.set::(duration); +``` + +With this API there's a strict transcribing of the conventions proposed above +with the `Sockopt` trait, and the major upside of this approach is that +libraries can define their own socket options (via a new type) which can use the +same syntax as all the standard socket options for being get/set. There are, +however, a number of downsides to this approach: + +* Setting an option requires at least one import. +* Socket options are now grouped together, but discoverability isn't necessarily + improved as there are still lots of implementors that need to be waded + through. +* It's not possible to statically prevent TCP options from being set on a UDP + socket (which will likely always generate an error at runtime). +* Lots of new types are introduced just for adding names, which is somewhat + unconventional in the standard library right now. + +### `TcpListener::bind_to_port` + +Requested, in [the std::net expansion issue][issue], it's noted that a common +desire is to simply bind a listener to a port not worrying about the address. +This RFC does not at this time add this sort of convenience method. + +### Enhancing `lookup_host` and `lookup_addr` + +These two bindings of `getaddrinfo` and `getnameinfo` are currently unstable yet +are quite useful from time to time. This RFC does not propose stabilizing these +APIs or reconsidering them at this time. The possible known drawbacks of the API +are: + +* The `getaddrinfo` and `getnameinfo` function calls have a number of arguments + which are not exposed at this layer of the API (e.g. flags, `serv`, `host`, + `hints`, etc). Additionally, the return value for `getaddrinfo` is not fully + exposed. + +* The return value of these APIs can be dubious by turning up duplicates. + +* There is a desire to possibly filter the return value based on certain + criteria. + +Overall these functions may want to be tweaked slightly to have different +arguments and/or return values after the underlying implementation has been +investigated more. At this time this RFC does not strive to handle these APIs. + +### Asynchronous I/O + +Networking primitives are one of the primary users of asynchronous I/O +utilities, and there are clear cross-platform abstractions for building this +sort of functionality such as `select` and setting a socket to nonblocking mode. + +This RFC does not, however, attempt to set forth a vision for nonblocking +programming with sockets. It is considered in a few locations (such as setting +`WSA_FLAG_OVERLAPPED` for sockets on Windows), but no explicit bindings are +proposed to be added at this time. + +# Unresolved questions + +None, yet.