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 (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:

  • fade_children(ADDRESS, TAG): send a FADE_CHILDREN control packet to the given address. The address is either a string containing a (numeric) IP address, or an address object created by control_resolve(). If a tag is specified, then only children with this tag are addressed.

  • flush_http_cache(ADDRESS, TAG): send a FLUSH_HTTP_CACHE control packet to the given address. The address is either a string containing a (numeric) IP address, or an address object created by control_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.

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)

`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 a 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.