Compare commits

..

51 Commits

Author SHA1 Message Date
2dust
71645cbbce Add NetBridge process hijacking feature for windows
https://github.com/2dust/NetBridge
https://github.com/InterceptSuite/ProxyBridge
2026-06-09 15:37:39 +08:00
2dust
b1a400b393 binConfigs only deletes test files periodically 2026-06-08 14:22:11 +08:00
2dust
879c736924 Update Utils.cs
5fed566a32
2026-06-07 15:49:31 +08:00
sparklelcm333
8147b39723 fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移 (#9498)
* fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移

WindowStartupLocation="CenterScreen"(来自 XAML)能正确包含 chrome 居中窗口,
但只能使用生效时的 Width/Height。旧代码在 OnLoaded 中设置保存的尺寸为时已晚
(CenterScreen 已应用),随后又用不含 chrome 的手工计算覆盖 Position,
导致窗口每次启动偏右下。

修复方式:在 Initialized 事件中设置保存的 Width/Height。
Avalonia 中 Initialized 在 Show() 过程中触发,早于 WindowStartupLocation 生效,
因此 CenterScreen 能用正确的保存尺寸居中。

同时移除 OnLoaded 中重复的 Width/Height 设置(Initialized 中已设置)。

* Update WindowBase.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-07 14:59:50 +08:00
2dust
5fed566a32 Add ReplaceLineBreaks extension and Fix bug
https://github.com/2dust/v2rayN/issues/9497
2026-06-07 14:49:39 +08:00
2dust
f0ee792778 up 7.22.6 2026-06-07 11:23:36 +08:00
JieXu
cd4aa5531c Fix (#9495)
* Update App.axaml.cs

* Update App.axaml.cs
2026-06-07 09:04:06 +08:00
DHR60
e6d08c2db1 Code clean (#9482) 2026-06-06 11:20:06 +08:00
JieXu
4dc9c60b85 Update AppBuilderExtension.cs && Fix Bug (#9483)
* Update AppBuilderExtension.cs

* Update OptionSettingWindow.axaml.cs

* Update OptionSettingWindow.axaml.cs

* Update Global.cs

* Rename ResUI.fa-Ir.resx to ResUI.fa.resx

* Update ServiceLib.csproj
2026-06-06 11:09:53 +08:00
tt2563
eb9c974436 Update Traditional Chinese translation (#9484)
* Update Traditional Chinese translation

Update Traditional Chinese translation

* Update ResUI.zh-Hant.resx

Update Traditional Chinese translation
2026-06-06 09:37:40 +08:00
DHR60
27b8f889fe Tail Fragmentation (#9476) 2026-06-05 19:45:49 +08:00
DHR60
0d5169e4cd Remove global AllowInsecure and MuxEnabled (#9473)
* Remove global AllowInsecure

* Remove global MuxEnabled

* Fix core config

* Centralize AllowInsecure handling

* Warn insecure configuration

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-05 15:31:23 +08:00
DHR60
abcdd9497c Support new kcp config (#9477) 2026-06-05 15:08:57 +08:00
DHR60
da65faa49f Set xray default tun dns (#9474) 2026-06-05 09:58:00 +08:00
DHR60
b6cfce069a Xray Route Exclude Address (#9469)
* Add IPNetwork2 package

* Xray Route Exclude Address

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-06-05 09:54:57 +08:00
JieXu
39ec9239d2 Revert "Fix NUGET_CERT_REVOCATION_MODE=offline && Update Directory.Packages.props (#9478)
* Revert "Fix NUGET_CERT_REVOCATION_MODE=offline (#9470)"

This reverts commit a2a5940d91.

* Update Directory.Packages.props

* Update build-linux.yml

* Update Directory.Packages.props
2026-06-05 09:41:21 +08:00
JieXu
a2a5940d91 Fix NUGET_CERT_REVOCATION_MODE=offline (#9470)
* Update build.yml

* Update package-rhel.sh

* Update package-rhel-riscv.sh

* Update package-debian.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh

* Update build-linux.yml

* Update Directory.Packages.props
2026-06-04 20:00:32 +08:00
2dust
ce8caa89a4 Add Route Exclude Address setting and UI for sing-box tun
https://github.com/2dust/v2rayN/issues/9456
2026-06-04 15:55:09 +08:00
DHR60
af3d579890 Fix (#9462)
* Remove outdated CA certificates

* Support verifyPeerCertByName

* Fix
2026-06-03 15:07:53 +08:00
2dust
b2bbc865af Add VerifyPeerCertByName support
https://github.com/2dust/v2rayN/issues/9427
2026-06-03 09:56:34 +08:00
Miheichev Aleksandr Sergeevich
2eb325643a chore(deps): update NuGet dependencies (#9455)
- Avalonia.Desktop 11.3.16 -> 11.3.17
- Avalonia.Diagnostics 11.3.16 -> 11.3.17
- Downloader 5.5.0 -> 5.7.0
- Microsoft.NET.Test.Sdk 18.5.1 -> 18.6.0
- Tmds.DBus.Protocol 0.21.3 -> 0.94.1
- YamlDotNet 17.1.0 -> 18.0.0
2026-06-03 09:03:14 +08:00
Miheichev Aleksandr Sergeevich
8609e2b8d1 fix(ui): omit country flag emoji on Windows to avoid DE(DE) display (#9454) 2026-06-03 09:03:11 +08:00
2dust
fcf6c1e3ae up 7.22.5 2026-06-02 19:14:15 +08:00
2dust
ab907e371b Clarify deprecation message for allowInsecure 2026-06-02 19:13:52 +08:00
2dust
ef4dda0996 Warn on deprecated allowInsecure usage
https://github.com/2dust/v2rayN/issues/9435
2026-06-02 16:42:04 +08:00
JieXu
f2e0bd4eba Bump skiasharp version && Add loong64 Update && Translate FR (#9433)
* Update Directory.Packages.props

* Update CoreInfoManager.cs

* Update CoreInfo.cs

* Update UpdateService.cs

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx
2026-06-01 13:46:57 +08:00
Miheichev Aleksandr Sergeevich
bca07f801d i18n(ru): refine Russian localization against the zh-Hans source (#9431)
* i18n(ru): refine Russian localization against the zh-Hans source

Translate newly added UI strings and align existing Russian entries
with the authoritative Simplified Chinese (zh-Hans) source, since the
project is maintained by a Chinese-speaking author.

New strings (check-for-update flow):
- MsgCheckUpdateHasNewVersion, menuCheckOnly, menuNewUpdate,
  MsgNotSupport, LvTestIpInfo

Correctness fixes (changed meaning):
- TbSettingsHysteriaBandwidth: restore upload/download order (Up/Dw)
- TbSettingsMixedConcurrencyCount: "multi-threaded" (was "multi-stage")
- TransportRequestHostTip4: "QUIC encryption method" (was "security")
- TransportPathTip3: "QUIC encryption key"
- TbDomesticDNS: "DNS for direct connections"

Terminology and clarity:
- Unify the alias field to one consistent term across all rule/profile
  labels, distinct from the memo field
- allowInsecure -> "skip certificate verification" (+ cert-pinning ref)
- Restore details dropped from EN but present in zh-Hans: Port Hopping
  range, pre-SOCKS split routing, optional / "may fail" hints

Consistency and cleanup:
- Orthography, transport names (httpupgrade, XHTTPObject),
  move-to-top/bottom wording, trailing-space cleanup

Scope: ServiceLib/Resx/ResUI.ru.resx only. Technical abbreviations are
kept in English (LAN, UUID, MTU, TLS, ALPN, SNI, ...). Stale Chinese
strings vs. current behavior were judged case by case.

* i18n(ru): shorten update-check button labels to fit fixed-width buttons

The check-update window has two fixed-width (100px) buttons. The Russian
labels were faithful but too long, so both were truncated and looked almost
identical, hiding the difference between the two actions:
- the "Only Check" button only checks whether a newer version is available
- the "Check Update" button checks, downloads, installs and restarts

Shorten both so they fit the button and clearly distinguish check vs update.

Note: menuCheckUpdate is also reused as the default row placeholder in the
update list, which now shows the shorter label as well.
2026-06-01 13:46:42 +08:00
JieXu
54a3388c99 Update App.axaml.cs (#9421)
* Update App.axaml.cs

* Update App.axaml.cs

* Create MacOSAppHelper.cs

* Update App.axaml.cs

Removed unused macOS activation policy methods and constants.

* Update and rename MacOSAppHelper.cs to MacAppUtils.cs

* Update App.axaml.cs
2026-05-31 17:57:59 +08:00
direnquanbuKO
5140954b37 Correct multiple DNS Hosts ips (#9365)
* Correct dns.cloudflare.com ips

* Add ipv6 for engage.cloudflareclient.com

* Add new ips for dns.sb
2026-05-31 10:32:14 +08:00
JieXu
c7efe39e1c Fix (#9418)
* Update package-osx.sh

* Update package-osx.sh
2026-05-30 20:50:20 +08:00
2dust
a74ca3f9d0 up 7.22.4 2026-05-29 11:08:39 +08:00
2dust
ed61f5c420 Rename ViewModel properties to PascalCase 2026-05-29 11:05:43 +08:00
2dust
bf98c4007f Use configurable page size and delay in speedtests
https://github.com/2dust/v2rayN/pull/9392
2026-05-29 10:37:00 +08:00
dependabot[bot]
78dcf51c3c Bump actions/setup-dotnet from 5.2.0 to 5.3.0 (#9407)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 09:56:43 +08:00
2dust
1e59344074 Add balancer fallbackTag
https://github.com/2dust/v2rayN/issues/9401
2026-05-29 09:55:59 +08:00
JieXu
1d2442d58a Fix the problem of font select (#9405)
* Update OptionSettingWindow.axaml.cs

* Update OptionSettingWindow.axaml.cs
2026-05-28 15:31:06 +08:00
2dust
3448782925 Remove unused C# style setting from .editorconfig 2026-05-28 14:31:27 +08:00
2dust
e6f4a57913 Limit update checks to selected core types
https://github.com/2dust/v2rayN/issues/9381
2026-05-28 14:31:01 +08:00
Roffild
4ae5c021fd Add in Global.cs speed and ping test URLs (#9374)
* Add in Global.cs speed and ping test URLs

* 100mb

* Update Global.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-05-27 16:54:26 +08:00
xihan123
0a357fd1a7 Change the SelectedValue bound to Text #9388 (#9389) 2026-05-26 20:20:15 +08:00
2dust
5ba5a805ce Make MTU combobox editable
https://github.com/2dust/v2rayN/issues/9370
2026-05-24 14:54:06 +08:00
YFdyh000
807f0aba06 Make Admin AutoRun faster (#9366)
* Set Admin AutoRun task priority to Normal rather than Below Normal

* Remove 30s delay for Admin AutoRun
2026-05-23 13:58:00 +08:00
2dust
f4a2086dfb Refine reload logic after RefreshServers
https://github.com/2dust/v2rayN/issues/9344
2026-05-22 10:07:36 +08:00
2dust
ccb0ffb3b6 up 7.22.3 2026-05-21 17:25:50 +08:00
2dust
1fa1246c0b Bug fix
https://github.com/2dust/v2rayN/issues/9351
2026-05-21 14:15:07 +08:00
2dust
14cc37d07a Add 'New Update' notification flow
Introduce a small update-notification feature: add AppEvents.HasUpdateNotified event and have TaskManager publish it when update messages exist. Add localized resource key (menuNewUpdate) and expose it in the ResUI designer; update resource files for several languages. In the UI, add a New Update button in MainWindow.xaml, wire its click to the existing check-update handler, bind its visibility to a new BlNewUpdate property on MainWindowViewModel, and subscribe the viewmodel to the new event. Also reset the notification flag after showing the Check Update dialog.
2026-05-21 11:25:49 +08:00
2dust
c7519f8ea7 Case-insensitive name checks and IpInfo visibility 2026-05-21 10:33:17 +08:00
2dust
0c796a157b Add IP info & flag emoji to test,add ip info column for main window
Fetch and display tested server IP and country (with emoji) in speed tests. Adds CountryExtension for country->emoji mapping and a new IpInfoResult type; ConnectionHandler now retrieves/parses IP API results and GetRealPingTime signature adjusted. Models and entities (ProfileItemModel, ProfileExItem, SpeedTestResult) gain IpInfo fields; ProfileExManager can store test IP info. UI/UX updated: new IpInfo column in ProfilesView (desktop and Avalonia), ResUI resource strings for "IP Info", and EServerColName ordering supports IpInfo. SpeedtestService now captures IP info and forwards it to the view model via the update function.
2026-05-20 19:16:58 +08:00
2dust
a9824fe6ec Improve ConnectionHandler DownloadService 2026-05-20 15:42:47 +08:00
2dust
f18758d4bf Support socks4/5 URIs and improve userinfo parsing 2026-05-20 14:28:01 +08:00
JieXu
c1a009a409 bump dependencies (#9341)
* Update build-linux.yml

* Update Directory.Packages.props

* Update Directory.Packages.props
2026-05-20 09:01:22 +08:00
130 changed files with 2598 additions and 1036 deletions

View File

@@ -100,7 +100,6 @@ csharp_style_prefer_tuple_swap = true:warning
csharp_style_prefer_utf8_string_literals = true:warning
csharp_style_throw_expression = true:warning
csharp_style_unused_value_assignment_preference = discard_variable:warning
csharp_style_unused_value_expression_statement_preference = discard_variable:warning
csharp_using_directive_placement = outside_namespace:warning
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:warning
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning

View File

@@ -335,13 +335,13 @@ jobs:
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/edk2-loongarch64-vars.fd
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.2
QEMU_VERSION: 10.2.3
steps:
- name: Prepare host tools

View File

@@ -52,7 +52,7 @@ jobs:
fetch-depth: '0'
- name: Setup .NET
uses: actions/setup-dotnet@v5.2.0
uses: actions/setup-dotnet@v5.3.0
with:
dotnet-version: '10.0.1xx'

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: '0'
- name: Setup .NET
uses: actions/setup-dotnet@v5.2.0
uses: actions/setup-dotnet@v5.3.0
with:
dotnet-version: '8.0.x'

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "v2rayN/GlobalHotKeys"]
path = v2rayN/GlobalHotKeys
url = https://github.com/2dust/GlobalHotKeys
[submodule "v2rayN/NetBridge"]
path = v2rayN/NetBridge
url = https://github.com/2dust/NetBridge

View File

@@ -22,7 +22,17 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>zh-Hant</string>
<string>en</string>
<string>fa</string>
<string>fr</string>
<string>ru</string>
<string>hu</string>
</array>
<key>CFBundleDisplayName</key>
<string>v2rayN</string>
<key>CFBundleExecutable</key>

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.22.2</Version>
<Version>7.22.6</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -7,33 +7,33 @@
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.17" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.17" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" />
<PackageVersion Include="CliWrap" Version="3.10.1" />
<PackageVersion Include="Downloader" Version="5.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="Downloader" Version="5.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.27" />
<PackageVersion Include="ReactiveUI" Version="23.2.28" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.4" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.2" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
<PackageVersion Include="YamlDotNet" Version="18.0.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup>
</Project>

View File

@@ -17,7 +17,7 @@ internal static class CoreConfigTestFactory
{
return new Config
{
CoreBasicItem = new CoreBasicItem { Loglevel = "warning", MuxEnabled = false },
CoreBasicItem = new CoreBasicItem { Loglevel = "warning" },
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
KcpItem = new KcpItem(),
GrpcItem = new GrpcItem(),
@@ -187,7 +187,7 @@ internal static class CoreConfigTestFactory
SimpleDnsItem = config.SimpleDNSItem,
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
FullConfigTemplate = null,
IsTunEnabled = false,
IsTunEnabled = config.TunModeItem.EnableTun,
ProtectDomainList = [],
};
}
@@ -206,4 +206,12 @@ internal static class CoreConfigTestFactory
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
return config;
}
public static Config CreateConfigWithTunRouteExcludeAddress(ECoreType coreType)
{
var config = CreateConfig(coreType);
config.TunModeItem.EnableTun = true;
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
return config;
}
}

View File

@@ -554,7 +554,7 @@ public class CoreConfigSingboxServiceTests
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Global.ToString());
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString());
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
}
}

View File

@@ -536,4 +536,27 @@ public class CoreConfigV2rayServiceTests
directOutbound.Should().NotBeNull();
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4");
}
[Fact]
public void GenerateClientConfigContent_TunRouteExcludeAddress()
{
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
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().NotContain("0.0.0.0/0");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
}
}

View File

@@ -0,0 +1,92 @@
namespace ServiceLib.Common;
/// <summary>
/// Extension methods for country code utilities
/// </summary>
public static class CountryExtension
{
/// <summary>
/// Country code to emoji flag mapping for common countries
/// </summary>
private static readonly Dictionary<string, string> CountryEmojiMap = new(StringComparer.OrdinalIgnoreCase)
{
// Asia
{ "CN", "🇨🇳" }, // China
{ "HK", "🇭🇰" }, // Hong Kong
{ "TW", "🇹🇼" }, // Taiwan
{ "JP", "🇯🇵" }, // Japan
{ "SG", "🇸🇬" }, // Singapore
{ "KR", "🇰🇷" }, // South Korea
{ "TH", "🇹🇭" }, // Thailand
{ "VN", "🇻🇳" }, // Vietnam
{ "ID", "🇮🇩" }, // Indonesia
{ "PH", "🇵🇭" }, // Philippines
{ "MY", "🇲🇾" }, // Malaysia
{ "IN", "🇮🇳" }, // India
{ "PK", "🇵🇰" }, // Pakistan
{ "BD", "🇧🇩" }, // Bangladesh
{ "LK", "🇱🇰" }, // Sri Lanka
{ "KH", "🇰🇭" }, // Cambodia
{ "LA", "🇱🇦" }, // Laos
{ "MM", "🇲🇲" }, // Myanmar
// Americas
{ "US", "🇺🇸" }, // United States
{ "CA", "🇨🇦" }, // Canada
{ "MX", "🇲🇽" }, // Mexico
{ "BR", "🇧🇷" }, // Brazil
{ "AR", "🇦🇷" }, // Argentina
{ "CL", "🇨🇱" }, // Chile
{ "CO", "🇨🇴" }, // Colombia
// Europe
{ "GB", "🇬🇧" }, // United Kingdom
{ "DE", "🇩🇪" }, // Germany
{ "FR", "🇫🇷" }, // France
{ "IT", "🇮🇹" }, // Italy
{ "ES", "🇪🇸" }, // Spain
{ "RU", "🇷🇺" }, // Russia
{ "NL", "🇳🇱" }, // Netherlands
{ "CH", "🇨🇭" }, // Switzerland
{ "SE", "🇸🇪" }, // Sweden
{ "NO", "🇳🇴" }, // Norway
{ "DK", "🇩🇰" }, // Denmark
{ "FI", "🇫🇮" }, // Finland
{ "PL", "🇵🇱" }, // Poland
{ "CZ", "🇨🇿" }, // Czech Republic
{ "AT", "🇦🇹" }, // Austria
{ "GR", "🇬🇷" }, // Greece
{ "PT", "🇵🇹" }, // Portugal
{ "TR", "🇹🇷" }, // Turkey
{ "UA", "🇺🇦" }, // Ukraine
{ "RO", "🇷🇴" }, // Romania
// Middle East & Central Asia
{ "AE", "🇦🇪" }, // United Arab Emirates
{ "SA", "🇸🇦" }, // Saudi Arabia
{ "IL", "🇮🇱" }, // Israel
{ "KZ", "🇰🇿" }, // Kazakhstan
// Oceania
{ "AU", "🇦🇺" }, // Australia
{ "NZ", "🇳🇿" }, // New Zealand
// Africa
{ "ZA", "🇿🇦" }, // South Africa
{ "EG", "🇪🇬" }, // Egypt
};
/// <summary>
/// Converts country code to flag emoji using predefined mapping
/// Example: "US" -> "🇺🇸", "CN" -> "🇨🇳"
/// </summary>
public static string? CountryToEmoji(this string? countryCode)
{
if (countryCode.IsNullOrEmpty())
{
return null;
}
return CountryEmojiMap.GetValueOrDefault(countryCode);
}
}

View File

@@ -47,7 +47,7 @@ public static class Extension
public static string TrimEx(this string? value)
{
return value == null ? string.Empty : value.Trim();
return value?.Trim() ?? string.Empty;
}
public static string RemovePrefix(this string value, char prefix)
@@ -118,4 +118,20 @@ public static class Extension
destination.Add(item);
}
}
/// <summary>
/// Replace all cross-platform newline characters with the specified string
/// </summary>
public static string ReplaceLineBreaks(this string input, string replacement)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
// You must replace \r\n first, and then replace the single characters \r and \n.
return input.Replace("\r\n", replacement)
.Replace("\r", replacement)
.Replace("\n", replacement);
}
}

View File

@@ -202,7 +202,7 @@ public static class FileUtils
}
}
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine)
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine, string? contains = null)
{
try
{
@@ -214,6 +214,10 @@ public static class FileUtils
{
continue;
}
if (contains.IsNotEmpty() && !file.Name.Contains(contains))
{
continue;
}
file.Delete();
}
}

View File

@@ -51,7 +51,7 @@ public class Utils
try
{
str = str.Replace(Environment.NewLine, string.Empty);
str = str.ReplaceLineBreaks(string.Empty);
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
}
catch (Exception ex)
@@ -114,9 +114,7 @@ public class Utils
}
plainText = plainText.Trim()
.Replace(Environment.NewLine, "")
.Replace("\n", "")
.Replace("\r", "")
.ReplaceLineBreaks("")
.Replace('_', '/')
.Replace('-', '+')
.Replace(" ", "");
@@ -321,7 +319,9 @@ public class Utils
return text;
}
return text.Replace("", ",").Replace(Environment.NewLine, ",");
return text.Replace("", ",")
.Replace(" ", "")
.ReplaceLineBreaks(",");
}
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum

View File

@@ -12,6 +12,7 @@ public enum EServerColName
SubRemarks,
DelayVal,
SpeedVal,
IpInfo,
TodayDown,
TodayUp,

View File

@@ -7,6 +7,7 @@ public static class AppEvents
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<bool> HasUpdateNotified = new();
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();

View File

@@ -55,7 +55,7 @@ public class Global
public const string DnsOutboundTag = "dns";
public const string DnsTag = "dns-module";
public const string DirectDnsTag = "direct-dns";
public const string BalancerTagSuffix = "-round";
public const string BalancerTagSuffix = "-balancer";
public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality";
public const string Loopback = "127.0.0.1";
@@ -90,6 +90,8 @@ public class Global
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000;
public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true";
public const string StringFalse = "false";
public const string SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns";
@@ -149,6 +151,9 @@ public class Global
public static readonly List<string> SpeedTestUrls =
[
@"https://cachefly.cachefly.net/50mb.test",
@"https://cachefly.cachefly.net/100mb.test",
@"https://cachefly.cachefly.net/1mb.test",
@"https://cachefly.cachefly.net/10mb.test",
@"https://speed.cloudflare.com/__down?bytes=10000000",
@"https://speed.cloudflare.com/__down?bytes=50000000",
@"https://speed.cloudflare.com/__down?bytes=99999999",
@@ -157,6 +162,8 @@ public class Global
public static readonly List<string> SpeedPingTestUrls =
[
@"https://www.google.com/generate_204",
@"https://www.youtube.com/generate_204",
@"https://www.googlevideo.com/generate_204",
@"https://www.gstatic.com/generate_204",
@"https://www.apple.com/library/test/success.html",
@"http://www.msftconnecttest.com/connecttest.txt"
@@ -207,6 +214,10 @@ public class Global
public const string NaiveQuicProtocolShare = "naive+quic://";
public const string SOCKS5Protocol = "socks5://";
public const string SOCKS4Protocol = "socks4://";
public static readonly Dictionary<EConfigType, string> ProtocolShares = new()
{
{ EConfigType.VMess, "vmess://" },
@@ -321,12 +332,12 @@ public class Global
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
{
{ "srtp", "header-srtp" },
{ "utp", "header-utp" },
{ "wechat-video", "header-wechat" },
{ "dtls", "header-dtls" },
{ "wireguard", "header-wireguard" },
{ "dns", "header-dns" }
{ "srtp", "srtp" },
{ "utp", "utp" },
{ "wechat-video", "wechat" },
{ "dtls", "dtls" },
{ "wireguard", "wireguard" },
{ "dns", "dns" }
};
public static readonly List<string> CoreTypes =
@@ -412,13 +423,6 @@ public class Global
"stream-one"
];
public static readonly List<string> AllowInsecure =
[
"true",
"false",
""
];
public static readonly List<string> DomainStrategy =
[
"AsIs",
@@ -469,7 +473,7 @@ public class Global
"zh-Hans",
"zh-Hant",
"en",
"fa-Ir",
"fa",
"fr",
"ru",
"hu"
@@ -676,15 +680,15 @@ public class Global
{ "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
{ "1dot1dot1dot1.cloudflare-dns.com", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
{ "cloudflare-dns.com", ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"] },
{ "dns.cloudflare.com", ["104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5"] },
{ "dns.cloudflare.com", ["162.159.61.8", "172.64.41.8", "2a06:98c1:52::8", "2803:f800:53::8"] },
{ "dot.pub", ["1.12.12.12", "120.53.53.53"] },
{ "doh.pub", ["1.12.12.12", "120.53.53.53"] },
{ "dns.quad9.net", ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"] },
{ "dns.yandex.net", ["77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"] },
{ "dns.sb", ["185.222.222.222", "2a09::"] },
{ "dns.sb", ["45.11.45.11", "185.222.222.222", "2a09::", "2a11::"] },
{ "dns.umbrella.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
{ "dns.sse.cisco.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
{ "engage.cloudflareclient.com", ["162.159.192.1"] }
{ "engage.cloudflareclient.com", ["162.159.192.1", "2606:4700:d0::a29f:c001"] }
};
public static readonly List<string> ExpectedIPs =

View File

@@ -111,7 +111,8 @@ public static class AutoStartupHandler
task.Settings.RunOnlyIfIdle = false;
task.Settings.IdleSettings.StopOnIdleEnd = false;
task.Settings.ExecutionTimeLimit = TimeSpan.Zero;
task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser, Delay = TimeSpan.FromSeconds(30) });
task.Settings.Priority = ProcessPriorityClass.Normal;
task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser });
task.Principal.RunLevel = Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest;
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));

View File

@@ -91,6 +91,20 @@ public class CoreConfigContextBuilder
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
}
}
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
{
try
{
IPNetwork2.Parse(addr);
}
catch
{
validatorResult.Errors.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
}
}
}
return new CoreConfigContextBuilderResult(context, validatorResult);
}

View File

@@ -29,35 +29,6 @@ public class NodeValidator
return v.ToResult();
}
private class ValidationContext
{
public List<string> Errors { get; } = [];
public List<string> Warnings { get; } = [];
public void Error(string message)
{
Errors.Add(message);
}
public void Warning(string message)
{
Warnings.Add(message);
}
public void Assert(bool condition, string errorMsg)
{
if (!condition)
{
Error(errorMsg);
}
}
public NodeValidatorResult ToResult()
{
return new NodeValidatorResult(Errors, Warnings);
}
}
private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v)
{
if (item.ConfigType is EConfigType.Custom)
@@ -126,6 +97,21 @@ public class NodeValidator
break;
}
if (coreType is ECoreType.Xray
&& (protocolExtra.Flow ?? string.Empty).StartsWith("xtls", StringComparison.OrdinalIgnoreCase)
&& item.MuxEnabled == true)
{
v.Warning(string.Format(ResUI.MsgOptionsConflict, "XTLS", "Mux.Cool"));
}
if (item.GetNetwork() is nameof(ETransport.ws)
&& item.EchConfigList.IsNullOrEmpty()
&& item.GetAlpn()?.FirstOrDefault() == "h3")
{
v.Warning(
"WebSocket but ALPN is set to h3, the core may ignore the ALPN setting or cause unexpected issues.");
}
// TLS & Security
if (item.StreamSecurity == Global.StreamSecurity)
{
@@ -134,6 +120,25 @@ public class NodeValidator
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
}
// Check for deprecated allowInsecure property when TLS is enabled
if (item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty()
&& item.CertSha.IsNullOrEmpty())
{
v.Warning(ResUI.MsgAllowInsecureDeprecated);
}
if ((coreType == ECoreType.Xray
&& item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty()
&& item.CertSha.IsNullOrEmpty())
|| (coreType == ECoreType.sing_box
&& item.GetAllowInsecure()
&& item.Cert.IsNullOrEmpty()))
{
v.Warning("Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.");
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)
@@ -183,4 +188,33 @@ public class NodeValidator
return null;
}
private class ValidationContext
{
public List<string> Errors { get; } = [];
public List<string> Warnings { get; } = [];
public void Error(string message)
{
Errors.Add(message);
}
public void Warning(string message)
{
Warnings.Add(message);
}
public void Assert(bool condition, string errorMsg)
{
if (!condition)
{
Error(errorMsg);
}
}
public NodeValidatorResult ToResult()
{
return new NodeValidatorResult(Errors, Warnings);
}
}
}

