sunshine remote setup

This guide documents the complete, native, headless gaming and remote streaming stack on Arch Linux (ul.drheck.dev) using Sunshine, Gamescope, and Steam, accelerated by the dedicated AMD Radeon AI PRO R9700 (32GB VRAM).

Clients connect using the open-source Moonlight client across any device (Desktop, Android TV, iOS, handhelds).


1. System Architecture & Network Topology


2. Required Packages

Install the native gaming stack, graphics drivers, and audio subsystems:

# Core tools, AMD Vulkan drivers, Gamescope, Steam, and audio
sudo pacman -S --needed gamescope steam vulkan-radeon lib32-vulkan-radeon \
  pipewire pipewire-pulse wireplumber seatd socat libcap

# Sunshine binary from AUR
paru -S sunshine-bin

Ensure user luke belongs to the required hardware access groups:

sudo usermod -aG video,audio,input,render,seat luke

Enable systemd lingering so the user's services run 24/7 in the background without requiring an active shell login:

loginctl enable-linger luke

3. Headless Display via Custom Boot EDID

On a headless server with no physical monitor plugged into the AMD GPU, the amdgpu driver will report disconnected and reject mode setting. To solve this without purchasing physical HDMI/DP dummy dongles, the kernel is configured to load an EDID firmware file at boot to simulate a physical 1080p 144Hz monitor on DP-1.

Step 1: Place Verified EDID Binary

Download and save a verified 1080p DisplayPort EDID (256-byte CEA-861 compliant binary) to the firmware directory:

sudo mkdir -p /usr/lib/firmware/edid
curl -s "https://git.linuxtv.org/v4l-utils.git/plain/utils/edid-decode/data/aoc-c24g1-dp" | sudo tee /usr/lib/firmware/edid/aoc_1080p.bin > /dev/null

Step 2: Bundle EDID into Initramfs

Because amdgpu loads early during boot (via the kms hook), the EDID binary must be packaged inside the initramfs. Edit /etc/mkinitcpio.conf:

FILES=(/usr/lib/firmware/edid/aoc_1080p.bin)

Regenerate the initramfs:

sudo mkinitcpio -P

Step 3: Configure Bootloader Kernel Parameters

In /boot/loader/entries/arch.conf, append the drm.edid_firmware and video parameters to the options line:

title Arch
linux /vmlinuz-linux-lts
initrd /amd-ucode.img
initrd /initramfs-linux-lts.img
options root="UUID=4496ac5e-4538-4ce4-8ca2-afc0b828ee80" rw drm.edid_firmware=DP-1:edid/aoc_1080p.bin video=DP-1:1920x1080@60e

After rebooting, the kernel recognizes DP-1 as a permanently connected 1080p DisplayPort monitor with all standard refresh rates.


4. Dual-Stack Networking & IPv4 socat Bridge

In our dual-stack architecture on ul, Sunshine binds natively to its dedicated IPv6 ULA address. To allow IPv4 clients to connect without asymmetric routing issues, a dedicated socat systemd service listens on 10.0.0.112 and forwards bidirectionally to the IPv6 address.

Bridge Script: /usr/local/bin/sunshine-ipv4-bridge.sh

#!/usr/bin/env bash
IPV4="10.0.0.112"
IPV6="fd77:7777:5010:0:1337:f1ac:d00d:4799"

TCP_PORTS=(47984 47989 47990 48010)
UDP_PORTS=(47998 47999 48000 48002 48010)

pids=()

term_handler() {
    for pid in "${pids[@]}"; do
        kill -TERM "$pid" 2>/dev/null
    done
    wait
    exit 0
}
trap term_handler SIGINT SIGTERM

for port in "${TCP_PORTS[@]}"; do
    socat TCP4-LISTEN:"$port",bind="$IPV4",reuseaddr,fork TCP6:["$IPV6"]:"$port" &
    pids+=($!)
done

for port in "${UDP_PORTS[@]}"; do
    socat UDP4-LISTEN:"$port",bind="$IPV4",reuseaddr,fork UDP6:["$IPV6"]:"$port" &
    pids+=($!)
