Compare commits

...

6 commits

Author SHA1 Message Date
DHR60
9f9b90cb97
Add hysteria2 uri cert sha pinning support (#8657)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2026-01-17 16:22:26 +08:00
DHR60
c42dcd2876
Add process matching rules support (#8643)
* Add process matching rules support

* Fix
2026-01-17 16:08:36 +08:00
2dust
2fefafdd37 Add support for CoreType7 (Hysteria2) in option settings 2026-01-17 16:06:29 +08:00
DHR60
2c9a90c878
Add xray hysteria2 outbound support (#8630) 2026-01-17 15:49:44 +08:00
DHR60
4e5f1838a2
Add Cert SHA-256 pinning support (#8613) 2026-01-17 15:42:40 +08:00
2dust
a45a1dc982 Ensure WebDAV base URL ends with trailing slash 2026-01-17 15:08:08 +08:00
33 changed files with 411 additions and 45 deletions

View file

@ -300,6 +300,7 @@ public class Global
EConfigType.VLESS, EConfigType.VLESS,
EConfigType.Shadowsocks, EConfigType.Shadowsocks,
EConfigType.Trojan, EConfigType.Trojan,
EConfigType.Hysteria2,
EConfigType.WireGuard, EConfigType.WireGuard,
EConfigType.SOCKS, EConfigType.SOCKS,
EConfigType.HTTP, EConfigType.HTTP,

View file

@ -253,6 +253,7 @@ public static class ConfigHandler
item.Extra = profileItem.Extra; item.Extra = profileItem.Extra;
item.MuxEnabled = profileItem.MuxEnabled; item.MuxEnabled = profileItem.MuxEnabled;
item.Cert = profileItem.Cert; item.Cert = profileItem.Cert;
item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList; item.EchConfigList = profileItem.EchConfigList;
item.EchForceQuery = profileItem.EchForceQuery; item.EchForceQuery = profileItem.EchForceQuery;
} }
@ -702,7 +703,7 @@ public static class ConfigHandler
public static async Task<int> AddHysteria2Server(Config config, ProfileItem profileItem, bool toFile = true) public static async Task<int> AddHysteria2Server(Config config, ProfileItem profileItem, bool toFile = true)
{ {
profileItem.ConfigType = EConfigType.Hysteria2; profileItem.ConfigType = EConfigType.Hysteria2;
profileItem.CoreType = ECoreType.sing_box; //profileItem.CoreType = ECoreType.sing_box;
profileItem.Address = profileItem.Address.TrimEx(); profileItem.Address = profileItem.Address.TrimEx();
profileItem.Id = profileItem.Id.TrimEx(); profileItem.Id = profileItem.Id.TrimEx();

View file

@ -74,6 +74,10 @@ public class BaseFmt
{ {
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList)); dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
} }
if (item.CertSha.IsNotEmpty())
{
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
}
dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp)); dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp));
@ -214,6 +218,7 @@ public class BaseFmt
item.SpiderX = GetQueryDecoded(query, "spx"); item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.EchConfigList = GetQueryDecoded(query, "ech"); item.EchConfigList = GetQueryDecoded(query, "ech");
item.CertSha = GetQueryDecoded(query, "pcs");
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1")) if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{ {

View file

@ -25,6 +25,10 @@ public class Hysteria2Fmt : BaseFmt
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
item.Path = GetQueryDecoded(query, "obfs-password"); item.Path = GetQueryDecoded(query, "obfs-password");
item.Ports = GetQueryDecoded(query, "mport"); item.Ports = GetQueryDecoded(query, "mport");
if (item.CertSha.IsNullOrEmpty())
{
item.CertSha = GetQueryDecoded(query, "pinSHA256");
}
return item; return item;
} }
@ -55,6 +59,16 @@ public class Hysteria2Fmt : BaseFmt
{ {
dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-'))); dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-')));
} }
if (!item.CertSha.IsNullOrEmpty())
{
var sha = item.CertSha;
var idx = sha.IndexOf('~');
if (idx > 0)
{
sha = sha[..idx];
}
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
}
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Id, dicQuery, remark); return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Id, dicQuery, remark);
} }

View file