View File

@@ -39,15 +39,14 @@ public static class ConfigHandler
{
LogEnabled = false,
Loglevel = "warning",
MuxEnabled = false,
};
if (config.Inbound == null)
{
config.Inbound = new List<InItem>();
config.Inbound = [];
InItem inItem = new()
{
Protocol = EInboundProtocol.socks.ToString(),
Protocol = nameof(EInboundProtocol.socks),
LocalPort = 10808,
UdpEnabled = true,
SniffingEnabled = true,
@@ -60,7 +59,7 @@ public static class ConfigHandler
{
if (config.Inbound.Count > 0)
{
config.Inbound.First().Protocol = EInboundProtocol.socks.ToString();
config.Inbound.First().Protocol = nameof(EInboundProtocol.socks);
}
}
@@ -99,8 +98,8 @@ public static class ConfigHandler
config.MsgUIItem ??= new();
config.UiItem ??= new();
config.UiItem.MainColumnItem ??= new();
config.UiItem.WindowSizeItem ??= new();
config.UiItem.MainColumnItem ??= [];
config.UiItem.WindowSizeItem ??= [];
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
{
@@ -158,17 +157,21 @@ public static class ConfigHandler
DownMbps = 100
};
config.ClashUIItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= [];
config.SystemProxyItem ??= new();
config.WebDavItem ??= new();
config.CheckUpdateItem ??= new();
config.NetBridgeItem ??= new()
{
RuleProcess = string.Empty
};
config.Fragment4RayItem ??= new()
{
Packets = "tlshello",
Length = "50-100",
Interval = "10-20"
};
config.GlobalHotkeys ??= new();
config.GlobalHotkeys ??= [];
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
{
@@ -255,6 +258,7 @@ public static class ConfigHandler
item.Cert = profileItem.Cert;
item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList;
item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName;
item.Finalmask = profileItem.Finalmask;
item.ProtoExtra = profileItem.ProtoExtra;
item.TransportExtra = profileItem.TransportExtra;
@@ -874,7 +878,7 @@ public static class ConfigHandler
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
profileItem.AllowInsecure = "false";
profileItem.AllowInsecure = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
@@ -923,6 +927,7 @@ public static class ConfigHandler
Delay = t33?.Delay ?? 0,
Speed = t33?.Speed ?? 0,
Sort = t33?.Sort ?? 0,
IpInfo = t33?.IpInfo ?? string.Empty,
TodayDown = (t22?.TodayDown ?? 0).ToString("D16"),
TodayUp = (t22?.TodayUp ?? 0).ToString("D16"),
TotalDown = (t22?.TotalDown ?? 0).ToString("D16"),
@@ -943,6 +948,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderBy(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(),
@@ -963,6 +969,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderByDescending(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(),
@@ -1053,8 +1060,8 @@ public static class ConfigHandler
return new Tuple<int, int>(0, 0);
}
List<ProfileItem> lstKeep = new();
List<ProfileItem> lstRemove = new();
List<ProfileItem> lstKeep = [];
List<ProfileItem> lstRemove = [];
if (!config.GuiItem.KeepOlderDedupl)
{
lstProfile.Reverse();
@@ -1103,10 +1110,6 @@ public static class ConfigHandler
}
else
{
if (profileItem.AllowInsecure.IsNullOrEmpty())
{
profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower();
}
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
{
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
@@ -1534,7 +1537,7 @@ public static class ConfigHandler
}
var countServers = 0;
List<ProfileItem> lstAdd = new();
List<ProfileItem> lstAdd = [];
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
if (isSub)
{
@@ -1630,7 +1633,7 @@ public static class ConfigHandler
{
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
}
if (lstProfiles != null && lstProfiles.Count > 0)
if (lstProfiles is { Count: > 0 })
{
var count = 0;
foreach (var it in lstProfiles)

View File

@@ -4,53 +4,41 @@ public static class ConnectionHandler
{
private static readonly string _tag = "ConnectionHandler";
/// <summary>
/// Runs ping and IP checks and returns a formatted result string.
/// </summary>
public static async Task<string> RunAvailabilityCheck()
{
var time = await GetRealPingTimeInfo();
var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None;
var ip = time > 0 ? await GetIPInfo() : Global.None;
return string.Format(ResUI.TestMeOutput, time, ip);
}
/// <summary>
/// Gets IP information using the default local proxy.
/// </summary>
private static async Task<string?> GetIPInfo()
{
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty())
{
return null;
}
var webProxy = await GetWebProxy();
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, true, "");
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code;
return $"({country ?? "unknown"}) {ip}";
var ipInfo = await GetIPInfo(webProxy);
return ipInfo?.ToString() ?? Global.None;
}
/// <summary>
/// Measures real ping time using configured test URL.
/// </summary>
private static async Task<int> GetRealPingTimeInfo()
{
var responseTime = -1;
try
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}");
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var webProxy = await GetWebProxy();
for (var i = 0; i < 2; i++)
{
responseTime = await GetRealPingTime(url, webProxy, 10);
responseTime = await GetRealPingTime(webProxy, 10);
if (responseTime > 0)
{
break;
@@ -66,8 +54,21 @@ public static class ConnectionHandler
return responseTime;
}
public static async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
/// <summary>
/// Creates local SOCKS proxy instance.
/// </summary>
private static async Task<WebProxy?> GetWebProxy()
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
return new WebProxy($"socks5://{Global.Loopback}:{port}");
}
/// <summary>
/// Measures response time by sending HTTP requests through proxy.
/// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout)
{
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1;
try
{
@@ -79,14 +80,14 @@ public static class ConnectionHandler
UseProxy = webProxy != null
});
List<int> oneTime = new();
List<int> oneTime = [];
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100);
await Task.Delay(100, cts.Token);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}
@@ -95,4 +96,41 @@ public static class ConnectionHandler
}
return responseTime;
}
/// <summary>
/// Gets IP and country information through specified proxy.
/// </summary>
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
{
try
{
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty())
{
return null;
}
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, webProxy, "");
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code ?? "unknown";
return new IpInfoResult(country, ip);
}
catch
{
return null;
}
}
}

View File

