Compare commits

...

36 Commits

Author SHA1 Message Date
2dust 1883178b5f up 7.25.6 2026-10-11 13:55:36 +08:00
2dust 1cde521d75 Prevent zip path traversal in app upgrade
Hardened upgrade extraction by validating each zip entry resolves under the app startup directory before writing files, and aborting with a clear failure message on suspicious paths. Added shared path utilities for directory containment checks and OS-aware path string comparison, then reused them for executable matching and `bin` prefix checks during extraction.

https://github.com/2dust/v2rayN/issues/10313
2026-10-11 13:52:28 +08:00
Ali Ahmadi 2407d44f4f Fix empty latency tests and Linux startup errors (#10335)
* Fix overflow when speed tests have no eligible profiles

* Preserve core executable permissions in Debian packages

* Avoid requesting an icon refresh before view initialization
2026-10-11 11:06:46 +08:00
dependabot[bot] fbf822d74c Bump TUnit from 1.72.16 to 1.73.19 (#10334)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.73.19
  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-10-11 10:56:36 +08:00
liu-xindi 4869a6cbba Route IPv6 into the Xray TUN when EnableIPv6Address is on (#10329)
* Route IPv6 into the Xray TUN on NAT66 hosts and when IPv6 is enabled

#10080 dropped ::/0 from autoSystemRoutingTable whenever the host held no
address inside 2000::/3. A host behind NAT66 holds only unique local
addresses, yet it reaches the IPv6 internet through its IPv6 default
gateway. With ::/0 gone, its IPv6 follows the system default route,
bypasses the tunnel and exposes the real IPv6 address (#10327).

The check also overrode EnableIPv6Address. Before #9930, enabling it
always added ::/0; since #10080 the option only assigned the interface
address, so users who had enabled it were affected as well.

Route ::/0 when EnableIPv6Address is on, and otherwise when the host has
IPv6 connectivity: a global IPv6 address or an IPv6 default gateway.
Routes whose next hop is ::, such as the one the tunnel installs, do not
count. A host with neither, as in #10051, still skips ::/0. The
RouteExcludeAddress branch uses the same decision.

Reproduced in network namespaces with Xray 26.7.28: on a NAT66 client the
7.25.4 TUN inbound let curl -6 reach the echo server from the NAT66
address, while the inbound generated by this change sends it through the
proxy, with EnableIPv6Address on or off.

Fixes #10327

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Simplify: route ::/0 when EnableIPv6Address is on

Drop the rename and the gateway detection; keep HasGlobalIPv6Address
and let EnableIPv6Address override it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142Bna7M2GhesX7P7d4CSrt

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:55:39 +08:00
DHR60 b501efbc45 Fix (#10326) 2026-10-10 09:14:30 +08:00
Miheichev Aleksandr Sergeevich 5ea8ae6420 fix(ui): wrap the pre-release label in the update dialog (#10303)
The per-core pre-release switch and its label sit in a horizontal
StackPanel inside a fixed-width column of the update dialog.
Translations longer than the column do not fit: in Avalonia the label
runs over the core name and the update status (Russian, French,
Hungarian, Persian), and in WPF it is cut off. Use a DockPanel so that
the label wraps inside its column, as the interval label of the
subscription window does since #10292. The dialog keeps its width.

In Avalonia the panel also had a vertical margin on top of the
margin of the switch, so the two rows with the switch were 8 px taller
than the other rows. Use a horizontal-only margin, as the WPF view
already does.
2026-10-07 11:11:27 +08:00
Miheichev Aleksandr Sergeevich fa9d4a4769 fix(ui): line up the backup dialog columns again (#10302)
#10292 made the label and button columns of the backup view Auto with
a minimum width, so that longer labels and buttons fit. Each of the two
cards sizes its own grid, so when the remote (WebDAV) buttons are wider
than the local ones, the local buttons no longer sit under the remote
ones and the separators of the two cards differ in length. This shows
in English and in every other language except Chinese.

Share the column widths of the two cards with SharedSizeGroup: both
cards line up again, and the long labels and buttons still fit. In
Avalonia the buttons stay centred in the shared column; in WPF they
fill it as before, so the local buttons get as wide as the remote ones.
2026-10-07 11:09:32 +08:00
DHR60 771ef3192a Update cert (#10308) 2026-10-07 11:04:58 +08:00
2dust f5747bb321 up 7.25.5 2026-10-05 16:43:51 +08:00
ariansharifi d099e44ff6 Fix subscription custom configs ignoring custom config socks port (#10296)
* Fix subscription custom configs ignoring custom config socks port

Full Xray/sing-box configs imported from a subscription no longer got the
subscription's "Custom config socks port" (SubItem.PreSocksPort) after
eff5845 and 9638b4c. 7.24.4 and earlier copied it onto every imported
config, and the Clash, Hysteria2 and raw-file paths still do.

Without it, GetPreSocksItem builds no pre-socks core for these profiles,
so they cannot be used in TUN mode. Setting the port on the profile is
not a workaround, because every subscription update re-creates the
profiles.

* Add regression test for subscription custom config socks port
2026-10-05 16:36:52 +08:00
DHR60 c63d271d16 Up TUnit.Assertions.Should (#10294) 2026-10-05 16:27:38 +08:00
Miheichev Aleksandr Sergeevich 6152cab494 fix(ui): make cut-off button labels and input hints readable (#10292)
* fix(ui): let update and backup buttons fit their labels

The update dialog gives its two buttons a fixed 100 px width, and the
backup view puts its buttons in a fixed 200 px grid column, so longer
labels are cut off, for example "Only Chec", "Check and" and "Backup
to remote (WebD" in English. Use MinWidth="100" for the update buttons
and an Auto column with MinWidth="200" for the backup buttons: short
labels keep their current size and long ones fit.

The "Automatic update interval (minutes)" label of the subscription
window is cut off the same way between the switch and the interval
field, so let it wrap.

* fix(ui): show cut-off input hints as tooltips

The hints of the previous and next proxy remarks fields in the
subscription window and of the server and log filters are wider than
their fields, so their end is cut off and there is no way to read it.
Show the same text as a tooltip, as the custom config socks port field
of the subscription window already does.

* fix(ui): let dialog buttons and backup labels fit longer translations

The Confirm and Cancel buttons of the editing and settings windows,
and the Reset button of the hotkey window, have a fixed 100 px width,
and the backup view puts its labels in a fixed 300 px column.
Translations longer than the English text are cut off there: the
Russian "Подтвердить" reads "Подтверди", and "Резервное копирование
на удалённый сервер (WebDAV)" loses its end. Use MinWidth="100" for
these buttons, as the message box already does with MinWidth="90",
and an Auto label column with MinWidth="300".
2026-10-05 16:27:19 +08:00
2dust 3187eeef79 Code clean 2026-10-04 11:41:34 +08:00
Miheichev Aleksandr Sergeevich 3364ec0e57 i18n(ru): translate the update dialog buttons in full (#10291)
Add the Russian string for menuCheckAndUpdate, which #10282 added to
ResUI.resx, as "Проверить и обновить". Restore the full Russian text
of menuCheckOnly ("Только проверить") and menuCheckUpdate ("Проверить
обновления"), which were shortened only to fit the fixed 100 px
buttons of the update dialog; they fit once the buttons are sized by
their labels.
2026-10-04 11:08:10 +08:00
dependabot[bot] 23033fa002 Bump TUnit from 1.68.17 to 1.72.16 (#10288)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.72.16
  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-10-04 11:07:42 +08:00
Eliot the Cougar 125cbb18a1 Keep Xray startup logs visible in horizontal and vertical layouts (#10272)
* Keep Xray log content visible in separate log layouts

* Revert wpf

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-10-04 11:06:06 +08:00
2dust f85326cf0e Add per-core prerelease update selection
Replaces the global prerelease toggle with per-core prerelease options in both WPF and Avalonia update views, and persists selections via `CheckPreReleaseCoreTypes` in config. Update checks now resolve prerelease mode per selected core (including auto-check flow), and the sing-box max-version lock is removed to allow newer versions.
2026-10-03 14:58:45 +08:00
Tosd 3666fc549d Resx: clarify update button wording (#10282) 2026-10-02 15:08:27 +08:00
Miheichev Aleksandr Sergeevich a147249f95 i18n(ru): translate the Add [MASQUE] menu item (#10276)
Add the Russian string for menuAddMasqueServer, which #10233 added to
ResUI.resx. Without it the Russian UI shows this one item of the
Configuration menu in English. The text follows the other
Add [protocol] items: "Добавить сервер [MASQUE]".
2026-10-02 15:06:49 +08:00
Eliot the Cougar a28f68c3a7 Avoid reentrant log scrolling during Avalonia window dragging (#10271) 2026-10-01 19:55:30 +08:00
Ezra-Y 1a689f869c Fix macOS TUN interface name collisions (#10270) 2026-10-01 19:51:08 +08:00
DHR60 50023a5976 Xray tun features (#10264)
* Xray tun features

* Try sync
2026-10-01 14:55:09 +08:00
DHR60 c659ca8af8 Add MASQUE support (#10233)
* Add MASQUE support

* Add SettingsCoreType
2026-10-01 14:49:45 +08:00
2dust 7d6a967c18 up 7.25.4 2026-09-30 14:38:34 +08:00
2dust 6c2edcb382 Revert "Bump Avalonia.Desktop from 12.1.2 to 12.1.3 (#10216)"
This reverts commit e1cb99cd6e.
2026-09-30 14:34:07 +08:00
2dust 086c991464 up 7.25.3 2026-09-30 09:55:08 +08:00
Alice Apate a9da04acc2 Add support for TLS1.3-only subscription URL for macOS (#10256)
* add support for TLS1.3-only subscription url for macOS

* Update v2rayN.Desktop.csproj

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-09-30 09:54:26 +08:00
2dust e0c790eeb2 Tune speedtest parallelism
Set `MaxDegreeOfParallelism` explicitly to the current selection size in three speed test parallel sections so task fan-out matches the workload. Also wrapped socket connect timing in a catch block that ignores connection exceptions, preventing a failed connect from aborting the flow while still completing cleanup.
https://github.com/2dust/v2rayN/issues/10246
2026-09-28 10:28:32 +08:00
2dust d433aa3b04 Fix
https://github.com/2dust/v2rayN/issues/10236
2026-09-26 11:43:22 +08:00
dependabot[bot] 91a7ed65d5 Bump ReactiveUI from 24.2.0 to 24.3.0 (#10231)
---
updated-dependencies:
- dependency-name: ReactiveUI
  dependency-version: 24.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-09-26 11:12:07 +08:00
Miheichev Aleksandr Sergeevich 984e685ab3 chore(deps): update Downloader to 5.9.8 (#10222)
Downloader 5.9.6 -> 5.9.8

5.9.7 adds RemoteFileInfo.ContentType. 5.9.8 fixes stop/dispose races:
a cancelled multi-chunk download could report a NullReferenceException
as its error, and a Dispose racing the completion signal could swallow
DownloadFileCompleted. Nothing DownloaderHelper calls changed its API.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 11:11:20 +08:00
DHR60 29f99ceacc Fix speed test (#10219) 2026-09-26 11:10:30 +08:00
DHR60 93d8174dbe Fix async (#10218)
* Fix async

* Fix async for statistics

* Try fix pac async

* Fix
2026-09-26 11:08:36 +08:00
DHR60 fa1e201c76 Fix sing-box dns (#10234) 2026-09-26 10:39:29 +08:00
dependabot[bot] e1cb99cd6e Bump Avalonia.Desktop from 12.1.2 to 12.1.3 (#10216)
---
updated-dependencies:
- dependency-name: Avalonia.Desktop
  dependency-version: 12.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 11:03:05 +08:00
96 changed files with 1092 additions and 482 deletions
+9
View File
@@ -676,6 +676,15 @@ EOF
find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} +
[[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true
# Core binaries must be executable before installation: ordinary users cannot
# chmod the root-owned files installed by dpkg.
local core
for core in xray/xray sing_box/sing-box mihomo/mihomo; do
if [[ -f "$stage/opt/v2rayN/bin/$core" ]]; then
chmod 0755 "$stage/opt/v2rayN/bin/$core"
fi
done
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
+10 -3
View File
@@ -45,6 +45,8 @@ internal class UpgradeApp
var thisAppOldFile = $"{Utils.GetExePath()}.tmp";
File.Delete(thisAppOldFile);
var splitKey = "/";
var allowedBaseDir = Utils.StartupPath();
var pathComparison = Utils.GetPathComparison();
using var archive = ZipFile.OpenRead(fileName);
foreach (var entry in archive.Entries)
@@ -65,16 +67,21 @@ internal class UpgradeApp
}
var fullName = string.Join(splitKey, lst[1..lst.Length]);
var entryOutputPath = Utils.GetPath(fullName);
if (!Utils.IsPathUnderDirectory(allowedBaseDir, entryOutputPath))
{
Console.WriteLine($"{Resx.Resource.FailedUpgrade} blocked potential path traversal: {entry.FullName}");
return;
}
if (string.Equals(Utils.GetExePath(), Utils.GetPath(fullName), StringComparison.OrdinalIgnoreCase))
if (string.Equals(Utils.GetExePath(), entryOutputPath, pathComparison))
{
File.Move(Utils.GetExePath(), thisAppOldFile);
}
var entryOutputPath = Utils.GetPath(fullName);
Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!);
//In the bin folder, if the file already exists, it will be skipped
if (fullName.StartsWith("bin") && File.Exists(entryOutputPath))
if (fullName.StartsWith("bin", pathComparison) && File.Exists(entryOutputPath))
{
continue;
}
+22
View File
@@ -24,6 +24,28 @@ internal class Utils
return Path.Combine(startupPath, fileName);
}
public static bool IsPathUnderDirectory(string baseDir, string targetPath)
{
if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(targetPath))
{
return false;
}
var baseFull = Path.GetFullPath(baseDir);
var targetFull = Path.GetFullPath(targetPath);
baseFull = baseFull.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
return targetFull.StartsWith(baseFull, GetPathComparison());
}
public static StringComparison GetPathComparison()
{
return OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
}
public static string V2rayN => "v2rayN";
public static void StartV2RayN()
+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.25.2</Version>
<Version>7.25.6</Version>
</PropertyGroup>
<PropertyGroup>
+5 -5
View File
@@ -13,11 +13,11 @@
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
<PackageVersion Include="CliWrap" Version="3.10.5" />
<PackageVersion Include="Downloader" Version="5.9.6" />
<PackageVersion Include="Downloader" Version="5.9.8" />
<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="24.2.0" />
<PackageVersion Include="ReactiveUI" Version="24.3.0" />
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="24.2.0" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
@@ -27,11 +27,11 @@
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.68.17" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<PackageVersion Include="TUnit" Version="1.73.19" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.72.16-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup>
</Project>
</Project>
@@ -0,0 +1,42 @@
namespace ServiceLib.Tests.Common;
public class UnixFileModeTests
{
[Test]
public async Task AlreadyExecutableSystemBinary_ShouldNotRequireOwnership()
{
if (OperatingSystem.IsWindows())
{
return;
}
// /bin/sh is executable but owned by root on the Unix CI runners.
// A regular user must not need chmod permission just to launch it.
var mode = File.GetUnixFileMode("/bin/sh");
await Utils.SetUnixFileMode("/bin/sh").Should().BeTrue();
await File.GetUnixFileMode("/bin/sh").Should().BeEqualTo(mode);
}
[Test]
public async Task NonExecutableOwnedFile_ShouldGainExecuteBitsWithoutLosingOtherPermissions()
{
if (OperatingSystem.IsWindows())
{
return;
}
var path = Path.GetTempFileName();
try
{
var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead;
File.SetUnixFileMode(path, mode);
await Utils.SetUnixFileMode(path).Should().BeTrue();
await File.GetUnixFileMode(path).Should().BeEqualTo(mode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
}
finally
{
File.Delete(path);
}
}
}
@@ -1,8 +1,3 @@
using System.Reflection;
using ServiceLib.Enums;
using ServiceLib.Manager;
using ServiceLib.Models;
namespace ServiceLib.Tests.CoreConfig;
internal static class CoreConfigTestFactory
@@ -716,7 +716,6 @@ public class CoreConfigSingboxServiceTests
}
}
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
@@ -51,7 +51,7 @@ public class CoreConfigV2rayServiceTests
.Select(item => item!.GetValue<string>())
.Should().BeEquivalentTo(["a=1", "b=2"]);
}
[Test]
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{
@@ -587,13 +587,11 @@ public class CoreConfigV2rayServiceTests
}
[Test]
[Arguments(true)]
[Arguments(false)]
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6(bool enableIPv6Address)
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6()
{
// A host without a global IPv6 address has no IPv6 traffic that could bypass the tunnel,
// while ::/0 would pull IPv6 attempts into a tunnel they cannot leave.
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: false);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
@@ -611,6 +609,26 @@ public class CoreConfigV2rayServiceTests
await ipv6Routes.Should().BeEmpty();
}
[Test]
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6WhenEnabledWithoutGlobalIPv6()
{
// Hosts behind NAT66 hold only ULA addresses, so EnableIPv6Address must still route ::/0.
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: true);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray, hasGlobalIPv6Address: false);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
await tunInbound.Should().NotBeNull();
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("::/0");
}
[Test]
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldSkipIPv6RangesWithoutGlobalIPv6()
{
@@ -0,0 +1,42 @@
using ServiceLib.Tests.CoreConfig;
namespace ServiceLib.Tests.Handler;
public class ConfigHandlerCustomSubscriptionTests
{
private const string FullXrayConfigs = """
[
{"remarks":"custom-1","inbounds":[{"tag":"socks","listen":"127.0.0.1","port":10808,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"127.0.0.1","port":443,"users":[{"id":"00000000-0000-0000-0000-000000000001","encryption":"none"}]}]},"streamSettings":{"network":"tcp"}}],"routing":{"domainStrategy":"AsIs","rules":[]}},
{"remarks":"custom-2","inbounds":[{"tag":"socks","listen":"127.0.0.1","port":10808,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"127.0.0.1","port":443,"users":[{"id":"00000000-0000-0000-0000-000000000001","encryption":"none"}]}]},"streamSettings":{"network":"tcp"}}],"routing":{"domainStrategy":"AsIs","rules":[]}}
]
""";
[Test]
public async Task AddBatchServers_FullConfigsFromSubscription_InheritSubscriptionPreSocksPort()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
SQLiteHelper.Instance.CreateTable<SubItem>();
SQLiteHelper.Instance.CreateTable<ProfileItem>();
var subId = $"sub-{Guid.NewGuid():N}";
await SQLiteHelper.Instance.ReplaceAsync(new SubItem
{
Id = subId,
Remarks = "sub",
Url = "http://127.0.0.1/sub",
PreSocksPort = 10808,
});
var count = await ConfigHandler.AddBatchServers(config, FullXrayConfigs, subId, true);
var profiles = await AppManager.Instance.ProfileItems(subId) ?? [];
await count.Should().BeEqualTo(2);
await profiles.Should().HaveCount(2);
foreach (var profile in profiles)
{
await profile.ConfigType.Should().BeEqualTo(EConfigType.Custom);
await profile.PreSocksPort.Should().BeEqualTo(10808);
}
}
}
@@ -0,0 +1,59 @@
namespace ServiceLib.Tests.Services;
public class SpeedtestServiceBatchTests
{
[Test]
[Arguments(0)]
[Arguments(1000)]
public async Task EmptySelection_ShouldReturnNoBatches(int pageSize)
{
await GetBatches([], pageSize).Count.Should().BeEqualTo(0);
}
[Test]
[Arguments(0)]
[Arguments(-1)]
public async Task NonPositivePageSize_ShouldKeepEveryEligibleProfile(int pageSize)
{
var profiles = CreateProfiles();
var batches = GetBatches(profiles, pageSize);
await batches.Count.Should().BeEqualTo(profiles.Count);
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
foreach (var profile in profiles)
{
await batches.SelectMany(batch => batch).Count(item => ReferenceEquals(item, profile)).Should().BeEqualTo(1);
}
}
[Test]
public async Task NormalBatches_ShouldRespectPageSizeAndKeepCoreTypesSeparate()
{
var profiles = CreateProfiles();
var batches = GetBatches(profiles, 2);
await batches.Count.Should().BeEqualTo(3);
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
foreach (var batch in batches)
{
await (batch.Count > 0 && batch.Count <= 2).Should().BeTrue();
await batch.Select(item => item.CoreType).Distinct().Count().Should().BeEqualTo(1);
}
}
private static List<ServerTestItem> CreateProfiles() =>
[
new() { CoreType = ECoreType.Xray },
new() { CoreType = ECoreType.Xray },
new() { CoreType = ECoreType.Xray },
new() { CoreType = ECoreType.sing_box },
new() { CoreType = ECoreType.sing_box }
];
private static List<List<ServerTestItem>> GetBatches(List<ServerTestItem> profiles, int pageSize)
{
var service = new SpeedtestService(new Config { SpeedTestItem = new() }, _ => Task.CompletedTask);
var method = typeof(SpeedtestService).GetMethod("GetTestBatchItem", BindingFlags.Instance | BindingFlags.NonPublic)!;
return (List<List<ServerTestItem>>)method.Invoke(service, [profiles, pageSize])!;
}
}
@@ -43,9 +43,9 @@ public class McBeService : IUdpTest
{
// 0x1c | client alive time in ms (recorded from previous ping) |
// server GUID | Magic | string length | Edition
//
//
// Edition Example:
//
//
// MCPE;Dedicated Server;527;1.19.1;0;10;13253860892328930865;Bedrock level;Survival;1;19132;19133;
if (mcbeResponseBytes.Length < 48)
{
+30 -1
View File
@@ -501,6 +501,31 @@ public class Utils
};
}
public static List<(string, string)> ParseHeaders(string? headers)
{
var result = new List<(string, string)>();
if (headers.IsNullOrEmpty())
{
return result;
}
var lines = headers.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var parts = line.Split(new[] { ':' }, 2);
if (parts.Length != 2)
{
continue;
}
var key = parts[0].Trim();
var value = parts[1].Trim();
if (!string.IsNullOrEmpty(key))
{
result.Add((key, value));
}
}
return result;
}
#endregion Conversion Functions
#region Data Checks
@@ -1416,7 +1441,11 @@ public class Utils
if (File.Exists(fileName))
{
var currentMode = File.GetUnixFileMode(fileName);
File.SetUnixFileMode(fileName, currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
var executableMode = currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
if (currentMode != executableMode)
{
File.SetUnixFileMode(fileName, executableMode);
}
return true;
}
}
+1
View File
@@ -15,6 +15,7 @@ public enum EConfigType
Anytls = 11,
Naive = 12,
Outbound = 13,
MASQUE = 14,
PolicyGroup = 101,
ProxyChain = 102,
}
+4 -1
View File
@@ -261,7 +261,8 @@ public class Global
{ EConfigType.TUIC, "tuic" },
{ EConfigType.WireGuard, "wireguard" },
{ EConfigType.Anytls, "anytls" },
{ EConfigType.Naive, "naive" }
{ EConfigType.Naive, "naive" },
{ EConfigType.MASQUE, "masque" },
};
public static readonly List<string> VmessSecurities =
@@ -371,6 +372,7 @@ public class Global
EConfigType.Trojan,
EConfigType.Hysteria2,
EConfigType.WireGuard,
EConfigType.MASQUE,
EConfigType.SOCKS,
EConfigType.HTTP,
];
@@ -386,6 +388,7 @@ public class Global
EConfigType.Anytls,
EConfigType.Naive,
EConfigType.WireGuard,
EConfigType.MASQUE,
EConfigType.SOCKS,
EConfigType.HTTP,
];
@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
IsWindows = Utils.IsWindows(),
IsMacOS = Utils.IsMacOS(),
IsLinux = Utils.IsLinux(),
HasGlobalIPv6Address = Utils.HasGlobalIPv6Address(),
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
};
+30 -3
View File
@@ -292,6 +292,7 @@ public static class ConfigHandler
EConfigType.WireGuard => await AddWireguardServer(config, item),
EConfigType.Anytls => await AddAnytlsServer(config, item),
EConfigType.Naive => await AddNaiveServer(config, item),
EConfigType.MASQUE => await AddMasqueServer(config, item),
_ => -1,
};
return ret;
@@ -992,6 +993,29 @@ public static class ConfigHandler
return 0;
}
public static async Task<int> AddMasqueServer(Config config, ProfileItem profileItem, bool toFile = true)
{
profileItem.ConfigType = EConfigType.MASQUE;
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
profileItem.AllowInsecure = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
}
if (profileItem.Password.IsNullOrEmpty())
{
return -1;
}
await AddServerCommon(config, profileItem, toFile);
return 0;
}
/// <summary>
/// Sort the server list by the specified column
/// Updates the sort order in the profile extension data
@@ -1684,6 +1708,7 @@ public static class ConfigHandler
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
EConfigType.MASQUE => await AddMasqueServer(config, profileItem, false),
_ => -1,
};
@@ -1753,7 +1778,7 @@ public static class ConfigHandler
}
if (lstProfiles.Count > 0)
{
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
if (count > 0)
{
return count;
@@ -1799,7 +1824,7 @@ public static class ConfigHandler
if (lstProfiles?.Count > 0)
{
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
if (count > 0)
{
return count;
@@ -1813,7 +1838,8 @@ public static class ConfigHandler
Config config,
List<ProfileItem> lstProfiles,
string subid,
bool isSub)
bool isSub,
SubItem? subItem)
{
var count = 0;
foreach (var it in lstProfiles)
@@ -1823,6 +1849,7 @@ public static class ConfigHandler
if (it.ConfigType == EConfigType.Custom)
{
it.PreSocksPort = subItem?.PreSocksPort;
if (await AddCustomServer(config, it, true) == 0)
{
count++;
@@ -3,6 +3,7 @@ namespace ServiceLib.Handler.SysProxy;
public static class SysProxyHandler
{
private static readonly string _tag = "SysProxyHandler";
private static readonly Lazy<PacManager> _pacManager = new(() => new PacManager());
public static async Task<bool> UpdateSysProxy(Config config, bool forceDisable)
{
@@ -56,7 +57,7 @@ public static class SysProxyHandler
if (type != ESysProxyType.Pac && Utils.IsWindows())
{
PacManager.Instance.Stop();
_pacManager.Value.Stop();
}
}
catch (Exception ex)
@@ -110,7 +111,7 @@ public static class SysProxyHandler
private static async Task SetWindowsProxyPac(int port)
{
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
await PacManager.Instance.StartAsync(port, portPac);
await _pacManager.Value.StartAsync(port, portPac);
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
ProxySettingWindows.SetProxy(strProxy, "", 4);
}
+7 -3
View File
@@ -22,7 +22,7 @@ public class HttpClientHelper
this.httpClient = httpClient;
}
public async Task<string?> TryGetAsync(string url)
public async Task<string?> TryGetAsync(string url, CancellationToken cancellationToken = default)
{
if (url.IsNullOrEmpty())
{
@@ -31,8 +31,12 @@ public class HttpClientHelper
try
{
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
var response = await httpClient.GetAsync(url, cancellationToken);
return await response.Content.ReadAsStringAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
+20 -7
View File
@@ -75,15 +75,28 @@ public sealed class SQLiteHelper
public async Task DisposeDbConnectionAsync()
{
await Task.Factory.StartNew(() =>
await Task.Run(() =>
{
_db?.Close();
_db?.Dispose();
_db = null;
try
{
_db?.Close();
_db?.Dispose();
}
finally
{
_db = null;
}
_dbAsync?.GetConnection()?.Close();
_dbAsync?.GetConnection()?.Dispose();
_dbAsync = null;
try
{
var conn = _dbAsync?.GetConnection();
conn?.Close();
conn?.Dispose();
}
finally
{
_dbAsync = null;
}
});
}
}
+1 -1
View File
@@ -229,7 +229,7 @@ public sealed class CoreInfoManager
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",
LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
//LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
Match = "sing-box",
VersionArg = "version",
},
+49 -64
View File
@@ -2,36 +2,37 @@ namespace ServiceLib.Manager;
public class PacManager
{
private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
public static PacManager Instance => _instance.Value;
private int _httpPort;
private const string Tag = "PacManager";
private CancellationTokenSource? _cts;
private int _pacPort;
private TcpListener? _tcpListener;
private byte[] _writeContent;
private bool _isRunning;
private bool _needRestart = true;
private byte[] _writeContent = [];
public async Task StartAsync(int httpPort, int pacPort)
{
_needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning;
var content = await InitText(httpPort);
_writeContent = content;
_httpPort = httpPort;
_pacPort = pacPort;
await InitText();
if (_needRestart)
if (_tcpListener is not null && _pacPort == pacPort)
{
Stop();
RunListener();
return;
}
Stop();
var cts = new CancellationTokenSource();
var listener = TcpListener.Create(pacPort);
listener.Start();
_cts = cts;
_pacPort = pacPort;
_tcpListener = listener;
_ = ListenLoopAsync(listener, cts.Token);
}
private async Task InitText()
private async Task<byte[]> InitText(int httpPort)
{
var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyPacPath;
var fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath))
var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem.CustomSystemProxyPacPath;
var fileName = customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath)
? customSystemProxyPacPath
: Path.Combine(Utils.GetConfigPath(), "pac.txt");
@@ -45,7 +46,7 @@ public class PacManager
}
var pacText = await File.ReadAllTextAsync(fileName);
pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;");
pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{httpPort};DIRECT;");
var sb = new StringBuilder();
sb.AppendLine("HTTP/1.0 200 OK");
@@ -54,59 +55,43 @@ public class PacManager
sb.AppendLine("Content-Length:" + Encoding.UTF8.GetByteCount(pacText));
sb.AppendLine();
sb.Append(pacText);
_writeContent = Encoding.UTF8.GetBytes(sb.ToString());
return Encoding.UTF8.GetBytes(sb.ToString());
}
private void RunListener()
private async Task ListenLoopAsync(TcpListener listener, CancellationToken token)
{
_tcpListener = TcpListener.Create(_pacPort);
_isRunning = true;
_tcpListener.Start();
Task.Factory.StartNew(async () =>
var buffer = new byte[1024];
try
{
while (_isRunning)
while (!token.IsCancellationRequested)
{
try
{
if (!_tcpListener.Pending())
{
await Task.Delay(10);
continue;
}
var client = await _tcpListener.AcceptTcpClientAsync();
await Task.Run(() => WriteContent(client));
}
catch
{
// ignored
}
using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
await using var stream = client.GetStream();
_ = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
await stream.WriteAsync(_writeContent, token).ConfigureAwait(false);
await stream.FlushAsync(token).ConfigureAwait(false);
}
}, TaskCreationOptions.LongRunning);
}
private void WriteContent(TcpClient client)
{
var stream = client.GetStream();
stream.Write(_writeContent, 0, _writeContent.Length);
stream.Flush();
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logging.SaveLog(Tag, ex);
}
finally
{
listener.Stop();
}
}
public void Stop()
{
if (_tcpListener == null)
{
return;
}
try
{
_isRunning = false;
_tcpListener.Stop();
_tcpListener = null;
}
catch
{
// ignored
}
_cts?.Cancel();
_tcpListener?.Stop();
_cts?.Dispose();
_cts = null;
_pacPort = 0;
_tcpListener = null;
}
}
+4 -10
View File
@@ -12,23 +12,17 @@ public class TaskManager
_config = config;
_updateFunc = updateFunc;
_ = Task.Factory.StartNew(
ScheduledTasks,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Task.Run(ScheduledTasks);
}
private async Task ScheduledTasks()
{
Logging.SaveLog("Setup Scheduled Tasks");
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
var numOfExecuted = 1;
while (true)
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
{
//1 minute
await Task.Delay(1000 * 60);
//Execute once 1 minute
try
{
@@ -140,7 +134,7 @@ public class TaskManager
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.CheckPreReleaseUpdate, _config.CheckUpdateItem.UpdateViaProxy);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.UpdateViaProxy);
foreach (var msg in msgs)
{
await _updateFunc?.Invoke(false, msg);
@@ -242,9 +242,9 @@ public class WebDavItem
[Serializable]
public class CheckUpdateItem
{
public bool CheckPreReleaseUpdate { get; set; }
public bool UpdateViaProxy { get; set; } = true;
public List<string>? SelectedCoreTypes { get; set; }
public List<string>? CheckPreReleaseCoreTypes { get; set; }
}
[Serializable]
@@ -24,6 +24,7 @@ public record CoreConfigContext
public bool IsWindows { get; init; }
public bool IsMacOS { get; init; }
public bool IsLinux { get; init; }
// Defaults to true so that a context built without this flag keeps routing IPv6 into the
// tunnel; only a positive detection of the host having no global IPv6 address turns it off.
@@ -128,8 +128,6 @@ public class User4Sbox
public class Outbound4Sbox : BaseServer4Sbox
{
public string? server { get; set; }
public int? server_port { get; set; }
public List<string>? server_ports { get; set; }
public string? uuid { get; set; }
public string? security { get; set; }
@@ -171,6 +169,10 @@ public class Endpoints4Sbox : BaseServer4Sbox
public string? udp_timeout { get; set; }
public int? workers { get; set; }
public List<Peer4Sbox> peers { get; set; }
public string? username { get; set; }
public string? password { get; set; }
public string? path { get; set; }
public Dictionary<string, List<string>>? headers { get; set; }
}
public class Peer4Sbox
@@ -271,10 +273,8 @@ public class Server4Sbox : BaseServer4Sbox
public string? inet4_range { get; set; }
public string? inet6_range { get; set; }
public string? client_subnet { get; set; }
public string? server { get; set; }
public new string? domain_resolver { get; set; }
[JsonPropertyName("interface")] public string? Interface { get; set; }
public int? server_port { get; set; }
public string? path { get; set; }
public Headers4Sbox? headers { get; set; }
@@ -359,4 +359,6 @@ public abstract class BaseServer4Sbox : DialFields4Sbox
{
public string type { get; set; }
public string tag { get; set; }
public string? server { get; set; }
public int? server_port { get; set; }
}
@@ -92,6 +92,10 @@ public class Inboundsettings4Ray
public string? autoOutboundsInterface { get; set; }
public List<string>? dns { get; set; }
public bool? autoSystemDnsToGateway { get; set; }
public List<string>? autoSystemWfpBlockLeak { get; set; }
}
public class Sniffing4Ray
@@ -315,6 +319,7 @@ public class StreamSettings4Ray
public GrpcSettings4Ray? grpcSettings { get; set; }
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
public MasqueSettings4Ray? masqueSettings { get; set; }
public object? finalmask { get; set; }
@@ -443,6 +448,12 @@ public class HysteriaSettings4Ray
public string? auth { get; set; }
}
public class MasqueSettings4Ray
{
public string? path { get; set; }
public Dictionary<string, string>? headers { get; set; }
}
public class UdpHop4Ray
{
public string? ports { get; set; }
@@ -3,6 +3,8 @@ namespace ServiceLib.Models.Dto;
public partial class CheckUpdateModel : ReactiveObject
{
public bool? IsSelected { get; set; }
public bool IsCheckPreRelease { get; set; }
public bool ShowCheckPreRelease { get; set; }
public ECoreType? CoreType { get; set; }
[Reactive] public partial string? Remarks { get; set; }
public string? FileName { get; set; }
@@ -46,6 +46,10 @@ public record ProtocolExtraItem
public int? InsecureConcurrency { get; init; }
public bool? NaiveQuic { get; init; }
// MASQUE
public string? MasquePath { get; init; }
public string? MasqueHeaders { get; init; }
// group profile
public string? GroupType { get; init; }
public string? ChildItems { get; init; }
+19 -1
View File
@@ -789,6 +789,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Add [MASQUE] 的本地化字符串。
/// </summary>
public static string menuAddMasqueServer {
get {
return ResourceManager.GetString("menuAddMasqueServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Add [NaïveProxy] 的本地化字符串。
/// </summary>
@@ -924,6 +933,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Check and Update 的本地化字符串。
/// </summary>
public static string menuCheckAndUpdate {
get {
return ResourceManager.GetString("menuCheckAndUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only Check 的本地化字符串。
/// </summary>
@@ -2149,7 +2167,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Not Support 的本地化字符串。
/// 查找类似 Not Supported 的本地化字符串。
/// </summary>
public static string MsgNotSupport {
get {
+7 -1
View File
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>Filter, press Enter to execute</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>Check and Update</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Check Update</value>
</data>
@@ -1899,4 +1902,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSortingProcess" xml:space="preserve">
<value>Process</value>
</data>
</root>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>Add [MASQUE]</value>
</data>
</root>
+8 -2
View File
@@ -399,8 +399,11 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>Фильтр, нажмите Enter для выполнения</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>Проверить и обновить</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Обновить</value>
<value>Проверить обновления</value>
</data>
<data name="menuClose" xml:space="preserve">
<value>Закрыть</value>
@@ -1780,7 +1783,7 @@
<value>Доступна новая версия {0}: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Проверить</value>
<value>Только проверить</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Не поддерживается</value>
@@ -1899,4 +1902,7 @@
<data name="TbSortingProcess" xml:space="preserve">
<value>Процесс</value>
</data>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>Добавить сервер [MASQUE]</value>
</data>
</root>
+7 -1
View File
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>过滤器,按回车执行</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>检查并更新</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>检查更新</value>
</data>
@@ -1891,4 +1894,7 @@
<data name="TbSortingProcess" xml:space="preserve">
<value>进程</value>
</data>
</root>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>添加 [MASQUE]</value>
</data>
</root>
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>過濾器,按 Enter 執行</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>檢查並更新</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>檢查更新</value>
</data>
-105
View File
@@ -300,40 +300,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe
MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw
IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL
SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH
SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh
ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X
DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1
TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ
fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA
sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU
WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS
nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH
dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip
NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC
AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF
MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB
uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl
PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP
JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/
gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2
j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6
5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB
o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS
/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z
Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE
W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D
hNQ+IIX3Sj0rnP0qCglN6oH4EZw=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
@@ -732,20 +698,6 @@ LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
4uJEvlz36hz1
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G
jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2
4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7
VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm
ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
@@ -805,38 +757,6 @@ HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt
nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY
6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu
MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k
RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg
f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV
+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo
dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW
Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa
G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq
gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H
vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8
0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC
B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u
NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg
yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev
HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6
xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR
TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg
JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV
7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl
6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
@@ -1189,31 +1109,6 @@ AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB
RA+GsCyRxj3qrg+E
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG
EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3
MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl
cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR
dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB
pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM
b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm
aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz
IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT
lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz
AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5
VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG
ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2
BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG
AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M
U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh
bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C
+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F
uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt
MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg
@@ -365,9 +365,16 @@ public partial class CoreConfigSingboxService
rule4ExpectedIPs = JsonUtils.DeepCopy(rule);
rule4ExpectedIPs.geosite = regionGeosite;
}
if (rule.geosite?.Count > 0
|| rule.domain?.Count > 0
|| rule.domain_keyword?.Count > 0
|| rule.domain_regex?.Count > 0
|| rule.domain_suffix?.Count > 0)
{
AddRules(rule, item, directDnsList);
}
}
if (rule.geosite?.Count > 0 || rule.domain?.Count > 0)
else
{
AddRules(rule, item, directDnsList);
}
@@ -63,7 +63,7 @@ public partial class CoreConfigSingboxService
}
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
tunInbound.interface_name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.interface_name = context.IsMacOS ? null : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
@@ -77,7 +77,7 @@ public partial class CoreConfigSingboxService
}
}
if (_node.ConfigType == EConfigType.WireGuard)
if (_node.ConfigType is EConfigType.WireGuard or EConfigType.MASQUE)
{
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
FillEndpoint(endpoint);
@@ -350,13 +350,13 @@ public partial class CoreConfigSingboxService
{
var protocolExtra = _node.GetProtocolExtra();
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
endpoint.type = Global.ProtocolTypes[_node.ConfigType];
switch (_node.ConfigType)
{
case EConfigType.WireGuard:
{
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
var peer = new Peer4Sbox
{
public_key = protocolExtra.WgPublicKey ?? string.Empty,
@@ -371,6 +371,28 @@ public partial class CoreConfigSingboxService
endpoint.peers = [peer];
break;
}
case EConfigType.MASQUE:
{
endpoint.type = "masque-client";
endpoint.server = _node.Address;
endpoint.server_port = _node.Port;
endpoint.username = _node.Username;
endpoint.password = _node.Password;
endpoint.path = protocolExtra?.MasquePath.NullIfEmpty();
if (!string.IsNullOrEmpty(protocolExtra?.MasqueHeaders))
{
var headers = Utils.ParseHeaders(protocolExtra.MasqueHeaders)
.GroupBy(x => x.Item1, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => g.Select(x => x.Item2).ToList(),
StringComparer.OrdinalIgnoreCase
);
endpoint.headers = headers.Count > 0 ? headers : null;
}
FillOutboundTls(endpoint);
break;
}
}
}
catch (Exception ex)
@@ -402,7 +424,7 @@ public partial class CoreConfigSingboxService
}
}
private void FillOutboundTls(Outbound4Sbox outbound)
private void FillOutboundTls(BaseServer4Sbox server)
{
try
{
@@ -476,7 +498,7 @@ public partial class CoreConfigSingboxService
{
tls.ech = ech;
}
outbound.tls = tls;
server.tls = tls;
}
catch (Exception ex)
{
@@ -67,12 +67,10 @@ public partial class CoreConfigV2rayService
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.settings.gateway = [address];
// Route both families into the tunnel regardless of EnableIPv6Address. That option only
// controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing
// table makes IPv6 follow the system default route and bypass the tunnel entirely.
// A host without a global IPv6 address is the exception: it has nothing to leak,
// and IPv6 sent into the tunnel would have no way back out.
tunInbound.settings.autoSystemRoutingTable = context.HasGlobalIPv6Address
// Without ::/0, IPv6 bypasses the tunnel. Skip it only when the host has no global IPv6
// address and EnableIPv6Address is off; NAT66 hosts have only ULA addresses.
var routeIPv6 = _config.TunModeItem.EnableIPv6Address || context.HasGlobalIPv6Address;
tunInbound.settings.autoSystemRoutingTable = routeIPv6
? ["0.0.0.0/0", "::/0"]
: ["0.0.0.0/0"];
if (_config.TunModeItem.EnableIPv6Address == true)
@@ -90,6 +88,15 @@ public partial class CoreConfigV2rayService
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
tunInbound.sniffing.routeOnly = true;
if (context.IsWindows && _config.TunModeItem.StrictRoute == true)
{
tunInbound.settings.autoSystemWfpBlockLeak = ["dns", "misconfigtun"];
}
if (context.IsLinux)
{
tunInbound.settings.autoSystemDnsToGateway = true;
}
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
var wholeInternet = IPNetwork2.Parse("0.0.0.0/0");
@@ -99,7 +106,7 @@ public partial class CoreConfigV2rayService
.Where(x => x != null).ToList();
var includeList = new List<IPNetwork2> { wholeInternet };
var includeListV6 = context.HasGlobalIPv6Address
var includeListV6 = routeIPv6
? new List<IPNetwork2> { wholeInternetV6 }
: new List<IPNetwork2>();
@@ -182,6 +182,12 @@ public partial class CoreConfigV2rayService
outboundSettings.version = 2;
break;
}
case EConfigType.MASQUE:
{
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
break;
}
case EConfigType.WireGuard:
{
var address = _node.Address;
@@ -257,8 +263,13 @@ public partial class CoreConfigV2rayService
{
network = "hysteria";
}
else if (_node.ConfigType == EConfigType.MASQUE)
{
network = "masque";
}
streamSettings.network = network;
var transport = _node.GetTransportExtra();
var protocolExtra = _node.GetProtocolExtra();
var host = string.Empty;
var path = string.Empty;
var kcpSeed = string.Empty;
@@ -502,7 +513,6 @@ public partial class CoreConfigV2rayService
break;
case "hysteria":
var protocolExtra = _node.GetProtocolExtra();
var ports = protocolExtra?.Ports;
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
? su
@@ -574,6 +584,32 @@ public partial class CoreConfigV2rayService
streamSettings.finalmask = hy2Finalmask;
break;
case "masque":
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var authValue = $"Basic {Utils.Base64Encode($"{_node.Username}:{_node.Password}")}";
headers.Add("Authorization", authValue);
if (!string.IsNullOrEmpty(protocolExtra?.MasqueHeaders))
{
var userHeaders = Utils.ParseHeaders(protocolExtra.MasqueHeaders)
.GroupBy(x => x.Item1, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => string.Join(", ", g.Select(x => x.Item2)),
StringComparer.OrdinalIgnoreCase
);
if (userHeaders.TryGetValue("Authorization", out var userAuthValue))
{
headers["Authorization"] = userAuthValue;
}
headers = userHeaders;
}
streamSettings.masqueSettings = new()
{
path = protocolExtra?.MasquePath.NullIfEmpty(),
headers = headers.Count > 0 ? headers : null,
};
break;
default:
// raw
if (headerType == Global.RawHeaderHttp)
+33 -12
View File
@@ -8,7 +8,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private readonly Lock _runLock = new();
private CancellationTokenSource? _runCts;
private readonly List<CancellationTokenSource> _runCtsList = [];
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
@@ -18,11 +18,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
lock (_runLock)
{
_runCts?.Cancel();
runCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_runCts = runCts;
_runCtsList.Add(runCts);
}
return RunLoopAsync(actionType, selecteds, runCts);
@@ -30,17 +28,30 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
public void ExitLoop()
{
CancellationTokenSource? runCts;
var counter = 0;
List<CancellationTokenSource> listToCancel;
lock (_runLock)
{
runCts = _runCts;
listToCancel = _runCtsList.ToList();
counter = listToCancel.Count;
}
if (runCts is not null)
foreach (var cts in listToCancel)
{
try
{
cts.Cancel();
}
catch (ObjectDisposedException)
{
// Ignored
}
}
if (counter > 0)
{
_ = UpdateFunc("", ResUI.SpeedtestingStop);
runCts.Cancel();
}
}
@@ -67,10 +78,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
lock (_runLock)
{
if (ReferenceEquals(_runCts, runCts))
{
_runCts = null;
}
_runCtsList.Remove(runCts);
}
runCts.Dispose();
@@ -202,6 +210,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = lst.Count,
CancellationToken = ct,
};
@@ -283,6 +292,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = selecteds.Count,
CancellationToken = ct,
};
@@ -373,6 +383,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = selecteds.Count,
CancellationToken = ct,
};
@@ -562,6 +573,10 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
await clientSocket.ConnectAsync(endPoint, linkedCts.Token).ConfigureAwait(false);
responseTime = (int)timer.ElapsedMilliseconds;
}
catch
{
// Ignore
}
finally
{
timer.Stop();
@@ -572,6 +587,12 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
{
List<List<ServerTestItem>> lstTest = [];
if (lstSelected.Count == 0)
{
return lstTest;
}
pageSize = Math.Max(pageSize, 1);
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
@@ -5,8 +5,7 @@ namespace ServiceLib.Services.Statistics;
public class StatisticsSingboxService
{
private readonly Config _config;
private bool _exitFlag;
private ClientWebSocket? webSocket;
private CancellationTokenSource? _cts;
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic";
private static readonly string _tag = "StatisticsSingboxService";
@@ -15,40 +14,17 @@ public class StatisticsSingboxService
{
_config = config;
_updateFunc = updateFunc;
_exitFlag = false;
_ = Task.Factory.StartNew(
Run,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
private async Task Init()
{
await Task.Delay(5000);
try
{
if (webSocket == null)
{
webSocket = new ClientWebSocket();
await webSocket.ConnectAsync(new Uri(Url), CancellationToken.None);
}
}
catch { }
Task.Run(Run);
}
public void Close()
{
try
{
_exitFlag = true;
if (webSocket != null)
{
webSocket.Abort();
webSocket = null;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
catch (Exception ex)
{
@@ -58,53 +34,63 @@ public class StatisticsSingboxService
private async Task Run()
{
await Init();
Close();
_cts = new CancellationTokenSource();
var token = _cts.Token;
while (!_exitFlag)
while (!token.IsCancellationRequested)
{
await Task.Delay(1000);
try
{
if (!AppManager.Instance.IsRunningCore(ECoreType.sing_box))
{
await Task.Delay(1000, token).ConfigureAwait(false);
continue;
}
if (webSocket != null)
using var ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri(Url), token).ConfigureAwait(false);
var buffer = new byte[1024];
while (ws.State == WebSocketState.Open
&& !token.IsCancellationRequested)
{
if (webSocket.State is WebSocketState.Aborted or WebSocketState.Closed)
var res = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), token);
if (res.MessageType == WebSocketMessageType.Close)
{
webSocket.Abort();
webSocket = null;
await Init();
continue;
break;
}
using var ms = new MemoryStream();
ms.Write(buffer, 0, res.Count);
while (!res.EndOfMessage)
{
res = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), token).ConfigureAwait(false);
ms.Write(buffer, 0, res.Count);
}
if (webSocket.State != WebSocketState.Open)
var result = Encoding.UTF8.GetString(ms.ToArray());
if (!result.IsNotEmpty())
{
continue;
}
ParseOutput(result, out var up, out var down);
var buffer = new byte[1024];
var res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!res.CloseStatus.HasValue)
if (_updateFunc != null)
{
var result = Encoding.UTF8.GetString(buffer, 0, res.Count);
if (result.IsNotEmpty())
await _updateFunc.Invoke(new ServerSpeedItem
{
ParseOutput(result, out var up, out var down);
await _updateFunc?.Invoke(new ServerSpeedItem()
{
ProxyUp = (long)(up / 1000),
ProxyDown = (long)(down / 1000)
});
}
res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
ProxyUp = (long)(up / 1000),
ProxyDown = (long)(down / 1000),
}).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
break;
}
catch
{
await Task.Delay(3000, token).ConfigureAwait(false);
}
}
}
@@ -5,33 +5,42 @@ public class StatisticsXrayService
private const long linkBase = 1024;
private ServerSpeedItem _serverSpeedItem = new();
private readonly Config _config;
private bool _exitFlag;
private CancellationTokenSource? _cts;
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars";
private static readonly string _tag = "StatisticsXrayService";
public StatisticsXrayService(Config config, Func<ServerSpeedItem, Task> updateFunc)
{
_config = config;
_updateFunc = updateFunc;
_exitFlag = false;
_ = Task.Factory.StartNew(
Run,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Task.Run(Run);
}
public void Close()
{
_exitFlag = true;
try
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private async Task Run()
{
while (!_exitFlag)
Close();
_cts = new CancellationTokenSource();
var token = _cts.Token;
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
await Task.Delay(1000);
try
{
if (AppManager.Instance.RunningCoreType != ECoreType.Xray)
@@ -39,16 +48,20 @@ public class StatisticsXrayService
continue;
}
var result = await HttpClientHelper.Instance.TryGetAsync(Url);
var result = await HttpClientHelper.Instance.TryGetAsync(Url, token);
if (result != null)
{
var server = ParseOutput(result) ?? new ServerSpeedItem();
await _updateFunc?.Invoke(server);
await _updateFunc!.Invoke(server);
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
break;
}
catch
{
// ignored
await Task.Delay(3000, token).ConfigureAwait(false);
}
}
}
+2 -1
View File
@@ -111,7 +111,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken);
}
public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
public async Task<List<string>> CheckHasUpdateOnlyAll(bool blProxy = true, CancellationToken cancellationToken = default)
{
var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
@@ -121,6 +121,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
continue;
}
var preRelease = _config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(type.ToString()) ?? false;
var result = await CheckHasUpdateOnly(type, preRelease, blProxy, cancellationToken);
if (result.Success && result.Version != null)
{
@@ -97,6 +97,12 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
[Reactive]
public partial int GeckoMaxPacketSize { get; set; }
[Reactive]
public partial string MasquePath { get; set; }
[Reactive]
public partial string MasqueHeaders { get; set; }
[Reactive]
public partial string RawHeaderType { get; set; }
@@ -322,6 +328,8 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
MasquePath = protocolExtra.MasquePath ?? string.Empty;
MasqueHeaders = protocolExtra.MasqueHeaders ?? string.Empty;
RawHeaderType = transport.RawHeaderType ?? Global.None;
Host = transport.Host ?? string.Empty;
@@ -443,6 +451,8 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
Hy2RealmUrl = realm?.ToUri().NullIfEmpty(),
GeckoMinPacketSize = GeckoMinPacketSize > 0 ? GeckoMinPacketSize.ToString() : null,
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
MasquePath = MasquePath.NullIfEmpty(),
MasqueHeaders = MasqueHeaders.NullIfEmpty(),
});
SelectedSource.SetTransportExtra(transport);
@@ -12,7 +12,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject
public BulkObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = [];
public ReactiveCommand<RxVoid, RxVoid> CheckUpdateCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> CheckOnlyCmd { get; }
[Reactive] public partial bool EnableCheckPreReleaseUpdate { get; set; }
[Reactive] public partial bool EnableUpdateViaProxy { get; set; }
public CheckUpdateViewModel()
@@ -33,20 +33,26 @@ public partial class CheckUpdateViewModel : MyReactiveObject
_ = UpdateView(_v2rayN, ex.Message);
});
EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate;
EnableUpdateViaProxy = _config.CheckUpdateItem.UpdateViaProxy;
this.WhenAnyValue(x => x.EnableCheckPreReleaseUpdate)
.SubscribeAsync(async _ => await OnCheckPreReleaseUpdateChanged());
this.WhenAnyValue(x => x.EnableUpdateViaProxy)
.Subscribe(c => _ = OnUpdateViaProxyChanged());
RefreshCheckUpdateItems();
AppEvents.HasUpdateNotified
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(bl => RefreshCheckUpdateItems(bl));
RefreshCheckUpdateItems(true);
}
private void RefreshCheckUpdateItems()
private void RefreshCheckUpdateItems(bool hasUpdate)
{
if (!hasUpdate)
{
return;
}
var models = CoreInfoManager.Instance.GetCheckUpdateCoreTypes()
.Select(t => GetCheckUpdateModel(t))
.ToList();
@@ -63,6 +69,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
return new()
{
IsSelected = false,
IsCheckPreRelease = false,
ShowCheckPreRelease = false,
CoreType = coreType,
IsGeoFile = false,
Remarks = ResUI.menuCheckUpdate + $" ({ResUI.MsgNotSupport})",
@@ -73,6 +81,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
return new()
{
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType.ToString()) ?? true,
IsCheckPreRelease = _config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(coreType.ToString()) ?? false,
ShowCheckPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(coreType, true),
CoreType = coreType,
IsGeoFile = false,
Remarks = lastResult ?? ResUI.menuCheckUpdate,
@@ -84,22 +94,14 @@ public partial class CheckUpdateViewModel : MyReactiveObject
return new()
{
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(_geo) ?? true,
IsCheckPreRelease = false,
ShowCheckPreRelease = false,
CoreType = null,
IsGeoFile = true,
Remarks = ResUI.menuCheckUpdate,
};
}
private async Task OnCheckPreReleaseUpdateChanged()
{
if (_config.CheckUpdateItem.CheckPreReleaseUpdate == EnableCheckPreReleaseUpdate)
{
return;
}
_config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate;
await SaveSelectedCoreTypes();
}
private async Task OnUpdateViaProxyChanged()
{
if (_config.CheckUpdateItem.UpdateViaProxy == EnableUpdateViaProxy)
@@ -117,6 +119,11 @@ public partial class CheckUpdateViewModel : MyReactiveObject
.Select(t => t.CoreTypeForStorage)
.ToList();
_config.CheckUpdateItem.CheckPreReleaseCoreTypes =
CheckUpdateModels.Where(t => t.IsCheckPreRelease == true)
.Select(t => t.CoreTypeForStorage)
.ToList();
await ConfigHandler.SaveConfig(_config);
}
@@ -157,7 +164,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject
}
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, EnableCheckPreReleaseUpdate, EnableUpdateViaProxy);
var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, item.IsCheckPreRelease, EnableUpdateViaProxy);
if (result.Success && result.Version != null)
{
await UpdateView(item.CoreType, string.Format(ResUI.MsgCheckUpdateHasNewVersion, item.CoreType, result.Version));
@@ -203,15 +210,11 @@ public partial class CheckUpdateViewModel : MyReactiveObject
await UpdateView(_v2rayN, ResUI.MsgNotSupport);
continue;
}
await CheckUpdateN(EnableCheckPreReleaseUpdate);
}
else if (item.CoreType is ECoreType.Xray or ECoreType.sing_box)
{
await CheckUpdateCore(item, EnableCheckPreReleaseUpdate);
await CheckUpdateN(item.IsCheckPreRelease);
}
else if (item.CoreType.HasValue)
{
await CheckUpdateCore(item, false);
await CheckUpdateCore(item, item.IsCheckPreRelease);
}
}
@@ -30,12 +30,7 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Factory.StartNew(
async () => await GetClashConnectionsTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
Task.Run(() => GetClashConnectionsTask(token));
});
}
@@ -129,9 +124,9 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
try
{
var numOfExecuted = 1;
while (!token.IsCancellationRequested)
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
await Task.Delay(1000, token);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
@@ -61,12 +61,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Factory.StartNew(
async () => await GetClashProxiesTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
Task.Run(() => GetClashProxiesTask(token));
});
}
@@ -113,9 +108,9 @@ public partial class ClashProxiesViewModel : MyReactiveObject
try
{
var numOfExecuted = 1;
while (!token.IsCancellationRequested)
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
await Task.Delay(1000, token);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
@@ -32,6 +32,7 @@ public partial class MainWindowViewModel : MyReactiveObject
public ReactiveCommand<RxVoid, RxVoid> AddWireguardServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddAnytlsServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddNaiveServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddMasqueServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddCustomServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddCustomOutboundServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddPolicyGroupServerCmd { get; }
@@ -140,6 +141,10 @@ public partial class MainWindowViewModel : MyReactiveObject
{
await AddServerAsync(EConfigType.Naive);
});
AddMasqueServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(EConfigType.MASQUE);
});
AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(EConfigType.Custom);
@@ -522,7 +527,7 @@ public partial class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaImageAsync()
{
var imageFileName = await BrowseImageFileInteraction.HandleSafe(RxVoid.Default);
await AddScanResultAsync(imageFileName);
await ScanImageResult(imageFileName);
}
public async Task ScanImageResult(string fileName)
@@ -113,6 +113,7 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
[Reactive] public partial string CoreType6 { get; set; }
[Reactive] public partial string CoreType7 { get; set; }
[Reactive] public partial string CoreType9 { get; set; }
[Reactive] public partial string CoreType14 { get; set; }
#endregion CoreType
@@ -283,6 +284,10 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
case 9:
CoreType9 = type;
break;
case 14:
CoreType14 = type;
break;
}
});
await Task.CompletedTask;
@@ -451,6 +456,10 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
type = CoreType9;
break;
case 14:
type = CoreType14;
break;
default:
continue;
}
@@ -219,7 +219,8 @@ public partial class StatusBarViewModel : MyReactiveObject
await ConfigHandler.InitBuiltinRouting(_config);
await RefreshRoutingsMenu();
await InboundDisplayStatus();
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true);
// The view refreshes its icon on initialization; interaction handlers are not registered yet.
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, false);
BlRouting = true;
}
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -23,12 +23,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -783,6 +783,70 @@
HorizontalAlignment="Left" />
</Grid>
<Grid
x:Name="gridMasque"
Grid.Row="2"
ColumnDefinitions="300,Auto"
IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbUsername}" />
<TextBox
x:Name="txtId14"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbId3}" />
<TextBox
x:Name="txtSecurity14"
Grid.Row="2"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPath}" />
<TextBox
x:Name="txtPath14"
Grid.Row="3"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="4"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbHttpOutboundHeaders}" />
<TextBox
x:Name="txtHttpHeaders14"
Grid.Row="4"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
Classes="TextArea"
PlaceholderText="Header: Value"
TextWrapping="NoWrap" />
</Grid>
<Separator
x:Name="sepa2"
Grid.Row="3"
@@ -141,6 +141,13 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.MASQUE:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasquePath, v => v.txtPath14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasqueHeaders, v => v.txtHttpHeaders14.Text).DisposeWith(currentTypeDisposables);
break;
}
})
.DisposeWith(disposables);
@@ -194,7 +201,6 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
}
private void InitializeData(ProfileItem profileItem)
@@ -284,6 +290,14 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
break;
case EConfigType.MASQUE:
gridMasque.IsVisible = true;
sepa2.IsVisible = false;
gridTransport.IsVisible = false;
cmbFingerprint.IsEnabled = false;
cmbAlpn.IsEnabled = false;
break;
}
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity;
@@ -24,16 +24,23 @@
HorizontalAlignment="Left" />
</StackPanel>
<StackPanel>
<StackPanel Grid.IsSharedSizeScope="True">
<Border
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Theme="{DynamicResource CardBorder}">
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="300,200"
RowDefinitions="Auto,Auto,Auto,Auto">
<Grid Margin="{StaticResource Margin4}" RowDefinitions="Auto,Auto,Auto,Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto"
MinWidth="300"
SharedSizeGroup="BackupLabel" />
<ColumnDefinition
Width="Auto"
MinWidth="200"
SharedSizeGroup="BackupButton" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
@@ -79,10 +86,17 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Theme="{DynamicResource CardBorder}">
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="300,200"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid Margin="{StaticResource Margin4}" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto"
MinWidth="300"
SharedSizeGroup="BackupLabel" />
<ColumnDefinition
Width="Auto"
MinWidth="200"
SharedSizeGroup="BackupButton" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Row="0"
Grid.Column="0"
@@ -35,27 +35,17 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}" />
<ToggleSwitch
x:Name="togEnableCheckPreReleaseUpdate"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<Button
x:Name="btnCheckOnly"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.menuCheckOnly}" />
<Button
x:Name="btnCheckUpdate"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.menuCheckUpdate}" />
Content="{x:Static resx:ResUI.menuCheckAndUpdate}" />
</StackPanel>
</Border>
@@ -75,8 +65,8 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Panel Width="500" VerticalAlignment="Center">
<Grid ColumnDefinitions="1*,1*,3*" RowDefinitions="Auto">
<Panel Width="680" VerticalAlignment="Center">
<Grid ColumnDefinitions="1*,3*,1*,3*" RowDefinitions="Auto">
<ToggleSwitch
x:Name="togAutoRefresh"
Grid.Column="0"
@@ -84,13 +74,32 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{Binding IsSelected}" />
<TextBlock
<DockPanel
Grid.Column="1"
Margin="{StaticResource MarginLr4}"
IsVisible="{Binding ShowCheckPreRelease}">
<ToggleSwitch
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{Binding IsCheckPreRelease}"
OffContent=""
OnContent="" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}"
TextWrapping="Wrap" />
</DockPanel>
<TextBlock
Grid.Column="2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding CoreTypeForStorage}" />
<TextBlock
Grid.Column="2"
Grid.Column="3"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding Remarks}"
@@ -10,7 +10,6 @@ public partial class CheckUpdateView : ReactiveUserControl<CheckUpdateViewModel>
{
this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCheckPreReleaseUpdate, v => v.togEnableCheckPreReleaseUpdate.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableUpdateViaProxy, v => v.togEnableUpdateViaProxy.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckOnlyCmd, v => v.btnCheckOnly).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -23,17 +23,17 @@
Orientation="Horizontal">
<Button
x:Name="btnReset"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbReset}" />
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -18,6 +18,15 @@
ShowInTaskbar="True"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Styles>
<!-- Keep the log selected; hide only the strip when it has no other tabs. -->
<Style Selector="TabControl.logPane /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="IsVisible" Value="{Binding ShowClashUI}" />
</Style>
<Style Selector="TabControl.logPane /template/ Border#PART_BorderSeparator">
<Setter Property="IsVisible" Value="{Binding ShowClashUI}" />
</Style>
</Window.Styles>
<dialogHost:DialogHost CloseOnClickAway="True" DisableOpeningAnimation="True">
<DockPanel>
<DockPanel Margin="{StaticResource Margin8}" DockPanel.Dock="Top">
@@ -45,6 +54,7 @@
<MenuItem x:Name="menuAddTrojanServer" Header="{x:Static resx:ResUI.menuAddTrojanServer}" />
<MenuItem x:Name="menuAddHysteria2Server" Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem x:Name="menuAddWireguardServer" Header="{x:Static resx:ResUI.menuAddWireguardServer}" />
<MenuItem x:Name="menuAddMasqueServer" Header="{x:Static resx:ResUI.menuAddMasqueServer}" />
<MenuItem x:Name="menuAddSocksServer" Header="{x:Static resx:ResUI.menuAddSocksServer}" />
<MenuItem x:Name="menuAddHttpServer" Header="{x:Static resx:ResUI.menuAddHttpServer}" />
<Separator />
@@ -123,6 +133,7 @@
<TabControl
x:Name="tabMain"
Grid.Column="2"
Classes="logPane"
HorizontalContentAlignment="Left">
<TabItem x:Name="tabMsgView" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
<TabItem x:Name="tabClashProxies" Header="{x:Static resx:ResUI.TbProxies}" />
@@ -138,6 +149,7 @@
<TabControl
x:Name="tabMain1"
Grid.Row="2"
Classes="logPane"
TabStripPlacement="Left">
<TabItem x:Name="tabMsgView1" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
<TabItem x:Name="tabClashProxies1" Header="{x:Static resx:ResUI.TbProxies}" />
@@ -46,6 +46,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddMasqueServerCmd, v => v.menuAddMasqueServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
@@ -418,7 +419,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.OneWayBind(ViewModel, vm => vm.MsgViewModel, v => v.tabMsgView.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashConnectionsViewModel, v => v.tabClashConnections.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections.IsVisible).DisposeWith(currentLayoutDisposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain.SelectedIndex).DisposeWith(currentLayoutDisposables);
@@ -429,7 +429,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.OneWayBind(ViewModel, vm => vm.MsgViewModel, v => v.tabMsgView1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashConnectionsViewModel, v => v.tabClashConnections1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView1.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies1.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections1.IsVisible).DisposeWith(currentLayoutDisposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain1.SelectedIndex).DisposeWith(currentLayoutDisposables);
+2 -1
View File
@@ -23,7 +23,8 @@
Width="200"
Margin="{StaticResource MarginLr8}"
VerticalContentAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.MsgFilterTitle}" />
PlaceholderText="{x:Static resx:ResUI.MsgFilterTitle}"
ToolTip.Tip="{x:Static resx:ResUI.MsgFilterTitle}" />
<Button
x:Name="btnCopy"
+8 -3
View File
@@ -47,9 +47,14 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
if (togScrollToEnd.IsChecked ?? true)
{
txtMsg.ScrollToEnd();
Dispatcher.UIThread.Invoke(() =>
txtMsg.ScrollTo(txtMsg.LineCount, 0, AvaloniaEdit.Rendering.VisualYPosition.TextBottom, txtMsg.Bounds.Height, 0),
DispatcherPriority.Background);
// Let layout finish without a nested dispatcher loop during a native window drag.
Dispatcher.UIThread.Post(() =>
{
if (togScrollToEnd.IsChecked ?? true)
{
txtMsg.ScrollTo(txtMsg.LineCount, 0, AvaloniaEdit.Rendering.VisualYPosition.TextBottom, txtMsg.Bounds.Height, 0);
}
}, DispatcherPriority.Background);
}
}
@@ -23,12 +23,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -1192,7 +1192,7 @@
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
Grid.Column="0"
@@ -1296,6 +1296,19 @@
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="9"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="MASQUE" />
<ComboBox
x:Name="cmbCoreType14"
Grid.Row="9"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
</Grid>
</TabItem>
</TabControl>
@@ -46,6 +46,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbCoreType6.ItemsSource = Global.CoreTypes;
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbCoreType14.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(Global.SpeedTestConcurrencyCountMin, 20).ToList();
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
@@ -145,6 +146,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.CoreType6, v => v.cmbCoreType6.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType7, v => v.cmbCoreType7.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType9, v => v.cmbCoreType9.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType14, v => v.cmbCoreType14.SelectedValue).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
@@ -28,12 +28,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -72,7 +72,8 @@
Margin="{StaticResource MarginLr4}"
VerticalContentAlignment="Center"
Text="{Binding ServerFilter, Mode=TwoWay}"
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" />
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}"
ToolTip.Tip="{x:Static resx:ResUI.MsgServerTitle}" />
</WrapPanel>
<!-- Profiles grid -->
@@ -66,7 +66,8 @@
Width="200"
Margin="{StaticResource MarginLr4}"
VerticalContentAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" />
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}"
ToolTip.Tip="{x:Static resx:ResUI.MsgServerTitle}" />
<Button
x:Name="btnAutofitColumnWidth"
@@ -439,6 +439,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
private static readonly DataFormat<ProfileItemModel> LstProfilesRowFormat =
DataFormat.CreateInProcessFormat<ProfileItemModel>("LstProfilesRow");
private (Point, PointerPressedEventArgs)? _dragStartPoint;
private void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e)
@@ -208,12 +208,12 @@
</StackPanel>
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -30,12 +30,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -22,12 +22,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -136,7 +136,8 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}" />
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}"
TextWrapping="Wrap" />
</DockPanel>
<TextBlock
@@ -230,7 +231,8 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}"
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectPrevProfile"
Grid.Row="10"
@@ -250,7 +252,8 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}"
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectNextProfile"
Grid.Row="11"
@@ -20,12 +20,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -10,6 +10,10 @@
<CETCompat>false</CETCompat>
</PropertyGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64' Or '$(RuntimeIdentifier)' == 'osx-arm64'">
<RuntimeHostConfigurationOption Include="System.Net.Security.UseNetworkFramework" Value="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="Avalonia.Controls.DataGrid">
@@ -27,13 +27,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
+2 -2
View File
@@ -25,13 +25,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
+85 -2
View File
@@ -33,13 +33,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -1000,6 +1000,89 @@
HorizontalAlignment="Left" />
</Grid>
<Grid
x:Name="gridMasque"
Grid.Row="2"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbUsername}" />
<TextBox
x:Name="txtId14"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbId3}" />
<TextBox
x:Name="txtSecurity14"
Grid.Row="2"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbPath}" />
<TextBox
x:Name="txtPath14"
Grid.Row="3"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="4"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbHttpOutboundHeaders}" />
<TextBox
x:Name="txtHeaders14"
Grid.Row="4"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
materialDesign:HintAssist.Hint="Header: Value"
AcceptsReturn="True"
MaxLines="12"
MinLines="3"
Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="Wrap" />
</Grid>
<Separator
x:Name="sepa2"
Grid.Row="3"
@@ -139,6 +139,13 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.MASQUE:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasquePath, v => v.txtPath14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasqueHeaders, v => v.txtHeaders14.Text).DisposeWith(currentTypeDisposables);
break;
}
})
.DisposeWith(disposables);
@@ -284,6 +291,14 @@ public partial class AddServerWindow
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
break;
case EConfigType.MASQUE:
gridMasque.Visibility = Visibility.Visible;
sepa2.Visibility = Visibility.Collapsed;
gridTransport.Visibility = Visibility.Collapsed;
cmbFingerprint.IsEnabled = false;
cmbAlpn.IsEnabled = false;
break;
}
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity;
+17 -5
View File
@@ -30,7 +30,7 @@
Style="{StaticResource ToolbarTextBlock}" />
</DockPanel>
<StackPanel>
<StackPanel Grid.IsSharedSizeScope="True">
<materialDesign:Card Width="Auto" Margin="{StaticResource Margin8}">
<Grid Margin="{StaticResource Margin8}">
<Grid.RowDefinitions>
@@ -40,8 +40,14 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="200" />
<ColumnDefinition
Width="Auto"
MinWidth="300"
SharedSizeGroup="BackupLabel" />
<ColumnDefinition
Width="Auto"
MinWidth="200"
SharedSizeGroup="BackupButton" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
@@ -98,8 +104,14 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="200" />
<ColumnDefinition
Width="Auto"
MinWidth="300"
SharedSizeGroup="BackupLabel" />
<ColumnDefinition
Width="Auto"
MinWidth="200"
SharedSizeGroup="BackupButton" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock
+30 -17
View File
@@ -14,7 +14,9 @@
x:TypeArguments="vms:CheckUpdateViewModel"
Style="{StaticResource ViewGlobal}"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
</UserControl.Resources>
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin8}"
@@ -32,28 +34,18 @@
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}" />
<ToggleButton
x:Name="togEnableCheckPreReleaseUpdate"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<Button
x:Name="btnCheckOnly"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin8}"
Content="{x:Static resx:ResUI.menuCheckOnly}"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCheckUpdate"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin8}"
Content="{x:Static resx:ResUI.menuCheckUpdate}"
Content="{x:Static resx:ResUI.menuCheckAndUpdate}"
IsDefault="True"
Style="{StaticResource DefButton}" />
</StackPanel>
@@ -71,7 +63,7 @@
<ListView.ItemTemplate>
<DataTemplate>
<Border
Width="500"
Width="660"
Margin="{StaticResource Margin8}"
Padding="0"
VerticalAlignment="Center">
@@ -81,6 +73,7 @@
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
@@ -91,12 +84,32 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{Binding IsSelected}" />
<TextBlock
<DockPanel
Grid.Column="1"
Margin="{StaticResource MarginLeftRight4}"
Visibility="{Binding ShowCheckPreRelease, Converter={StaticResource BoolToVisConverter}}">
<ToggleButton
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{Binding IsCheckPreRelease}" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}"
TextWrapping="Wrap" />
</DockPanel>
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource ListItemTitle}"
Text="{Binding CoreTypeForStorage}" />
<TextBlock
Grid.Column="2"
Grid.Column="3"
VerticalAlignment="Center"
Style="{StaticResource ListItemSubTitle}"
Text="{Binding Remarks}"
TextWrapping="WrapWithOverflow" />
@@ -10,7 +10,6 @@ public partial class CheckUpdateView
{
this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCheckPreReleaseUpdate, v => v.togEnableCheckPreReleaseUpdate.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableUpdateViaProxy, v => v.togEnableUpdateViaProxy.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckOnlyCmd, v => v.btnCheckOnly).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
+2 -2
View File
@@ -25,13 +25,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -25,13 +25,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -25,19 +25,19 @@
Orientation="Horizontal">
<Button
x:Name="btnReset"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbReset}"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
+4
View File
@@ -112,6 +112,10 @@
x:Name="menuAddWireguardServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddWireguardServer}" />
<MenuItem
x:Name="menuAddMasqueServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddMasqueServer}" />
<MenuItem
x:Name="menuAddSocksServer"
Height="{StaticResource MenuItemHeight}"
+1
View File
@@ -46,6 +46,7 @@ public partial class MainWindow
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddMasqueServerCmd, v => v.menuAddMasqueServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
+2 -1
View File
@@ -28,7 +28,8 @@
materialDesign:TextFieldAssist.HasClearButton="True"
AutomationProperties.Name="{x:Static resx:ResUI.MsgFilterTitle}"
IsEditable="True"
Style="{StaticResource DefComboBox}" />
Style="{StaticResource DefComboBox}"
ToolTip="{x:Static resx:ResUI.MsgFilterTitle}" />
<Button
x:Name="btnCopy"
Width="24"
+18 -2
View File
@@ -32,13 +32,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -1473,6 +1473,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -1597,6 +1598,21 @@
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefComboBox}" />
<TextBlock
Grid.Row="9"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="MASQUE" />
<ComboBox
x:Name="cmbCoreType14"
Grid.Row="9"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefComboBox}" />
</Grid>
</TabItem>
</TabControl>
@@ -42,6 +42,7 @@ public partial class OptionSettingWindow
cmbCoreType6.ItemsSource = Global.CoreTypes;
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbCoreType14.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(Global.SpeedTestConcurrencyCountMin, 20).ToList();
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
@@ -141,6 +142,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.CoreType6, v => v.cmbCoreType6.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType7, v => v.cmbCoreType7.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType9, v => v.cmbCoreType9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType14, v => v.cmbCoreType14.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
@@ -30,13 +30,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -77,7 +77,8 @@
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.MsgServerTitle}"
materialDesign:TextFieldAssist.HasClearButton="True"
AutomationProperties.Name="{x:Static resx:ResUI.MsgServerTitle}"
Style="{StaticResource DefTextBox}" />
Style="{StaticResource DefTextBox}"
ToolTip="{x:Static resx:ResUI.MsgServerTitle}" />
</WrapPanel>
<DataGrid
x:Name="lstProfiles"
@@ -149,5 +149,6 @@ public partial class ProfilesSelectWindow
e.Handled = true;
}
}
#endregion Event
}
+2 -1
View File
@@ -65,7 +65,8 @@
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.MsgServerTitle}"
materialDesign:TextFieldAssist.HasClearButton="True"
AutomationProperties.Name="{x:Static resx:ResUI.MsgServerTitle}"
Style="{StaticResource DefTextBox}" />
Style="{StaticResource DefTextBox}"
ToolTip="{x:Static resx:ResUI.MsgServerTitle}" />
<Button
x:Name="btnAutofitColumnWidth"
@@ -237,13 +237,13 @@
</StackPanel>
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -74,13 +74,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
+8 -5
View File
@@ -32,13 +32,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -174,7 +174,8 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}" />
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}"
TextWrapping="Wrap" />
</DockPanel>
<TextBlock
@@ -282,7 +283,8 @@
VerticalAlignment="Center"
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.LvPrevProfileTip}"
AcceptsReturn="True"
Style="{StaticResource MyOutlinedTextBox}" />
Style="{StaticResource MyOutlinedTextBox}"
ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectPrevProfile"
Grid.Row="10"
@@ -307,7 +309,8 @@
VerticalAlignment="Center"
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.LvPrevProfileTip}"
AcceptsReturn="True"
Style="{StaticResource MyOutlinedTextBox}" />
Style="{StaticResource MyOutlinedTextBox}"
ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectNextProfile"
Grid.Row="11"