@ -168,7 +168,9 @@ public class ActionPrecheckManager
if (item.StreamSecurity == Global.StreamSecurity) if (item.StreamSecurity == Global.StreamSecurity)
{ {
// check certificate validity // check certificate validity
if ((!item.Cert.IsNullOrEmpty()) && (CertPemManager.ParsePemChain(item.Cert).Count == 0)) if (!item.Cert.IsNullOrEmpty()
&& (CertPemManager.ParsePemChain(item.Cert).Count == 0)
&& !item.CertSha.IsNullOrEmpty())
{ {
errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate")); errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate"));
} }

View file

@ -416,4 +416,22 @@ public class CertPemManager
return string.Concat(pemList); return string.Concat(pemList);
} }
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
{
try
{
var cert = X509Certificate2.CreateFromPem(pemCert);
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
if (includeColon)
{
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
}
return thumbprint;
}
catch
{
return string.Empty;
}
}
} }

View file

@ -43,9 +43,12 @@ public sealed class WebDavManager
_webDir = _config.WebDavItem.DirName.TrimEx(); _webDir = _config.WebDavItem.DirName.TrimEx();
} }
// Ensure BaseAddress URL ends with a trailing slash
var baseUrl = _config.WebDavItem.Url.Trim().TrimEnd('/') + "/";
var clientParams = new WebDavClientParams var clientParams = new WebDavClientParams
{ {
BaseAddress = new Uri(_config.WebDavItem.Url), BaseAddress = new Uri(baseUrl),
Credentials = new NetworkCredential(_config.WebDavItem.UserName, _config.WebDavItem.Password) Credentials = new NetworkCredential(_config.WebDavItem.UserName, _config.WebDavItem.Password)
}; };
_client = new WebDavClient(clientParams); _client = new WebDavClient(clientParams);

View file

@ -161,6 +161,7 @@ public class ProfileItem : ReactiveObject
public string Extra { get; set; } public string Extra { get; set; }
public bool? MuxEnabled { get; set; } public bool? MuxEnabled { get; set; }
public string Cert { get; set; } public string Cert { get; set; }
public string CertSha { get; set; }
public string EchConfigList { get; set; } public string EchConfigList { get; set; }
public string EchForceQuery { get; set; } public string EchForceQuery { get; set; }
} }

View file

@ -68,6 +68,7 @@ public class Rule4Sbox
public List<string>? ip_cidr { get; set; } public List<string>? ip_cidr { get; set; }
public List<string>? source_ip_cidr { get; set; } public List<string>? source_ip_cidr { get; set; }
public List<string>? process_name { get; set; } public List<string>? process_name { get; set; }
public List<string>? process_path { get; set; }
public List<string>? rule_set { get; set; } public List<string>? rule_set { get; set; }
public List<Rule4Sbox>? rules { get; set; } public List<Rule4Sbox>? rules { get; set; }
public string? action { get; set; } public string? action { get; set; }

View file