@@ -73,6 +73,10 @@ public class BaseFmt
{
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
}
if (item.VerifyPeerCertByName.IsNotEmpty())
{
dicQuery.Add("vcn", Utils.UrlEncode(item.VerifyPeerCertByName));
}
if (item.CertSha.IsNotEmpty())
{
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
@@ -199,7 +203,7 @@ public class BaseFmt
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.AllowInsecure.Equals(Global.AllowInsecure.First()))
if (item.GetAllowInsecure())
{
// Add two for compatibility
dicQuery.Add("insecure", "1");
@@ -227,6 +231,7 @@ public class BaseFmt
item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.EchConfigList = GetQueryDecoded(query, "ech");
item.VerifyPeerCertByName = GetQueryDecoded(query, "vcn");
item.CertSha = GetQueryDecoded(query, "pcs");
var finalmaskDecoded = GetQueryDecoded(query, "fm");
@@ -249,11 +254,11 @@ public class BaseFmt
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{
item.AllowInsecure = Global.AllowInsecure.First();
item.AllowInsecure = Global.StringTrue;
}
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
{
item.AllowInsecure = Global.AllowInsecure.Skip(1).First();
item.AllowInsecure = Global.StringFalse;
}
else
{

View File

@@ -53,7 +53,9 @@ public class FmtHandler
{
return ShadowsocksFmt.Resolve(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS]))
else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS])
|| str.StartsWith(Global.SOCKS5Protocol)
|| str.StartsWith(Global.SOCKS4Protocol))
{
return SocksFmt.Resolve(str, out msg);
}
@@ -65,7 +67,8 @@ public class FmtHandler
{
return VLESSFmt.Resolve(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2]) || str.StartsWith(Global.Hysteria2ProtocolShare))
else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2])
|| str.StartsWith(Global.Hysteria2ProtocolShare))
{
return Hysteria2Fmt.Resolve(str, out msg);
}

View File

@@ -185,7 +185,7 @@ public class InnerFmt
return null;
}
// Check Enum.IsDefined
if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType))
if (!Enum.IsDefined(profileItem.ConfigType))
{
return null;
}

View File

@@ -71,7 +71,7 @@ public class SocksFmt : BaseFmt
return null;
}
var arr21 = arr1.First().Split(':');
var indexPort = arr1.Last().LastIndexOf(":");
var indexPort = arr1.Last().LastIndexOf(':');
if (arr21.Length != 2 || indexPort < 0)
{
return null;
@@ -99,12 +99,17 @@ public class SocksFmt : BaseFmt
};
// parse base64 UserInfo
var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo);
var userInfo = Utils.Base64Decode(rawUserInfo);
var userInfoParts = userInfo.Split([':'], 2);
if (userInfoParts.Length == 2)
if (rawUserInfo.IsNotEmpty())
{
item.Username = userInfoParts.First();
item.Password = userInfoParts[1];
var userInfoParts = rawUserInfo.Contains(':')
? rawUserInfo.Split(":", 2)
: Utils.Base64Decode(rawUserInfo).Split(":", 2);
if (userInfoParts.Length == 2)
{
item.Username = userInfoParts.First();
item.Password = userInfoParts.Last();
}
}
return item;

View File

@@ -66,7 +66,7 @@ public class VmessFmt : BaseFmt
sni = item.Sni,
alpn = item.Alpn,
fp = item.Fingerprint,
insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0"
insecure = item.GetAllowInsecure() ? "1" : "0"
};
var url = JsonUtils.Serialize(vmessQRCode);
@@ -140,7 +140,7 @@ public class VmessFmt : BaseFmt
item.Sni = Utils.ToString(vmessQRCode.sni);
item.Alpn = Utils.ToString(vmessQRCode.alpn);
item.Fingerprint = Utils.ToString(vmessQRCode.fp);
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty;
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.StringTrue : string.Empty;
return item;
}

View File

@@ -133,12 +133,12 @@ public static class SubscriptionHandler
if (!url.Contains("target="))
{
url += string.Format("&target={0}", item.ConvertTarget);
url += $"&target={item.ConvertTarget}";
}
if (!url.Contains("config="))
{
url += string.Format("&config={0}", Global.SubConvertConfig.FirstOrDefault());
url += $"&config={Global.SubConvertConfig.FirstOrDefault()}";
}
}

View File

@@ -268,7 +268,7 @@ public sealed class AppManager
.ToListAsync();
var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(id => itemMap.GetValueOrDefault(id))
return idList.Select(itemMap.GetValueOrDefault)
.Where(item => item != null)
.ToList();
}

View File

@@ -4,16 +4,15 @@ using System.Security.Cryptography.X509Certificates;
namespace ServiceLib.Manager;
/// <summary>
/// Manager for certificate operations with CA pinning to prevent MITM attacks
/// Manager for certificate operations with CA pinning to prevent MITM attacks
/// </summary>
public class CertPemManager
{
private static readonly string _tag = "CertPemManager";
private static readonly Lazy<CertPemManager> _instance = new(() => new());
public static CertPemManager Instance => _instance.Value;
/// <summary>
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
/// </summary>
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
{
@@ -24,21 +23,17 @@ public class CertPemManager
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
"CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
"F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA
"4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
"EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
@@ -46,10 +41,6 @@ public class CertPemManager
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2
"0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial
"0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking
"70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium
"BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC
"5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
@@ -62,7 +53,6 @@ public class CertPemManager
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
"DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
@@ -116,16 +106,12 @@ public class CertPemManager
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2
"97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority
"945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority
"55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority
"88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
"9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
@@ -179,7 +165,6 @@ public class CertPemManager
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
"BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
@@ -200,10 +185,13 @@ public class CertPemManager
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
};
public static CertPemManager Instance => _instance.Value;
/// <summary>
/// Get certificate in PEM format from a server with CA pinning validation
/// Get certificate in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4,
List<string>? verifyPeerCertByName = null, bool allowInsecure = false)
{
try
{
@@ -216,13 +204,14 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [],
allowInsecure));
await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = callback
RemoteCertificateValidationCallback = callback,
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -249,9 +238,10 @@ public class CertPemManager
}
/// <summary>
/// Get certificate chain in PEM format from a server with CA pinning validation
/// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4,
List<string>? verifyPeerCertByName = null, bool allowInsecure = false)
{
var pemList = new List<string>();
try
@@ -265,13 +255,14 @@ public class CertPemManager
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [],
allowInsecure));
await using var ssl = new SslStream(client.GetStream(), false, callback);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = callback
RemoteCertificateValidationCallback = callback,
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
@@ -301,13 +292,14 @@ public class CertPemManager
}
/// <summary>
/// Validate server certificate with CA pinning
/// Validate server certificate with CA pinning
/// </summary>
private bool ValidateServerCertificate(
private static bool ValidateServerCertificate(
object _,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors,
List<string> verifyPeerCertByName,
bool allowInsecure)
{
if (certificate == null)
@@ -321,22 +313,21 @@ public class CertPemManager
return true;
}
// Check certificate name mismatch
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
{
return false;
}
// Build certificate chain
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
var certChain = chain ?? new X509Chain();
certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
certChain.ChainPolicy.VerificationTime = DateTime.Now;
if (chain == null)
{
certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
certChain.ChainPolicy.VerificationTime = DateTime.UtcNow;
certChain.Build(cert2);
if (!certChain.Build(cert2))
{
return false;
}
}
// Find root CA
if (certChain.ChainElements.Count == 0)
@@ -344,10 +335,37 @@ public class CertPemManager
return false;
}
var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate;
var rootCert = certChain.ChainElements[^1].Certificate;
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
return TrustedCaThumbprints.Contains(rootThumbprint);
if (!TrustedCaThumbprints.Contains(rootThumbprint))
{
return false;
}
if (!sslPolicyErrors.HasFlag(
SslPolicyErrors.RemoteCertificateNameMismatch))
{
return true;
}
if (verifyPeerCertByName.Count == 0)
{
return false;
}
foreach (var ext in cert2.Extensions)
{
if (ext is not X509SubjectAlternativeNameExtension san)
{
continue;
}
return san.EnumerateDnsNames().Any(dnsName =>
verifyPeerCertByName.Contains(dnsName, StringComparer.OrdinalIgnoreCase));
}
return false;
}
public static string ExportCertToPem(X509Certificate2 cert)
@@ -358,8 +376,8 @@ public class CertPemManager
}
/// <summary>
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// </summary>
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
/// <returns>List of individual PEM certificate strings with normalized format</returns>
@@ -411,18 +429,13 @@ public class CertPemManager
}
/// <summary>
/// Concatenate a list of PEM certificates into a single string
/// Concatenate a list of PEM certificates into a single string
/// </summary>
/// <param name="pemList">List of individual PEM certificate strings</param>
/// <returns>Concatenated PEM certificates string</returns>
public static string ConcatenatePemChain(IEnumerable<string> pemList)
public static string ConcatenatePemChain(IEnumerable<string>? pemList)
{
if (pemList == null)
{
return string.Empty;
}
return string.Concat(pemList);
return pemList == null ? string.Empty : string.Concat(pemList);
}
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
@@ -431,11 +444,7 @@ public class CertPemManager
{
var cert = X509Certificate2.CreateFromPem(pemCert);
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
if (includeColon)
{
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
}
return thumbprint;
return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
}
catch
{

View File

@@ -45,19 +45,14 @@ public sealed class ClashApiManager
{
await GetClashProxiesAsync();
}
lstProxy = new List<ClashProxyModel>();
foreach (var kv in _proxies ?? [])
{
if (Global.notAllowTestType.Contains(kv.Value.type?.ToLower()))
{
continue;
}
lstProxy.Add(new ClashProxyModel()
{
Name = kv.Value.name,
Type = kv.Value.type?.ToLower(),
});
}
lstProxy = [];
lstProxy.AddRange(from kv in _proxies ?? []
where !Global.notAllowTestType.Contains(kv.Value.type?.ToLower())
select new ClashProxyModel()
{
Name = kv.Value.name,
Type = kv.Value.type?.ToLower(),
});
}
if (lstProxy is not { Count: > 0 })

View File

@@ -113,6 +113,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip",
DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip",
DownloadUrlLinuxRiscV64 = urlN + "/download/{0}/v2rayN-linux-riscv64.zip",
DownloadUrlLinuxLoong64 = urlN + "/download/{0}/v2rayN-linux-loong64.zip",
DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip",
DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip",
},
@@ -157,6 +158,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip",
DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip",
DownloadUrlLinuxRiscV64 = urlXray + "/download/{0}/Xray-linux-riscv64.zip",
DownloadUrlLinuxLoong64 = urlXray + "/download/{0}/Xray-linux-loong64.zip",
DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip",
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
Match = "Xray",
@@ -180,6 +182,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
DownloadUrlLinuxRiscV64 = urlMihomo + "/download/{0}/mihomo-linux-riscv64-{0}.gz",
DownloadUrlLinuxLoong64 = urlMihomo + "/download/{0}/mihomo-linux-loong64-abi2-{0}.gz",
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
Match = "Mihomo",
@@ -223,6 +226,7 @@ public sealed class CoreInfoManager
DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz",
DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz",
DownloadUrlLinuxRiscV64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-riscv64.tar.gz",
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
Match = "sing-box",
@@ -314,6 +318,7 @@ public sealed class CoreInfoManager
names.Add("mihomo-linux-amd64");
names.Add("mihomo-linux-arm64");
names.Add("mihomo-linux-riscv64");
names.Add("mihomo-linux-loong64-abi2");
}
else if (Utils.IsMacOS())
{

View File

@@ -9,7 +9,7 @@ public class GroupProfileManager
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo)
{
return await HasCycle(indexId, extraInfo, new HashSet<string>(), new HashSet<string>());
return await HasCycle(indexId, extraInfo, [], []);
}
private static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack)
@@ -55,7 +55,7 @@ public class GroupProfileManager
var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
foreach (var childItem in childItems)
{
if (await HasCycle(childItem.IndexId, childItem?.GetProtocolExtra(), visited, stack))
if (await HasCycle(childItem.IndexId, childItem.GetProtocolExtra(), visited, stack))
{
return true;
}
@@ -71,7 +71,7 @@ public class GroupProfileManager
public static async Task<(List<ProfileItem> Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem)
{
var protocolExtra = profileItem?.GetProtocolExtra();
var protocolExtra = profileItem.GetProtocolExtra();
return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra);
}

View File

@@ -0,0 +1,222 @@
using NetBridgeLib.Services;
namespace ServiceLib.Manager;
public sealed class NetBridgeManager
{
private static readonly Lazy<NetBridgeManager> _instance = new(() => new());
public static NetBridgeManager Instance => _instance.Value;
private readonly Config _config = AppManager.Instance.Config;
private NetBridgeService? _netBridgeService;
private bool _isProxyRunning;
private bool _isInitialized;
private List<NetBridgeRuleConfig> _ruleConfigs = [];
private Func<bool, string, Task>? _updateFunc;
private uint _proxyConfigId;
public async Task Init(Func<bool, string, Task>? updateFunc = null)
{
if (_isInitialized)
{
return;
}
_updateFunc = updateFunc;
try
{
_netBridgeService = new NetBridgeService();
_netBridgeService.LogReceived += msg =>
{
var message = $"NetBridge Log: {msg}";
_ = _updateFunc?.Invoke(false, message);
};
_netBridgeService.ConnectionReceived += (processName, pid, destIp, destPort, proxyInfo) =>
{
var message = $"NetBridge Connection: {processName} (PID: {pid}) -> {destIp}:{destPort} -> {proxyInfo}";
_ = _updateFunc?.Invoke(false, message);
};
_ruleConfigs = BuildRuleConfigs(_config.NetBridgeItem?.RuleProcess);
_isInitialized = true;
}
catch (Exception ex)
{
var error = $"Failed to initialize NetBridgeService: {ex.Message}";
await _updateFunc?.Invoke(true, error);
}
}
public async Task<bool> Start()
{
if (_isProxyRunning)
{
return true;
}
try
{
if (_netBridgeService == null)
{
return false;
}
var started = _netBridgeService.Start();
if (!started)
{
return false;
}
_isProxyRunning = true;
}
catch (Exception ex)
{
var error = $"Failed to start NetBridgeService: {ex.Message}";
await _updateFunc?.Invoke(true, error);
return false;
}
return true;
}
public async Task<bool> Stop()
{
if (!_isProxyRunning)
{
return true;
}
try
{
if (_netBridgeService == null)
{
return false;
}
var stopped = _netBridgeService.Stop();
if (!stopped)
{
return false;
}
_isProxyRunning = false;
}
catch (Exception ex)
{
var error = $"Failed to stop NetBridgeService: {ex.Message}";
await _updateFunc?.Invoke(true, error);
return false;
}
return true;
}
public async Task<bool> UpdateRoutes(string? ruleProcess)
{
var newRuleConfigs = BuildRuleConfigs(ruleProcess);
_ruleConfigs = newRuleConfigs;
if (!_isProxyRunning)
{
return true;
}
return await ApplyRoutesInternal();
}
public async Task<bool> UpdateProxyConfig(string proxyHost, int proxyPort)
{
try
{
if (_netBridgeService == null)
{
return false;
}
var proxyType = "SOCKS5";
var username = "";
var password = "";
if (_proxyConfigId > 0)
{
var edited = _netBridgeService.EditProxyConfig(_proxyConfigId, proxyType, proxyHost, (ushort)proxyPort, username, password);
if (!edited)
{
return false;
}
}
else
{
_proxyConfigId = _netBridgeService.AddProxyConfig(proxyType, proxyHost, (ushort)proxyPort, username, password);
if (_proxyConfigId == 0)
{
return false;
}
}
return await Task.FromResult(true);
}
catch (Exception ex)
{
var error = $"Failed to update proxy config: {ex.Message}";
await _updateFunc?.Invoke(true, error);
return false;
}
}
private async Task<bool> ApplyRoutesInternal()
{
if (_netBridgeService == null)
{
return false;
}
List<NetBridgeRuleConfig> rules;
rules = _ruleConfigs.Select(JsonUtils.DeepCopy).ToList();
foreach (var rule in rules.Where(x => x.RuleId > 0))
{
try
{
_ = _netBridgeService.DeleteRule(rule.RuleId);
}
catch
{
// ignored
}
}
for (var i = 0; i < rules.Count; i++)
{
var rule = rules[i];
var newRuleId = _netBridgeService.AddRule(rule.ProcessName, rule.TargetHosts, rule.TargetPorts, rule.Protocol, rule.Action, rule.ProxyConfigId);
if (newRuleId == 0)
{
return false;
}
rules[i].RuleId = newRuleId;
}
_ruleConfigs = rules;
return await Task.FromResult(true);
}
private static List<NetBridgeRuleConfig> BuildRuleConfigs(string? ruleProcess)
{
var processNames = Utils.String2List(Utils.Convert2Comma(ruleProcess));
return processNames.Select(processName => new NetBridgeRuleConfig
{
ProcessName = processName,
TargetHosts = "*",
TargetPorts = "*",
Protocol = "BOTH",
Action = "PROXY",
ProxyConfigId = 0
}).ToList();
}
}

View File

@@ -111,7 +111,7 @@ public class ProfileExManager
public async Task ClearAll()
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
_lstProfileEx = new();
_lstProfileEx = [];
}
public async Task SaveTo()
@@ -150,6 +150,14 @@ public class ProfileExManager
IndexIdEnqueue(indexId);
}
public void SetTestIpInfo(string indexId, string ipInfo)
{
var profileEx = GetProfileExItem(indexId);
profileEx.IpInfo = ipInfo;
IndexIdEnqueue(indexId);
}
public void SetSort(string indexId, int sort)
{
var profileEx = GetProfileExItem(indexId);
@@ -174,6 +182,6 @@ public class ProfileExManager
{
return 0;
}
return _lstProfileEx.Max(t => t == null ? 0 : t.Sort);
return _lstProfileEx.Max(t => t?.Sort ?? 0);
}
}

View File

@@ -45,7 +45,7 @@ public class StatisticsManager
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem ");
_serverStatItem = null;
_lstServerStat = new();
_lstServerStat = [];
}
public async Task SaveTo()

View File

@@ -56,7 +56,7 @@ public class TaskManager
{
//Logging.SaveLog("Execute delete expired files");
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1));
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1), "Test");
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
@@ -142,5 +142,10 @@ public class TaskManager
await _updateFunc?.Invoke(false, msg);
}
NoticeManager.Instance.Enqueue(string.Join("\n", msgs));
if (msgs.Count > 0)
{
AppEvents.HasUpdateNotified.Publish(true);
}
}
}

