Compare commits

..

11 Commits

Author SHA1 Message Date
2dust
0db611b7a9 up 7.20.3 2026-04-12 10:00:43 +08:00
2dust
c3d67d186a Update xunit.runner and fix socks port names 2026-04-12 10:00:14 +08:00
DHR60
6b07ca63a0 Add sing-box send-through (#9081)
* Add sing-box send-through

* Perf resx
2026-04-12 09:25:17 +08:00
renwofei423
6c8f22ab86 feat(send-through): 支持配置本地出站地址 (#8946)
* feat(send-through): 支持配置本地出站地址

为 Xray 增加 SendThrough 配置项,允许指定本机 IPv4 作为出站源地址。

- 在核心设置页新增 SendThrough 输入框及中英文提示文案
- 保存配置时校验并持久化本机 IPv4 地址
- 生成 Xray 配置时为所有 outbound 写入 sendThrough 字段

影响说明:
- 仅对 Xray 生效,留空时不设置该字段

* fix(send-through): limit sendThrough to remote egress outbounds
2026-04-11 21:02:43 +08:00
DHR60
49f65579aa Replace protect-ss with protect-socks (#9052) 2026-04-11 19:45:23 +08:00
dependabot[bot]
a69e407bda Bump actions/upload-artifact from 7.0.0 to 7.0.1 (#9073)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v7.0.0...v7.0.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  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-04-11 19:42:39 +08:00
taskmgr818
96e5c11fc7 Replace 100MB Cloudflare speedtest URL with 99MB (#9076) 2026-04-11 19:39:25 +08:00
2dust
53041906b3 Use LibraryImport for native APIs 2026-04-06 20:16:30 +08:00
2dust
f2d929f40e Update GlobalHotKeys 2026-04-06 20:00:48 +08:00
2dust
1160d8c154 Add Windows screen QR scan support for avalonia 2026-04-06 15:34:25 +08:00
2dust
75f2cbaef9 Code clean 2026-04-05 17:18:28 +08:00
45 changed files with 747 additions and 96 deletions

View File

@@ -47,7 +47,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o "$OutputPathArm64"
- name: Upload build artifacts
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v7.0.1
with:
name: v2rayN-linux
path: |
@@ -116,7 +116,7 @@ jobs:
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
- name: Upload DEB artifacts
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v7.0.1
with:
name: v2rayN-deb
path: dist/deb/**/*.deb
@@ -229,7 +229,7 @@ jobs:
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPM artifacts
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v7.0.1
with:
name: v2rayN-rpm
path: dist/rpm/**/*.rpm

View File

@@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v7.0.1
with:
name: v2rayN-macos
path: |

View File

@@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v7.0.1
with:
name: v2rayN-windows-desktop
path: |

View File

@@ -42,7 +42,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v7.0.1
with:
name: v2rayN-windows
path: |

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.20.2</Version>
<Version>7.20.3</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -12,10 +12,11 @@
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
<PackageVersion Include="CliWrap" Version="3.10.1" />
<PackageVersion Include="Downloader" Version="5.1.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.1" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.3.1.1" />
<PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.1" />
@@ -26,6 +27,8 @@
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
</ItemGroup>

View File

@@ -0,0 +1,228 @@
using System.Text.Json.Nodes;
using ServiceLib;
using ServiceLib.Enums;
using ServiceLib.Models;
using ServiceLib.Services.CoreConfig;
using Xunit;
namespace ServiceLib.Tests;
public class CoreConfigV2rayServiceTests
{
private const string SendThrough = "198.51.100.10";
[Fact]
public void GenerateClientConfigContent_OnlyAppliesSendThroughToRemoteProxyOutbounds()
{
var node = CreateProxyNode("proxy-1", "198.51.100.1", 443);
var service = new CoreConfigV2rayService(CreateContext(node));
var result = service.GenerateClientConfigContent();
Assert.True(result.Success);
var outbounds = GetOutbounds(result.Data?.ToString());
var proxyOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue<string>() == Global.ProxyTag);
var directOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue<string>() == Global.DirectTag);
var blockOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue<string>() == Global.BlockTag);
Assert.Equal(SendThrough, proxyOutbound["sendThrough"]?.GetValue<string>());
Assert.Null(directOutbound["sendThrough"]);
Assert.Null(blockOutbound["sendThrough"]);
}
[Fact]
public void GenerateClientConfigContent_OnlyAppliesSendThroughToChainExitOutbounds()
{
var exitNode = CreateProxyNode("exit", "198.51.100.2", 443);
var entryNode = CreateProxyNode("entry", "198.51.100.3", 443);
var chainNode = CreateChainNode("chain", exitNode, entryNode);
var service = new CoreConfigV2rayService(CreateContext(
chainNode,
allProxiesMap: new Dictionary<string, ProfileItem>
{
[exitNode.IndexId] = exitNode,
[entryNode.IndexId] = entryNode,
}));
var result = service.GenerateClientConfigContent();
Assert.True(result.Success);
var outbounds = GetOutbounds(result.Data?.ToString())
.Where(outbound => outbound["protocol"]?.GetValue<string>() is not ("freedom" or "blackhole" or "dns"))
.ToList();
var sendThroughOutbounds = outbounds
.Where(outbound => outbound["sendThrough"]?.GetValue<string>() == SendThrough)
.ToList();
var chainedOutbounds = outbounds
.Where(outbound => outbound["streamSettings"]?["sockopt"]?["dialerProxy"] is not null)
.ToList();
Assert.Single(sendThroughOutbounds);
Assert.All(chainedOutbounds, outbound => Assert.Null(outbound["sendThrough"]));
}
[Fact]
public void GenerateClientConfigContent_DoesNotApplySendThroughToTunRelayLoopbackOutbound()
{
var node = CreateProxyNode("proxy-1", "198.51.100.4", 443);
var config = CreateConfig();
config.TunModeItem.EnableLegacyProtect = false;
var service = new CoreConfigV2rayService(CreateContext(
node,
config,
isTunEnabled: true,
tunProtectSsPort: 10811,
proxyRelaySsPort: 10812));
var result = service.GenerateClientConfigContent();
Assert.True(result.Success);
var outbounds = GetOutbounds(result.Data?.ToString());
Assert.DoesNotContain(outbounds, outbound => outbound["sendThrough"]?.GetValue<string>() == SendThrough);
}
private static CoreConfigContext CreateContext(
ProfileItem node,
Config? config = null,
Dictionary<string, ProfileItem>? allProxiesMap = null,
bool isTunEnabled = false,
int tunProtectSsPort = 0,
int proxyRelaySsPort = 0)
{
return new CoreConfigContext
{
Node = node,
RunCoreType = ECoreType.Xray,
AppConfig = config ?? CreateConfig(),
AllProxiesMap = allProxiesMap ?? new(),
SimpleDnsItem = new SimpleDNSItem(),
IsTunEnabled = isTunEnabled,
TunProtectSocksPort = tunProtectSsPort,
ProxyRelaySocksPort = proxyRelaySsPort,
};
}
private static Config CreateConfig()
{
return new Config
{
IndexId = string.Empty,
SubIndexId = string.Empty,
CoreBasicItem = new()
{
LogEnabled = false,
Loglevel = "warning",
MuxEnabled = false,
DefAllowInsecure = false,
DefFingerprint = Global.Fingerprints.First(),
DefUserAgent = string.Empty,
SendThrough = SendThrough,
EnableFragment = false,
EnableCacheFile4Sbox = true,
},
TunModeItem = new()
{
EnableTun = false,
AutoRoute = true,
StrictRoute = true,
Stack = string.Empty,
Mtu = 9000,
EnableIPv6Address = false,
IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
EnableLegacyProtect = false,
},
KcpItem = new(),
GrpcItem = new(),
RoutingBasicItem = new()
{
DomainStrategy = Global.DomainStrategies.First(),
DomainStrategy4Singbox = Global.DomainStrategies4Sbox.First(),
RoutingIndexId = string.Empty,
},
GuiItem = new(),
MsgUIItem = new(),
UiItem = new()
{
CurrentLanguage = "en",
CurrentFontFamily = string.Empty,
MainColumnItem = [],
WindowSizeItem = [],
},
ConstItem = new(),
SpeedTestItem = new(),
Mux4RayItem = new()
{
Concurrency = 8,
XudpConcurrency = 8,
XudpProxyUDP443 = "reject",
},
Mux4SboxItem = new()
{
Protocol = string.Empty,
},
HysteriaItem = new(),
ClashUIItem = new()
{
ConnectionsColumnItem = [],
},
SystemProxyItem = new(),
WebDavItem = new(),
CheckUpdateItem = new(),
Fragment4RayItem = null,
Inbound = [new InItem
{
Protocol = EInboundProtocol.socks.ToString(),
LocalPort = 10808,
UdpEnabled = true,
SniffingEnabled = true,
RouteOnly = false,
}],
GlobalHotkeys = [],
CoreTypeItem = [],
SimpleDNSItem = new(),
};
}
private static ProfileItem CreateProxyNode(string indexId, string address, int port)
{
return new ProfileItem
{
IndexId = indexId,
Remarks = indexId,
ConfigType = EConfigType.SOCKS,
CoreType = ECoreType.Xray,
Address = address,
Port = port,
};
}
private static ProfileItem CreateChainNode(string indexId, params ProfileItem[] nodes)
{
var chainNode = new ProfileItem
{
IndexId = indexId,
Remarks = indexId,
ConfigType = EConfigType.ProxyChain,
CoreType = ECoreType.Xray,
};
chainNode.SetProtocolExtra(new ProtocolExtraItem
{
ChildItems = string.Join(',', nodes.Select(node => node.IndexId)),
});
return chainNode;
}
private static List<JsonObject> GetOutbounds(string? json)
{
var root = JsonNode.Parse(json ?? throw new InvalidOperationException("Config JSON is missing"))?.AsObject()
?? throw new InvalidOperationException("Failed to parse config JSON");
return root["outbounds"]?.AsArray().Select(node => node!.AsObject()).ToList()
?? throw new InvalidOperationException("Config JSON does not contain outbounds");
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
</ItemGroup>
</Project>

View File

@@ -522,6 +522,23 @@ public class Utils
return false;
}
public static bool IsIpv4(string? ip)
{
if (ip.IsNullOrEmpty())
{
return false;
}
ip = ip.Trim();
if (!IPAddress.TryParse(ip, out var address))
{
return false;
}
return address.AddressFamily == AddressFamily.InterNetwork
&& ip.Count(c => c == '.') == 3;
}
public static bool IsIpAddress(string? ip)
{
if (ip.IsNullOrEmpty())

View File

@@ -143,7 +143,7 @@ public class Global
@"https://cachefly.cachefly.net/50mb.test",
@"https://speed.cloudflare.com/__down?bytes=10000000",
@"https://speed.cloudflare.com/__down?bytes=50000000",
@"https://speed.cloudflare.com/__down?bytes=100000000",
@"https://speed.cloudflare.com/__down?bytes=99999999",
];
public static readonly List<string> SpeedPingTestUrls =

View File

@@ -1,5 +1,3 @@
using System.ComponentModel.DataAnnotations;
namespace ServiceLib.Handler.Builder;
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
@@ -27,14 +25,14 @@ public record CoreConfigContextBuilderAllResult(
[.. MainResult.ValidatorResult.Warnings, .. PreSocksResult?.ValidatorResult.Warnings ?? []]);
/// <summary>
/// The main context with TunProtectSsPort/ProxyRelaySsPort and ProtectDomainList merged in
/// The main context with TunProtectSocksPort/ProxyRelaySocksPort and ProtectDomainList merged in
/// from the pre-socks result (if any). Pass this to the core runner.
/// </summary>
public CoreConfigContext ResolvedMainContext => PreSocksResult is not null
? MainResult.Context with
{
TunProtectSsPort = PreSocksResult.Context.TunProtectSsPort,
ProxyRelaySsPort = PreSocksResult.Context.ProxyRelaySsPort,
TunProtectSocksPort = PreSocksResult.Context.TunProtectSocksPort,
ProxyRelaySocksPort = PreSocksResult.Context.ProxyRelaySocksPort,
ProtectDomainList = [.. MainResult.Context.ProtectDomainList ?? [], .. PreSocksResult.Context.ProtectDomainList ?? []],
}
: MainResult.Context;
@@ -60,8 +58,8 @@ public class CoreConfigContextBuilder
IsTunEnabled = config.TunModeItem.EnableTun,
SimpleDnsItem = config.SimpleDNSItem,
ProtectDomainList = [],
TunProtectSsPort = 0,
ProxyRelaySsPort = 0,
TunProtectSocksPort = 0,
ProxyRelaySocksPort = 0,
RawDnsItem = await AppManager.Instance.GetDNSItem(coreType),
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
};
@@ -157,28 +155,23 @@ public class CoreConfigContextBuilder
return null;
}
var tunProtectSsPort = Utils.GetFreePort();
var proxyRelaySsPort = Utils.GetFreePort();
var tunProtectSocksPort = Utils.GetFreePort();
var proxyRelaySocksPort = Utils.GetFreePort();
var preItem = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.Shadowsocks,
ConfigType = EConfigType.SOCKS,
Address = Global.Loopback,
Port = proxyRelaySsPort,
Password = Global.None,
Port = proxyRelaySocksPort,
};
preItem.SetProtocolExtra(preItem.GetProtocolExtra() with
{
SsMethod = Global.None,
});
var preResult2 = await Build(nodeContext.AppConfig, preItem);
return preResult2 with
{
Context = preResult2.Context with
{
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preResult2.Context.ProtectDomainList ?? []],
TunProtectSsPort = tunProtectSsPort,
ProxyRelaySsPort = proxyRelaySsPort,
TunProtectSocksPort = tunProtectSocksPort,
ProxyRelaySocksPort = proxyRelaySocksPort,
}
};
}