@ -128,7 +128,8 @@ public class Outboundsettings4Ray
public string? secretKey { get; set; } public string? secretKey { get; set; }
public List<string>? address { get; set; } public Object? address { get; set; }
public int? port { get; set; }
public List<WireguardPeer4Ray>? peers { get; set; } public List<WireguardPeer4Ray>? peers { get; set; }
@ -139,6 +140,8 @@ public class Outboundsettings4Ray
public List<int>? reserved { get; set; } public List<int>? reserved { get; set; }
public int? workers { get; set; } public int? workers { get; set; }
public int? version { get; set; }
} }
public class WireguardPeer4Ray public class WireguardPeer4Ray
@ -256,6 +259,8 @@ public class RulesItem4Ray
public List<string>? domain { get; set; } public List<string>? domain { get; set; }
public List<string>? protocol { get; set; } public List<string>? protocol { get; set; }
public List<string>? process { get; set; }
} }
public class BalancersItem4Ray public class BalancersItem4Ray
@ -336,6 +341,10 @@ public class StreamSettings4Ray
public GrpcSettings4Ray? grpcSettings { get; set; } public GrpcSettings4Ray? grpcSettings { get; set; }
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
public List<UdpMasks4Ray>? udpmasks { get; set; }
public Sockopt4Ray? sockopt { get; set; } public Sockopt4Ray? sockopt { get; set; }
} }
@ -355,6 +364,7 @@ public class TlsSettings4Ray
public string? spiderX { get; set; } public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; } public string? mldsa65Verify { get; set; }
public List<CertificateSettings4Ray>? certificates { get; set; } public List<CertificateSettings4Ray>? certificates { get; set; }
public string? pinnedPeerCertSha256 { get; set; }
public bool? disableSystemRoot { get; set; } public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; } public string? echConfigList { get; set; }
public string? echForceQuery { get; set; } public string? echForceQuery { get; set; }
@ -459,6 +469,32 @@ public class GrpcSettings4Ray
public int? initial_windows_size { get; set; } public int? initial_windows_size { get; set; }
} }
public class HysteriaSettings4Ray
{
public int version { get; set; }
public string? auth { get; set; }
public string? up { get; set; }
public string? down { get; set; }
public HysteriaUdpHop4Ray? udphop { get; set; }
}
public class HysteriaUdpHop4Ray
{
public string? ports { get; set; }
public int? interval { get; set; }
}
public class UdpMasks4Ray
{
public string type { get; set; }
public UdpMasksSettings4Ray? settings { get; set; }
}
public class UdpMasksSettings4Ray
{
public string? password { get; set; }
}
public class AccountsItem4Ray public class AccountsItem4Ray
{ {
public string user { get; set; } public string user { get; set; }

View file

@ -2617,7 +2617,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Server Certificate (PEM format, optional) /// 查找类似 Pinned certificate (fill in either one)
///When specified, the certificate will be pinned, and &quot;Allow Insecure&quot; will be disabled. ///When specified, the certificate will be pinned, and &quot;Allow Insecure&quot; will be disabled.
/// ///
///The &quot;Get Certificate&quot; action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. 的本地化字符串。 ///The &quot;Get Certificate&quot; action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. 的本地化字符串。
@ -2628,6 +2628,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Certificate fingerprint (SHA-256) 的本地化字符串。
/// </summary>
public static string TbCertSha256Tips {
get {
return ResourceManager.GetString("TbCertSha256Tips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Clear system proxy 的本地化字符串。 /// 查找类似 Clear system proxy 的本地化字符串。
/// </summary> /// </summary>
@ -2889,6 +2898,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Full certificate (chain), PEM format 的本地化字符串。
/// </summary>
public static string TbFullCertTips {
get {
return ResourceManager.GetString("TbFullCertTips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core&apos;s basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. 的本地化字符串。 /// 查找类似 This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core&apos;s basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. 的本地化字符串。
/// </summary> /// </summary>
@ -3268,7 +3286,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Full process name (Tun mode) 的本地化字符串。 /// 查找类似 Process (Tun mode) 的本地化字符串。
/// </summary> /// </summary>
public static string TbRoutingRuleProcess { public static string TbRoutingRuleProcess {
get { get {

View file

@ -1027,7 +1027,7 @@
<value>پروتکل sing-box Mux</value> <value>پروتکل sing-box Mux</value>
</data> </data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>نام کامل فرانید (حالت Tun)</value> <value>Process (Tun mode)</value>
</data> </data>
<data name="TbRoutingRuleIP" xml:space="preserve"> <data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP or IP CIDR</value> <value>IP or IP CIDR</value>
@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEchForceQuery" xml:space="preserve"> <data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root> </root>

View file

@ -1024,7 +1024,7 @@
<value>Protocole de multiplexage Mux (sing-box)</value> <value>Protocole de multiplexage Mux (sing-box)</value>
</data> </data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Nom complet du processus (mode Tun)</value> <value>Process (Tun mode)</value>
</data> </data>
<data name="TbRoutingRuleIP" xml:space="preserve"> <data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP ou IP CIDR</value> <value>IP ou IP CIDR</value>
@ -1606,10 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Certificat serveur (format PEM, facultatif) <value>Pinned certificate (fill in either one)
Si le certificat est défini, il est fixé et loption « Ignorer la vérification » est désactivée. When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
Si un certificat auto-signé est utilisé ou si le système contient une CA non fiable ou malveillante, laction « Obtenir le certificat » peut échouer.</value> The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Obtenir le certificat</value> <value>Obtenir le certificat</value>
@ -1644,4 +1644,10 @@ Si un certificat auto-signé est utilisé ou si le système contient une CA non
<data name="TbEchForceQuery" xml:space="preserve"> <data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root> </root>

View file

@ -1027,7 +1027,7 @@
<value>sing-box Mux protokoll</value> <value>sing-box Mux protokoll</value>
</data> </data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Teljes folyamatnév (Tun mód)</value> <value>Process (Tun mode)</value>
</data> </data>
<data name="TbRoutingRuleIP" xml:space="preserve"> <data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP vagy IP CIDR</value> <value>IP vagy IP CIDR</value>
@ -1609,7 +1609,7 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional) <value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value> The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEchForceQuery" xml:space="preserve"> <data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root> </root>

View file

@ -1027,7 +1027,7 @@
<value>sing-box Mux Protocol</value> <value>sing-box Mux Protocol</value>
</data> </data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Full process name (Tun mode)</value> <value>Process (Tun mode)</value>
</data> </data>
<data name="TbRoutingRuleIP" xml:space="preserve"> <data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP or IP CIDR</value> <value>IP or IP CIDR</value>
@ -1609,7 +1609,7 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional) <value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value> The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEchForceQuery" xml:space="preserve"> <data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root> </root>

View file

@ -1027,7 +1027,7 @@
<value>Протокол Mux для sing-box</value> <value>Протокол Mux для sing-box</value>
</data> </data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Полное имя процесса (режим TUN)</value> <value>Process (Tun mode)</value>
</data> </data>
<data name="TbRoutingRuleIP" xml:space="preserve"> <data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP-адрес или сеть CIDR</value> <value>IP-адрес или сеть CIDR</value>
@ -1609,7 +1609,7 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional) <value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value> The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbEchForceQuery" xml:space="preserve"> <data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root> </root>

View file

@ -1024,7 +1024,7 @@
<value>sing-box Mux 多路复用协议</value> <value>sing-box Mux 多路复用协议</value>
</data> </data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>进程名全称 (Tun 模式)</value> <value>进程 (Tun 模式)</value>
</data> </data>
<data name="TbRoutingRuleIP" xml:space="preserve"> <data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP 或 IP CIDR</value> <value>IP 或 IP CIDR</value>
@ -1606,10 +1606,10 @@
<value>固定证书</value> <value>固定证书</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>服务器证书PEM 格式,可选 <value>固定证书(二选一填写即可
当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。 当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。
“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。</value> “获取证书”操作可能失败,原因包括使用了自签名证书,或系统中存在不受信任甚至恶意的 CA。</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>获取证书</value> <value>获取证书</value>
@ -1644,4 +1644,10 @@
<data name="TbEchForceQuery" xml:space="preserve"> <data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve">
<value>完整证书PEM 格式</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>证书指纹SHA-256</value>
</data>
</root> </root>

View file

@ -1024,7 +1024,7 @@
<value>sing-box Mux 多路復用協定</value> <value>sing-box Mux 多路復用協定</value>
</data> </data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>行程名全稱 (Tun 模式)</value> <value>行程 (Tun 模式)</value>
</data> </data>
<data name="TbRoutingRuleIP" xml:space="preserve"> <data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP 或 IP CIDR</value> <value>IP 或 IP CIDR</value>
@ -1606,7 +1606,7 @@
<value>憑證綁定</value> <value>憑證綁定</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>伺服器憑證PEM 格式,可選 <value>固定憑證(二選一填寫即可
若已指定,憑證將會被綁定,並且「跳過憑證驗證」將被停用。 若已指定,憑證將會被綁定,並且「跳過憑證驗證」將被停用。
若使用自簽憑證,或系統中存在不受信任或惡意的 CA「取得憑證」動作可能會失敗。</value> 若使用自簽憑證,或系統中存在不受信任或惡意的 CA「取得憑證」動作可能會失敗。</value>
@ -1644,4 +1644,10 @@
<data name="TbEchForceQuery" xml:space="preserve"> <data name="TbEchForceQuery" xml:space="preserve">
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
</data>
</root> </root>

View file

@ -280,9 +280,49 @@ public partial class CoreConfigSingboxService
if (_config.TunModeItem.EnableTun && item.Process?.Count > 0) if (_config.TunModeItem.EnableTun && item.Process?.Count > 0)
{ {
rule3.process_name = item.Process; var ruleProcName = JsonUtils.DeepCopy(rule3);
rules.Add(rule3); var ruleProcPath = JsonUtils.DeepCopy(rule3);
hasDomainIp = true; foreach (var process in item.Process)
{
// sing-box doesn't support this, fall back to process name match
if (process is "self/" or "xray/")
{
ruleProcName.process_name.Add(Utils.GetExeName("sing-box"));
continue;
}
if (process.Contains('/') || process.Contains('\\'))
{
var procPath = process;
if (Utils.IsWindows())
{
procPath = procPath.Replace('/', '\\');
}
ruleProcPath.process_path.Add(procPath);
continue;
}
// sing-box strictly matches the exe suffix on Windows
var procName = process;
if (Utils.IsWindows() && !procName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
procName += ".exe";
}
ruleProcName.process_name.Add(procName);
}
if (ruleProcName.process_name.Count > 0)
{
rules.Add(ruleProcName);
hasDomainIp = true;
}
if (ruleProcPath.process_path.Count > 0)
{
rules.Add(ruleProcPath);
hasDomainIp = true;
}
} }
if (!hasDomainIp if (!hasDomainIp

View file

@ -178,6 +178,18 @@ public partial class CoreConfigV2rayService
outbound.settings.vnext = null; outbound.settings.vnext = null;
break; break;
} }
case EConfigType.Hysteria2:
{
outbound.settings = new()
{
version = 2,
address = node.Address,
port = node.Port,
};
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
case EConfigType.WireGuard: case EConfigType.WireGuard:
{ {
var address = node.Address; var address = node.Address;
@ -206,6 +218,10 @@ public partial class CoreConfigV2rayService
} }
outbound.protocol = Global.ProtocolTypes[node.ConfigType]; outbound.protocol = Global.ProtocolTypes[node.ConfigType];
if (node.ConfigType == EConfigType.Hysteria2)
{
outbound.protocol = "hysteria";
}
await GenBoundStreamSettings(node, outbound); await GenBoundStreamSettings(node, outbound);
} }
catch (Exception ex) catch (Exception ex)
@ -246,7 +262,12 @@ public partial class CoreConfigV2rayService
try try
{ {
var streamSettings = outbound.streamSettings; var streamSettings = outbound.streamSettings;
streamSettings.network = node.GetNetwork(); var network = node.GetNetwork();
if (node.ConfigType == EConfigType.Hysteria2)
{
network = "hysteria";
}
streamSettings.network = network;
var host = node.RequestHost.TrimEx(); var host = node.RequestHost.TrimEx();
var path = node.Path.TrimEx(); var path = node.Path.TrimEx();
var sni = node.Sni.TrimEx(); var sni = node.Sni.TrimEx();
@ -301,6 +322,10 @@ public partial class CoreConfigV2rayService
tlsSettings.disableSystemRoot = true; tlsSettings.disableSystemRoot = true;
tlsSettings.allowInsecure = false; tlsSettings.allowInsecure = false;
} }
else if (!node.CertSha.IsNullOrEmpty())
{
tlsSettings.pinnedPeerCertSha256 = node.CertSha;
}
streamSettings.tlsSettings = tlsSettings; streamSettings.tlsSettings = tlsSettings;
} }
@ -324,7 +349,7 @@ public partial class CoreConfigV2rayService
} }
//streamSettings //streamSettings
switch (node.GetNetwork()) switch (network)
{ {
case nameof(ETransport.kcp): case nameof(ETransport.kcp):
KcpSettings4Ray kcpSettings = new() KcpSettings4Ray kcpSettings = new()
@ -463,6 +488,35 @@ public partial class CoreConfigV2rayService
streamSettings.grpcSettings = grpcSettings; streamSettings.grpcSettings = grpcSettings;
break; break;
case "hysteria":
HysteriaUdpHop4Ray? udpHop = null;
if (node.Ports.IsNotEmpty() &&
(node.Ports.Contains(':') || node.Ports.Contains('-') || node.Ports.Contains(',')))
{
udpHop = new()
{
ports = node.Ports.Replace(':', '-'),
interval = _config.HysteriaItem.HopInterval > 0
? _config.HysteriaItem.HopInterval
: null,
};
}
HysteriaSettings4Ray hysteriaSettings = new()
{
version = 2,
auth = node.Id,
up = _config.HysteriaItem.UpMbps > 0 ? $"{_config.HysteriaItem.UpMbps}mbps" : null,
down = _config.HysteriaItem.DownMbps > 0 ? $"{_config.HysteriaItem.DownMbps}mbps" : null,
udphop = udpHop,
};
streamSettings.hysteriaSettings = hysteriaSettings;
if (node.Path.IsNotEmpty())
{
streamSettings.udpmasks =
[new() { type = "salamander", settings = new() { password = node.Path.TrimEx(), } }];
}
break;
default: default:
//tcp //tcp
if (node.HeaderType == Global.TcpHeaderHttp) if (node.HeaderType == Global.TcpHeaderHttp)

View file

@ -77,12 +77,17 @@ public partial class CoreConfigV2rayService
{ {
rule.inboundTag = null; rule.inboundTag = null;
} }
if (rule.process?.Count == 0)
{
rule.process = null;
}
var hasDomainIp = false; var hasDomainIp = false;
if (rule.domain?.Count > 0) if (rule.domain?.Count > 0)
{ {
var it = JsonUtils.DeepCopy(rule); var it = JsonUtils.DeepCopy(rule);
it.ip = null; it.ip = null;
it.process = null;
it.type = "field"; it.type = "field";
for (var k = it.domain.Count - 1; k >= 0; k--) for (var k = it.domain.Count - 1; k >= 0; k--)
{ {
@ -99,6 +104,16 @@ public partial class CoreConfigV2rayService
{ {
var it = JsonUtils.DeepCopy(rule); var it = JsonUtils.DeepCopy(rule);
it.domain = null; it.domain = null;
it.process = null;
it.type = "field";
v2rayConfig.routing.rules.Add(it);
hasDomainIp = true;
}
if (_config.TunModeItem.EnableTun && rule.process?.Count > 0)
{
var it = JsonUtils.DeepCopy(rule);
it.domain = null;
it.ip = null;
it.type = "field"; it.type = "field";
v2rayConfig.routing.rules.Add(it); v2rayConfig.routing.rules.Add(it);
hasDomainIp = true; hasDomainIp = true;

View file

@ -14,6 +14,9 @@ public class AddServerViewModel : MyReactiveObject
[Reactive] [Reactive]
public string CertTip { get; set; } public string CertTip { get; set; }
[Reactive]
public string CertSha { get; set; }
public ReactiveCommand<Unit, Unit> FetchCertCmd { get; } public ReactiveCommand<Unit, Unit> FetchCertCmd { get; }
public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; } public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
@ -39,6 +42,12 @@ public class AddServerViewModel : MyReactiveObject
this.WhenAnyValue(x => x.Cert) this.WhenAnyValue(x => x.Cert)
.Subscribe(_ => UpdateCertTip()); .Subscribe(_ => UpdateCertTip());
this.WhenAnyValue(x => x.CertSha)
.Subscribe(_ => UpdateCertTip());
this.WhenAnyValue(x => x.Cert)
.Subscribe(_ => UpdateCertSha());
if (profileItem.IndexId.IsNullOrEmpty()) if (profileItem.IndexId.IsNullOrEmpty())
{ {
profileItem.Network = Global.DefaultNetwork; profileItem.Network = Global.DefaultNetwork;
@ -97,7 +106,8 @@ public class AddServerViewModel : MyReactiveObject
} }
} }
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
SelectedSource.Cert = Cert.IsNullOrEmpty() ? null : Cert; SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert;
SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha;
if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) if (await ConfigHandler.AddServer(_config, SelectedSource) == 0)
{ {
@ -113,10 +123,36 @@ public class AddServerViewModel : MyReactiveObject
private void UpdateCertTip(string? errorMessage = null) private void UpdateCertTip(string? errorMessage = null)
{ {
CertTip = errorMessage.IsNullOrEmpty() CertTip = errorMessage.IsNullOrEmpty()
? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet) ? ((Cert.IsNullOrEmpty() && CertSha.IsNullOrEmpty()) ? ResUI.CertNotSet : ResUI.CertSet)
: errorMessage; : errorMessage;
} }
private void UpdateCertSha()
{
if (Cert.IsNullOrEmpty())
{
return;
}
var certList = CertPemManager.ParsePemChain(Cert);
if (certList.Count == 0)
{
return;
}
List<string> shaList = new();
foreach (var cert in certList)
{
var sha = CertPemManager.GetCertSha256Thumbprint(cert);
if (sha.IsNullOrEmpty())
{
return;
}
shaList.Add(sha);
}
CertSha = string.Join('~', shaList);
}
private async Task FetchCert() private async Task FetchCert()
{ {
if (SelectedSource.StreamSecurity != Global.StreamSecurity) if (SelectedSource.StreamSecurity != Global.StreamSecurity)
@ -142,8 +178,8 @@ public class AddServerViewModel : MyReactiveObject
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";
} }
string certError;
(Cert, certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName);
UpdateCertTip(certError); UpdateCertTip(certError);
} }
@ -172,8 +208,8 @@ public class AddServerViewModel : MyReactiveObject
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";
} }
string certError;
(var certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName);
Cert = CertPemManager.ConcatenatePemChain(certs); Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError); UpdateCertTip(certError);
} }

View file

@ -108,6 +108,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public string CoreType4 { get; set; } [Reactive] public string CoreType4 { get; set; }
[Reactive] public string CoreType5 { get; set; } [Reactive] public string CoreType5 { get; set; }
[Reactive] public string CoreType6 { get; set; } [Reactive] public string CoreType6 { get; set; }
[Reactive] public string CoreType7 { get; set; }
[Reactive] public string CoreType9 { get; set; } [Reactive] public string CoreType9 { get; set; }
#endregion CoreType #endregion CoreType
@ -276,6 +277,10 @@ public class OptionSettingViewModel : MyReactiveObject
CoreType6 = type; CoreType6 = type;
break; break;
case 7:
CoreType7 = type;
break;
case 9: case 9:
CoreType9 = type; CoreType9 = type;
break; break;
@ -427,6 +432,10 @@ public class OptionSettingViewModel : MyReactiveObject
type = CoreType6; type = CoreType6;
break; break;
case 7:
type = CoreType7;
break;
case 9: case 9:
type = CoreType9; type = CoreType9;
break; break;

View file

@ -841,6 +841,23 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbFetchCertChain}" /> Content="{x:Static resx:ResUI.TbFetchCertChain}" />
</StackPanel> </StackPanel>
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertSha256Tips}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtCertSha256Pinning"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbFullCertTips}"
TextWrapping="Wrap" />
<TextBox <TextBox
x:Name="txtCert" x:Name="txtCert"
Width="400" Width="400"

View file

@ -75,7 +75,6 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
gridHysteria2.IsVisible = true; gridHysteria2.IsVisible = true;
sepa2.IsVisible = false; sepa2.IsVisible = false;
gridTransport.IsVisible = false; gridTransport.IsVisible = false;
cmbCoreType.IsEnabled = false;
cmbFingerprint.IsEnabled = false; cmbFingerprint.IsEnabled = false;
cmbFingerprint.SelectedValue = string.Empty; cmbFingerprint.SelectedValue = string.Empty;
break; break;
@ -186,6 +185,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.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables);

View file

@ -43,11 +43,11 @@
<MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" /> <MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" />
<MenuItem x:Name="menuAddShadowsocksServer" Header="{x:Static resx:ResUI.menuAddShadowsocksServer}" /> <MenuItem x:Name="menuAddShadowsocksServer" Header="{x:Static resx:ResUI.menuAddShadowsocksServer}" />
<MenuItem x:Name="menuAddTrojanServer" Header="{x:Static resx:ResUI.menuAddTrojanServer}" /> <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="menuAddWireguardServer" Header="{x:Static resx:ResUI.menuAddWireguardServer}" />
<MenuItem x:Name="menuAddSocksServer" Header="{x:Static resx:ResUI.menuAddSocksServer}" /> <MenuItem x:Name="menuAddSocksServer" Header="{x:Static resx:ResUI.menuAddSocksServer}" />
<MenuItem x:Name="menuAddHttpServer" Header="{x:Static resx:ResUI.menuAddHttpServer}" /> <MenuItem x:Name="menuAddHttpServer" Header="{x:Static resx:ResUI.menuAddHttpServer}" />
<Separator /> <Separator />
<MenuItem x:Name="menuAddHysteria2Server" Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem x:Name="menuAddTuicServer" Header="{x:Static resx:ResUI.menuAddTuicServer}" /> <MenuItem x:Name="menuAddTuicServer" Header="{x:Static resx:ResUI.menuAddTuicServer}" />
<MenuItem x:Name="menuAddAnytlsServer" Header="{x:Static resx:ResUI.menuAddAnytlsServer}" /> <MenuItem x:Name="menuAddAnytlsServer" Header="{x:Static resx:ResUI.menuAddAnytlsServer}" />
</MenuItem> </MenuItem>

View file

@ -463,7 +463,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding BlIsLinux}" IsVisible="{Binding BlIsLinux}"
Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenCloseTip}" /> Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenCloseTip}" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="10"
Grid.Column="0" Grid.Column="0"
@ -477,7 +477,7 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
IsVisible="{Binding BlIsIsMacOS}"/> IsVisible="{Binding BlIsIsMacOS}" />
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="11"
@ -876,7 +876,7 @@
<Grid <Grid
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
ColumnDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
@ -960,10 +960,23 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Hysteria2" />
<ComboBox
x:Name="cmbCoreType7"
Grid.Row="7"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="8"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="Wireguard" /> Text="Wireguard" />
<ComboBox <ComboBox
x:Name="cmbCoreType9" x:Name="cmbCoreType9"
Grid.Row="7" Grid.Row="8"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />

