Add xray tun support

This commit is contained in:
DHR60 2026-04-10 01:40:43 +08:00
parent 53041906b3
commit 703f962ff5
15 changed files with 187 additions and 294 deletions

View file

@ -53,19 +53,23 @@ internal static class WindowsUtils
public static async Task RemoveTunDevice()
{
try
var tunNameList = new List<string> { "singbox_tun", "xray_tun" };
foreach (var tunName in tunNameList)
{
var sum = MD5.HashData(Encoding.UTF8.GetBytes("wintunsingbox_tun"));
var guid = new Guid(sum);
var pnpUtilPath = @"C:\Windows\System32\pnputil.exe";
var arg = $$""" /remove-device "SWD\Wintun\{{{guid}}}" """;
try
{
var sum = MD5.HashData(Encoding.UTF8.GetBytes($"wintun{tunName}"));
var guid = new Guid(sum);
var pnpUtilPath = @"C:\Windows\System32\pnputil.exe";
var arg = $$""" /remove-device "SWD\Wintun\{{{guid}}}" """;
// Try to remove the device
_ = await Utils.GetCliWrapOutput(pnpUtilPath, arg);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
// Try to remove the device
_ = await Utils.GetCliWrapOutput(pnpUtilPath, arg);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
}
}

View file

@ -24,6 +24,8 @@ public class Global
public const string V2raySampleHttpResponseFileName = NamespaceSample + "SampleHttpResponse";
public const string V2raySampleInbound = NamespaceSample + "SampleInbound";
public const string V2raySampleOutbound = NamespaceSample + "SampleOutbound";
public const string V2raySampleTunInbound = NamespaceSample + "SampleTunInbound";
public const string V2raySampleTunRules = NamespaceSample + "SampleTunRules";
public const string SingboxSampleOutbound = NamespaceSample + "SingboxSampleOutbound";
public const string CustomRoutingFileName = NamespaceSample + "custom_routing_";
public const string TunSingboxDNSFileName = NamespaceSample + "tun_singbox_dns";
@ -48,6 +50,7 @@ public class Global
public const string ProxyTag = "proxy";
public const string DirectTag = "direct";
public const string BlockTag = "block";
public const string DnsOutboundTag = "dns";
public const string DnsTag = "dns-module";
public const string DirectDnsTag = "direct-dns";
public const string BalancerTagSuffix = "-round";

View file

@ -31,8 +31,6 @@ public record CoreConfigContextBuilderAllResult(
public CoreConfigContext ResolvedMainContext => PreSocksResult is not null
? MainResult.Context with
{
TunProtectSsPort = PreSocksResult.Context.TunProtectSsPort,
ProxyRelaySsPort = PreSocksResult.Context.ProxyRelaySsPort,
ProtectDomainList = [.. MainResult.Context.ProtectDomainList ?? [], .. PreSocksResult.Context.ProtectDomainList ?? []],
}
: MainResult.Context;
@ -58,8 +56,6 @@ public class CoreConfigContextBuilder
IsTunEnabled = config.TunModeItem.EnableTun,
SimpleDnsItem = config.SimpleDNSItem,
ProtectDomainList = [],
TunProtectSsPort = 0,
ProxyRelaySsPort = 0,
RawDnsItem = await AppManager.Instance.GetDNSItem(coreType),
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
};
@ -148,37 +144,7 @@ public class CoreConfigContextBuilder
};
}
if (!nodeContext.IsTunEnabled
|| coreType != ECoreType.Xray
|| node.ConfigType == EConfigType.Custom)
{
return null;
}
var tunProtectSsPort = Utils.GetFreePort();
var proxyRelaySsPort = Utils.GetFreePort();
var preItem = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.Shadowsocks,
Address = Global.Loopback,
Port = proxyRelaySsPort,
Password = Global.None,
};
preItem.SetProtocolExtra(preItem.GetProtocolExtra() with
{
SsMethod = Global.None,
});
var preResult2 = await Build(nodeContext.AppConfig, preItem);
return preResult2 with
{
Context = preResult2.Context with
{
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preResult2.Context.ProtectDomainList ?? []],
TunProtectSsPort = tunProtectSsPort,
ProxyRelaySsPort = proxyRelaySsPort,
}
};
return null;
}
/// <summary>

View file

