What is Polluxd

polluxd is Gemini file and CGI server implementation for unix-like systems. It is the reference server implementation for the libpxd Gemini protocol library. It has been tested on Linux, OpenBSD, FreeBSD, and NetBSD and has been built with both gcc and clang.

For bug reports, feature requests, or general feedback please email: ingrix at sdf dot org

(Correspondence need not be detailed)

Features

Polluxd can:

Security mitigations.

polluxd has been written with some security in mind.

The main server can be made to run from a chroot, can drop privileges, and contains a docroot specification for ensuring that files are always served from within a specific hierarchy. On OpenBSD the docroot is further enforced by the use of unveil(2).

Each location block (see below) which serves CGI scripts can be run from a chroot and set of user/group permissions. The CGI script chroot/docroot/user/group may be distinct from the main server. The CGI scripts are also served in such a way that they never have access to the server's key material at any point, which helps protect the server integrity.

Building

polluxd is built alongside the pxd library. The only other dependencies are the standard C library and OpenSSL (or a compatible library, e.g. LibreSSL). It should build on

# Extract the source and build
tar -xf libpxd-d082e53-20250825.tar.gz
cd libpxd-d082e53
make

To run polluxd you need to create a config file (see below) and make sure libpxd.so is in the library search path. If we assume you're running out of the build directory:

export LD_LIBRARY_PATH=$PWD
./polluxd/polluxd -f polluxd.conf

Configuration

For a quickstart, see the next section

A configuration file must be provided to polluxd to configure its behavior. The configuration is line-based and is largely a key-value specification, with the exception of the location blocks which are formatted like 'location /path {}'.

An example configuration file with comprehensive explanations

The polluxd configuration consists of two parts, top-level configuration options and location-specific configurations. Top-level options set whole-server settings and defaults for location-specific settings. Location-specific settings define the association between Gemini request paths and filesystem hierarchies, and the behaviors that are taken for those requests.

An example configuration and directory hierarchy which shows polluxd features.

Default behavior and a simple configuration

The simplest configuration is something like this:

host=ingrix.info
listen_addr=127.0.0.1
port=1965
cert_file=cert.pem
key_file=key.pem
docroot=/var/gemini
index_file=index.gmi
location / {
  action = file
}

What this gets you:

General options

The general or top-level configuration options are:

Location-specific options

TL;DR locations are specified in blocks. Basic wildcards can be used which match single path components.

location / {
    ...options...
}

location /cgi {
    ...options...
}

location /~* {
    ...options for serving home dirs...
}

The paths specified in the 'location' line represent the client's requested path (i.e. what is received via Gemini). Directories are treated hierarchically, and the closest/deepest location block will be used for a given request path. Sub-directories override the settings of their parent directories (e.g. you can use a 'location /some/path' to override settings for the 'location /some'). The root directory "/" is not treated specially, and must be specified as a location block if you want to serve anything from the top-level docroot directory.

Basic wildcards can be used for individual path components (see fnmatch(3)). These wildcards will match only single components. Example:

location /some/~*/path {
    ...
}

# the above block will match requests for /some/~ingrix/path and /some/~/path but not /some/~ingrix/sub/path

The list of location-specific configuration options is:

Actions

The available actions for each location block are "deny", "file", and "cgi"

"deny" returns a "Permission denied" error to the client. This is the default setting for all hierarchies.

"file" serves a static file from the directory hierarchy, or reads from a pipe or socket.

"cgi" executes a CGI script as translated from the request path (or a fixed script if cgi_script is specified)

A detailed explanation of the CGI architecture in polluxd

Path translation

TL;DR:

# Assume the configuration settings:
# docroot = /tmp/docroot
# prefix = subdir
# strip = 1
# Gemini request: gemini://ingrix.info/some/random/extra/../path

(request path) /some/random/extra/../path
   |
   | canonicalize
   v
/some/random/path
   |
   | strip
   v
/random/path
   |
   | prefix
   v
/subdir/random/path
   |
   | docroot
   v
/tmp/docroot/subdir/random/path (the filesystem path)

Polluxd translates paths from Gemini request path to an effective filesystem path using the 'docroot', 'strip', and 'prefix' options appropriate for each location block. The procedure is as follows:

Requiring certificates/Certificate whitelists

Each location block can contain the 'require_cert' or 'allowed_cert_file' options. If one or both are specified and the certificate validation fails then a 'Permission denied' error is returned for the hierarchy.

'require_cert' means that the client must send a cryptographically valid client certificate to the server. The specific information in the certificate doesn't matter, but one has to be present. This is somewhat useful for filtering out bots from the Gemini traffic.

'allowed_cert_file' specifies a path (relative to the chroot) to read valid certificate hashes from. The file is a simple list of SHA256 hashes of allowed client certificates, one certificate per line. The file also supports #-prefixed comments at the ends of lines to provide comments. Whitespace is also stripped off.

Notes and Tricks

Password-protected keys

If you want to use a password-protected SSL keyfile, you can direct this key_file option to a fifo on the filesystem, decrypt the key, and pipe the decrypted key data to the fifo. You can also do this for cert_file but honestly I don't know why you would want to.