View File

@@ -41,6 +41,7 @@ public static class ConfigHandler
Loglevel = "warning",
MuxEnabled = false,
};
config.CoreBasicItem.SendThrough = config.CoreBasicItem.SendThrough?.TrimEx();
if (config.Inbound == null)
{

View File

@@ -306,6 +306,7 @@ public sealed class AppManager
}
#pragma warning disable CS0618
public async Task MigrateProfileExtra()
{
await MigrateProfileExtraGroup();
@@ -500,6 +501,7 @@ public sealed class AppManager
//await ProfileGroupItemManager.Instance.ClearAll();
}
#pragma warning restore CS0618
#endregion SqliteHelper

View File

@@ -15,6 +15,8 @@ public class CoreBasicItem
public string DefUserAgent { get; set; }
public string? SendThrough { get; set; }
public bool EnableFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;

View File

@@ -20,6 +20,6 @@ public record CoreConfigContext
// -> tun inbound --(if routing proxy)--> relay outbound
// -> proxy core (relay inbound --> proxy outbound --(dialerProxy)--> protect outbound)
// -> protect inbound -> direct proxy outbound data -> internet
public int TunProtectSsPort { get; init; } = 0;
public int ProxyRelaySsPort { get; init; } = 0;
public int TunProtectSocksPort { get; init; } = 0;
public int ProxyRelaySocksPort { get; init; } = 0;
}