@ -1416,20 +1416,7 @@ public static class ConfigHandler
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
{
ProfileItem? itemSocks = null;
if (node.ConfigType != EConfigType.Custom
&& coreType != ECoreType.sing_box
&& config.TunModeItem.EnableTun
&& config.TunModeItem.EnableLegacyProtect)
{
itemSocks = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.SOCKS,
Address = Global.Loopback,
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
};
}
else if (node.ConfigType == EConfigType.Custom
if (node.ConfigType == EConfigType.Custom
&& node.PreSocksPort is > 0 and <= 65535)
{
var preCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;

View file

@ -17,9 +17,4 @@ public record CoreConfigContext
// TUN Compatibility
public bool IsTunEnabled { get; init; } = false;
public HashSet<string> ProtectDomainList { get; init; } = new();
// -> tun inbound --(if routing proxy)--> relay outbound
// -> proxy core (relay inbound --> proxy outbound --(dialerProxy)--> protect outbound)
// -> protect inbound -> direct proxy outbound data -> internet
public int TunProtectSsPort { get; init; } = 0;
public int ProxyRelaySsPort { get; init; } = 0;
}

View file

@ -75,6 +75,18 @@ public class Inboundsettings4Ray
public bool? allowTransparent { get; set; }
public List<AccountsItem4Ray>? accounts { get; set; }
public string? name { get; set; }
public List<int>? MTU { get; set; }
public List<string>? gateway { get; set; }
public List<string>? autoSystemRoutingTable { get; set; }
public string? autoOutboundsInterface { get; set; }
// public List<string>? dns { get; set; }
}
public class UsersItem4Ray
@ -509,6 +521,8 @@ public class AccountsItem4Ray
public class Sockopt4Ray
{
public string? dialerProxy { get; set; }
[JsonPropertyName("interface")]
public string? Interface { get; set; }
}
public class FragmentItem4Ray

View file

@ -0,0 +1,26 @@
{
"tag": "tun",
"protocol": "tun",
"settings": {
"name": "xray_tun",
"MTU": [
9000
],
"gateway": [
"172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
"autoSystemRoutingTable": [
"0.0.0.0/0",
"::/0"
],
"autoOutboundsInterface": "auto"
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
]
}
}

View file

@ -0,0 +1,14 @@
[
{
"network": "udp",
"port": "135,137-139,5353",
"outboundTag": "block"
},
{
"ip": [
"224.0.0.0/3",
"ff00::/8"
],
"outboundTag": "block"
}
]

View file

@ -38,6 +38,8 @@
<EmbeddedResource Include="Sample\SampleHttpResponse" />
<EmbeddedResource Include="Sample\SampleInbound" />
<EmbeddedResource Include="Sample\SampleOutbound" />
<EmbeddedResource Include="Sample\SampleTunInbound" />
<EmbeddedResource Include="Sample\SampleTunRules" />
<EmbeddedResource Include="Sample\SingboxSampleClientConfig" />
<EmbeddedResource Include="Sample\SingboxSampleOutbound" />
<EmbeddedResource Include="Sample\tun_singbox_dns" />

View file

@ -61,52 +61,6 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ret.Success = true;
ret.Data = ApplyFullConfigTemplate();
if (!context.AppConfig.TunModeItem.EnableLegacyProtect
&& context.TunProtectSsPort is > 0 and <= 65535)
{
var ssInbound = new
{
type = "shadowsocks",
tag = "tun-protect-ss",
listen = Global.Loopback,
listen_port = context.TunProtectSsPort,
method = "none",
password = "none",
};
var directRule = new Rule4Sbox()
{
inbound = new List<string> { ssInbound.tag },
outbound = Global.DirectTag,
};
var singboxConfigNode = JsonUtils.ParseJson(ret.Data.ToString())!.AsObject();
var inboundsNode = singboxConfigNode["inbounds"]!.AsArray();
inboundsNode.Add(JsonUtils.SerializeToNode(ssInbound, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}));
var routeNode = singboxConfigNode["route"]?.AsObject();
var rulesNode = routeNode?["rules"]?.AsArray();
var protectRuleNode = JsonUtils.SerializeToNode(directRule,
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
if (rulesNode != null)
{
rulesNode.Insert(0, protectRuleNode);
}
else
{
var newRulesNode = new JsonArray() { protectRuleNode };
if (routeNode is null)
{
var newRouteNode = new JsonObject() { ["rules"] = newRulesNode };
singboxConfigNode["route"] = newRouteNode;
}
else
{
routeNode["rules"] = newRulesNode;
}
}
ret.Data = JsonUtils.Serialize(singboxConfigNode);
}
return ret;
}
catch (Exception ex)

View file