done

wait

Make executable: sudo chmod +x /usr/local/bin/sunshine-ipv4-bridge.sh

Systemd Unit: /etc/systemd/system/sunshine-ipv4-bridge.service

[Unit]
Description=Sunshine IPv4 to IPv6 socat Bridge
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/sunshine-ipv4-bridge.sh
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now sunshine-ipv4-bridge.service

Nginx Reverse Proxy Config

Nginx on ul handles SSL termination and Authentik ForwardAuth for the web interface in /etc/nginx/sites-available/sunshine:

location / {
    proxy_pass https://10.0.0.112:47990;
    proxy_ssl_verify off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade_keepalive_sunshine;
    proxy_set_header Host $host;
    proxy_buffering off;
}

5. Permissions & Hardware Capabilities

1. Game Controller Permissions (/dev/uinput and /dev/uhid)

Sunshine emulates Xbox controllers via uinput and native PlayStation 5 DualSense controllers via uhid. Ensure /etc/udev/rules.d/85-sunshine-uinput.rules contains:

KERNEL=="uinput", SUBSYSTEM=="misc", GROUP="input", MODE="0660"
KERNEL=="uhid", SUBSYSTEM=="misc", GROUP="input", MODE="0660"

Ensure the uhid kernel module loads at boot:

echo "uhid" | sudo tee /etc/modules-load.d/uhid.conf

2. Session Management (seatd)

To allow Gamescope to open DRM display sessions without an interactive login seat, configure /etc/systemd/system/seatd.service.d/group.conf:

[Service]
ExecStart=
ExecStart=seatd -g video

Enable and start: sudo systemctl enable --now seatd

4. DualSense HIDAPI Permissions (/dev/hidraw*)

Steam accesses DualSense controllers via its raw HIDAPI driver on /dev/hidraw*. On a headless server without an interactive physical seat, the default uaccess tag fails to grant permissions to non-root users. Create /etc/udev/rules.d/99-steam-controller-perms.rules:

KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", GROUP="input", MODE="0666"
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", GROUP="input", MODE="0666"
SUBSYSTEM=="hidraw", ENV{HID_NAME}=="*DualSense*", GROUP="input", MODE="0666"
SUBSYSTEM=="hidraw", ENV{HID_NAME}=="*Wireless Controller*", GROUP="input", MODE="0666"
SUBSYSTEM=="hidraw", ENV{HID_PHYS}=="libvirtualhid/uhid/*", GROUP="input", MODE="0666"

Reload udev rules:

sudo udevadm control --reload-rules && sudo udevadm trigger

3. Binary Capabilities

Allow Gamescope to set real-time thread scheduling and Sunshine to capture DRM frames without root:

sudo setcap 'cap_sys_nice=eip' /usr/bin/gamescope
sudo setcap 'cap_sys_admin,cap_sys_nice=eip' /usr/bin/sunshine

6. Sunshine Configuration

File: ~/.config/sunshine/sunshine.conf

system_tray = disabled
min_log_level = info
origin_pin_allowed = lan
origin_web_ui_allowed = lan
csrf_allowed_origins = https://sunshine.drheck.dev

# Network binding
bind_address = fd77:7777:5010:0:1337:f1ac:d00d:4799
address_family = both
external_ip = 10.0.0.112

# Explicit GPU & Display pinning (prevents capturing motherboard ASPEED VGA)
adapter_name = /dev/dri/card1
output_name = DP-1
gamepad = ds5

File: ~/.config/sunshine/apps.json

{
  "env": {
    "PATH": "$(PATH):$(HOME)/bin:$(HOME)/.local/bin"
  },
  "apps": [
    {
      "name": "Steam Big Picture",
      "cmd": "/home/luke/bin/launch-gamescope-steam.sh",
      "image-path": "steam.png"
    }
  ]
}

7. Gamescope Launch Script

Create /home/luke/bin/launch-gamescope-steam.sh:

#!/usr/bin/env bash
set -e

