Title: Creating a NixOS thin gaming client live USB
Author: Solène
Date: 20 May 2022
Tags: nixos gaming
Description: I created a bootable USB media to play on my gaming
computer the games installed on my laptop

# Introduction

This article will cover a use case I suppose very personal, but I love
the way I solved it so let me share this story.

I'm a gamer, mostly on computer, but I have a big rig running Windows
because many games still don't work well with Linux, but I also play
video games on my Linux laptop.  Unfortunately, my laptop only has an
intel integrated graphic card, so many games won't run well enough to
be played, so I'm using an external GPU for some games.  But it's not
ideal, the eGPU is big (think of it as a big shoes box), doesn't have
mouse/keyboard/usb connectors, so I've put it into another room with a
screen at a height to play while standing up, controller in hands. 
This doesn't solve everything, but I can play most games running on it
and allowing a controller.

But if I install a game on both the big rig and the laptop, I have to
manually sync the saves (I'm buying most of the games on GOG which
doesn't have a Linux client to sync saves), it's highly boring and
error-prone.

So, thanks to NixOS, I made a recipe to generate a USB live media to
play on the big rig, using the data from the laptop, so it's acting as
a thin client.  The idea of a read only media to boot from is very
nice, because USB memory sticks are terrible if you try to install
Linux on them (I tried many times, it always ended with I/O errors
quickly) and there is exactly what you need, generated from a
declarative file.

What does it solve concretely? I can play some games on my laptop
anywhere on the small screen, I can also play with my eGPU on the
standing desk, but now I can also play all the installed games from the
big rig with mouse/keyboard/144hz screen.

# What's in the live image?

The generated ISO (USB capable) should come with a desktop environment
like Xfce, Nvidia drivers, Steam, Lutris, Minigalaxy and some other
programs I like to use, I keep the programs list minimal because I
could still use nix-shell to run a program later.

For the system configuration, I declare the user "gaming" with the same
uid as the user on my laptop, and use an NFS mount at boot time.

I'm not using Network Manager because I need the system to get an IP
before connecting to a user account.

# The code

I'll be using flakes for this, it makes pinning so much easier.

I have two files, "flake.nix" and "iso.nix" in the same directory.

flake.nix file:

```flake.nix
{
  inputs = {
    nixpkgs.url = "nixpkgs/nixos-unstable";

  };

  outputs = { self, nixpkgs, ... }@inputs:
    let
      system = "x86_64-linux";

      pkgs = import nixpkgs { inherit system; config = { allowUnfree = true; }; };
      lib = nixpkgs.lib;

    in
    {

      nixosConfigurations.isoimage = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./iso.nix
          "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-base.nix"
        ];
      };

    };
}
```

And iso.nix file:

```iso nix code
{ config, pkgs, ... }:
{

  # compress 6x faster than default
  # but iso is 15% bigger
  # tradeoff acceptable because we don't want to distribute
  # default is xz which is very slow
  isoImage.squashfsCompression = "zstd -Xcompression-level 6";
  
  # my azerty keyboard
  i18n.defaultLocale = "fr_FR.UTF-8";
  services.xserver.layout = "fr";
  console = {
    keyMap = "fr";
  };
  
  # xanmod kernel for better performance
  # see https://xanmod.org/
  boot.kernelPackages = pkgs.linuxPackages_xanmod;
  
  # prevent GPU to stay at 100% performance
  hardware.nvidia.powerManagement.enable = true;
  
  # sound support
  hardware.pulseaudio.enable = true;
 
  # getting IP from dhcp
  # no network manager
  networking.dhcpcd.enable = true;
  networking.hostName = "biggy"; # Define your hostname.
  networking.wireless.enable = false;

  # many programs I use are under a non-free licence
  nixpkgs.config.allowUnfree = true;

  # enable steam
  programs.steam.enable = true;

  # enable ACPI
  services.acpid.enable = true;

  # thermal CPU management
  services.thermald.enable = true;

  # enable XFCE, nvidia driver and autologin
  services.xserver.desktopManager.xfce.enable = true;
  services.xserver.displayManager.lightdm.autoLogin.timeout = 10;
  services.xserver.displayManager.lightdm.enable = true;
  services.xserver.enable = true;
  services.xserver.libinput.enable = true;
  services.xserver.videoDrivers = [ "nvidia" ];
  services.xserver.xkbOptions = "eurosign:e";

  time.timeZone = "Europe/Paris";

  # declare the gaming user and its fixed password
  users.mutableUsers = false;
  users.users.gaming.initialHashedPassword = "$6$bVayIA6aEVMCIGaX$FYkalbiet783049zEfpugGjZ167XxirQ19vk63t.GSRjzxw74rRi6IcpyEdeSuNTHSxi3q1xsaZkzy6clqBU4b0";
  users.users.gaming = {
    isNormalUser = true;
    shell = pkgs.fish;
    uid = 1001;
    extraGroups = [ "networkmanager" "video" ];
  };
  services.xserver.displayManager.autoLogin = {
    enable = true;
    user = "gaming";
  };

  # mount the NFS before login
  systemd.services.mount-gaming = {
    path = with pkgs; [ nfs-utils ];
    serviceConfig.Type = "oneshot";
    script = ''
      mount.nfs -o fsc,nfsvers=4.2,wsize=1048576,rsize=1048576,async,noatime t470-eth.local:/home/jeux/ /home/jeux/
    '';
    before = [ "display-manager.service" ];
    wantedBy = [ "display-manager.service" ];
    after = [ "network-online.target" ];
  };

  # useful packages
  environment.systemPackages = with pkgs; [
    bwm_ng
    chiaki
    dunst # for notify-send required in Dead Cells
    file
    fzf
    kakoune
    libstrangle
    lutris
    mangohud
    minigalaxy
    ncdu
    nfs-utils
    steam
    steam-run
    tmux
    unzip
    vlc
    xorg.libXcursor
    zip
  ];

}
```

Then I can update the sources using "nix flake lock --update-input
nixpkgs", that will tell you the date of the nixpkgs repository image
you are using, and you can compare the dates for updating.  I recommend
using a program like git to keep track of your files, if you see a
failure with a more recent nixpkgs after the lock update, you can have
fun pinpointing the issue and reporting it, or restoring the lock to
the previous version and be able to continue building ISOs.

You can build the iso with the command "nix build
.#nixosConfigurations.isoimage.config.system.build.isoImage", this will
create a symlink "result" in the directory, containing the ISO that you
can burn on a disk or copy to a memory stick using dd.

# Server side

Of course, because I'm using NFS to share the data, I need to configure
my laptop to serves the files over NFS, this is easy to achieve, just
add the following code to your "configuration.nix" file and rebuild the
system:

```configuration.nix
services.nfs.server.enable = true;
services.nfs.server.exports = ''
  /home/gaming 10.42.42.141(rw,nohide,insecure,no_subtree_check)
'';
```

If like me you are using the firewall, I'd recommend opening the NFS
4.2 port (TCP/2049) on the Ethernet interface only:

```configuration.nix
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ ];
networking.firewall.allowedUDPPorts = [ ];
networking.firewall.interfaces.enp0s31f6.allowedTCPPorts = [ 2049 ];
```

In this case, you can see my NFS client is 10.42.42.141, and previously
the NFS server was referred to as laptop-ethernet.local which I declare
in my LAN unbound DNS server.

You could make a specialisation for the NFS server part, so it would
only be enabled when you choose this option at boot.

# NFS performance improvement

If you have a few GB of spare memory on the gaming computer, you can
enable cachefilesd, a service that will cache some NFS accesses to make
the experience even smoother.  You need memory because the cache will
have to be stored in the tmpfs and it needs a few gigabytes to be
useful.

If you want to enable it, just add the code to the iso.nix file, this
will create a 10 MB * 300 cache disk.  As tmpfs lacks user_xattr mount
option, we need to create a raw disk on the tmpfs root partition and
format it with ext4, then mount on the fscache directory used by
cachefilesd.

```nix code
services.cachefilesd.enable = true;
services.cachefilesd.extraConfig = ''
  brun 6%
  bcull 3%
  bstop 1%
  frun 6%
  fcull 3%
  fstop 1%
'';

# hints from http://www.indimon.co.uk/2016/cachefilesd-on-tmpfs/
systemd.services.tmpfs-cache = {
  path = with pkgs; [ e2fsprogs busybox ];
  serviceConfig.Type = "oneshot";
  script = '' 
    if [ ! -f /disk0 ]; then 
      dd if=/dev/zero of=/disk0 bs=10M count=600 
      echo 'y' | mkfs.ext4 /disk0 
    fi 
    mkdir -p /var/cache/fscache 
    mount | grep fscache || mount /disk0 /var/cache/fscache -t ext4 -o loop,user_xattr 
  '';
  before = [ "cachefilesd.service" ];
  wantedBy = [ "cachefilesd.service" ];
};
```

# Security consideration

Opening an NFS server on the network must be done only in a safe LAN,
however I don't consider my gaming account to contain any important
secret, but it would be bad if someone on the LAN mount it and delete
all the files.

However, there are two NFS alternatives that could be used:

* using sshfs using an SSH key that you transport on another media, but
it's tedious for a local LAN, I've been surprised to see sshfs
performance were nearly as good as NFS!
* using sshfs using a password, you could only open ssh to the LAN,
which would make security acceptable in my opinion
* using WireGuard to establish a VPN between the client and the server
and use NFS on top of it, but the secret of the tunnel would be in the
USB memory stick so better not have it stolen

# Size optimization

The generated ISO can be reduced in size by removing some packages.

## Gnome

for example Gnome comes with orca which will bring many dependencies
for text-to-speech.  You can easily exclude many Gnome packages.

```
environment.gnome.excludePackages = with pkgs.gnome; [
  pkgs.orca
  epiphany
  yelp
  totem
  gnome-weather
  gnome-calendar
  gnome-contacts
  gnome-logs
  gnome-maps
  gnome-music
  pkgs.gnome-photos
];
```

## Wine

I found that Wine came with the Windows compiler as a dependency, but
yet it doesn't seem useful for running games in Lutris.
NixOS discourse: Wine installing mingw32 compiler?
It's possible to rebuild Wine used by Lutris without support for the
mingw compiler, replace the lutris line in the "systemPackages" list
with the following code:

```
(lutris-free.override {
  lutris-unwrapped = lutris-unwrapped.override {
    wine = wineWowPackages.staging.override {
      mingwSupport = false;
    };
  };
})
```

Note that I'm using lutris-free which doesn't support Steam because it
makes it a bit lighter and I don't need to manage my Steam games with
Lutris.

# Possible improvements

It could be possible to try getting a package from the nix-store on the
NFS server before trying cache.nixos.org which would improve bandwidth
usage, it's easy to achieve but yet I need to try it in this context.

# Issue

I found Steam games running with Proton are slow to start. I made a bug
report on the Steam Linux client github.
Github:  Proton games takes around 5 minutes to start from a network share
This can be solved partially by mounting
~/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var as
tmpfs, it will uses less than 650MB.

# Conclusion

I really love this setup, I can backup my games and saves from the
laptop, play on the laptop, but now I can extend all this with a bigger
and more comfortable setup. The USB live media doesn't take long to be
copied to a USB memory stick, so in case one is defective, I can just
recopy the image.  The live media can be booted all in memory then be
unplugged, this gives a crazy fast responsive desktop and can't be
altered.

My previous attempts at installing Linux on an USB memory stick all
gave bad results, it was extremely slow, i/o errors were common enough
that the system became unusable after a few hours.  I could add a small
partition to one disk of the big rig or add a new disk, but this will
increase the maintenance of a system that doesn't do much.