View File

@@ -29,6 +29,7 @@ public class Config
public SystemProxyItem SystemProxyItem { get; set; }
public WebDavItem WebDavItem { get; set; }
public CheckUpdateItem CheckUpdateItem { get; set; }
public NetBridgeItem NetBridgeItem { get; set; }
public Fragment4RayItem? Fragment4RayItem { get; set; }
public List<InItem> Inbound { get; set; }
public List<KeyEventItem> GlobalHotkeys { get; set; }

View File

@@ -7,10 +7,6 @@ public class CoreBasicItem
public string Loglevel { get; set; }
public bool MuxEnabled { get; set; }
public bool DefAllowInsecure { get; set; }
public string DefFingerprint { get; set; }
public string DefUserAgent { get; set; }
@@ -21,6 +17,8 @@ public class CoreBasicItem
public bool EnableFragment { get; set; }
public bool EnableFinalFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;
}
@@ -148,6 +146,7 @@ public class TunModeItem
public bool EnableIPv6Address { get; set; }
public string IcmpRouting { get; set; }
public bool EnableLegacyProtect { get; set; }
public List<string>? RouteExcludeAddress { get; set; }
}
[Serializable]
@@ -159,6 +158,8 @@ public class SpeedTestItem
public int MixedConcurrencyCount { get; set; }
public string IPAPIUrl { get; set; }
public string UdpTestTarget { get; set; }
public int? SpeedTestPageSize { get; set; }
public int? SpeedTestDelayInterval { get; set; }
}
[Serializable]
@@ -276,3 +277,9 @@ public class SimpleDNSItem
public string? Hosts { get; set; }
public string? DirectExpectedIPs { get; set; }
}
[Serializable]
public class NetBridgeItem
{
public string? RuleProcess { get; set; }
}

View File

@@ -13,6 +13,7 @@ public class CoreInfo
public string? DownloadUrlLinux64 { get; set; }
public string? DownloadUrlLinuxArm64 { get; set; }
public string? DownloadUrlLinuxRiscV64 { get; set; }
public string? DownloadUrlLinuxLoong64 { get; set; }
public string? DownloadUrlOSX64 { get; set; }
public string? DownloadUrlOSXArm64 { get; set; }
public string? Match { get; set; }

View File

@@ -93,6 +93,7 @@ public class Rule4Sbox
public List<string>? wifi_bssid { get; set; }
public bool? rule_set_ip_cidr_match_source { get; set; }
public bool? rule_set_ip_cidr_accept_empty { get; set; }
public bool? tls_record_fragment { get; set; }
}
[Serializable]
@@ -110,6 +111,7 @@ public class Inbound4Sbox
public bool? endpoint_independent_nat { get; set; }
public string? stack { get; set; }
public List<User4Sbox> users { get; set; }
public List<string>? route_exclude_address { get; set; }
}
public class User4Sbox

View File

@@ -86,7 +86,7 @@ public class Inboundsettings4Ray
public string? autoOutboundsInterface { get; set; }
// public List<string>? dns { get; set; }
public List<string>? dns { get; set; }
}
public class UsersItem4Ray
@@ -136,7 +136,7 @@ public class Outboundsettings4Ray
public Response4Ray? response { get; set; }
public string domainStrategy { get; set; }
public string? domainStrategy { get; set; }
public int? userLevel { get; set; }
@@ -276,6 +276,7 @@ public class BalancersItem4Ray
public List<string>? selector { get; set; }
public BalancersStrategy4Ray? strategy { get; set; }
public string? tag { get; set; }
public string? fallbackTag { get; set; }
}
public class BalancersStrategy4Ray
@@ -372,6 +373,7 @@ public class TlsSettings4Ray
public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; }
public List<CertificateSettings4Ray>? certificates { get; set; }
public string? verifyPeerCertByName { get; set; }
public string? pinnedPeerCertSha256 { get; set; }
public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; }
@@ -487,6 +489,7 @@ public class UdpHop4Ray
public class Finalmask4Ray
{
public List<Mask4Ray>? tcp { get; set; }
public List<Mask4Ray>? udp { get; set; }
public QuicParams4Ray? quicParams { get; set; }
}
@@ -499,8 +502,10 @@ public class Mask4Ray
public class MaskSettings4Ray
{
public string? header { get; set; }
public string? value { get; set; }
public string? password { get; set; }
public string? domain { get; set; }
// fragment
public string? packets { get; set; }

View File

@@ -17,3 +17,12 @@ public class LocationInfo
{
public string? country_code { get; set; }
}
public readonly record struct IpInfoResult(string Country, string? Ip)
{
public override string ToString()
{
var emoji = Utils.IsWindows() ? null : Country.CountryToEmoji();
return $"{emoji}({Country}) {Ip}";
}
}

View File

@@ -0,0 +1,12 @@
namespace ServiceLib.Models.Dto;
public sealed class NetBridgeRuleConfig
{
public uint RuleId { get; set; }
public string ProcessName { get; set; }
public string TargetHosts { get; set; }
public string TargetPorts { get; set; }
public string Protocol { get; set; }
public string Action { get; set; }
public uint ProxyConfigId { get; set; }
}

View File

@@ -26,6 +26,9 @@ public class ProfileItemModel : ReactiveObject
[Reactive]
public string SpeedVal { get; set; }
[Reactive]
public string IpInfo { get; set; }
[Reactive]
public string TodayUp { get; set; }

View File

@@ -1,26 +1,16 @@
namespace ServiceLib.Models.Dto;
public class RetResult
public class RetResult(bool success, string? msg, object? data)
{
public bool Success { get; set; }
public string? Msg { get; set; }
public object? Data { get; set; }
public RetResult(bool success = false)
public RetResult(bool success = false) : this(success, null, null)
{
Success = success;
}
public RetResult(bool success, string? msg)
public RetResult(bool success, string? msg) : this(success, msg, null)
{
Success = success;
Msg = msg;
}
public RetResult(bool success, string? msg, object? data)
{
Success = success;
Msg = msg;
Data = data;
}
public bool Success { get; set; } = success;
public string? Msg { get; set; } = msg;
public object? Data { get; set; } = data;
}

View File

@@ -8,4 +8,6 @@ public class SpeedTestResult
public string? Delay { get; set; }
public string? Speed { get; set; }
public string? IpInfo { get; set; }
}

View File

@@ -10,4 +10,5 @@ public class ProfileExItem
public decimal Speed { get; set; }
public int Sort { get; set; }
public string? Message { get; set; }
public string? IpInfo { get; set; }
}

View File

@@ -146,6 +146,11 @@ public class ProfileItem
TransportExtra = JsonUtils.Serialize(transportExtra, false);
}
public bool GetAllowInsecure()
{
return AllowInsecure == Global.StringTrue;
}
#endregion function
[PrimaryKey]
@@ -191,6 +196,7 @@ public class ProfileItem
public string Cert { get; set; }
public string CertSha { get; set; }
public string EchConfigList { get; set; }
public string VerifyPeerCertByName { get; set; }
public string Finalmask { get; set; }
public string ProtoExtra { get; set; }

View File

