feat: ship Rust login manager and GitLab release pipeline

Replace the legacy Go implementation as the production path, quarantine the reference tree, add deterministic release packaging and version metadata, and run verification through GitLab CI.
This commit is contained in:
2026-09-25 21:33:46 +05:30
parent 1723ab53b8
commit faf5b27328
49 changed files with 6795 additions and 747 deletions

286
README.md
View File

@@ -1,190 +1,148 @@
# latchd
a display manager that runs in your terminal. lets you pick a desktop environment and log in — supports tty, x11, and wayland.
`latchd` is a Linux/PAM terminal login manager written in Rust. It presents a keyboard-first Ratatui login screen on a virtual terminal, authenticates through PAM, and starts an isolated TTY, X11, or Wayland session with an explicit per-user environment.
## what problem does this solve
The Rust binary is the supported implementation. The original Go/cgo tree under `legacy/go/` is retained as a quarantined comparison artifact and is not built by the default targets.
Most display managers are graphical. that means you need x11 or wayland running before you can even log in. latchd runs in a plain linux terminal (like tty2), so you can log in first, then start your desktop. it's lightweight, looks decent, and doesn't drag in a full graphics stack just to show a login screen.
## Security model
## how it works
- The service requires root for real sessions and refuses to start from an existing desktop session.
- PAM work is serialized on one dedicated worker thread. PAM handles never cross that thread boundary; authentication tickets are opaque RAII-owned transactions.
- Password buffers are zeroized and are never placed in argv, logs, or the rendered UI.
- Session environments are rebuilt from an allowlist for every login. Shell commands are argv vectors; no configured command is passed through a shell.
- Desktop files and session scripts must be root-owned, regular, non-group-writable files. Account shells such as `false` and `nologin` are rejected.
- Xauthority cookies are generated in a `0600` file owned by the account, supplied to `xauth` through stdin, and removed by an RAII cleanup guard.
- Main, graphical-session, and Xorg logs are private (`0600`) and opened without following symlinks.
- Power actions are an explicit absolute-path allowlist and require confirmation. They are disabled in `--preview` mode.
latchd takes over a virtual terminal (tty2 by default) and draws a login screen using ansi escape codes, powered by [bubbletea](https://github.com/charmbracelet/bubbletea). you type your username and password, pick a session from the switcher, and hit enter.
The implementation intentionally does not provide an administrator unlock shortcut: account lockout and authorization remain the responsibility of the configured PAM policy.
The actual authentication happens through pam (pluggable authentication modules). Once you're authenticated, latchd forks a child process that opens the pam session, sets up your environment, spawns your desktop or window manager, and does utmp accounting so tools like `who` and `loginctl` can see you're logged in.
## Build and test
For x11 sessions it handles the whole setup — xauthority cookies, sigusr1 handshake with the x server, timing out if xorg fails to start. for wayland it just runs your compositor script. for tty it drops you into your shell.
While the login screen is up, it renders a conway's game of life simulation as the background at 30fps, using unicode half-block characters for a pixelated look.
## what you get
- Session switcher with left/right arrows and dot indicators
- username and password fields
- Status line for errors and info
- power controls (f1 shutdown, f2 reboot by default — configurable)
- caps lock indicator
- shake animation on wrong password
- spring animation on switcher transitions
- faillock integration — shows remaining attempts, detects lockouts
- Admin override (ctrl+u) to unlock a locked-out account
- configurable color theming via toml
- variable substitution in config (`$varname`)
- Preview mode for testing inside an existing session
## Known Issues
This project is a work in progress. here's what you should know:
- **no tests**. at all. no unit tests, no integration tests. if you hit a bug, you'll find it the hard way.
- **Session spawning is a bit rough**. it uses `ForkExec` to re-launch itself as a child process, which means the pam session lifecycle isn't as clean as the original fork-then-exec model. this can cause pam modules that track session leaders to get confused.
- **a handful of open bugs** — edge cases in the switcher, occasional terminal state corruption on abrupt exits, and the automata background can flicker when the terminal resizes rapidly.
- **not all config options are fully wired**. things like `SwitcherConfig.ShowNeighbours`, various color options, and input field styling fields exist in the config struct but may not all be hooked up to the rendering yet.
## installing
### from source
you need go 1.22 or later, `libpam0g-dev` (debian/ubuntu) or `pam-devel` (rhel/fedora) or `pam` (arch), and a c compiler (`build-essential`).
Requirements: Rust 1.88 or newer, a Linux PAM development package, and a C toolchain. Debian/Ubuntu packages are `libpam0g-dev`, `libpam-modules`, and `build-essential`.
```bash
make deps # install system dependencies
sudo make install # build, install binary + configs + systemd unit
sudo systemctl enable latchd
make deps
make build
make test # unit tests and rustdoc warnings
make lint # Clippy with warnings denied
make audit # RustSec dependency advisory scan
make docker-test # the same checks in a pinned Debian container
make package # deterministic install archive plus SHA-256
make release # all local gates, Docker, and package output
```
to test without rebooting:
`make build` installs the release binary at `build/latchd`. The reproducible test image is defined by [Dockerfile.test](Dockerfile.test) and does not require `--privileged`.
## Releases
The supported release pipeline is GitLab CI in [`.gitlab-ci.yml`](.gitlab-ci.yml).
Every branch and merge request runs formatting, tests, Clippy, rustdoc, a
RustSec dependency audit, and the Docker suite. Docker-in-Docker requires a trusted privileged GitLab Runner with
the runner client-certificate volume configured; untrusted fork pipelines
should not be assigned to that runner. A SemVer tag
such as `v0.3.0` additionally builds a deterministic
Linux archive containing the binary, systemd unit, PAM policy, configuration,
license, changelog, build metadata, and a SHA-256 checksum, then publishes it
as a GitLab Release. The tag version must match the workspace version. CI
creates the GitLab Release only after both Rust and Docker verification pass.
Release builds use `SOURCE_DATE_EPOCH`, commit metadata, stripped symbols,
`opt-level = 3`, thin LTO, one codegen unit, no incremental state, and
`panic = "abort"`. Set `TARGET` or `DESTDIR` when invoking `make package` for a
controlled package build; `VERSION` must match the workspace version. The
legacy Go tree is physically isolated under `legacy/go/` and is never included
in release artifacts.
## Installation
```bash
make run-preview
sudo make install
sudo systemctl enable latchd.service
```
### arch
Installation places the binary at `/usr/bin/latchd`, the configuration at `/etc/latchd/config.toml`, the PAM policy at `/etc/pam.d/latchd`, and the systemd unit at `/usr/lib/systemd/system/latchd.service`. The unit owns `/dev/tty2`; adjust `TTYPath`, `After`, and the `tty` configuration together if another VT is required.
```bash
pacman -S latchd
systemctl enable latchd
Keep the previous package/binary available while validating a deployment. `--preview` is a safe UI exercise that does not authenticate, create a user session, or execute power actions. `--no-tui` (also `--preflight`) validates configuration and exits without entering the login screen.
## Command line
```text
latchd [OPTIONS]
-c, --config <PATH> Configuration TOML (default /etc/latchd/config.toml)
-v, --variables <PATH> Optional TOML variable file
--tty <N> Override the virtual terminal number
--xsessions <DIR> Override the X11 desktop-entry directory
--wlsessions <DIR> Override the Wayland desktop-entry directory
--initial-path <PATH> Override the session PATH
--preview Draw the UI without authentication or session startup
--no-tui, --preflight Validate configuration and exit
--no-log Disable the main log
--show-config Print the key validated settings and exit
--build-info Print version, commit, and build epoch and exit
-V, --version Print the version
```
## Command Line
An explicit configuration or variables file used by a real (non-preview) service must be root-owned, regular, and not group/other writable. The default configuration is optional: when `/etc/latchd/config.toml` is absent, built-in defaults are used.
```bash
latchd [options]
## Configuration
-c, --config <file> config path (default: /etc/latchd/config.toml)
-v, --variables <file> variables path
--tty <n> override the tty number to run on
--preview run inside an existing session for testing
--no-log disable all logging
--show-config print the parsed config and exit
-V, --version print version and exit
--xsessions <dir> override xsessions path
--wlsessions <dir> override wayland-sessions path
--initial-path <path> override initial PATH value
See [extra/config.toml](extra/config.toml) for the complete schema. Values may be substituted from a TOML variables file with `$NAME` syntax. Power entries contain an executable path and an argv array, never a shell command string.
Session discovery reads root-owned `.desktop` files from the configured X11 and Wayland directories and executable root-owned scripts from `/etc/latchd/wms` and `/etc/latchd/wayland`. Unsupported desktop-entry field codes are rejected rather than interpreted by a shell. A TTY shell is offered when no graphical sessions are available, or when `include_tty_shell = true`.
When enabled, the account chooser discovers eligible NSS users (by default,
UID 1000 and above with a real home directory and login shell). `Ctrl-N` and
`Ctrl-P` cycle accounts, clear the password, and start a bounded advisory
`faillock` query. The displayed lockout state never overrides PAM: a PAM
conversation remains the only authorization decision. Set
`accounts.enumerate = false` when listing local accounts is not appropriate.
The `[visual]` section is TUI-only. It can load a root-owned PNG/JPEG as a
downsampled terminal-cell wallpaper, run a low-rate cellular-automata ambient
layer, and show short-lived notifications. `reduced_motion = true` disables
motion while preserving the static layout; `NO_COLOR=1` disables the image
layer and all color effects.
## TUI controls
The screen is designed for 80×24 and remains usable down to 60 columns. Below the supported minimum it reports the required size without drawing a partial frame.
| Key | Action |
| --- | --- |
| Tab / Shift-Tab | Move focus |
| Up / Down | Move focus |
| Left / Right | Change session when the chooser is focused |
| Ctrl-N / Ctrl-P | Select the next / previous discovered account |
| Ctrl-B | Toggle the TUI-only wallpaper/automata background |
| Ctrl-M | Toggle reduced motion |
| Enter | Authenticate from the password field |
| Esc | Clear the password / cancel power confirmation |
| `?` | Show keyboard help |
| Ctrl-C | Restore the terminal and quit |
| F1–F12 | Confirm and run a configured power action |
`NO_COLOR=1` requests a monochrome semantic style, and `LATCHD_ASCII=1` selects ASCII session markers for terminals without reliable Unicode support.
## Project layout
```text
crates/
├── latchd-core/ validated config, accounts, sessions, and UTMPX accounting
├── latchd-auth/ PAM FFI, conversation callback, and transaction worker
└── latchd-tui/ Ratatui state machine, wallpaper, automata, and effects
rust/main.rs CLI, privilege checks, logging, and orchestration
extra/ example config, PAM policy, systemd unit, session directories
scripts/ Docker test entry point
legacy/
└── go/ non-production Go reference (explicit `make legacy-build` only)
```
## Adding Sessions
## Platform support
Drop executable scripts into the right folder and they show up in the switcher.
The first release targets Linux with PAM and systemd. BSD and non-PAM platforms are not claimed or tested. Real PAM/VT/X11 integration should be exercised on an isolated test host; the ordinary Docker suite covers compilation, unit tests, rendering tests, formatting, Clippy, and rustdoc without privileged container access.
**x11** — put your xinitrc scripts in `/etc/latchd/wms/`:
## License
```bash
# /etc/latchd/wms/bspwm
#!/bin/sh
sxhkd &
exec bspwm
```
```bash
chmod +x /etc/latchd/wms/bspwm
```
**wayland** — same thing in `/etc/latchd/wayland/`:
```bash
# /etc/latchd/wayland/sway
#!/bin/sh
exec sway
```
latchd also reads freedesktop `.desktop` files from `/usr/share/xsessions` and `/usr/share/wayland-sessions` automatically.
tty sessions are added automatically if no other environments are found, or you can force one with `include_tty_shell = true` in the config.
## configuring
The config lives at `/etc/latchd/config.toml`. all options are documented inline in the file. you can also use a `variables.toml` file with `$VAR` syntax:
```toml
# /etc/latchd/variables.toml
accent = "orange"
```
```toml
# /etc/latchd/config.toml
[password_field.style]
title_color_focused = "$accent"
border_color_focused = "$accent"
```
## Debugging
three log files:
- `/var/log/latchd.log` — main latchd events
- `/var/log/latchd.client.log` — stdout/stderr of your session
- `/var/log/latchd.xorg.log` — x server output (64mb cap)
run `latchd --show-config` to verify your configuration. run `latchd --preview` to test the login screen without leaving your current session.
## Keybindings
standard shell-style bindings in text fields:
| key | what it does |
|---|---|
| ctrl+a | start of line |
| ctrl+e | end of line |
| ctrl+l or ctrl+u | clear field |
| ctrl+d | delete forward |
| ctrl+h | backspace |
| ctrl+b/f | left/right |
| ctrl+p/n | up/down (previous/next field) |
| tab, shift+tab | next/prev field |
| down / up | next/prev field |
| left/right | previous/next session (switcher mode) |
| , / . | previous/next session (any mode) |
| f1, f2 | power controls |
| ctrl+u | admin unlock (when account is locked) |
## source layout
```bash
src/
├── main.go entry point, cli, vt switching, session forking
├── auth/
│ ├── pam.go cgo pam bindings (locks goroutine to os thread)
│ ├── faillock.go faillock tally detection and reset
│ └── utmpx.go user accounting records
├── config/
│ └── config.go toml parsing, variable substitution, defaults
├── session/
│ ├── env.go xdg and environment variable setup
│ ├── exec.go process spawning, privilege dropping, log limits
│ ├── scan.go session discovery and .desktop parsing
│ ├── x11.go xorg setup, xauth, sigusr1 handshake
│ └── wayland.go wayland placeholder
├── login/
│ └── login.go login form ui, state machine, animations, rendering
└── tui/
└── bg/
└── automata.go conway's game of life background
```
## license
MIT or Apache-2.0
## credits
the `extra/` directory structure and the session scanning / pam delegation approach are adapted from [lemurs](https://github.com/coastalwhite/lemurs), a rust display manager.
Apache-2.0; see [LICENSE](LICENSE).