···11<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
1213- [gtklock](https://github.com/jovanlanik/gtklock), a GTK-based lockscreen for Wayland. Available as [programs.gtklock](#opt-programs.gtklock.enable).
01415- [FileBrowser](https://filebrowser.org/), a web application for managing and sharing files. Available as [services.filebrowser](#opt-services.filebrowser.enable).
16
···11<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
1213- [gtklock](https://github.com/jovanlanik/gtklock), a GTK-based lockscreen for Wayland. Available as [programs.gtklock](#opt-programs.gtklock.enable).
14+- [Chrysalis](https://github.com/keyboardio/Chrysalis), a graphical configurator for Kaleidoscope-powered keyboards. Available as [programs.chrysalis](#opt-programs.chrysalis.enable).
1516- [FileBrowser](https://filebrowser.org/), a web application for managing and sharing files. Available as [services.filebrowser](#opt-services.filebrowser.enable).
17
···1+{
2+ config,
3+ pkgs,
4+ lib,
5+ ...
6+}:
7+8+let
9+ cfg = config.programs.nekoray;
10+in
11+{
12+ options = {
13+ programs.nekoray = {
14+ enable = lib.mkEnableOption "nekoray, a GUI proxy configuration manager";
15+16+ package = lib.mkPackageOption pkgs "nekoray" { };
17+18+ tunMode = {
19+ enable = lib.mkEnableOption "TUN mode of nekoray";
20+21+ setuid = lib.mkEnableOption ''
22+ setting suid bit for nekobox_core to run as root, which is less
23+ secure than default setcap method but closer to upstream assumptions.
24+ Enable this if you find the default setcap method configured in
25+ this module doesn't work for you
26+ '';
27+ };
28+ };
29+ };
30+31+ config = lib.mkIf cfg.enable {
32+ environment.systemPackages = [ cfg.package ];
33+34+ security.wrappers.nekobox_core = lib.mkIf cfg.tunMode.enable {
35+ source = "${cfg.package}/share/nekoray/nekobox_core";
36+ owner = "root";
37+ group = "root";
38+ setuid = lib.mkIf cfg.tunMode.setuid true;
39+ # Taken from https://github.com/SagerNet/sing-box/blob/dev-next/release/config/sing-box.service
40+ capabilities = lib.mkIf (
41+ !cfg.tunMode.setuid
42+ ) "cap_net_admin,cap_net_raw,cap_net_bind_service,cap_sys_ptrace,cap_dac_read_search+ep";
43+ };
44+45+ # avoid resolvectl password prompt popping up three times
46+ # https://github.com/SagerNet/sing-tun/blob/0686f8c4f210f4e7039c352d42d762252f9d9cf5/tun_linux.go#L1062
47+ # We use a hack here to determine whether the requested process is nekobox_core
48+ # Detect whether its capabilities contain at least `net_admin` and `net_raw`.
49+ # This does not reduce security, as we can already bypass `resolved` with them.
50+ # Alternatives to consider:
51+ # 1. Use suid to execute as a specific user, and check username with polkit.
52+ # However, NixOS module doesn't let us to set setuid and capabilities at the
53+ # same time, and it's tricky to make both work together because of some security
54+ # considerations in the kernel.
55+ # 2. Check cmdline to get executable path. This is insecure because the process can
56+ # change its own cmdline. `/proc/<pid>/exe` is reliable but kernel forbids
57+ # checking that entry of process from different users, and polkit runs `spawn`
58+ # as an unprivileged user.
59+ # 3. Put nekobox_core into a systemd service, and let polkit check service name.
60+ # This is the most secure and convenient way but requires heavy modification
61+ # to nekoray source code. Would be good to let upstream support that eventually.
62+ security.polkit.extraConfig =
63+ lib.mkIf (cfg.tunMode.enable && (!cfg.tunMode.setuid) && config.services.resolved.enable)
64+ ''
65+ polkit.addRule(function(action, subject) {
66+ const allowedActionIds = [
67+ "org.freedesktop.resolve1.set-domains",
68+ "org.freedesktop.resolve1.set-default-route",
69+ "org.freedesktop.resolve1.set-dns-servers"
70+ ];
71+72+ if (allowedActionIds.indexOf(action.id) !== -1) {
73+ try {
74+ var parentPid = polkit.spawn(["${lib.getExe' pkgs.procps "ps"}", "-o", "ppid=", subject.pid]).trim();
75+ var parentCap = polkit.spawn(["${lib.getExe' pkgs.libcap "getpcaps"}", parentPid]).trim();
76+ if (parentCap.includes("cap_net_admin") && parentCap.includes("cap_net_raw")) {
77+ return polkit.Result.YES;
78+ } else {
79+ return polkit.Result.NOT_HANDLED;
80+ }
81+ } catch (e) {
82+ return polkit.Result.NOT_HANDLED;
83+ }
84+ }
85+ })
86+ '';
87+ };
88+89+ meta.maintainers = with lib.maintainers; [ aleksana ];
90+}
···1+diff --git a/src/global/NekoGui.cpp b/src/global/NekoGui.cpp
2+index 7943d7a..5bb20cc 100644
3+--- a/src/global/NekoGui.cpp
4++++ b/src/global/NekoGui.cpp
5+@@ -355,6 +355,12 @@ namespace NekoGui {
6+ // System Utils
7+8+ QString FindNekoBoxCoreRealPath() {
9++ // find in PATH first
10++ QString path = QStandardPaths::findExecutable("nekobox_core");
11++ if (!path.isEmpty()) {
12++ return path;
13++ }
14++
15+ auto fn = QApplication::applicationDirPath() + "/nekobox_core";
16+ auto fi = QFileInfo(fn);
17+ if (fi.isSymLink()) return fi.symLinkTarget();
18+diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp
19+index 9aa46b2..ba7137a 100644
20+--- a/src/ui/mainwindow.cpp
21++++ b/src/ui/mainwindow.cpp
22+@@ -125,8 +125,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
23+ NekoGui::dataStore->core_port = MkPort();
24+ if (NekoGui::dataStore->core_port <= 0) NekoGui::dataStore->core_port = 19810;
25+26+- auto core_path = QApplication::applicationDirPath() + "/";
27+- core_path += "nekobox_core";
28++ auto core_path = NekoGui::FindNekoBoxCoreRealPath();
29+30+ QStringList args;
31+ args.push_back("nekobox");
32+@@ -844,6 +843,15 @@ bool MainWindow::get_elevated_permissions(int reason) {
33+ return true;
34+ }
35+ if (NekoGui::IsAdmin()) return true;
36++ QMessageBox::critical(
37++ GetMessageBoxParent(),
38++ tr("Unable to elevate privileges when installed with Nix"),
39++ tr("Due to the read-only property of Nix store, we cannot set suid for nekobox_core. If you are using NixOS, please set `programs.nekoray.tunMode.enable` option to elevate privileges."),
40++ QMessageBox::Ok
41++ );
42++ return false;
43++ // The following code isn't effective, preserve to avoid merge conflict
44++
45+ #ifdef Q_OS_LINUX
46+ if (!Linux_HavePkexec()) {
47+ MessageBoxWarning(software_name, "Please install \"pkexec\" first.");
+14-1
pkgs/by-name/ne/nekoray/package.nix
···60 # we already package those two files in nixpkgs
61 # we can't place file at that location using our builder so we must change the search directory to be relative to the built executable
62 ./search-for-geodata-in-install-location.patch
0000063 ];
6465 installPhase = ''
···99 inherit (finalAttrs) version src;
100 sourceRoot = "${finalAttrs.src.name}/core/server";
10100000102 vendorHash = "sha256-hZiEIJ4/TcLUfT+pkqs6WfzjqppSTjKXEtQC+DS26Ug=";
103104 # ldflags and tags are taken from script/build_go.sh
···127 homepage = "https://github.com/Mahdi-zarei/nekoray";
128 license = lib.licenses.gpl3Plus;
129 mainProgram = "nekoray";
130- maintainers = with lib.maintainers; [ tomasajt ];
000131 platforms = lib.platforms.linux;
132 };
133})
···60 # we already package those two files in nixpkgs
61 # we can't place file at that location using our builder so we must change the search directory to be relative to the built executable
62 ./search-for-geodata-in-install-location.patch
63+64+ # disable suid request as it cannot be applied to nekobox_core in nix store
65+ # and prompt users to use NixOS module instead. And use nekobox_core from PATH
66+ # to make use of security wrappers
67+ ./nixos-disable-setuid-request.patch
68 ];
6970 installPhase = ''
···104 inherit (finalAttrs) version src;
105 sourceRoot = "${finalAttrs.src.name}/core/server";
106107+ patches = [
108+ # also check cap_net_admin so we don't have to set suid
109+ ./core-also-check-capabilities.patch
110+ ];
111+112 vendorHash = "sha256-hZiEIJ4/TcLUfT+pkqs6WfzjqppSTjKXEtQC+DS26Ug=";
113114 # ldflags and tags are taken from script/build_go.sh
···137 homepage = "https://github.com/Mahdi-zarei/nekoray";
138 license = lib.licenses.gpl3Plus;
139 mainProgram = "nekoray";
140+ maintainers = with lib.maintainers; [
141+ tomasajt
142+ aleksana
143+ ];
144 platforms = lib.platforms.linux;
145 };
146})
···1504 pipewire-media-session = throw "pipewire-media-session is no longer maintained and has been removed. Please use Wireplumber instead.";
1505 platypus = throw "platypus is unmaintained and has not merged Python3 support"; # Added 2025-03-20
1506 pleroma-otp = throw "'pleroma-otp' has been renamed to/replaced by 'pleroma'"; # Converted to throw 2024-10-17
01507 plots = throw "'plots' has been replaced by 'gnome-graphs'"; # Added 2025-02-05
1508 pltScheme = racket; # just to be sure
1509 poac = cabinpkg; # Added 2025-01-22
···1504 pipewire-media-session = throw "pipewire-media-session is no longer maintained and has been removed. Please use Wireplumber instead.";
1505 platypus = throw "platypus is unmaintained and has not merged Python3 support"; # Added 2025-03-20
1506 pleroma-otp = throw "'pleroma-otp' has been renamed to/replaced by 'pleroma'"; # Converted to throw 2024-10-17
1507+ plex-media-player = throw "'plex-media-player' has been discontinued, the new official client is available as 'plex-desktop'"; # Added 2025-05-28
1508 plots = throw "'plots' has been replaced by 'gnome-graphs'"; # Added 2025-02-05
1509 pltScheme = racket; # just to be sure
1510 poac = cabinpkg; # Added 2025-01-22