View file

@ -41,6 +41,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbCoreType4.ItemsSource = Global.CoreTypes; cmbCoreType4.ItemsSource = Global.CoreTypes;
cmbCoreType5.ItemsSource = Global.CoreTypes; cmbCoreType5.ItemsSource = Global.CoreTypes;
cmbCoreType6.ItemsSource = Global.CoreTypes; cmbCoreType6.ItemsSource = Global.CoreTypes;
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes; cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList(); cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
@ -122,6 +123,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.CoreType4, v => v.cmbCoreType4.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType4, v => v.cmbCoreType4.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType5, v => v.cmbCoreType5.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType5, v => v.cmbCoreType5.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType6, v => v.cmbCoreType6.SelectedValue).DisposeWith(disposables); 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.CoreType9, v => v.cmbCoreType9.SelectedValue).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);

View file

@ -1072,6 +1072,26 @@
Content="{x:Static resx:ResUI.TbFetchCertChain}" Content="{x:Static resx:ResUI.TbFetchCertChain}"
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
</StackPanel> </StackPanel>
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCertSha256Tips}"
TextWrapping="Wrap" />
<TextBox
x:Name="txtCertSha256Pinning"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
Style="{StaticResource DefTextBox}" />
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbFullCertTips}"
TextWrapping="Wrap" />
<TextBox <TextBox
x:Name="txtCert" x:Name="txtCert"
Width="400" Width="400"

