Compare commits

...

6 commits

Author SHA1 Message Date
DHR60
1d54f88e79
Merge 18ccabd193 into c2c13ad318 2025-09-21 13:20:40 +08:00
2dust
c2c13ad318 Create v2rayN.slnx
https://github.com/2dust/v2rayN/pull/7969
2025-09-21 12:12:24 +08:00
2dust
3a21596d95 Fix node domain resolving in TUN mode
https://github.com/2dust/v2rayN/pull/7989
2025-09-21 12:05:06 +08:00
DHR60
18ccabd193 Code clean 2025-09-17 21:25:08 +08:00
DHR60
942335db61 Refactor 2025-09-17 21:11:05 +08:00
DHR60
12b46e7c43 Add CertSha256 support 2025-09-17 21:04:58 +08:00
22 changed files with 200 additions and 26 deletions

View file

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Principal; using System.Security.Principal;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
@ -357,6 +358,14 @@ public class Utils
return userHostsMap; return userHostsMap;
} }
public static List<string> ParseCertSha256ToList(string certSha256Content)
{
return String2List(certSha256Content)
.Select(s => s.Replace(":", "").Replace(" ", ""))
.Where(s => s.Length == 64 && Regex.IsMatch(s, @"\A\b[0-9a-fA-F]+\b\Z"))
.ToList();
}
#endregion #endregion
#region #region

View file

@ -598,6 +598,7 @@ public class Global
{ "cloudflare-dns.com", new List<string> { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } }, { "cloudflare-dns.com", new List<string> { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } },
{ "dns.cloudflare.com", new List<string> { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } }, { "dns.cloudflare.com", new List<string> { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } },
{ "dot.pub", new List<string> { "1.12.12.12", "120.53.53.53" } }, { "dot.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
{ "doh.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
{ "dns.quad9.net", new List<string> { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } }, { "dns.quad9.net", new List<string> { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } },
{ "dns.yandex.net", new List<string> { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } }, { "dns.yandex.net", new List<string> { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } },
{ "dns.sb", new List<string> { "185.222.222.222", "2a09::" } }, { "dns.sb", new List<string> { "185.222.222.222", "2a09::" } },

View file

@ -253,6 +253,7 @@ public static class ConfigHandler
item.ShortId = profileItem.ShortId; item.ShortId = profileItem.ShortId;
item.SpiderX = profileItem.SpiderX; item.SpiderX = profileItem.SpiderX;
item.Mldsa65Verify = profileItem.Mldsa65Verify; item.Mldsa65Verify = profileItem.Mldsa65Verify;
item.CertSha256 = profileItem.CertSha256;
item.Extra = profileItem.Extra; item.Extra = profileItem.Extra;
item.MuxEnabled = profileItem.MuxEnabled; item.MuxEnabled = profileItem.MuxEnabled;
} }
@ -1214,11 +1215,11 @@ public static class ConfigHandler
CoreType = ECoreType.sing_box, CoreType = ECoreType.sing_box,
ConfigType = EConfigType.SOCKS, ConfigType = EConfigType.SOCKS,
Address = Global.Loopback, Address = Global.Loopback,
Sni = node.Address, //Tun2SocksAddress SpiderX = node.Address, // Tun2SocksAddress
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
}; };
} }
else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)) else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)
{ {
var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
itemSocks = new ProfileItem() itemSocks = new ProfileItem()

View file

@ -94,6 +94,7 @@ public class ProfileItem : ReactiveObject
public string ShortId { get; set; } public string ShortId { get; set; }
public string SpiderX { get; set; } public string SpiderX { get; set; }
public string Mldsa65Verify { get; set; } public string Mldsa65Verify { get; set; }
public string CertSha256 { get; set; }
public string Extra { get; set; } public string Extra { get; set; }
public bool? MuxEnabled { get; set; } public bool? MuxEnabled { get; set; }
} }

View file

@ -182,6 +182,7 @@ public class Tls4Sbox
public bool? fragment { get; set; } public bool? fragment { get; set; }
public string? fragment_fallback_delay { get; set; } public string? fragment_fallback_delay { get; set; }
public bool? record_fragment { get; set; } public bool? record_fragment { get; set; }
public List<string>? certificate_sha256 { get; set; }
} }
public class Multiplex4Sbox public class Multiplex4Sbox

View file

