# RPM spec for termland-server # # Local build: # cd /path/to/parent && tar czf ~/rpmbuild/SOURCES/termland-0.3.0.tar.gz \ # --transform='s,^termland,termland-0.3.0,' termland/ # rpmbuild -ba termland/packaging/termland-server.spec # # COPR: upload this spec + source tarball for automated builds. %global crate_name termland %global version 0.9.0 # find-debuginfo can produce an empty debugsourcefiles.list for a Rust # binary (unlike typical C sources), which newer Fedora's rpmbuild tolerates # but EL8's treats as a hard error ("Empty %%files file ... debugsourcefiles # .list"). Disable debuginfo package generation outright rather than fight # find-debuginfo's Rust handling per-distro - standard practice for Rust RPM # packages, and this project doesn't rely on -debuginfo/-debugsource # subpackages for anything. %global debug_package %{nil} Name: termland-server Version: %{version} Release: 1%{?dist} Summary: Termland remote desktop server — stream Wayland sessions via AV1/VP9/HEVC/H.264/Opus License: LGPL-3.0-or-later URL: https://github.com/jboero/termland Source0: https://github.com/jboero/termland/archive/v%{version}/%{crate_name}-%{version}.tar.gz # Vendored crate dependencies, so %%build works offline (COPR/mock build roots # have no network). Regenerate with: packaging/make-vendor-tarball.sh Source1: https://github.com/jboero/termland/releases/download/v%{version}/%{crate_name}-%{version}-vendor.tar.xz # ─── Build dependencies ────────────────────────────────────────────────────── # Rust toolchain (cargo, rustc) BuildRequires: rust >= 1.85 BuildRequires: cargo >= 1.85 # FFmpeg development libraries (AV1 encoding via libavcodec/libavformat). # ffmpeg-free-devel is in base Fedora; ffmpeg-devel (RPM Fusion) adds HW encoders. BuildRequires: (ffmpeg-free-devel or ffmpeg-devel) # Opus audio codec BuildRequires: opus-devel # PulseAudio client libraries (audio capture from session) BuildRequires: pulseaudio-libs-devel # PAM development headers (authentication) BuildRequires: pam-devel # libclang (ffmpeg-sys-next uses bindgen for FFI generation) BuildRequires: clang-devel # TLS (aws-lc-rs / ring build deps) BuildRequires: cmake BuildRequires: gcc BuildRequires: gcc-c++ # File-based, not `perl-interpreter`: that virtual-provide name is a # Fedora/RHEL convention Mageia doesn't recognize ("No match for argument: # perl-interpreter"). Requiring the actual binary path resolves identically # via file-provides on every RPM-based distro regardless of how they've # packaged/split Perl. BuildRequires: /usr/bin/perl # Wayland client libraries (screencopy, input injection, output management) BuildRequires: wayland-devel BuildRequires: wayland-protocols-devel # ─── Runtime dependencies ──────────────────────────────────────────────────── # Headless Wayland compositors. Desktop sessions - the default mode - always # run labwc; there is no fallback to cage, which only serves --mode app:. # This used to be "Requires: (labwc or cage)", which a machine that already # had cage satisfied without labwc: the package installed, and every desktop # session then failed to start. %if 0%{?fedora} Requires: labwc Recommends: cage %else # EPEL packages no labwc (8, 9, 10) and cage only in 10, so requiring labwc # would make the package uninstallable there rather than desktop-less. Keep # the old either-or; desktop mode needs a labwc built or installed locally. Requires: (labwc or cage) %endif # FFmpeg runtime (AV1 encoder backends: QSV, NVENC, VA-API, SVT-AV1) Requires: (ffmpeg-libs or libavcodec-free) # PulseAudio API (audio capture per session via null sink monitor). # Modern Fedora uses pipewire-pulseaudio; classic PulseAudio also works. Requires: (pipewire-pulseaudio or pulseaudio) Requires: pulseaudio-utils # Opus codec runtime Requires: opus # PAM runtime (authentication) Requires: pam # Wayland tools for clipboard, cursor, etc. Requires: wl-clipboard # For SSH subsystem mode Requires: openssh-server %description Termland is a multi-tenant Wayland remote desktop server that streams interactive desktop and application sessions using modern codecs. Video is encoded as AV1 using hardware acceleration when available (Intel QSV, NVIDIA NVENC, AMD AMF/VA-API) with SVT-AV1 software fallback. Audio is forwarded via Opus over PulseAudio. Transport modes: - SSH subsystem: zero-config, piggybacks on existing sshd (recommended) - Direct TCP with TLS + PAM authentication Each session runs an isolated headless Wayland compositor (labwc for desktop, cage for single-app kiosk) with its own screen capture, input injection, and audio sink. %prep %setup -q -n %{crate_name}-%{version} # Unpack vendored crate deps (Source1) and point cargo at them so %%build runs # fully offline — COPR/mock build roots have no network access. tar -xf %{SOURCE1} mkdir -p .cargo cat > .cargo/config.toml <<'CARGOEOF' [source.crates-io] replace-with = "vendored-sources" [source.vendored-sources] directory = "vendor" CARGOEOF %build %ifarch %{ix86} # ffmpeg-sys-next 9.0.0 generates its bindings against its own stub vulkan.h, # which asserts sizeof(VkPhysicalDeviceFeatures2) == 240. That is the 64-bit # size; on i386 the struct is 228 bytes, as in the real Vulkan headers, so the # assertion fails and bindgen aborts. It only checks the stub, never changes a # layout, so compile it out for bindgen's parse. Drop once the crate is fixed. export BINDGEN_EXTRA_CLANG_ARGS='-D_Static_assert(a,b)=' %endif cargo build --release --offline --bin termland-server # Generate shell completions ./target/release/termland-server --completions bash > termland-server.bash ./target/release/termland-server --completions zsh > _termland-server ./target/release/termland-server --completions fish > termland-server.fish %install # Binary install -Dm755 target/release/termland-server %{buildroot}%{_bindir}/termland-server # Systemd service + environment config install -Dm644 packaging/termland-server.service %{buildroot}%{_unitdir}/termland-server.service install -Dm644 packaging/termland-server.env %{buildroot}%{_sysconfdir}/sysconfig/termland-server # PAM service install -Dm644 packaging/termland.pam %{buildroot}%{_sysconfdir}/pam.d/termland # SSH subsystem drop-in (sshd_config.d) install -Dm644 packaging/50-termland.conf %{buildroot}%{_sysconfdir}/ssh/sshd_config.d/50-termland.conf # TLS keypair directory. Owned by the package (0700 root:root) so the private # key is never world-readable, and so the service's ReadWritePaths= entry for # it resolves on a fresh install. Left empty: the cert is generated on first # start, or provisioned here out of band. install -dm700 %{buildroot}%{_sysconfdir}/pki/termland # Shell completions install -Dm644 termland-server.bash %{buildroot}%{_datadir}/bash-completion/completions/termland-server install -Dm644 _termland-server %{buildroot}%{_datadir}/zsh/site-functions/_termland-server install -Dm644 termland-server.fish %{buildroot}%{_datadir}/fish/vendor_completions.d/termland-server.fish %post %systemd_post termland-server.service # Generate the self-signed TLS keypair here rather than from the service: # termland-server.service runs under ProtectSystem=strict and so cannot write # /etc/pki itself. Idempotent, and guarded so an existing keypair is never # replaced — an upgrade must not change the fingerprint clients have pinned. # Never fail the transaction over this; the server can still be pointed at a # keypair via --tls-cert/--tls-key. if [ ! -s %{_sysconfdir}/pki/termland/cert.pem ] || [ ! -s %{_sysconfdir}/pki/termland/key.pem ]; then if ! %{_bindir}/termland-server --generate-cert >/dev/null 2>&1; then echo " Note: could not generate a TLS keypair in %{_sysconfdir}/pki/termland." echo " Run 'termland-server --generate-cert' as root, or provision" echo " cert.pem/key.pem there yourself, before starting the service." fi fi # Activate the sshd drop-in, which registers the "termland" subsystem: sshd # only reads its configuration at start and on reload, so until then every # client gets "subsystem request failed". Reload, never restart, and only if # the whole configuration validates - a reload with a broken config can leave # sshd down, on a machine that may only be reachable over ssh. Existing ssh # sessions are separate processes and survive a reload. Done on upgrades too, # so an install that missed it is repaired. Skipped when sshd isn't running # (build roots, containers). sshd_reloaded=0 if systemctl is-active --quiet sshd.service 2>/dev/null; then if %{_sbindir}/sshd -t >/dev/null 2>&1; then systemctl reload sshd.service >/dev/null 2>&1 && sshd_reloaded=1 else echo " Note: 'sshd -t' reports a configuration problem, so sshd was not" echo " reloaded and the termland ssh subsystem is not active yet. Fix the" echo " sshd configuration, then: systemctl reload sshd" fi fi # Hint about setup echo "" echo " Termland server installed. Two ways to run:" echo "" echo " 1) SSH subsystem (recommended — auto-configured):" echo " An sshd drop-in was installed at /etc/ssh/sshd_config.d/50-termland.conf" if [ "$sshd_reloaded" = 1 ]; then echo " sshd was reloaded, so it is active now." else echo " Activate it with: systemctl reload sshd" fi echo " Clients connect with: termland-client --ssh user@host" echo "" echo " 2) Standalone TCP service (with TLS + PAM auth):" echo " Edit /etc/sysconfig/termland-server, then:" echo " systemctl enable --now termland-server" echo " Clients connect with: termland-client [--tls] host:7867" echo "" %preun %systemd_preun termland-server.service %postun %systemd_postun_with_restart termland-server.service # On removal the drop-in is gone; reload so sshd stops offering a subsystem # whose binary no longer exists. Same guard as in %%post. if [ $1 -eq 0 ] && systemctl is-active --quiet sshd.service 2>/dev/null; then %{_sbindir}/sshd -t >/dev/null 2>&1 && systemctl reload sshd.service >/dev/null 2>&1 || : fi %files %license LICENSE %doc README.md ROADMAP.md %{_bindir}/termland-server %{_unitdir}/termland-server.service %config(noreplace) %{_sysconfdir}/sysconfig/termland-server %config(noreplace) %{_sysconfdir}/pam.d/termland %config(noreplace) %{_sysconfdir}/ssh/sshd_config.d/50-termland.conf %attr(0700,root,root) %dir %{_sysconfdir}/pki/termland %{_datadir}/bash-completion/completions/termland-server %{_datadir}/zsh/site-functions/_termland-server %{_datadir}/fish/vendor_completions.d/termland-server.fish %changelog * Wed Oct 14 2026 John Boero - 0.9.0-1 - Capture and encode only when the screen changes (#40). An idle 1080p session drops from 65% CPU (NVENC) or 268% (SVT-AV1) to under 1%. --capture-mode full restores the old every-frame capture. Clients older than 0.9.0 are still sent a frame a second while the screen is idle. - Quality now affects every encoder: SVT-AV1, libvpx-vp9, x264 and x265 used to ignore it. For AV1 it maps to a tenth of the old bitrate (quality 75 is about 1.1 Mbps), and a client can ask for an explicit bitrate. - QUIC connections send keep-alives, so an idle session is not dropped. - Fix the session loop spinning a CPU core when the cursor or clipboard watcher thread exits. - Forced keyframes are now real keyframes on NVENC, libx265 and QSV. - VA-API encoding works (#46). It never had: frames were not uploaded to the GPU, so every VA-API encoder failed to open, and AMD GPUs went unused without the proprietary AMF driver. - Trying encoders no longer prints FFmpeg errors for hardware the machine does not have. * Fri Sep 25 2026 John Boero - 0.8.1-1 - No functional server changes; version kept in step with termland-client 0.8.1. I/O errors in the log no longer repeat their message ("io error: X: X"). * Fri Sep 25 2026 John Boero - 0.8.0-2 - Fix the i386 build: work around ffmpeg-sys-next 9.0.0's Vulkan stub header asserting a 64-bit struct size, which aborted binding generation on 32-bit x86. No change on other architectures. * Fri Sep 25 2026 John Boero - 0.8.0-1 - First packaged release since 0.6.1: 0.7.0 was tagged but never built, so the 0.7.0 changes below - including its UPGRADE NOTE about the runtime directory move - reach RPM users with this release. - Fix the service passing multi-word TERMLAND_TLS_FLAGS/TERMLAND_AUTH_FLAGS as a single argument, which made every documented non-default setting in /etc/sysconfig/termland-server fail to start. systemd's "${VAR}" always expands to exactly one argument and an empty value to one empty argument; both are rejected by the argument parser. The unit now uses "$VAR", which splits at whitespace and contributes nothing when empty, so explicit --tls-cert/--tls-key paths and the documented "disable" settings both work. TERMLAND_BIND/TERMLAND_PORT also gained Environment= defaults so a missing or partially-commented environment file no longer yields an empty --bind. - Session teardown now confirms the session's process group is actually gone (SIGTERM the leader, then the group, then SIGKILL, checking in between) instead of assuming SIGTERM worked and deleting the record. Stranded plasmashell stacks left behind this way were colliding with the host desktop's global shortcuts and taking kded6/plasmashell down with them. - Find terminals and desktop shells by searching PATH directly instead of running which(1). Without which(1) installed - the default on minimal and container installs - every lookup failed and sessions died with "exec: xterm: not found". - Experimental WebTransport listener (--webtransport) for the browser clients in web/. Off by default, and refuses every browser origin unless allowed with --webtransport-origin. The browser client page is not packaged yet; see docs/webtransport.md. * Fri Aug 14 2026 John Boero - 0.7.0-1 - Fix service sandboxing that blocked session startup and cert generation. ProtectHome=read-only covers /home, /root AND /run/user, all three of which the server must write. This was the single cause of three user reports: first-run TLS cert generation failing, the compositor dying before it could create its Wayland socket, and a session failing with a misleading "No such file or directory" for its logfile. - TLS keypair moves to /etc/pki/termland/ and is generated at install time via the new "termland-server --generate-cert", since the hardened service cannot write /etc. An existing keypair under /root/.config/termland/ is still used, so upgrades do not change a fingerprint clients have already pinned. - Session registry and compositor logs move from /run/user/ to RuntimeDirectory=termland. logind creates /run/user/0 only for a real root login and removes it when the last one ends, deleting live state under a running service. - Sessions no longer hijack the machine's default audio sink. Applications in a session are routed with PULSE_SINK instead, so starting a session no longer silences the local desktop, and a crashed server no longer leaves the default pointed at a dead sink. - Negotiate the audio codec instead of assuming Opus. Wire-compatible in both directions; Opus remains the only codec. - Enumerate session windows and send them to the client, so a client can offer a task list. KDE's own taskbar cannot do this on labwc: it speaks only org_kde_plasma_window_management, which is a KWin protocol. - Document the wire protocol and its data structures in docs/protocol.md. - Add CI, and stop the integration tests stranding a desktop session per run. UPGRADE NOTE: the runtime directory move means detached sessions created by a pre-0.7.0 server are not visible to the new one. Their compositors keep running, orphaned. Close sessions before upgrading, or terminate the leftover compositor processes by hand afterwards. * Thu Aug 6 2026 John Boero - 0.6.1-2 - Disable debuginfo package generation (%%global debug_package %%{nil}): find-debuginfo produces an empty debugsourcefiles.list for this Rust binary, which EL8's rpmbuild treats as a hard error. - BuildRequires the real /usr/bin/perl binary instead of the perl-interpreter virtual-provide, which is a Fedora/RHEL-only naming convention Mageia doesn't recognize. * Wed Aug 5 2026 John Boero - 0.6.1-1 - Fix aarch64 build failure in session isolation: getpwnam_r's buffer was declared Vec, but libc's buffer type follows the platform's c_char (signed on x86_64, unsigned on aarch64) - failed to compile at all on aarch64. Now Vec. - Source1 (vendored crates, needed for the offline COPR build) is now a real URL (a GitHub release asset) instead of a bare filename, so a plain spec-URL COPR build submission works without a local SRPM upload. * Wed Aug 5 2026 John Boero - 0.6.0-1 - Session isolation: setuid into the PAM-authenticated user after --auth, instead of every session running as the server's own (root) user; sessions now also record and enforce an owner so one authenticated user cannot list, attach to, or close another's session. - Embedded SSH (russh) and QUIC transports, the latter now with Q2 (video on its own reliable QUIC stream, audio on datagrams) so a lost video packet no longer head-of-line-blocks control/input. - Bidirectional clipboard sync, including file transfer via clipboard paste. - Real cursor-shape sync (actual compositor cursor bitmap, not a placeholder). - Seamless reconnect: auto-retry with backoff and reattach to the same session after an unexpected connection drop. - --list-sessions / --close-session admin CLI (registry-only, no running server process required). - Desktop session manager (--manager): saved multi-host connection profiles with a live per-host session list, resume/new/close. - First Android client (termland-mobile-core + native Kotlin/Compose app): hardware keyboard/mouse as primary input, touch as secondary, audio playback, session resume list. * Fri Jul 10 2026 John Boero - 0.5.0-1 - Persistent detachable sessions (X2Go/NX-style): a session keeps running on the server after the client disconnects and can be resumed later. Daemon-free — each session is a detached compositor (setsid) tracked in a filesystem registry under $XDG_RUNTIME_DIR/termland/sessions, so even a stateless SSH subsystem connection can list and resume it. - Session control protocol: list / attach / close; session_id in SessionReady. - Disconnect detaches (session persists); explicit close terminates it. * Fri Jul 10 2026 John Boero - 0.4.2-1 - Version bump in lockstep with the client (client-side decode/codec/UX fixes; no server behavior changes) - Shared codec crate: fixed HEVC/H.264 software decode fallback * Fri Jul 10 2026 John Boero - 0.4.1-2 - Build offline from vendored crate deps (COPR/mock build roots have no network) * Fri Jul 10 2026 John Boero - 0.4.1-1 - Version bump to keep server/client in lockstep (client scroll-direction fix; no server-side changes) * Fri Jul 10 2026 John Boero - 0.4.0-1 - v0.4.0 release - Multi-codec video with automatic fallback: AV1, VP9, VP8, H.265/HEVC, H.264 - Codec negotiation: client advertises decodable codecs in SessionCreate; server picks the best its hardware supports and announces it in SessionReady; every VideoFrame is codec-tagged (backward compatible with older peers) - Encoder/decoder probe is hardware-first (open-source codecs first within each tier), so GPUs without AV1 encode (e.g. Volta/GV100) use hardware HEVC/H.264 instead of falling back to software AV1 - Decoder skips the Intel QSV backend ahead of CUVID/VA-API/V4L2 to avoid wasting the opening frames on a doomed QSV context on non-Intel systems * Wed Apr 15 2026 John Boero - 0.3.0-1 - v0.3.0 release - AV1 video encoding (QSV/NVENC/AMF/VA-API/SVT-AV1 auto-detect) - AV1 decoding (QSV/CUVID/dav1d auto-fallback) - Opus audio forwarding via PulseAudio null sink per session - SSH subsystem transport (sshd drop-in config included) - Direct TCP with TLS (auto-generated self-signed certs) + PAM auth - Desktop mode (labwc) and app kiosk mode (cage) - Live window resize with compositor + encoder reinit - Client-side cursor, data rate overlay, menubar (F10) - Shell completions for bash, zsh, fish - systemd service + documented environment config