View file

@ -70,7 +70,6 @@ public partial class AddServerWindow
gridHysteria2.Visibility = Visibility.Visible; gridHysteria2.Visibility = Visibility.Visible;
sepa2.Visibility = Visibility.Collapsed; sepa2.Visibility = Visibility.Collapsed;
gridTransport.Visibility = Visibility.Collapsed; gridTransport.Visibility = Visibility.Collapsed;
cmbCoreType.IsEnabled = false;
cmbFingerprint.IsEnabled = false; cmbFingerprint.IsEnabled = false;
cmbFingerprint.Text = string.Empty; cmbFingerprint.Text = string.Empty;
break; break;
@ -181,6 +180,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.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);

View file

@ -99,6 +99,10 @@
x:Name="menuAddTrojanServer" x:Name="menuAddTrojanServer"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddTrojanServer}" /> Header="{x:Static resx:ResUI.menuAddTrojanServer}" />
<MenuItem
x:Name="menuAddHysteria2Server"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem <MenuItem
x:Name="menuAddWireguardServer" x:Name="menuAddWireguardServer"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
@ -112,10 +116,6 @@
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddHttpServer}" /> Header="{x:Static resx:ResUI.menuAddHttpServer}" />
<Separator Margin="-40,5" /> <Separator Margin="-40,5" />
<MenuItem
x:Name="menuAddHysteria2Server"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem <MenuItem
x:Name="menuAddTuicServer" x:Name="menuAddTuicServer"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"

