# gnix a simple stupid reverse proxy ## Features - HTTP/1.1 and HTTP/2 - Simple to configure (see below) - Somewhat fast (about 150k req/s on a rpi5 for simple routes) - Composable modules - Handles connection upgrades correctly by default (websocket, etc.) - TLS support with automatic certificate selection - Configuration hot-reloading - Client authentification (http basic auth, form + cookie) ## Links - [AUR Package](https://aur.archlinux.org/packages/gnix) - [Matrix Room](https://matrix.to/#/#gnix:metamuffin.org) for questions, announcements and general discussion around the software. ## Quick Start Run the binary with the a path to the configuration as the first argument. The configuration file is written in YAML and could look like this: ```yaml http: bind: "[::]:80" https: bind: "[::]:443" cert_path: "/etc/letsencrypt/live" # Scans directory and selects a valid certificate based on hostname handler: !hosts # !hosts routes requests for different hostnames. # requests for `example.org` are forwarded to 127.0.0.1:8000. "example.org": !proxy { backend: "127.0.0.1:8000" } # requests for `mydomain.com` will access files from /srv/http. "mydomain.com": !files root: "/srv/http" index: true # requests for `panel.mydomain.com` will be logged, authentificated and passed on to `otherserver`. "panel.mydomain.com": !access_log file: "/var/log/gnix/panel" next: !cookie_auth users: "/etc/panel_creds" expire: 86400 fail: !file { path: "/usr/share/gnix/login.html" } next: !proxy { backend: "otherserver:80" } ``` ## Reference The configuration uses YAML formatting. When the configuration file is changed, it will automatically be loaded and applied if valid. Configuration errors are reported in stderr and are only fatal at startup. - **section `http`** - Optional section. Omit to disable unencrypted http. - `bind`: Addresses to accept http requests on (string or list of strings). - **section `https`** - Optional section. Omit to disable https. - `bind`: Addresses to accept https requests on (string or list of strings). - `cert_path`: Path to a directory structure that certificates are loaded from. The hierachy should contain directories containing corresponding `fullchain.pem` and `privkey.pem` files. The correct certificate is selected automatically by subject (`CN`). Pointing this directly at `/etc/letsencrypt/live` is possible. (string or list of strings) - `cert_fallback`: Path to a single directory containing a key-cert pair that is used when no other certificate seems appropriate. This is useful for testing locally with a self-signed subjectless certificate. (optional string) - **section `limits`** - Note: Make sure you do not exceed the maximum file descriptor limit on your platform. - `max_incoming_connections` number of maximum incoming (downstream) connections. excess connections are rejected. Default: 512 - `max_outgoing_connections` number of maximum outgoing (upstream) connections. excess connections are rejected. Default: 256 - **section `handler`** - A module to handle all requests. Usually an instance of `hosts` or `paths`. - `watch_config`: boolean if to watch the configuration file for changes and apply them accordingly. Default: true (Note: This will watch the entire parent directory of the config since most editors first move the file. Currently any change will trigger a reload. TODO) ### Modules Modules handle requests. Some of them have arguments which are modules themselves; in that case the request is passed on. - **module `hosts`** - Hands over the requests to different modules depending on the `host` header. - Takes a map from hostname (string) to handler (module) - **module `paths`** - Routes requests by matching the path against regexes. The first matching regex determines the module to handle this request. If the regex has an unnamed capture group, its value will become the downstream request URI. - Takes a map from regex (string) to handler (module) OR a sequence of such maps. The priority is assigned first by position in the sequence then alphabetically in the map. - **module `proxy`** - Forwards the request as-is to some other server. the `x-real-ip` header is added to the request. Connection upgrades are handled by direct forwarding of network traffic. - `backend`: socket address (string) to the backend server - **module `files`** - Provides a simple built-in fileserver. The server handles `accept-ranges`. The `content-type` header is inferred from the file extension and falls back to `application/octet-stream`. If a directory is requested `index.html` will be served or else when indexing is enabled, `index.banner.html` will be prepended to the response. - `root`: root directory to be served (string) - `index`: enables directory indexing (boolean) - `localize`: sends a localized version if possible. for localized content the ISO 639-1 language code is injected in front of the first file name extension (boolean) **semi-experimental** - **module `file`** - Replies with static content. - `path`: file path to the content. _This will be loaded into memory at startup!_ (string) - `content`: inline declaration of the content. (string) - `type`: type of content reflected in `content-type` header. (string) - **module `access_log`** - Logs requests to a file. - `file`: file path to log (string) - `reject_on_fail`: rejects requests if log could not be written (boolean) - `flush`: flushes log on every request (boolean) - `next`: module for further handling of the request (module) - **module `error`** - Rejects every request with a custom error message. - Takes an error message (string) - **module `http_basic_auth`** - Filters requests via HTTP Basic Authentification. Unauthorized clients will be challenged. - `realm`: describes what the user is logging into (most modern browsers dont show this anymore -_-) - `users`: list of valid logins (credentials) - `next`: a module to handle this request on successfully authentificated. (module) - **module `cookie_auth`** - Authentificates a client based on cookies. The cookies are set on login by a POST request to `/_gnix_login` with form fields `password` and `username` (optional, default: "user") in `x-www-form-urlencoded` format. In any case the users submitting this request will be directed back to the page they come from. For successful logins two cookies are set: `gnix_username` containing the username and `gnix_auth` containing an opaque authentification token. The `gnix_username` cookie is authentificated by gnix and can therefore be used by applications. Alternatively a login may be implemented by returning the `gnix-auth-success` header with a username as the value from the `fail` handler, which is handled like a sucessful login for that user. If `gnix-auth-no-redirect` is set the response of the `fail` handler is passed instead of a redirect being sent. This method can be useful for implementing custom login logic like OTP login or a CAPTCHA. - `users`: list of valid logins (credentials) - `expire`: seconds before logins expire; not setting this option keeps the login valid forever on the server but cleared after the session on the client (optional number) - `secure`: makes the cookies accessable from secure contexts exclusively i.e. HTTPS (boolean) - `next`: a module to handle this request on successfully authentification. (module) - `fail`: a module to handle the request when a user is not authorized. This could show an HTML form prompting the user to log in. An implementation of such a form is provided with the distribution of this software, usually in `/usr/share/gnix/login.html`. It can return the `gnix-auth-success` header, see above. (module) - **module `switch`** - Decides between two possible routes based on a condition. - `condition`: - `!is_websocket_upgrade`: Checks if a websocket was requested. - `!is_get`: Checks if this is a GET request - `!is_post`: Checks if this is a POST request - `!path_starts_with `: Checks if the URI path starts with some prefix - `!path_is `: Checks if the URI path is exactly what you specified - `!has_header `: Checks if the request includes a certain header. - `!any [conditions]`: Checks if any of a set of conditions are satisfied. - `!all [conditions]`: Checks if all conditions are satisfied. - `case_true` Handler with matched requests (module) - `case_false` Handler for all other requests (module) - **module `headers`** - Appends multiple headers to the response. - `headers`: A map of all header name-value pairs. (object string:string) - `inner`: The handler to add the headers to. (module) - **module `redirect`** - Responds with a permanent redirect. - Takes the location to redirect to. (string) - **module `cgi`** - Runs a CGI script on the request. - `bin`: Path to the CGI binary (string) - `user`: User that the script is executed as. Requires to run gnix as root. (optional string) - **module `cache`** - Caches requests. **This is experimental! Don't use this.** - Not documented yet - **module `loadbalance`** - Balances load across multiple modules (presumably proxied backends). The current implementation selects the least busy handler for each request and does not scale well to many modules. **This is experimental! Don't use this.** - Takes a set of handlers. (sequence of module) - **module `debug`** - Replies with information about the request to debug. Includes source address, HTTP version, URI and headers. #### Credentials config format Login credentials for `cookie_auth` and `http_basic_auth` are supplied as either an object mapping usernames to PHC strings or a file path pointing to a file that contains that map in YALM format. Currently only `argon2id` is supported. ### Additional Notes Internally gnix processes requests as they would be sent in HTTP/1.1. HTTP/2 is translated on arrival. Paths matching `/_gnix*` might be used internally in gnix for purposes like OpenID callback or login action endpoints. I hope your application doesn't rely on using them for itself. ## License AGPL-3.0-only; see [COPYING](./COPYING)