View File

@@ -105,6 +105,8 @@ public class Outbounds4Ray
public string protocol { get; set; }
public string? sendThrough { get; set; }
public string? targetStrategy { get; set; }
public Outboundsettings4Ray settings { get; set; }

View File

@@ -222,6 +222,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
/// </summary>
public static string FillCorrectSendThroughIPv4 {
get {
return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please enter the correct port format. 的本地化字符串。
/// </summary>
@@ -4149,6 +4158,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Local outbound address (SendThrough) 的本地化字符串。
/// </summary>
public static string TbSettingsSendThrough {
get {
return ResourceManager.GetString("TbSettingsSendThrough", resourceCulture);
}
}
/// <summary>
/// 查找类似 For multi-interface environments, enter the local machine&apos;s IPv4 address 的本地化字符串。
/// </summary>
public static string TbSettingsSendThroughTip {
get {
return ResourceManager.GetString("TbSettingsSendThroughTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Set Win10 UWP Loopback 的本地化字符串。
/// </summary>

View File

@@ -1698,4 +1698,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
</root>

View File

@@ -1695,4 +1695,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
</root>

View File

@@ -1698,4 +1698,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
</root>

View File

@@ -1320,6 +1320,15 @@
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Local outbound address (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TransportHeaderTypeTip5" xml:space="preserve">
<value>*xhttp mode</value>
</data>

View File

@@ -1698,4 +1698,7 @@
<data name="TbLegacyProtect" xml:space="preserve">
<value>Устаревшая защита TUN (Legacy Protect)</value>
</data>
</root>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
</root>

View File

@@ -1317,6 +1317,15 @@
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>本地出站地址 (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>用于多网口环境,请填写本机 IPv4 地址</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>请填写正确的 SendThrough IPv4 地址。</value>
</data>
<data name="TransportHeaderTypeTip5" xml:space="preserve">
<value>*XHTTP 模式</value>
</data>

View File

@@ -1695,4 +1695,7 @@
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
</data>
</root>

View File

@@ -57,21 +57,30 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ConvertGeo2Ruleset();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = ApplyFullConfigTemplate();
if (!context.AppConfig.TunModeItem.EnableLegacyProtect
&& context.TunProtectSsPort is > 0 and <= 65535)
&& context.TunProtectSocksPort is > 0 and <= 65535)
{
// Replace relay proxy outbound, avoid mux or other feature cause issue, and add a socks inbound for tun protect
var relayProxyIndex = _coreConfig.outbounds.FindIndex(o => o.tag == Global.ProxyTag);
_coreConfig.outbounds[relayProxyIndex] = new Outbound4Sbox()
{
type = Global.ProtocolTypes[EConfigType.SOCKS],
tag = Global.ProxyTag,
server = Global.Loopback,
server_port = context.ProxyRelaySocksPort,
};
var ssInbound = new
{
type = "shadowsocks",
tag = "tun-protect-ss",
type = "socks",
tag = "tun-protect-socks",
listen = Global.Loopback,
listen_port = context.TunProtectSsPort,
method = "none",
password = "none",
listen_port = context.TunProtectSocksPort,
};
var directRule = new Rule4Sbox()
{
@@ -214,6 +223,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
_coreConfig.route.rules.Add(rule);
}
ApplyOutboundSendThrough();
ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig);
return ret;
@@ -272,6 +282,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
});
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;

View File

@@ -58,4 +58,40 @@ public partial class CoreConfigSingboxService
return JsonUtils.Serialize(fullConfigTemplateNode);
}
private void ApplyOutboundSendThrough()
{
var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.inet4_bind_address = ShouldApplySendThrough(outbound, sendThrough) ? sendThrough : null;
}
}
private static bool ShouldApplySendThrough(Outbound4Sbox outbound, string? sendThrough)
{
if (sendThrough.IsNullOrEmpty())
{
return false;
}
if (outbound.type is "direct" or "block" or "dns" or "selector" or "urltest")
{
return false;
}
if (!outbound.detour.IsNullOrEmpty())
{
return false;
}
var outboundAddress = outbound.server ?? string.Empty;
if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return !IPAddress.TryParse(outboundAddress, out var address) || !IPAddress.IsLoopback(address);
}
}

View File

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

View File

@@ -17,8 +17,8 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
if (!context.AppConfig.TunModeItem.EnableLegacyProtect
&& context.IsTunEnabled
&& context.TunProtectSsPort is > 0 and <= 65535
&& context.ProxyRelaySsPort is > 0 and <= 65535)
&& context.TunProtectSocksPort is > 0 and <= 65535
&& context.ProxyRelaySocksPort is > 0 and <= 65535)
{
return GenerateClientProxyRelayConfig();
}
@@ -62,6 +62,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
GenDns();
GenStatistic();
ApplyOutboundSendThrough();
var finalRule = BuildFinalRule();
if (!string.IsNullOrEmpty(finalRule?.balancerTag))
@@ -195,6 +196,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
_coreConfig.routing.rules.Add(rule);
}
ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig);
@@ -255,6 +257,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
});
_coreConfig.routing.rules.Add(BuildFinalRule());
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
@@ -309,17 +312,16 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
var protectNode = new ProfileItem()
{
CoreType = ECoreType.Xray,
ConfigType = EConfigType.Shadowsocks,
ConfigType = EConfigType.SOCKS,
Address = Global.Loopback,
Port = context.TunProtectSsPort,
Password = Global.None,
Port = context.TunProtectSocksPort,
};
protectNode.SetProtocolExtra(protectNode.GetProtocolExtra() with
{
SsMethod = Global.None,
});
const string protectTag = "tun-protect-ss";
const string protectTag = "tun-protect-socks";
foreach (var outbound in _coreConfig.outbounds
.Where(o => o.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
{
@@ -368,7 +370,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
var hasBalancer = _coreConfig.routing.balancers is { Count: > 0 };
_coreConfig.routing.rules.Add(new()
{
inboundTag = ["proxy-relay-ss"],
inboundTag = ["proxy-relay-socks"],
outboundTag = hasBalancer ? null : Global.ProxyTag,
balancerTag = hasBalancer ? Global.ProxyTag + Global.BalancerTagSuffix : null,
type = "field"
@@ -376,19 +378,19 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
//_coreConfig.inbounds.Clear();
ApplyOutboundSendThrough();
var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!;
configNode["inbounds"]!.AsArray().Add(new
{
listen = Global.Loopback,
port = context.ProxyRelaySsPort,
protocol = "shadowsocks",
port = context.ProxyRelaySocksPort,
protocol = "socks",
settings = new
{
network = "tcp,udp",
method = Global.None,
password = Global.None,
auth = "noauth",
udp = true,
},
tag = "proxy-relay-ss",
tag = "proxy-relay-socks",
});
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");

View File

@@ -127,4 +127,43 @@ public partial class CoreConfigV2rayService
return JsonUtils.Serialize(fullConfigTemplateNode);
}
private void ApplyOutboundSendThrough()
{
var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.sendThrough = ShouldApplySendThrough(outbound, sendThrough) ? sendThrough : null;
}
}
private static bool ShouldApplySendThrough(Outbounds4Ray outbound, string? sendThrough)
{
if (sendThrough.IsNullOrEmpty())
{
return false;
}
if (outbound.protocol is "freedom" or "blackhole" or "dns" or "loopback")
{
return false;
}
if (outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == false)
{
return false;
}
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address
?? outbound.settings?.vnext?.FirstOrDefault()?.address
?? outbound.settings?.address?.ToString()
?? string.Empty;
if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return !IPAddress.TryParse(outboundAddress, out var address) || !IPAddress.IsLoopback(address);
}
}

View File

@@ -20,6 +20,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool defAllowInsecure { get; set; }
[Reactive] public string defFingerprint { get; set; }
[Reactive] public string defUserAgent { get; set; }
[Reactive] public string sendThrough { get; set; }
[Reactive] public string mux4SboxProtocol { get; set; }
[Reactive] public bool enableCacheFile4Sbox { get; set; }
[Reactive] public int? hyUpMbps { get; set; }
@@ -154,6 +155,7 @@ public class OptionSettingViewModel : MyReactiveObject
defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure;
defFingerprint = _config.CoreBasicItem.DefFingerprint;
defUserAgent = _config.CoreBasicItem.DefUserAgent;
sendThrough = _config.CoreBasicItem.SendThrough;
mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
hyUpMbps = _config.HysteriaItem.UpMbps;
@@ -297,6 +299,12 @@ public class OptionSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return;
}
var sendThroughValue = sendThrough?.TrimEx();
if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue))
{
NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4);
return;
}
var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics
|| DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed
|| EnableDragDropSort != _config.UiItem.EnableDragDropSort
@@ -336,6 +344,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.CoreBasicItem.DefAllowInsecure = defAllowInsecure;
_config.CoreBasicItem.DefFingerprint = defFingerprint;
_config.CoreBasicItem.DefUserAgent = defUserAgent;
_config.CoreBasicItem.SendThrough = sendThrough?.TrimEx();
_config.Mux4SboxItem.Protocol = mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = hyUpMbps ?? 0;

