From 30df687540fee71968af1056f1540ced72e7ef51 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 14:18:45 +0000 Subject: [PATCH 1/2] Add setup-kyber-linux.sh for native Linux Steam Runs Kyber outside Wolf so its embedded WebView2 can initialize without nested-bwrap blocking CLONE_NEWUSER. Builds the Wine prefix, ensures the WebView2 Evergreen runtime is installed (the component that intercepts EA's qrc:// OAuth redirect), registers Kyber as a non-Steam shortcut forced to Proton Experimental, and notes the Steam Remote Play path for headless boxes. No fake cmd.exe / Firefox scaffolding needed here. Co-Authored-By: Claude --- scripts/setup-kyber-linux.sh | 335 +++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100755 scripts/setup-kyber-linux.sh diff --git a/scripts/setup-kyber-linux.sh b/scripts/setup-kyber-linux.sh new file mode 100755 index 0000000..78c0b1b --- /dev/null +++ b/scripts/setup-kyber-linux.sh @@ -0,0 +1,335 @@ +#!/bin/bash +# setup-kyber-linux.sh — Install the Kyber Launcher for SWBF2 (2017) on a +# native Linux Steam machine (headless or desktop) with Proton Experimental. +# +# Kyber is a community multiplayer client for Star Wars Battlefront II (2017) +# after EA shut down the official servers. It is a Windows app (Flutter/Rust) +# with an embedded Microsoft Edge WebView2 used for its EA OAuth login flow. +# +# ── Why this is the RIGHT place to run Kyber ─────────────────────────────── +# +# Kyber's login redirects to a qrc:// URI (a Qt-internal resource scheme) that +# only its OWN embedded WebView2 can intercept. WebView2 therefore MUST work. +# +# Inside a Wolf / Games-on-Whales Docker container, WebView2 cannot start: +# Proton wraps Wine in bubblewrap (bwrap), and nesting that inside Docker +# blocks CLONE_NEWUSER, which WebView2's subprocess sandbox requires. The +# result is endless login failures (see setup-kyber-wolf.sh for the gory +# details and the fake-cmd.exe / Firefox workarounds that still cannot finish +# the flow because no external browser can handle qrc://). +# +# On NATIVE Linux Steam there is only a single bwrap layer (Proton's own), +# which has enough namespace privilege for WebView2 to initialize. So here the +# login flow works as designed: Kyber opens its built-in browser, you log in +# to EA, EA redirects to qrc://, and Kyber's WebView2 catches the auth code. +# +# No fake cmd.exe. No Firefox. No URL watcher. None of that scaffolding is +# needed here — it only existed to work around WebView2 being unable to start. +# +# ── Do you still need WebView2? YES ──────────────────────────────────────── +# +# WebView2 is not the problem — it is the solution. It is the only component +# that can complete Kyber's EA OAuth login. This script makes sure the +# WebView2 Evergreen RUNTIME (not just the bootstrapper stub) is actually +# installed in Kyber's Wine prefix. If only the stub is present, Kyber falls +# back to `cmd /c start ` and login fails even on native Linux. +# +# ── Headless note ────────────────────────────────────────────────────────── +# +# On a headless GPU box you do not need Wolf to stream. Use Steam Remote Play: +# install Steam, set up a virtual display so Steam has something to render to, +# add Kyber + SWBF2, then connect with the Steam Link app from any device. +# This script sets up a dummy/virtual X display if no display is detected. +# +# ── Prerequisites ────────────────────────────────────────────────────────── +# a. Steam installed and launched at least once (native, not Flatpak ideally +# — Flatpak works but paths differ; this script handles both). +# b. SWBF2 (AppID 1237950) installed and working with Proton Experimental +# (run setup-swbf2-linux.sh first). +# c. KyberLauncher.exe downloaded from https://kyber.gg saved to +# ~/Downloads/KyberLauncher.exe (or pass the path as $1). +# +# ── Usage ────────────────────────────────────────────────────────────────── +# chmod +x setup-kyber-linux.sh +# ./setup-kyber-linux.sh [/path/to/KyberLauncher.exe] + +set -euo pipefail + +KYBER_INSTALLER="${1:-$HOME/Downloads/KyberLauncher.exe}" +KYBER_COMPAT_ID="kyber" # Wine prefix name under compatdata/ +KYBER_APPID="9900000001" # non-Steam shortcut appid + +echo "=== Kyber Launcher — Native Linux Steam Setup ===" +echo "" + +# ── Locate Steam home ────────────────────────────────────────────────────── +find_steam_home() { + for candidate in \ + "$HOME/.steam/steam" \ + "$HOME/.local/share/Steam" \ + "$HOME/.var/app/com.valvesoftware.Steam/.steam/steam"; do + if [ -d "$candidate/steamapps" ]; then + echo "$candidate" + return 0 + fi + done + return 1 +} + +STEAM_HOME=$(find_steam_home) || { + echo "ERROR: Steam home not found. Install Steam and launch it once." + exit 1 +} +echo "Steam home: $STEAM_HOME" + +# ── Verify Kyber installer ───────────────────────────────────────────────── +if [ ! -f "$KYBER_INSTALLER" ]; then + echo "" + echo "ERROR: Kyber installer not found at: $KYBER_INSTALLER" + echo "" + echo "Download KyberLauncher.exe from https://kyber.gg, then:" + echo " $0 /path/to/KyberLauncher.exe" + exit 1 +fi +echo "Kyber installer: $KYBER_INSTALLER" + +# ── Locate Proton Experimental ───────────────────────────────────────────── +# Kyber's WebView2 is best supported on Proton Experimental (newest Wine + +# the most complete WebView2/Edge compatibility shims). GE-Proton also works, +# but Experimental tends to have the freshest fixes for Chromium sandboxing. +PROTON_DIR=$(find "$STEAM_HOME/steamapps/common" -maxdepth 1 -type d \ + -iname "Proton Experimental" 2>/dev/null | head -1) +if [ -z "$PROTON_DIR" ]; then + PROTON_DIR=$(find "$STEAM_HOME/steamapps/common" -maxdepth 1 -type d \ + -iname "Proton*" 2>/dev/null | sort | tail -1) +fi +if [ -z "$PROTON_DIR" ] || [ ! -x "$PROTON_DIR/proton" ]; then + echo "" + echo "ERROR: Proton not found under $STEAM_HOME/steamapps/common." + echo " In Steam → Settings → Compatibility, install Proton Experimental," + echo " then re-run this script." + exit 1 +fi +echo "Proton: $PROTON_DIR" + +# ── Headless display check ───────────────────────────────────────────────── +# WebView2 (and Kyber's Flutter UI) need a display to render to. On a headless +# box with no X/Wayland session, Kyber's window has nowhere to draw. +if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then + echo "" + echo "NOTE: No DISPLAY or WAYLAND_DISPLAY detected (headless box)." + echo " For Steam Remote Play you need a virtual display so Steam can" + echo " render. Options:" + echo " - Configure your GPU driver's dummy/virtual display (recommended" + echo " for hardware-encoded Remote Play), OR" + echo " - Run this whole setup under Xvfb for the install step only:" + echo " xvfb-run -a $0 $KYBER_INSTALLER" + echo "" + echo " Continuing the install (the prefix can be built headless), but you" + echo " must have a real or virtual display when you actually launch Kyber." + echo "" +fi + +# ── Build Kyber Wine prefix and run the installer ────────────────────────── +echo "" +echo "[1/5] Creating Kyber Wine prefix and running the installer..." + +KYBER_PFX="$STEAM_HOME/steamapps/compatdata/$KYBER_COMPAT_ID" +mkdir -p "$KYBER_PFX" + +export STEAM_COMPAT_DATA_PATH="$KYBER_PFX" +export STEAM_COMPAT_CLIENT_INSTALL_PATH="$STEAM_HOME" +export PROTON_NO_ESYNC=1 + +# /S runs most NSIS/Inno installers silently. If Kyber's installer ignores it, +# a GUI installer window appears — complete it normally (needs a display). +"$PROTON_DIR/proton" run "$KYBER_INSTALLER" /S || \ + "$PROTON_DIR/proton" run "$KYBER_INSTALLER" || true + +sleep 3 + +KYBER_EXE_PATH=$(find "$KYBER_PFX/pfx" -name "Kyber.exe" 2>/dev/null | head -1) +if [ -z "$KYBER_EXE_PATH" ]; then + echo "" + echo "WARNING: Kyber.exe not found after installation." + echo " If a GUI installer appeared, make sure you completed it." + echo " Default install path assumed; continuing." + KYBER_EXE_WIN='C:\Program Files\Kyber\Kyber.exe' + KYBER_START_DIR='C:\Program Files\Kyber\' +else + echo " Kyber.exe: $KYBER_EXE_PATH" + rel=$(echo "$KYBER_EXE_PATH" | sed "s|.*/pfx/drive_c/||") + KYBER_EXE_WIN="C:\\$(echo "$rel" | sed 's|/|\\|g')" + dir_rel=$(dirname "$rel") + KYBER_START_DIR="C:\\$(echo "$dir_rel" | sed 's|/|\\|g')\\" +fi +echo " Windows path: $KYBER_EXE_WIN" + +# ── Ensure WebView2 Evergreen runtime is installed ───────────────────────── +echo "" +echo "[2/5] Verifying WebView2 runtime in the Kyber prefix..." + +# The Evergreen runtime lives under one of these in the prefix once installed. +WV2_FOUND=$(find "$KYBER_PFX/pfx/drive_c" -iname "msedgewebview2.exe" 2>/dev/null | head -1) +if [ -n "$WV2_FOUND" ]; then + echo " WebView2 runtime present:" + echo " $WV2_FOUND" +else + echo " WebView2 runtime NOT found — installing Evergreen bootstrapper..." + WV2_BOOT="/tmp/MicrosoftEdgeWebview2Setup_$$.exe" + # Microsoft Evergreen Standalone/Bootstrapper installer (stable channel) + WV2_URL="https://go.microsoft.com/fwlink/p/?LinkId=2124703" + if curl -L --progress-bar -o "$WV2_BOOT" "$WV2_URL"; then + # /silent /install performs an unattended Evergreen runtime install. + "$PROTON_DIR/proton" run "$WV2_BOOT" /silent /install || true + rm -f "$WV2_BOOT" + sleep 3 + WV2_FOUND=$(find "$KYBER_PFX/pfx/drive_c" -iname "msedgewebview2.exe" 2>/dev/null | head -1) + if [ -n "$WV2_FOUND" ]; then + echo " WebView2 runtime installed:" + echo " $WV2_FOUND" + else + echo " WARNING: WebView2 install did not produce msedgewebview2.exe." + echo " Kyber may fall back to cmd /c start and fail to log in." + echo " Try installing it manually:" + echo " STEAM_COMPAT_DATA_PATH=$KYBER_PFX \\" + echo " STEAM_COMPAT_CLIENT_INSTALL_PATH=$STEAM_HOME \\" + echo " \"$PROTON_DIR/proton\" run MicrosoftEdgeWebview2Setup.exe /silent /install" + fi + else + echo " WARNING: Could not download WebView2 bootstrapper." + echo " Kyber bundles it too — its installer may have already placed it." + fi +fi + +# ── Add Kyber as a non-Steam shortcut ────────────────────────────────────── +echo "" +echo "[3/5] Adding Kyber as a non-Steam shortcut..." + +STEAM_UID=$(ls "$STEAM_HOME/userdata/" 2>/dev/null | grep -E '^[0-9]+$' | head -1) +if [ -z "$STEAM_UID" ]; then + echo " WARNING: No Steam userdata found — sign in to Steam once, then re-run." + echo " Skipping shortcut + compat mapping." + SKIP_STEAM_CFG=1 +else + SHORTCUTS_DIR="$STEAM_HOME/userdata/$STEAM_UID/config" + SHORTCUTS_FILE="$SHORTCUTS_DIR/shortcuts.vdf" + mkdir -p "$SHORTCUTS_DIR" + [ -f "$SHORTCUTS_FILE" ] && cp "$SHORTCUTS_FILE" "$SHORTCUTS_FILE.bak" + + python3 - "$SHORTCUTS_FILE" "$KYBER_APPID" "$KYBER_EXE_WIN" "$KYBER_START_DIR" << 'PYEOF' +import sys, struct, os + +def s(key, value): # VDF string field + return b'\x01' + key.encode() + b'\x00' + value.encode() + b'\x00' +def i(key, value): # VDF int field + return b'\x02' + key.encode() + b'\x00' + struct.pack(' Date: Tue, 23 Jun 2026 14:25:56 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Add=20fix-wolf-webview2-userns.sh=20?= =?UTF-8?q?=E2=80=94=20finish=20Kyber-in-Wolf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WolfSteam app container already runs seccomp=unconfined + apparmor=unconfined + SYS_ADMIN, so the WebView2 CLONE_NEWUSER failure is gated at the HOST level by kernel.apparmor_restrict_unprivileged_userns=1 (default-on since Ubuntu 23.10), which overrides the unconfined container. This script relaxes that sysctl (and the legacy unprivileged_userns_clone) persistently, then probes userns creation on the host and inside the running container so login can complete without leaving Wolf. Co-Authored-By: Claude --- scripts/fix-wolf-webview2-userns.sh | 168 ++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100755 scripts/fix-wolf-webview2-userns.sh diff --git a/scripts/fix-wolf-webview2-userns.sh b/scripts/fix-wolf-webview2-userns.sh new file mode 100755 index 0000000..7c64ace --- /dev/null +++ b/scripts/fix-wolf-webview2-userns.sh @@ -0,0 +1,168 @@ +#!/bin/bash +# fix-wolf-webview2-userns.sh — Let WebView2 (and other Chromium sandboxes) +# initialize inside Wolf / Games-on-Whales app containers, so Kyber's EA OAuth +# login works WITHOUT leaving Wolf. +# +# ── The problem ──────────────────────────────────────────────────────────── +# +# Kyber's login depends on its embedded Microsoft Edge WebView2. WebView2 (like +# all Chromium-based sandboxes) spawns a zygote/renderer in a NEW user namespace +# via clone(CLONE_NEWUSER). Inside the WolfSteam container that clone fails with +# EPERM, WebView2 cannot start, Kyber falls back to `cmd /c start `, and the +# login flow dies (see setup-kyber-wolf.sh for the full autopsy). +# +# ── Why it is NOT the container's fault ──────────────────────────────────── +# +# The WolfSteam app container ALREADY runs with: +# security_opt = seccomp=unconfined, apparmor=unconfined +# cap_add = SYS_ADMIN, ... +# (see services/wolf.sh, the 'steam' entry in CATALOG). So Docker's own seccomp +# and AppArmor confinement are not the gate. +# +# The real gate is a HOST kernel setting that Ubuntu 23.10+ (and 24.04 / newer +# kernels) ship enabled by default: +# +# kernel.apparmor_restrict_unprivileged_userns = 1 +# +# When set to 1, the kernel's AppArmor LSM blocks creation of unprivileged user +# namespaces for processes under the (even "unconfined") profile — which is +# exactly what WebView2 needs. Because this is enforced at the host kernel level, +# passing apparmor=unconfined to the container does NOT bypass it. The sysctl +# must be relaxed on the host. +# +# Older kernels (Debian, pre-5.x Ubuntu) used a different toggle: +# kernel.unprivileged_userns_clone = 0 (1 = allow) +# This script handles both. +# +# ── What this script does ────────────────────────────────────────────────── +# 1. Reports the current value of both sysctls. +# 2. Sets apparmor_restrict_unprivileged_userns=0 (and unprivileged_userns_clone=1 +# if present) for the running kernel AND persistently via /etc/sysctl.d/. +# 3. Probes whether unprivileged user namespaces now work, on the host and +# inside the running WolfSteam container. +# 4. Tells you to relaunch the Steam app in Moonlight, then retry Kyber login. +# +# ── Security note ────────────────────────────────────────────────────────── +# Relaxing this sysctl re-enables unprivileged user namespaces host-wide. That +# is the pre-23.10 default and how most distros still ship. It widens the kernel +# attack surface slightly (some past CVEs were reachable via unprivileged +# userns). On a single-user home gaming box this is a reasonable trade; on a +# shared/multi-tenant host, weigh it before applying. +# +# ── Usage ────────────────────────────────────────────────────────────────── +# sudo ./fix-wolf-webview2-userns.sh + +set -euo pipefail + +if [ "$(id -u)" != "0" ]; then + echo "Run with sudo: sudo $0" + exit 1 +fi + +SYSCTL_AA="kernel.apparmor_restrict_unprivileged_userns" +SYSCTL_CLONE="kernel.unprivileged_userns_clone" +SYSCTL_FILE="/etc/sysctl.d/99-wolf-webview2-userns.conf" + +echo "=== Wolf WebView2 / unprivileged-userns fix ===" +echo "" + +# ── 1. Report current state ──────────────────────────────────────────────── +have_aa=0 +have_clone=0 +if [ -e "/proc/sys/${SYSCTL_AA//.//}" ]; then + have_aa=1 + cur_aa=$(sysctl -n "$SYSCTL_AA") + echo " $SYSCTL_AA = $cur_aa (1 = blocks WebView2, 0 = allows)" +else + echo " $SYSCTL_AA: not present on this kernel (fine — nothing to relax here)" +fi +if [ -e "/proc/sys/${SYSCTL_CLONE//.//}" ]; then + have_clone=1 + cur_clone=$(sysctl -n "$SYSCTL_CLONE") + echo " $SYSCTL_CLONE = $cur_clone (0 = blocks, 1 = allows)" +else + echo " $SYSCTL_CLONE: not present on this kernel (fine — newer kernels use the AppArmor gate)" +fi +echo "" + +if [ "$have_aa" = 0 ] && [ "$have_clone" = 0 ]; then + echo "Neither sysctl exists — unprivileged user namespaces are not gated by" + echo "these knobs on your kernel. If WebView2 still fails, the cause is" + echo "elsewhere (check 'dmesg' for apparmor/seccomp denials while launching" + echo "Kyber, and confirm the WolfSteam container has seccomp=unconfined)." +fi + +# ── 2. Apply the relaxation (runtime + persistent) ───────────────────────── +echo "[1/3] Applying sysctl changes..." +{ + echo "# Allow unprivileged user namespaces so Chromium/WebView2 sandboxes can" + echo "# initialize inside Wolf app containers (Kyber EA OAuth login)." + echo "# Written by fix-wolf-webview2-userns.sh" +} > "$SYSCTL_FILE" + +if [ "$have_aa" = 1 ]; then + echo "$SYSCTL_AA = 0" >> "$SYSCTL_FILE" + sysctl -w "$SYSCTL_AA=0" >/dev/null + echo " set $SYSCTL_AA = 0 (runtime + $SYSCTL_FILE)" +fi +if [ "$have_clone" = 1 ]; then + echo "$SYSCTL_CLONE = 1" >> "$SYSCTL_FILE" + sysctl -w "$SYSCTL_CLONE=1" >/dev/null + echo " set $SYSCTL_CLONE = 1 (runtime + $SYSCTL_FILE)" +fi +echo " persistent config: $SYSCTL_FILE" +echo "" + +# ── 3. Probe that unprivileged userns now works ──────────────────────────── +echo "[2/3] Probing unprivileged user-namespace creation..." + +probe_host() { + # unshare -U returns non-zero if the kernel refuses a new user namespace. + if unshare -U --map-root-user true 2>/dev/null; then + echo " HOST: unprivileged user namespace OK" + return 0 + else + echo " HOST: still BLOCKED — a reboot may be required for the AppArmor" + echo " policy change to fully take effect. Reboot and re-run." + return 1 + fi +} +probe_host || true + +WOLF_STEAM=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i WolfSteam | head -1 || true) +if [ -n "$WOLF_STEAM" ]; then + echo " Testing inside running container: $WOLF_STEAM" + if docker exec "$WOLF_STEAM" unshare -U --map-root-user true 2>/dev/null; then + echo " CONTAINER: unprivileged user namespace OK — WebView2 should start now" + else + echo " CONTAINER: still blocked. Restart the Steam app in Moonlight" + echo " (stop + relaunch so the container is recreated), then re-run the probe:" + echo " docker exec unshare -U --map-root-user true && echo OK" + fi +else + echo " No running WolfSteam container found — launch Steam in Moonlight," + echo " then retest: docker exec unshare -U --map-root-user true && echo OK" +fi +echo "" + +# ── 4. Next steps ────────────────────────────────────────────────────────── +echo "[3/3] Done." +echo "" +echo "=== Next steps ===" +echo "" +echo " 1. If the HOST probe still showed BLOCKED, REBOOT now and re-run this" +echo " script — some AppArmor policy changes only apply on a fresh boot." +echo " 2. In Moonlight: STOP the Steam app if it is running, then relaunch it" +echo " so Wolf recreates the WolfSteam container with the new host policy." +echo " 3. Launch Kyber. Click Login. WebView2 should now initialize, open the" +echo " EA login page inside Kyber, and — after you log in — catch the qrc://" +echo " redirect INTERNALLY. No fake cmd.exe, no Firefox, no manual steps." +echo "" +echo " If WebView2 STILL fails after a reboot + relaunch:" +echo " - Watch for denials while launching Kyber:" +echo " sudo dmesg -w | grep -i 'apparmor\\|userns\\|seccomp'" +echo " - Confirm the container has the right opts:" +echo " docker inspect $WOLF_STEAM --format '{{.HostConfig.SecurityOpt}}'" +echo " Expected: [seccomp=unconfined apparmor=unconfined]" +echo " - As a last resort, fall back to the native path:" +echo " scripts/setup-kyber-linux.sh"