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

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

Open
Clockwork-Muse opened this issue May 9, 2023 · 13 comments
Open

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

Clockwork-Muse opened this issue May 9, 2023 · 13 comments
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-net Module: tokio/net

Comments

@Clockwork-Muse
Copy link

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.

@Clockwork-Muse Clockwork-Muse added A-tokio Area: The main tokio crate C-feature-request Category: A feature request. labels May 9, 2023
@Darksonn Darksonn added the M-net Module: tokio/net label May 10, 2023
@Darksonn
Copy link
Contributor

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

@Clockwork-Muse
Copy link
Author

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
Copy link
Contributor

Darksonn commented May 10, 2023

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

@Clockwork-Muse
Copy link
Author

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
Copy link

Hawk777 commented Dec 2, 2023

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
Copy link
Contributor

Darksonn commented Dec 8, 2023

@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
Copy link

Hawk777 commented Dec 8, 2023

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
Copy link
Contributor

Darksonn commented Dec 8, 2023

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
Copy link

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
Copy link
Contributor

@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
Copy link
Author

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
Copy link

@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
Copy link

Hawk777 commented Jan 31, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-net Module: tokio/net
Projects
None yet
Development

No branches or pull requests

4 participants