diff --git a/README.md b/README.md index b2520a6..99e87a2 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,39 @@ sudo scutil --set LocalHostName "$YOUR_HOSTNAME" - **Enable FileVault:** `sudo fdesetup enable` - **Disable Homebrew Analytics:** `brew analytics off` +### Remote Desktop (Ubuntu, optional) + +Not part of the default bootstrap. `install/ubuntu/install-remote-desktop.sh` +sets up an `xrdp` server, then restricts port 3389 to your local subnet via +`ufw` (allowing SSH first so it can't lock you out): + +```bash +bash install/ubuntu/install-remote-desktop.sh # interactive (XFCE) +bash install/ubuntu/install-remote-desktop.sh --yes # no prompt +DESKTOP=gnome bash install/ubuntu/install-remote-desktop.sh # opt into GNOME +SUBNET=10.0.0.0/24 bash install/ubuntu/install-remote-desktop.sh # override subnet +``` + +- **Desktop:** defaults to lightweight **XFCE** (most reliable over RDP). GNOME + is opt-in via `DESKTOP=gnome`. +- **GPU acceleration:** the script disables xorgxrdp glamor (forces software + rendering) so the X backend works on any GPU. GNOME Shell crashes on software + GL, so `DESKTOP=gnome` only works on hosts with functioning GPU acceleration; + everywhere else stick with the XFCE default. +- **Session fallback:** in the XFCE path the script pins the system default + session manager to XFCE. If `~/.xsession` ever goes missing, xrdp would + otherwise fall back to GNOME and black-screen/crash on a GPU-less host; pinning + prevents that silent fallback. +- **Subnet:** auto-detected; override with `SUBNET=` for multi-NIC hosts. +- **Polkit:** the script installs an override that silences the Ubuntu 24.04 + "authentication required to create a color profile" popups over RDP. +- **One session per user:** do not stay logged into the physical console as the + same user you RDP in as. GNOME/XFCE refuse two simultaneous sessions and you + get a black screen or crash (`loginctl list-sessions` to check). +- **Untrusted network:** prefer an SSH tunnel over the LAN rule: + `ssh -L 3389:localhost:3389 user@host`, then point your RDP client at + `localhost:3389`. + ## Platform Compatibility | Platform | Architecture | Status | diff --git a/install/ubuntu/install-remote-desktop.sh b/install/ubuntu/install-remote-desktop.sh new file mode 100755 index 0000000..e63eb79 --- /dev/null +++ b/install/ubuntu/install-remote-desktop.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# Opt-in Ubuntu remote desktop: XFCE + xrdp, LAN-scoped via ufw. +# +# NOT run by bootstrap.sh. Installs a graphical desktop and an RDP server, then +# restricts port 3389 to the local subnet. For untrusted networks prefer an SSH +# tunnel (ssh -L 3389:localhost:3389 user@host) over the LAN firewall rule. +# +# Desktop is selectable via DESKTOP=xfce|gnome|auto (default XFCE: most reliable +# over RDP). GNOME is opt-in (DESKTOP=gnome) and only works on hosts with usable +# GPU acceleration; it crashes on software-rendered GL. +# +# Usage: +# bash install/ubuntu/install-remote-desktop.sh # interactive (XFCE) +# bash install/ubuntu/install-remote-desktop.sh --yes # no prompt +# DESKTOP=gnome bash install/ubuntu/install-remote-desktop.sh +# SUBNET=10.0.0.0/24 bash install/ubuntu/install-remote-desktop.sh +set -euo pipefail + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_err() { echo -e "${RED}[ERR ]${NC} $1" >&2; } + +ASSUME_YES=false +[[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && ASSUME_YES=true + +# --- Guard: Ubuntu/apt only --- +if ! command -v apt-get &>/dev/null; then + log_err "This script targets Ubuntu (apt not found). Aborting." + exit 1 +fi + +# --- Resolve the interactive user (script calls sudo internally) --- +TARGET_USER="${SUDO_USER:-$USER}" +TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6) +if [[ -z "$TARGET_HOME" ]]; then + log_err "Could not resolve home directory for user '$TARGET_USER'." + exit 1 +fi + +# --- Detect LAN subnet (override with SUBNET=...) --- +if [[ -z "${SUBNET:-}" ]]; then + IFACE=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}') + if [[ -n "$IFACE" ]]; then + SUBNET=$(ip route show dev "$IFACE" 2>/dev/null | awk '/proto kernel/ {print $1; exit}') + fi +fi +if [[ -z "${SUBNET:-}" ]]; then + log_err "Could not auto-detect the LAN subnet. Re-run with SUBNET=, e.g. SUBNET=192.168.1.0/24" + exit 1 +fi + +# --- Choose desktop (DESKTOP=xfce|gnome|auto) --- +# Default to XFCE: it is the most reliable desktop over RDP (no GL compositing, +# works on software rendering). GNOME is opt-in via DESKTOP=gnome and only +# suitable on hosts with working GPU acceleration. +DESKTOP="${DESKTOP:-auto}" +[[ "$DESKTOP" == "auto" ]] && DESKTOP=xfce +case "$DESKTOP" in + gnome|xfce) ;; + *) log_err "Invalid DESKTOP='${DESKTOP}'. Use gnome, xfce, or auto."; exit 1 ;; +esac +if [[ "$DESKTOP" == "gnome" ]] && ! command -v gnome-session &>/dev/null; then + log_err "DESKTOP=gnome but gnome-session is not installed." + log_err "Install GNOME first (e.g. sudo apt install ubuntu-desktop-minimal) or use DESKTOP=xfce." + exit 1 +fi + +# --- Confirmation --- +log_warn "This sets up remote desktop using: ${DESKTOP^^}" +[[ "$DESKTOP" == "xfce" ]] && log_warn " XFCE will be installed (pulls in several hundred MB)." +[[ "$DESKTOP" == "gnome" ]] && log_warn " Uses your existing GNOME (no XFCE installed)." +log_warn "An RDP server (xrdp) will run, port 3389/tcp opened to: ${SUBNET}" +log_warn "Desktop session will be configured for user: ${TARGET_USER}" +if [[ "$ASSUME_YES" != true ]]; then + read -r -p "Proceed? [y/N] " reply + [[ "$reply" =~ ^[Yy]$ ]] || { log_info "Aborted."; exit 0; } +fi + +# --- Desktop environment --- +if [[ "$DESKTOP" == "xfce" ]]; then + log_info "Checking XFCE desktop..." + if dpkg -l xfce4 2>/dev/null | grep -q '^ii'; then + log_info "XFCE already installed." + else + log_info "Installing XFCE desktop (this pulls in several hundred MB)..." + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y xfce4 xfce4-goodies dbus-x11 + log_info "XFCE installed." + fi + # XFCE has no Secret Service of its own; without one, Chromium/Electron apps + # warn the OS keyring is unavailable and fall back to plaintext storage. + if dpkg -l gnome-keyring 2>/dev/null | grep -q '^ii'; then + log_info "gnome-keyring already installed." + else + log_info "Installing gnome-keyring (Secret Service for XFCE)..." + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y gnome-keyring libsecret-1-0 seahorse + log_info "gnome-keyring installed." + fi + # Make XFCE the system default session manager. If ~/.xsession is ever missing + # or drifts, Ubuntu falls back to x-session-manager (GNOME by default), which + # crashes on GPUs without acceleration. Pin it to XFCE to prevent that silent + # fallback. (GDM console logins are unaffected; they pick the session directly.) + if command -v xfce4-session &>/dev/null && \ + update-alternatives --list x-session-manager 2>/dev/null | grep -qx /usr/bin/xfce4-session; then + if [[ "$(readlink -f /etc/alternatives/x-session-manager 2>/dev/null)" == /usr/bin/xfce4-session ]]; then + log_info "x-session-manager already defaults to XFCE." + else + log_info "Pinning x-session-manager default to XFCE (prevents GNOME fallback)..." + sudo update-alternatives --set x-session-manager /usr/bin/xfce4-session + fi + fi +else + log_info "Using existing GNOME desktop (no XFCE install)." +fi + +# --- xrdp --- +log_info "Checking xrdp..." +if command -v xrdp &>/dev/null; then + log_info "xrdp already installed." +else + log_info "Installing xrdp..." + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y xrdp + log_info "xrdp installed." +fi + +# --- Disable xorgxrdp glamor (GPU) acceleration for broad compatibility --- +# On old/integrated GPUs glamor's EGL shaders fail to compile, breaking the +# framebuffer-to-client path and producing a black screen. Forcing software +# rendering is rock-solid everywhere and the cost is negligible for a 2D desktop +# over LAN. +XORG_CONF=/etc/X11/xrdp/xorg.conf +if [[ -f "$XORG_CONF" ]]; then + if grep -q 'Option "DRMDevice" ""' "$XORG_CONF" && grep -q 'Option "DRI3" "0"' "$XORG_CONF"; then + log_info "xorgxrdp glamor already disabled." + else + log_info "Disabling xorgxrdp glamor (software rendering) for GPU compatibility..." + [[ -f "${XORG_CONF}.bak" ]] || sudo cp "$XORG_CONF" "${XORG_CONF}.bak" + sudo sed -i -E 's|(Option +"DRMDevice" +)"[^"]*"|\1""|; s|(Option +"DRI3" +)"[^"]*"|\1"0"|' "$XORG_CONF" + log_info "Glamor disabled in ${XORG_CONF}." + fi +else + log_warn "${XORG_CONF} not found; skipping glamor tweak." +fi + +# --- Session config: write ~/.xsession for the chosen desktop --- +log_info "Configuring xrdp ${DESKTOP^^} session for ${TARGET_USER}..." +XS="${TARGET_HOME}/.xsession" +if [[ "$DESKTOP" == "gnome" ]]; then + # GNOME over xrdp must run an Xorg (not Wayland) session. + read -r -d '' XS_CONTENT <<'EOF' || true +export GNOME_SHELL_SESSION_MODE=ubuntu +export XDG_CURRENT_DESKTOP=ubuntu:GNOME +export XDG_SESSION_TYPE=x11 +exec /usr/bin/gnome-session +EOF +else + XS_CONTENT='exec startxfce4' +fi +if [[ -f "$XS" ]] && [[ "$(cat "$XS" 2>/dev/null)" == "$XS_CONTENT" ]]; then + log_info ".xsession already configured for ${DESKTOP}." +else + printf '%s\n' "$XS_CONTENT" | sudo tee "$XS" > /dev/null + sudo chown "${TARGET_USER}:${TARGET_USER}" "$XS" + log_info "Wrote ${XS}" +fi +# Fixes black-screen-on-login from cert permission errors (idempotent). +sudo usermod -aG ssl-cert xrdp + +# --- Polkit: stop the colord "authentication required" popups over RDP --- +# (Known Ubuntu 24.04 regression; affects GNOME and XFCE remote sessions alike.) +POLKIT_RULE=/etc/polkit-1/rules.d/45-allow-colord.rules +if [[ -f "$POLKIT_RULE" ]]; then + log_info "Polkit color-manager override already present." +else + log_info "Installing polkit override to suppress color-profile auth popups..." + sudo tee "$POLKIT_RULE" > /dev/null <<'EOF' +polkit.addRule(function(action, subject) { + if (action.id.indexOf("org.freedesktop.color-manager.") == 0) { + return polkit.Result.YES; + } +}); +EOF + sudo systemctl restart polkit 2>/dev/null || true +fi + +# --- gnome-keyring auto-unlock for XFCE/xrdp (Secret Service via PAM) --- +# GNOME console logins unlock the keyring via PAM; XFCE-over-xrdp does not, so +# Chromium/Electron apps fall back to plaintext storage. Hook pam_gnome_keyring +# into xrdp-sesman so the keyring unlocks with the login password. XFCE only; +# the GNOME path already handles its own keyring. +if [[ "$DESKTOP" == "xfce" ]]; then + PAM_FILE=/etc/pam.d/xrdp-sesman + if [[ -f "$PAM_FILE" ]] && grep -q 'pam_gnome_keyring.so' "$PAM_FILE"; then + log_info "pam_gnome_keyring already configured in ${PAM_FILE}." + elif [[ -f "$PAM_FILE" ]]; then + log_info "Adding pam_gnome_keyring hooks to ${PAM_FILE}..." + sudo tee -a "$PAM_FILE" > /dev/null <<'EOF' + +# gnome-keyring auto-unlock for remote (xrdp) sessions +auth optional pam_gnome_keyring.so +session optional pam_gnome_keyring.so auto_start +EOF + log_warn "Keyring auto-unlocks only if the keyring password matches the login password." + else + log_warn "${PAM_FILE} not found; skipping keyring PAM hook (is xrdp installed?)." + fi +fi + +# --- Firewall: ufw, LAN-only, lockout-safe (allow SSH BEFORE enabling) --- +log_info "Configuring firewall (ufw)..." +if ! command -v ufw &>/dev/null; then + log_info "Installing ufw..." + sudo apt-get install -y ufw +fi +# Allow SSH first so enabling ufw can never strand a remote session. +sudo ufw allow 22/tcp comment 'SSH' > /dev/null +# Restrict RDP to the LAN subnet. +if sudo ufw status | grep -q "3389/tcp.*ALLOW.*${SUBNET}"; then + log_info "ufw rule for 3389 from ${SUBNET} already present." +else + sudo ufw allow from "${SUBNET}" to any port 3389 proto tcp comment 'xrdp LAN' > /dev/null + log_info "Allowed 3389/tcp from ${SUBNET}." +fi +if sudo ufw status | grep -q '^Status: active'; then + log_info "ufw already active." +else + sudo ufw --force enable + log_info "ufw enabled." +fi + +# --- Enable service --- +log_info "Enabling xrdp service..." +sudo systemctl enable --now xrdp +sudo systemctl restart xrdp + +# --- Summary --- +HOST_IP=$(ip -o -f inet addr show "${IFACE:-}" 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1) +echo +log_info "Remote desktop ready." +log_info " Desktop : ${DESKTOP^^}" +log_info " Service : $(systemctl is-active xrdp)" +log_info " Listen : 3389/tcp restricted to ${SUBNET}" +log_info " Connect : RDP client (Microsoft Remote Desktop / Remmina) -> ${HOST_IP:-}:3389" +log_info " Login : user '${TARGET_USER}' with its system password" +echo +log_warn "Do NOT be logged into the console as '${TARGET_USER}' at the same time:" +log_warn " GNOME/XFCE refuse two simultaneous sessions for one user (black screen/crash)." +log_warn " Check with: loginctl list-sessions" +if [[ "$DESKTOP" == "gnome" ]]; then + log_warn "If GNOME shows a black screen (a known 24.04 Xorg-backend bug), pick the" + log_warn " 'Xvnc' session from the dropdown on the xrdp login page instead of 'Xorg'." +fi +if [[ "$DESKTOP" == "xfce" ]]; then + log_info "Keyring: unlocks automatically when its password matches the login password." + log_info " Restart already-running apps (or reconnect) for the keyring to take effect." +fi +echo +log_info "To revert:" +log_info " sudo systemctl disable --now xrdp" +log_info " sudo ufw delete allow from ${SUBNET} to any port 3389 proto tcp"