Passage ======= Author: Max Kellermann *Passage* is a daemon which allows jailed/containerized processes to talk to the host to trigger certain actions. Configuration ------------- .. highlight:: lua The file :file:`/etc/cm4all/passage/config.lua` is a `Lua `_ script which is executed at startup. It contains at least one :samp:`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 :envvar:`systemd` (not the string literal :samp:`"systemd"`) will listen on the sockets passed by systemd (from unit :file:`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: * :samp:`command`: The command string. * :samp:`args`: An array containing command arguments. * :samp:`headers`: A table containing headers (name/value pairs). * :samp:`body`: An optional string of (binary) data; may be ``nil`` if none was specified. * :samp:`pid`: The client's process id. * :samp:`uid`: The client's user id. * :samp:`gid`: The client's group id. * :samp:`cgroup`: The control group of the client process with the following attributes: * ``path``: the cgroup path as noted in :file:`/proc/self/cgroup`, e.g. :file:`/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 (or ``nil`` 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: * :samp:`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'}) * :samp:`exec_pipe({PATH, ARG, ...}, [{OPTIONS}])`: execute the given program (should be an absolute path because there is no :envvar:`$PATH` resolution 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's ``stderr`` to a pipe and return the read side to the client. - ``cgroup='client'``: Spawn the child process in the same cgroup as the client. * :samp:`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 URL - ``method``: the HTTP request method - ``query``: a table containing names and values of query parameters - ``headers``: a table containing custom request headers - ``body``: the request body; switches the default request method to ``POST`` - ``max_size``: a size limit for the response body (in bytes); the default is 64 kB * :samp:`error([MESSAGE], [HEADERS])`: send an error response to the client. Takes an optional error message parameter. If a message is provided (and not ``nil``), 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 :samp:`nil`) is considered a silent success. If you encounter a problem, raise an exception by invoking the Lua function :samp:`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. ``OPTIONS`` may be a table with the following keys: - ``type``: the socket type, one of ``stream`` (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. ``START`` and ``END`` are start and end position within the string with the same semantics as in ``string.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. `Helpers `__:: bin = sodium.hex2bin("deadbeef") -- returns "\xde\xad\xbe\ef" hex = sodium.bin2hex("A\0\xff") -- returns "4100ff" `Generating random data `__:: key = sodium.randombytes(32) `Sealed boxes `__:: pk, sk = sodium.crypto_box_keypair() ciphertext = sodium.crypto_box_seal('hello world', pk) message = sodium.crypto_box_seal_open(ciphertext, pk, sk) `Public-key signatures `__:: 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) `Point*scalar multiplication `__:: 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 :samp:`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. :samp:`m.sender`), but methods are referenced with a colon (e.g. :samp:`m:reject()`). Usage ----- .. highlight:: shell The Debian package :file:`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 :envvar:`--header=NAME:VALUE` can be used to send headers to the server. By default, the client connects to :file:`/run/cm4all/passage/socket`, but the option :envvar:`--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 (:envvar:`AF_LOCAL` / :envvar:`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 :samp:`OK` or :samp:`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. * :samp:`fade_children`: send a :samp:`FADE_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.