Passage¶
Author: Max Kellermann <mk@cm4all.com>
Passage is a daemon which allows jailed/containerized processes to talk to the host to trigger certain actions.
Configuration¶
The file /etc/cm4all/passage/config.lua
is a Lua script which is executed at startup. It
contains at least one passage_listen()
call, for example:
passage_listen('/run/cm4all/passage/passage.sock', function(request)
return m:connect('192.168.1.99')
end)
The first parameter is the socket path to listen on. Passing the
global variable systemd
(not the string literal
"systemd"
) will listen on the sockets passed by systemd (from
unit cm4all-passage.socket
):
passage_listen(systemd, function(request) ...
To use this socket from within a container, move it to a dedicated directory and bind-mount this directory into the container. Mounting just the socket doesn’t work because a daemon restart must create a new socket, but the bind mount cannot be refreshed.
The second parameter is a callback function which shall decide what to do with an incoming request. This function receives a request object which can be inspected. Multiple listeners can share the same handler by declaring the function explicitly:
function handler(request)
if request.command == 'restart' then
return request:fade_children(control_address)
else
error("Unknown command")
end
end
passage_listen('/foo', handler)
passage_listen('/bar', handler)
It is important that the function finishes quickly. It must never block, because this would block the whole daemon process. This means it must not do any network I/O, launch child processes, and should avoid anything but querying the request’s parameters.
SIGHUP
¶
On systemctl reload cm4all-qrelay
(i.e. SIGHUP
), qrelay
calls the Lua function reload
if one was defined. It is up to the
Lua script to define the exact meaning of this feature.
Inspecting Incoming Requests¶
The following attributes can be queried:
command
: The command string.args
: An array containing command arguments.headers
: A table containing headers (name/value pairs).pid
: The client’s process id.uid
: The client’s user id.gid
: The client’s group id.cgroup
: The control group of the client process with the following attributes:path
: the cgroup path as noted in/proc/self/cgroup
, e.g./user.slice/user-1000.slice/session-42.scope
xattr
: A table containing extended attributes of the control group.parent
: Information about the parent of this cgroup; it is another object of this type (ornil
if there is no parent cgroup).
Actions¶
The handler function shall return an object describing what to do with the request. The request object contains several methods which create such action objects; they do not actually perform the action.
The following actions are possible:
fade_children(ADDRESS, TAG)
: send aFADE_CHILDREN
control packet to the given address. The address is either a string containing a (numeric) IP address, or an address object created bycontrol_resolve()
. If a tag is specified, then only children with this tag are addressed.flush_http_cache(ADDRESS, TAG)
: send aFLUSH_HTTP_CACHE
control packet to the given address. The address is either a string containing a (numeric) IP address, or an address object created bycontrol_resolve()
. The tag selects the cache items which shall be flushed.exec_pipe(PATH, ARG, ...)
: execute the given program (should be an absolute path because there is no$PATH
resolution here) and connect a pipe to its standard output; send the pipe’s reading side to the client.
Returning without an action from the handler function (i.e. returning
nil
) is considered a silent success.
If you encounter a problem, raise an exception by invoking the Lua
function error()
. The message passed to this function will be
logged.
Addresses¶
It is recommended to create all address objects during startup, to avoid putting unnecessary pressure on the Lua garbage collector, and to reduce the overhead for invoking the system resolver (which blocks Passage execution). The function control_resolve() creates such an address object:
server1 = control_resolve('192.168.0.2')
server2 = control_resolve('[::1]:4321')
server3 = control_resolve('server1.local:1234')
server4 = control_resolve('/run/server5.sock')
server5 = control_resolve('@server4')
These examples do the following:
convert a numeric IPv4 address to an address object (port defaults to 5478, the beng-proxy control standard port)
convert a numeric IPv6 address with a non-standard port to an address object
invoke the system resolver to resolve a host name to an IP address (which blocks passage startup; not recommended)
convert a path string to a “local” socket address
convert a name to an abstract “local” socket address (prefix ‘@’ is converted to a null byte, making the address “abstract”)
libsodium¶
There are some libsodium bindings.
bin = sodium.hex2bin("deadbeef") -- returns "\xde\xad\xbe\ef"
hex = sodium.bin2hex("A\0\xff") -- returns "4100ff"
key = sodium.randombytes(32)
pk, sk = sodium.crypto_box_keypair()
ciphertext = sodium.crypto_box_seal('hello world', pk)
message = sodium.crypto_box_seal_open(ciphertext, pk, sk)
`Point*scalar multiplication <https://doc.libsodium.org/advanced/scalar_multiplication>__:
pk = sodium.crypto_scalarmult_base(sk)
Security¶
This software and the Lua code used to configure it is very sensitive, because untrusted processes can send arbitrary data to it.
Never trust the information from the packet payload.
Do not try to establish an authentication protocol. If you want to
know who the client is, query those attributes which cannot be changed
by the client, such as cgroup membership and file system mounts.
Consider that the client may be able to create a new mount namespace
and change all mounts. If you have doubts about the client’s
identity, bail out (e.g. with Lua’s error()
function).
About Lua¶
Programming in Lua (a tutorial book), Lua 5.3 Reference Manual.
Note that in Lua, attributes are referenced with a dot
(e.g. m.sender
), but methods are referenced with a colon
(e.g. m:reject()
).
Usage¶
The Debian package cm4all-passage-client
contains a very
simple and generic client. The first parameter specifies the command,
and positional argument strings can be specified after that.
Example:
cm4all-passage-client fade_children
The option --header=NAME:VALUE
can be used to send headers
to the server.
By default, the client connects to /run/cm4all/passage/socket
,
but the option --server=PATH
can be used to change that:
cm4all-passage-client --server=/tmp/passage.socket fade_children
Protocol¶
The daemon listens on a local “sequential packet” socket
(AF_LOCAL
/ SOCK_SEQPACKET
).
The client sends a request in one packet, and each packet gets acknowledged by the server in a response packet. Both request and response share the same general structure:
COMMAND/STATUS [PARAM1 PARAM2 ...]\n
HEADER1: VALUE1\n
HEADER2: VALUE2\n
\0BINARY
A packet consists of at least one command (request) or status
(response). The command is an unquoted string consisting of ASCII
letters, digits or underscore. The response status can be either
OK
or ERROR
(unquoted). An error status may be
followed by a message as the first (and only) parameter.
There may be positional string parameters, and named headers. The last newline character may be omitted. Finally, binary data may be appended, separated from the rest with a null byte. Ancillary data may contain file descriptors.
The meaning of commands, parameters, headers, binary data and the file descriptors is defined by the Lua configuration script.
Note that binary data is not yet implemented.
Common Commands¶
This section describes common commands, to establish a convention on how they shall be implemented.
fade_children
: send aFADE_CHILDREN
control packet to a configured address. The Lua script shall determine the client’s identity and should only fade child processes belonging to that user account.