diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index f99c18d64..ef685f921 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -66,6 +66,8 @@ - [Public Key Infrastructure](scs/pki.md) - [Security Fix Automation](scs/ghaf-security-fix-automation.md) - [Release Notes](release_notes/release_notes.md) + - [Release ghaf-24.09.4](release_notes/ghaf-24.09.4.md) + - [Release ghaf-24.09.3](release_notes/ghaf-24.09.3.md) - [Release ghaf-24.09.2](release_notes/ghaf-24.09.2.md) - [Release ghaf-24.09.1](release_notes/ghaf-24.09.1.md) - [Release ghaf-24.09](release_notes/ghaf-24.09.md) diff --git a/docs/src/release_notes/ghaf-24.09.3.md b/docs/src/release_notes/ghaf-24.09.3.md new file mode 100644 index 000000000..20045b803 --- /dev/null +++ b/docs/src/release_notes/ghaf-24.09.3.md @@ -0,0 +1,76 @@ + + +# Release ghaf-24.09.3 + +This patch release is targeted at [Secure Laptop](../scenarios/showcases.md#secure-laptop) (Lenovo X1 Carbon) test participants and brings in new features and bug fixes. + +Lenovo X1 Carbon has been fully tested for this release, other platforms have been sanity-tested only. + + +## Release Tag + + + + +## Supported Hardware + +The following target hardware is supported by this release: + +* NVIDIA Jetson AGX Orin +* NVIDIA Jetson Orin NX +* Generic x86 (PC) +* Polarfire Icicle Kit +* Lenovo ThinkPad X1 Carbon Gen 11 +* Lenovo ThinkPad X1 Carbon Gen 10 +* NXP i.MX 8M Plus + + +## What is New in ghaf-24.09.3 + +Lenovo X1 Carbon Gen 10/11: + + * Chromium was replaced with Google Chrome. + * Dynamic updates of Microsoft endpoint URLs. + * Updated GALA version 0.1.30 with SACA[^note1]. + * Bluetooth applet added to the system tray. + * Auto-reconnect hotplugged devices when the VM restarts. + + +## Bug Fixes + +* NVIDIA Jetson AGX Orin/Orin NX: the taskbar is no longer available. +* Bluetooth notification windows stay on the screen. +* Audio recording is delayed by several seconds. + + +## Known Issues and Limitations + +| Issue | Status | Comments | +|-----------------|-------------|--------------------------------------| +| Application menu icons are missing in the first boot after the software installation  | In Progress | Workaround: close and re-open the menu, icons will be available again. | +| Some cursor types are missing causing a cursor to disappear in some cases  | In Progress | Will be fixed in ghaf-24.09.4. | +| Cannot open images and PDF files from the file manager  | In Progress | Will be fixed in ghaf-24.09.4. | +| The Control Panel is non-functional apart from the Display Settings  | In Progress | The functionality will be gradually improved in coming releases. | +| Time synchronization between host and VMs does not work in all scenarios  | In Progress | Under investigation. | +| Suspend does not work from the taskbar power menu  | In Progress | Will be fixed in ghaf-24.09.4. | +| VPN credentials are not saved  | On Hold | It is not clear if this can be fixed. | +| The keyboard always boots up with the English layout  | In Progress | Workaround: use Alt+Shift to switch between English-Arabic-Finnish layout. | + + +## Environment Requirements + +There are no specific requirements for the environment with this release. + + +## Installation Instructions + +Released images are available at [ghafreleasesstorage.z16.web.core.windows.net/ghaf-24-09-3](https://ghafreleasesstorage.z16.web.core.windows.net/ghaf-24-09-3). + +Download the required image and use the following instructions: [Build and Run](../ref_impl/build_and_run). + + +[^note1]: Secure Android Cloud Application + diff --git a/docs/src/release_notes/ghaf-24.09.4.md b/docs/src/release_notes/ghaf-24.09.4.md new file mode 100644 index 000000000..9a531a56d --- /dev/null +++ b/docs/src/release_notes/ghaf-24.09.4.md @@ -0,0 +1,70 @@ + + +# Release ghaf-24.09.4 + +This patch release is targeted at [Secure Laptop](../scenarios/showcases.md#secure-laptop) (Lenovo X1 Carbon) test participants and brings in new features and bug fixes. + +Lenovo X1 Carbon has been fully tested for this release, other platforms have been sanity-tested only. + + +## Release Tag + + + + +## Supported Hardware + +The following target hardware is supported by this release: + +* NVIDIA Jetson AGX Orin +* NVIDIA Jetson Orin NX +* Generic x86 (PC) +* Polarfire Icicle Kit +* Lenovo ThinkPad X1 Carbon Gen 11 +* Lenovo ThinkPad X1 Carbon Gen 10 +* NXP i.MX 8M Plus + + +## What is New in ghaf-24.09.4 + +Lenovo X1 Carbon Gen 10/11: + + * Local and timezone settings are added to the Control Panel. + * The username is displayed on a lock screen. + * The Powerbar module is added to a lock screen. + * System idle behavior reworked. + * Allowed URLs for business-vm are now fetched from the separate configurable repository. + + +## Bug Fixes + +* Some cursor types are missing causing a cursor to disappear in some cases. +* Cannot open images and PDF files from the file manager. +* Suspend does not work from the taskbar power menu. + + +## Known Issues and Limitations + +| Issue | Status | Comments | +|-----------------|-------------|--------------------------------------| +| Application menu icons are missing in the first boot after the software installation  | In Progress | Workaround: close and re-open the menu, icons will be available again. | +| The Control Panel is non-functional apart from the Display Settings, Local and Timezone settings  | In Progress | The functionality will be gradually improved in coming releases. | +| Time synchronization between host and VMs does not work in all scenarios | In Progress | Under investigation. | +| VPN credentials are not saved | On Hold | It is not clear if this can be fixed. | +| The keyboard boots up with the English layout  | In Progress | Workaround: use Alt+Shift to switch between English-Arabic-Finnish layout. | + + +## Environment Requirements + +There are no specific requirements for the environment with this release. + + +## Installation Instructions + +Released images are available at [ghafreleasesstorage.z16.web.core.windows.net/ghaf-24-09-4](https://ghafreleasesstorage.z16.web.core.windows.net/ghaf-24-09-4). + +Download the required image and use the following instructions: [Build and Run](../ref_impl/build_and_run). + diff --git a/docs/src/release_notes/release_notes.md b/docs/src/release_notes/release_notes.md index 0a2a4cb0c..ed725418b 100644 --- a/docs/src/release_notes/release_notes.md +++ b/docs/src/release_notes/release_notes.md @@ -5,13 +5,15 @@ # Ghaf Release Notes -Ghaf is released 4 times per year at the end of each quarter. Additional releases may be made as per request. +Ghaf is released 4 times per year at the end of each quarter. Additional releases or point releases may be made as per request. Point releases are related to user trials as a way to get the fixes and feature requests out faster. -Release numbering scheme: *ghaf-yy.mm.patch*. +Release numbering scheme: *ghaf-yy.mm*. ## In This Chapter +- [Release ghaf-24.09.4](../release_notes/ghaf-24.09.4.md) +- [Release ghaf-24.09.3](../release_notes/ghaf-24.09.3.md) - [Release ghaf-24.09.2](../release_notes/ghaf-24.09.2.md) - [Release ghaf-24.09.1](../release_notes/ghaf-24.09.1.md) - [Release ghaf-24.09](../release_notes/ghaf-24.09.md) diff --git a/flake.lock b/flake.lock index 8ec193540..4f700735f 100644 --- a/flake.lock +++ b/flake.lock @@ -210,11 +210,11 @@ ] }, "locked": { - "lastModified": 1730959682, - "narHash": "sha256-Jx/c9MO3dQBSKXS1MqsafScfQ8uuzJYvc0zMilX6CYQ=", + "lastModified": 1733291815, + "narHash": "sha256-J2lWG+T99LjS3dTp4c4ZrQGfj4qq50mDNodv6gM4fzY=", "owner": "tiiuae", "repo": "ghafpkgs", - "rev": "0d8e48443c757d49f4a6d14abc8065c21f274d1d", + "rev": "39494827ae32568aab54868753add673ecec8390", "type": "github" }, "original": { diff --git a/modules/common/development/debug-tools.nix b/modules/common/development/debug-tools.nix index 0d01e1a1f..84a5136f4 100644 --- a/modules/common/development/debug-tools.nix +++ b/modules/common/development/debug-tools.nix @@ -55,8 +55,8 @@ in iperf tree file - # to build ghaf on target + # to build ghaf on target git # Grpc testing @@ -89,9 +89,11 @@ in ++ lib.optional (config.nixpkgs.hostPlatform.system == "riscv64-linux") perf-test-script-icicle # runtimeShell (unixbench dependency) not available on RISC-V nor on cross-compiled Orin AGX/NX ++ lib.optional (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) pkgs.unixbench - # Build VLC only on x86 + # Build VLC only on x86. Ffmpeg7 and v4l for camera related testing only on x86 ++ lib.optionals (config.nixpkgs.hostPlatform.system == "x86_64-linux") (rmDesktopEntries [ pkgs.vlc + pkgs.ffmpeg_7 + pkgs.v4l-utils ]); }; } diff --git a/modules/common/security/apparmor/default.nix b/modules/common/security/apparmor/default.nix new file mode 100755 index 000000000..67d6f7266 --- /dev/null +++ b/modules/common/security/apparmor/default.nix @@ -0,0 +1,29 @@ +# Copyright 2024-2025 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ config, lib, ... }: +let + cfg = config.ghaf.security.apparmor; +in +{ + ## Option to enable Apparmor security + options.ghaf.security.apparmor = { + enable = lib.mkOption { + description = '' + Enable Apparmor security. + ''; + type = lib.types.bool; + default = false; + }; + }; + + imports = [ + ./profiles/google-chrome.nix + ./profiles/ping.nix + ]; + + config = lib.mkIf cfg.enable { + security.apparmor.enable = true; + security.apparmor.killUnconfinedConfinables = lib.mkDefault true; + services.dbus.apparmor = "enabled"; + }; +} diff --git a/modules/common/security/apparmor/profiles/google-chrome.nix b/modules/common/security/apparmor/profiles/google-chrome.nix new file mode 100644 index 000000000..37b4f8857 --- /dev/null +++ b/modules/common/security/apparmor/profiles/google-chrome.nix @@ -0,0 +1,199 @@ +# Copyright 2024-2025 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + pkgs, + ... +}: +{ + ## Apparmor profile for Chromium + config.security.apparmor.policies."bin.chrome" = lib.mkIf config.ghaf.security.apparmor.enable { + enforce = lib.mkForce true; + profile = '' + abi , + include + + @{INTEGER}=[0-9]{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],}{[0-9],} + @{ETC}=/etc + @{NIX}=/nix + + ${pkgs.google-chrome}/share/google/chrome/google-chrome { + include + include + include + include + + ${config.environment.etc."os-release".source} r, + ${config.environment.etc."lsb-release".source} r, + + capability sys_admin, + capability sys_chroot, + capability sys_ptrace, + + deny @{ETC}/nixos/** r, + deny @{ETC}@{NIX}/** r, + @{NIX}/store/ mr, + @{NIX}/store/** mr, + + ptrace (read, readby), + + ${pkgs.xdg-utils}/bin/* ixr, + ${pkgs.coreutils}/bin/* ixr, + ${pkgs.coreutils-full}/bin/* ixr, + ${pkgs.procps}/bin/ps ixr, + ${pkgs.gnugrep}/bin/grep ixr, + ${pkgs.gnused}/bin/sed ixr, + ${pkgs.which}/bin/which ixr, + ${pkgs.gawk}/bin/* ixr, + ${pkgs.google-chrome}/bin/* ixr, + ${pkgs.google-chrome}/share/google/chrome/* ixr, + ${pkgs.chromium}-sandbox/bin/* ixr, + ${pkgs.givc-cli}/bin/givc-cli ixr, + ${pkgs.open-normal-extension}/* ixr, + ${config.ghaf.services.xdghandlers.handlerPath}/bin/* ixr, + ${pkgs.systemd}/bin/* ixr, + ${pkgs.bashInteractive}/bin/* ixr, + ${pkgs.libressl.nc}/bin/* ixr, + ${pkgs.openssh}/bin/* ixr, + ${pkgs.perlPackages.FileMimeInfo}/bin/mimetype ixr, + + @{sys}/kernel/mm/transparent_hugepage/hpage_pmd_size r, + @{sys}/module/ r, + @{sys}/module/** r, + @{sys}/bus/ r, + @{sys}/bus/** r, + @{sys}/class/ r, + @{sys}/class/** r, + @{sys}/devices/ r, + @{sys}/devices/** r, + @{sys}/fs/ r, + @{sys}/fs/** r, + @{sys}/dev/ rw, + @{sys}/dev/** rw, + @{sys}/devices/pci*/** rw, + + @{PROC} r, + @{PROC}/[0-9]*/net/ipv6_route r, + @{PROC}/[0-9]*/net/arp r, + @{PROC}/[0-9]*/net/if_inet6 r, + @{PROC}/[0-9]*/net/route r, + @{PROC}/[0-9]*/net/ipv6_route r, + @{PROC}/[0-9]*/stat rix, + @{PROC}/[0-9]*/task/@{tid}/comm rw, + @{PROC}/[0-9]*/task/@{tid}/status rix, + owner @{PROC}/[0-9]*/cgroup r, + owner @{PROC}/[0-9]*/fd/ r, + owner @{PROC}/[0-9]*/io r, + owner @{PROC}/[0-9]*/mountinfo r, + owner @{PROC}/[0-9]*/mounts r, + owner @{PROC}/[0-9]*/oom_score_adj w, + owner @{PROC}/[0-9]*/gid_map w, + owner @{PROC}/[0-9]*/setgroups w, + owner @{PROC}/[0-9]*/uid_map w, + owner @{PROC}/[0-9]*/smaps rix, + owner @{PROC}/[0-9]*/statm rix, + owner @{PROC}/[0-9]*/task/ r, + owner @{PROC}/[0-9]*/cmdline rix, + owner @{PROC}/[0-9]*/environ rix, + owner @{PROC}/[0-9]*/clear_refs rw, + owner @{PROC}/[0-9]*/comm rw, + owner @{PROC}/self/* r, + owner @{PROC}/self/exe rix, + owner @{PROC}/self/fd/* rw, + @{PROC}/sys/kernel/yama/ptrace_scope rw, + @{PROC}/sys/fs/inotify/max_user_watches r, + @{PROC}/ati/major r, + + /dev/** rw, + /dev/fb0 rw, + /dev/hidraw@{INTEGER} rw, + /dev/shm/** rw, + /dev/tty rw, + /dev/video@{INTEGER} rw, + owner /dev/shm/pulse-shm* m, + owner /dev/tty@{INTEGER} rw, + /dev/v4l/** rw, + /dev/snd/** rw, + /dev/null rw, + + /run/udev/ ixr, + /run/udev/** ixr, + /run/mount/ ixr, + /run/mount/** ixr, + /run/current-system/sw/bin/* lr, + + @{ETC}/static/ r, + @{ETC}/static/** r, + @{ETC}/chromium/** r, + @{ETC}/host/ r, + @{ETC}/host/** r, + + @{ETC}/opt/ rix, + @{ETC}/opt/** rix, + @{ETC}/static/opt/ rix, + @{ETC}/static/opt/** rix, + @{ETC}/xdg/mimeapps.list lr, + @{ETC}/static/xdg/mimeapps.list lr, + + owner @{HOME} r, + owner @{HOME}/.DCOPserver_* r, + owner @{HOME}/.ICEauthority r, + owner @{HOME}/.nix-profile/ r, + owner @{HOME}/.nix-profile/** r, + owner @{HOME}/.fonts.* lrw, + owner @{HOME}/.cache/ wrk, + owner @{HOME}/.cache/** wrk, + owner @{HOME}/.config/google-chrome rwkm, + owner @{HOME}/.config/google-chrome/** rwkm, + owner @{HOME}/.config/** rw, + owner @{HOME}/.config rw, + + owner @{HOME}/.local/ rw, + owner @{HOME}/.local/** rw, + owner @{HOME}/.local/share/mime/mime.cache rw, + owner @{HOME}/.local/share/applications/mimeapps.list rw, + owner @{HOME}/.pki/ rwkm, + owner @{HOME}/.pki/** rwkm, + owner @{HOME}/Downloads/ rw, + owner @{HOME}/Downloads/** rw, + + owner @{HOME}/Unsafe\ share/ rw, + owner @{HOME}/Unsafe\ share/** rw, + owner @{HOME}/tmp/** rwkl, + owner @{HOME}/tmp/ rw, + + + @{ETC}/profiles r, + @{ETC}/profiles/** r, + @{NIX}/var r, + @{NIX}/var/** r, + + owner @{run}/user/[0-9]*/ rw, + owner @{run}/user/[0-9]*/** rw, + @{run}/user/[0-9]*/ r, + @{run}/user/[0-9]*/** r, + + /var/tmp/ rw, + owner /var/tmp/** rwkl, + + /tmp/ rw, + owner /tmp/** rwkl, + /tmp/.X[0-9]*-lock r, + + deny /boot/EFI/systemd/** r, + deny /boot/EFI/nixos/** r, + deny /boot/loader/** r, + deny /boot/vmlinuz* r, + deny /var/cache/fontconfig/ w, + + ### Networking ### + network inet stream, + network inet6 stream, + network inet dgram, + network inet6 dgram, + network netlink raw, + } + ''; + }; +} diff --git a/modules/common/security/apparmor/profiles/ping.nix b/modules/common/security/apparmor/profiles/ping.nix new file mode 100644 index 000000000..95b757cb0 --- /dev/null +++ b/modules/common/security/apparmor/profiles/ping.nix @@ -0,0 +1,31 @@ +# Copyright 2024-2025 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + pkgs, + ... +}: +{ + ## Apparmor profile for ping + config.security.apparmor.policies."bin.ping" = lib.mkIf config.ghaf.security.apparmor.enable { + profile = '' + #include + ${pkgs.iputils}/bin/ping { + #include + #include + #include + + include "${pkgs.apparmorRulesFromClosure { name = "ping"; } [ pkgs.iputils ]}" + + capability net_raw, + capability setuid, + network inet raw, + + ${pkgs.iputils}/bin/ping mixr, + /etc/modules.conf r, + } + + ''; + }; +} diff --git a/modules/common/security/default.nix b/modules/common/security/default.nix index e3c4b07a0..59e65498b 100644 --- a/modules/common/security/default.nix +++ b/modules/common/security/default.nix @@ -1,3 +1,8 @@ # Copyright 2022-2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ imports = [ ./sshkeys.nix ]; } +{ + imports = [ + ./sshkeys.nix + ./apparmor + ]; +} diff --git a/modules/common/services/default.nix b/modules/common/services/default.nix index c5bff9b7a..18f920387 100644 --- a/modules/common/services/default.nix +++ b/modules/common/services/default.nix @@ -6,7 +6,6 @@ ./audio.nix ./wifi.nix ./firmware.nix - ./desktop.nix ./xdgopener.nix ./xdghandlers.nix ./namespaces.nix diff --git a/modules/common/services/desktop.nix b/modules/common/services/desktop.nix index f3223588b..e4fbfc616 100644 --- a/modules/common/services/desktop.nix +++ b/modules/common/services/desktop.nix @@ -195,7 +195,7 @@ in { name = "Audio Control"; description = "System Audio Control"; - path = "${pkgs.ghaf-audio-control}/bin/GhafAudioControlStandalone --pulseaudio_server=audio-vm:${toString pulseaudioTcpControlPort}"; + path = "${pkgs.ghaf-audio-control}/bin/GhafAudioControlStandalone --pulseaudio_server=audio-vm:${toString pulseaudioTcpControlPort} --indicator_icon_name=preferences-sound"; icon = "preferences-sound"; } diff --git a/modules/common/services/xdghandlers.nix b/modules/common/services/xdghandlers.nix index 68ff09a9e..0e51f14b8 100644 --- a/modules/common/services/xdghandlers.nix +++ b/modules/common/services/xdghandlers.nix @@ -60,8 +60,13 @@ in { options.ghaf.services.xdghandlers = { enable = mkEnableOption "Enable Ghaf XDG handlers"; + handlerPath = lib.mkOption { + description = "Path of xdgHandler script."; + type = lib.types.str; + }; }; config = mkIf cfg.enable { + ghaf.services.xdghandlers.handlerPath = xdgOpenFile.outPath; environment.systemPackages = [ pkgs.xdg-utils xdgPdfItem diff --git a/modules/desktop/graphics/ewwbar.nix b/modules/desktop/graphics/ewwbar.nix index ab295fabb..c66e4373d 100644 --- a/modules/desktop/graphics/ewwbar.nix +++ b/modules/desktop/graphics/ewwbar.nix @@ -143,7 +143,8 @@ let text = '' start() { # Get the number of connected displays using wlr-randr and parse the output with jq - displays=$(wlr-randr --json | jq 'length') + wlr_randr_output=$(wlr-randr --json) + displays=$(echo "$wlr_randr_output" | jq 'length') # Check if there are any connected displays if [ "$displays" -eq 0 ]; then @@ -151,19 +152,17 @@ let exit 1 fi - echo "Found connected displays: $displays" - # Start eww daemon ${ewwCmd} kill - ${ewwCmd} daemon - sleep 0.2 - update-vars + ${ewwCmd} --force-wayland daemon sleep 0.2 + update-vars & # Launch ewwbar for each connected display - for ((display=0; display ~/.config/eww/display && sleep 0.5 + + open_bar() { + local display_name=$1 + ${ewwCmd} open --force-wayland --no-daemonize --screen "$display_name" bar --id bar:"$display_name" --arg screen="$display_name" + } + + close_bar() { + local display_name=$1 + ${ewwCmd} close bar:"$display_name" + } + + wlr_randr_output=$(wlr-randr --json) + prev_displays=$(echo "$wlr_randr_output" | jq 'length') + mapfile -t prev_display_names < <(echo "$wlr_randr_output" | jq -r '.[] | select(.enabled == true) | .model') + + inotifywait -m -e close_write ~/.config/eww/display | while read -r; do + wlr_randr_output=$(wlr-randr --json) + current_displays=$(echo "$wlr_randr_output" | jq 'length') + mapfile -t current_display_names < <(echo "$wlr_randr_output" | jq -r '.[] | select(.enabled == true) | .model') + + if (( current_displays > prev_displays )); then + # Open bars for added displays + mapfile -t added_displays < <(comm -13 <(printf "%s\n" "''${prev_display_names[@]}" | sort) <(printf "%s\n" "''${current_display_names[@]}" | sort)) + for display_name in "''${added_displays[@]}"; do + open_bar "$display_name" + done + elif (( current_displays < prev_displays )); then + # Close bars for removed displays + mapfile -t removed_displays < <(comm -23 <(printf "%s\n" "''${prev_display_names[@]}" | sort) <(printf "%s\n" "''${current_display_names[@]}" | sort)) + for display_name in "''${removed_displays[@]}"; do + close_bar "$display_name" + done + fi + + # Update previous state + prev_displays=$current_displays + prev_display_names=("''${current_display_names[@]}") + done + ''; + }; + mkPopupHandler = { name, @@ -398,6 +449,7 @@ in (defvar volume-popup-visible "false") (defvar brightness-popup-visible "false") + (defvar workspace-popup-visible "false") (defvar workspaces-visible "false") ;; (defpoll bluetooth :interval "3s" :initial "{}" "${pkgs.bt-launcher}/bin/bt-launcher status") @@ -612,7 +664,7 @@ in ;; Brightness Popup Widget ;; (defwidget brightness-popup [] - (revealer :transition "crossfade" :duration "200ms" :reveal brightness-popup-visible + (revealer :transition "crossfade" :duration "200ms" :reveal brightness-popup-visible :active false (box :class "wrapper_widget" (box :class "hotkey" (sys_slider @@ -622,7 +674,7 @@ in ;; Volume Popup Widget ;; (defwidget volume-popup [] - (revealer :transition "crossfade" :duration "200ms" :reveal volume-popup-visible + (revealer :transition "crossfade" :duration "200ms" :reveal volume-popup-visible :active false (box :class "wrapper_widget" (box :class "hotkey" (sys_slider @@ -630,6 +682,13 @@ in :icon {volume.icon} :level {volume.level}))))) + ;; Workspace Popup Widget ;; + (defwidget workspace-popup [] + (revealer :transition "crossfade" :duration "200ms" :reveal workspace-popup-visible :active false + (box :class "wrapper_widget" + (box :class "hotkey" + (label :text "Desktop ''${workspace}"))))) + ;; Quick Settings Button ;; (defwidget quick-settings-button [screen bat-icon vol-icon bright-icon] (button :class "icon_button" @@ -637,7 +696,7 @@ in ''${EWW_CMD} close closer quick-settings & \ else \ ''${EWW_CMD} close power-menu calendar & \ - ''${EWW_CMD} open --screen ''${screen} closer --arg window=\"quick-settings\" && ''${EWW_CMD} open --screen ''${screen} quick-settings; \ + ''${EWW_CMD} open --screen \"''${screen}\" closer --arg window=\"quick-settings\" && ''${EWW_CMD} open --screen \"''${screen}\" quick-settings; \ fi &" (box :orientation "h" :space-evenly "false" @@ -662,7 +721,7 @@ in ''${EWW_CMD} close closer power-menu & \ else \ ''${EWW_CMD} close quick-settings calendar & \ - ''${EWW_CMD} open --screen ''${screen} closer --arg window=\"power-menu\" && ''${EWW_CMD} open --screen ''${screen} power-menu; \ + ''${EWW_CMD} open --screen \"''${screen}\" closer --arg window=\"power-menu\" && ''${EWW_CMD} open --screen \"''${screen}\" power-menu; \ fi &" (box :class "icon" :hexpand false @@ -714,7 +773,7 @@ in ''${EWW_CMD} close closer calendar & \ else \ ''${EWW_CMD} close quick-settings power-menu & \ - ''${EWW_CMD} open --screen ''${screen} closer --arg window=\"calendar\" && ''${EWW_CMD} open --screen ''${screen} calendar; \ + ''${EWW_CMD} open --screen \"''${screen}\" closer --arg window=\"calendar\" && ''${EWW_CMD} open --screen \"''${screen}\" calendar; \ fi &" :class "icon_button date" "''${formattime(EWW_TIME, "%a %b %-d")}")) @@ -734,7 +793,7 @@ in :orientation "h" :space-evenly "false" (button :class "icon_button" - :tooltip "Current workspace" + :tooltip "Current desktop" :onclick {workspaces-visible == "false" ? "''${EWW_CMD} update workspaces-visible=true" : "''${EWW_CMD} update workspaces-visible=false"} workspace) (revealer @@ -744,12 +803,15 @@ in (eventbox :onhoverlost "''${EWW_CMD} update workspaces-visible=false" (box :orientation "h" :space-evenly "true" - (button :class "icon_button" - :onclick "${ghaf-workspace}/bin/ghaf-workspace switch 1; ''${EWW_CMD} update workspaces-visible=false" - "1") - (button :class "icon_button" - :onclick "${ghaf-workspace}/bin/ghaf-workspace switch 2; ''${EWW_CMD} update workspaces-visible=false" - "2")))))) + ${ + lib.concatStringsSep "\n" ( + builtins.map (index: '' + (button :class "icon_button" + :onclick "${ghaf-workspace}/bin/ghaf-workspace switch ${toString index}; ''${EWW_CMD} update workspaces-visible=false" + "${toString index}") + '') (lib.lists.range 1 cfg.maxDesktops) + ) + }))))) (defwidget left [] (box @@ -811,8 +873,7 @@ in :height "36px" :width "100%" :anchor "top center") - :wm-ignore true - :windowtype "normal" + :focusable "false" :hexpand "false" :vexpand "false" :stacking "fg" @@ -862,6 +923,15 @@ in :anchor "bottom center") :stacking "overlay" (brightness-popup)) + + ;; Workspace Popup Window ;; + (defwindow workspace-popup + :monitor 0 + :geometry (geometry :y "150px" + :x "0px" + :anchor "bottom center") + :stacking "overlay" + (workspace-popup)) ''} ;; Closer Window ;; @@ -1138,6 +1208,7 @@ in @include floating_widget($margin: 0, $padding: 10px 12px); @include icon; .slider{ @include slider($slider-width: 150px, $thumb: false, $slider-height: 5px); } + font-size: 1.3em; } .widget-button {@include widget-button; } @@ -1292,7 +1363,7 @@ in }; services.udev.extraRules = '' - ACTION=="change", SUBSYSTEM=="drm", TAG+="systemd", ENV{SYSTEMD_USER_WANTS}="eww-restart.service" + ACTION=="change", SUBSYSTEM=="drm", TAG+="systemd", ENV{SYSTEMD_USER_WANTS}+="eww-display-trigger.service" ''; systemd.user.services.ewwbar = { @@ -1311,18 +1382,6 @@ in partOf = [ "ghaf-session.target" ]; }; - systemd.user.services.eww-restart = { - description = "eww-restart"; - serviceConfig = { - Type = "oneshot"; - ExecStart = "systemctl --user try-restart ewwbar.service"; - Restart = "on-failure"; - RestartSec = "100ms"; - }; - startLimitIntervalSec = 0; - after = [ "ewwbar.service" ]; - }; - systemd.user.services.eww-brightness-popup = { enable = true; serviceConfig = { @@ -1341,6 +1400,27 @@ in partOf = [ "ghaf-session.target" ]; }; + systemd.user.services.eww-display-trigger = { + description = "eww-display-trigger"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.bash}/bin/bash -c 'echo 1 > ~/.config/eww/display'"; + }; + after = [ "ewwbar.service" ]; + }; + + systemd.user.services.eww-display-handler = { + enable = true; + serviceConfig = { + Type = "simple"; + ExecStart = "${eww-display}/bin/eww-display"; + Restart = "on-failure"; + }; + after = [ "ewwbar.service" ]; + wantedBy = [ "ewwbar.service" ]; + partOf = [ "ghaf-session.target" ]; + }; + systemd.user.services.eww-volume-popup = { enable = true; serviceConfig = { @@ -1358,5 +1438,23 @@ in wantedBy = [ "ewwbar.service" ]; partOf = [ "ghaf-session.target" ]; }; + + systemd.user.services.eww-workspace-popup = { + enable = true; + serviceConfig = { + Type = "simple"; + ExecStart = "${ + mkPopupHandler { + name = "workspace-popup-handler"; + stateFile = "workspace"; + popupName = "workspace-popup"; + } + }/bin/workspace-popup-handler"; + Restart = "on-failure"; + }; + after = [ "ewwbar.service" ]; + wantedBy = [ "ewwbar.service" ]; + partOf = [ "ghaf-session.target" ]; + }; }; } diff --git a/modules/desktop/graphics/labwc.config.nix b/modules/desktop/graphics/labwc.config.nix index 1538e7818..5848ba02c 100644 --- a/modules/desktop/graphics/labwc.config.nix +++ b/modules/desktop/graphics/labwc.config.nix @@ -48,7 +48,7 @@ let else ${ghaf-workspace}/bin/ghaf-workspace switch "$current_workspace" fi - ${ghaf-workspace}/bin/ghaf-workspace max 2 + ${ghaf-workspace}/bin/ghaf-workspace max ${toString cfg.maxDesktops} # Write the GTK settings to the settings.ini file in the GTK config directory # Note: @@ -100,22 +100,26 @@ let cascade - + 0 - - - - - - + ${ + lib.concatStringsSep "\n" ( + builtins.map (index: '' + + + + + '') (lib.lists.range 1 cfg.maxDesktops) + ) + } - + - + @@ -343,6 +347,10 @@ in }; }; + services.udev.extraRules = '' + ACTION=="change", SUBSYSTEM=="drm", TAG+="systemd", ENV{SYSTEMD_USER_WANTS}+="mako-reset.service" + ''; + systemd.user.services = { ghaf-launcher = { enable = true; @@ -370,6 +378,13 @@ in wantedBy = [ "ghaf-session.target" ]; }; + mako-reset = { + enable = true; + serviceConfig = { + ExecStart = "${pkgs.mako}/bin/makoctl set-mode default"; + }; + }; + mako = { enable = true; description = "Notification daemon"; diff --git a/modules/desktop/graphics/labwc.nix b/modules/desktop/graphics/labwc.nix index 1348f8c7e..3e52afa10 100644 --- a/modules/desktop/graphics/labwc.nix +++ b/modules/desktop/graphics/labwc.nix @@ -8,6 +8,7 @@ }: let cfg = config.ghaf.graphics.labwc; + userName = config.ghaf.users.accounts.user; in { options.ghaf.graphics.labwc = { @@ -26,7 +27,7 @@ in }; autologinUser = lib.mkOption { type = lib.types.nullOr lib.types.str; - default = config.ghaf.users.accounts.user; + default = userName; description = '' Username of the account that will be automatically logged in to the desktop. If unspecified, the login manager is shown as usual. @@ -81,6 +82,11 @@ in default = [ ]; description = "Wayland security context settings"; }; + maxDesktops = lib.mkOption { + type = lib.types.int; + default = 4; + description = "Max number of virtual desktops."; + }; gtk = lib.mkOption { type = lib.types.submodule { options = { @@ -185,6 +191,11 @@ in # DBus service for accessing the list of user accounts and information attached to those accounts services.accounts-daemon.enable = true; + # We can explicitly specify the icon path, using which user can set custom image when system is locked + system.activationScripts.userIcon.text = '' + mkdir -p /var/lib/AccountsService/users + echo -e "[User]\nIcon=/home/${userName}/Pictures/.face\n" > /var/lib/AccountsService/users/${userName} + ''; ghaf.graphics.launchers = lib.mkIf config.ghaf.profiles.debug.enable [ { diff --git a/modules/hardware/definition.nix b/modules/hardware/definition.nix index 33412faeb..8a5e722e9 100644 --- a/modules/hardware/definition.nix +++ b/modules/hardware/definition.nix @@ -266,6 +266,21 @@ in }; audio = { + removePciDevice = mkOption { + description = "PCI Device path to remove at VM reboot"; + type = types.nullOr types.str; + default = null; + }; + rescanPciDevice = mkOption { + description = "PCI Device path to rescan at VM reboot"; + type = types.nullOr types.str; + default = null; + }; + acpiPath = mkOption { + description = "Path to ACPI file to add to a VM"; + type = types.nullOr types.str; + default = null; + }; # With the current implementation, the whole PCI IOMMU group 14: # 00:1f.x in the example from Lenovo X1 Carbon # must be defined for passthrough to AudioVM diff --git a/modules/microvm/virtualization/microvm/appvm.nix b/modules/microvm/virtualization/microvm/appvm.nix index c0d40dc14..6e912a174 100644 --- a/modules/microvm/virtualization/microvm/appvm.nix +++ b/modules/microvm/virtualization/microvm/appvm.nix @@ -13,20 +13,35 @@ let configHost = config; cfg = config.ghaf.virtualization.microvm.appvm; - sshKeysHelper = pkgs.callPackage ../../../../packages/ssh-keys-helper { - inherit pkgs; - config = configHost; - }; - makeVm = { vm, vmIndex }: let vmName = "${vm.name}-vm"; cid = if vm.cid > 0 then vm.cid else cfg.vsockBaseCID + vmIndex; + # A list of applications for the GIVC service + givcApplications = map (app: { + name = app.givcName; + command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/${app.command}"; + args = app.givcArgs; + }) vm.applications; + # Packages and extra modules from all applications defined in the appvm + appPackages = builtins.concatLists (map (app: app.packages) vm.applications); + appExtraModules = builtins.concatLists (map (app: app.extraModules) vm.applications); + sshKeysHelper = pkgs.callPackage ../../../../packages/ssh-keys-helper { + inherit pkgs; + config = configHost; + }; appvmConfiguration = { imports = [ inputs.impermanence.nixosModules.impermanence inputs.self.nixosModules.givc-appvm + { + ghaf.givc.appvm = { + enable = true; + name = lib.mkForce vmName; + applications = givcApplications; + }; + } (import ./common/vm-networking.nix { inherit config lib vmName; inherit (vm) macAddress; @@ -126,7 +141,7 @@ let pkgs.tpm2-tools pkgs.opensc pkgs.givc-cli - ]; + ] ++ vm.packages ++ appPackages; security.tpm2 = { enable = true; @@ -137,6 +152,8 @@ let lib.mkIf configHost.ghaf.virtualization.microvm.idsvm.mitmproxy.enable [ ./idsvm/mitmproxy/mitmproxy-ca/mitmproxy-ca-cert.pem ]; + time.timeZone = configHost.time.timeZone; + microvm = { optimize.enable = false; mem = vm.ramMb; @@ -197,19 +214,9 @@ let { autostart = true; config = appvmConfiguration // { - imports = - appvmConfiguration.imports - ++ cfg.extraModules - ++ vm.extraModules - ++ [ { environment.systemPackages = vm.packages; } ]; + imports = appvmConfiguration.imports ++ cfg.extraModules ++ vm.extraModules ++ appExtraModules; }; }; - - # Host service dependencies - after = optional config.ghaf.services.audio.enable "pulseaudio.service"; - requires = after; - # Sleep appvms to give gui-vm time to start - serviceConfig.ExecStartPre = "/bin/sh -c 'sleep 8'"; in { options.ghaf.virtualization.microvm.appvm = { @@ -218,7 +225,7 @@ in description = '' List of AppVMs to be created ''; - type = lib.types.listOf ( + type = types.listOf ( types.submodule { options = { name = mkOption { @@ -227,6 +234,62 @@ in ''; type = types.str; }; + applications = mkOption { + description = '' + Applications to include in the AppVM + ''; + type = types.listOf ( + types.submodule ( + { config, lib, ... }: + { + options = rec { + name = mkOption { + type = types.str; + description = "The name of the application"; + }; + description = mkOption { + type = types.str; + description = "A brief description of the application"; + }; + packages = mkOption { + type = types.listOf types.package; + description = "A list of packages required for the application"; + default = [ ]; + }; + icon = mkOption { + type = types.str; + description = "Application icon"; + default = null; + }; + command = mkOption { + type = types.str; + description = "The command to run the application"; + default = null; + }; + extraModules = mkOption { + description = "Additional modules required for the application"; + type = types.listOf types.attrs; + default = [ ]; + }; + givcName = mkOption { + description = "GIVC name for the application"; + type = types.str; + }; + givcArgs = mkOption { + description = "A list of GIVC arguments for the application"; + type = types.listOf types.str; + default = [ ]; + }; + }; + config = { + # Create a default GIVC name for the application + givcName = lib.mkDefault (lib.strings.toLower (lib.replaceStrings [ " " ] [ "-" ] config.name)); + }; + } + ) + ); + default = [ ]; + }; packages = mkOption { description = '' Packages that are included into the AppVM @@ -356,6 +419,7 @@ in ) cfg.vms; in lib.mkIf cfg.enable { + # Define microvms for each AppVM configuration microvm.vms = let vms = lib.imap0 (vmIndex: vm: { "${vm.name}-vm" = makeVm { inherit vmIndex vm; }; }) cfg.vms; @@ -367,7 +431,11 @@ in let serviceDependencies = map (vm: { "microvm@${vm.name}-vm" = { - inherit after requires serviceConfig; + # Host service dependencies + after = optional config.ghaf.services.audio.enable "pulseaudio.service"; + requires = optional config.ghaf.services.audio.enable "pulseaudio.service"; + # Sleep appvms to give gui-vm time to start + serviceConfig.ExecStartPre = "/bin/sh -c 'sleep 8'"; }; "${vm.name}-swtpm" = makeSwtpmService { inherit vm; }; }) cfg.vms; diff --git a/modules/microvm/virtualization/microvm/audiovm.nix b/modules/microvm/virtualization/microvm/audiovm.nix index 438aa14ed..64c47cab1 100644 --- a/modules/microvm/virtualization/microvm/audiovm.nix +++ b/modules/microvm/virtualization/microvm/audiovm.nix @@ -12,6 +12,7 @@ let vmName = "audio-vm"; macAddress = "02:00:00:03:03:03"; isGuiVmEnabled = config.ghaf.virtualization.microvm.guivm.enable; + has_acpi_path = config.ghaf.hardware.definition.audio.acpiPath != null; sshKeysHelper = pkgs.callPackage ../../../../packages/ssh-keys-helper { inherit pkgs; @@ -119,12 +120,15 @@ let aarch64-linux = "virt"; } .${configHost.nixpkgs.hostPlatform.system}; - extraArgs = [ - "-device" - "qemu-xhci" - "-acpitable" - "file=/sys/firmware/acpi/tables/NHLT" - ]; + extraArgs = + [ + "-device" + "qemu-xhci" + ] + ++ lib.optionals has_acpi_path [ + "-acpitable" + "file=${config.ghaf.hardware.definition.audio.acpiPath}" + ]; }; }; diff --git a/modules/microvm/virtualization/microvm/guivm.nix b/modules/microvm/virtualization/microvm/guivm.nix index bc6f5e18a..9dd1759d5 100644 --- a/modules/microvm/virtualization/microvm/guivm.nix +++ b/modules/microvm/virtualization/microvm/guivm.nix @@ -42,6 +42,27 @@ let ${lib.optionalString config.ghaf.givc.enableTls "--key /run/givc/ghaf-host-key.pem"} ${lib.optionalString (!config.ghaf.givc.enableTls) "--notls"} ''; + # A list of applications from all AppVMs + virtualApps = lib.lists.concatMap ( + vm: map (app: app // { vmName = "${vm.name}-vm"; }) vm.applications + ) config.ghaf.virtualization.microvm.appvm.vms; + + # Launchers for all virtualized applications that run in AppVMs + virtualLaunchers = map (app: rec { + inherit (app) name; + inherit (app) description; + #inherit (app) givcName; + vm = app.vmName; + path = "${pkgs.givc-cli}/bin/givc-cli ${cliArgs} start --vm ${vm} ${app.givcName}"; + inherit (app) icon; + }) virtualApps; + # Launchers for all desktop, non-virtualized applications that run in the GUIVM + guivmLaunchers = map (app: { + inherit (app) name; + inherit (app) description; + path = app.command; + inherit (app) icon; + }) cfg.applications; in { ghaf = { @@ -52,6 +73,9 @@ let graphics.enable = true; }; + # Create launchers for regular apps running in the GUIVM and virtualized ones if GIVC is enabled + graphics.launchers = guivmLaunchers ++ lib.optionals config.ghaf.givc.enable virtualLaunchers; + # To enable screen locking set to true graphics.labwc = { autolock.enable = lib.mkDefault config.ghaf.graphics.labwc.autolock.enable; @@ -251,6 +275,21 @@ let # We dont enable services.blueman because it adds blueman desktop entry services.dbus.packages = [ pkgs.blueman ]; systemd.packages = [ pkgs.blueman ]; + + systemd.user.services.audio-control = { + enable = true; + description = "Audio Control application"; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + RestartSec = "5"; + ExecStart = "${pkgs.ghaf-audio-control}/bin/GhafAudioControlStandalone --pulseaudio_server=audio-vm:${toString config.ghaf.services.audio.pulseaudioTcpControlPort} --deamon_mode=true --indicator_icon_name=preferences-sound"; + }; + + partOf = [ "ghaf-session.target" ]; + wantedBy = [ "ghaf-session.target" ]; + }; } ) ]; @@ -288,6 +327,37 @@ in Context Identifier (CID) of the GUIVM VSOCK ''; }; + + applications = lib.mkOption { + description = '' + Applications to include in the GUIVM + ''; + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "The name of the application"; + }; + description = lib.mkOption { + type = lib.types.str; + description = "A brief description of the application"; + }; + icon = lib.mkOption { + type = lib.types.str; + description = "Application icon"; + default = null; + }; + command = lib.mkOption { + type = lib.types.str; + description = "The command to run the application"; + default = null; + }; + }; + } + ); + default = [ ]; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/microvm/virtualization/microvm/microvm-host.nix b/modules/microvm/virtualization/microvm/microvm-host.nix index b1c57274c..f049cdc80 100644 --- a/modules/microvm/virtualization/microvm/microvm-host.nix +++ b/modules/microvm/virtualization/microvm/microvm-host.nix @@ -9,6 +9,14 @@ }: let cfg = config.ghaf.virtualization.microvm-host; + has_remove_pci_device = config.ghaf.hardware.definition.audio.removePciDevice != null; + has_rescan_pci_device = config.ghaf.hardware.definition.audio.rescanPciDevice != null; + has_acpi_path = config.ghaf.hardware.definition.audio.acpiPath != null; + rescan_pci_device = + if has_rescan_pci_device then + config.ghaf.hardware.definition.audio.rescanPciDevice + else + config.ghaf.hardware.definition.audio.removePciDevice; in { imports = [ @@ -62,21 +70,21 @@ in lib.optionalAttrs config.ghaf.virtualization.microvm.audiovm.enable { # The + here is a systemd feature to make the script run as root. - ExecStartPre = [ + ExecStartPre = lib.mkIf has_acpi_path [ "+${pkgs.writeShellScript "ACPI-table-permission" '' # The script gives permissionf sot a microvm user # to read ACPI tables of soundcaed mic array. - ${pkgs.coreutils}/bin/chmod 444 /sys/firmware/acpi/tables/NHLT + ${pkgs.coreutils}/bin/chmod 444 ${config.ghaf.hardware.definition.audio.acpiPath} ''}" ]; - ExecStopPost = [ + ExecStopPost = lib.mkIf has_remove_pci_device [ "+${pkgs.writeShellScript "reload-audio" '' # The script makes audio device internal state to reset # This fixes issue of audio device getting into some unexpected # state when the VM is being shutdown during audio mic recording - echo "1" > /sys/bus/pci/devices/0000:00:1f.3/remove + echo "1" > ${config.ghaf.hardware.definition.audio.removePciDevice} sleep 0.1 - echo "1" > /sys/bus/pci/devices/0000:00:1f.0/rescan + echo "1" > ${rescan_pci_device} ''}" ]; }; diff --git a/modules/microvm/virtualization/microvm/modules.nix b/modules/microvm/virtualization/microvm/modules.nix index 96dbd91e7..63b7fa852 100644 --- a/modules/microvm/virtualization/microvm/modules.nix +++ b/modules/microvm/virtualization/microvm/modules.nix @@ -65,11 +65,6 @@ let # Fprint module fprint = optionalAttrs cfg.guivm.fprint { config.ghaf.services.fprint.enable = true; }; - # Desktop module - desktop = { - config.ghaf.services.desktop.enable = true; - }; - # XDG opener xdgOpener = { config.ghaf.services.xdgopener.enable = true; @@ -162,7 +157,6 @@ in kernelConfigs.guivm firmwareModule qemuModules.guivm - serviceModules.desktop serviceModules.fprint serviceModules.yubikey serviceModules.xdgOpener diff --git a/modules/reference/appvms/appflowy.nix b/modules/reference/appvms/appflowy.nix deleted file mode 100644 index 7a7604abf..000000000 --- a/modules/reference/appvms/appflowy.nix +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2024 TII (SSRC) and the Ghaf contributors -# SPDX-License-Identifier: Apache-2.0 -# -{ - lib, - pkgs, - config, - ... -}: -{ - name = "appflowy"; - packages = [ pkgs.appflowy ]; - macAddress = "02:00:00:03:08:01"; - ramMb = 768; - cores = 1; - extraModules = [ - { - hardware.graphics.enable = true; - time.timeZone = config.time.timeZone; - ghaf.givc.appvm = { - enable = true; - name = lib.mkForce "appflowy-vm"; - applications = [ - { - name = "appflowy"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/appflowy"; - } - ]; - }; - } - ]; - borderColor = "#4c3f7a"; -} diff --git a/modules/reference/appvms/business.nix b/modules/reference/appvms/business.nix index e4b6344ae..ec42cdc2c 100644 --- a/modules/reference/appvms/business.nix +++ b/modules/reference/appvms/business.nix @@ -7,200 +7,153 @@ lib, ... }: -let - inherit (lib) mkIf optionalString; - #TODO: Move this to a common place - name = "business"; - tiiVpnAddr = "151.253.154.18"; - vpnOnlyAddr = "${tiiVpnAddr},jira.tii.ae,access.tii.ae,confluence.tii.ae,i-service.tii.ae,catalyst.atrc.ae"; - netvmEntry = builtins.filter (x: x.name == "net-vm") config.ghaf.networking.hosts.entries; - netvmAddress = lib.head (builtins.map (x: x.ip) netvmEntry); - adminvmEntry = builtins.filter (x: x.name == "admin-vm") config.ghaf.networking.hosts.entries; - adminvmAddress = lib.head (builtins.map (x: x.ip) adminvmEntry); - # Remove rounded corners from the text editor window - gnomeTextEditor = pkgs.gnome-text-editor.overrideAttrs (oldAttrs: { - postPatch = - (oldAttrs.postPatch or "") - + '' - echo -e '\nwindow { border-radius: 0px; }' >> src/style.css - ''; - }); -in { - name = "${name}"; - packages = - [ - pkgs.google-chrome - pkgs.globalprotect-openconnect - pkgs.losslesscut-bin - pkgs.openconnect - gnomeTextEditor - pkgs.xarchiver - - ] - ++ lib.optionals config.ghaf.profiles.debug.enable [ pkgs.tcpdump ] - ++ lib.optionals config.ghaf.givc.enable [ pkgs.open-normal-extension ]; + name = "business"; + packages = lib.optionals config.ghaf.profiles.debug.enable [ pkgs.tcpdump ]; # TODO create a repository of mac addresses to avoid conflicts macAddress = "02:00:00:03:10:01"; ramMb = 6144; cores = 4; - extraModules = [ - ( - { pkgs, ... }: + borderColor = "#218838"; + ghafAudio.enable = true; + vtpm.enable = true; + applications = + let + inherit (config.microvm.vms."business-vm".config.config.ghaf.reference.services.pac) proxyPacUrl; + browserCommand = "google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland ${config.ghaf.givc.idsExtraArgs} --load-extension=${pkgs.open-normal-extension} --proxy-pac-url=${proxyPacUrl}"; + in + [ { - imports = [ - # ../programs/chromium.nix - ../programs/google-chrome.nix - ../services/globalprotect-vpn/default.nix + name = "Trusted Browser"; + description = "Isolated Trusted Browsing"; + packages = [ pkgs.google-chrome ]; + icon = "thorium-browser"; + command = browserCommand; + givcArgs = [ + "url" ]; - time.timeZone = config.time.timeZone; - - microvm = { - qemu.extraArgs = lib.optionals ( - config.ghaf.hardware.usb.internal.enable - && (lib.hasAttr "cam0" config.ghaf.hardware.usb.internal.qemuExtraArgs) - ) config.ghaf.hardware.usb.internal.qemuExtraArgs.cam0; - devices = [ ]; - }; - - ghaf = { - givc.appvm = { - enable = true; - name = lib.mkForce "business-vm"; - applications = [ - { - name = "google-chrome"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland ${config.ghaf.givc.idsExtraArgs} --load-extension=${pkgs.open-normal-extension}"; - args = [ "url" ]; - } - { - name = "outlook"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland --app=https://outlook.office.com/mail/ ${config.ghaf.givc.idsExtraArgs} --load-extension=${pkgs.open-normal-extension}"; - } - { - name = "office"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland --app=https://microsoft365.com ${config.ghaf.givc.idsExtraArgs} --load-extension=${pkgs.open-normal-extension}"; - } - { - name = "teams"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland --app=https://teams.microsoft.com ${config.ghaf.givc.idsExtraArgs} --load-extension=${pkgs.open-normal-extension}"; - } - { - name = "gpclient"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/gpclient -platform wayland"; - } - { - name = "gnome-text-editor"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/gnome-text-editor"; - } - { - name = "losslesscut"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/losslesscut --enable-features=UseOzonePlatform --ozone-platform=wayland"; - } - { - name = "xarchiver"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/xarchiver"; - } + extraModules = [ + { + imports = [ + #../programs/chromium.nix + ../programs/google-chrome.nix ]; - }; - reference = { - programs.google-chrome.enable = true; + ghaf.reference.programs.google-chrome.enable = true; + ghaf.reference.programs.google-chrome.openInNormalExtension = true; + ghaf.services.xdghandlers.enable = true; + ghaf.security.apparmor.enable = true; + } + ]; + } + { + name = "Microsoft Outlook"; + description = "Microsoft Email Client"; + icon = "ms-outlook"; + command = "${browserCommand} --app=https://outlook.office.com/mail/"; + } + { + name = "Microsoft 365"; + description = "Microsoft 365 Software Suite"; + icon = "microsoft-365"; + command = "${browserCommand} --app=https://microsoft365.com"; + } + { + name = "Teams"; + description = "Microsoft Teams Collaboration Application"; + icon = "teams-for-linux"; + command = "${browserCommand} --app=https://teams.microsoft.com"; + } + { + name = "VPN"; + description = "GlobalProtect VPN Client"; + packages = [ + pkgs.globalprotect-openconnect + pkgs.openconnect + ]; + icon = "yast-vpn"; + command = "gpclient -platform wayland"; + extraModules = [ + { + imports = [ + ../services/globalprotect-vpn/default.nix + ]; - services.globalprotect = { + ghaf.reference.services.globalprotect = { enable = true; csdWrapper = "${pkgs.openconnect}/libexec/openconnect/hipreport.sh"; }; - }; - - services.xdghandlers.enable = true; - }; - environment.etc."opt/chrome/native-messaging-hosts/fi.ssrc.open_normal.json" = - mkIf config.ghaf.givc.enable - { - source = "${pkgs.open-normal-extension}/fi.ssrc.open_normal.json"; - }; - - # environment.etc."chromium/native-messaging-hosts/fi.ssrc.open_normal.json" = - # mkIf config.ghaf.givc.enable - # { - # source = "${pkgs.open-normal-extension}/fi.ssrc.open_normal.json"; - # }; - environment.etc."open-normal-extension.cfg" = mkIf config.ghaf.givc.enable { - text = - let - cliArgs = builtins.replaceStrings [ "\n" ] [ " " ] '' - --name ${config.ghaf.givc.adminConfig.name} - --addr ${config.ghaf.givc.adminConfig.addr} - --port ${config.ghaf.givc.adminConfig.port} - ${optionalString config.ghaf.givc.enableTls "--cacert /run/givc/ca-cert.pem"} - ${optionalString config.ghaf.givc.enableTls "--cert /run/givc/business-vm-cert.pem"} - ${optionalString config.ghaf.givc.enableTls "--key /run/givc/business-vm-key.pem"} - ${optionalString (!config.ghaf.givc.enableTls) "--notls"} - ''; - in - '' - export GIVC_PATH="${pkgs.givc-cli}" - export GIVC_OPTS="${cliArgs}" - ''; - }; - - # Enable dconf and icon pack for gnome text editor - programs.dconf.enable = true; - environment.systemPackages = [ pkgs.adwaita-icon-theme ]; - - #Firewall Settings - networking = { - proxy = { - default = "http://${netvmAddress}:${toString config.ghaf.reference.services.proxy-server.bindPort}"; - noProxy = "192.168.101.10,${adminvmAddress},127.0.0.1,localhost,${vpnOnlyAddr}"; - }; - firewall = { - enable = true; - extraCommands = '' - - add_rule() { - local ip=$1 - iptables -I OUTPUT -p tcp -d $ip --dport 80 -j ACCEPT - iptables -I OUTPUT -p tcp -d $ip --dport 443 -j ACCEPT - iptables -I INPUT -p tcp -s $ip --sport 80 -j ACCEPT - iptables -I INPUT -p tcp -s $ip --sport 443 -j ACCEPT - } - # Default policy - iptables -P INPUT DROP - - # Block any other unwanted traffic (optional) - iptables -N logreject - iptables -A logreject -j LOG - iptables -A logreject -j REJECT - - # allow everything for local VPN traffic - iptables -A INPUT -i tun0 -j ACCEPT - iptables -A FORWARD -i tun0 -j ACCEPT - iptables -A FORWARD -o tun0 -j ACCEPT - iptables -A OUTPUT -o tun0 -j ACCEPT - - # WARN: if all the traffic including VPN flowing through proxy is intended, - # remove "add_rule 151.253.154.18" rule and pass "--proxy-server=http://192.168.100.1:3128" to openconnect(VPN) app. - # also remove "151.253.154.18,tii.ae,.tii.ae,sapsf.com,.sapsf.com" addresses from noProxy option and add - # them to allow acl list in modules/reference/appvms/3proxy-config.nix file. - # Allow VPN access.tii.ae - add_rule ${tiiVpnAddr} - - # Block all other HTTP and HTTPS traffic - iptables -A OUTPUT -p tcp --dport 80 -j logreject - iptables -A OUTPUT -p tcp --dport 443 -j logreject - iptables -A OUTPUT -p udp --dport 80 -j logreject - iptables -A OUTPUT -p udp --dport 443 -j logreject - - ''; - }; - }; + } + ]; } - ) + { + name = "Text Editor"; + description = "Simple Text Editor"; + packages = + let + # Remove rounded corners from the text editor window + gnomeTextEditor = pkgs.gnome-text-editor.overrideAttrs (oldAttrs: { + postPatch = + (oldAttrs.postPatch or "") + + '' + echo -e '\nwindow { border-radius: 0px; }' >> src/style.css + ''; + }); + in + [ + gnomeTextEditor + pkgs.adwaita-icon-theme + ]; + icon = "org.gnome.TextEditor"; + command = "gnome-text-editor"; + extraModules = [ + { + # Enable dconf for gnome text editor + programs.dconf.enable = true; + } + ]; + } + { + name = "Xarchiver"; + description = "File Compressor"; + packages = [ pkgs.xarchiver ]; + icon = "xarchiver"; + command = "xarchiver"; + } + { + name = "Video Editor"; + description = "Losslesscut Video Editor"; + packages = [ pkgs.losslesscut-bin ]; + icon = "${pkgs.losslesscut-bin}/share/icons/losslesscut.png"; + command = "losslesscut --enable-features=UseOzonePlatform --ozone-platform=wayland"; + } + ]; + extraModules = [ + + { + # Attach integrated camera to this vm + microvm = { + qemu.extraArgs = lib.optionals ( + config.ghaf.hardware.usb.internal.enable + && (lib.hasAttr "cam0" config.ghaf.hardware.usb.internal.qemuExtraArgs) + ) config.ghaf.hardware.usb.internal.qemuExtraArgs.cam0; + devices = [ ]; + }; + + imports = [ + ../services/pac/pac.nix + ../services/firewall/firewall.nix + ]; + + # Enable Proxy Auto-Configuration service for the browser + ghaf.reference.services.pac.enable = true; + ghaf.reference.services.pac.proxyAddress = + config.ghaf.reference.services.proxy-server.internalAddress; + ghaf.reference.services.pac.proxyPort = config.ghaf.reference.services.proxy-server.bindPort; + + # Enable firewall and allow access to TII VPN + ghaf.reference.services.firewall.enable = true; + } ]; - borderColor = "#218838"; - ghafAudio.enable = true; - vtpm.enable = true; } diff --git a/modules/reference/appvms/chromium.nix b/modules/reference/appvms/chromium.nix index 166ed6a7d..5888b88de 100644 --- a/modules/reference/appvms/chromium.nix +++ b/modules/reference/appvms/chromium.nix @@ -7,50 +7,47 @@ config, ... }: -let - name = "chromium"; -in { - name = "${name}"; - packages = [ - pkgs.chromium - ] ++ lib.optional config.ghaf.development.debug.tools.enable pkgs.alsa-utils; + name = "chromium"; + packages = lib.optional config.ghaf.development.debug.tools.enable pkgs.alsa-utils; # TODO create a repository of mac addresses to avoid conflicts macAddress = "02:00:00:03:05:01"; ramMb = 6144; cores = 4; + borderColor = "#B83232"; + ghafAudio.enable = true; + vtpm.enable = true; + applications = [ + { + # The SPKI fingerprint is calculated like this: + # $ openssl x509 -noout -in mitmproxy-ca-cert.pem -pubkey | openssl asn1parse -noout -inform pem -out public.key + # $ openssl dgst -sha256 -binary public.key | openssl enc -base64 + name = "Chromium"; + description = "Isolated General Browsing"; + packages = [ pkgs.chromium ]; + icon = "chromium"; + command = "chromium --enable-features=UseOzonePlatform --ozone-platform=wayland ${config.ghaf.givc.idsExtraArgs}"; + givcArgs = [ + "url" + "flag" + ]; + extraModules = [ + { + imports = [ ../programs/chromium.nix ]; + ghaf.reference.programs.chromium.enable = true; + ghaf.services.xdghandlers.enable = true; + } + ]; + } + ]; extraModules = [ { - imports = [ ../programs/chromium.nix ]; - - time.timeZone = config.time.timeZone; - # Disable camera for now, because, due to the bug, the camera is not accessable in BusinessVM # microvm.qemu.extraArgs = optionals ( # config.ghaf.hardware.usb.internal.enable # && (hasAttr "cam0" config.ghaf.hardware.usb.internal.qemuExtraArgs) # ) config.ghaf.hardware.usb.internal.qemuExtraArgs.cam0; microvm.devices = [ ]; - - ghaf.givc.appvm = { - enable = true; - name = lib.mkForce "chromium-vm"; - applications = [ - { - name = "chromium"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/chromium --enable-features=UseOzonePlatform --ozone-platform=wayland ${config.ghaf.givc.idsExtraArgs}"; - args = [ - "url" - "flag" - ]; - } - ]; - }; - ghaf.reference.programs.chromium.enable = true; - ghaf.services.xdghandlers.enable = true; } ]; - borderColor = "#B83232"; - ghafAudio.enable = true; - vtpm.enable = true; } diff --git a/modules/reference/appvms/comms.nix b/modules/reference/appvms/comms.nix index 54c6f8dd8..3a210ce3a 100644 --- a/modules/reference/appvms/comms.nix +++ b/modules/reference/appvms/comms.nix @@ -8,71 +8,65 @@ ... }: let - name = "comms"; inherit (lib) hasAttr optionals; - dendrite-pinecone = pkgs.callPackage ../../../packages/dendrite-pinecone { }; - isDendritePineconeEnabled = - if (hasAttr "services" config.ghaf.reference) then - config.ghaf.reference.services.dendrite - else - false; in { - name = "${name}"; - + name = "comms"; packages = [ pkgs.google-chrome - pkgs.element-desktop - pkgs.element-gps pkgs.gpsd - pkgs.tcpdump - ] ++ pkgs.lib.optionals isDendritePineconeEnabled [ dendrite-pinecone ]; + ] ++ lib.optionals config.ghaf.profiles.debug.enable [ pkgs.tcpdump ]; macAddress = "02:00:00:03:09:01"; ramMb = 4096; cores = 4; + borderColor = "#337aff"; + ghafAudio.enable = true; + applications = [ + { + name = "Element"; + description = "General Messaging Application"; + packages = [ pkgs.element-desktop ]; + icon = "element-desktop"; + command = "element-desktop --enable-features=UseOzonePlatform --ozone-platform=wayland"; + extraModules = [ + { + imports = [ + ../programs/element-desktop.nix + ]; + ghaf.reference.programs.element-desktop.enable = true; + } + ]; + } + { + name = "Slack"; + description = "Teams Collaboration & Messaging Application"; + icon = "slack"; + command = "google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland --app=https://app.slack.com/client ${config.ghaf.givc.idsExtraArgs}"; + } + { + name = "Zoom"; + description = "Zoom Videoconferencing Application"; + icon = "Zoom"; + command = "google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland --app=https://app.zoom.us/wc/home ${config.ghaf.givc.idsExtraArgs}"; + } + ]; extraModules = [ { imports = [ # ../programs/chromium.nix ../programs/google-chrome.nix - ]; - systemd = { - services = { - element-gps = { - description = "Element-gps is a GPS location provider for Element websocket interface."; - enable = true; - serviceConfig = { - Type = "simple"; - ExecStart = "${pkgs.element-gps}/bin/main.py"; - Restart = "on-failure"; - RestartSec = "2"; - }; - wantedBy = [ "multi-user.target" ]; - }; - - "dendrite-pinecone" = pkgs.lib.mkIf isDendritePineconeEnabled { - description = "Dendrite is a second-generation Matrix homeserver with Pinecone which is a next-generation P2P overlay network"; - enable = true; - serviceConfig = { - Type = "simple"; - ExecStart = "${dendrite-pinecone}/bin/dendrite-demo-pinecone"; - Restart = "on-failure"; - RestartSec = "2"; - }; - wantedBy = [ "multi-user.target" ]; - }; - }; - }; - - networking = pkgs.lib.mkIf isDendritePineconeEnabled { - firewall.allowedTCPPorts = [ dendrite-pinecone.TcpPortInt ]; - firewall.allowedUDPPorts = [ dendrite-pinecone.McastUdpPortInt ]; - }; + ghaf.reference.programs.google-chrome.enable = true; + ghaf.services.xdghandlers.enable = true; - time.timeZone = config.time.timeZone; + # Attach GPS receiver to this VM + microvm.qemu.extraArgs = optionals ( + config.ghaf.hardware.usb.external.enable + && (hasAttr "gps0" config.ghaf.hardware.usb.external.qemuExtraArgs) + ) config.ghaf.hardware.usb.external.qemuExtraArgs.gps0; + # GPSD collects data from GPS and makes it available on TCP port 2947 services.gpsd = { enable = true; devices = [ "/dev/ttyUSB0" ]; @@ -81,34 +75,6 @@ in listenany = true; extraArgs = [ "-n" ]; # Do not wait for a client to connect before polling }; - - microvm.qemu.extraArgs = optionals ( - config.ghaf.hardware.usb.external.enable - && (hasAttr "gps0" config.ghaf.hardware.usb.external.qemuExtraArgs) - ) config.ghaf.hardware.usb.external.qemuExtraArgs.gps0; - - ghaf.givc.appvm = { - enable = true; - name = lib.mkForce "${name}-vm"; - applications = [ - { - name = "element"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/element-desktop --enable-features=UseOzonePlatform --ozone-platform=wayland"; - } - { - name = "slack"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland --app=https://app.slack.com/client ${config.ghaf.givc.idsExtraArgs}"; - } - { - name = "zoom"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland --app=https://app.zoom.us/wc/home ${config.ghaf.givc.idsExtraArgs}"; - } - ]; - }; - ghaf.reference.programs.google-chrome.enable = true; - ghaf.services.xdghandlers.enable = true; } ]; - borderColor = "#337aff"; - ghafAudio.enable = true; } diff --git a/modules/reference/appvms/gala.nix b/modules/reference/appvms/gala.nix index 6ae25a08a..657753ffb 100644 --- a/modules/reference/appvms/gala.nix +++ b/modules/reference/appvms/gala.nix @@ -2,31 +2,22 @@ # SPDX-License-Identifier: Apache-2.0 # { - lib, pkgs, - config, ... }: { name = "gala"; - packages = [ pkgs.gala-app ]; macAddress = "02:00:00:03:06:01"; ramMb = 1536; cores = 2; - extraModules = [ + borderColor = "#027d7b"; + applications = [ { - time.timeZone = config.time.timeZone; - ghaf.givc.appvm = { - enable = true; - name = lib.mkForce "gala-vm"; - applications = [ - { - name = "gala"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/gala --enable-features=UseOzonePlatform --ozone-platform=wayland"; - } - ]; - }; + name = "GALA"; + description = "Secure Android-in-the-Cloud"; + packages = [ pkgs.gala-app ]; + icon = "distributor-logo-android"; + command = "gala --enable-features=UseOzonePlatform --ozone-platform=wayland"; } ]; - borderColor = "#027d7b"; } diff --git a/modules/reference/appvms/google-chrome.nix b/modules/reference/appvms/google-chrome.nix index 4d6abaf4b..0b06cba8d 100644 --- a/modules/reference/appvms/google-chrome.nix +++ b/modules/reference/appvms/google-chrome.nix @@ -7,50 +7,48 @@ config, ... }: -let - name = "chrome"; -in { - name = "${name}"; - packages = [ - pkgs.google-chrome - ] ++ lib.optional config.ghaf.development.debug.tools.enable pkgs.alsa-utils; + name = "chrome"; + packages = lib.optional config.ghaf.development.debug.tools.enable pkgs.alsa-utils; # TODO create a repository of mac addresses to avoid conflicts macAddress = "02:00:00:03:11:01"; ramMb = 6144; cores = 4; + borderColor = "#630505"; + ghafAudio.enable = true; + vtpm.enable = true; + applications = [ + { + # The SPKI fingerprint is calculated like this: + # $ openssl x509 -noout -in mitmproxy-ca-cert.pem -pubkey | openssl asn1parse -noout -inform pem -out public.key + # $ openssl dgst -sha256 -binary public.key | openssl enc -base64 + name = "Google Chrome"; + description = "Isolated General Browsing"; + packages = [ pkgs.google-chrome ]; + icon = "google-chrome"; + command = "google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland ${config.ghaf.givc.idsExtraArgs}"; + givcArgs = [ + "url" + "flag" + ]; + extraModules = [ + { + imports = [ ../programs/google-chrome.nix ]; + ghaf.reference.programs.google-chrome.enable = true; + ghaf.services.xdghandlers.enable = true; + ghaf.security.apparmor.enable = true; + } + ]; + } + ]; extraModules = [ { - imports = [ ../programs/google-chrome.nix ]; - - time.timeZone = config.time.timeZone; - # Disable camera for now, because, due to the bug, the camera is not accessable in BusinessVM # microvm.qemu.extraArgs = optionals ( # config.ghaf.hardware.usb.internal.enable # && (hasAttr "cam0" config.ghaf.hardware.usb.internal.qemuExtraArgs) # ) config.ghaf.hardware.usb.internal.qemuExtraArgs.cam0; microvm.devices = [ ]; - - ghaf.givc.appvm = { - enable = true; - name = lib.mkForce "chrome-vm"; - applications = [ - { - name = "google-chrome"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/google-chrome-stable --enable-features=UseOzonePlatform --ozone-platform=wayland ${config.ghaf.givc.idsExtraArgs}"; - args = [ - "url" - "flag" - ]; - } - ]; - }; - ghaf.reference.programs.google-chrome.enable = true; - ghaf.services.xdghandlers.enable = true; } ]; - borderColor = "#630505"; - ghafAudio.enable = true; - vtpm.enable = true; } diff --git a/modules/reference/appvms/zathura.nix b/modules/reference/appvms/zathura.nix index a62e7ecd0..cf6c58d97 100644 --- a/modules/reference/appvms/zathura.nix +++ b/modules/reference/appvms/zathura.nix @@ -4,40 +4,37 @@ { lib, pkgs, - config, ... }: { name = "zathura"; packages = [ - pkgs.zathura + # Image viewer pkgs.pqiv ]; macAddress = "02:00:00:03:07:01"; ramMb = 512; cores = 1; + borderColor = "#122263"; + applications = [ + { + name = "PDF Viewer"; + description = "Isolated PDF Viewer"; + packages = [ pkgs.zathura ]; + icon = "document-viewer"; + command = "zathura"; + extraModules = [ + { + imports = [ ../programs/zathura.nix ]; + ghaf.reference.programs.zathura.enable = true; + } + ]; + } + ]; extraModules = [ { - imports = [ ../programs/zathura.nix ]; - time.timeZone = config.time.timeZone; - ghaf = { - reference.programs.zathura.enable = true; - - givc.appvm = { - enable = true; - name = lib.mkForce "zathura-vm"; - applications = [ - { - name = "zathura"; - command = "${config.ghaf.givc.appPrefix}/run-waypipe ${config.ghaf.givc.appPrefix}/zathura"; - } - ]; - }; - - #this vm should be stateless so nothing stored between boots. - storagevm.enable = lib.mkForce false; - }; + # This vm should be stateless so nothing stored between boots + ghaf.storagevm.enable = lib.mkForce false; } ]; - borderColor = "#122263"; } diff --git a/modules/reference/desktop/applications.nix b/modules/reference/desktop/applications.nix new file mode 100644 index 000000000..9b8b4fc42 --- /dev/null +++ b/modules/reference/desktop/applications.nix @@ -0,0 +1,83 @@ +# Copyright 2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.ghaf.reference.desktop.applications; + inherit (config.ghaf.services.audio) pulseaudioTcpControlPort; +in +{ + options.ghaf.reference.desktop.applications = { + enable = lib.mkEnableOption "desktop applications"; + }; + config = lib.mkIf cfg.enable { + ghaf.virtualization.microvm.guivm.applications = + [ + { + name = "Calculator"; + description = "Solve Math Problems"; + icon = "${pkgs.gnome-calculator}/share/icons/hicolor/scalable/apps/org.gnome.Calculator.svg"; + command = "${pkgs.gnome-calculator}/bin/gnome-calculator"; + } + + { + name = "Sticky Notes"; + description = "Sticky Notes on your Desktop"; + icon = "${pkgs.sticky-notes}/share/icons/hicolor/scalable/apps/com.vixalien.sticky.svg"; + command = "${pkgs.sticky-notes}/bin/com.vixalien.sticky"; + } + + { + name = "File Manager"; + description = "Organize & Manage Files"; + icon = "system-file-manager"; + command = "${pkgs.pcmanfm}/bin/pcmanfm"; + } + + { + name = "Bluetooth Settings"; + description = "Manage Bluetooth Devices & Settings"; + icon = "bluetooth-48"; + command = "${pkgs.bt-launcher}/bin/bt-launcher"; + } + + { + name = "Audio Control"; + description = "System Audio Control"; + icon = "preferences-sound"; + command = "${pkgs.ghaf-audio-control}/bin/GhafAudioControlStandalone --pulseaudio_server=audio-vm:${toString pulseaudioTcpControlPort} --indicator_icon_name=preferences-sound"; + } + + { + name = "Falcon AI"; + description = "Your local large language model, developed by TII"; + icon = "${pkgs.ghaf-artwork}/icons/falcon-icon.svg"; + command = "${pkgs.alpaca}/bin/alpaca"; + } + + { + name = "Control panel"; + description = "Control panel"; + icon = "utilities-tweak-tool"; + command = "${pkgs.ctrl-panel}/bin/ctrl-panel"; + } + ] + ++ lib.optionals config.ghaf.reference.programs.windows-launcher.enable ( + let + winConfig = config.ghaf.reference.programs.windows-launcher; + in + [ + { + name = "Windows"; + description = "Virtualized Windows System"; + icon = "distributor-logo-windows"; + command = "${pkgs.virt-viewer}/bin/remote-viewer -f spice://${winConfig.spice-host}:${toString winConfig.spice-port}"; + } + ] + ); + }; +} diff --git a/modules/reference/desktop/default.nix b/modules/reference/desktop/default.nix new file mode 100644 index 000000000..06c42ad34 --- /dev/null +++ b/modules/reference/desktop/default.nix @@ -0,0 +1,7 @@ +# Copyright 2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + imports = [ + ./applications.nix + ]; +} diff --git a/modules/reference/hardware/lenovo-x1/definitions/x1-gen10.nix b/modules/reference/hardware/lenovo-x1/definitions/x1-gen10.nix index c14217c53..06bed134b 100644 --- a/modules/reference/hardware/lenovo-x1/definitions/x1-gen10.nix +++ b/modules/reference/hardware/lenovo-x1/definitions/x1-gen10.nix @@ -89,6 +89,13 @@ # 00:1f.x in the Lenovo X1 Carbon 10 gen # must be defined for passthrough to AudioVM audio = { + # Force a PCI device reset to the device to get pci device to the default state at shutdown + removePciDevice = "0000:00:1f.3"; + # Force a PCI device rescan after resetting a device to refind the device on host + rescanPciDevice = "0000:00:1f.0"; + # Add acpi table to audioVM to enable microphone array profile + acpiPath = "/sys/firmware/acpi/tables/NHLT"; + pciDevices = [ { # ISA bridge: Intel Corporation Alder Lake PCH eSPI Controller(rev 01) diff --git a/modules/reference/hardware/lenovo-x1/definitions/x1-gen11.nix b/modules/reference/hardware/lenovo-x1/definitions/x1-gen11.nix index a241c92ee..1ffcfe561 100644 --- a/modules/reference/hardware/lenovo-x1/definitions/x1-gen11.nix +++ b/modules/reference/hardware/lenovo-x1/definitions/x1-gen11.nix @@ -99,6 +99,15 @@ # 00:1f.x in the example from Lenovo X1 Carbon # must be defined for passthrough to AudioVM audio = { + # Force a PCI device reset to the audio device + # This is to get the pci hardware device to the default state at shutdown + removePciDevice = "0000:00:1f.3"; + # Force a PCI device rescan after resetting a device + # to refind the device on host after reset + rescanPciDevice = "0000:00:1f.0"; + # Add acpi table file to audioVM to enable Lenovo X1 microphone array profile + acpiPath = "/sys/firmware/acpi/tables/NHLT"; + pciDevices = [ { # ISA bridge: Intel Corporation Raptor Lake LPC/eSPI Controller (rev 01) diff --git a/modules/reference/personalize/authorizedSshKeys.nix b/modules/reference/personalize/authorizedSshKeys.nix index 666ba2128..7c05fb61e 100644 --- a/modules/reference/personalize/authorizedSshKeys.nix +++ b/modules/reference/personalize/authorizedSshKeys.nix @@ -25,6 +25,7 @@ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwsW+YJw6ukhoWPEBLN93EFiGhN7H2VJn5yZcKId56W mb@mmm" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKbBp2dH2X3dcU1zh+xW3ZsdYROKpJd3n13ssOP092qE joerg@turingmachine" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIstCgKDX1vVWI8MgdVwsEMhju6DQJubi3V0ziLcU/2h vunny.sodhi@unikie.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINfyjcPGIRHEtXZgoF7wImA5gEY6ytIfkBeipz4lwnj6 Ganga.Ram@tii.ae" # For ghaf-installer automated testing: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAolaKCuIUBQSBFGFZI1taNX+JTAr8edqUts7A6k2Kv7" diff --git a/modules/reference/profiles/laptop-x86.nix b/modules/reference/profiles/laptop-x86.nix index 93b7c6d85..0d97c423d 100644 --- a/modules/reference/profiles/laptop-x86.nix +++ b/modules/reference/profiles/laptop-x86.nix @@ -18,6 +18,7 @@ in ../../hardware/common ../../hardware/definition.nix ../../lanzaboote + ../desktop ]; options.ghaf.reference.profiles.laptop-x86 = { @@ -134,6 +135,8 @@ in port = 9999; }; }; + + reference.desktop.applications.enable = true; }; }; } diff --git a/modules/reference/programs/chromium.nix b/modules/reference/programs/chromium.nix index a3e3c7aab..ebb9e83b7 100644 --- a/modules/reference/programs/chromium.nix +++ b/modules/reference/programs/chromium.nix @@ -1,13 +1,18 @@ # Copyright 2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.ghaf.reference.programs.chromium; in { options.ghaf.reference.programs.chromium = { enable = lib.mkEnableOption "Enable Chromium program settings"; - useZathuraVM = lib.mkEnableOption "Open PDFs in Zathura VM"; + openInNormalExtension = lib.mkEnableOption "browser extension to open links in the normal browser"; }; config = lib.mkIf cfg.enable { programs.chromium = { @@ -21,6 +26,32 @@ in # Don't use pdf.js, open externally. extraOpts."AlwaysOpenPdfExternally" = true; + + }; + + environment.etc = lib.mkIf (cfg.openInNormalExtension && config.ghaf.givc.enable) { + "chromium/native-messaging-hosts/fi.ssrc.open_normal.json" = { + source = "${pkgs.open-normal-extension}/fi.ssrc.open_normal.json"; + }; + + "open-normal-extension.cfg" = { + text = + let + cliArgs = builtins.replaceStrings [ "\n" ] [ " " ] '' + --name ${config.ghaf.givc.adminConfig.name} + --addr ${config.ghaf.givc.adminConfig.addr} + --port ${config.ghaf.givc.adminConfig.port} + ${lib.optionalString config.ghaf.givc.enableTls "--cacert /run/givc/ca-cert.pem"} + ${lib.optionalString config.ghaf.givc.enableTls "--cert /run/givc/business-vm-cert.pem"} + ${lib.optionalString config.ghaf.givc.enableTls "--key /run/givc/business-vm-key.pem"} + ${lib.optionalString (!config.ghaf.givc.enableTls) "--notls"} + ''; + in + '' + export GIVC_PATH="${pkgs.givc-cli}" + export GIVC_OPTS="${cliArgs}" + ''; + }; }; }; } diff --git a/modules/reference/programs/default.nix b/modules/reference/programs/default.nix index 0fbb37c51..aba3ce6e8 100644 --- a/modules/reference/programs/default.nix +++ b/modules/reference/programs/default.nix @@ -3,8 +3,9 @@ { imports = [ ./zathura.nix - # ./chromium.nix + ./chromium.nix ./google-chrome.nix ./windows-launcher.nix + ./element-desktop.nix ]; } diff --git a/modules/reference/programs/element-desktop.nix b/modules/reference/programs/element-desktop.nix new file mode 100644 index 000000000..8848e8797 --- /dev/null +++ b/modules/reference/programs/element-desktop.nix @@ -0,0 +1,60 @@ +# Copyright 2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.ghaf.reference.programs.element-desktop; + dendrite-pinecone = pkgs.callPackage ../../../packages/dendrite-pinecone { }; + isDendritePineconeEnabled = + if (lib.hasAttr "services" config.ghaf.reference) then + config.ghaf.reference.services.dendrite + else + false; + +in +{ + options.ghaf.reference.programs.element-desktop = { + enable = lib.mkEnableOption "element-desktop program settings"; + }; + config = lib.mkIf cfg.enable { + + systemd.services = { + + # The element-gps listens for WebSocket connections on localhost port 8000 from element-desktop + # When a new connection is received, it executes the gpspipe program to get GPS data from GPSD and forwards it over the WebSocket + element-gps = { + description = "Element-gps is a GPS location provider for Element websocket interface."; + enable = true; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.element-gps}/bin/main.py"; + Restart = "on-failure"; + RestartSec = "2"; + }; + wantedBy = [ "multi-user.target" ]; + }; + + "dendrite-pinecone" = pkgs.lib.mkIf isDendritePineconeEnabled { + description = "Dendrite is a second-generation Matrix homeserver with Pinecone which is a next-generation P2P overlay network"; + enable = true; + serviceConfig = { + Type = "simple"; + ExecStart = "${dendrite-pinecone}/bin/dendrite-demo-pinecone"; + Restart = "on-failure"; + RestartSec = "2"; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; + + networking = pkgs.lib.mkIf isDendritePineconeEnabled { + firewall.allowedTCPPorts = [ dendrite-pinecone.TcpPortInt ]; + firewall.allowedUDPPorts = [ dendrite-pinecone.McastUdpPortInt ]; + }; + + }; +} diff --git a/modules/reference/programs/google-chrome.nix b/modules/reference/programs/google-chrome.nix index 9ef6ded39..543f33a29 100644 --- a/modules/reference/programs/google-chrome.nix +++ b/modules/reference/programs/google-chrome.nix @@ -1,17 +1,22 @@ # Copyright 2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.ghaf.reference.programs.google-chrome; in { options.ghaf.reference.programs.google-chrome = { enable = lib.mkEnableOption "Enable Google chrome program settings"; - useZathuraVM = lib.mkEnableOption "Open PDFs in Zathura VM"; + openInNormalExtension = lib.mkEnableOption "browser extension to open links in the normal browser"; defaultPolicy = lib.mkOption { type = lib.types.attrs; description = '' - Google chrome policy options. A list of available policies + Google chrome policy options. A list of available policies can be found in the Chrome Enterprise documentation: Make sure the selected policy is supported on Linux and your browser version. @@ -20,8 +25,6 @@ in PromptForDownloadLocation = true; AlwaysOpenPdfExternally = true; DefaultBrowserSettingEnabled = true; - StartupBrowserWindowLaunchSuppressed = true; - DeviceMetricsReportingEnabled = false; MetricsReportingEnabled = false; }; example = lib.literalExpression '' @@ -30,6 +33,7 @@ in } ''; }; + extraOpts = lib.mkOption { type = lib.types.attrs; description = '' @@ -38,9 +42,9 @@ in Make sure the selected policy is supported on Linux and your browser version. ''; - default = { - - }; + default = + { + }; example = lib.literalExpression '' { "BrowserSignin" = 0; @@ -54,17 +58,60 @@ in } ''; }; + + policyOwner = lib.mkOption { + type = lib.types.str; + default = "root"; + description = "Policy files owner"; + }; + + policyOwnerGroup = lib.mkOption { + type = lib.types.str; + default = "root"; + description = "Policy files group"; + }; }; config = lib.mkIf cfg.enable { - environment.etc = { - "opt/chrome/policies/managed/default.json" = lib.mkIf (cfg.defaultPolicy != { }) { - text = builtins.toJSON cfg.defaultPolicy; - }; - "opt/chrome/policies/managed/extra.json" = lib.mkIf (cfg.extraOpts != { }) { - text = builtins.toJSON cfg.extraOpts; - }; + environment.etc = lib.mkMerge [ + { + "opt/chrome/policies/managed/default.json" = { + text = builtins.toJSON cfg.defaultPolicy; + user = "${cfg.policyOwner}"; # Owner is proxy-user + group = "${cfg.policyOwnerGroup}"; # Group is proxy-admin + mode = "0664"; # Permissions: read/write for owner/group, no permissions for others + }; + "opt/chrome/policies/managed/extra.json" = { + text = builtins.toJSON cfg.extraOpts; + user = "${cfg.policyOwner}"; # Owner is proxy-user + group = "${cfg.policyOwnerGroup}"; # Group is proxy-admin + mode = "0664"; # Permissions: read/write for owner/group, no permissions for others + }; + } + (lib.mkIf (cfg.openInNormalExtension && config.ghaf.givc.enable) { + "opt/chrome/native-messaging-hosts/fi.ssrc.open_normal.json" = { + source = "${pkgs.open-normal-extension}/fi.ssrc.open_normal.json"; + }; - }; + "open-normal-extension.cfg" = { + text = + let + cliArgs = builtins.replaceStrings [ "\n" ] [ " " ] '' + --name ${config.ghaf.givc.adminConfig.name} + --addr ${config.ghaf.givc.adminConfig.addr} + --port ${config.ghaf.givc.adminConfig.port} + ${lib.optionalString config.ghaf.givc.enableTls "--cacert /run/givc/ca-cert.pem"} + ${lib.optionalString config.ghaf.givc.enableTls "--cert /run/givc/business-vm-cert.pem"} + ${lib.optionalString config.ghaf.givc.enableTls "--key /run/givc/business-vm-key.pem"} + ${lib.optionalString (!config.ghaf.givc.enableTls) "--notls"} + ''; + in + '' + export GIVC_PATH="${pkgs.givc-cli}" + export GIVC_OPTS="${cliArgs}" + ''; + }; + }) + ]; }; } diff --git a/modules/reference/services/firewall/firewall.nix b/modules/reference/services/firewall/firewall.nix new file mode 100644 index 000000000..6b9aaa4ae --- /dev/null +++ b/modules/reference/services/firewall/firewall.nix @@ -0,0 +1,71 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + lib, + config, + ... +}: +let + cfg = config.ghaf.reference.services.firewall; +in +{ + options.ghaf.reference.services.firewall = { + enable = lib.mkEnableOption "Ghaf reference firewall for virtual machines"; + + # WARN: if all the traffic including VPN flowing through proxy is intended, + # remove "151.253.154.18" rule and pass "--proxy-server=http://192.168.100.1:3128" to openconnect(VPN) app. + # also remove "151.253.154.18,tii.ae,.tii.ae,sapsf.com,.sapsf.com" addresses from noProxy option and add + # them to allow acl list in modules/reference/appvms/3proxy-config.nix file. + allowedIPs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "151.253.154.18" ]; + description = "List of IP addresses allowed through the firewall"; + }; + }; + + config = lib.mkIf cfg.enable { + networking = { + firewall = { + enable = true; + extraCommands = + let + allowRules = lib.concatStringsSep "\n" ( + map (ip: '' + iptables -I OUTPUT -p tcp -d ${ip} --dport 80 -j ACCEPT + iptables -I OUTPUT -p tcp -d ${ip} --dport 443 -j ACCEPT + iptables -I INPUT -p tcp -s ${ip} --sport 80 -j ACCEPT + iptables -I INPUT -p tcp -s ${ip} --sport 443 -j ACCEPT + '') cfg.allowedIPs + ); + in + '' + # Default policy + iptables -P INPUT DROP + + iptables -A INPUT -i lo -j ACCEPT + iptables -A OUTPUT -o lo -j ACCEPT + + # Block any other unwanted traffic (optional) + iptables -N logreject + iptables -A logreject -j LOG + iptables -A logreject -j REJECT + + # allow everything for local VPN traffic + iptables -A INPUT -i tun0 -j ACCEPT + iptables -A FORWARD -i tun0 -j ACCEPT + iptables -A FORWARD -o tun0 -j ACCEPT + iptables -A OUTPUT -o tun0 -j ACCEPT + + ${allowRules} + + # Block all other HTTP and HTTPS traffic + iptables -A OUTPUT -p tcp --dport 80 -j logreject + iptables -A OUTPUT -p tcp --dport 443 -j logreject + iptables -A OUTPUT -p udp --dport 80 -j logreject + iptables -A OUTPUT -p udp --dport 443 -j logreject + + ''; + }; + }; + }; +} diff --git a/modules/reference/services/pac/pac.nix b/modules/reference/services/pac/pac.nix new file mode 100644 index 000000000..722d4de38 --- /dev/null +++ b/modules/reference/services/pac/pac.nix @@ -0,0 +1,154 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + lib, + pkgs, + config, + ... +}: +let + cfg = config.ghaf.reference.services.pac; + proxyUserName = "proxy-user"; + proxyGroupName = "proxy-admin"; + pacFileName = "ghaf.pac"; + pacServerAddr = "127.0.0.1:8000"; + _ghafPacFileFetcher = + let + pacFileDownloadUrl = cfg.pacUrl; + proxyServerUrl = "http://${cfg.proxyAddress}:${toString cfg.proxyPort}"; + logTag = "ghaf-pac-fetcher"; + in + pkgs.writeShellApplication { + name = "ghafPacFileFetcher"; + runtimeInputs = [ + pkgs.coreutils # Provides 'mv', 'rm', etc. + pkgs.curl # For downloading PAC files + pkgs.inetutils # Provides 'logger' + ]; + text = '' + # Variables + TEMP_PAC_PATH=$(mktemp) + LOCAL_PAC_PATH="/etc/proxy/${pacFileName}" + # Logging function with timestamp + log() { + logger -t "${logTag}" "$1" + } + log "Starting the pac file fetch process..." + # Fetch the pac file using curl with a proxy + log "Fetching pac file from ${pacFileDownloadUrl} using proxy ${proxyServerUrl}..." + http_status=$(curl --proxy "${proxyServerUrl}" -s -o "$TEMP_PAC_PATH" -w "%{http_code}" "${pacFileDownloadUrl}") + log "HTTP status code: $http_status" + # Check if the fetch was successful + if [[ "$http_status" -ne 200 ]]; then + log "Error: Failed to download pac file from ${pacFileDownloadUrl}. HTTP status code: $http_status" + rm -f "$TEMP_PAC_PATH" # Clean up temporary file + exit 2 + fi + # Verify the downloaded file is not empty + if [[ ! -s "$TEMP_PAC_PATH" ]]; then + log "Error: The downloaded pac file is empty." + rm -f "$TEMP_PAC_PATH" # Clean up temporary file + exit 3 + fi + # Log the download success + log "Pac file downloaded successfully. Proceeding with update..." + # Copy the content from the temporary pac file to the target file + log "Copying the content from temporary file to the target pac file at $LOCAL_PAC_PATH..." + # Check if the copy was successful + if cat "$TEMP_PAC_PATH" > "$LOCAL_PAC_PATH"; then + log "Pac file successfully updated at $LOCAL_PAC_PATH." + else + log "Error: Failed to update the pac file at $LOCAL_PAC_PATH." + rm -f "$TEMP_PAC_PATH" # Clean up temporary file + exit 4 + fi + # Clean up temporary file + rm -f "$TEMP_PAC_PATH" + log "Pac file fetch and update process completed successfully." + exit 0 + ''; + }; +in +{ + options.ghaf.reference.services.pac = { + enable = lib.mkEnableOption "Proxy Auto-Configuration (PAC)"; + + proxyAddress = lib.mkOption { + type = lib.types.str; + description = "Proxy address"; + }; + + proxyPort = lib.mkOption { + type = lib.types.int; + description = "Proxy port"; + }; + + pacUrl = lib.mkOption { + type = lib.types.str; + description = "URL of the Proxy Auto-Configuration (PAC) file"; + default = "https://raw.githubusercontent.com/tiiuae/ghaf-rt-config/refs/heads/main/network/proxy/ghaf.pac"; + }; + + proxyPacUrl = lib.mkOption { + type = lib.types.str; + description = "Local PAC URL that can be passed to the browser"; + default = "http://${pacServerAddr}/${pacFileName}"; + readOnly = true; + }; + }; + + config = lib.mkIf cfg.enable { + # Define a new group for proxy management + users.groups.${proxyGroupName} = { }; # Create a group named proxy-admin + # Define a new user with a specific username + users.users.${proxyUserName} = { + isSystemUser = true; + description = "Proxy User for managing allowlist and services"; + # extraGroups = [ "${proxyGroupName}" ]; # Adding to 'proxy-admin' for specific access + group = "${proxyGroupName}"; + }; + environment.etc."proxy/${pacFileName}" = { + text = ''''; + user = "${proxyUserName}"; # Owner is proxy-user + group = "${proxyGroupName}"; # Group is proxy-admin + mode = "0664"; # Permissions: read/write for owner/group, no permissions for others + }; + systemd.services.pacServer = { + description = "Http server to make PAC file accessible for web browsers"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = "${pkgs.busybox}/bin/busybox httpd -f -p ${pacServerAddr} -h /etc/proxy"; + # Ensure ghafFetchUrl starts after the network is up + Type = "simple"; + # Restart policy on failure + Restart = "always"; # Restart the service if it fails + RestartSec = "15s"; # Wait 15 seconds before restarting + User = "${proxyUserName}"; + }; + }; + systemd.services.ghafPacFileFetcher = { + description = "Fetch ghaf pac file periodically with retries if internet is available"; + serviceConfig = { + ExecStart = "${_ghafPacFileFetcher}/bin/ghafPacFileFetcher"; + # Ensure ghafFetchUrl starts after the network is up + Type = "simple"; + # Restart policy on failure + Restart = "on-failure"; # Restart the service if it fails + RestartSec = "15s"; # Wait 15 seconds before restarting + User = "${proxyUserName}"; + }; + }; + systemd.timers.ghafPacFileFetcher = { + description = "Run ghafPacFileFetcher periodically"; + wantedBy = [ "timers.target" ]; + timerConfig = { + User = "${proxyUserName}"; + Persistent = true; # Ensures the timer runs after a system reboot + OnCalendar = "daily"; # Set to your desired schedule + OnBootSec = "90s"; + }; + }; + + }; +} diff --git a/modules/reference/services/proxy-server/3proxy-config.nix b/modules/reference/services/proxy-server/3proxy-config.nix index 5bae71611..ed768070c 100644 --- a/modules/reference/services/proxy-server/3proxy-config.nix +++ b/modules/reference/services/proxy-server/3proxy-config.nix @@ -9,14 +9,20 @@ let cfg = config.ghaf.reference.services.proxy-server; inherit (lib) mkEnableOption mkIf; - proxyUserName = "proxy-user"; proxyGroupName = "proxy-admin"; - proxyAllowListName = "allowlist.txt"; - proxyWritableAllowListPath = "/etc/${proxyAllowListName}"; - ms-url-fetcher = pkgs.callPackage ./ms_url_fetcher.nix { - allowListPath = proxyWritableAllowListPath; - }; + url-fetcher = pkgs.callPackage ./url_fetcher.nix { }; + + msUrls = "https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7"; + ghafUrls = "https://api.github.com/repos/tiiuae/ghaf-rt-config/contents/network/proxy/urls?ref=main"; + + msAllowFilePath = "3proxy/ms_whitelist.txt"; + ghafAllowFilePath = "3proxy/ghaf_whitelist.txt"; + + allowListPaths = [ + msAllowFilePath + ghafAllowFilePath + ]; _3proxy-restart = pkgs.writeShellApplication { name = "3proxy-restart"; @@ -40,90 +46,6 @@ let echo "3proxy service successfully started" ''; }; - tiiUrls = [ - #for jira avatars - "*.gravatar.com" - # for confluence icons - "*.atlassian.com" - "*tii.ae" - "*tii.org" - "tiiuae.sharepoint.com" - "tiiuae-my.sharepoint.com" - "hcm22.sapsf.com" - "aderp.addigital.gov.ae" - "s1.mn1.ariba.com" - "tii.sourcing.mn1.ariba.com" - "a1c7ohabl.accounts.ondemand.com" - "flpnwc-ojffapwnic.dispatcher.ae1.hana.ondemand.com" - "*.docusign.com" - "access.clarivate.com" - ]; - - ssrcUrls = [ - "*.cachix.org" - "vedenemo.dev" - "loki.ghaflogs.vedenemo.dev" - "ghaflogs.vedenemo.dev" - "himalia.vedenemo.dev" - ]; - - extraMsUrls = [ - #ms366 - "graph.microsoft.com" - "ocws.officeapps.live.com" - "microsoft365.com" - "*.azureedge.net" # microsoft365 icons - "consentreceiverfd-prod.azurefd.net" # ms365 cookies - "c.s-microsoft.com" - "js.monitor.azure.com" - "ocws.officeapps.live.com" - "northcentralus0-mediap.svc.ms" - "*.bing.com" - "cdnjs.cloudfare.com" - "store-images.s-microsoft.com" - "www.office.com" - "res-1.cdn.office.net" - "secure.skypeassets.com" - "js.live.net" - "skyapi.onedrive.live.com" - "am3pap006files.storage.live.com" - "c7rr5q.am.files.1drv.com" - #teams - "teams.live.com" - "*.teams.live.com" - "fpt.live.com" # teams related - "statics.teams.cdn.live.net" - "ipv6.login.live.com" - #outlook - "outlook.live.com" # outlook login - "csp.microsoft.com" - "arc.msn.com" - "www.msn.com" - "outlook.com" - #https://learn.microsoft.com/en-us/microsoft-365/enterprise/managing-office-365-endpoints?view=o365-worldwide#why-do-i-see-names-such-as-nsatcnet-or-akadnsnet-in-the-microsoft-domain-names - "*.akadns.net" - "*.akam.net" - "*.akamai.com" - "*.akamai.net" - "*.akamaiedge.net" - "*.akamaihd.net" - "*.akamaized.net" - "*.edgekey.net" - "*.edgesuite.net" - "*.nsatc.net" - "*.exacttarget.com" - #onedrive - "1drv.ms" - "onedrive.live.com" - "p.sfx.ms" - "my.microsoftpersonalcontent.com" - "*.onedrive.com" - "cdn.onenote.net" - "wvcyna.db.files.1drv.com" - "*.storage.live.com" - ]; - # Concatenate the lists and join with commas - concatenatedUrls = builtins.concatStringsSep "," (tiiUrls ++ ssrcUrls ++ extraMsUrls); config_file_content = '' # log to stdout @@ -136,14 +58,14 @@ let #private addresses deny * * 0.0.0.0/8,127.0.0.0/8,10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16,::,::1,fc00::/7 - allow * * ${concatenatedUrls} * #include dynamic whitelist ips - include "${proxyWritableAllowListPath}" + include "/etc/${msAllowFilePath}" + include "/etc/${ghafAllowFilePath}" deny * * * * maxconn 200 - proxy -i${netvmAddr} -p${toString cfg.bindPort} + proxy -i${cfg.internalAddress} -p${toString cfg.bindPort} flush @@ -155,6 +77,11 @@ in { options.ghaf.reference.services.proxy-server = { enable = mkEnableOption "Enable proxy server module"; + internalAddress = lib.mkOption { + type = lib.types.str; + default = netvmAddr; + description = "Internal address for proxy server"; + }; bindPort = lib.mkOption { type = lib.types.int; default = 3128; @@ -177,14 +104,23 @@ in group = "${proxyGroupName}"; }; - # Set up the permissions for allowlist.txt - environment.etc."${proxyAllowListName}" = { - text = ''''; - user = "${proxyUserName}"; # Owner is proxy-user - group = "${proxyGroupName}"; # Group is proxy-admin - mode = "0660"; # Permissions: read/write for owner/group, no permissions for others - }; - + # Apply the allowListConfig generated from the list + + # Create environment.etc configuration for each allow list path + # Loop over the allowListPaths and apply the configuration directly + environment.etc = builtins.foldl' ( + acc: path: + acc + // { + "${path}" = { + text = ''''; + user = "${proxyUserName}"; # Owner is proxy-user + group = "${proxyGroupName}"; # Group is proxy-admin + mode = "0660"; # Permissions: read/write for owner/group, no permissions for others + }; + } + ) { } allowListPaths; + # Apply the configurations for each allow list path # Allow proxy-admin group to manage specific systemd services without a password security = { polkit = { @@ -207,7 +143,7 @@ in }; - environment.systemPackages = [ ms-url-fetcher ]; + environment.systemPackages = [ url-fetcher ]; #Firewall Settings networking = { firewall.enable = true; @@ -218,12 +154,12 @@ in ''; }; # systemd service for fetching the file - systemd.services.fetchFile = { - description = "Fetch a file periodically with retries if internet is available"; + systemd.services.msFetchUrl = { + description = "Fetch microsoft URLs periodically with retries if internet is available"; serviceConfig = { - ExecStart = "${ms-url-fetcher}/bin/ms-url-fetch"; - # Ensure fetchFile starts after the network is up + ExecStart = "${url-fetcher}/bin/url-fetcher -u ${msUrls} -p /etc/${msAllowFilePath}"; + # Ensure msFetchUrl starts after the network is up Type = "simple"; # Retry until systemctl restart 3proxy succeeds ExecStartPost = "${_3proxy-restart}/bin/3proxy-restart"; @@ -235,8 +171,8 @@ in }; # systemd timer to trigger the service every 10 minutes - systemd.timers.fetchFile = { - description = "Run fetch-file periodically"; + systemd.timers.msFetchUrl = { + description = "Run msFetchUrl periodically"; wantedBy = [ "timers.target" ]; timerConfig = { User = "${proxyUserName}"; @@ -246,6 +182,35 @@ in }; }; + # systemd service for fetching the file + systemd.services.ghafFetchUrl = { + description = "Fetch ghaf related URLs periodically with retries if internet is available"; + + serviceConfig = { + ExecStart = "${url-fetcher}/bin/url-fetcher -f ${ghafUrls} -p /etc/${ghafAllowFilePath}"; + # Ensure ghafFetchUrl starts after the network is up + Type = "simple"; + # Retry until systemctl restart 3proxy succeeds + ExecStartPost = "${_3proxy-restart}/bin/3proxy-restart"; + # Restart policy on failure + Restart = "on-failure"; # Restart the service if it fails + RestartSec = "15s"; # Wait 15 seconds before restarting + User = "${proxyUserName}"; + }; + }; + + # systemd timer to trigger the service every 10 minutes + systemd.timers.ghafFetchUrl = { + description = "Run ghafFetchUrl periodically"; + wantedBy = [ "timers.target" ]; + timerConfig = { + User = "${proxyUserName}"; + Persistent = true; # Ensures the timer runs after a system reboot + OnCalendar = "hourly"; # Set to your desired schedule + OnBootSec = "90s"; + }; + }; + systemd.services."3proxy".serviceConfig = { RestartSec = "5s"; User = "${proxyUserName}"; @@ -258,37 +223,6 @@ in confFile = pkgs.writeText "3proxy.conf" '' ${config_file_content} ''; - - /* - NOTE allow and deny configurations should must be placed before the other configs - it is not possible to do with extraConfig. Because it appends the file - */ - /* - services = [ - { - type = "proxy"; - bindAddress = "${netvmAddr}"; - inherit (cfg) bindPort; - maxConnections = 200; - auth = [ "iponly" ]; - acl = [ - { - rule = "allow"; - targets = tiiUrls; - } - { - rule = "allow"; - targets = ssrcUrls; - } - { - rule = "allow"; - targets = extraMsUrls; - } - { rule = "deny"; } - ]; - } - ]; - */ }; }; diff --git a/modules/reference/services/proxy-server/ms_url_fetcher.nix b/modules/reference/services/proxy-server/ms_url_fetcher.nix deleted file mode 100644 index 57ea3721f..000000000 --- a/modules/reference/services/proxy-server/ms_url_fetcher.nix +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2024 TII (SSRC) and the Ghaf contributors -# SPDX-License-Identifier: Apache-2.0 -{ - writeShellApplication, - lib, - pkgs, - allowListPath, - ... -}: -let - url = "https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7"; - logTag = "ms-url-fetcher"; -in -writeShellApplication { - name = "ms-url-fetch"; - runtimeInputs = [ - pkgs.inetutils - pkgs.curl - pkgs.jq - ]; - text = '' - # Function to write to the allow list - write_to_allow_list() { - - local processedUrls="$1" - local allowListPath="$2" - - { - printf "allow * * " || { logger -t ms-url-fetcher "Failed to print prefix"; return 1; } - echo "$processedUrls" || { logger -t ms-url-fetcher "Failed to echo processed URLs"; return 1; } - } > "$allowListPath" || { logger -t ms-url-fetcher "Failed to write to $allowListPath"; return 2; } - return 0 # Indicate success - } - # Check if the device is connected to the internet. - if ping -c 1 8.8.8.8 &> /dev/null; then - logger -t ${logTag} "Fetching the Microsoft URLs from ${url}" - - # Fetch the JSON file using curl with retry logic - if curl_output=$(curl -s --retry 5 --retry-delay 10 --retry-connrefused "${url}"); then - msurl_output=$(echo "$curl_output" | jq -r '.[]? | select(.category == "Optimize" or .category == "Allow" or .category == "Default") | .urls[]?' | sort | uniq) - # Check if msurl_output is empty - if [ -z "$msurl_output" ]; then - logger -t ${logTag} "No valid URLs found in the fetched data." - exit 4 # No URLs found error - fi - - # Convert the list of URLs into a comma-separated format and save to allowListPath - processedUrls=$(echo "$msurl_output" | tr '\n' ',' | sed 's/,$//'); - - - - # Add the prefix once and save to allowListPath - if write_to_allow_list "$processedUrls" "${allowListPath}"; then - logger -t ${logTag} "Microsoft URLs fetched and saved to ${allowListPath} successfully" - exit 0 # Success exit code - else - logger -t ${logTag} "Failed to process Microsoft URLs with jq" - exit 2 # JQ processing error - fi - else - logger -t ${logTag} "Failed to fetch Microsoft URLs after multiple attempts" - exit 1 # Curl fetching error - fi - else - logger -t ${logTag} "No internet connection. Microsoft URLs not fetched." - exit 3 # No internet connection error - fi - ''; - meta = with lib; { - description = " - The application is a shell script designed to fetch a list of Microsoft URLs - from a specified endpoint and save them to an allow list file. The script includes error - handling and retry logic to ensure robustness in various network conditions. - "; - }; -} diff --git a/modules/reference/services/proxy-server/url_fetcher.nix b/modules/reference/services/proxy-server/url_fetcher.nix new file mode 100644 index 000000000..888915a8b --- /dev/null +++ b/modules/reference/services/proxy-server/url_fetcher.nix @@ -0,0 +1,144 @@ +# Copyright 2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + writeShellApplication, + lib, + pkgs, + ... +}: +let + logTag = "url-fetcher"; +in +writeShellApplication { + name = "url-fetcher"; + runtimeInputs = [ + pkgs.inetutils + pkgs.curl + pkgs.jq + pkgs.gawk + ]; + text = '' + # Default values for variables + url="" + url_folder="" + allowListPath="" + # Function to write to the allow list + write_to_allow_list() { + local processedUrls="$1" + local allowListPath="$2" + + { + # Ensure the "allow" prefix is written + printf "allow * * " || { logger -t ${logTag} "Failed to print prefix"; return 1; } + echo "$processedUrls" || { logger -t ${logTag} "Failed to echo processed URLs"; return 1; } + } > "$allowListPath" || { logger -t ${logTag} "Failed to write to $allowListPath"; return 2; } + + return 0 # Indicate success + } + + # Function to fetch and process URLs from a JSON file + fetch_and_process_url() { + local file_url="$1" + + # Fetch and parse the JSON + if json_content=$(curl -s --retry 5 --retry-delay 10 --retry-connrefused "$file_url"); then + echo "$json_content" | jq -r '.[]? | select(.category == "Optimize" or .category == "Allow" or .category == "Default") | .urls[]?' | sort | uniq + else + logger -t ${logTag} "Failed to fetch or parse JSON from $file_url" + return 1 + fi + } + + # Parse command line arguments + while getopts "u:f:p:" opt; do + case $opt in + u) url="$OPTARG" ;; # Single JSON file URL to fetch + f) url_folder="$OPTARG" ;; # Folder API URL containing JSON files + p) allowListPath="$OPTARG" ;; # Path to the allow list file + \?) echo "Usage: $0 -u | -f -p " + exit 1 ;; + esac + done + + # Validate input parameters + if [[ -z "$allowListPath" ]]; then + echo "Error: Allow List Path (-p) must be provided." + echo "Usage: $0 -u | -f -p " + exit 1 + fi + + if [[ -n "$url" && -n "$url_folder" ]]; then + echo "Error: Only one of -u or -f should be provided, not both." + exit 1 + elif [[ -z "$url" && -z "$url_folder" ]]; then + echo "Error: One of -u or -f must be provided." + exit 1 + fi + + # Check if the device is connected to the internet + if ! ping -c 1 8.8.8.8 &> /dev/null; then + logger -t ${logTag} "No internet connection. URLs not fetched." + exit 3 + fi + + # Process a single URL (-u option) + all_urls="" + if [[ -n "$url" ]]; then + logger -t ${logTag} "Fetching URLs from $url" + + # Fetch and process the single JSON file + fetched_urls=$(fetch_and_process_url "$url") + if [[ -z "$fetched_urls" ]]; then + logger -t ${logTag} "No valid URLs found in the file $url" + exit 4 + fi + all_urls="$fetched_urls" + fi + + # Process a folder of JSON files (-f option) + if [[ -n "$url_folder" ]]; then + logger -t ${logTag} "Fetching JSON files from folder $url_folder" + + # Use the folder URL directly as the API endpoint + folder_api_url="$url_folder" + + # Fetch the folder contents from the API + folder_response=$(curl -s -H "Accept: application/vnd.github.v3+json" "$folder_api_url") + + # Extract JSON file URLs + file_urls=$(echo "$folder_response" | jq -r '.[] | select(.name | endswith(".json")) | .download_url') + + if [[ -z "$file_urls" ]]; then + logger -t ${logTag} "No JSON files found in folder $folder_api_url" + exit 4 + fi + + # Process each JSON file URL + for file_url in $file_urls; do + fetched_urls=$(fetch_and_process_url "$file_url") + all_urls+="$fetched_urls"$'\n' + done + fi + + # Deduplicate and format URLs + all_urls=$(echo "$all_urls" | sort | uniq | tr '\n' ',') # Sort, deduplicate, join with commas + all_urls=$(echo "$all_urls" | awk '{sub(/^,/, ""); print}') + all_urls=$(echo "$all_urls" | awk '{gsub(/^,|,$/, ""); print}') + + # Write to the allow list + if write_to_allow_list "$all_urls" "$allowListPath"; then + logger -t ${logTag} "URLs fetched and saved to $allowListPath successfully" + exit 0 # Success + else + logger -t ${logTag} "Failed to save URLs to allow list" + exit 2 + fi + ''; + meta = with lib; { + description = " + The application is a shell script designed to fetch a list of URLs + from a specified endpoint and save them to an allow list file. The script includes error + handling and retry logic to ensure robustness in various network conditions. + "; + }; +}