View File

@@ -0,0 +1,189 @@
using System.Runtime.InteropServices;
using SkiaSharp;
namespace v2rayN.Desktop.Common;
public partial class QRCodeAvaloniaUtils
{
public static byte[]? CaptureScreen()
{
if (!Utils.IsWindows())
{
return null;
}
try
{
return CaptureScreenWindows();
}
catch (Exception ex)
{
Logging.SaveLog("CaptureScreen", ex);
return null;
}
}
private static byte[]? CaptureScreenWindows()
{
var hdcScreen = IntPtr.Zero;
var hdcMemory = IntPtr.Zero;
var hBitmap = IntPtr.Zero;
try
{
var workArea = new RECT();
SystemParametersInfo(SPI_GETWORKAREA, 0, ref workArea, 0);
var left = workArea.Left;
var top = workArea.Top;
var width = workArea.Right - workArea.Left;
var height = workArea.Bottom - workArea.Top;
if (width <= 0 || height <= 0)
{
left = 0;
top = 0;
width = GetSystemMetrics(0);
height = GetSystemMetrics(1);
}
hdcScreen = GetDC(IntPtr.Zero);
if (hdcScreen == IntPtr.Zero)
{
return null;
}
hdcMemory = CreateCompatibleDC(hdcScreen);
hBitmap = CreateCompatibleBitmap(hdcScreen, width, height);
if (hBitmap == IntPtr.Zero)
{
return null;
}
SelectObject(hdcMemory, hBitmap);
const int SRCCOPY = 0x00CC0020;
BitBlt(hdcMemory, 0, 0, width, height, hdcScreen, left, top, SRCCOPY);
var bmi = new BITMAPINFO
{
biSize = Marshal.SizeOf(typeof(BITMAPINFO)),
biWidth = width,
biHeight = -height,
biPlanes = 1,
biBitCount = 32,
biCompression = 0
};
var imageSize = width * height * 4;
var imageData = new byte[imageSize];
var scanLines = GetDIBits(hdcScreen, hBitmap, 0, (uint)height, imageData, ref bmi, 0);
if (scanLines == 0)
{
return null;
}
using var bitmap = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
Marshal.Copy(imageData, 0, bitmap.GetPixels(), imageSize);
using var image = SKImage.FromBitmap(bitmap);
using var encoded = image.Encode(SKEncodedImageFormat.Png, 100);
return encoded.ToArray();
}
catch (Exception ex)
{
Logging.SaveLog("CaptureScreenWindows", ex);
return null;
}
finally
{
if (hBitmap != IntPtr.Zero)
{
DeleteObject(hBitmap);
}
if (hdcMemory != IntPtr.Zero)
{
DeleteDC(hdcMemory);
}
if (hdcScreen != IntPtr.Zero)
{
ReleaseDC(IntPtr.Zero, hdcScreen);
}
}
}
#region Win32 API
[LibraryImport("user32.dll")]
private static partial IntPtr GetDC(IntPtr hwnd);
[LibraryImport("user32.dll")]
private static partial int ReleaseDC(IntPtr hwnd, IntPtr hdc);
[LibraryImport("gdi32.dll")]
private static partial IntPtr CreateCompatibleDC(IntPtr hdc);
[LibraryImport("gdi32.dll")]
private static partial IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
[LibraryImport("gdi32.dll")]
private static partial IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
[LibraryImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight,
IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
[LibraryImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DeleteObject(IntPtr hObject);
[LibraryImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DeleteDC(IntPtr hdc);
[LibraryImport("gdi32.dll")]
private static partial int GetDIBits(IntPtr hdc, IntPtr hbmp, uint uStartScan, uint cScanLines,
byte[] lpvBits, ref BITMAPINFO lpbmi, uint uUsage);
[LibraryImport("user32.dll")]
private static partial int GetSystemMetrics(int nIndex);
[LibraryImport("user32.dll", EntryPoint = "SystemParametersInfoW", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SystemParametersInfo(int uiAction, int uiParam, ref RECT pvParam, int fWinIni);
private const int SPI_GETWORKAREA = 0x0030;
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
private struct BITMAPINFO
{
public int biSize;
public int biWidth;
public int biHeight;
public short biPlanes;
public short biBitCount;
public int biCompression;
public int biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public int biClrUsed;
public int biClrImportant;
}
#endregion Win32 API
}

View File

@@ -53,7 +53,7 @@ public partial class JsonEditor : UserControl
var keyColor = dark ? "#9CDCFE" : "#0451A5";
var strColor = dark ? "#CE9178" : "#A31515";
var numColor = dark ? "#B5CEA8" : "#098658";
var kwColor = dark ? "#569CD6" : "#0000FF";
var kwColor = dark ? "#569CD6" : "#0000FF";
var xshd = $"""
<?xml version="1.0"?>
<SyntaxDefinition name="JSON" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
@@ -91,6 +91,8 @@ public partial class JsonEditor : UserControl
}
private void Copy_Click(object? sender, RoutedEventArgs e) => Editor.Copy();
private void Paste_Click(object? sender, RoutedEventArgs e) => Editor.Paste();
private void SelectAll_Click(object? sender, RoutedEventArgs e) => Editor.SelectAll();
}

View File

@@ -162,8 +162,8 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
else
{
Title = $"{Utils.GetVersion()}";
menuAddServerViaScan.IsVisible = false;
}
menuAddServerViaScan.IsVisible = false;
if (_config.UiItem.AutoHideStartup && Utils.IsWindows())
{
@@ -336,17 +336,17 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
public async Task ScanScreenTaskAsync()
{
//ShowHideWindow(false);
ShowHideWindow(false);
NoticeManager.Instance.SendMessageAndEnqueue("Not yet implemented.(还未实现)");
await Task.CompletedTask;
//if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
//{
// //var bytes = QRCodeHelper.CaptureScreen(desktop);
// //await ViewModel?.ScanScreenResult(bytes);
//}
await Task.Delay(200);
//ShowHideWindow(true);
var bytes = QRCodeAvaloniaUtils.CaptureScreen();
if (bytes != null && ViewModel != null)
{
await ViewModel.ScanScreenResult(bytes);
}
ShowHideWindow(true);
}
private async Task ScanImageTaskAsync()

View File

@@ -325,6 +325,27 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="21"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
<TextBox
x:Name="txtsendThrough"
Grid.Row="21"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
Watermark="0.0.0.0" />
<TextBlock
Grid.Row="21"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSendThroughTip}"
TextWrapping="Wrap" />
</Grid>
</ScrollViewer>
</TabItem>

View File

@@ -76,6 +76,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
@@ -6,6 +6,7 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AssemblyName>v2rayN</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@@ -52,4 +53,4 @@
</None>
</ItemGroup>
</Project>
</Project>

View File

@@ -32,6 +32,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Action", "GitHub Act
..\.github\workflows\winget-publish.yml = ..\.github\workflows\winget-publish.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceLib.Tests", "ServiceLib.Tests\ServiceLib.Tests.csproj", "{E0B6C5C7-ED48-42EB-947A-877779E9F555}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -58,6 +60,10 @@ Global
{CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|Any CPU.Build.0 = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -16,6 +16,7 @@
</Folder>
<Project Path="AmazTool/AmazTool.csproj" />
<Project Path="GlobalHotKeys/src/GlobalHotKeys/GlobalHotKeys.csproj" />
<Project Path="ServiceLib.Tests/ServiceLib.Tests.csproj" />
<Project Path="ServiceLib/ServiceLib.csproj" />
<Project Path="v2rayN.Desktop/v2rayN.Desktop.csproj" />
<Project Path="v2rayN/v2rayN.csproj" />

View File

@@ -5,7 +5,7 @@ using Microsoft.Win32;
namespace v2rayN.Common;
internal static class WindowsUtils
internal static partial class WindowsUtils
{
private static readonly string _tag = "WindowsUtils";
@@ -39,8 +39,8 @@ internal static class WindowsUtils
}
}
[DllImport("dwmapi.dll")]
public static extern int DwmSetWindowAttribute(nint hwnd, DWMWINDOWATTRIBUTE attribute, ref int attributeValue, uint attributeSize);
[LibraryImport("dwmapi.dll")]
public static partial int DwmSetWindowAttribute(nint hwnd, DWMWINDOWATTRIBUTE attribute, ref int attributeValue, uint attributeSize);
public static ImageSource IconToImageSource(Icon icon)
{

View File

@@ -1,6 +1,6 @@
namespace v2rayN.Manager;
public sealed class HotkeyManager
public sealed partial class HotkeyManager
{
private static readonly Lazy<HotkeyManager> _instance = new(() => new());
public static HotkeyManager Instance = _instance.Value;
@@ -165,11 +165,13 @@ public sealed class HotkeyManager
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool RegisterHotKey(nint hWnd, int id, int fsModifiers, int vlc);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool RegisterHotKey(nint hWnd, int id, int fsModifiers, int vlc);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnregisterHotKey(nint hWnd, int id);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool UnregisterHotKey(nint hWnd, int id);
[Flags]
private enum KeyModifiers

View File

@@ -894,7 +894,6 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
</Grid>
<Separator

View File

@@ -391,6 +391,30 @@
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="21"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
<TextBox
x:Name="txtsendThrough"
Grid.Row="21"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefTextBox}"
materialDesign:HintAssist.Hint="0.0.0.0" />
<TextBlock
Grid.Row="21"
Grid.Column="2"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsSendThroughTip}"
TextWrapping="Wrap" />
</Grid>
</ScrollViewer>
</TabItem>

View File

@@ -73,6 +73,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);

View File

@@ -8,6 +8,7 @@
<ApplicationIcon>Resources\v2rayN.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@@ -35,4 +36,4 @@
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
</ItemGroup>
</Project>
</Project>