Passage¶
Author: Max Kellermann <max.kellermann@ionos.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-passage (i.e. SIGHUP), Passage
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).body: An optional string of (binary) data; may benilif none was specified.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.scopexattr: A table containing extended attributes of the control group.parent: Information about the parent of this cgroup; it is another object of this type (ornilif 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:
ok([BODY], [HEADERS]): send a successful response to the client.Example:
return request:ok() return request:ok('Some message', {foo='A response header'}) return request:ok(nil, {foo='Only headers, no body'})
exec_pipe(PATH, ARG, ..., [OPTIONS]): execute the given program (should be an absolute path because there is no$PATHresolution here) and connect a pipe to its standard output; send the pipe’s reading side to the client.The second parameter may be a table specifying options:
env: a table with environment variables for the child process.stderr='pipe': Connect the program’sstderrto a pipe and return the read side to the client.cgroup='client': Spawn the child process in the same cgroup as the client.
http_request(URL): perform a HTTP request and send the response to the Passage client. Non-successful HTTP responses (anything other than 2xx) cause the operation to fail. (This works only with small HTTP responses because Passage responses are limited to one datagram.)Instead of a simple URL string, you can construct more complex requests by passing a table:
return request:http_request({ url='http://example.com/', })
The following table keys are recognized:
url: an absolute HTTP URLmethod: the HTTP request methodquery: a table containing names and values of query parametersheaders: a table containing custom request headersbody: the request body; switches the default request method toPOSTmax_size: a size limit for the response body (in bytes); the default is 64 kB
error([MESSAGE], [HEADERS]): send an error response to the client. Takes an optional error message parameter. If a message is provided (and notnil), it will be included in the error response. This allows the handler to explicitly return an error status without raising a Lua exception.The second parameter allows setting response headers. For example, the Passage client uses the “exit_status” header as its process exit status.
Example:
return request:error('Something went wrong', {exit_status='75'})
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”)
socket¶
A simple low-level networking library. Example:
tcp = socket:connect('localhost:1234')
udp = socket:connect('localhost:4321', {type='dgram'})
multicast = socket:connect('[ff02::dead:beef]:2345', {type='dgram'})
unix = socket:connect('/run/test.socket')
abstract = socket:connect('@test', {type='seqpacket'})
abstract:send('hello world')
The socket library has the following methods:
connect(ADDRESS, [OPTIONS]): Create a new socket connected to the specified address.OPTIONSmay be a table with the following keys:type: the socket type, one ofstream(the default),dgram,seqpacket.
Returns a new socket object on success or
[nil,error]on error.
Socket objects have the following methods:
close(): Close the socket.send(DATA, [START], [END]): Send data (i.e. a string) to the peer.STARTandENDare start and end position within the string with the same semantics as instring.sub(). Returns the number of bytes sent on success or[nil,error]on error.
control_client¶
A client for the beng-proxy control protocol.
During startup, create a control_client object:
-- IPv4 (default port)
c = control_client:new('224.0.0.42')
-- IPv6 on default port
c = control_client:new('ff02::dead:beef')
-- IPv6 on non-default port (requires square brackets)
c = control_client:new('[ff02::dead:beef]:1234')
-- local socket
c = control_client:new('/run/cm4all/workshop/control')
-- abstract socket
c = control_client:new('@bp-control')
The new() constructor returns nil,error on error (and thus the
call can be wrapped in assert() to raise a Lua error instead).
The method build() creates an object which can be used to build a
control datagram with one or more commands. After that datagram has
been assembled, it can be sent with the send() method. Example:
c:send(c:build():fade_children('foo'):flush_http_cache('bar'))
The send() method returns nil,error on error.
The builder implements the following methods:
cancel_job(PARTITION_NAME, JOB_ID)discard_session(ID)disconnect_database(ACCOUNT)fade_children(TAG)flush_filter_cache(TAG)flush_http_cache(TAG)reject_client(ADDRESS)reset_limiter(ACCOUNT)tarpit_client(ADDRESS)terminate_children(TAG)
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)
pk, sk = sodium.crypto_sign_keypair()
m = 'hello world'
sig = sodium.crypto_sign_detached(m, sk)
valid = sodium.crypto_sign_verify_detached(sig, m, pk)
pk = sodium.crypto_scalarmult_base(sk)
json¶
If built with nlohmann json, the
following functions are available in the json namespace:
- the function ``json.dump()`` converts the parameter to a JSON
string:
print(json.dump{x=42, y="z"}) {"x":42,"y":"z"}
the function
json.parse()parses a JSON string and returns it as a Lua value:print(inspect(json.parse('{"x":42,"y":"z"}'))) { x = 42, y = "z" }
JWT¶
The function jwt.sign() generates a JSON Web Token:
pk, sk = sodium.crypto_sign_keypair()
header = {kid='foo'}
payload = {iss="joe",exp=1300819380}
token = jwt.sign(sk, header, payload)
The JWT library requires building with nlohmann_json and
libsodium.
PostgreSQL Client¶
The Lua script can query a PostgreSQL database. First, a connection should be established during initialization:
db = pg:new('dbname=foo', 'schemaname')
In the handler function, queries can be executed like this (the API is similar to LuaSQL):
local result = assert(db:execute('SELECT id, name FROM bar'))
local row = result:fetch({}, "a")
print(row.id, row.name)
Query parameters are passed to db:execute() as an array after the
SQL string:
local result = assert(
db:execute('SELECT name FROM bar WHERE id=$1', {42}))
The functions pg:encode_array() and pg:decode_array() support
PostgreSQL arrays; the former encodes a Lua array to a PostgreSQL
array string, and the latter decodes a PostgreSQL array string to a
Lua array.
To listen for PostgreSQL notifications, invoke
the listen method with a callback function:
db:listen('bar', function()
print("Received a PostgreSQL NOTIFY")
end)
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
\0BODY
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.
After the command, there may be positional string parameters separated by a space. An unquoted parameter is a non-empty string of ASCII letters, digits, underscores or dashes. Parameters that contain other characters must be enclosed in double quotes. The double quote and backslash may be escaped by preceding it with a backslash character. Control characters and null bytes (0x00..0x1f) are not allowed.
Following lines may be headers, i.e. name-value pairs. Header names follow the same syntax as unquoted parameters; header values may contain any printable ASCII character. The last newline character may be omitted.
Finally, a body of 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, body and the file descriptors is defined by the Lua configuration script.
Common Commands¶
This section describes common commands, to establish a convention on how they shall be implemented.
fade_children: send aFADE_CHILDRENcontrol 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.