feat: Enable iPXE stateless kiosk targets

This commit is contained in:
UGA Innovation Factory
2025-12-12 16:12:24 -05:00
committed by Hunter Halloran
parent 91115fc52e
commit c46b0aa685
7 changed files with 147 additions and 118 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
result/
result
deploy

View File

@@ -1,5 +1,5 @@
# This module defines the software stack for a stateless kiosk.
# It includes a custom Firefox wrapper, Cage (Wayland kiosk compositor), and specific networking configuration.
## This module defines the software stack for a stateless kiosk.
# It now uses Sway (Wayland compositor) and Chromium in kiosk mode.
{
config,
lib,
@@ -9,22 +9,15 @@
}:
lib.mkMerge [
(import ./kiosk-browser.nix {
inherit
config
lib
pkgs
inputs
;
inherit config lib pkgs inputs;
})
(import ./services.nix {
inherit config lib pkgs inputs;
})
(import ./net.nix {
inherit
config
lib
pkgs
inputs
;
inherit config lib pkgs inputs;
})
(import ./programs.nix {
inherit config lib pkgs inputs;
})
{
services.openssh.enable = false;
}
]

View File

@@ -1,52 +1,14 @@
# This module configures Firefox for kiosk mode.
# It wraps Firefox with specific policies to disable UI elements and lock down the browser.
# It also includes a startup script that determines the kiosk URL based on the machine's MAC address.
# This module configures Chromium for kiosk mode under Sway.
# It includes a startup script that determines the kiosk URL based on the machine's MAC address.
{ config, lib, pkgs, ... }:
let
kioskPolicies = {
DisableAppUpdate = true;
DisableFirefoxStudies = true;
DisableTelemetry = true;
DisablePocket = true;
DisableSetDesktopBackground = true;
DisableFeedbackCommands = true;
DontCheckDefaultBrowser = true;
OverrideFirstRunPage = "";
OverridePostUpdatePage = "";
NoDefaultBookmarks = true;
DisableProfileImport = true;
Permissions = {
Camera = { Allow = ["homeassistant.lan"]; };
Microphone = { Allow = ["homeassistant.lan"]; };
Location = { Allow = ["homeassistant.lan"]; };
Notifications = { Allow = ["homeassistant.lan"]; };
Clipboard = { Allow = ["homeassistant.lan"]; };
Fullscreen = { Allow = ["homeassistant.lan"]; };
};
macCaseBuilder = (import ./mac-hostmap.nix { inherit lib; }).macCaseBuilder;
macCases = macCaseBuilder {
varName = "STATION";
};
extraPrefs = pkgs.writeText "kiosk-prefs.js" ''
pref("browser.shell.checkDefaultBrowser", false);
pref("browser.startup.homepage_override.mstone", "ignore");
pref("startup.homepage_welcome_url", "");
pref("startup.homepage_welcome_url.additional", "");
pref("browser.sessionstore.resume_from_crash", false);
pref("browser.sessionstore.max_resumed_crashes", 0);
pref("network.captive-portal-service.enabled", false);
pref("network.connectivity-service.enabled", false);
pref("browser.messaging-system.whatsNewPanel.enabled", false);
pref("browser.aboutwelcome.enabled", false);
pref("privacy.popups.showBrowserMessage", false);
'';
firefoxWrapped = pkgs.wrapFirefox pkgs.firefox-unwrapped {
extraPolicies = kioskPolicies;
extraPrefsFiles = [ extraPrefs ];
};
firefoxKiosk = pkgs.writeShellScriptBin "firefoxkiosk" ''
chromiumKiosk = pkgs.writeShellScriptBin "chromiumkiosk" ''
#!/usr/bin/env bash
set -eu
@@ -70,12 +32,7 @@ let
# Map MAC addresses to specific station IDs
case "$MAC" in
"00:e0:4c:46:0b:32") STATION="1" ;;
"00:e0:4c:46:07:26") STATION="2" ;;
"00:e0:4c:46:05:94") STATION="3" ;;
"00:e0:4c:46:07:11") STATION="4" ;;
"00:e0:4c:46:08:02") STATION="5" ;;
"00:e0:4c:46:08:5c") STATION="6" ;;
${macCases}
*) ;;
esac
@@ -93,31 +50,41 @@ let
# Add BrowserID query param if we have one
if [ -n "$BROWSER_ID" ]; then
if [[ "$URL" == *"?"* ]]; then
URL="$URL&BrowserID=$BROWSER_ID"
URL="$URL&BrowserID=$BROWSER_ID"
else
URL="$URL?BrowserID=$BROWSER_ID"
URL="$URL?BrowserID=$BROWSER_ID"
fi
fi
sleep 2
# Wait for the URL to resolve, up to 30 seconds
timeout=30
elapsed=0
while ! ${pkgs.curl}/bin/curl -sf --max-time 2 "$URL" >/dev/null; do
sleep 1
elapsed=$((elapsed+1))
if [ "$elapsed" -ge "$timeout" ]; then
echo "ERROR: $URL did not resolve after $timeout seconds" >&2
exit 1
fi
done
exec ${firefoxWrapped}/bin/firefox --kiosk "$URL"
exec ${pkgs.chromium}/bin/chromium --kiosk --noerrdialogs --disable-infobars --disable-session-crashed-bubble "$URL"
'';
in
{
environment.systemPackages = [ firefoxKiosk ];
environment.systemPackages = [
pkgs.chromium
chromiumKiosk
];
services.xserver.enable = false;
services.seatd.enable = true;
services.cage = {
enable = true;
user = "engr-ugaif";
program = "${firefoxKiosk}/bin/firefoxkiosk";
};
systemd.services.cage = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
systemd.user.services.chromium-kiosk = {
description = "Chromium Kiosk";
wantedBy = [ "graphical-session.target" ];
partOf = [ "graphical-session.target" ];
serviceConfig = {
ExecStart = "${chromiumKiosk}/bin/chromiumkiosk";
Restart = "on-failure";
Type = "simple";
};
};
}

