Open Clockwork-Muse opened 1 year ago
It sounds like you're looking for way to set an option on the socket. Which option is it?
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 bind
ing first, which isn't possible here, obviously)
For example, an example of doing this correctly in C would help me understand the requirements better.
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.
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:
std::os::fd::OwnedFd
via from_raw_fd
; this is the only unsafe step in this process.std::net::TcpListener
via that type’s From<OwnedFd>
impl.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?
@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>
.
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).
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.)
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?
@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.
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 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.
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.
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, asome-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.