NAME
Fugu::Control, Fugu::Control::Client - a control socket for a running daemon
SYNOPSIS
# In the daemon
use Fugu::Control;
my $control = Fugu::Control->new(path => '/var/run/myd/control.sock');
$control->register(status => sub ($args) { { paired => 1 } });
$control->register(devices => sub ($args) { [ @devices ] });
$control->listen(loop => $loop) or die $control->error;
# and at shutdown
$control->shutdown(loop => $loop);
# In the tool
my $client = Fugu::Control::Client->new(
path => '/var/run/myd/control.sock');
my $status = $client->request('status');
unless ($status) {
die "the daemon is not running\n" if $client->socket_absent;
die $client->error . "\n";
}
DESCRIPTION
A tool that reports on a daemon has two ways to get its answer. It can guess from files on disk, or it can ask the daemon. The first way reads what the daemon wrote at some earlier time, and it is wrong whenever the two disagree. Fugu::Control is the second way.
The server registers with a Fugu::EventLoop and answers commands from process state. The client connects, sends one request, and reads the reply. The payload is JSON, and the transport is the imsg(3) framing of Fugu::Imsg.
Everything that arrives on the socket is untrusted. An unknown command, a payload that is not JSON, and a frame over the limit all give an error reply. None of them ends the daemon.
The server
new(%args)-
Make a server. The method opens nothing.
pathis the socket, and it is necessary.logis the logger, and it defaults to the process default of Fugu::Log. register($command, $code)-
Add a command. The code gets the decoded arguments hash reference and returns the reply, which must encode as JSON. A handler that dies gives the caller an error reply.
listen(%args)-
Bind the socket and start accepting. The listener and every accepted connection register with the event loop that
loopnames. That argument is necessary, and the method dies without it.modeis the socket mode, as an integer, with the default0600.groupis the socket group, as a name or as a numeric group id, with no default. A group name resolves withgetgrnam. An unresolvable name, a failed chown(2) and a failed chmod(2) are each a recoverable failure: the method takes the socket down, setserror(), and returnsundef, so a half-built socket never accepts a connection.The socket never accepts a wider set of users than the final set. With no
groupthe mode holds from birth, through a umask(2) guard. With agroupthe socket binds under the owner bits of the final mode, takes the group with chown(2), and widens tomodewith chmod(2) last. A daemon drops privileges first, and it callslistenafter. A process can chgrp its own file to a group that it belongs to, so the group form needs no root. accept_one($loop)-
Take one connection and register it as a read handler on the loop. Where
peer_supported()is true, the method reads the peer credentials of the connection once, with getsockopt(2) andSO_PEERCRED. A credential read that fails closes the connection at once, and the server logs the reason at the error level: a control socket that cannot name its peer must not answer. peer()-
The credentials of the connection that the server is answering now, as a hash reference with
uid,gidandpid: the effective user id, the effective group id, and the process id of the peer. A handler that needs the operator identity calls it, and a handler that does not need it ignores it.The method returns
undefoutside a handler call, andundefwherepeer_supported()is false. It holds no policy and reports three numbers. The group of the socket is the coarse gate, and the handler is the fine gate.On OpenBSD the read returns a
struct sockpeercred, which holds the user id, then the group id, then the process id. The field order differs from the Linuxstruct ucred, which holds the process id first, and one module must not carry two field orders. The method therefore reads the credentials on OpenBSD only, and it reports "not supported" everywhere else, throughpeer_supported(). peer_supported()-
A class method. It returns true only where the platform reports peer credentials in the
struct sockpeercredorder. A caller or a test uses it to tell "not supported" from "the read failed". shutdown(%args)-
Close every connection, close the listener, and remove the socket. A socket left behind names a daemon that is not there.
path()-
The socket path.
error()-
The most recent failure.
The client
new(%args)-
Make a client.
pathis necessary;timeoutis the per-frame deadline and defaults to 5 seconds. request($command, $args)-
Send one command and return the decoded reply, or
undefwith the reason inerror(). The method connects if it is not connected. connect(),disconnect()-
Open and close the connection by hand. Both are idempotent.
socket_absent()-
Report if the most recent failure was an absent socket, and not a refusal. A tool says "the daemon is not running" for the first and "the daemon said no" for the second. A tool that cannot tell them apart reports the wrong thing to an operator half the time.
A socket inside a directory that the caller may not search is not an absent socket.
connect()tells the two apart and reports the permission, because an operator who reads "not running" about a daemon that is running looks in the wrong place. error()-
The most recent failure.
RETURN VALUES
listen() returns the server object, or undef with the reason in error(). request() returns the decoded reply, or undef. socket_absent() and peer_supported() return 1 or 0. peer() returns a hash reference, or undef. Every other method returns its object.
FILES
The socket that the caller names. The server creates it mode 0600 by default, under a umask(2) guard, so it is never world-reachable even for an instant. The mode and group arguments of listen() widen it, and the socket still never accepts a wider set of users than the final set.
The mode of the socket is the inner boundary. The directory that holds it is the outer one, and the caller owns that. A daemon that drops privileges creates the directory while it is still root, mode 0700, owned by the daemon user.
ERRORS
new() dies without a path, and register() dies without a code reference. listen() dies for a mode that is not an integer from 0 to 0777. Those are programming errors.
Nothing that arrives on the socket makes anything die. A malformed request, an unknown command, and a handler that died all give an error reply, and the connection stays usable.
listen() refuses to take a socket that another process answers on, and replaces one that nothing is behind. bind(2) fails on an existing name, and a daemon that will not start after a crash needs a hand at every reboot.
SEE ALSO
imsg(3), Fugu::EventLoop, Fugu::Imsg, Fugu::Log
AUTHORS
Dick Olsson <hi@senzilla.io>
CAVEATS
The socket carries no secret. That is a rule for the caller, which writes the command handlers, and this module cannot enforce it. A daemon whose configuration holds a setup code and a broker password must not answer a command that echoes them back: the reply lands in the terminal of an operator who may not be alone.
One imsg frame carries about 16 KB. A larger reply spans several frames and the client puts them back together, up to one megabyte in total. A command that would answer with more than that gets an error instead.
A handler runs inside the event loop of the daemon. A handler that blocks blocks the daemon.