mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-06 20:45:00 +03:00
Fix TUN core elevation: decide sudo from the config context snapshot (#9830)
CoreManager.RunProcess decided sudo elevation from the live mutable
_config.TunModeItem.EnableTun while the launched config was generated
from the immutable CoreConfigContext snapshot. If the TUN state changed
while a reload was in flight, a core whose config contains a TUN
inbound could launch without elevation and die within the 100ms health
check ("Failed to run core"), with no stderr shown.
The elevation decision now follows context.IsTunEnabled /
preContext.IsTunEnabled, so the generated config and the launch mode
always agree. With legacy TUN protect, the sing-box pre-core hosting
TUN is elevated and the main core no longer runs as root needlessly.
This commit is contained in:
@@ -592,4 +592,38 @@ public class CoreConfigSingboxServiceTests
|
|||||||
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
|
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
|
||||||
proxy.server.Should().BeNull();
|
proxy.server.Should().BeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
|
||||||
|
{
|
||||||
|
// Regression test for #9820: sing-box fails with "need one more IPv6 address in
|
||||||
|
// first prefix for system stack" when the TUN inbound uses a /128 IPv6 prefix.
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.TunModeItem.EnableTun = true;
|
||||||
|
config.TunModeItem.Stack = "system";
|
||||||
|
config.TunModeItem.EnableIPv6Address = true;
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
IsTunEnabled = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var tun = cfg.inbounds.First(i => i.type == "tun");
|
||||||
|
|
||||||
|
tun.address.Should().NotBeNullOrEmpty();
|
||||||
|
foreach (var address in tun.address!)
|
||||||
|
{
|
||||||
|
var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
|
||||||
|
var isIpv6 = address.Contains(':');
|
||||||
|
prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
|
||||||
|
$"'{address}' must leave room for the peer address the system stack derives");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Manager;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.Manager;
|
||||||
|
|
||||||
|
public class CoreManagerTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ECoreType.sing_box)]
|
||||||
|
[InlineData(ECoreType.mihomo)]
|
||||||
|
[InlineData(ECoreType.Xray)]
|
||||||
|
public void ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
|
||||||
|
{
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
|
||||||
|
{
|
||||||
|
// Regression guard for the macOS TUN failure: the elevation decision must follow
|
||||||
|
// the context snapshot that generated the config. A launch whose snapshot has TUN
|
||||||
|
// disabled must never elevate, and a launch whose snapshot has TUN enabled must
|
||||||
|
// elevate regardless of later changes to the live config.
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldRunAsSudo_OnWindows_ShouldNotElevate()
|
||||||
|
{
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ECoreType.v2fly)]
|
||||||
|
[InlineData(ECoreType.hysteria)]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
|
||||||
|
{
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -183,7 +183,7 @@ public class CoreManager
|
|||||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
||||||
|
|
||||||
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
|
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
|
||||||
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true);
|
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true, context.IsTunEnabled);
|
||||||
if (proc is null)
|
if (proc is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -201,7 +201,7 @@ public class CoreManager
|
|||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
|
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
|
||||||
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true);
|
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true, preContext.IsTunEnabled);
|
||||||
if (proc is null)
|
if (proc is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -289,7 +289,20 @@ public class CoreManager
|
|||||||
|
|
||||||
#region Process
|
#region Process
|
||||||
|
|
||||||
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
|
/// <summary>
|
||||||
|
/// Decides whether a core launch must be elevated on non-Windows platforms.
|
||||||
|
/// The TUN state comes from the immutable <see cref="CoreConfigContext" /> snapshot that
|
||||||
|
/// generated the config, never from the live mutable config: the generated config and the
|
||||||
|
/// launch mode must always agree, even if TUN is toggled while a reload is in flight.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ShouldRunAsSudo(bool isTunLaunch, ECoreType? coreType, bool isNonWindows)
|
||||||
|
{
|
||||||
|
return isTunLaunch
|
||||||
|
&& coreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray
|
||||||
|
&& isNonWindows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo, bool isTunLaunch = false)
|
||||||
{
|
{
|
||||||
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
|
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
|
||||||
if (fileName.IsNullOrEmpty())
|
if (fileName.IsNullOrEmpty())
|
||||||
@@ -301,9 +314,7 @@ public class CoreManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (mayNeedSudo
|
if (mayNeedSudo
|
||||||
&& _config.TunModeItem.EnableTun
|
&& ShouldRunAsSudo(isTunLaunch, coreInfo.CoreType, Utils.IsNonWindows()))
|
||||||
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray)
|
|
||||||
&& Utils.IsNonWindows())
|
|
||||||
{
|
{
|
||||||
_linuxSudo = true;
|
_linuxSudo = true;
|
||||||
await CoreAdminManager.Instance.Init(_config, _updateFunc);
|
await CoreAdminManager.Instance.Init(_config, _updateFunc);
|
||||||
|
|||||||
Reference in New Issue
Block a user