WIDTH="${SUNSHINE_CLIENT_WIDTH:-1920}"
HEIGHT="${SUNSHINE_CLIENT_HEIGHT:-1080}"
FPS="${SUNSHINE_CLIENT_FPS:-60}"

export XDG_RUNTIME_DIR="/run/user/1000"
export LIBSEAT_BACKEND="seatd"
export RADV_PERFTEST="video_encode"
export MESA_VK_DEVICE_SELECT="1002:7551"

# Force Steam to use SDL evdev instead of broken HIDAPI on virtual DualSense
export SDL_JOYSTICK_HIDAPI_PS5=0
export SDL_JOYSTICK_HIDAPI=0

# Verified DualSense mapping for Sunshine virtual pad (including PS Guide button b10)
export SDL_GAMECONTROLLERCONFIG="050057564c050000e60c000000006800,PS5 Controller,crc:5657,platform:Linux,a:b0,b:b1,x:b3,y:b2,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,leftstick:b11,rightx:a3,righty:a4,rightstick:b12,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,back:b8,start:b9,guide:b10,steam:2,050000004c050000e60c000011810000,PS5 Controller,crc:519b,platform:Linux,a:b0,b:b1,x:b3,y:b2,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,leftstick:b11,rightx:a3,righty:a4,rightstick:b12,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,back:b8,start:b9,guide:b10,steam:2,"

exec gamescope -e --backend drm --prefer-vk-device 1002:7551 -W "$WIDTH" -H "$HEIGHT" -r "$FPS" -- steam -bigpicture

Make executable: chmod +x /home/luke/bin/launch-gamescope-steam.sh


8. Sunshine Systemd User Service

Create ~/.config/systemd/user/sunshine.service:

[Unit]
Description=Sunshine Game Stream Host (Headless)
After=network.target pipewire.service
Wants=pipewire.service

[Service]
Type=simple
ExecStart=/usr/bin/sunshine /home/luke/.config/sunshine/sunshine.conf
Restart=always
RestartSec=3

[Install]
WantedBy=default.target

Enable and start:

systemctl --user daemon-reload
systemctl --user enable --now sunshine.service


9. Dedicated Game Storage Configuration (/data/games/steam)

To prevent Steam games, shader caches, and downloading staging files from filling up the root NVMe SSD (/), the game library is permanently linked to the 151 TB ZFS storage pool at /data/games/steam/:

  1. Storage Directory:
    mkdir -p /data/games/steam/steamapps
    chown -R luke:luke /data/games/steam/steamapps
    
  2. Permanent Symlinks:
    ln -sfn /data/games/steam/steamapps ~/.local/share/Steam/steamapps
    ln -sfn /data/games/steam/steamapps ~/.steam/steam/steamapps
    
  3. Steam Library Config (~/.local/share/Steam/config/libraryfolders.vdf): Only library folder "0" (/home/luke/.local/share/Steam) is configured, seamlessly routing 100% of game downloads directly to /data/games/steam/steamapps/. Zero bytes of game files touch the root drive.

10. Troubleshooting & Gotchas

Gotcha 1: Moonlight Pairing Fails with "Client is not authorized"

Gotcha 2: Sunshine Startup Encoder Probe Delay ("Establishing Connection...")

Gotcha 3: Motherboard ASPEED Video Hijacking Stream

Gotcha 4: PS5 DualSense Controller Layout & Android TV

Gotcha 5: Native Linux Builds vs. Proton

Gotcha 6: Linux Console Displaying on TV on Multi-GPU Servers

Gotcha 7: Controller Registers as Keyboard & Mouse in Games (e.g. Portal)

Gotcha 8: PS5 Controller Drops After 5 Seconds (hid_read failure)

Gotcha 9: Controller Moves Mouse Cursor / "NO CONTROLLER" in Games (e.g. Hollow Knight)

Gotcha 10: Accessing Steam Menu & Quitting Games with PS5 Controller


Revision #1
Created 2026-09-21 02:00:10 UTC by luKe
Updated 2026-09-21 21:22:59 UTC by luKe