@ -345,14 +345,6 @@ public partial class CoreConfigSingboxService
{
try
{
// The synthetic TUN relay outbound talks to the local Xray shadowsocks relay.
// Xray cannot terminate sing-box h2mux, so muxing here turns local relay traffic
// into sp.mux.sing-box.arpa pseudo-destinations and breaks DNS over TUN.
if (IsTunRelayProxyOutbound())
{
return;
}
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
{
@ -372,21 +364,6 @@ public partial class CoreConfigSingboxService
}
}
private bool IsTunRelayProxyOutbound()
{
if (!context.IsTunEnabled
|| _node.ConfigType != EConfigType.Shadowsocks
|| _node.Address != Global.Loopback
|| _node.Port != context.ProxyRelaySsPort
|| _node.Password != Global.None)
{
return false;
}
var protocolExtra = _node.GetProtocolExtra();
return protocolExtra.SsMethod == Global.None;
}
private void FillOutboundTls(Outbound4Sbox outbound)
{
try

View file

@ -15,13 +15,6 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
var ret = new RetResult();
try
{
if (!context.AppConfig.TunModeItem.EnableLegacyProtect
&& context.IsTunEnabled
&& context.TunProtectSsPort is > 0 and <= 65535
&& context.ProxyRelaySsPort is > 0 and <= 65535)
{
return GenerateClientProxyRelayConfig();
}
if (_node == null
|| !_node.IsValid())
{
@ -269,140 +262,5 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
}
}
public RetResult GenerateClientProxyRelayConfig()
{
var ret = new RetResult();
try
{
if (_node == null
|| !_node.IsValid())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (_node.GetNetwork() is nameof(ETransport.quic))
{
ret.Msg = ResUI.Incorrectconfiguration + $" - {_node.GetNetwork()}";
return ret;
}
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
if (result.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
_coreConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (_coreConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
GenLog();
_coreConfig.outbounds.Clear();
GenOutbounds();
GenStatistic();
var protectNode = new ProfileItem()
{
CoreType = ECoreType.Xray,
ConfigType = EConfigType.Shadowsocks,
Address = Global.Loopback,
Port = context.TunProtectSsPort,
Password = Global.None,
};
protectNode.SetProtocolExtra(protectNode.GetProtocolExtra() with
{
SsMethod = Global.None,
});
const string protectTag = "tun-protect-ss";
foreach (var outbound in _coreConfig.outbounds
.Where(o => o.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
{
outbound.streamSettings ??= new();
outbound.streamSettings.sockopt ??= new();
outbound.streamSettings.sockopt.dialerProxy = protectTag;
}
// ech protected
foreach (var outbound in _coreConfig.outbounds
.Where(outbound => outbound.streamSettings?.tlsSettings?.echConfigList?.IsNullOrEmpty() == false))
{
outbound.streamSettings!.tlsSettings!.echSockopt ??= new();
outbound.streamSettings.tlsSettings.echSockopt.dialerProxy = protectTag;
}
// xhttp download protected
foreach (var outbound in _coreConfig.outbounds
.Where(o => o.streamSettings?.xhttpSettings?.extra is not null))
{
var xhttpExtra = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings.xhttpSettings!.extra));
if (xhttpExtra is not JsonObject xhttpExtraObject
|| xhttpExtraObject["downloadSettings"] is not JsonObject downloadSettings)
{
continue;
}
// dialerProxy
var sockopt = downloadSettings["sockopt"] as JsonObject ?? new JsonObject();
sockopt["dialerProxy"] = protectTag;
downloadSettings["sockopt"] = sockopt;
// ech protected
if (downloadSettings["tlsSettings"] is JsonObject tlsSettings
&& tlsSettings["echConfigList"] is not null)
{
tlsSettings["echSockopt"] = new JsonObject
{
["dialerProxy"] = protectTag
};
}
outbound.streamSettings.xhttpSettings.extra = xhttpExtraObject;
}
_coreConfig.outbounds.Add(new CoreConfigV2rayService(context with
{
Node = protectNode,
}).BuildProxyOutbound(protectTag));
_coreConfig.routing.rules ??= [];
var hasBalancer = _coreConfig.routing.balancers is { Count: > 0 };
_coreConfig.routing.rules.Add(new()
{
inboundTag = ["proxy-relay-ss"],
outboundTag = hasBalancer ? null : Global.ProxyTag,
balancerTag = hasBalancer ? Global.ProxyTag + Global.BalancerTagSuffix : null,
type = "field"
});
//_coreConfig.inbounds.Clear();
var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!;
configNode["inbounds"]!.AsArray().Add(new
{
listen = Global.Loopback,
port = context.ProxyRelaySsPort,
protocol = "shadowsocks",
settings = new
{
network = "tcp,udp",
method = Global.None,
password = Global.None,
},
tag = "proxy-relay-ss",
});
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = JsonUtils.Serialize(configNode);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
#endregion public gen function
}

View file

@ -7,36 +7,61 @@ public partial class CoreConfigV2rayService
try
{
var listen = "0.0.0.0";
var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
_coreConfig.inbounds = [];
var inbound = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks, true);
_coreConfig.inbounds.Add(inbound);
if (_config.Inbound.First().SecondLocalPortEnabled)
if (!context.IsTunEnabled
|| (context.IsTunEnabled && _node.Port != listenPort))
{
var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true);
_coreConfig.inbounds.Add(inbound2);
}
_coreConfig.inbounds.Add(inbound);
if (_config.Inbound.First().AllowLANConn)
{
if (_config.Inbound.First().NewPort4LAN)
if (_config.Inbound.First().SecondLocalPortEnabled)
{
var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true);
inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3);
var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true);
_coreConfig.inbounds.Add(inbound2);
}
//auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
if (_config.Inbound.First().AllowLANConn)
{
if (_config.Inbound.First().NewPort4LAN)
{
inbound3.settings.auth = "password";
inbound3.settings.accounts = new List<AccountsItem4Ray> { new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } };
var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true);
inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3);
//auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
{
inbound3.settings.auth = "password";
inbound3.settings.accounts = new List<AccountsItem4Ray>
{
new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass }
};
}
}
else
{
inbound.listen = listen;
}
}
else
}
if (context.IsTunEnabled)
{
if (_config.TunModeItem.Mtu <= 0)
{
inbound.listen = listen;
_config.TunModeItem.Mtu = Global.TunMtus.First();
}
var tunInbound = JsonUtils.Deserialize<Inbounds4Ray>(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? new Inbounds4Ray { };
tunInbound.settings.name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "xray_tun";
tunInbound.settings.MTU = [_config.TunModeItem.Mtu];
if (_config.TunModeItem.EnableIPv6Address == false)
{
tunInbound.settings.gateway = ["172.18.0.1/30"];
}
tunInbound.sniffing = inbound.sniffing;
_coreConfig.inbounds.Add(tunInbound);
}
}
catch (Exception ex)

