diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 0632561cb77..baeae29a35d 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -341,7 +341,7 @@ jobs: QCOW2_IMAGE: debian13-loong64.qcow2 EFI_CODE: edk2-loongarch64-code.fd EFI_VARS: edk2-loongarch64-vars.fd - QEMU_VERSION: 10.2.2 + QEMU_VERSION: 10.2.3 steps: - name: Prepare host tools diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 671305e6bee..086c726f88a 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -7,32 +7,32 @@ - - + + - + + - - + + - + - + - + - - + diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs index 812a5a32aad..63bc45cac35 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs @@ -187,7 +187,7 @@ public static CoreConfigContext CreateContext(Config config, ProfileItem node, E SimpleDnsItem = config.SimpleDNSItem, AllProxiesMap = new Dictionary { [node.IndexId] = node }, FullConfigTemplate = null, - IsTunEnabled = false, + IsTunEnabled = config.TunModeItem.EnableTun, ProtectDomainList = [], }; } @@ -206,4 +206,12 @@ public static Config CreateConfigWithBootstrapDNS(ECoreType coreType, string boo config.SimpleDNSItem.BootstrapDNS = bootstrapDns; return config; } + + public static Config CreateConfigWithTunRouteExcludeAddress(ECoreType coreType) + { + var config = CreateConfig(coreType); + config.TunModeItem.EnableTun = true; + config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"]; + return config; + } } diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs index 29b26648b3c..4847050d8c1 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -536,4 +536,27 @@ public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig() directOutbound.Should().NotBeNull(); directOutbound!.settings.domainStrategy.Should().Be("UseIPv4"); } + + [Fact] + public void GenerateClientConfigContent_TunRouteExcludeAddress() + { + var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); + + tunInbound.Should().NotBeNull(); + + tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0"); + tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32"); + tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31"); + } } diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index eb1af21b136..b7481e1aaa8 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -91,6 +91,20 @@ public static async Task Build(Config config, Pr context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; } } + if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 }) + { + foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress) + { + try + { + IPNetwork2.Parse(addr); + } + catch + { + validatorResult.Errors.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr)); + } + } + } return new CoreConfigContextBuilderResult(context, validatorResult); } diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs index 51535990a43..2b8199400a6 100644 --- a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -136,7 +136,9 @@ private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreT } // Check for deprecated allowInsecure property when TLS is enabled - if (item.AllowInsecure == "true") + if (item.AllowInsecure == "true" + && item.Cert.IsNullOrEmpty() + && item.CertSha.IsNullOrEmpty()) { v.Warning(ResUI.MsgAllowInsecureDeprecated); } diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index e5da70070ad..22fe0ab2595 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -255,6 +255,7 @@ public static async Task AddServer(Config config, ProfileItem profileItem) item.Cert = profileItem.Cert; item.CertSha = profileItem.CertSha; item.EchConfigList = profileItem.EchConfigList; + item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName; item.Finalmask = profileItem.Finalmask; item.ProtoExtra = profileItem.ProtoExtra; item.TransportExtra = profileItem.TransportExtra; diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs index ff7d6d20ea2..0f82242763e 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -73,6 +73,10 @@ protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dicti { dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList)); } + if (item.VerifyPeerCertByName.IsNotEmpty()) + { + dicQuery.Add("vcn", Utils.UrlEncode(item.VerifyPeerCertByName)); + } if (item.CertSha.IsNotEmpty()) { dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha)); @@ -227,6 +231,7 @@ protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem item.SpiderX = GetQueryDecoded(query, "spx"); item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); item.EchConfigList = GetQueryDecoded(query, "ech"); + item.VerifyPeerCertByName = GetQueryDecoded(query, "vcn"); item.CertSha = GetQueryDecoded(query, "pcs"); var finalmaskDecoded = GetQueryDecoded(query, "fm"); diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs index 0a58e2deccf..7f5d388984d 100644 --- a/v2rayN/ServiceLib/Manager/CertPemManager.cs +++ b/v2rayN/ServiceLib/Manager/CertPemManager.cs @@ -4,16 +4,15 @@ namespace ServiceLib.Manager; /// -/// Manager for certificate operations with CA pinning to prevent MITM attacks +/// Manager for certificate operations with CA pinning to prevent MITM attacks /// public class CertPemManager { private static readonly string _tag = "CertPemManager"; private static readonly Lazy _instance = new(() => new()); - public static CertPemManager Instance => _instance.Value; /// - /// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks + /// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks /// private static readonly HashSet TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase) { @@ -24,21 +23,17 @@ public class CertPemManager "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root "85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2 "18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3 - "CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root "C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA "1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA "3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA "4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA "7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA "62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2 - "F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA - "4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA "0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority "1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority "41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA "E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna "C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority - "EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA "6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány "3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009 "CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3 @@ -46,10 +41,6 @@ public class CertPemManager "45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2 "2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2 "568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2 - "0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial - "0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking - "70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium - "BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC "5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA "BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority "513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2 @@ -62,7 +53,6 @@ public class CertPemManager "E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2 "9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1 "59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA - "DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1 "91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2 "F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011 "8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3 @@ -116,16 +106,12 @@ public class CertPemManager "C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017 "BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017 "657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2 - "97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority - "945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority - "55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority "88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority "554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS "319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45 "5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45 "4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46 "CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46 - "9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020 "FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA "6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA "FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA @@ -179,7 +165,6 @@ public class CertPemManager "578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020 "78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023 "EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023 - "BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB "3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA "3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2 "3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12 @@ -200,10 +185,13 @@ public class CertPemManager "B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023 }; + public static CertPemManager Instance => _instance.Value; + /// - /// Get certificate in PEM format from a server with CA pinning validation + /// Get certificate in PEM format from a server with CA pinning validation /// - public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false) + public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, + List? verifyPeerCertByName = null, bool allowInsecure = false) { try { @@ -216,13 +204,14 @@ public class CertPemManager await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => - ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure)); + ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], + allowInsecure)); await using var ssl = new SslStream(client.GetStream(), false, callback); var sslOptions = new SslClientAuthenticationOptions { TargetHost = serverName, - RemoteCertificateValidationCallback = callback + RemoteCertificateValidationCallback = callback, }; await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); @@ -249,9 +238,10 @@ public class CertPemManager } /// - /// Get certificate chain in PEM format from a server with CA pinning validation + /// Get certificate chain in PEM format from a server with CA pinning validation /// - public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false) + public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, + List? verifyPeerCertByName = null, bool allowInsecure = false) { var pemList = new List(); try @@ -265,13 +255,14 @@ public class CertPemManager await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => - ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure)); + ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], + allowInsecure)); await using var ssl = new SslStream(client.GetStream(), false, callback); var sslOptions = new SslClientAuthenticationOptions { TargetHost = serverName, - RemoteCertificateValidationCallback = callback + RemoteCertificateValidationCallback = callback, }; await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); @@ -301,13 +292,14 @@ public class CertPemManager } /// - /// Validate server certificate with CA pinning + /// Validate server certificate with CA pinning /// - private bool ValidateServerCertificate( + private static bool ValidateServerCertificate( object _, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors, + List verifyPeerCertByName, bool allowInsecure) { if (certificate == null) @@ -321,22 +313,21 @@ private bool ValidateServerCertificate( return true; } - // Check certificate name mismatch - if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) - { - return false; - } - // Build certificate chain var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); var certChain = chain ?? new X509Chain(); - certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; - certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; - certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; - certChain.ChainPolicy.VerificationTime = DateTime.Now; + if (chain == null) + { + certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + certChain.ChainPolicy.VerificationTime = DateTime.UtcNow; - certChain.Build(cert2); + if (!certChain.Build(cert2)) + { + return false; + } + } // Find root CA if (certChain.ChainElements.Count == 0) @@ -344,10 +335,37 @@ private bool ValidateServerCertificate( return false; } - var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate; + var rootCert = certChain.ChainElements[^1].Certificate; var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256); - return TrustedCaThumbprints.Contains(rootThumbprint); + if (!TrustedCaThumbprints.Contains(rootThumbprint)) + { + return false; + } + + if (!sslPolicyErrors.HasFlag( + SslPolicyErrors.RemoteCertificateNameMismatch)) + { + return true; + } + + if (verifyPeerCertByName.Count == 0) + { + return false; + } + + foreach (var ext in cert2.Extensions) + { + if (ext is not X509SubjectAlternativeNameExtension san) + { + continue; + } + + return san.EnumerateDnsNames().Any(dnsName => + verifyPeerCertByName.Contains(dnsName, StringComparer.OrdinalIgnoreCase)); + } + + return false; } public static string ExportCertToPem(X509Certificate2 cert) @@ -358,8 +376,8 @@ public static string ExportCertToPem(X509Certificate2 cert) } /// - /// Parse concatenated PEM certificates string into a list of individual certificates - /// Normalizes format: removes line breaks from base64 content for better compatibility + /// Parse concatenated PEM certificates string into a list of individual certificates + /// Normalizes format: removes line breaks from base64 content for better compatibility /// /// Concatenated PEM certificates string (supports both \r\n and \n line endings) /// List of individual PEM certificate strings with normalized format @@ -411,18 +429,13 @@ public static List ParsePemChain(string pemChain) } /// - /// Concatenate a list of PEM certificates into a single string + /// Concatenate a list of PEM certificates into a single string /// /// List of individual PEM certificate strings /// Concatenated PEM certificates string - public static string ConcatenatePemChain(IEnumerable pemList) + public static string ConcatenatePemChain(IEnumerable? pemList) { - if (pemList == null) - { - return string.Empty; - } - - return string.Concat(pemList); + return pemList == null ? string.Empty : string.Concat(pemList); } public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false) @@ -431,11 +444,7 @@ public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = { var cert = X509Certificate2.CreateFromPem(pemCert); var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256); - if (includeColon) - { - return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))); - } - return thumbprint; + return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint; } catch { diff --git a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs index d5101275f41..e3026184ad0 100644 --- a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs @@ -148,6 +148,7 @@ public class TunModeItem public bool EnableIPv6Address { get; set; } public string IcmpRouting { get; set; } public bool EnableLegacyProtect { get; set; } + public List? RouteExcludeAddress { get; set; } } [Serializable] diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs index 1514cae4524..a5249acb778 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs @@ -110,6 +110,7 @@ public class Inbound4Sbox public bool? endpoint_independent_nat { get; set; } public string? stack { get; set; } public List users { get; set; } + public List? route_exclude_address { get; set; } } public class User4Sbox diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs index ed6219abe73..c2dd39fe107 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs @@ -86,7 +86,7 @@ public class Inboundsettings4Ray public string? autoOutboundsInterface { get; set; } - // public List? dns { get; set; } + public List? dns { get; set; } } public class UsersItem4Ray @@ -373,6 +373,7 @@ public class TlsSettings4Ray public string? spiderX { get; set; } public string? mldsa65Verify { get; set; } public List? certificates { get; set; } + public string? verifyPeerCertByName { get; set; } public string? pinnedPeerCertSha256 { get; set; } public bool? disableSystemRoot { get; set; } public string? echConfigList { get; set; } diff --git a/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs b/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs index dddd84d55c2..0257ab67b4b 100644 --- a/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs +++ b/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs @@ -22,7 +22,7 @@ public readonly record struct IpInfoResult(string Country, string? Ip) { public override string ToString() { - var emoji = Country.CountryToEmoji(); + var emoji = Utils.IsWindows() ? null : Country.CountryToEmoji(); return $"{emoji}({Country}) {Ip}"; } } diff --git a/v2rayN/ServiceLib/Models/Entities/ProfileItem.cs b/v2rayN/ServiceLib/Models/Entities/ProfileItem.cs index cb5deabdcfc..94dd3a69b88 100644 --- a/v2rayN/ServiceLib/Models/Entities/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProfileItem.cs @@ -191,6 +191,7 @@ public void SetTransportExtra(TransportExtraItem transportExtra) public string Cert { get; set; } public string CertSha { get; set; } public string EchConfigList { get; set; } + public string VerifyPeerCertByName { get; set; } public string Finalmask { get; set; } public string ProtoExtra { get; set; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 2729e284c7b..b7cf06dacb6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -2229,6 +2229,15 @@ public static string MsgSubscriptionPrevProfileNotFound { } } + /// + /// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。 + /// + public static string MsgTunRouteExcludeInvalidAddress { + get { + return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture); + } + } + /// /// 查找类似 Unpacking... 的本地化字符串。 /// @@ -3537,6 +3546,24 @@ public static string TbRoundRobin { } } + /// + /// 查找类似 Route Exclude Address 的本地化字符串。 + /// + public static string TbRouteExcludeAddress { + get { + return ResourceManager.GetString("TbRouteExcludeAddress", resourceCulture); + } + } + + /// + /// 查找类似 Use commas (,) to separate. 的本地化字符串。 + /// + public static string TbRouteExcludeAddressTip { + get { + return ResourceManager.GetString("TbRouteExcludeAddressTip", resourceCulture); + } + } + /// /// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// @@ -4707,6 +4734,15 @@ public static string TbValidateDirectExpectedIPsDesc { } } + /// + /// 查找类似 Verify Peer Cert By Name 的本地化字符串。 + /// + public static string TbVerifyPeerCertByName { + get { + return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture); + } + } + /// /// 查找类似 The delay: {0} ms, {1} 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index df5fdeb0783..ec97d10a905 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1581,6 +1581,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchConfigList + + Verify Peer Cert By Name + Full certificate (chain), PEM format @@ -1749,4 +1752,13 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration. + + Route Exclude Address + + + Use commas (,) to separate. + + + Invalid address in TUN route exclude list: {0} + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 1577baf6da2..4654b723948 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1746,4 +1746,16 @@ 警告:Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 + + Verify Peer Cert By Name + + + 路由排除地址 + + + 使用逗号 (,) 分隔 + + + TUN 路由排除列表包含无效地址:{0} + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index f52b640f7db..2f1c1857cf6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1746,4 +1746,13 @@ 警告:Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。 + + Verify Peer Cert By Name + + + 路由排除位址 + + + 使用逗號 (,) 分隔 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SampleTunInbound b/v2rayN/ServiceLib/Sample/SampleTunInbound index 95cefbc18a1..2edc5ea0643 100644 --- a/v2rayN/ServiceLib/Sample/SampleTunInbound +++ b/v2rayN/ServiceLib/Sample/SampleTunInbound @@ -8,6 +8,9 @@ "172.18.0.1/30", "fdfe:dcba:9876::1/126" ], + "dns": [ + "172.18.0.1" + ], "autoSystemRoutingTable": [ "0.0.0.0/0", "::/0" diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index 6a506e36f95..5b0b807261b 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -6,6 +6,7 @@ + true diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs index 2e13c624b25..3aa06d4e058 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs @@ -71,6 +71,7 @@ private void GenInbounds() { tunInbound.address = ["172.18.0.1/30"]; } + tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress; _coreConfig.inbounds.Add(tunInbound); } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index 239c68add93..7f950a83b32 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -30,14 +30,19 @@ private void GenInbounds() inbound3.listen = listen; _coreConfig.inbounds.Add(inbound3); - //auth + // auth if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) { inbound3.settings.auth = "password"; - inbound3.settings.accounts = new List - { - new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } - }; + inbound3.settings.accounts = + [ + new() + { + user = _config.Inbound.First().User, + pass = _config.Inbound.First().Pass, + }, + + ]; } } else @@ -53,12 +58,15 @@ private void GenInbounds() { _config.TunModeItem.Mtu = Global.TunMtus.First(); } - var tunInbound = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? new Inbounds4Ray { }; + var tunInbound = + JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? + new Inbounds4Ray(); tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun"; tunInbound.settings.MTU = _config.TunModeItem.Mtu; - if (_config.TunModeItem.EnableIPv6Address == false) + if (!_config.TunModeItem.EnableIPv6Address) { tunInbound.settings.gateway = ["172.18.0.1/30"]; + tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"]; } var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); if (!bindInterface.IsNullOrEmpty()) @@ -66,6 +74,53 @@ private void GenInbounds() tunInbound.settings.autoOutboundsInterface = bindInterface; } tunInbound.sniffing = inbound.sniffing; + + if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 }) + { + var wholeInternet = IPNetwork2.Parse("0.0.0.0/0"); + var wholeInternetV6 = IPNetwork2.Parse("::/0"); + + var excludeList = _config.TunModeItem.RouteExcludeAddress.Select(IPNetwork2.Parse) + .Where(x => x != null).ToList(); + + var includeList = new List { wholeInternet }; + var includeListV6 = new List { wholeInternetV6 }; + + foreach (var exclude in excludeList) + { + var temp = new List(); + if (exclude.AddressFamily == AddressFamily.InterNetwork) + { + foreach (var net in includeList) + { + temp.AddRange(net.Subtract(exclude)); + } + includeList = temp; + } + else if (exclude.AddressFamily == AddressFamily.InterNetworkV6) + { + foreach (var net in includeListV6) + { + temp.AddRange(net.Subtract(exclude)); + } + includeListV6 = temp; + } + } + + includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList(); + includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList(); + + if (_config.TunModeItem.EnableIPv6Address) + { + tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()) + .Concat(includeListV6.Select(x => x.ToString())).ToList(); + } + else + { + tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList(); + } + } + _coreConfig.inbounds.Add(tunInbound); } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 0cd35085b83..beb0aa5b197 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -384,6 +384,7 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) alpn = _node.GetAlpn(), fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint, echConfigList = _node.EchConfigList.NullIfEmpty(), + verifyPeerCertByName = _node.VerifyPeerCertByName.NullIfEmpty(), }; if (sni.IsNotEmpty()) { diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 169e8a4424d..f5906f2159d 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -464,7 +464,9 @@ private async Task FetchCert() domain += $":{SelectedSource.Port}"; } - (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch); + (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, + verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), + allowInsecure: AllowInsecureCertFetch); UpdateCertTip(certError); } @@ -489,7 +491,9 @@ private async Task FetchCertChain() domain += $":{SelectedSource.Port}"; } - var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch); + var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, + verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), + allowInsecure: AllowInsecureCertFetch); Cert = CertPemManager.ConcatenatePemChain(certs); UpdateCertTip(certError); } diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 23a21b99f05..583502c8686 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -100,6 +100,7 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public bool TunEnableIPv6Address { get; set; } [Reactive] public string TunIcmpRouting { get; set; } [Reactive] public bool TunEnableLegacyProtect { get; set; } + [Reactive] public string TunRouteExcludeAddress { get; set; } #endregion Tun mode @@ -228,6 +229,7 @@ private async Task Init() TunEnableIPv6Address = _config.TunModeItem.EnableIPv6Address; TunIcmpRouting = _config.TunModeItem.IcmpRouting; TunEnableLegacyProtect = _config.TunModeItem.EnableLegacyProtect; + TunRouteExcludeAddress = Utils.List2String(_config.TunModeItem.RouteExcludeAddress, true); #endregion Tun mode @@ -397,6 +399,7 @@ private async Task SaveSettingAsync() _config.TunModeItem.EnableIPv6Address = TunEnableIPv6Address; _config.TunModeItem.IcmpRouting = TunIcmpRouting; _config.TunModeItem.EnableLegacyProtect = TunEnableLegacyProtect; + _config.TunModeItem.RouteExcludeAddress = Utils.String2List(TunRouteExcludeAddress); //coreType await SaveCoreType(); diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index dbf031b11a6..574d4716862 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -1060,7 +1060,7 @@ Grid.Row="8" ColumnDefinitions="300,Auto" IsVisible="False" - RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> + + + vm.AllowInsecureCertFetch, v => v.togAllowInsecureCertFetch.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AllowInsecureCertFetch, v => v.txtAllowInsecureCertFetchTips.IsVisible).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables); //reality this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml index 2baea96c8d8..2038c702518 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml @@ -818,107 +818,132 @@ - - - - + + ColumnDefinitions="Auto,Auto,Auto" + DockPanel.Dock="Top" + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> - - + + - - + + - - + + - - + + - - + + - - - + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs index f08c37c2f2b..f54a145ddce 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs @@ -122,6 +122,7 @@ public OptionSettingWindow() this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunIcmpRouting, v => v.cmbIcmpRoutingPolicy.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunEnableLegacyProtect, v => v.togEnableLegacyProtect.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.TunRouteExcludeAddress, v => v.txtRouteExcludeAddress.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType2, v => v.cmbCoreType2.SelectedValue).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayN/v2rayN/Views/AddServerWindow.xaml index 16512751430..4a8ef291329 100644 --- a/v2rayN/v2rayN/Views/AddServerWindow.xaml +++ b/v2rayN/v2rayN/Views/AddServerWindow.xaml @@ -1377,6 +1377,7 @@ + @@ -1461,6 +1462,22 @@ HorizontalAlignment="Left" Style="{StaticResource DefTextBox}" /> + + + v.txtAllowInsecureCertFetchTips.Visibility); 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.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables); //reality this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml index df46d963939..2e972f31293 100644 --- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml +++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml @@ -1065,129 +1065,161 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - - + + + + + + + + + diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs index d48248268d9..1ede6bd7823 100644 --- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs @@ -126,6 +126,7 @@ public OptionSettingWindow() this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunIcmpRouting, v => v.cmbIcmpRoutingPolicy.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunEnableLegacyProtect, v => v.togEnableLegacyProtect.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.TunRouteExcludeAddress, v => v.txtRouteExcludeAddress.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType2, v => v.cmbCoreType2.Text).DisposeWith(disposables);