tokio-rs / tokio

A runtime for writing reliable asynchronous applications with Rust. Provides I/O, networking, scheduling, timers, ...
https://tokio.rs
MIT License
27.2k stars 2.5k forks source link

net::UnixListener: Allow binding/using an existing socket #5678

Open Clockwork-Muse opened 1 year ago

Clockwork-Muse commented 1 year ago

Is your feature request related to a problem? Please describe. UnixListener::bind(...) fails if the socket file already exists, which will be the case for systemd-managed socket entries (eg, a some-systemd-service.socket file). The purpose of creating such a socket entry is that the systemd service will not be started until after the first connection is made to the socket, which means that with ephemeral or intermittent services they are not required to be constantly running.

Describe the solution you'd like Provide a method to bind to an existing socket file.

Describe alternatives you've considered Deleting the socket file is expressly forbidden by the systemd documentation. There may be additional ways to configure systemd/tokio to work around this, but these will be arcane or require unsafe code. Otherwise, a constantly-running service will be required.

Additional context Add any other context or screenshots about the feature request here.

Darksonn commented 1 year ago

It sounds like you're looking for way to set an option on the socket. Which option is it?

Clockwork-Muse commented 1 year ago

From which side, rust/tokio, or the OS?

I've found some (potential - I haven't tried them yet) rust/tokio examples of how to set this up, but it's somewhat involved, needing to create a manual socket in std rust and then wiring up a stream manually.

I'd like to avoid that, and feel that it's something that probably should be streamlined for other users.

I'm not entirely sure what all needs to happen in terms of setting up a socket, but my guess is that some sort of accept-like constructor would be needed (the current method is an instance method, which would require binding first, which isn't possible here, obviously)

Darksonn commented 1 year ago

For example, an example of doing this correctly in C would help me understand the requirements better.

Clockwork-Muse commented 1 year ago

Alas, I don't know how to do this in C/C++.

Generally what happens is that you end up with a systemd socket definition that looks like this:

#Socket file (/etc/systemd/system/mysocket.socket)
[Unit]
Description=My Socket

[Socket]
ListenStream=/run/mysocket.sock

[Install]
WantedBy=sockets.target
#Service file (/etc/systemd/system/mysocket.service)
[Unit]
Description=My Service

[Service]
ExecStart=/usr/bin/myprogram /run/mysocket.sock

[Install]
WantedBy=multi-user.target

... and then the calling program is supposed to "be able to accept connections on the socket", whatever that means.

Um. I was assuming that the actual UDS socket file path was all that was needed, but looking at some of the verbiage in the actual systemd socket documentation it looks like instead it would pass a different file type. I have found examples of using this method, but they involve unsafe code since they're grabbing the raw file descriptor.
I dunno, I'm not very well versed on that part.

Hawk777 commented 12 months ago

What happens here is that systemd invokes your binary with more file descriptors (3+, rather than only the usual stdin/stdout/stderr). Those extra descriptors are the listening sockets. It tells you how many there are by means of the LISTEN_FDS environment variable (there’s also LISTEN_PID to tell you which process is supposed to access them or whether they belong to someone else). Your binary is supposed to start accepting connections from them rather than binding to anything itself. Each listening socket could be TCP or it could be UNIX-domain.

I think you can already do this, although it takes jumping through a few hoops:

  1. Read the environment variable to figure out how many listening sockets there are, and then do the remaining steps for each one.
  2. Calculate the file descriptor number, which is just consecutive starting from 3 for the first socket.
  3. Create a std::os::fd::OwnedFd via from_raw_fd; this is the only unsafe step in this process.
  4. Create a std::net::TcpListener via that type’s From<OwnedFd> impl.
  5. Create a tokio::net::TcpListener via that type’s from_std function.

Unfortunately Tokio doesn’t seem to have a “generic listener” which accepts sockets of any type, so to be properly general, you’d actually want to check the FD type (with getsockopt(SO_DOMAIN) between steps 3 and 4, and if it returns AF_UNIX you’d want to go through UnixListener, TcpListener for AF_INET or AF_INET6, and probably bail out for other values).

Maybe there’s room for Tokio to add some support to make a few of these steps easier?

Darksonn commented 11 months ago

@Hawk777 Thank you for explaining!

We have a Listener trait in tokio-util, and we could provide a method there to call getsockopt(SO_DOMAIN) and turn that into an Either<UnixListener, TcpListener>.

Hawk777 commented 11 months ago

To use a trait method, wouldn’t you have to have already figured out whether the socket is TCP or UNIX so you can decide with trait impl to call the method on? Whereas the issue here is that you don’t know yet. Maybe a free function that takes a socket, internally calls getsockopt(SO_DOMAIN), and returns Result<Either<UnixListener, TcpListener>, …>? It needs to be fallible because (1) getsockopt(SO_DOMAIN) could fail if you pass it a closed FD, something that’s not a socket, etc., and (2) even if it’s a socket, it could be neither TCP nor UNIX (even if you trust that the caller is systemd and is only passing legitimate sockets, I think it could also be AF_VSOCK if you’re running as PID 1 in a VM or container or something; if you don’t have that level of trust, it could be any of a couple dozen other address families).

Darksonn commented 11 months ago

Sorry, to clarify, I did not mean to add the function to the trait, but to add a free-standing method to the tokio_util::net module. The trait is relevant because it allows you to treat the Either as a listener once you have it. (see #6201.)

pronebird commented 10 months ago

On a related note, it seems that UnixListener does not remove the socket file after shutdown. Is it something that perhaps can be accounted for too?

Darksonn commented 10 months ago

@pronebird hopefully we have the same behavior as std. If there's an issue, then please open a new issue. Otherwise it will get lost.

Clockwork-Muse commented 10 months ago

Note that from what I posted originally, I believe that in my case I'm not supposed to remove the socket file, since it would be maintained by systemd itself.

pronebird commented 10 months ago

@pronebird hopefully we have the same behavior as std. If there's an issue, then please open a new issue. Otherwise it will get lost.

After digging around I figured that it’s indeed how things work not only rust but in C too. So the behaviour is consistent.

Hawk777 commented 10 months ago

Note that from what I posted originally, I believe that in my case I'm not supposed to remove the socket file, since it would be maintained by systemd itself.

In case of a systemd socket, yes, you shouldn’t unlink it because it’s not yours to manage (in fact, if you restart a service, systemd will usually keep the socket open so there’s zero downtime and the new socket instance can start accepting from the same socket).

After digging around I figured that it’s indeed how things work not only rust but in C too. So the behaviour is consistent.

The kernel doesn’t automatically unlink the socket. Userspace code—whether that’s the application or some library—is responsible for doing that sometime (whether on termination or at startup) before binding the new listener.