@ -355,6 +355,7 @@ public class TlsSettings4Ray
public string? shortId { get; set; } public string? shortId { get; set; }
public string? spiderX { get; set; } public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; } public string? mldsa65Verify { get; set; }
public List<string>? pinnedPeerCertificateSha256 { get; set; }
} }
public class TcpSettings4Ray public class TcpSettings4Ray

View file

@ -2364,6 +2364,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Server Certificate Fingerprint (SHA-256) 的本地化字符串。
/// </summary>
public static string TbCertSha256 {
get {
return ResourceManager.GetString("TbCertSha256", resourceCulture);
}
}
/// <summary>
/// 查找类似 Hex SHA-256, comma-separated for certificate chain 的本地化字符串。
/// </summary>
public static string TbCertSha256Tips {
get {
return ResourceManager.GetString("TbCertSha256Tips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Clear system proxy 的本地化字符串。 /// 查找类似 Clear system proxy 的本地化字符串。
/// </summary> /// </summary>

View file

@ -1515,4 +1515,10 @@
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data> </data>
<data name="TbCertSha256" xml:space="preserve">
<value>Server Certificate Fingerprint (SHA-256)</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Hex SHA-256, comma-separated for certificate chain</value>
</data>
</root> </root>

View file

@ -1515,4 +1515,10 @@
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data> </data>
<data name="TbCertSha256" xml:space="preserve">
<value>Server Certificate Fingerprint (SHA-256)</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Hex SHA-256, comma-separated for certificate chain</value>
</data>
</root> </root>

View file

@ -1515,4 +1515,10 @@
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data> </data>
<data name="TbCertSha256" xml:space="preserve">
<value>Server Certificate Fingerprint (SHA-256)</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Hex SHA-256, comma-separated for certificate chain</value>
</data>
</root> </root>

View file

@ -1515,4 +1515,10 @@
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data> </data>
<data name="TbCertSha256" xml:space="preserve">
<value>Server Certificate Fingerprint (SHA-256)</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Hex SHA-256, comma-separated for certificate chain</value>
</data>
</root> </root>

View file

@ -1512,4 +1512,10 @@
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value> <value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value>
</data> </data>
<data name="TbCertSha256" xml:space="preserve">
<value>服务器证书指纹 (SHA-256)</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>十六进制 SHA-256证书链用逗号分隔</value>
</data>
</root> </root>

View file

@ -1512,4 +1512,10 @@
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data> </data>
<data name="TbCertSha256" xml:space="preserve">
<value>Server Certificate Fingerprint (SHA-256)</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Hex SHA-256, comma-separated for certificate chain</value>
</data>
</root> </root>

View file

@ -43,16 +43,7 @@ public partial class CoreConfigSingboxService
}); });
} }
// Tun2SocksAddress await GenOutboundDnsRule(node, singboxConfig, Global.SingboxOutboundResolverTag);
if (node != null && Utils.IsDomain(node.Address))
{
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = Global.SingboxOutboundResolverTag,
domain = [node.Address],
});
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -346,16 +337,7 @@ public partial class CoreConfigSingboxService
await GenDnsDomainsLegacyCompatible(singboxConfig, item); await GenDnsDomainsLegacyCompatible(singboxConfig, item);
} }
// Tun2SocksAddress await GenOutboundDnsRule(node, singboxConfig, Global.SingboxFinalResolverTag);
if (node != null && Utils.IsDomain(node.Address))
{
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = Global.SingboxFinalResolverTag,
domain = [node.Address],
});
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -425,6 +407,37 @@ public partial class CoreConfigSingboxService
return await Task.FromResult(0); return await Task.FromResult(0);
} }
private async Task<int> GenOutboundDnsRule(ProfileItem? node, SingboxConfig singboxConfig, string? server)
{
if (node == null)
{
return 0;
}
var domain = string.Empty;
if (Utils.IsDomain(node.Address)) // normal outbound
{
domain = node.Address;
}
else if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty() && Utils.IsDomain(node.SpiderX)) // Tun2SocksAddress
{
domain = node.SpiderX;
}
if (domain.IsNullOrEmpty())
{
return 0;
}
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = server,
domain = [domain],
});
return await Task.FromResult(0);
}
private static Server4Sbox? ParseDnsAddress(string address) private static Server4Sbox? ParseDnsAddress(string address)
{ {
var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim(); var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim();

View file

@ -251,6 +251,20 @@ public partial class CoreConfigSingboxService
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint
}; };
} }
if (node.CertSha256.IsNotEmpty())
{
// hex to raw to base64
var certSha256List = Utils.ParseCertSha256ToList(node.CertSha256)
.Select(s => Convert.ToBase64String(
Enumerable.Range(0, 32)
.Select(i => Convert.ToByte(s.Substring(i * 2, 2), 16))
.ToArray()))
.ToList();
if (certSha256List.Count > 0)
{
tls.certificate_sha256 = certSha256List;
}
}
if (node.StreamSecurity == Global.StreamSecurityReality) if (node.StreamSecurity == Global.StreamSecurityReality)
{ {
tls.reality = new Reality4Sbox() tls.reality = new Reality4Sbox()

View file

@ -277,6 +277,14 @@ public partial class CoreConfigV2rayService
{ {
tlsSettings.serverName = Utils.String2List(host)?.First(); tlsSettings.serverName = Utils.String2List(host)?.First();
} }
if (node.CertSha256.IsNotEmpty())
{
var certSha256List = Utils.ParseCertSha256ToList(node.CertSha256);
if (certSha256List.Count > 0)
{
tlsSettings.pinnedPeerCertificateSha256 = certSha256List;
}
}
streamSettings.tlsSettings = tlsSettings; streamSettings.tlsSettings = tlsSettings;
} }