@@ -564,6 +564,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 IP Info 的本地化字符串。
/// </summary>
public static string LvTestIpInfo {
get {
return ResourceManager.GetString("LvTestIpInfo", resourceCulture);
}
}
/// <summary>
/// 查找类似 Speed (MB/s) 的本地化字符串。
/// </summary>
@@ -1311,6 +1320,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Process traffic hijacking (experimental) 的本地化字符串。
/// </summary>
public static string menuNetBridge {
get {
return ResourceManager.GetString("menuNetBridge", resourceCulture);
}
}
/// <summary>
/// 查找类似 New Update 的本地化字符串。
/// </summary>
public static string menuNewUpdate {
get {
return ResourceManager.GetString("menuNewUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Open the storage location 的本地化字符串。
/// </summary>
@@ -1896,6 +1923,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration. 的本地化字符串。
/// </summary>
public static string MsgAllowInsecureDeprecated {
get {
return ResourceManager.GetString("MsgAllowInsecureDeprecated", resourceCulture);
}
}
/// <summary>
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
/// </summary>
@@ -2085,6 +2121,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Conflict between {0} and {1} 的本地化字符串。
/// </summary>
public static string MsgOptionsConflict {
get {
return ResourceManager.GetString("MsgOptionsConflict", resourceCulture);
}
}
/// <summary>
/// 查找类似 Resolved {0} successfully 的本地化字符串。
/// </summary>
@@ -2202,6 +2247,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。
/// </summary>
public static string MsgTunRouteExcludeInvalidAddress {
get {
return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture);
}
}
/// <summary>
/// 查找类似 Unpacking... 的本地化字符串。
/// </summary>
@@ -2256,6 +2310,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 The process names that need to be proxied; separate multiple processes with commas. Processes not in the list will also be hijacked, but will connect directly. 的本地化字符串。
/// </summary>
public static string NetBridgeRuleTips {
get {
return ResourceManager.GetString("NetBridgeRuleTips", resourceCulture);
}
}
/// <summary>
/// 查找类似 Non-VMess or SS protocol 的本地化字符串。
/// </summary>
@@ -2988,6 +3051,33 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Enable Final Fragment 的本地化字符串。
/// </summary>
public static string TbEnableFinalFragment {
get {
return ResourceManager.GetString("TbEnableFinalFragment", resourceCulture);
}
}
/// <summary>
/// 查找类似 Split the tail of packets into smaller fragments. This may affect throughput and latency. 的本地化字符串。
/// </summary>
public static string TbEnableFinalFragmentTip {
get {
return ResourceManager.GetString("TbEnableFinalFragmentTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Enables process traffic hijacking (experimental), similar to Proxifer; can coexist with system proxy; conflicts with TUN mode, please do not enable them simultaneously; 的本地化字符串。
/// </summary>
public static string TbEnableNetBridge {
get {
return ResourceManager.GetString("TbEnableNetBridge", resourceCulture);
}
}
/// <summary>
/// 查找类似 Enable Tun 的本地化字符串。
/// </summary>
@@ -3510,6 +3600,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Route Exclude Address 的本地化字符串。
/// </summary>
public static string TbRouteExcludeAddress {
get {
return ResourceManager.GetString("TbRouteExcludeAddress", resourceCulture);
}
}
/// <summary>
/// 查找类似 Use commas (,) to separate. 的本地化字符串。
/// </summary>
public static string TbRouteExcludeAddressTip {
get {
return ResourceManager.GetString("TbRouteExcludeAddressTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary>
@@ -4680,6 +4788,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Verify Peer Cert By Name 的本地化字符串。
/// </summary>
public static string TbVerifyPeerCertByName {
get {
return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture);
}
}
/// <summary>
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
/// </summary>

View File

@@ -1740,4 +1740,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
</root>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

View File

@@ -1675,7 +1675,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Ajouter [NaïveProxy]</value>
</data>
<data name="TbInsecureConcurrency" xml:space="preserve">
<value>Insecure Concurrency</value>
<value>Concurrence non sûre</value>
</data>
<data name="TbUsername" xml:space="preserve">
<value>Nom dutilisateur</value>
@@ -1699,10 +1699,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Allow insecure cert fetch (self-signed)</value>
</data>
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
<value>Pour obtenir des certificats auto-signés uniquement. Risque MITM.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Adresse sortante locale (SendThrough)</value>
<value>Adresse de sortie locale (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
@@ -1717,24 +1717,30 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
</data>
<data name="menuUdpTestServer" xml:space="preserve">
<value>Test Configurations UDP Delay</value>
<value>Tester la latence UDP des profils</value>
</data>
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
<value>UDP Test Url</value>
<value>URL de test UDP</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
</data>
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Export v2rayN Internal Share Link to Clipboard</value>
<value>Exporter le lien interne v2rayN vers le presse-papiers</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
<value>{0} a une nouvelle version disponible: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
<value>Uniquement vérifier</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
<value>Non supporté</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>Détails IP</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>Nouvelle MAJ</value>
</data>
</root>

View File

@@ -1740,4 +1740,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

View File

@@ -1581,6 +1581,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
@@ -1740,4 +1743,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
</root>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration.</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Route Exclude Address</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Use commas (,) to separate.</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Invalid address in TUN route exclude list: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>Conflict between {0} and {1}</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>Enable Final Fragment</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
</data>
<data name="TbEnableNetBridge" xml:space="preserve">
<value>Enables process traffic hijacking (experimental), similar to Proxifer; can coexist with system proxy; conflicts with TUN mode, please do not enable them simultaneously;</value>
</data>
<data name="NetBridgeRuleTips" xml:space="preserve">
<value>The process names that need to be proxied; separate multiple processes with commas. Processes not in the list will also be hijacked, but will connect directly.</value>
</data>
<data name="menuNetBridge" xml:space="preserve">
<value>Process traffic hijacking (experimental)</value>
</data>
</root>

View File

@@ -121,7 +121,7 @@
<value>Ссылка успешно скопирована в буфер обмена</value>
</data>
<data name="CheckServerSettings" xml:space="preserve">
<value>Сначала проверьте настройки сервера</value>
<value>Недопустимая конфигурация, проверьте или выберите заново</value>
</data>
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
<value>Недопустимый формат конфигурации</value>
@@ -166,7 +166,7 @@
<value>Некорректная конфигурация. Проверьте</value>
</data>
<data name="InitialConfiguration" xml:space="preserve">
<value>Исходная конфигурация</value>
<value>Инициализация конфигурации</value>
</data>
<data name="IsLatestCore" xml:space="preserve">
<value>{0} {1} является последней версией</value>
@@ -256,7 +256,7 @@
<value>Операция не удалась, проверьте и повторите попытку</value>
</data>
<data name="PleaseFillRemarks" xml:space="preserve">
<value>Введите примечания</value>
<value>Введите псевдоним</value>
</data>
<data name="PleaseSelectEncryption" xml:space="preserve">
<value>Выберите метод шифрования</value>
@@ -307,7 +307,7 @@
<value>{0}: одно из обязательных полей</value>
</data>
<data name="LvRemarks" xml:space="preserve">
<value>Примечания</value>
<value>Псевдоним</value>
</data>
<data name="LvUrl" xml:space="preserve">
<value>URL (необязательно)</value>
@@ -334,13 +334,13 @@
<value>Введите корректный пользовательский DNS</value>
</data>
<data name="TransportPathTip1" xml:space="preserve">
<value>*Путь ws/http upgrade/xhttp</value>
<value>*Путь ws/httpupgrade/xhttp</value>
</data>
<data name="TransportPathTip2" xml:space="preserve">
<value>*HTTP2-путь</value>
</data>
<data name="TransportPathTip3" xml:space="preserve">
<value>*QUIC-ключ / KCP-seed</value>
<value>*Ключ шифрования QUIC</value>
</data>
<data name="TransportPathTip4" xml:space="preserve">
<value>Имя сервиса gRPC</value>
@@ -349,13 +349,13 @@
<value>*http-хосты, разделённые запятыми (,)</value>
</data>
<data name="TransportRequestHostTip2" xml:space="preserve">
<value>*Хост ws/http upgrade/xhttp</value>
<value>*Хост ws/httpupgrade/xhttp</value>
</data>
<data name="TransportRequestHostTip3" xml:space="preserve">
<value>*HTTP2-хосты, разделённые запятыми (,)</value>
</data>
<data name="TransportRequestHostTip4" xml:space="preserve">
<value>Безопасность *QUIC</value>
<value>*Метод шифрования QUIC</value>
</data>
<data name="TransportHeaderType1" xml:space="preserve">
<value>Тип raw-камуфляжа</value>
@@ -400,7 +400,7 @@
<value>Фильтр, нажмите Enter для выполнения</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Проверить обновления</value>
<value>Обновить</value>
</data>
<data name="menuClose" xml:space="preserve">
<value>Закрыть</value>
@@ -415,13 +415,13 @@
<value>Помощь</value>
</data>
<data name="menuOptionSetting" xml:space="preserve">
<value>Настройки</value>
<value>Параметры</value>
</data>
<data name="menuPromotion" xml:space="preserve">
<value>Продвижение</value>
</data>
<data name="menuReload" xml:space="preserve">
<value>Перезагрузить</value>
<value>Перезапустить службу</value>
</data>
<data name="menuRoutingSetting" xml:space="preserve">
<value>Настройки маршрутизации</value>
@@ -445,10 +445,10 @@
<value>Настройки групп подписки</value>
</data>
<data name="menuSubUpdate" xml:space="preserve">
<value>Обновить подписку без прокси</value>
<value>Обновить все подписки без прокси</value>
</data>
<data name="menuSubUpdateViaProxy" xml:space="preserve">
<value>Обновить подписку с прокси</value>
<value>Обновить все подписки через прокси</value>
</data>
<data name="menuSystemproxy" xml:space="preserve">
<value>Системный прокси</value>
@@ -562,7 +562,7 @@
<value>Сортировка</value>
</data>
<data name="LvUserAgent" xml:space="preserve">
<value>User-Agent</value>
<value>User-Agent (необязательно)</value>
</data>
<data name="TbCancel" xml:space="preserve">
<value>Отмена</value>
@@ -577,7 +577,7 @@
<value>Адрес</value>
</data>
<data name="TbAllowInsecure" xml:space="preserve">
<value>Разрешить небезопасные</value>
<value>Пропустить проверку сертификата (allowInsecure)</value>
</data>
<data name="TbAlpn" xml:space="preserve">
<value>ALPN</value>
@@ -616,10 +616,10 @@
<value>TLS</value>
</data>
<data name="TipNetwork" xml:space="preserve">
<value>*По-умолчанию raw</value>
<value>*По умолчанию raw; при неверном выборе подключение будет невозможно</value>
</data>
<data name="TbCoreType" xml:space="preserve">
<value>Ядро</value>
<value>Тип ядра</value>
</data>
<data name="TbFlow5" xml:space="preserve">
<value>Управление потоком (Flow)</value>
@@ -640,7 +640,7 @@
<value>Шифрование</value>
</data>
<data name="TbSecurity4" xml:space="preserve">
<value>Пользователь (необязательно)</value>
<value>Имя пользователя (необязательно)</value>
</data>
<data name="TbSecurity5" xml:space="preserve">
<value>Шифрование</value>
@@ -649,7 +649,7 @@
<value>Порт SOCKS</value>
</data>
<data name="TipPreSocksPort" xml:space="preserve">
<value>* После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости</value>
<value>*Порт SOCKS пользовательской конфигурации можно не указывать. Если он задан, Xray/sing-box (TUN) дополнительно запустит предварительную службу SOCKS для разделения трафика, отображения скорости и других функций</value>
</data>
<data name="TbBrowse" xml:space="preserve">
<value>Обзор</value>
@@ -682,7 +682,7 @@
<value>Настройки типа ядра</value>
</data>
<data name="TbSettingsDefAllowInsecure" xml:space="preserve">
<value>Разрешить небезопасные</value>
<value>По умолчанию пропускать проверку сертификата (allowInsecure)</value>
</data>
<data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve">
<value>«Freedom»: стратегия обработки доменов исходящего трафика</value>
@@ -703,7 +703,7 @@
<value>Показывать скорость в реальном времени (требуется перезапуск)</value>
</data>
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
<value>Сохранить старые при удалении дублей</value>
<value>При удалении дубликатов сохранять элементы с меньшим порядковым номером</value>
</data>
<data name="TbSettingsLogEnabled" xml:space="preserve">
<value>Записывать логи</value>
@@ -730,13 +730,13 @@
<value>Смешанный порт</value>
</data>
<data name="TbSettingsStartBoot" xml:space="preserve">
<value>Запускать при старте системы</value>
<value>Запускать при старте системы (может не сработать)</value>
</data>
<data name="TbSettingsStatistics" xml:space="preserve">
<value>Включить статистику трафика (требуется перезапуск)</value>
</data>
<data name="TbSettingsSubConvert" xml:space="preserve">
<value>URL конвертации подписок</value>
<value>URL конвертации подписок (необязательно)</value>
</data>
<data name="TbSettingsSystemproxy" xml:space="preserve">
<value>Настройки системного прокси</value>
@@ -787,13 +787,13 @@
<value>Запущено от имени администратора</value>
</data>
<data name="menuMoveBottom" xml:space="preserve">
<value>Спуститься вниз</value>
<value>Переместить в самый низ</value>
</data>
<data name="menuMoveDown" xml:space="preserve">
<value>Вниз</value>
</data>
<data name="menuMoveTop" xml:space="preserve">
<value>Подняться наверх</value>
<value>Переместить в самый верх</value>
</data>
<data name="menuMoveUp" xml:space="preserve">
<value>Вверх</value>
@@ -805,7 +805,7 @@
<value>Веб-сайт {0}</value>
</data>
<data name="menuRoutingAdvancedAdd" xml:space="preserve">
<value>Добавить</value>
<value>Добавить набор правил</value>
</data>
<data name="menuRoutingAdvancedImportRules" xml:space="preserve">
<value>Импортировать правила</value>
@@ -841,7 +841,7 @@
<value>Добавить правило</value>
</data>
<data name="menuRuleExportSelected" xml:space="preserve">
<value>Экспорт выделенных правил</value>
<value>Экспортировать выбранные правила в буфер обмена</value>
</data>
<data name="menuRuleList" xml:space="preserve">
<value>Список правил</value>
@@ -859,7 +859,7 @@
<value>Документация RuleObject</value>
</data>
<data name="TbDnsObjectDoc" xml:space="preserve">
<value>Поддерживаются DNS-объекты, нажмите для просмотра документации</value>
<value>Заполните DNS-объект (формат JSON), нажмите для просмотра документации</value>
</data>
<data name="SubUrlTips" xml:space="preserve">
<value>Для группы оставьте это поле пустым</value>
@@ -871,13 +871,13 @@
<value>Системные прокси изменены</value>
</data>
<data name="TbSettingsRouteOnly" xml:space="preserve">
<value>Только маршрут</value>
<value>Только для маршрутизации (routeOnly)</value>
</data>
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
<value>Не использовать прокси для локальных (интранет) адресов</value>
</data>
<data name="menuMixedTestServer" xml:space="preserve">
<value>Тест задержки и скорости всех серверов (Ctrl+E)</value>
<value>Многопоточный тест задержки и скорости (Ctrl+E)</value>
</data>
<data name="LvTestDelay" xml:space="preserve">
<value>Задержка (мс)</value>
@@ -886,10 +886,10 @@
<value>Скорость (МБ/с)</value>
</data>
<data name="FailedToRunCore" xml:space="preserve">
<value>Не удалось запустить ядро, посмотрите логи</value>
<value>Не удалось запустить ядро, см. сообщение</value>
</data>
<data name="LvFilter" xml:space="preserve">
<value>Фильтр по примечаниям (регулярные выражения)</value>
<value>Фильтр по псевдониму (регулярные выражения)</value>
</data>
<data name="TbDisplayLog" xml:space="preserve">
<value>Отображать журнал</value>
@@ -937,7 +937,7 @@
<value>Шрифт (требуется перезапуск)</value>
</data>
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек</value>
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и перезапустите приложение</value>
</data>
<data name="TbSettingsSocksPortTip" xml:space="preserve">
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
@@ -1009,7 +1009,7 @@
<value>Пользовательский DNS для sing-box</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value>
<value>Заполните структуру DNS (формат JSON), нажмите для просмотра документации</value>
</data>
<data name="TbSettingDnsImportDefConfig" xml:space="preserve">
<value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value>
@@ -1033,7 +1033,7 @@
<value>Добавить сервер [Hysteria2]</value>
</data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Максимальная пропускная способность Hysteria (загрузка/отдача)</value>
<value>Максимальная пропускная способность Hysteria (отдача/загрузка)</value>
</data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>Использовать системный файл hosts</value>
@@ -1045,13 +1045,13 @@
<value>Управление перегрузками</value>
</data>
<data name="LvPrevProfile" xml:space="preserve">
<value>Примечания к предыдущему прокси</value>
<value>Псевдоним предыдущего прокси</value>
</data>
<data name="LvNextProfile" xml:space="preserve">
<value>Примечания к следующему прокси</value>
<value>Псевдоним следующего прокси</value>
</data>
<data name="LvPrevProfileTip" xml:space="preserve">
<value>Убедитесь, что примечание существует и является уникальным</value>
<value>Убедитесь, что псевдоним существует и является уникальным</value>
</data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>Автоматическая маршрутизация</value>
@@ -1177,7 +1177,7 @@
<value>Глобальный режим</value>
</data>
<data name="menuModeNothing" xml:space="preserve">
<value>Не менять</value>
<value>Как в исходной конфигурации</value>
</data>
<data name="menuModeRule" xml:space="preserve">
<value>Правила</value>
@@ -1210,7 +1210,7 @@
<value>Экспорт ссылок в формате Base64 в буфер обмена</value>
</data>
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
<value>Экспортировать полную конфигурацию в буфер обмена</value>
<value>Экспортировать выбранную полную конфигурацию в буфер обмена</value>
</data>
<data name="menuShowOrHideMainWindow" xml:space="preserve">
<value>Показать или скрыть главное окно</value>
@@ -1300,7 +1300,7 @@
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
</data>
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
<value>Установите шрифт в систему, выберите или введите имя шрифта, перезапустите настройки</value>
<value>Установите шрифт в систему, выберите или введите имя шрифта и перезапустите приложение</value>
</data>
<data name="menuExitTips" xml:space="preserve">
<value>Вы уверены, что хотите выйти?</value>
@@ -1318,13 +1318,13 @@
<value>XHTTP-режим</value>
</data>
<data name="TransportExtraTip" xml:space="preserve">
<value>Сырой JSON, формат: { XHTTP Object }</value>
<value>Сырой JSON, формат: { XHTTPObject }</value>
</data>
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
<value>Сворачивать в трей при закрытии окна</value>
</data>
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
<value>Количество одновременно выполняемых тестов при многоэтапном тестировании</value>
<value>Количество параллельных задач при многопоточном тестировании</value>
</data>
<data name="TbSettingsExceptionTip2" xml:space="preserve">
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
@@ -1336,7 +1336,7 @@
<value>Включить второй смешанный порт</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
<value>tun: входящий TUN, socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>Тема</value>
@@ -1357,7 +1357,7 @@
<value>Удалено {0} недействительных</value>
</data>
<data name="TbPorts7" xml:space="preserve">
<value>Диапазон портов сервера</value>
<value>Диапазон портов для переключения (Port Hopping)</value>
</data>
<data name="TbPorts7Tips" xml:space="preserve">
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
@@ -1369,7 +1369,7 @@
<value>URL для тестирования текущего соединения</value>
</data>
<data name="TbRuleOutboundTagTip" xml:space="preserve">
<value>Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально</value>
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
</data>
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
<value>Неверный пароль, попробуйте ещё раз.</value>
@@ -1384,7 +1384,7 @@
<value>Удалённый DNS</value>
</data>
<data name="TbDomesticDNS" xml:space="preserve">
<value>Внутренний DNS</value>
<value>DNS для прямых подключений</value>
</data>
<data name="TbDirectResolveStrategy" xml:space="preserve">
<value>Стратегия разрешения прямых соединений</value>
@@ -1489,13 +1489,13 @@
<value>Тип группы политик</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Добавить группу политик </value>
<value>Добавить группу политик</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Добавить цепочку прокси</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Добавить дочернюю конфигурацию </value>
<value>Добавить дочернюю конфигурацию</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Удалить дочернюю конфигурацию </value>
@@ -1534,7 +1534,7 @@
<value>Bootstrap DNS</value>
</data>
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Разрешает домены DNS-серверов, требуется IP-адрес</value>
<value>Для разрешения доменных имён DNS-серверов необходимо указать IP-адрес</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>Тест реальной задержки</value>
@@ -1547,7 +1547,7 @@
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Разрешить небезопасные» отключится.
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
</data>
@@ -1732,12 +1732,18 @@
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
<value>Доступна новая версия {0}: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
<value>Проверить</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
<value>Не поддерживается</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>Информация об IP</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>Доступно обновление</value>
</data>
</root>

View File

@@ -1737,4 +1737,43 @@
<data name="MsgNotSupport" xml:space="preserve">
<value>不支持</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 信息</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 </value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>路由排除地址</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>使用逗号 (,) 分隔</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>TUN 路由排除列表包含无效地址:{0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>{0} 与 {1} 冲突</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>启用末端分片</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。</value>
</data>
<data name="TbEnableNetBridge" xml:space="preserve">
<value>启用进程流量劫持(实验性),类似 Proxifer 功能;可以和系统代理并存;和 TUN 模式冲突,请不要同时开启;</value>
</data>
<data name="NetBridgeRuleTips" xml:space="preserve">
<value>需要代理的进程名,多个进程请用逗号分隔;不在列表中的进程也会被劫持,但是会直连</value>
</data>
<data name="menuNetBridge" xml:space="preserve">
<value>进程流量劫持(实验性)</value>
</data>
</root>

View File

@@ -1333,7 +1333,7 @@
<value>開啟第二個本機監聽埠</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks埠,socks2第二個本埠,socks3區域網路埠</value>
<value>TunTUN 入站Socks機連接埠,Socks2第二個本機連接埠,Socks3區域網路連接埠</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>主題</value>
@@ -1737,4 +1737,43 @@
<data name="MsgNotSupport" xml:space="preserve">
<value>不支援</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 資訊</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。</value>
</data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>路由排除位址</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>使用逗號 (,) 分隔</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>TUN 路由排除清單中的位址無效: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>{0} 與 {1} 發生衝突</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>啟用末端分片</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲</value>
</data>
<data name="TbEnableNetBridge" xml:space="preserve">
<value>啟用進程流量劫持(實驗性),類似 Proxifer 功能;可以和系統代理並存;和 TUN 模式衝突,請不要同時開啟;</value>
</data>
<data name="NetBridgeRuleTips" xml:space="preserve">
<value>需要代理程式的進程名,多個進程請用逗號分隔;不在清單中的進程也會被劫持,但是會直連</value>
</data>
<data name="menuNetBridge" xml:space="preserve">
<value>進程流量劫持(實驗性)</value>
</data>
</root>

View File

@@ -8,6 +8,9 @@
"172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
"dns": [
"172.18.0.1"
],
"autoSystemRoutingTable": [
"0.0.0.0/0",
"::/0"

View File

@@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Downloader" />
<PackageReference Include="IPNetwork2" />
<PackageReference Include="ReactiveUI">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
@@ -56,7 +57,7 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<EmbeddedResource Update="Resx\ResUI.fa-Ir.resx">
<EmbeddedResource Update="Resx\ResUI.fa.resx">
<SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
@@ -86,6 +87,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NetBridge\src\NetBridgeLib\NetBridgeLib.csproj" />
<ProjectReference Include="..\ServiceLib.UdpTest\ServiceLib.UdpTest.csproj" />
</ItemGroup>

View File

@@ -3,16 +3,10 @@ namespace ServiceLib.Services.CoreConfig;
/// <summary>
/// Core configuration file processing class
/// </summary>
public class CoreConfigClashService
public class CoreConfigClashService(Config config)
{
private Config _config;
private static readonly string _tag = "CoreConfigClashService";
public CoreConfigClashService(Config config)
{
_config = config;
}
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
{
var ret = new RetResult();
@@ -56,7 +50,7 @@ public class CoreConfigClashService
var tagYamlStr1 = "!<str>";
var tagYamlStr2 = "__strn__";
var tagYamlStr3 = "!!str";
var txtFile = File.ReadAllText(addressFileName);
var txtFile = await File.ReadAllTextAsync(addressFileName);
txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2);
//YAML anchors
@@ -75,13 +69,13 @@ public class CoreConfigClashService
//mixed-port
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
//log-level
fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel);
fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
//external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Remove("secret");
//allow-lan
if (_config.Inbound.First().AllowLANConn)
if (config.Inbound.First().AllowLANConn)
{
fileContent["allow-lan"] = "true";
fileContent["bind-address"] = "*";
@@ -92,23 +86,23 @@ public class CoreConfigClashService
}
//ipv6
fileContent["ipv6"] = _config.ClashUIItem.EnableIPv6;
fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
//mode
if (!fileContent.ContainsKey("mode"))
{
fileContent["mode"] = ERuleMode.Rule.ToString().ToLower();
fileContent["mode"] = nameof(ERuleMode.Rule).ToLower();
}
else
{
if (_config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
if (config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
{
fileContent["mode"] = _config.ClashUIItem.RuleMode.ToString().ToLower();
fileContent["mode"] = config.ClashUIItem.RuleMode.ToString().ToLower();
}
}
//enable tun mode
if (_config.TunModeItem.EnableTun)
if (config.TunModeItem.EnableTun)
{
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
if (tun.IsNotEmpty())
@@ -156,7 +150,7 @@ public class CoreConfigClashService
private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node)
{
if (!_config.ClashUIItem.EnableMixinContent)
if (!config.ClashUIItem.EnableMixinContent)
{
return;
}
@@ -177,7 +171,7 @@ public class CoreConfigClashService
}
foreach (var item in mixinContent)
{
if (!_config.TunModeItem.EnableTun && item.Key == "tun")
if (!config.TunModeItem.EnableTun && item.Key == "tun")
{
continue;
}
@@ -220,9 +214,8 @@ public class CoreConfigClashService
return;
}
if (!blRemoved && !fileContent.ContainsKey(key))
if (!blRemoved && fileContent.TryAdd(key, value))
{
fileContent.Add(key, value);
return;
}
var lstOri = (List<object>)fileContent[key];
@@ -244,7 +237,7 @@ public class CoreConfigClashService
}
else
{
lstValue.ForEach(item => lstOri.Add(item));
lstValue.ForEach(lstOri.Add);
}
}

View File

@@ -153,7 +153,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
{
listen = Global.Loopback,
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
type = nameof(EInboundProtocol.mixed),
};
inbound.tag = inbound.type + inbound.listen_port.ToString();
_coreConfig.inbounds.Add(inbound);
@@ -229,7 +229,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
tag = $"{EInboundProtocol.mixed}{port}",
listen = Global.Loopback,
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
type = nameof(EInboundProtocol.mixed),
});
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();

View File

@@ -23,7 +23,7 @@ public partial class CoreConfigSingboxService
}
// Process outbounds
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : [];
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
foreach (var outbound in _coreConfig.outbounds)
{
if (outbound.type.ToLower() is "direct" or "block")
@@ -42,9 +42,9 @@ public partial class CoreConfigSingboxService
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints
if (_coreConfig.endpoints != null && _coreConfig.endpoints.Count > 0)
if (_coreConfig.endpoints is { Count: > 0 })
{
var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : [];
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
foreach (var endpoint in _coreConfig.endpoints)
{
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())

View File

@@ -26,7 +26,7 @@ public partial class CoreConfigSingboxService
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
if (rules?.LastOrDefault() is { OutboundTag: Global.DirectTag } lastRule)
{
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
@@ -82,7 +82,7 @@ public partial class CoreConfigSingboxService
if (simpleDnsItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts != null && systemHosts.Count > 0)
if (systemHosts is { Count: > 0 })
{
foreach (var host in systemHosts)
{
@@ -182,13 +182,13 @@ public partial class CoreConfigSingboxService
{
server = Global.SingboxRemoteDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy),
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
},
new Rule4Sbox
{
server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
}
});
@@ -454,12 +454,12 @@ public partial class CoreConfigSingboxService
dns4Sbox.rules.Insert(0, new()
{
server = tag,
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
});
dns4Sbox.rules.Insert(0, new()
{
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote",
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
});
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;

View File

@@ -15,8 +15,8 @@ public partial class CoreConfigSingboxService
{
var inbound = new Inbound4Sbox()
{
type = EInboundProtocol.mixed.ToString(),
tag = EInboundProtocol.socks.ToString(),
type = nameof(EInboundProtocol.mixed),
tag = nameof(EInboundProtocol.socks),
listen = Global.Loopback,
};
_coreConfig.inbounds.Add(inbound);
@@ -71,6 +71,7 @@ public partial class CoreConfigSingboxService
{
tunInbound.address = ["172.18.0.1/30"];
}
tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress;
_coreConfig.inbounds.Add(tunInbound);
}
@@ -86,7 +87,7 @@ public partial class CoreConfigSingboxService
var inbound = JsonUtils.DeepCopy(inItem);
inbound.tag = protocol.ToString();
inbound.listen_port = inItem.listen_port + (int)protocol;
inbound.type = EInboundProtocol.mixed.ToString();
inbound.type = nameof(EInboundProtocol.mixed);
return inbound;
}
}

View File

@@ -190,15 +190,16 @@ public partial class CoreConfigSingboxService
outbound.packet_encoding = "xudp";
if (!protocolExtra.Flow.IsNullOrEmpty())
if (protocolExtra.Flow is "xtls-rprx-vision" or "xtls-rprx-vision-udp443")
{
outbound.flow = "xtls-rprx-vision";
}
else if (!protocolExtra.Flow.IsNullOrEmpty())
{
outbound.flow = protocolExtra.Flow;
}
else
{
FillOutboundMux(outbound);
}
FillOutboundMux(outbound);
FillOutboundTransport(outbound);
break;
}
@@ -342,7 +343,7 @@ public partial class CoreConfigSingboxService
{
try
{
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
var muxEnabled = _node.MuxEnabled ?? false;
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
{
var mux = new Multiplex4Sbox()
@@ -396,7 +397,7 @@ public partial class CoreConfigSingboxService
enabled = true,
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
server_name = serverName,
insecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
insecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(),
};
if (_node.Fingerprint.IsNotEmpty())
@@ -729,8 +730,8 @@ public partial class CoreConfigSingboxService
{
return;
}
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
var outbounds = servers.OfType<Outbound4Sbox>().ToList();
var endpoints = servers.OfType<Endpoints4Sbox>().ToList();
singboxConfig.endpoints ??= [];
if (prepend)
{

View File

@@ -96,6 +96,15 @@ public partial class CoreConfigSingboxService
new() { protocol = ["dns"] },
],
});
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
protocol = ["tls"],
action = "route-options",
tls_record_fragment = true,
});
}
}
else
{
@@ -104,6 +113,14 @@ public partial class CoreConfigSingboxService
port = [53],
action = "hijack-dns",
});
if (_config.CoreBasicItem.EnableFinalFragment)
{
_coreConfig.route.rules.Add(new()
{
action = "route-options",
tls_record_fragment = true,
});
}
}
var hostsDomains = new List<string>();
@@ -176,12 +193,12 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
clash_mode = ERuleMode.Direct.ToString()
clash_mode = nameof(ERuleMode.Direct)
});
_coreConfig.route.rules.Add(new()
{
outbound = Global.ProxyTag,
clash_mode = ERuleMode.Global.ToString()
clash_mode = nameof(ERuleMode.Global)
});
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();