View file

@ -12,6 +12,10 @@ public partial class CoreConfigV2rayService
GenObservatory(multipleLoad);
GenBalancer(multipleLoad);
}
if (context.IsTunEnabled)
{
_coreConfig.outbounds.Add(BuildDnsOutbound());
}
}
private List<Outbounds4Ray> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag)
@ -824,4 +828,10 @@ public partial class CoreConfigV2rayService
}
}
}
private static Outbounds4Ray BuildDnsOutbound()
{
var outbound = new Outbounds4Ray { tag = Global.DnsOutboundTag, protocol = "dns", };
return outbound;
}
}

View file

@ -6,6 +6,31 @@ public partial class CoreConfigV2rayService
{
try
{
if (_config.TunModeItem.EnableTun)
{
var tunRules = JsonUtils.Deserialize<List<RulesItem4Ray>>(EmbedUtils.GetEmbedText(Global.V2raySampleTunRules));
if (tunRules != null)
{
_coreConfig.routing.rules.AddRange(tunRules);
}
var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe();
_coreConfig.routing.rules.Add(new()
{
port = "53",
process = lstDnsExe,
outboundTag = Global.DnsOutboundTag,
});
_coreConfig.routing.rules.Add(new()
{
process = lstDirectExe,
outboundTag = Global.DirectTag,
});
_coreConfig.routing.rules.Add(new()
{
port = "53",
outboundTag = Global.DnsOutboundTag,
});
}
if (_coreConfig.routing?.rules != null)
{
_coreConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy;
@ -205,4 +230,37 @@ public partial class CoreConfigV2rayService
}
return finalRule;
}
private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe()
{
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo();
foreach (var coreConfig in coreInfoResult)
{
if (coreConfig.CoreType == ECoreType.v2rayN)
{
continue;
}
foreach (var baseExeName in coreConfig.CoreExes)
{
if (coreConfig.CoreType != ECoreType.sing_box)
{
dnsExeSet.Add(Utils.GetExeName(baseExeName));
}
directExeSet.Add(Utils.GetExeName(baseExeName));
}
}
directExeSet.Add("xray/");
directExeSet.Add("self/");
var lstDnsExe = new List<string>(dnsExeSet);
var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
}
}