My journey to get Alpine + Wayland + River to work on ThinkPad T14 G3 AMD
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
linux-on-desktop/README.md

813 lines
24 KiB

---
gitea: none
include_toc: true
---
# linux-on-desktop
My journey to get Alpine + Wayland + River to work on ThinkPad T14 G3 AMD (21CF004PGE)
## Preparing hardware
Disable secure boot and fn-lock in bios (so that F-keys work as F-keys by default and require Fn for their secondary multimedia functions).
## Installing Alpine
Put alpine standard image on Ventoy USB, load it from Ventoy in GRUB mode.
Follow https://wiki.alpinelinux.org/wiki/Installation
## Postinstall
```
doas apk add nano
```
In `/etc/apk/repositories`, comment out the version you installed, add edge (main, community, testing),
since some of the packages we're going to install (river, element-desktop)
only exist in testing.
Then
```
doas apk update
doas apk upgrade
```
## Usable editor by default (not vi)
```
doas apk add micro
doas apk del nano
echo "export EDITOR=micro" >>.profile
echo "permit setenv { EDITOR=\$EDITOR } :wheel" | tee -a /etc/doas.d/doas.conf
```
## Login manager (needed for desktop)
```
doas apk add elogind polkit-elogind
doas rc-update add elogind
doas rc-update add polkit
doas apk add greetd greetd-agreety
doas rc-update add greetd
```
Reboot.
Make sure that everything works:
you should be automatically greeted with login prompt on `tty7`;
log in, and check that e.g. `XDG_RUNTIME_DIR` is set in `env`.
## Installing river
```
doas apk add river river-doc
doas apk add adwaita-icon-theme foot ttf-dejavu
doas setup-devd udev
doas apk add mesa-dri-gallium mesa-va-gallium
install -Dm0755 /usr/share/doc/river/examples/init -t ~/.config/river
```
Also:
```
doas apk add xwayland
```
because the latest river in testing is built in a way that requires xwayland
(see also: https://github.com/riverwm/river/issues/913).
Try to run with
```
river
```
or
```
dbus-run-session -- river
```
You should see the blue screen of river. Try to open terminal with Win+Shift+Enter. Try to exit with Win+Shift+E.
### Login manager
To get to river after logging in on `tty1`, and get back to `tty1` after exiting river,
change `/etc/greetd/config.toml`
```
command = "agreety --cmd \"dbus-run-session -- river\""
```
and reboot.
### Installing Waybar
```
doas apk add waybar font-roboto font-awesome
```
and add startup section at the end of river init file (`~/.config/river/init`):
```
# startup
riverctl spawn "waybar"
```
### HiDPI
```
doas apk add way-displays
mkdir .config/way-displays
```
and create `.config/way-displays/cfg.yaml` with the following: **TODO**
#### HiDPI - cursors
And in order to have decently sized mouse cursors instead of the tiniest ones, add the following line to the top of `.config/river/init`:
```
riverctl xcursor-theme Adwaita 24
```
This will only affect river itself, the cursor will stay tiny in waybar and firefox and maybe other applications. To solve this:
```
doas apk add gsettings-desktop-schemas
gsettings set org.gnome.desktop.interface cursor-theme 'Adwaita'
```
#### HiDPI - terminal
In terminal (not in terminal emulator in WM; you can always switch to the new terminal with Ctrl+Alt+F2)
```
doas apk add terminus-font
setfont /usr/share/console-fonts/ter-132n.psf.gz
```
If it looks decent: change consolefont in `/etc/conf.d/consolefont` to `"ter-132n.psf.gz"` and `doas rc-update add consolefont boot`
### Environment
In order to not have to create wrapper scripts for all apps:
Create `/usr/local/bin/inga-river` (and later `chmod +x /usr/local/bin/inga-river`) with the [following content](global/usr/local/bin/inga-river).
and replace `\"dbus-run-session -- river\"` with `inga-river` in `/etc/greetd/config.toml`.
### Keyring
`doas apk add gnome-keyring`
Add following lines to `/usr/local/bin/inga-river` before the call to river:
```
eval $(gnome-keyring-daemon)
export SSH_AUTH_SOCK
```
Reboot, login, make sure with `ps aux | grep key` that keyring daemon is running.
### Sudo for GUI apps
`doas apk add polkit-gnome`, and add `riverctl spawn /usr/lib/polkit-gnome/polkit-gnome-authentication-agent` to your river config.
### Launcher
`doas apk add bemenu`, and then add this line into your river config:
```
riverctl map normal Super R spawn 'pidof bemenu-run || bemenu-run -i -n'
```
### Screenshots
`doas apk add grimshot`, then add this line to river config:
```
riverctl map normal None Print spawn 'grimshot copy area'
```
### Clipboard
Works by default, use Ctrl+Shift+C and Ctrl+Shift+V in foot
### Emoji keyboard
`doas apk add rofi-emoji rofi-emoji-wayland wtype font-noto-emoji` and add this line to your river config
```
riverctl map normal Super period spawn 'rofi -modi emoji -show emoji'
```
(For some reason it broke after reboot and only copies things to clipboard, even though `wtype` continues to work without any problems)
### Notifications
```
doas apk add dunst
dunst &
dunstify test
```
You should get a notification with the text "test".
Then add `riverctl spawn "dunst"` to the startup section of your river config.
TODO: [wired-notify](https://github.com/Toqozz/wired-notify) instead of dunst (currently not packaged for alpine)
## Other software
### Firefox
```
doas apk add firefox
firefox
```
go to `about:support` and make sure that Window Protocol is wayland, not xwayland.
(it should be wayland because MOZ_ENABLE_WAYLAND is set to 1 by inga-river)
For some reason, while sound in general works fine in firefox (after following the steps from Hardware section),
in WebRTC pages there is crackling much louder than the actual voices, making it unusable for voice/video calls/meetings.
### Chrome
```
doas apk add chromium
```
It should also be wayland by default, but you can check it by `doas apk add xeyes && xeyes`.
In order for screen sharing to work, go to `chrome://flags/#enable-webrtc-pipewire-capturer` and enable it.
Note though that every time you screenshare, there will be two promps from xdg-desktop-portal, one for picking a source, and another for actually sharing.
### Yubikey
```
doas apk add yubikey-manager
doas addgroup YOUR_USER plugdev
```
(not sure if the two commands above are necessary)
Then:
```
doas rc-service pcscd start
doas rc-update add pcscd
```
Yubikey should work in Chrome
### Archives
`doas apk add ouch` to avoid having to remember `tar` flags etc, and instead do `ouch decompress archive.tar.gz` (for any archive format).
### Git
```
doas apk add git
git config --global credential.helper --store
```
### Telegram
```
doas apk add telegram-desktop
```
### Element
```
doas apk add element-desktop
```
## Hardware
### Sleep
**TODO 2024**
With default settings, laptop goes to sleep after some idle period. When it wakes up, the root fs is readonly, meaning that I have to restart the laptop.
Adding `acpiphp.disable=1` and `pcie_aspm=off` to grub config does not solve the issue.
Disabling S0ix in UEFI only made things worse: even though `cat /sys/power/mem_sleep` reported that S3 (deep) is default,
after system goes to sleep it is impossible to wake it up, it does not react to key or power button presses.
What did solve the issue was:
* reenable S0ix in BIOS,
* `doas apk add linux-firmware-amdgpu`,
* editing `GRUB_CMDLINE_LINUX_DEFAULT` in `/etc/default/grub` to add the following options:
`acpiphp.disable=1 pcie_aspm=off acpi_osi='Windows 2020' iommu=soft`.
Now after waking up (after being suspended with `doas pm-suspend` from `pm-utils` package) root fs is still readwrite.
But sometimes network disappears after wakeup.
For suspend on lid close and unsuspend on open, follow https://wiki.alpinelinux.org/wiki/Suspend_on_LID_close :
```
doas mkdir -p /etc/acpi/LID
doas micro /etc/acpi/LID/00000080
```
should have the following content
```
#!/bin/sh
exec pm-suspend
```
and then
```
doas chmod +x /etc/acpi/LID/00000080
doas rc-service acpid restart
```
### WiFi
`setup-alpine` should have prompted for access point, everything should work.
To avoid having to manually configure wpa_supplicant,
install network manager, and use it instead of out-of-the-box network service:
```
doas apk add networkmanager-wifi networkmanager-tui
doas rc-update add networkmanager
doas rc-update del networking boot
```
and update NetworkManager config (https://gitlab.alpinelinux.org/alpine/aports/-/issues/10267):
`/etc/NetworkManager/NetworkManager.conf` should look like that:
```
[main]
dhcp=internal
[ifupdown]
managed=true
```
After reboot, switching between wireless networks (or between wireless and wired)
can easily be done with `nmtui` in the shell.
### Backlight
```
doas apk add enlighten
doas rc-service udev restart
```
and add the following lines to river config:
```
riverctl map $mode None XF86MonBrightnessUp spawn "BACKLIGHT_DEVICE=amdgpu_bl1 enlighten +5%"
riverctl map $mode None XF86MonBrightnessDown spawn "BACKLIGHT_DEVICE=amdgpu_bl1 enlighten -5%"
```
Backlight control with Fn+F5/F6 should work now.
(`amdgpu_bl0` is specific for this laptop; value for others can be obtained from `/sys/class/backlight/`)
### Trackpoint
Disregard this:
> ```
>find /sys/devices/platform/i8042/ -name name | xargs grep -Fl TrackPoint
> ```
> to find which serio corresponds to trackpoint, then
> ```
> echo 70 | doas tee /sys/devices/platform/i8042/serio1/sensitivity
> ```
> for reasonably low sensitivity (does not persist; for persistence TODO separate udev rule).
Instead of configuring sensitivity, it's probably better to configure pointer speed in river.
Find your trackpoint with `riverctl list-inputs | grep -i trackpoint`,
it will look like `2:10:TPPS/2_Elan_TrackPoint`.
Then add the following to your river config:
```
riverctl input 2:10:TPPS/2_Elan_TrackPoint accel-profile adaptive
riverctl input 2:10:TPPS/2_Elan_TrackPoint pointer-accel -0.5
```
### Sound
Based on https://wiki.alpinelinux.org/wiki/PipeWire
```
doas apk add pipewire wireplumber rtkit pipewire-alsa
#doas addgroup YOURUSER rtkit
```
Then
```
/usr/libexec/pipewire-launcher
```
Make sure that everything works (with `wpctl status`, `pw-cat -p YOURFILE.flac`
or just opening YouTube in FF).
Then make pipewire start automatically: in river config, add another startup line:
```
riverctl spawn "/usr/libexec/pipewire-launcher"
```
Control volume with `wpctl`.
And change the handlers for XF86Audio (adding `-repeat` and replacing the spawned command):
```
riverctl map -repeat $mode None XF86AudioRaiseVolume spawn 'wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 1%+'
riverctl map -repeat $mode None XF86AudioLowerVolume spawn 'wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 1%-'
riverctl map $mode None XF86AudioMute spawn 'wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle'
```
Note that the internal microphone does not work and is not detected by pipewire.
(This still holds on P14s G4 in 2024.)
Only external microphones work.
#### Mic mute button
```
doas apk add alsa-tools
```
Add the following to the river config:
```
riverctl map $mode None XF86AudioMicMute spawn 'amixer --card 1 set "Capture" toggle'
```
(Editing ACPI hooks no longer needed on P14s G4 in 2024.)
### Webcam
Should work after following the steps for "Audio".
Can be tested in https://webrtc.github.io/samples
## Additional
### Screen sharing
```
doas apk add xdg-desktop-portal xdg-desktop-portal-wlr
```
Create `/usr/local/bin/inga-xdg-desktop-portal-wlr` with the following:
```
#!/bin/sh
killall /usr/libexec/xdg-desktop-portal
killall /usr/libexec/xdg-desktop-portal-wlr
/usr/libexec/xdg-desktop-portal-wlr
```
and to your river config
```
riverctl spawn "inga-xdg-desktop-portal-wlr"
```
(for some reason `xdg-desktop-portal` gets started automatically and after that newly started xdg-desktop-portal-wlr does not work;
we need to kill that xdg-desktop-portal first)
Also create `.config/xdg-desktop-portal-wlr/config` with the following:
```
[screencast]
chooser_cmd=bemenu
chooser_type=dmenu
```
### Prevent firefox sharing indicator from taking the entire tile
Add the following lines to river config (before the last `exec` line):
```
riverctl float-filter-add title "Firefox — Sharing Indicator"
riverctl float-filter-add title 'Firefox — Sharing Indicator'
```
(TODO: check which kind of quotes works)
## Development (containers)
### Unprivileged LXC (ran without privileges on host), with routing
(based partially on https://linuxcontainers.org/lxc/getting-started and https://wiki.alpinelinux.org/wiki/LXC)
**Note that docker won't work inside this container**, you'll need to create container using privileged LXD, as described in the next section (see https://discuss.linuxcontainers.org/t/lxc-on-alpine-host-sys-fs-cgroup-is-not-mounted-into-unprivileged-alpine-guest/15026/1 for more details on why this doesn't work).
#### Networking (host)
(assuming that your internet-connected interface is eth0,
and that you want to use 10.157.1.0/24 subnet for the container)
Add the following to `/etc/network/interfaces`:
```
auto br0
iface br0 inet static
bridge-ports dummy0
bridge-stp 0
address 10.157.1.1
netmask 255.255.255.0
```
and do
```
doas apk add bridge
doas modprobe dummy
echo dummy | doas tee -a /etc/modules
echo 1 | doas tee -a /proc/sys/net/ipv4/ip_forward
doas apk add iptables
doas rc-update add iptables
doas iptables --table nat --append POSTROUTING --out-interface eth0 -j MASQUERADE
doas iptables --append FORWARD --in-interface br0 -j ACCEPT
doas /etc/init.d/iptables save
```
#### Containers support
```
doas apk add lxc lxcfs lxc-download xz gnupg
echo "$(id -un):10000000:5000000" | doas tee -a /etc/subuid
echo "$(id -un):10000000:5000000 | doas tee -a /etc/subgid
echo "$(id -un) veth br0 10" | doas tee -a /etc/lxc/lxc-usernet
doas rc-update add cgroups lxc lxcfs dbus
```
#### Creating container
Create `~/.config/lxc/CONTAINERNAME.conf" with the following content:
```
lxc.net.0.type = veth
lxc.net.0.flags = up
lxc.net.0.link = br0
lxc.net.0.ipv4.address = 10.157.1.2/24 10.157.1.255
lxc.net.0.ipv4.gateway = 10.157.1.1
lxc.net.0.veth.pair = veth-if-0
# this is not a mistype, 500K should be enough for all your nesting needs, and 5M in /etc/subuid should be enough if you want to create any other containers
lxc.idmap = u 0 10000000 500000
lxc.idmap = g 0 10000000 500000
lxc.include = /usr/share/lxc/config/nesting.conf
lxc.apparmor.allow_nesting = 1
lxc.seccomp.allow_nesting = 1
lxc.mount.auto = proc sys cgroup:rw:force
```
Then:
```
lxc-create -n CONTAINERNAME -f .config/lxc/CONTAINERNAME.conf -t download
# pick OS (alpine/edge/amd64 in my case)
lxc-start -n CONTAINERNAME # make sure it does not produce any errors
lxc-attach --clear-env -n CONTAINERNAME
```
You'll get into a container root console.
(but still inside this container `apk cgroups start` will produce errors
and mount everything in /sys/fs/cgroups except for openrc as nobody:nobody,
and won't mount openrc at all, and nested containers won't work.)
#### Networking (container)
In container root console, check if network is up with `ifconfig`.
If there are no IPv4 address for eth0, you'll have to configure it manually,
by editing `/etc/network/interfaces`.
The easiest way to do it is by
```
doas micro ~/.local/share/lxc/CONTAINERNAME/rootfs/etc/network/interfaces`
```
on the host.
In the end the file should look like
```
auto eth0
iface eth0 inet static
address 10.157.1.2
netmask 255.255.255.0
gateway 10.157.1.1
hostname $(hostname)
```
Then `rc-service networking restart`, and check `ifconfig`.
If everything is right, there should be an ipv4 address in `ifconfig`,
and `ping 10.157.1.1` inside container and `ping 10.157.1.2` inside host should work.
`ping 8.8.8.8` inside container should work too, thanks for routing.
Now, if `ping google.com` does not work, configure DNS in container:
```
echo nameserver 8.8.8.8 >> /etc/resolv.conf
echo nameserver 8.8.4.4 >> /etc/resolv.conf
```
(or add them using micro on the host, as you did for interfaces)
Make sure `ping 8.8.8.8` works.
APK should work too: `apk add micro neofetch`
#### Creating an user inside container
In container root shell:
```
adduser -g USERNAME USERNAME
adduser USERNAME wheel
echo "permit persist :wheel" >> /etc/doas.d/doas.conf
```
Now exit root shell (just with `exit`), and try `lxc-console -n CONTAINERNAME`.
You should be able to log in using the new username and password.
(To exit lxc console, use Ctrl+A, Q).
### Alternatively: unprivileged LXC using LXD / Incus (ran as privileged service on host)
#### Security notes
Note that with LXD, unprivileged containers run under root, which is not supposed to give them any extra privileges
(source: https://discuss.linuxcontainers.org/t/privileged-and-unprivileged-containers/12060/2), but this implies that:
Containers can only be managed with LXD using root access.
Which means either `doas` for every command (including connecting to the container shell),
or adding your user to the `lxd` group which will have access to LXD daemon,
**which will effectively give your user passwordless sudo (since access to LXD daemon can trivially be used to gain root privileges),
so that any process running under your user can trivially gain root privileges on the host**
(which is for some reason not considered by LXD maintainers to be a problem).
**DO NOT add your user to the `lxd` group, and DO NOT uncomment ` --group lxd` in `/etc/conf.d/lxd`**
Instead the secure way of doing things would probably be to only use lxd as a root,
and connect to the container using ssh.
#### Containers support
As simple as
```
doas apk add incus incus-client
doas rc-update add incusd
doas rc-service start incusd
doas incus admin init
```
Networking with routing should work automatically.
#### SSH support
```
doas apk add openssh-client
ssh-keygen -t ed25519
```
(Also make sure that `echo $SSH_AUTH_SOCK` is not empty; it shouldn't be if gnome-keyring-daemon is configured properly.)
#### Creating container
```
doas incus launch images:alpine/edge -c security.nesting=true -c security.privileged=false -c security.idmap.isolated=true -c security.idmap.size=6553600 test-alpine-container
doas incus exec test-alpine-container -- /bin/ash
```
Networking should work inside of container.
Then, in target container (from root, `lxc-attach`/`lxc exec`)
```
apk add openssh doas
rc-update add sshd
rc-service sshd start
adduser -g YOUR_USER YOUR_USER
adduser YOUR_USER wheel
```
Check IP of container with `ifconfig`, and then on host,
```
ssh-copy-id CONTAINER_IP
ssh CONTAINER_IP
```
### Webdev
#### Accessing dev sites running inside container
On host, create new FF profile for that purpose only. In its `about:config`, enable `network.proxy.allow_hijacking_localhost` (so that requests to localhost are proxied too).
##### With squid (HTTP/HTTPS only, ran as a service)
In container: `doas apk add squid`, and edit `/etc/squid/squid/conf` accordingly
(most likely you'll only need to change local network definition to match the subnet shared between the host and the container).
Then:
```
doas rc-update add squid
doas rc-service squid start
```
(Note that squid requires devfs service to be running).
Configure FF profile to use squid proxy running inside of container.
It is not clear how to get websockets working with squid, information on the web is very sparse.
##### With SSH tunnel (supports websockets)
Alternatively, without any need to squid:
* Configure container for tunnelling support (no idea why it is required for tunnelling to work: https://web.archive.org/web/20210125210954/https://blog.felixbrucker.com/2015/10/01/how-to-enable-tuntap-inside-lxc/):
* On LXC: add `lxc.cgroup.devices.allow = c 10:200 rwm` to your `~/.config/lxc/CONTAINERNAME.conf` file;
* On LXD: `doas lxc config set CONTAINERNAME raw.lxc="lxc.cgroup.devices.allow = c 10:200 rwm"`;
* (of course, restart the container after that);
* Enable `AllowTcpForwarding` and `PermitTunnel` in `/etc/ssh/sshd_config` (and of course restart `sshd`);
* On host, `ssh CONTAINER_IP -ND TUNNEL_PORT` (TUNNEL_PORT can be anything above 1024 to avoid requiring root privileges);
* On host, in target FF profile, configure proxy to use SOCKS v5 proxy on CONTAINER_IP:CONTAINER_PORT (leave HTTP / HTTPS proxy empty), and check "Proxy DNS when using Socks v5" checkbox.
* Note that it will only work as long as ssh tunnelling command on host is running. So you'll need to run it again after reboot etc. Or wrap it in a service for OpenRC.
#### VS Code Remote
Note that VS Code (and all related products) has a protection intended to prevent OSS variants from connecting to proprietary versions of VS code.
However, apparently, it is implemented in such a way that it prevents even different OSS products from connecting to each other.
Only "Code OSS" is packaged for Alpine (until https://gitlab.alpinelinux.org/alpine/aports/-/issues/14860 is implemented);
and only VSCodium has official server-side builds, so you'll need to use custom server-side builds with Code OSS.
Steps to get it running, assuming that you already have keyring and key-based SSH auth (with non-RSA key) configured:
In container (one of the sources: https://github.com/microsoft/vscode-remote-release/issues/6347):
```
doas apk add gcompat libstdc++ curl bash git
```
and enable `AllowTcpForwarding` and `PermitTunnel` in `/etc/ssh/sshd_config`.
On host (where you intend to run IDE client):
`doas apk add code-oss`
> ##### Patch code-oss manifests
>
> (TODO: figure how to optimize this step without having to manually patch them after every update)
>
> In /usr/lib/code-uss/resources/app:
>
> * Update `product.json`: add `commit` field corresponding to the matching release of openvscode-server.
>
> Additionally, if there is no openvscode-server of the exact same version (for example, latest code-oss in aports at some point was 1.83.1, but openvscode-server was only released for 1.83.0):
>
> * Update `package.json`: update `version` and `distro` fields to refer to the compatible version https://github.com/gitpod-io/openvscode-server/releases ;
> * Update `product.json`: update `version` field.
Run `code-oss`, add "Open Remote - SSH" extension by jeanp413.
Go to its settings, and:
* Set "Server Download Url Template" to
`https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v${version}/openvscode-server-v${version}-${os}-${arch}.tar.gz`
(otherwise it will download VSCodium server-side builds by default).
* Set "Experimental: Server Binary Name" to `openvscode-server` (otherwise it will try to launch remote `code-server-oss` which does not exist and never will, taking its name from Code OSS `product.json`).
You'll get remote button under the marketplace button.
Go there, add your host, right-click it, connect.
Connection should be successful, and you should see "connected to remote" on Code OSS main screen.
> ##### Work around certain musl incompatibilities
>
> Some openvscode-server versions cannot start on Alpine (https://github.com/gitpod-io/openvscode-server/issues/534).
>
> In order to solve this, after Open Remote - SSH extension downloaded and unpacked REH binaries and displayed an error saying that it could not start the remote server:
>
> SSH into the container, go to `~/.vscode-server-oss/bin/COMMIT_NAME/`,
> ```
> doas apk add ouch
> wget https://unofficial-builds.nodejs.org/download/release/v18.15.0/node-v18.15.0-linux-x64-musl.tar.xz
> ouch decompress https://unofficial-builds.nodejs.org/download/release/v18.15.0/node-v18.15.0-linux-x64-musl.tar.xz
> rm node
> cp node-v18.15.0-linux-x64-musl/bin/node .
> ```
> (replacing 18.15.0 with the appropriate version from https://github.com/gitpod-io/openvscode-server/blob/main/build/checksums/nodejs.txt for your version of openvscode-server) and tell Code OSS to try again.
"Open folder" should take you to the remote directory structure, allowing you to open projects hosted in container.
All code-related extensions will also work in remote context, isolated from the parent system and unable to affect it,
no matter what malicious npm packages get installed into container.
### Docker
For container terminal apps to be usable:
```
doas apk add ncurses-terminfo
```
#### (inside LXC)
TODO once nesting in LXC works (reference: https://discuss.linuxcontainers.org/t/lxc-on-alpine-host-sys-fs-cgroup-is-not-mounted-into-unprivileged-alpine-guest/15026/1)
#### (inside LXD)
As simple as
```
doas apk add docker
doas rc-update docker start
doas docker run hello-world
```
in the guest.
For networked docker containers, `doas lxc config edit CONTAINERNAME`,
add `linux.kernel_modules: br_netfilter` to the `config:` section,
and restart container (`doas lxc restart CONTAINERNAME`).
## TODO
* Fix internal mic
* Docker in containers ran by unprivileged users
* Make river usable
* Make waybar usable (+waybar fonts)
* nushell + starship instead of ash
* Mail client
* Fix call audio in firefox