View File

@@ -18,15 +18,15 @@ public partial class CoreConfigSingboxService
//convert route geosite & geoip to ruleset
foreach (var rule in _coreConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
rule.geosite = null;
AddRuleSets(ruleSets, rule.rule_set);
}
foreach (var rule in _coreConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
rule.geoip = null;
AddRuleSets(ruleSets, rule.rule_set);
}
@@ -34,14 +34,14 @@ public partial class CoreConfigSingboxService
//convert dns geosite & geoip to ruleset
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
rule.geosite = null;
}
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
rule.rule_set ??= [];
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
rule.geoip = null;
}
foreach (var dnsRule in _coreConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? [])

View File

@@ -60,6 +60,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
@@ -161,7 +165,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
protocol = nameof(EInboundProtocol.mixed),
settings = new Inboundsettings4Ray()
{
udp = true,
@@ -204,6 +208,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
@@ -262,7 +270,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
tag = $"{EInboundProtocol.socks}{port}",
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
protocol = nameof(EInboundProtocol.mixed),
settings = new Inboundsettings4Ray()
{
udp = true,
@@ -276,6 +284,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
if (_config.CoreBasicItem.EnableFinalFragment)
{
ApplyFinalFragment();
}
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();

View File

@@ -56,7 +56,7 @@ public partial class CoreConfigV2rayService
}
else
{
_coreConfig.burstObservatory.subjectSelector ??= new();
_coreConfig.burstObservatory.subjectSelector ??= [];
_coreConfig.burstObservatory.subjectSelector.Add(baseTagName);
}
}
@@ -75,7 +75,7 @@ public partial class CoreConfigV2rayService
}
else
{
_coreConfig.observatory.subjectSelector ??= new();
_coreConfig.observatory.subjectSelector ??= [];
_coreConfig.observatory.subjectSelector.Add(baseTagName);
}
}
@@ -104,8 +104,9 @@ public partial class CoreConfigV2rayService
},
},
tag = balancerTag,
fallbackTag = multipleLoad == EMultipleLoad.Fallback ? Global.DirectTag : null,
};
_coreConfig.routing.balancers ??= new();
_coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer);
}
}

View File

@@ -46,10 +46,7 @@ public partial class CoreConfigV2rayService
}
// Ensure routing node exists
if (fullConfigTemplateNode["routing"] == null)
{
fullConfigTemplateNode["routing"] = new JsonObject();
}
fullConfigTemplateNode["routing"] ??= new JsonObject();
// Handle balancers - append instead of override
if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode)

View File

@@ -34,7 +34,7 @@ public partial class CoreConfigV2rayService
return;
}
var simpleDnsItem = context.SimpleDnsItem;
var dnsItem = _coreConfig.dns is Dns4Ray dns4Ray ? dns4Ray : new Dns4Ray();
var dnsItem = _coreConfig.dns as Dns4Ray ?? new Dns4Ray();
var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs;
//Outbound Freedom domainStrategy
@@ -246,7 +246,7 @@ public partial class CoreConfigV2rayService
var useDirectDns = false;
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
if (rules?.LastOrDefault() is { OutboundTag: Global.DirectTag } lastRule)
{
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
@@ -384,9 +384,11 @@ public partial class CoreConfigV2rayService
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null)
{
outbound.settings = new();
outbound.settings.domainStrategy = domainStrategy4Freedom;
outbound.settings.userLevel = 0;
outbound.settings = new()
{
domainStrategy = domainStrategy4Freedom,
userLevel = 0,
};
}
}

View File

@@ -30,14 +30,19 @@ public partial class CoreConfigV2rayService
inbound3.listen = listen;
_coreConfig.inbounds.Add(inbound3);
//auth
// 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 }
};
inbound3.settings.accounts =
[
new()
{
user = _config.Inbound.First().User,
pass = _config.Inbound.First().Pass,
},
];
}
}
else
@@ -53,12 +58,15 @@ public partial class CoreConfigV2rayService
{
_config.TunModeItem.Mtu = Global.TunMtus.First();
}
var tunInbound = JsonUtils.Deserialize<Inbounds4Ray>(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? new Inbounds4Ray { };
var tunInbound =
JsonUtils.Deserialize<Inbounds4Ray>(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ??
new Inbounds4Ray();
tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun";
tunInbound.settings.MTU = _config.TunModeItem.Mtu;
if (_config.TunModeItem.EnableIPv6Address == false)
if (!_config.TunModeItem.EnableIPv6Address)
{
tunInbound.settings.gateway = ["172.18.0.1/30"];
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
}
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty())
@@ -66,6 +74,53 @@ public partial class CoreConfigV2rayService
tunInbound.settings.autoOutboundsInterface = bindInterface;
}
tunInbound.sniffing = inbound.sniffing;
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
var wholeInternet = IPNetwork2.Parse("0.0.0.0/0");
var wholeInternetV6 = IPNetwork2.Parse("::/0");
var excludeList = _config.TunModeItem.RouteExcludeAddress.Select(IPNetwork2.Parse)
.Where(x => x != null).ToList();
var includeList = new List<IPNetwork2> { wholeInternet };
var includeListV6 = new List<IPNetwork2> { wholeInternetV6 };
foreach (var exclude in excludeList)
{
var temp = new List<IPNetwork2>();
if (exclude.AddressFamily == AddressFamily.InterNetwork)
{
foreach (var net in includeList)
{
temp.AddRange(net.Subtract(exclude));
}
includeList = temp;
}
else if (exclude.AddressFamily == AddressFamily.InterNetworkV6)
{
foreach (var net in includeListV6)
{
temp.AddRange(net.Subtract(exclude));
}
includeListV6 = temp;
}
}
includeList = IPNetwork2.Supernet(includeList.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();
}
else
{
tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList();
}
}
_coreConfig.inbounds.Add(tunInbound);
}
}
@@ -90,7 +145,7 @@ public partial class CoreConfigV2rayService
}
inbound.tag = protocol.ToString();
inbound.port = inItem.LocalPort + (int)protocol;
inbound.protocol = EInboundProtocol.mixed.ToString();
inbound.protocol = nameof(EInboundProtocol.mixed);
inbound.settings.udp = inItem.UdpEnabled;
inbound.sniffing.enabled = inItem.SniffingEnabled;
inbound.sniffing.destOverride = inItem.DestOverride;

View File

@@ -62,7 +62,7 @@ public partial class CoreConfigV2rayService
try
{
var protocolExtra = _node.GetProtocolExtra();
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
var muxEnabled = _node.MuxEnabled ?? false;
switch (_node.ConfigType)
{
case EConfigType.VMess:
@@ -243,9 +243,9 @@ public partial class CoreConfigV2rayService
version = 2,
address = _node.Address,
port = _node.Port,
vnext = null,
servers = null,
};
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
case EConfigType.WireGuard:
@@ -380,10 +380,11 @@ public partial class CoreConfigV2rayService
TlsSettings4Ray tlsSettings = new()
{
allowInsecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
allowInsecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(),
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
echConfigList = _node.EchConfigList.NullIfEmpty(),
verifyPeerCertByName = _node.VerifyPeerCertByName.NullIfEmpty(),
};
if (sni.IsNotEmpty())
{
@@ -449,14 +450,13 @@ public partial class CoreConfigV2rayService
KcpSettings4Ray kcpSettings = new()
{
mtu = kcpMtu,
tti = _config.KcpItem.Tti
tti = _config.KcpItem.Tti,
uplinkCapacity = _config.KcpItem.UplinkCapacity,
downlinkCapacity = _config.KcpItem.DownlinkCapacity,
cwndMultiplier = _config.KcpItem.CwndMultiplier,
maxSendingWindow = _config.KcpItem.MaxSendingWindow,
};
kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity;
kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity;
kcpSettings.cwndMultiplier = _config.KcpItem.CwndMultiplier;
kcpSettings.maxSendingWindow = _config.KcpItem.MaxSendingWindow;
var kcpFinalmask = new Finalmask4Ray();
if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header))
{
@@ -464,8 +464,8 @@ public partial class CoreConfigV2rayService
[
new Mask4Ray
{
type = header,
settings = null
type = "mkcp-legacy",
settings = new MaskSettings4Ray { header = header },
}
];
}
@@ -474,15 +474,15 @@ public partial class CoreConfigV2rayService
{
kcpFinalmask.udp.Add(new Mask4Ray
{
type = "mkcp-original"
type = "mkcp-legacy",
});
}
else
{
kcpFinalmask.udp.Add(new Mask4Ray
{
type = "mkcp-aes128gcm",
settings = new MaskSettings4Ray { password = kcpSeed }
type = "mkcp-legacy",
settings = new MaskSettings4Ray { value = kcpSeed },
});
}
streamSettings.kcpSettings = kcpSettings;
@@ -530,7 +530,7 @@ public partial class CoreConfigV2rayService
break;
//xhttp
case nameof(ETransport.xhttp):
streamSettings.network = ETransport.xhttp.ToString();
streamSettings.network = nameof(ETransport.xhttp);
XhttpSettings4Ray xhttpSettings = new();
if (path.IsNotEmpty())
@@ -825,29 +825,7 @@ public partial class CoreConfigV2rayService
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
.ToList();
var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello";
var configLength = _config.Fragment4RayItem?.Length ?? "50-100";
var configDelay = _config.Fragment4RayItem?.Interval ?? "10-20";
var fragmentMask = new Mask4Ray
{
type = "fragment",
settings = new MaskSettings4Ray
{
packets = configPackets,
length = configLength,
delay = configDelay,
}
};
var noiseMask = new Mask4Ray
{
type = "noise",
settings = new MaskSettings4Ray
{
length = "10-20",
delay = "10-16",
}
};
var (fragmentMask, noiseMask) = BuildFragmentsMasks();
foreach (var outbound in actOutboundWithTlsList)
{
@@ -881,4 +859,67 @@ public partial class CoreConfigV2rayService
outbound.streamSettings.finalmask = finalMaskJsonObj;
}
}
private void ApplyFinalFragment()
{
var (fragmentMask, noiseMask) = BuildFragmentsMasks();
var actOutboundList = _coreConfig.outbounds.Where(n => n.tag.StartsWith(Global.ProxyTag)).ToList();
var fragmentFreedom = new Outbounds4Ray()
{
tag = $"{Global.ProxyTag}-fragment-freedom",
protocol = "freedom",
streamSettings = new StreamSettings4Ray
{
finalmask = new Finalmask4Ray
{
tcp = [fragmentMask],
udp = [noiseMask],
}
}
};
foreach (var outbound in actOutboundList)
{
var index = _coreConfig.outbounds.IndexOf(outbound);
var originalTag = outbound.tag;
var afterTag = $"fragment-{originalTag}";
var cloneFragmentFreedom = JsonUtils.DeepCopy(fragmentFreedom);
cloneFragmentFreedom.tag = originalTag;
cloneFragmentFreedom.streamSettings.sockopt ??= new();
cloneFragmentFreedom.streamSettings.sockopt.dialerProxy = afterTag;
outbound.tag = afterTag;
_coreConfig.outbounds.Insert(index, cloneFragmentFreedom);
}
}
private (Mask4Ray tcpFragment, Mask4Ray udpNoise) BuildFragmentsMasks()
{
var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello";
var configLength = _config.Fragment4RayItem?.Length ?? "50-100";
var configDelay = _config.Fragment4RayItem?.Interval ?? "10-20";
var fragmentMask = new Mask4Ray
{
type = "fragment",
settings = new MaskSettings4Ray
{
packets = configPackets,
length = configLength,
delay = configDelay,
}
};
var noiseMask = new Mask4Ray
{
type = "noise",
settings = new MaskSettings4Ray
{
length = "10-20",
delay = "10-16",
}
};
return (fragmentMask, noiseMask);
}
}

View File

@@ -6,7 +6,7 @@ public partial class CoreConfigV2rayService
{
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{
var tag = EInboundProtocol.api.ToString();
const string tag = nameof(EInboundProtocol.api);
Metrics4Ray apiObj = new();
Policy4Ray policyObj = new();
SystemPolicy4Ray policySystemSetting = new();

View File

@@ -3,7 +3,7 @@ using System.Net.Http.Headers;
namespace ServiceLib.Services;
/// <summary>
///Download
/// Download
/// </summary>
public class DownloadService
{
@@ -13,7 +13,10 @@ public class DownloadService
private static readonly string _tag = "DownloadService";
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
/// <summary>
/// Downloads data with the specified proxy and reports progress messages.
/// </summary>
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
{
try
{
@@ -36,6 +39,9 @@ public class DownloadService
return 0;
}
/// <summary>
/// Downloads a file and reports progress through events.
/// </summary>
public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout)
{
try
@@ -64,6 +70,9 @@ public class DownloadService
}
}
/// <summary>
/// Gets redirect target URL without following redirects automatically.
/// </summary>
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
{
var webRequestHandler = new SocketsHttpHandler
@@ -86,11 +95,23 @@ public class DownloadService
}
}
/// <summary>
/// Tries to download string content using proxy switch setting.
/// </summary>
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
{
var webProxy = await GetWebProxy(blProxy);
return await TryDownloadString(url, webProxy, userAgent);
}
/// <summary>
/// Tries to download string content with a specified proxy.
/// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
{
try
{
var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15);
var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15);
if (result1.IsNotEmpty())
{
return result1;
@@ -108,7 +129,7 @@ public class DownloadService
try
{
var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15);
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15);
if (result2.IsNotEmpty())
{
return result2;
@@ -128,14 +149,12 @@ public class DownloadService
}
/// <summary>
/// DownloadString
/// Downloads string content via HttpClient.
/// </summary>
/// <param name="url"></param>
private async Task<string?> DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
{
try
{
var webProxy = await GetWebProxy(blProxy);
var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
@@ -172,15 +191,12 @@ public class DownloadService
}
/// <summary>
/// DownloadString
/// Downloads string content via DownloaderHelper.
/// </summary>
/// <param name="url"></param>
private async Task<string?> DownloadStringViaDownloader(string url, bool blProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
{
try
{
var webProxy = await GetWebProxy(blProxy);
if (userAgent.IsNullOrEmpty())
{
userAgent = Utils.GetVersion(false);
@@ -200,6 +216,9 @@ public class DownloadService
return null;
}
/// <summary>
/// Creates local SOCKS proxy when proxy switch is enabled.
/// </summary>
private async Task<WebProxy?> GetWebProxy(bool blProxy)
{
if (!blProxy)
@@ -215,6 +234,9 @@ public class DownloadService
return new WebProxy($"socks5://{Global.Loopback}:{port}");
}
/// <summary>
/// Checks whether the specified TCP endpoint is reachable.
/// </summary>
private async Task<bool> SocketCheck(string ip, int port)
{
try

View File

@@ -7,7 +7,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static readonly string _tag = "SpeedtestService";
private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = new();
private static readonly ConcurrentBag<string> _lstExitLoop = [];
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
{
@@ -31,7 +33,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static bool ShouldStopTest(string exitLoopKey)
{
return !_lstExitLoop.Any(p => p == exitLoopKey);
return _lstExitLoop.All(p => p != exitLoopKey);
}
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds)
@@ -135,36 +137,43 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task RunTcpingAsync(List<ServerTestItem> selecteds)
{
List<Task> tasks = [];
foreach (var it in selecteds)
{
tasks.Add(Task.Run(async () =>
{
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
foreach (var lst in lstBatch)
{
List<Task> tasks = [];
foreach (var it in lst)
{
tasks.Add(Task.Run(async () =>
{
Logging.SaveLog(_tag, ex);
}
}));
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}));
}
await Task.WhenAll(tasks);
await Task.Delay(_delayInterval);
}
await Task.WhenAll(tasks);
}
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
{
if (pageSize <= 0)
{
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize;
pageSize = Math.Min(lstSelected.Count, _speedTestPageSize);
}
var lstTest = GetTestBatchItem(lstSelected, pageSize);
List<ServerTestItem> lstFailed = new();
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunRealPingAsync(lst, exitLoopKey);
@@ -172,7 +181,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
lstFailed.AddRange(lst);
}
await Task.Delay(100);
await Task.Delay(_delayInterval);
}
//Retest the failed part
@@ -210,7 +219,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
}
await Task.Delay(1000);
List<Task> tasks = new();
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (!it.AllowTest)
@@ -249,11 +258,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
if (pageSize <= 0)
{
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize;
pageSize = Math.Min(lstSelected.Count, _speedTestPageSize);
}
var lstTest = GetTestBatchItem(lstSelected, pageSize);
List<ServerTestItem> lstFailed = new();
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunUdpTestAsync(lst, exitLoopKey);
@@ -261,7 +270,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
lstFailed.AddRange(lst);
}
await Task.Delay(100);
await Task.Delay(_delayInterval);
}
//Retest the failed part
@@ -291,7 +300,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
}
await Task.Delay(1000);
List<Task> tasks = new();
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (!it.AllowTest)
@@ -329,7 +338,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
var downloadHandle = new DownloadService();
List<Task> tasks = new();
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (ShouldStopTest(exitLoopKey))
@@ -392,10 +401,23 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoRealPing(ServerTestItem it)
{
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
if (responseTime > 0)
{
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipStr = ipInfo?.ToString() ?? Global.None;
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
await UpdateIpInfoFunc(it.IndexId, ipStr);
}
else
{
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
}
return responseTime;
}
@@ -467,7 +489,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
{
List<List<ServerTestItem>> lstTest = new();
List<List<ServerTestItem>> lstTest = [];
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
@@ -491,4 +513,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestMessage(indexId, speed);
}
}
private async Task UpdateIpInfoFunc(string indexId, string ip)
{
await _updateFunc?.Invoke(new() { IndexId = indexId, IpInfo = ip });
}
}

