Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/designs/network-accelerator-ui-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
## 核心安装

- 首次使用可直接在“订阅信息”卡片点击“安装核心”。
- 当前固定使用稳定版 `1.13.12`,按 `windows-amd64`、`darwin-amd64` 或 `darwin-arm64` 自动选择官方包。
- 当前固定使用稳定版 `1.13.12`,按 `windows-amd64`、`darwin-amd64`、`darwin-arm64`、`linux-amd64` 或 `linux-arm64` 自动选择官方包。Linux 动态包在本机没有其动态链接器时(常见于未启用 nix-ld 的 NixOS)改用同版本官方 musl 静态包。
- 下载地址只允许 SagerNet 官方 GitHub Release,并校验 Release API 对应资产提供的 SHA-256 digest。
- 核心安装到应用数据目录的 `core` 子目录;也兼容应用包内 `core` 目录或系统 `PATH` 中已有的 sing-box。
- Windows/macOS 开启 TUN 仍可能需要系统管理员权限;权限失败时页面保持“未连接”并显示核心错误。
Expand Down
168 changes: 168 additions & 0 deletions src/NetworkAccelerator.Core/Services/LinuxExecutableFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System.Buffers.Binary;
using System.Text;

namespace NetworkAccelerator.Core.Services;

/// <summary>
/// File capabilities are applied to the ELF the kernel executes. A shebang script, including the
/// NixOS pattern that execs <c>ld-linux</c> against a sibling <c>sing-box.bin</c>, never receives them.
/// </summary>
public static class LinuxExecutableFormat
{
public static bool CanHoldFileCapabilities(string path)
{
try
{
using var stream = File.OpenRead(path);
var header = new byte[64];
var read = stream.Read(header, 0, header.Length);
return CanHoldFileCapabilities(header.AsSpan(0, read));
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
return false;
}
}

public static bool CanHoldFileCapabilities(ReadOnlySpan<byte> header)
{
if (IsShebang(header)) return false;
return IsElfExecutable(header);
}

public static bool IsShebang(ReadOnlySpan<byte> header) =>
header.Length >= 2 && header[0] == (byte)'#' && header[1] == (byte)'!';

public static bool IsElfExecutable(ReadOnlySpan<byte> header)
{
if (header.Length < 20 || !HasElfMagic(header)) return false;
var klass = header[4];
var data = header[5];
if (klass is not (1 or 2) || data is not (1 or 2) || header[6] != 1) return false;
var type = ReadU16(header, 16, data);
return type is 2 or 3;
}

/// <summary>
/// Returns the PT_INTERP path when it is recorded and not present on this machine.
/// Static binaries and scripts return null.
/// </summary>
public static string? MissingInterpreter(string path)
{
try
{
var interpreter = ReadInterpreter(path);
if (string.IsNullOrWhiteSpace(interpreter)) return null;
return File.Exists(interpreter) ? null : interpreter;
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
return null;
}
}

public static string? ReadInterpreter(string path)
{
using var stream = File.OpenRead(path);
var header = new byte[64];
var read = stream.Read(header, 0, header.Length);
if (!IsElfExecutable(header.AsSpan(0, read))) return null;
var klass = header[4];
var data = header[5];
long phoff;
int phentsize;
int phnum;
if (klass == 2)
{
if (read < 64) return null;
phoff = (long)ReadU64(header, 32, data);
phentsize = ReadU16(header, 54, data);
phnum = ReadU16(header, 56, data);
}
else
{
if (read < 52) return null;
phoff = ReadU32(header, 28, data);
phentsize = ReadU16(header, 42, data);
phnum = ReadU16(header, 44, data);
}

if (phoff <= 0 || phnum <= 0 || phentsize is < 16 or > 512 || phnum > 128) return null;
if (phoff > 16L * 1024 * 1024) return null;
var tableLength = phentsize * phnum;
var table = new byte[tableLength];
stream.Position = phoff;
if (stream.Read(table, 0, table.Length) != table.Length) return null;

for (var index = 0; index < phnum; index++)
{
var entry = table.AsSpan(index * phentsize, phentsize);
if (ReadU32(entry, 0, data) != 3) continue;
long offset;
long size;
if (klass == 2)
{
if (entry.Length < 40) return null;
offset = (long)ReadU64(entry, 8, data);
size = (long)ReadU64(entry, 32, data);
}
else
{
if (entry.Length < 20) return null;
offset = ReadU32(entry, 4, data);
size = ReadU32(entry, 16, data);
}

if (offset < 0 || size is <= 1 or > 4096) return null;
var text = new byte[size];
stream.Position = offset;
if (stream.Read(text, 0, text.Length) != text.Length) return null;
var end = Array.IndexOf(text, (byte)0);
if (end < 0) end = text.Length;
var interpreter = Encoding.UTF8.GetString(text, 0, end);
return interpreter.Length > 0 && interpreter[0] == '/' ? interpreter : null;
}

return null;
}

public static bool LooksLikeMissingLoader(string? text)
{
if (string.IsNullOrWhiteSpace(text)) return false;
return text.Contains("ld-linux", StringComparison.Ordinal)
|| text.Contains("ld.so", StringComparison.Ordinal)
|| text.Contains("error while loading shared libraries", StringComparison.OrdinalIgnoreCase)
|| text.Contains("cannot open shared object", StringComparison.OrdinalIgnoreCase);
}

private static bool HasElfMagic(ReadOnlySpan<byte> header) =>
header.Length >= 4
&& header[0] == 0x7F
&& header[1] == (byte)'E'
&& header[2] == (byte)'L'
&& header[3] == (byte)'F';

private static ushort ReadU16(ReadOnlySpan<byte> buffer, int offset, byte data)
{
var slice = buffer.Slice(offset, 2);
return data == 2
? BinaryPrimitives.ReadUInt16BigEndian(slice)
: BinaryPrimitives.ReadUInt16LittleEndian(slice);
}

private static uint ReadU32(ReadOnlySpan<byte> buffer, int offset, byte data)
{
var slice = buffer.Slice(offset, 4);
return data == 2
? BinaryPrimitives.ReadUInt32BigEndian(slice)
: BinaryPrimitives.ReadUInt32LittleEndian(slice);
}

private static ulong ReadU64(ReadOnlySpan<byte> buffer, int offset, byte data)
{
var slice = buffer.Slice(offset, 8);
return data == 2
? BinaryPrimitives.ReadUInt64BigEndian(slice)
: BinaryPrimitives.ReadUInt64LittleEndian(slice);
}
}
15 changes: 11 additions & 4 deletions src/NetworkAccelerator.Core/Services/LinuxTunAuthorization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public readonly record struct LinuxTunFacts(
bool SetcapAvailable,
bool PkexecAvailable,
bool FilesystemHonorsFileCapabilities,
bool PlainPkexecExists = false);
bool PlainPkexecExists = false,
bool SourceCanHoldFileCapabilities = true);

