Compare commits

...

15 Commits

Author SHA1 Message Date
liuclare
e01717d832 Narrow the TUN self-address drop rule to single addresses (#9935)
The rule added in #9897 takes the TUN inbound's `address` verbatim as `ip_cidr`,
so a /30 or /126 interface prefix becomes the match range.

sing-tun derives the TUN's DNS entry from the address right after the interface's
own and hands it to the system resolver: Windows through luid.SetDNS in
tun_windows.go, Linux through systemd-resolved in tun_linux.go, both guarded only
by AutoRoute && !EXP_DisableDNSHijack. HasNextAddress keeps that address inside
the interface prefix, every preset in Global.TunIPv4Address is a /30 and every
IPv6 preset a /126, and the sing-box system stack rejects single-address
prefixes, so there is no configuration where it falls outside.

Queries from the system resolver then hit the drop rule and time out with no
response and no ICMP. Name resolution fails for the whole system while the proxy
path itself stays healthy, which makes it read as a DNS outage rather than a
routing rule. Reported in #9934 and #9926.

Matching each address on its own keeps what #9897 set out to block - the loop it
diagnosed was addressed to the interface address itself - and leaves the DNS
entry to sing-box.

Also restores the two regression tests #9897 came with, removed by eff58459
(#9817) while its implementation and template fix stayed in place.
ShouldRejectTrafficToTunOwnAddresses now asserts the single-address form and
additionally pins the prefix length, so it covers both the loop it was written
for and the resolver address it must not cover.

Verified on Linux by running sing-box directly from a generated config, changing
only this rule's prefix length between runs:

  ip_cidr ["172.18.0.1/30"]   getent hosts www.google.com -> empty, 3/3
  ip_cidr ["172.18.0.1/32"]   getent hosts www.google.com -> resolved, 3/3

dig against a public resolver, naked-IP HTTPS and the local mixed port were
unaffected in both runs. End to end, a build of this branch emits
drop ip_cidr ["172.18.0.1/32"] and system resolution works while its TUN is up.

Co-authored-by: liuclare <177657698+liuclare@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:09:04 +08:00
DHR60
8b4063b44b Set tun "route only" to true (#9933) 2026-08-09 20:53:00 +08:00
DHR60
3136f573dd Revert "Fix Windows TUN restart issue (#9925)" (#9932)
This reverts commit f347fe1c03.
2026-08-09 20:52:36 +08:00
liuclare
e101b1d7b0 Always route IPv6 into Xray TUN regardless of EnableIPv6Address (#9930)
* Always route IPv6 into Xray TUN regardless of EnableIPv6Address

EnableIPv6Address controls whether the TUN interface is assigned an IPv6
address, but it also gated whether ::/0 was added to autoSystemRoutingTable.
With the default (false), IPv6 had no route pointing at the TUN device and
followed the system default route instead, leaving the tunnel unproxied and
exposing the host's real IPv6 address.

The embedded template SampleTunInbound already declares both families; the
generated config discarded it. #9843 restored ::/0 only inside the
EnableIPv6Address == true branch, so the false branch still leaks.

Route both families unconditionally and let the option control only the
interface address. The same conditional existed a second time in the
RouteExcludeAddress branch and is fixed as well.

Fixes #9929

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Add regression tests for IPv6 routing in the Xray TUN inbound

Both assertions fail on 31044f44 and pass with the fix:

  Tun_ShouldRouteIPv6IntoTunnel(enableIPv6Address: False)
    Expected collection {"0.0.0.0/0"} to contain "::/0".

  TunRouteExcludeAddress_ShouldIncludeIPv6Ranges
    Expected collection {...44 IPv4 ranges...} to have an item matching x.Contains(:).

The theory also covers enableIPv6Address: true, which passes on both revisions,
so the tests only fail while the defect is present. The gateway count assertion
pins the intended split of responsibilities: EnableIPv6Address governs the
interface address, never the routing table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: liuclare <177657698+liuclare@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:15:18 +08:00
2dust
31044f449d up 7.24.6 2026-08-08 20:38:18 +08:00
2dust
eacf8b0784 Reuse inbound config and fix tun sniffing flag
https://github.com/2dust/v2rayN/issues/9921
2026-08-08 20:36:18 +08:00
Mangoo
f347fe1c03 Fix Windows TUN restart issue (#9925) 2026-08-08 20:20:41 +08:00
JieXu
71208eb711 Update build-windows-x86.yml (#9924) 2026-08-08 20:01:13 +08:00
tt2563
339eaa6bfd Update ResUI.zh-Hant.resx (#9923)
* Update ResUI.zh-Hant.resx

Update Traditional Chinese translation

* Fix typo in outbound/endpoint message
2026-08-08 19:58:55 +08:00
Miheichev Aleksandr Sergeevich
717ef7f0a7 i18n(ru): add missing translations and remove trailing periods (#9916)
* i18n(ru): add missing Russian translations

Translate the 10 strings missing from ResUI.ru.resx (proxy dial
resolution strategy, Happy Eyeballs, IPv4/IPv6 address labels, and
the custom outbound group). Russian now has full key parity with
ResUI.resx (580/580); placeholder consistency verified for all keys.

Technical terms and config values (outbound, endpoint, UseIP, Happy
Eyeballs) are kept untranslated, matching the zh-Hans locale.

* i18n(ru): remove trailing periods from translations

Align with the Russian UI convention of omitting sentence-final
periods in labels, tooltips and messages (32 values). Inner
punctuation and ellipses are kept; no wording changes.

* i18n(ru): translate TbFakeIPTips left in English

The key existed in ResUI.ru.resx but its value was the untranslated
English text. Translate it following the established glossary (FakeIP
and sing-box remain untranslated).
2026-08-07 14:19:59 +08:00
2dust
4a9d83f9dc Fix duplicated wording in UI resource strings
https://github.com/2dust/v2rayN/pull/9914
2026-08-07 14:13:32 +08:00
JieXu
1ac14d0903 Update package-osx.sh (#9913)
* Update Directory.Packages.props

* Update package-osx.sh
2026-08-07 14:07:44 +08:00
Miheichev Aleksandr Sergeevich
08d423e1b4 docs: link minimum OS requirements from README (#9912)
Replace the requirements table with a single bilingual line under
"Supported Platforms" pointing to the wiki page (Release files
introduction), per the review feedback in the PR: the README stays
minimal and the wiki remains the single source of truth.
2026-08-07 14:07:22 +08:00
2dust
9638b4c368 Fix custom config import
https://github.com/2dust/v2rayN/issues/9902
2026-08-06 09:29:33 +08:00
Miheichev Aleksandr Sergeevich
1034ce6e02 chore(deps): update NuGet dependencies (#9832)
* chore(deps): bump Avalonia.Desktop, CliWrap, Semi.Avalonia and Repobot.SQLite

- Avalonia.Desktop          12.1.0    -> 12.1.1
- CliWrap                   3.10.2    -> 3.10.4
- Semi.Avalonia             12.1.0    -> 12.1.0.1
- Semi.Avalonia.DataGrid    12.1.0    -> 12.1.0.1
- Repobot.SQLite.Unofficial 3.53.3.10 -> 3.53.4

Left unchanged on purpose:

- ReactiveUI.Avalonia: nuget.org reports 14.7.1 as the highest version, but it
  is unlisted and belongs to the old versioning line (netstandard2.0/net6.0/
  net7.0, Avalonia >= 11.0.0, ReactiveUI >= 19.4.1). Moving to it would revert
  both #9148 and #9678.
- SkiaSharp.NativeAssets.Linux: 4.151.0 is available, but Avalonia.Skia 12.1.1
  resolves the managed SkiaSharp to 3.119.4. Pairing 4.x native assets with a
  3.x managed binding risks native entry point failures on Linux; 3.119.4 is
  the latest release on the 3.x branch.

* chore(deps): bump ReactiveUI to 24.1.0 and ReactiveUI.SourceGenerators to 3.2.0

- ReactiveUI                  24.0.0 -> 24.1.0
- ReactiveUI.WPF              24.0.0 -> 24.1.0
- ReactiveUI.SourceGenerators 3.1.0  -> 3.2.0

ReactiveUI and ReactiveUI.WPF move together: ReactiveUI.WPF 24.1.0 requires
ReactiveUI >= 24.1.0. ReactiveUI.Avalonia 12.1.0 declares a ReactiveUI >= 24.0.0
minimum, so it resolves against 24.1.0 without changes; ReactiveUI.Primitives
(7.1.0) and Splat (20.2.0) are unaffected.

* chore(deps): bump Avalonia.Controls.DataGrid and ReactiveUI.Avalonia

- Avalonia.Controls.DataGrid 12.1.0 -> 12.1.2
- ReactiveUI.Avalonia        12.1.0 -> 12.1.1

ReactiveUI.Avalonia 12.1.1 requires Avalonia >= 12.1.1 and ReactiveUI >= 24.1.0,
both of which are already in place after the previous commits on this branch.
2026-08-06 08:24:21 +08:00
19 changed files with 300 additions and 92 deletions

View File

@@ -146,7 +146,7 @@ jobs:
$xrayTag = "v$xrayVer" $xrayTag = "v$xrayVer"
$singTag = "v$singVer" $singTag = "v$singVer"
$xrayUrl = "https://github.com/XTLS/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip" $xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip" $singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer" Write-Host "Bundled Xray version: $xrayVer"

View File

@@ -50,6 +50,8 @@ Read the Wiki for usage guides and configuration details.
| Linux | ✅ | - | ✅ | ✅ | ✅ | | Linux | ✅ | - | ✅ | ✅ | ✅ |
| macOS | ✅ | - | ✅ | - | - | | macOS | ✅ | - | ✅ | - | - |
Minimum OS requirements: [Release files introduction](https://github.com/2dust/v2rayN/wiki/Release-files-introduction) / 最低系统要求:[发布文件介绍](https://github.com/2dust/v2rayN/wiki/Release-files-introduction)
--- ---
## GPG Verification / GPG 签名校验 ## GPG Verification / GPG 签名校验

View File

@@ -54,7 +54,7 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.7</string> <string>13.6</string>
</dict> </dict>
</plist> </plist>
EOF EOF

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.24.5</Version> <Version>7.24.6</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -6,28 +6,28 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.0" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" /> <PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" /> <PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.0" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
<PackageVersion Include="CliWrap" Version="3.10.2" /> <PackageVersion Include="CliWrap" Version="3.10.4" />
<PackageVersion Include="Downloader" Version="5.9.5" /> <PackageVersion Include="Downloader" Version="5.9.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" /> <PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="24.0.0" /> <PackageVersion Include="ReactiveUI" Version="24.1.0" />
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.1.0" /> <PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="24.0.0" /> <PackageVersion Include="ReactiveUI.WPF" Version="24.1.0" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0" /> <PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
<PackageVersion Include="NLog" Version="6.1.4" /> <PackageVersion Include="NLog" Version="6.1.4" />
<PackageVersion Include="sqlite-net-e" Version="1.11.285" /> <PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3.10" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
@@ -36,4 +36,4 @@
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" /> <PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -251,4 +251,12 @@ internal static class CoreConfigTestFactory
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"]; config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
return config; return config;
} }
public static Config CreateConfigWithTun(ECoreType coreType, bool enableIPv6Address)
{
var config = CreateConfig(coreType);
config.TunModeItem.EnableTun = true;
config.TunModeItem.EnableIPv6Address = enableIPv6Address;
return config;
}
} }

View File

@@ -56,6 +56,85 @@ public class CoreConfigSingboxServiceTests
cfg.inbounds.Should().Contain(i => i.type == "tun"); cfg.inbounds.Should().Contain(i => i.type == "tun");
} }
[Fact]
public void GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
{
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
// embedded template makes JsonUtils.Deserialize return null and silently
// drops every one of them.
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.TunModeItem.EnableTun = 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())!;
cfg.route.rules.Should().Contain(
r => r.action == "reject"
&& r.network != null && r.network.Contains("udp")
&& r.port != null && r.port.Contains(5353),
"the embedded tun rules must reject mDNS/NetBIOS noise");
cfg.route.rules.Should().Contain(
r => r.action == "reject"
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
"the embedded tun rules must reject multicast traffic");
}
[Fact]
public void GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
{
// Regression test: traffic addressed to the TUN interface's own addresses must
// never reach an outbound. auto_route hijacks the default route, so `direct`
// writes such a packet straight back into the TUN, which routes it to the
// outbound again - an infinite loop that pins a CPU core. Observed in the wild
// with WebRTC ICE connectivity checks against the TUN's own fc00::/7 ULA
// address, sustaining ~8k packets/s out of the TUN interface.
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.TunModeItem.EnableTun = true;
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 self = IPAddress.Parse(address.Split('/').First());
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
var expected = $"{self}/{hostBits}";
cfg.route.rules.Should().Contain(
r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected),
$"traffic to the TUN's own address '{address}' must be rejected, not routed");
}
// The match has to stay on the addresses themselves. sing-tun derives the TUN's DNS
// entry from the address right after the interface's own, and every prefix offered
// here leaves room for it, so a prefix match would drop system name lookups too.
var dropRule = cfg.route.rules.First(r =>
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
dropRule.ip_cidr!.Should().OnlyContain(c =>
c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
}
[Fact] [Fact]
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface() public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
{ {

View File

@@ -570,6 +570,51 @@ public class CoreConfigV2rayServiceTests
directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4"); directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4");
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
{
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
// EnableIPv6Address governs the interface address only, never the routing table.
tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
}
[Fact]
public void GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
{
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
config.TunModeItem.EnableIPv6Address = false;
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
tunInbound.Should().NotBeNull();
tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
}
[Fact] [Fact]
public void GenerateClientConfigContent_TunRouteExcludeAddress() public void GenerateClientConfigContent_TunRouteExcludeAddress()
{ {

View File

@@ -753,11 +753,11 @@ public class Utils
return false; return false;
} }
public static int GetFreePort(int defaultPort = 0) public static int GetFreePort(int defaultPort)
{ {
try try
{ {
if (!(defaultPort == 0 || Utils.PortInUse(defaultPort))) if (!PortInUse(defaultPort))
{ {
return defaultPort; return defaultPort;
} }

View File

@@ -586,7 +586,6 @@ public static class ConfigHandler
return 0; return 0;
} }
public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true) public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true)
{ {
var fileName = profileItem.Address; var fileName = profileItem.Address;
@@ -1739,8 +1738,16 @@ public static class ConfigHandler
SubItem? subItem) SubItem? subItem)
{ {
var subRemarks = subItem?.Remarks; var subRemarks = subItem?.Remarks;
// Safe Mode: Only allow full configuration if it's not from a subscription // Prioritize using complete custom parsing, followed by custom outbound parsing.
var lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks); var lstProfiles = V2rayFmt.ResolveToCustom(strData, subRemarks);
if (lstProfiles.Count == 0)
{
lstProfiles = SingboxFmt.ResolveToCustom(strData, subRemarks);
}
if (lstProfiles.Count == 0)
{
lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
}
if (lstProfiles.Count == 0) if (lstProfiles.Count == 0)
{ {
lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks); lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
@@ -1750,7 +1757,7 @@ public static class ConfigHandler
return -1; return -1;
} }
var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub); var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
if (count > 0) if (count > 0)
{ {
return count; return count;
@@ -1786,7 +1793,7 @@ public static class ConfigHandler
var subRemarks = subItem.Remarks; var subRemarks = subItem.Remarks;
var customCoreType = subItem.CustomCoreType!.Value; var customCoreType = subItem.CustomCoreType!.Value;
List<ProfileItem>? lstProfiles = customCoreType switch var lstProfiles = customCoreType switch
{ {
ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks), ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks),
ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks), ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks),
@@ -1800,7 +1807,7 @@ public static class ConfigHandler
return -1; return -1;
} }
var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub); var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
if (count > 0) if (count > 0)
{ {
return count; return count;
@@ -1810,7 +1817,7 @@ public static class ConfigHandler
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType); return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
} }
private static async Task<int> AddCustomOutboundServers( private static async Task<int> AddBatchCustomServers(
Config config, Config config,
List<ProfileItem> lstProfiles, List<ProfileItem> lstProfiles,
string subid, string subid,
@@ -1821,9 +1828,20 @@ public static class ConfigHandler
{ {
it.Subid = subid; it.Subid = subid;
it.IsSub = isSub; it.IsSub = isSub;
if (await AddCustomOutboundServer(config, it, true) == 0)
if (it.ConfigType == EConfigType.Custom)
{ {
count++; if (await AddCustomServer(config, it, true) == 0)
{
count++;
}
}
else
{
if (await AddCustomOutboundServer(config, it, true) == 0)
{
count++;
}
} }
} }
return count; return count;

View File

@@ -76,6 +76,7 @@ public class SingboxFmt : BaseFmt
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject)); var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem var profileItem = new ProfileItem
{ {
ConfigType = EConfigType.Custom,
CoreType = ECoreType.sing_box, CoreType = ECoreType.sing_box,
Address = fileName, Address = fileName,
Remarks = subRemarks ?? "singbox_custom", Remarks = subRemarks ?? "singbox_custom",

View File

@@ -77,6 +77,7 @@ public class V2rayFmt : BaseFmt
var profileItem = new ProfileItem var profileItem = new ProfileItem
{ {
ConfigType = EConfigType.Custom,
CoreType = ECoreType.Xray, CoreType = ECoreType.Xray,
Address = fileName, Address = fileName,
Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom", Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom",

View File

@@ -178,7 +178,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Failed to import custom configuration Configuration 的本地化字符串。 /// 查找类似 Failed to import configuration 的本地化字符串。
/// </summary> /// </summary>
public static string FailedImportedCustomServer { public static string FailedImportedCustomServer {
get { get {
@@ -268,7 +268,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Please browse to import Configuration configuration 的本地化字符串。 /// 查找类似 Please browse to import configuration 的本地化字符串。
/// </summary> /// </summary>
public static string FillServerAddressCustom { public static string FillServerAddressCustom {
get { get {
@@ -2653,7 +2653,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Custom configuration Configuration imported successfully 的本地化字符串。 /// 查找类似 Custom configuration imported successfully 的本地化字符串。
/// </summary> /// </summary>
public static string SuccessfullyImportedCustomServer { public static string SuccessfullyImportedCustomServer {
get { get {

View File

@@ -142,7 +142,7 @@
<value>Failed to get the default configuration</value> <value>Failed to get the default configuration</value>
</data> </data>
<data name="FailedImportedCustomServer" xml:space="preserve"> <data name="FailedImportedCustomServer" xml:space="preserve">
<value>Failed to import custom configuration Configuration</value> <value>Failed to import configuration</value>
</data> </data>
<data name="FailedReadConfiguration" xml:space="preserve"> <data name="FailedReadConfiguration" xml:space="preserve">
<value>Failed to read configuration file</value> <value>Failed to read configuration file</value>
@@ -283,7 +283,7 @@
<value>Configuration successful. {0}</value> <value>Configuration successful. {0}</value>
</data> </data>
<data name="SuccessfullyImportedCustomServer" xml:space="preserve"> <data name="SuccessfullyImportedCustomServer" xml:space="preserve">
<value>Custom configuration Configuration imported successfully</value> <value>Custom configuration imported successfully</value>
</data> </data>
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve"> <data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
<value>{0} Configurations have been imported from clipboard</value> <value>{0} Configurations have been imported from clipboard</value>
@@ -385,7 +385,7 @@
<value>All</value> <value>All</value>
</data> </data>
<data name="FillServerAddressCustom" xml:space="preserve"> <data name="FillServerAddressCustom" xml:space="preserve">
<value>Please browse to import Configuration configuration</value> <value>Please browse to import configuration</value>
</data> </data>
<data name="Speedtesting" xml:space="preserve"> <data name="Speedtesting" xml:space="preserve">
<value>Testing...</value> <value>Testing...</value>
@@ -1860,4 +1860,4 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="LvCustomCoreType" xml:space="preserve"> <data name="LvCustomCoreType" xml:space="preserve">
<value>Custom config core</value> <value>Custom config core</value>
</data> </data>
</root> </root>

View File

@@ -268,7 +268,7 @@
<value>Сначала выберите сервер</value> <value>Сначала выберите сервер</value>
</data> </data>
<data name="RemoveDuplicateServerResult" xml:space="preserve"> <data name="RemoveDuplicateServerResult" xml:space="preserve">
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}.</value> <value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}</value>
</data> </data>
<data name="RemoveServer" xml:space="preserve"> <data name="RemoveServer" xml:space="preserve">
<value>Вы уверены, что хотите удалить сервер?</value> <value>Вы уверены, что хотите удалить сервер?</value>
@@ -1048,7 +1048,7 @@
<value>HTTP-заголовки</value> <value>HTTP-заголовки</value>
</data> </data>
<data name="TipHttpOutboundHeaders" xml:space="preserve"> <data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк.</value> <value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк</value>
</data> </data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Псевдоним предыдущего прокси</value> <value>Псевдоним предыдущего прокси</value>
@@ -1120,28 +1120,28 @@
<value>Длина фрагмента</value> <value>Длина фрагмента</value>
</data> </data>
<data name="TbSettingsFragmentLengthTip" xml:space="preserve"> <data name="TbSettingsFragmentLengthTip" xml:space="preserve">
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию.</value> <value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию</value>
</data> </data>
<data name="TbSettingsFragmentInterval" xml:space="preserve"> <data name="TbSettingsFragmentInterval" xml:space="preserve">
<value>Интервал фрагментации</value> <value>Интервал фрагментации</value>
</data> </data>
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve"> <data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию.</value> <value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию</value>
</data> </data>
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve"> <data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
<value>Макс. разделение</value> <value>Макс. разделение</value>
</data> </data>
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve"> <data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию.</value> <value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию</value>
</data> </data>
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve"> <data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
<value>Резервная задержка</value> <value>Резервная задержка</value>
</data> </data>
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve"> <data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию.</value> <value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию</value>
</data> </data>
<data name="FillFragmentParameterError" xml:space="preserve"> <data name="FillFragmentParameterError" xml:space="preserve">
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100).</value> <value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100)</value>
</data> </data>
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve"> <data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
<value>Включить файл кэша для sing-box (файлы наборов правил)</value> <value>Включить файл кэша для sing-box (файлы наборов правил)</value>
@@ -1351,7 +1351,7 @@
<value>Пароль sudo системы</value> <value>Пароль sudo системы</value>
</data> </data>
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve"> <data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска.</value> <value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска</value>
</data> </data>
<data name="TransportHeaderType5" xml:space="preserve"> <data name="TransportHeaderType5" xml:space="preserve">
<value>XHTTP-режим</value> <value>XHTTP-режим</value>
@@ -1411,7 +1411,7 @@
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value> <value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
</data> </data>
<data name="SudoIncorrectPasswordTip" xml:space="preserve"> <data name="SudoIncorrectPasswordTip" xml:space="preserve">
<value>Неверный пароль, попробуйте ещё раз.</value> <value>Неверный пароль, попробуйте ещё раз</value>
</data> </data>
<data name="TbMldsa65Verify" xml:space="preserve"> <data name="TbMldsa65Verify" xml:space="preserve">
<value>Mldsa65Verify</value> <value>Mldsa65Verify</value>
@@ -1492,7 +1492,7 @@
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value> <value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
</data> </data>
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value> <value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve"> <data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Начинается разбор и обработка содержимого подписки</value> <value>Начинается разбор и обработка содержимого подписки</value>
@@ -1501,7 +1501,7 @@
<value>Выбрать профиль</value> <value>Выбрать профиль</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value> <value>Применяется глобально по умолчанию; встроенная фильтрация FakeIP есть только в sing-box</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Добавьте хотя бы одну конфигурацию</value> <value>Добавьте хотя бы одну конфигурацию</value>
@@ -1588,7 +1588,7 @@
<value>Привязанный сертификат (заполните любое из полей) <value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится. При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value> Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Получить сертификат</value> <value>Получить сертификат</value>
@@ -1642,10 +1642,10 @@
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value> <value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
</data> </data>
<data name="TbDirectResolveStrategyTips" xml:space="preserve"> <data name="TbDirectResolveStrategyTips" xml:space="preserve">
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль.</value> <value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль</value>
</data> </data>
<data name="TbRemoteResolveStrategyTips" xml:space="preserve"> <data name="TbRemoteResolveStrategyTips" xml:space="preserve">
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль.</value> <value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль</value>
</data> </data>
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Интервал смены портов (Port Hopping)</value> <value>Интервал смены портов (Port Hopping)</value>
@@ -1660,37 +1660,37 @@
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value> <value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
</data> </data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve"> <data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел.</value> <value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел</value>
</data> </data>
<data name="MsgGroupCycleDependency" xml:space="preserve"> <data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен.</value> <value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен</value>
</data> </data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve"> <data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value> <value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
</data> </data>
<data name="MsgGroupChildNodeError" xml:space="preserve"> <data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен.</value> <value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен</value>
</data> </data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve"> <data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value> <value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
</data> </data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve"> <data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен.</value> <value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен</value>
</data> </data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve"> <data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>У группы {0} нет допустимых дочерних узлов.</value> <value>У группы {0} нет допустимых дочерних узлов</value>
</data> </data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve"> <data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел.</value> <value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел</value>
</data> </data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve"> <data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел.</value> <value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел</value>
</data> </data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve"> <data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Предыдущий прокси подписки {0} не найден. Пропущено.</value> <value>Предыдущий прокси подписки {0} не найден. Пропущено</value>
</data> </data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve"> <data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Следующий прокси подписки {0} не найден. Пропущено.</value> <value>Следующий прокси подписки {0} не найден. Пропущено</value>
</data> </data>
<data name="menuGenGroupServer" xml:space="preserve"> <data name="menuGenGroupServer" xml:space="preserve">
<value>Сгенерировать группу политик</value> <value>Сгенерировать группу политик</value>
@@ -1756,7 +1756,7 @@
<value>Привязать интерфейс</value> <value>Привязать интерфейс</value>
</data> </data>
<data name="TbSettingsBindInterfaceTip" xml:space="preserve"> <data name="TbSettingsBindInterfaceTip" xml:space="preserve">
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value> <value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN</value>
</data> </data>
<data name="TbPreSharedKey" xml:space="preserve"> <data name="TbPreSharedKey" xml:space="preserve">
<value>Общий ключ (PSK)</value> <value>Общий ключ (PSK)</value>
@@ -1780,13 +1780,13 @@
<value>Доступно обновление</value> <value>Доступно обновление</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve"> <data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя.</value> <value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя</value>
</data> </data>
<data name="TbRouteExcludeAddress" xml:space="preserve"> <data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Адреса, исключаемые из маршрутизации</value> <value>Адреса, исключаемые из маршрутизации</value>
</data> </data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve"> <data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Разделяйте запятыми (,).</value> <value>Разделяйте запятыми (,)</value>
</data> </data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve"> <data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value> <value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
@@ -1798,22 +1798,22 @@
<value>Включить финальную фрагментацию (Final Fragment)</value> <value>Включить финальную фрагментацию (Final Fragment)</value>
</data> </data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve"> <data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку.</value> <value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку</value>
</data> </data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve"> <data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS через Bridge</value> <value>DNS через Bridge</value>
</data> </data>
<data name="MsgInsecureConfiguration" xml:space="preserve"> <data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM).</value> <value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM)</value>
</data> </data>
<data name="TbHy2RealmUrl" xml:space="preserve"> <data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value> <value>Realm URL</value>
</data> </data>
<data name="InvalidHy2RealmUrl" xml:space="preserve"> <data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Некорректный Realm URL.</value> <value>Некорректный Realm URL</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve"> <data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Введите корректный JSON заголовков HTTP-запроса.</value> <value>Введите корректный JSON заголовков HTTP-запроса</value>
</data> </data>
<data name="TbHy2RealmUrlTip" xml:space="preserve"> <data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Формат: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value> <value>Формат: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
@@ -1822,12 +1822,42 @@
<value>Размер пакета Gecko (мин/макс)</value> <value>Размер пакета Gecko (мин/макс)</value>
</data> </data>
<data name="TbLegacyProtectTip" xml:space="preserve"> <data name="TbLegacyProtectTip" xml:space="preserve">
<value>Если включено, используется sing-box TUN; иначе — xray TUN.</value> <value>Если включено, используется sing-box TUN; иначе — xray TUN</value>
</data> </data>
<data name="TbRootCertificateProvider" xml:space="preserve"> <data name="TbRootCertificateProvider" xml:space="preserve">
<value>Поставщик корневых сертификатов</value> <value>Поставщик корневых сертификатов</value>
</data> </data>
<data name="TbRootCertificateProviderTip" xml:space="preserve"> <data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром.</value> <value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром</value>
</data>
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
<value>Стратегия разрешения DNS при подключении к прокси</value>
</data>
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
<value>Не рекомендуется; может вызвать петли маршрутизации</value>
</data>
<data name="TbEnableHappyEyeballs" xml:space="preserve">
<value>Включить Happy Eyeballs</value>
</data>
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
<value>Требуется стратегия UseIP. При включении одновременно устанавливаются соединения по IPv4 и IPv6 и автоматически выбирается более быстрый доступный путь</value>
</data>
<data name="TbIpv6Address" xml:space="preserve">
<value>Адрес IPv6</value>
</data>
<data name="TbIpv4Address" xml:space="preserve">
<value>Адрес IPv4</value>
</data>
<data name="menuAddCustomOutboundServer" xml:space="preserve">
<value>Добавить пользовательский outbound</value>
</data>
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
<value>Файл пользовательского outbound {0} не найден: {1}</value>
</data>
<data name="TbCustomOutboundTip" xml:space="preserve">
<value>Для xray/sing-box поддерживается только один outbound/endpoint</value>
</data>
<data name="LvCustomCoreType" xml:space="preserve">
<value>Ядро пользовательской конфигурации</value>
</data> </data>
</root> </root>

View File

@@ -1849,4 +1849,16 @@
<data name="TbIpv4Address" xml:space="preserve"> <data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 位址</value> <value>Ipv4 位址</value>
</data> </data>
</root> <data name="menuAddCustomOutboundServer" xml:space="preserve">
<value>新增自訂出站</value>
</data>
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
<value>自訂出站 {0} 的檔案未找到:{1}</value>
</data>
<data name="TbCustomOutboundTip" xml:space="preserve">
<value>僅支援 xray/sing-box 的單一 outbound/endpoint</value>
</data>
<data name="LvCustomCoreType" xml:space="preserve">
<value>自訂設定核心</value>
</data>
</root>

View File

@@ -21,26 +21,27 @@ public partial class CoreConfigSingboxService
}; };
_coreConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
var inboundConf = _config.Inbound.First();
inbound.listen_port = listenPort; inbound.listen_port = listenPort;
if (_config.Inbound.First().SecondLocalPortEnabled) if (inboundConf.SecondLocalPortEnabled)
{ {
var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true); var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true);
_coreConfig.inbounds.Add(inbound2); _coreConfig.inbounds.Add(inbound2);
} }
if (_config.Inbound.First().AllowLANConn) if (inboundConf.AllowLANConn)
{ {
if (_config.Inbound.First().NewPort4LAN) if (inboundConf.NewPort4LAN)
{ {
var inbound3 = BuildInbound(inbound, EInboundProtocol.socks3, true); var inbound3 = BuildInbound(inbound, EInboundProtocol.socks3, true);
inbound3.listen = listen; inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3); _coreConfig.inbounds.Add(inbound3);
//auth //auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) if (inboundConf.User.IsNotEmpty() && inboundConf.Pass.IsNotEmpty())
{ {
inbound3.users = new() { new() { username = _config.Inbound.First().User, password = _config.Inbound.First().Pass } }; inbound3.users = new() { new() { username = inboundConf.User, password = inboundConf.Pass } };
} }
} }
else else

View File

@@ -48,12 +48,18 @@ public partial class CoreConfigSingboxService
// packet straight back into the TUN, which hands it to the outbound again - // packet straight back into the TUN, which hands it to the outbound again -
// an infinite loop that pins a CPU core. Drop instead of rejecting so no // an infinite loop that pins a CPU core. Drop instead of rejecting so no
// ICMP unreachable is generated back towards the same addresses. // ICMP unreachable is generated back towards the same addresses.
//
// Match each address on its own, not the prefix it carries. On Linux sing-tun
// registers Inet4Address[0].Addr().Next() with systemd-resolved as a "~." DNS
// upstream, and every prefix offered here is a /30 or /126, so carrying the
// prefix through would cover that resolver address too and drop every system
// name lookup along with the loop.
var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address; var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address;
if (tunAddresses?.Count > 0) if (tunAddresses?.Count > 0)
{ {
_coreConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
ip_cidr = [.. tunAddresses], ip_cidr = [.. tunAddresses.Select(ToSingleAddressPrefix)],
action = "reject", action = "reject",
method = "drop", method = "drop",
}); });
@@ -284,6 +290,14 @@ public partial class CoreConfigSingboxService
} }
} }
private static string ToSingleAddressPrefix(string address)
{
var addr = address.Split('/').First();
return IPAddress.TryParse(addr, out var ip)
? $"{addr}/{(ip.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32)}"
: address;
}
private List<string> BuildRoutingDirectExe() private List<string> BuildRoutingDirectExe()
{ {
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

View File

@@ -9,37 +9,38 @@ public partial class CoreConfigV2rayService
var listen = "0.0.0.0"; var listen = "0.0.0.0";
var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
_coreConfig.inbounds = []; _coreConfig.inbounds = [];
var inbound = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks, true); var inboundConf = _config.Inbound.First();
var inbound = BuildInbound(inboundConf, EInboundProtocol.socks, true);
var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort; var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort;
if (!context.IsTunEnabled || !isUsingLocalMixedPort) if (!context.IsTunEnabled || !isUsingLocalMixedPort)
{ {
_coreConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
if (_config.Inbound.First().SecondLocalPortEnabled) if (inboundConf.SecondLocalPortEnabled)
{ {
var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true); var inbound2 = BuildInbound(inboundConf, EInboundProtocol.socks2, true);
_coreConfig.inbounds.Add(inbound2); _coreConfig.inbounds.Add(inbound2);
} }
if (_config.Inbound.First().AllowLANConn) if (inboundConf.AllowLANConn)
{ {
if (_config.Inbound.First().NewPort4LAN) if (inboundConf.NewPort4LAN)
{ {
var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true); var inbound3 = BuildInbound(inboundConf, EInboundProtocol.socks3, true);
inbound3.listen = listen; inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3); _coreConfig.inbounds.Add(inbound3);
// auth // auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) if (inboundConf.User.IsNotEmpty() && inboundConf.Pass.IsNotEmpty())
{ {
inbound3.settings.auth = "password"; inbound3.settings.auth = "password";
inbound3.settings.accounts = inbound3.settings.accounts =
[ [
new() new()
{ {
user = _config.Inbound.First().User, user = inboundConf.User,
pass = _config.Inbound.First().Pass, pass = inboundConf.Pass,
}, },
]; ];
@@ -66,20 +67,23 @@ public partial class CoreConfigV2rayService
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First(); var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.settings.gateway = [address]; tunInbound.settings.gateway = [address];
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"]; // Route both families into the tunnel regardless of EnableIPv6Address. That option only
// controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing
// table makes IPv6 follow the system default route and bypass the tunnel entirely.
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0", "::/0"];
if (_config.TunModeItem.EnableIPv6Address == true) if (_config.TunModeItem.EnableIPv6Address == true)
{ {
var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First(); var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.settings.gateway.Add(address6); tunInbound.settings.gateway.Add(address6);
tunInbound.settings.autoSystemRoutingTable.Add("::/0");
} }
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty()) if (!bindInterface.IsNullOrEmpty())
{ {
tunInbound.settings.autoOutboundsInterface = bindInterface; tunInbound.settings.autoOutboundsInterface = bindInterface;
} }
tunInbound.sniffing = inbound.sniffing; tunInbound.sniffing = inbound.sniffing;
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
tunInbound.sniffing.routeOnly = true; tunInbound.sniffing.routeOnly = true;
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 }) if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
@@ -117,15 +121,8 @@ public partial class CoreConfigV2rayService
includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList(); includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList();
includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList(); includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList();
if (_config.TunModeItem.EnableIPv6Address) tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
{ .Concat(includeListV6.Select(x => x.ToString())).ToList();
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString())
.Concat(includeListV6.Select(x => x.ToString())).ToList();
}
else
{
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList();
}
} }
_coreConfig.inbounds.Add(tunInbound); _coreConfig.inbounds.Add(tunInbound);