View File

@@ -117,6 +117,11 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
{
if (!(_config.CheckUpdateItem.SelectedCoreTypes?.Contains(type.ToString()) ?? true))
{
continue;
}
var result = await CheckHasUpdateOnly(type, preRelease);
if (result.Success && result.Version != null)
{
@@ -337,6 +342,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
{
Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64,
Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64,
Architecture.LoongArch64 => coreInfo?.DownloadUrlLinuxLoong64,
Architecture.X64 => coreInfo?.DownloadUrlLinux64,
_ => null,
};

View File

@@ -31,8 +31,7 @@ public sealed class WindowsJobService : IDisposable
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr,
(uint)length))
{
throw new Exception(string.Format("Unable to set information. Error: {0}",
Marshal.GetLastWin32Error()));
throw new Exception($"Unable to set information. Error: {Marshal.GetLastWin32Error()}");
}
}
finally

View File

@@ -216,7 +216,7 @@ public class AddGroupServerViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
return;
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : Enum.Parse<ECoreType>(CoreType);
if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) ||
SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup))
{

View File

@@ -50,7 +50,7 @@ public class AddServer2ViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom);
return;
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0)
{

View File

@@ -8,6 +8,12 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string? CoreType { get; set; }
[Reactive]
public bool AllowInsecure { get; set; }
[Reactive]
public bool MuxEnabled { get; set; }
[Reactive]
public string Cert { get; set; }
@@ -273,8 +279,10 @@ public class AddServerViewModel : MyReactiveObject
SelectedSource = JsonUtils.DeepCopy(profileItem);
}
CoreType = SelectedSource?.CoreType?.ToString();
Cert = SelectedSource?.Cert?.ToString() ?? string.Empty;
CertSha = SelectedSource?.CertSha?.ToString() ?? string.Empty;
AllowInsecure = SelectedSource?.GetAllowInsecure() == true;
MuxEnabled = SelectedSource?.MuxEnabled == true;
Cert = SelectedSource?.Cert ?? string.Empty;
CertSha = SelectedSource?.CertSha ?? string.Empty;
var protocolExtra = SelectedSource?.GetProtocolExtra() ?? new();
var transport = SelectedSource?.GetTransportExtra() ?? new();
@@ -352,7 +360,9 @@ public class AddServerViewModel : MyReactiveObject
return;
}
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse;
SelectedSource.MuxEnabled = MuxEnabled;
SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert;
SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha;
if (!Global.Networks.Contains(SelectedSource.Network))
@@ -464,7 +474,9 @@ public class AddServerViewModel : MyReactiveObject
domain += $":{SelectedSource.Port}";
}
(Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch);
(Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName,
verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName),
allowInsecure: AllowInsecureCertFetch);
UpdateCertTip(certError);
}
@@ -489,7 +501,9 @@ public class AddServerViewModel : MyReactiveObject
domain += $":{SelectedSource.Port}";
}
var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch);
var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName,
verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName),
allowInsecure: AllowInsecureCertFetch);
Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError);
}

View File

@@ -36,7 +36,7 @@ public class CheckUpdateViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.EnableCheckPreReleaseUpdate,
y => y == true)
.Subscribe(c => _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate);
.Subscribe(c => _ = OnCheckPreReleaseUpdateChanged());
RefreshCheckUpdateItems();
}
@@ -87,12 +87,23 @@ public class CheckUpdateViewModel : MyReactiveObject
};
}
private async Task OnCheckPreReleaseUpdateChanged()
{
if (_config.CheckUpdateItem.CheckPreReleaseUpdate == EnableCheckPreReleaseUpdate)
{
return;
}
_config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate;
await SaveSelectedCoreTypes();
}
private async Task SaveSelectedCoreTypes()
{
_config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels
.Where(t => t.IsSelected == true)
.Select(t => t.CoreTypeForStorage)
.ToList();
_config.CheckUpdateItem.SelectedCoreTypes =
CheckUpdateModels.Where(t => t.IsSelected == true)
.Select(t => t.CoreTypeForStorage)
.ToList();
await ConfigHandler.SaveConfig(_config);
}

View File

@@ -69,7 +69,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
var dtNow = DateTime.Now;
var lstModel = new List<ClashConnectionModel>();
foreach (var item in connections ?? new())
foreach (var item in connections ?? [])
{
var host = $"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}";
if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
@@ -85,7 +85,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
Host = host,
Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds,
Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"),
Chain = $"{item.rule} , {string.Join("->", item.chains ?? new())}"
Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}"
};
lstModel.Add(model);

View File

@@ -67,7 +67,7 @@ public class ClashProxiesViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.SelectedGroup,
y => y != null && y.Name.IsNotEmpty())
.Subscribe(c => RefreshProxyDetails(c));
.Subscribe(RefreshProxyDetails);
this.WhenAnyValue(
x => x.RuleModeSelected,
@@ -77,7 +77,7 @@ public class ClashProxiesViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.SortingSelected,
y => y >= 0)
.Subscribe(c => DoSortingSelected(c));
.Subscribe(DoSortingSelected);
this.WhenAnyValue(
x => x.AutoRefresh,
@@ -188,15 +188,14 @@ public class ClashProxiesViewModel : MyReactiveObject
ProxyGroups.Clear();
var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups();
if (proxyGroups != null && proxyGroups.Count > 0)
if (proxyGroups is { Count: > 0 })
{
foreach (var it in proxyGroups)
{
if (it.name.IsNullOrEmpty() || !_proxies.ContainsKey(it.name))
if (it.name.IsNullOrEmpty() || !_proxies.TryGetValue(it.name, out var item))
{
continue;
}
var item = _proxies[it.name];
if (!Global.allowSelectType.Contains(item.type.ToLower()))
{
continue;
@@ -230,7 +229,7 @@ public class ClashProxiesViewModel : MyReactiveObject
});
}
if (ProxyGroups != null && ProxyGroups.Count > 0)
if (ProxyGroups is { Count: > 0 })
{
if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName))
{

View File

@@ -56,7 +56,7 @@ public class DNSSettingViewModel : MyReactiveObject
});
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
.Select(x => !(x.Item1 && x.Item2))
.Select(x => x is not { Item1: true, Item2: true })
.ToPropertyEx(this, x => x.IsSimpleDNSEnabled);
_ = Init();

View File

@@ -65,6 +65,8 @@ public class MainWindowViewModel : MyReactiveObject
[Reactive] public bool BlIsWindows { get; set; }
[Reactive] public bool BlNewUpdate { get; set; }
#endregion Menu
#region Init
@@ -251,6 +253,11 @@ public class MainWindowViewModel : MyReactiveObject
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
AppEvents.HasUpdateNotified
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async bl => BlNewUpdate = bl);
#endregion AppEvents
_ = Init();
@@ -297,10 +304,22 @@ public class MainWindowViewModel : MyReactiveObject
{
var indexIdOld = _config.IndexId;
await RefreshServers();
if (indexIdOld != _config.IndexId)
// If indexId changed or subIndexId is empty, directly reload.
if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty())
{
await Reload();
}
else
{
// The activity config belongs to the current group.
var profile = await AppManager.Instance.GetProfileItem(_config.IndexId);
if (profile != null && profile.Subid == _config.SubIndexId)
{
await Reload();
}
}
if (_config.UiItem.EnableAutoAdjustMainLvColWidth)
{
AppEvents.AdjustMainLvColWidthRequested.Publish();

View File

@@ -0,0 +1,100 @@
namespace ServiceLib.ViewModels;
public class NetBridgeViewModel : MyReactiveObject
{
[Reactive]
public bool EnableNetBridge { get; set; }
[Reactive]
public string RuleProcess { get; set; }
public ReactiveCommand<Unit, Unit> SaveRulesCmd { get; }
public NetBridgeViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{
_config = AppManager.Instance.Config;
_updateView = updateView;
SaveRulesCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SaveRulesAsync();
});
this.WhenAnyValue(x => x.EnableNetBridge)
.Skip(1)
.Subscribe(async enabled =>
{
await ToggleNetBridgeAsync(enabled);
});
_ = Init();
}
private async Task Init()
{
_config.NetBridgeItem ??= new()
{
RuleProcess = string.Empty
};
EnableNetBridge = false;
RuleProcess = _config.NetBridgeItem.RuleProcess;
if (RuleProcess.IsNullOrEmpty())
{
RuleProcess = "Chrome.exe";
}
await Task.CompletedTask;
}
private async Task ToggleNetBridgeAsync(bool enabled)
{
await NetBridgeManager.Instance.Init(UpdateViewHandler);
if (enabled)
{
var succeed = await NetBridgeManager.Instance.Start();
if (succeed)
{
await NetBridgeManager.Instance.UpdateProxyConfig(Global.Loopback, AppManager.Instance.GetLocalPort(EInboundProtocol.socks));
await NetBridgeManager.Instance.UpdateRoutes(RuleProcess);
}
NoticeManager.Instance.Enqueue(succeed ? ResUI.OperationSuccess : ResUI.OperationFailed);
}
else
{
var succeed = await NetBridgeManager.Instance.Stop();
NoticeManager.Instance.Enqueue(succeed ? ResUI.OperationSuccess : ResUI.OperationFailed);
}
}
/// </summary>
private async Task SaveRulesAsync()
{
_config.NetBridgeItem ??= new();
var normalizedRuleProcess = RuleProcess;
_config.NetBridgeItem.RuleProcess = normalizedRuleProcess;
RuleProcess = normalizedRuleProcess;
if (await ConfigHandler.SaveConfig(_config) != 0)
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
await NetBridgeManager.Instance.Init(UpdateViewHandler);
if (EnableNetBridge)
{
var routesUpdated = await NetBridgeManager.Instance.UpdateRoutes(normalizedRuleProcess);
NoticeManager.Instance.Enqueue(routesUpdated ? ResUI.OperationSuccess : ResUI.OperationFailed);
}
}
private async Task<bool> UpdateViewHandler(bool isError, string msg)
{
NoticeManager.Instance.SendMessageEx(msg);
return await Task.FromResult(true);
}
}

View File