View File

@@ -0,0 +1,27 @@
# Shared MAC address to station mapping and case builder for stateless-kiosk modules
{ lib }:
let
hostmap = {
"00:e0:4c:46:0b:32" = "1";
"00:e0:4c:46:07:26" = "2";
"00:e0:4c:46:05:94" = "3";
"00:e0:4c:46:07:11" = "4";
"00:e0:4c:46:08:02" = "5";
"00:e0:4c:46:08:5c" = "6";
};
# macCaseBuilder: builds a shell case statement from a hostmap
# varName: the shell variable to assign
# prefix: optional string to prepend to the value (default: "")
# attrset: attribute set to use (default: hostmap)
macCaseBuilder = {
varName,
prefix ? "",
attrset ? hostmap
}:
lib.concatStringsSep "\n" (
lib.mapAttrsToList (mac: val: " ${mac}) ${varName}=${prefix}${val} ;;") attrset
);
in
{
inherit hostmap macCaseBuilder;
}

View File

@@ -1,45 +1,24 @@
# This module configures the network for the stateless kiosk.
# It uses systemd-networkd to set up a VLAN (ID 5) on the primary interface.
# This module configures the network for the stateless kiosk using base networking (no systemd-networkd).
{ config, lib, pkgs, inputs, ... }:
{
# Minimal container networking (systemd-networkd)
networking = {
useNetworkd = true;
useNetworkd = false;
networkmanager.enable = false;
dhcpcd.enable = false;
dhcpcd.enable = true;
useDHCP = false;
useHostResolvConf = false;
};
systemd.network = {
enable = true;
wait-online.enable = true;
networks."10-wired" = {
matchConfig.Type = "ether";
networkConfig = {
LinkLocalAddressing = false;
DHCP = "no";
VLAN = [ "vlan5" ];
};
linkConfig.RequiredForOnline = "no";
# Set up VLAN 5 on the primary interface (assume eth0, adjust if needed)
vlans.vlan5 = {
id = 5;
interface = "eth0";
};
netdevs."20-vlan5" = {
netdevConfig = {
Kind = "vlan";
Name = "vlan5";
};
vlanConfig.Id = 5;
};
networks."30-vlan5" = {
matchConfig.Name = "vlan5";
networkConfig = {
DHCP = "ipv4";
IPv6AcceptRA = true;
};
linkConfig.RequiredForOnline = "routable";
interfaces.vlan5 = {
useDHCP = true;
};
};
# Disable systemd-networkd and systemd-hostnamed
systemd.network.enable = false;
}

View File

@@ -0,0 +1,7 @@
{ config, lib, pkgs, ... }:
{
programs.sway = {
enable = true;
wrapperFeatures.gtk = true;
};
}

View File

@@ -0,0 +1,55 @@
{ config, lib, pkgs, ... }:
let
macCaseBuilder = (import ./mac-hostmap.nix { inherit lib; }).macCaseBuilder;
shellCases = macCaseBuilder {
varName = "NEW_HOST";
prefix = "nix-station";
};
in
{
services.xserver.enable = false;
services.seatd.enable = true;
services.openssh.enable = true;
services.dbus.enable = true;
security.polkit.enable = true;
services.displayManager = {
autoLogin = {
enable = true;
user = "engr-ugaif";
};
sddm = {
enable = true;
wayland.enable = true;
};
defaultSession = "sway";
};
systemd.services.dynamic-hostname = {
description = "Set hostname based on MAC address";
wantedBy = [ "sysinit.target" ];
before = [ "network-pre.target" ];
wants = [ "network-pre.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "dynamic-hostname" ''
set -euo pipefail
# Pick first non-loopback interface with a MAC
IFACE="$(ls /sys/class/net | grep -v '^lo$' | head -n1)"
MAC="$(cat /sys/class/net/$IFACE/address | tr '[:upper:]' '[:lower:]')"
case "$MAC" in
${shellCases}
*) NEW_HOST="nix-station-anon" ;;
esac
${pkgs.nettools}/bin/hostname "$NEW_HOST"
'';
};
};
}