public static class LinuxTunAuthorization
{
Expand All @@ -38,13 +39,16 @@ public static class LinuxTunAuthorization

public static LinuxTunDecision Decide(LinuxTunFacts facts)
{
if (facts.CopyExists && facts.CopyHasNetAdmin && facts.HashMatches)
// getcap/xattr on a shell wrapper still report cap_net_admin after setcap, but the kernel
// never grants those capabilities to a shebang or ld-linux script. Ignore them.
var capsUsable = facts.SourceCanHoldFileCapabilities;
if (capsUsable && facts.CopyExists && facts.CopyHasNetAdmin && facts.HashMatches)
return LinuxTunDecision.UseCapableCopy;
if (facts.SourceHasNetAdmin)
if (capsUsable && facts.SourceHasNetAdmin)
return LinuxTunDecision.UseSource;
if (!facts.PkexecAvailable)
return facts.PlainPkexecExists ? LinuxTunDecision.SetuidWrapperMissing : LinuxTunDecision.PkexecMissing;
if (facts.SetcapAvailable && facts.FilesystemHonorsFileCapabilities)
if (capsUsable && facts.SetcapAvailable && facts.FilesystemHonorsFileCapabilities)
return LinuxTunDecision.CopyAndSetcap;
return LinuxTunDecision.LaunchWithPkexec;
}
Expand All @@ -71,6 +75,9 @@ public static LinuxPkexecOutcome ClassifyPkexec(int exitCode, string? standardEr
public static bool OutputHasNetAdmin(string? text) =>
text?.Contains("cap_net_admin", StringComparison.Ordinal) == true;

public static bool PkexecHandedOff(string? command) =>
!string.IsNullOrEmpty(command) && !command.Equals("pkexec", StringComparison.Ordinal);

public static bool RequiresSetuidWrapper(string? text) =>
Contains(text ?? string.Empty, "setuid") || Contains(text ?? string.Empty, "enablePkexecWrapper");

Expand Down
Loading
Loading