@@ -4,29 +4,28 @@ public class OptionSettingViewModel : MyReactiveObject
{
#region Core
[Reactive] public int localPort { get; set; }
[Reactive] public int LocalPort { get; set; }
[Reactive] public bool SecondLocalPortEnabled { get; set; }
[Reactive] public bool udpEnabled { get; set; }
[Reactive] public bool sniffingEnabled { get; set; }
public IList<string> destOverride { get; set; }
[Reactive] public bool routeOnly { get; set; }
[Reactive] public bool allowLANConn { get; set; }
[Reactive] public bool newPort4LAN { get; set; }
[Reactive] public string user { get; set; }
[Reactive] public string pass { get; set; }
[Reactive] public bool muxEnabled { get; set; }
[Reactive] public bool logEnabled { get; set; }
[Reactive] public string loglevel { get; set; }
[Reactive] public bool defAllowInsecure { get; set; }
[Reactive] public string defFingerprint { get; set; }
[Reactive] public string defUserAgent { get; set; }
[Reactive] public string sendThrough { get; set; }
[Reactive] public string bindInterface { get; set; }
[Reactive] public string mux4SboxProtocol { get; set; }
[Reactive] public bool enableCacheFile4Sbox { get; set; }
[Reactive] public int? hyUpMbps { get; set; }
[Reactive] public int? hyDownMbps { get; set; }
[Reactive] public bool enableFragment { get; set; }
[Reactive] public bool UdpEnabled { get; set; }
[Reactive] public bool SniffingEnabled { get; set; }
public IList<string> DestOverride { get; set; }
[Reactive] public bool RouteOnly { get; set; }
[Reactive] public bool AllowLANConn { get; set; }
[Reactive] public bool NewPort4LAN { get; set; }
[Reactive] public string User { get; set; }
[Reactive] public string Pass { get; set; }
[Reactive] public bool LogEnabled { get; set; }
[Reactive] public string Loglevel { get; set; }
[Reactive] public string DefFingerprint { get; set; }
[Reactive] public string DefUserAgent { get; set; }
[Reactive] public string SendThrough { get; set; }
[Reactive] public string BindInterface { get; set; }
[Reactive] public string Mux4SboxProtocol { get; set; }
[Reactive] public bool EnableCacheFile4Sbox { get; set; }
[Reactive] public int? HyUpMbps { get; set; }
[Reactive] public int? HyDownMbps { get; set; }
[Reactive] public bool EnableFragment { get; set; }
[Reactive] public bool EnableFinalFragment { get; set; }
#endregion Core
@@ -83,9 +82,9 @@ public class OptionSettingViewModel : MyReactiveObject
#region System proxy
[Reactive] public bool notProxyLocalAddress { get; set; }
[Reactive] public string systemProxyAdvancedProtocol { get; set; }
[Reactive] public string systemProxyExceptions { get; set; }
[Reactive] public bool NotProxyLocalAddress { get; set; }
[Reactive] public string SystemProxyAdvancedProtocol { get; set; }
[Reactive] public string SystemProxyExceptions { get; set; }
[Reactive] public string CustomSystemProxyPacPath { get; set; }
[Reactive] public string CustomSystemProxyScriptPath { get; set; }
@@ -100,6 +99,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool TunEnableIPv6Address { get; set; }
[Reactive] public string TunIcmpRouting { get; set; }
[Reactive] public bool TunEnableLegacyProtect { get; set; }
[Reactive] public string TunRouteExcludeAddress { get; set; }
#endregion Tun mode
@@ -142,28 +142,27 @@ public class OptionSettingViewModel : MyReactiveObject
#region Core
var inbound = _config.Inbound.First();
localPort = inbound.LocalPort;
LocalPort = inbound.LocalPort;
SecondLocalPortEnabled = inbound.SecondLocalPortEnabled;
udpEnabled = inbound.UdpEnabled;
sniffingEnabled = inbound.SniffingEnabled;
routeOnly = inbound.RouteOnly;
allowLANConn = inbound.AllowLANConn;
newPort4LAN = inbound.NewPort4LAN;
user = inbound.User;
pass = inbound.Pass;
muxEnabled = _config.CoreBasicItem.MuxEnabled;
logEnabled = _config.CoreBasicItem.LogEnabled;
loglevel = _config.CoreBasicItem.Loglevel;
defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure;
defFingerprint = _config.CoreBasicItem.DefFingerprint;
defUserAgent = _config.CoreBasicItem.DefUserAgent;
sendThrough = _config.CoreBasicItem.SendThrough ?? string.Empty;
bindInterface = _config.CoreBasicItem.BindInterface ?? string.Empty;
mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
hyUpMbps = _config.HysteriaItem.UpMbps;
hyDownMbps = _config.HysteriaItem.DownMbps;
enableFragment = _config.CoreBasicItem.EnableFragment;
UdpEnabled = inbound.UdpEnabled;
SniffingEnabled = inbound.SniffingEnabled;
RouteOnly = inbound.RouteOnly;
AllowLANConn = inbound.AllowLANConn;
NewPort4LAN = inbound.NewPort4LAN;
User = inbound.User;
Pass = inbound.Pass;
LogEnabled = _config.CoreBasicItem.LogEnabled;
Loglevel = _config.CoreBasicItem.Loglevel;
DefFingerprint = _config.CoreBasicItem.DefFingerprint;
DefUserAgent = _config.CoreBasicItem.DefUserAgent;
SendThrough = _config.CoreBasicItem.SendThrough ?? string.Empty;
BindInterface = _config.CoreBasicItem.BindInterface ?? string.Empty;
Mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
EnableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
HyUpMbps = _config.HysteriaItem.UpMbps;
HyDownMbps = _config.HysteriaItem.DownMbps;
EnableFragment = _config.CoreBasicItem.EnableFragment;
EnableFinalFragment = _config.CoreBasicItem.EnableFinalFragment;
#endregion Core
@@ -211,9 +210,9 @@ public class OptionSettingViewModel : MyReactiveObject
#region System proxy
notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress;
systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol;
systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions;
NotProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress;
SystemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol;
SystemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions;
CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath;
CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath;
@@ -228,6 +227,7 @@ public class OptionSettingViewModel : MyReactiveObject
TunEnableIPv6Address = _config.TunModeItem.EnableIPv6Address;
TunIcmpRouting = _config.TunModeItem.IcmpRouting;
TunEnableLegacyProtect = _config.TunModeItem.EnableLegacyProtect;
TunRouteExcludeAddress = Utils.List2String(_config.TunModeItem.RouteExcludeAddress, true);
#endregion Tun mode
@@ -236,12 +236,9 @@ public class OptionSettingViewModel : MyReactiveObject
private async Task InitCoreType()
{
if (_config.CoreTypeItem == null)
{
_config.CoreTypeItem = new List<CoreTypeItem>();
}
_config.CoreTypeItem ??= [];
foreach (EConfigType it in Enum.GetValues(typeof(EConfigType)))
foreach (var it in Enum.GetValues<EConfigType>())
{
if (_config.CoreTypeItem.FindIndex(t => t.ConfigType == it) >= 0)
{
@@ -297,13 +294,13 @@ public class OptionSettingViewModel : MyReactiveObject
private async Task SaveSettingAsync()
{
if (localPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(localPort.ToString())
|| localPort <= 0 || localPort >= Global.MaxPort)
if (LocalPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(LocalPort.ToString())
|| LocalPort <= 0 || LocalPort >= Global.MaxPort)
{
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return;
}
var sendThroughValue = sendThrough.TrimEx();
var sendThroughValue = SendThrough.TrimEx();
if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue))
{
NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4);
@@ -328,33 +325,32 @@ public class OptionSettingViewModel : MyReactiveObject
//}
//Core
_config.Inbound.First().LocalPort = localPort;
_config.Inbound.First().LocalPort = LocalPort;
_config.Inbound.First().SecondLocalPortEnabled = SecondLocalPortEnabled;
_config.Inbound.First().UdpEnabled = udpEnabled;
_config.Inbound.First().SniffingEnabled = sniffingEnabled;
_config.Inbound.First().DestOverride = destOverride?.ToList();
_config.Inbound.First().RouteOnly = routeOnly;
_config.Inbound.First().AllowLANConn = allowLANConn;
_config.Inbound.First().NewPort4LAN = newPort4LAN;
_config.Inbound.First().User = user;
_config.Inbound.First().Pass = pass;
_config.Inbound.First().UdpEnabled = UdpEnabled;
_config.Inbound.First().SniffingEnabled = SniffingEnabled;
_config.Inbound.First().DestOverride = DestOverride?.ToList();
_config.Inbound.First().RouteOnly = RouteOnly;
_config.Inbound.First().AllowLANConn = AllowLANConn;
_config.Inbound.First().NewPort4LAN = NewPort4LAN;
_config.Inbound.First().User = User;
_config.Inbound.First().Pass = Pass;
if (_config.Inbound.Count > 1)
{
_config.Inbound.RemoveAt(1);
}
_config.CoreBasicItem.LogEnabled = logEnabled;
_config.CoreBasicItem.Loglevel = loglevel;
_config.CoreBasicItem.MuxEnabled = muxEnabled;
_config.CoreBasicItem.DefAllowInsecure = defAllowInsecure;
_config.CoreBasicItem.DefFingerprint = defFingerprint;
_config.CoreBasicItem.DefUserAgent = defUserAgent;
_config.CoreBasicItem.SendThrough = sendThrough.TrimEx();
_config.CoreBasicItem.BindInterface = bindInterface.TrimEx();
_config.Mux4SboxItem.Protocol = mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = hyUpMbps ?? 0;
_config.HysteriaItem.DownMbps = hyDownMbps ?? 0;
_config.CoreBasicItem.EnableFragment = enableFragment;
_config.CoreBasicItem.LogEnabled = LogEnabled;
_config.CoreBasicItem.Loglevel = Loglevel;
_config.CoreBasicItem.DefFingerprint = DefFingerprint;
_config.CoreBasicItem.DefUserAgent = DefUserAgent;
_config.CoreBasicItem.SendThrough = SendThrough.TrimEx();
_config.CoreBasicItem.BindInterface = BindInterface.TrimEx();
_config.Mux4SboxItem.Protocol = Mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = EnableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = HyUpMbps ?? 0;
_config.HysteriaItem.DownMbps = HyDownMbps ?? 0;
_config.CoreBasicItem.EnableFragment = EnableFragment;
_config.CoreBasicItem.EnableFinalFragment = EnableFinalFragment;
_config.GuiItem.AutoRun = AutoRun;
_config.GuiItem.EnableStatistics = EnableStatistics;
@@ -383,9 +379,9 @@ public class OptionSettingViewModel : MyReactiveObject
_config.SpeedTestItem.IPAPIUrl = IPAPIUrl;
//systemProxy
_config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions;
_config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress;
_config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol;
_config.SystemProxyItem.SystemProxyExceptions = SystemProxyExceptions;
_config.SystemProxyItem.NotProxyLocalAddress = NotProxyLocalAddress;
_config.SystemProxyItem.SystemProxyAdvancedProtocol = SystemProxyAdvancedProtocol;
_config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath;
_config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath;
@@ -397,6 +393,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.TunModeItem.EnableIPv6Address = TunEnableIPv6Address;
_config.TunModeItem.IcmpRouting = TunIcmpRouting;
_config.TunModeItem.EnableLegacyProtect = TunEnableLegacyProtect;
_config.TunModeItem.RouteExcludeAddress = Utils.String2List(TunRouteExcludeAddress);
//coreType
await SaveCoreType();
@@ -458,7 +455,7 @@ public class OptionSettingViewModel : MyReactiveObject
default:
continue;
}
item.CoreType = (ECoreType)Enum.Parse(typeof(ECoreType), type);
item.CoreType = Enum.Parse<ECoreType>(type);
}
await Task.CompletedTask;
}

View File

@@ -5,13 +5,10 @@ public class ProfilesSelectViewModel : MyReactiveObject
#region private prop
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
private readonly Dictionary<string, bool> _dicHeaderSort = new();
private string _subIndexId = string.Empty;
// ConfigType filter state: default include-mode with all types selected
private List<EConfigType> _filterConfigTypes = new();
private bool _filterExclude = false;
#endregion private prop
@@ -33,18 +30,11 @@ public class ProfilesSelectViewModel : MyReactiveObject
public string ServerFilter { get; set; }
// Include/Exclude filter for ConfigType
public List<EConfigType> FilterConfigTypes
{
get => _filterConfigTypes;
set => this.RaiseAndSetIfChanged(ref _filterConfigTypes, value);
}
[Reactive]
public List<EConfigType> FilterConfigTypes { get; set; }
[Reactive]
public bool FilterExclude
{
get => _filterExclude;
set => this.RaiseAndSetIfChanged(ref _filterExclude, value);
}
public bool FilterExclude { get; set; }
#endregion ObservableCollection
@@ -91,11 +81,11 @@ public class ProfilesSelectViewModel : MyReactiveObject
try
{
FilterExclude = false;
FilterConfigTypes = Enum.GetValues(typeof(EConfigType)).Cast<EConfigType>().ToList();
FilterConfigTypes = Enum.GetValues<EConfigType>().ToList();
}
catch
{
FilterConfigTypes = new();
FilterConfigTypes = [];
}
await RefreshSubscriptions();
@@ -165,14 +155,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
if (lstModel.Count > 0)
{
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
if (selected != null)
{
SelectedProfile = selected;
}
else
{
SelectedProfile = lstModel.First();
}
SelectedProfile = selected ?? lstModel.First();
}
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
@@ -213,7 +196,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
}).OrderBy(t => t.Sort).ToList();
// Apply ConfigType filter (include or exclude)
if (FilterConfigTypes != null && FilterConfigTypes.Count > 0)
if (FilterConfigTypes is { Count: > 0 })
{
if (FilterExclude)
{
@@ -321,7 +304,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
// External setter for ConfigType filter
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
{
FilterConfigTypes = types?.Distinct().ToList() ?? new List<EConfigType>();
FilterConfigTypes = types?.Distinct().ToList() ?? [];
FilterExclude = exclude;
}

View File

@@ -6,7 +6,7 @@ public class ProfilesViewModel : MyReactiveObject
private List<ProfileItem> _lstProfile;
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
private readonly Dictionary<string, bool> _dicHeaderSort = new();
private SpeedtestService? _speedtestService;
private string? _pendingSelectIndexId;
@@ -190,7 +190,7 @@ public class ProfilesViewModel : MyReactiveObject
}, canEditRemove);
SortServerResultCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SortServer(EServerColName.DelayVal.ToString());
await SortServer(nameof(EServerColName.DelayVal));
});
RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(async () =>
{
@@ -303,6 +303,10 @@ public class ProfilesViewModel : MyReactiveObject
{
item.SpeedVal = result.Speed ?? string.Empty;
}
if (result.IpInfo.IsNotEmpty())
{
item.IpInfo = result.IpInfo ?? string.Empty;
}
await Task.CompletedTask;
}
@@ -437,6 +441,7 @@ public class ProfilesViewModel : MyReactiveObject
Speed = t33?.Speed ?? 0,
DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty,
SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty,
IpInfo = t33?.IpInfo ?? string.Empty,
TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown),
TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp),
TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown),

View File

@@ -73,7 +73,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
}
SelectedSource.Protocol = ProtocolItems?.ToList();
SelectedSource.InboundTag = InboundTagItems?.ToList();
SelectedSource.RuleType = RuleType.IsNullOrEmpty() ? null : (ERuleType)Enum.Parse(typeof(ERuleType), RuleType);
SelectedSource.RuleType = RuleType.IsNullOrEmpty() ? null : Enum.Parse<ERuleType>(RuleType);
var hasRule = SelectedSource.Domain?.Count > 0
|| SelectedSource.Ip?.Count > 0

View File

@@ -86,7 +86,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
SelectedSource = new();
SelectedRouting = routingItem;
_rules = routingItem.Id.IsNullOrEmpty() ? new() : JsonUtils.Deserialize<List<RulesItem>>(SelectedRouting.RuleSet);
_rules = routingItem.Id.IsNullOrEmpty() ? [] : JsonUtils.Deserialize<List<RulesItem>>(SelectedRouting.RuleSet);
RefreshRulesItems();
}

View File

@@ -313,9 +313,8 @@ public class StatusBarViewModel : MyReactiveObject
}
BlServers = true;
for (var k = 0; k < lstModel.Count; k++)
foreach (var it in lstModel)
{
var it = lstModel[k];
var name = it.GetSummary();
var item = new ComboItem() { ID = it.IndexId, Text = name };

View File

@@ -1,3 +1,4 @@
using v2rayN.Desktop.Common;
using v2rayN.Desktop.Views;
namespace v2rayN.Desktop;
@@ -24,11 +25,34 @@ public partial class App : Application
desktop.Exit += OnExit;
desktop.MainWindow = new MainWindow();
if (OperatingSystem.IsMacOS())
{
Current?.TryGetFeature<IActivatableLifetime>()?.Activated += OnMacOSActivated;
}
}
base.OnFrameworkInitializationCompleted();
}
private void OnMacOSActivated(object? sender, ActivatedEventArgs args)
{
if (args.Kind != ActivationKind.Reopen)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
if (!AppManager.Instance.Config.UiItem.MacOSShowInDock)
{
MacAppUtils.SetActivationPolicyAccessory();
}
((ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow)?.ShowHideWindow(true);
});
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject != null)
@@ -48,14 +72,18 @@ public partial class App : Application
private async void MenuAddServerViaClipboardClick(object? sender, EventArgs e)
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
try
{
if (desktop.MainWindow != null)
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: not null })
{
AppEvents.AddServerViaClipboardRequested.Publish();
await Task.Delay(1000);
}
}
catch (Exception ex)
{
Logging.SaveLog("MenuAddServerViaClipboardClick", ex);
}
}
private async void MenuExit_Click(object? sender, EventArgs e)

View File

@@ -4,6 +4,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
{
public WindowBase()
{
Initialized += OnWindowInitialized;
Loaded += OnLoaded;
}
@@ -12,30 +13,33 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
throw new NotImplementedException();
}
protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
private void OnWindowInitialized(object? sender, EventArgs e)
{
try
{
var sizeItem = ConfigHandler.GetWindowSizeItem(AppManager.Instance.Config, GetType().Name);
if (sizeItem == null)
if (sizeItem is null)
{
return;
}
Width = sizeItem.Width;
Height = sizeItem.Height;
if (sizeItem.Width > 0 && !Width.Equals(sizeItem.Width))
{
Width = sizeItem.Width;
}
var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea;
var scaling = (Utils.IsMacOS() ? null : VisualRoot?.RenderScaling) ?? 1.0;
var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2);
var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2);
Position = new PixelPoint((int)x, (int)y);
if (sizeItem.Height > 0 && !Height.Equals(sizeItem.Height))
{
Height = sizeItem.Height;
}
}
catch { }
}
protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
{
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);

View File

@@ -2,24 +2,48 @@ namespace v2rayN.Desktop.Common;
public static class AppBuilderExtension
{
private static readonly string DefaultFontFamilyName =
Path.Combine(Global.AvaAssets, "Fonts#Noto Sans SC");
public static AppBuilder WithFontByDefault(this AppBuilder appBuilder)
{
var fallbacks = new List<FontFallback>();
var notoSansSc = new FontFamily(Path.Combine(Global.AvaAssets, "Fonts#Noto Sans SC"));
fallbacks.Add(new FontFallback { FontFamily = notoSansSc });
var notoSansSc = new FontFamily(DefaultFontFamilyName);
if (OperatingSystem.IsLinux())
fallbacks.Add(new FontFallback
{
fallbacks.Add(new FontFallback
{
FontFamily = new FontFamily("Noto Color Emoji")
});
FontFamily = notoSansSc
});
if (OperatingSystem.IsWindows())
{
AddFontFallback(fallbacks, "Segoe UI Symbol");
AddFontFallback(fallbacks, "Segoe UI Emoji");
}
else if (OperatingSystem.IsMacOS())
{
AddFontFallback(fallbacks, "Apple Symbols");
AddFontFallback(fallbacks, "Apple Color Emoji");
}
else if (OperatingSystem.IsLinux())
{
AddFontFallback(fallbacks, "Noto Sans Symbols");
AddFontFallback(fallbacks, "Noto Color Emoji");
}
return appBuilder.With(new FontManagerOptions
{
DefaultFamilyName = DefaultFontFamilyName,
FontFallbacks = fallbacks.ToArray()
});
}
private static void AddFontFallback(List<FontFallback> fallbacks, string fontFamilyName)
{
fallbacks.Add(new FontFallback
{
FontFamily = new FontFamily(fontFamilyName)
});
}
}

View File

@@ -0,0 +1,27 @@
using System.Runtime.InteropServices;
namespace v2rayN.Desktop.Common;
internal static class MacAppUtils
{
private const string LibObjC = "/usr/lib/libobjc.dylib";
private const nint ActivationPolicyAccessory = 1;
public static void SetActivationPolicyAccessory()
=> objc_msgSend(
objc_msgSend(objc_getClass("NSApplication"), sel_registerName("sharedApplication")),
sel_registerName("setActivationPolicy:"),
ActivationPolicyAccessory);
[DllImport(LibObjC)]
private static extern nint objc_getClass(string name);
[DllImport(LibObjC)]
private static extern nint sel_registerName(string name);
[DllImport(LibObjC, EntryPoint = "objc_msgSend")]
private static extern nint objc_msgSend(nint receiver, nint selector);
[DllImport(LibObjC, EntryPoint = "objc_msgSend")]
private static extern void objc_msgSend(nint receiver, nint selector, nint argument);
}

View File

@@ -41,7 +41,7 @@ public class ThemeSettingViewModel : MyReactiveObject
{
_config.UiItem.CurrentTheme = CurrentTheme;
ModifyTheme();
ConfigHandler.SaveConfig(_config);
_ = ConfigHandler.SaveConfig(_config);
}
});
@@ -54,7 +54,7 @@ public class ThemeSettingViewModel : MyReactiveObject
{
_config.UiItem.CurrentFontSize = CurrentFontSize;
ModifyFontSize();
ConfigHandler.SaveConfig(_config);
_ = ConfigHandler.SaveConfig(_config);
}
});
@@ -67,7 +67,7 @@ public class ThemeSettingViewModel : MyReactiveObject
{
_config.UiItem.CurrentLanguage = CurrentLanguage;
Thread.CurrentThread.CurrentUICulture = new(CurrentLanguage);
ConfigHandler.SaveConfig(_config);
_ = ConfigHandler.SaveConfig(_config);
NoticeManager.Instance.Enqueue(ResUI.NeedRebootTips);
}
});

View File

@@ -18,7 +18,7 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
btnCancel.Click += (s, e) => Close();
ViewModel = new AddServer2ViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != ECoreType.v2rayN.ToString()).ToList().AppendEmpty();
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
this.WhenActivated(disposables =>
{

Some files were not shown because too many files have changed in this diff Show More