View file

@ -709,9 +709,9 @@
<Grid <Grid
x:Name="gridTlsMore" x:Name="gridTlsMore"
Grid.Row="7" Grid.Row="7"
ColumnDefinitions="180,Auto" ColumnDefinitions="180,Auto,Auto"
IsVisible="False" IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
@ -765,6 +765,26 @@
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertSha256}" />
<TextBox
x:Name="txtCertSha256"
Grid.Row="5"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="5"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertSha256Tips}" />
</Grid> </Grid>
<Grid <Grid
x:Name="gridRealityMore" x:Name="gridRealityMore"

View file

@ -189,6 +189,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.CertSha256, v => v.txtCertSha256.Text).DisposeWith(disposables);
//reality //reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables);

24
v2rayN/v2rayN.slnx Normal file
View file

@ -0,0 +1,24 @@
<Solution>
<Folder Name="/GitHub Action/">
<File Path="../.github/workflows/build-all.yml" />
<File Path="../.github/workflows/build-linux.yml" />
<File Path="../.github/workflows/build-osx.yml" />
<File Path="../.github/workflows/build-windows-desktop.yml" />
<File Path="../.github/workflows/build-windows.yml" />
<File Path="../.github/workflows/winget-publish.yml" />
<File Path="../package-appimage.sh" />
<File Path="../package-debian.sh" />
<File Path="../package-osx.sh" />
<File Path="../package-release-zip.sh" />
<File Path="../pkg2appimage.yml" />
</Folder>
<Folder Name="/Solution Files/">
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
</Folder>
<Project Path="AmazTool/AmazTool.csproj" />
<Project Path="GlobalHotKeys/src/GlobalHotKeys/GlobalHotKeys.csproj" />
<Project Path="ServiceLib/ServiceLib.csproj" />
<Project Path="v2rayN.Desktop/v2rayN.Desktop.csproj" />
<Project Path="v2rayN/v2rayN.csproj" />
</Solution>

View file

@ -32,8 +32,8 @@ public class QRCodeUtils
{ {
GetDpi(window, out var dpiX, out var dpiY); GetDpi(window, out var dpiX, out var dpiY);
var left = (int)(SystemParameters.WorkArea.Left); var left = (int)SystemParameters.WorkArea.Left;
var top = (int)(SystemParameters.WorkArea.Top); var top = (int)SystemParameters.WorkArea.Top;
var width = (int)(SystemParameters.WorkArea.Width / dpiX); var width = (int)(SystemParameters.WorkArea.Width / dpiX);
var height = (int)(SystemParameters.WorkArea.Height / dpiY); var height = (int)(SystemParameters.WorkArea.Height / dpiY);

View file

@ -928,10 +928,12 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="180" /> <ColumnDefinition Width="180" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock <TextBlock
@ -995,6 +997,29 @@
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Style="{StaticResource DefComboBox}" /> Style="{StaticResource DefComboBox}" />
<TextBlock
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCertSha256}" />
<TextBox
x:Name="txtCertSha256"
Grid.Row="5"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="5"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCertSha256Tips}" />
</Grid> </Grid>
<Grid <Grid
x:Name="gridRealityMore" x:Name="gridRealityMore"

View file

@ -183,6 +183,7 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.CertSha256, v => v.txtCertSha256.Text).DisposeWith(disposables);
//reality //reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.Text).DisposeWith(disposables);