nekoray: fix TUN functionality (#410840)

This commit is contained in:
Aleksana 2025-06-01 11:19:48 +08:00 committed by GitHub
commit a6f4ca139b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 195 additions and 1 deletions

View file

@ -262,6 +262,7 @@
./programs/nano.nix
./programs/nautilus-open-any-terminal.nix
./programs/nbd.nix
./programs/nekoray.nix
./programs/neovim.nix
./programs/nethoscope.nix
./programs/nexttrace.nix

View file

@ -0,0 +1,90 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.programs.nekoray;
in
{
options = {
programs.nekoray = {
enable = lib.mkEnableOption "nekoray, a GUI proxy configuration manager";
package = lib.mkPackageOption pkgs "nekoray" { };
tunMode = {
enable = lib.mkEnableOption "TUN mode of nekoray";
setuid = lib.mkEnableOption ''
setting suid bit for nekobox_core to run as root, which is less
secure than default setcap method but closer to upstream assumptions.
Enable this if you find the default setcap method configured in
this module doesn't work for you
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
security.wrappers.nekobox_core = lib.mkIf cfg.tunMode.enable {
source = "${cfg.package}/share/nekoray/nekobox_core";
owner = "root";
group = "root";
setuid = lib.mkIf cfg.tunMode.setuid true;
# Taken from https://github.com/SagerNet/sing-box/blob/dev-next/release/config/sing-box.service
capabilities = lib.mkIf (
!cfg.tunMode.setuid
) "cap_net_admin,cap_net_raw,cap_net_bind_service,cap_sys_ptrace,cap_dac_read_search+ep";
};
# avoid resolvectl password prompt popping up three times
# https://github.com/SagerNet/sing-tun/blob/0686f8c4f210f4e7039c352d42d762252f9d9cf5/tun_linux.go#L1062
# We use a hack here to determine whether the requested process is nekobox_core
# Detect whether its capabilities contain at least `net_admin` and `net_raw`.
# This does not reduce security, as we can already bypass `resolved` with them.
# Alternatives to consider:
# 1. Use suid to execute as a specific user, and check username with polkit.
# However, NixOS module doesn't let us to set setuid and capabilities at the
# same time, and it's tricky to make both work together because of some security
# considerations in the kernel.
# 2. Check cmdline to get executable path. This is insecure because the process can
# change its own cmdline. `/proc/<pid>/exe` is reliable but kernel forbids
# checking that entry of process from different users, and polkit runs `spawn`
# as an unprivileged user.
# 3. Put nekobox_core into a systemd service, and let polkit check service name.
# This is the most secure and convenient way but requires heavy modification
# to nekoray source code. Would be good to let upstream support that eventually.
security.polkit.extraConfig =
lib.mkIf (cfg.tunMode.enable && (!cfg.tunMode.setuid) && config.services.resolved.enable)
''
polkit.addRule(function(action, subject) {
const allowedActionIds = [
"org.freedesktop.resolve1.set-domains",
"org.freedesktop.resolve1.set-default-route",
"org.freedesktop.resolve1.set-dns-servers"
];
if (allowedActionIds.indexOf(action.id) !== -1) {
try {
var parentPid = polkit.spawn(["${lib.getExe' pkgs.procps "ps"}", "-o", "ppid=", subject.pid]).trim();
var parentCap = polkit.spawn(["${lib.getExe' pkgs.libcap "getpcaps"}", parentPid]).trim();
if (parentCap.includes("cap_net_admin") && parentCap.includes("cap_net_raw")) {
return polkit.Result.YES;
} else {
return polkit.Result.NOT_HANDLED;
}
} catch (e) {
return polkit.Result.NOT_HANDLED;
}
}
})
'';
};
meta.maintainers = with lib.maintainers; [ aleksana ];
}

View file

@ -0,0 +1,43 @@
diff --git a/server.go b/server.go
index c2a6be0..8aeca1c 100644
--- a/server.go
+++ b/server.go
@@ -11,6 +11,7 @@ import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/service"
+ "golang.org/x/sys/unix"
"log"
"nekobox_core/gen"
"nekobox_core/internal/boxbox"
@@ -359,13 +360,25 @@ func (s *server) CompileGeoSiteToSrs(ctx context.Context, in *gen.CompileGeoSite
}
func (s *server) IsPrivileged(ctx context.Context, _ *gen.EmptyReq) (*gen.IsPrivilegedResponse, error) {
- if runtime.GOOS == "windows" {
- return &gen.IsPrivilegedResponse{
- HasPrivilege: false,
- }, nil
+ ret := false
+ if runtime.GOOS == "windows" || os.Geteuid() == 0 {
+ ret = true
+ } else if runtime.GOOS == "linux" {
+ caps := unix.CapUserHeader{
+ Version: unix.LINUX_CAPABILITY_VERSION_3,
+ Pid: 0, // current
+ }
+ var data [2]unix.CapUserData
+ err := unix.Capget(&caps, &data[0])
+ if err != nil {
+ ret = false
+ } else {
+ // CAP_NET_ADMIN = 12
+ ret = (data[0].Effective & (1 << unix.CAP_NET_ADMIN)) != 0
+ }
}
- return &gen.IsPrivilegedResponse{HasPrivilege: os.Geteuid() == 0}, nil
+ return &gen.IsPrivilegedResponse{HasPrivilege: ret}, nil
}
func (s *server) SpeedTest(ctx context.Context, in *gen.SpeedTestRequest) (*gen.SpeedTestResponse, error) {

View file

@ -0,0 +1,47 @@
diff --git a/src/global/NekoGui.cpp b/src/global/NekoGui.cpp
index 7943d7a..5bb20cc 100644
--- a/src/global/NekoGui.cpp
+++ b/src/global/NekoGui.cpp
@@ -355,6 +355,12 @@ namespace NekoGui {
// System Utils
QString FindNekoBoxCoreRealPath() {
+ // find in PATH first
+ QString path = QStandardPaths::findExecutable("nekobox_core");
+ if (!path.isEmpty()) {
+ return path;
+ }
+
auto fn = QApplication::applicationDirPath() + "/nekobox_core";
auto fi = QFileInfo(fn);
if (fi.isSymLink()) return fi.symLinkTarget();
diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp
index 9aa46b2..ba7137a 100644
--- a/src/ui/mainwindow.cpp
+++ b/src/ui/mainwindow.cpp
@@ -125,8 +125,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
NekoGui::dataStore->core_port = MkPort();
if (NekoGui::dataStore->core_port <= 0) NekoGui::dataStore->core_port = 19810;
- auto core_path = QApplication::applicationDirPath() + "/";
- core_path += "nekobox_core";
+ auto core_path = NekoGui::FindNekoBoxCoreRealPath();
QStringList args;
args.push_back("nekobox");
@@ -844,6 +843,15 @@ bool MainWindow::get_elevated_permissions(int reason) {
return true;
}
if (NekoGui::IsAdmin()) return true;
+ QMessageBox::critical(
+ GetMessageBoxParent(),
+ tr("Unable to elevate privileges when installed with Nix"),
+ 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."),
+ QMessageBox::Ok
+ );
+ return false;
+ // The following code isn't effective, preserve to avoid merge conflict
+
#ifdef Q_OS_LINUX
if (!Linux_HavePkexec()) {
MessageBoxWarning(software_name, "Please install \"pkexec\" first.");

View file

@ -60,6 +60,11 @@ stdenv.mkDerivation (finalAttrs: {
# we already package those two files in nixpkgs
# 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
./search-for-geodata-in-install-location.patch
# disable suid request as it cannot be applied to nekobox_core in nix store
# and prompt users to use NixOS module instead. And use nekobox_core from PATH
# to make use of security wrappers
./nixos-disable-setuid-request.patch
];
installPhase = ''
@ -99,6 +104,11 @@ stdenv.mkDerivation (finalAttrs: {
inherit (finalAttrs) version src;
sourceRoot = "${finalAttrs.src.name}/core/server";
patches = [
# also check cap_net_admin so we don't have to set suid
./core-also-check-capabilities.patch
];
vendorHash = "sha256-hZiEIJ4/TcLUfT+pkqs6WfzjqppSTjKXEtQC+DS26Ug=";
# ldflags and tags are taken from script/build_go.sh
@ -127,7 +137,10 @@ stdenv.mkDerivation (finalAttrs: {
homepage = "https://github.com/Mahdi-zarei/nekoray";
license = lib.licenses.gpl3Plus;
mainProgram = "nekoray";
maintainers = with lib.maintainers; [ tomasajt ];
maintainers = with lib.maintainers; [
tomasajt
aleksana
];
platforms = lib.platforms.linux;
};
})