View file

@ -1138,6 +1138,7 @@
<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="Auto" /> <ColumnDefinition Width="Auto" />
@ -1239,10 +1240,25 @@
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="Hysteria2" />
<ComboBox
x:Name="cmbCoreType7"
Grid.Row="7"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefComboBox}" />
<TextBlock
Grid.Row="8"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="Wireguard" /> Text="Wireguard" />
<ComboBox <ComboBox
x:Name="cmbCoreType9" x:Name="cmbCoreType9"
Grid.Row="7" Grid.Row="8"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"

View file

@ -38,6 +38,7 @@ public partial class OptionSettingWindow
cmbCoreType4.ItemsSource = Global.CoreTypes; cmbCoreType4.ItemsSource = Global.CoreTypes;
cmbCoreType5.ItemsSource = Global.CoreTypes; cmbCoreType5.ItemsSource = Global.CoreTypes;
cmbCoreType6.ItemsSource = Global.CoreTypes; cmbCoreType6.ItemsSource = Global.CoreTypes;
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes; cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList(); cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
@ -127,6 +128,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.CoreType4, v => v.cmbCoreType4.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType4, v => v.cmbCoreType4.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType5, v => v.cmbCoreType5.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType5, v => v.cmbCoreType5.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType6, v => v.cmbCoreType6.Text).DisposeWith(disposables); 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.CoreType9, v => v.cmbCoreType9.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);