From 8104cb13dfbc0a8b0707239b1bd80f143bc56402 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Wed, 15 Jan 2025 10:01:09 +0100 Subject: [PATCH] Add IPAddress.IsValid (#111433) * Add IPAddress.IsValid * Speed up fuzzing inner loop * Rename IsValid for bytes to IsValidUtf8 --- .../libraries/fuzzing/deploy-to-onefuzz.yml | 8 ++ .../Interop.OpenSsl.cs | 2 +- .../System/Net/IPv6AddressHelper.Common.cs | 2 +- .../Net/Security/TargetHostNameHelper.cs | 43 +------ src/libraries/Fuzzing/DotnetFuzzing/Assert.cs | 10 +- .../DotnetFuzzing/DotnetFuzzing.csproj | 1 + .../DotnetFuzzing/Fuzzers/IPAddressFuzzer.cs | 107 ++++++++++++++++++ .../Fuzzing/DotnetFuzzing/Program.cs | 31 +++-- .../src/System/Net/ServiceNameStore.cs | 2 +- .../ref/System.Net.Primitives.cs | 2 + .../src/System/Net/IPAddress.cs | 8 ++ .../src/System/Net/IPAddressParser.cs | 89 +++++++++------ .../tests/FunctionalTests/IPAddressParsing.cs | 15 +++ .../UnitTests/Fakes/IPv6AddressHelper.cs | 2 +- .../src/System/Net/Quic/QuicConnection.cs | 4 +- .../Pal.Android/SafeDeleteSslContext.cs | 2 +- .../Security/Pal.OSX/SafeDeleteSslContext.cs | 2 +- .../src/System/Net/WebProxy.Wasm.cs | 2 +- 18 files changed, 234 insertions(+), 98 deletions(-) create mode 100644 src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/IPAddressFuzzer.cs diff --git a/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml b/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml index 2c9d95a807d11b..1f7a53fe649409 100644 --- a/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml +++ b/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml @@ -98,6 +98,14 @@ extends: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Send HttpHeadersFuzzer to OneFuzz + - task: onefuzz-task@0 + inputs: + onefuzzOSes: 'Windows' + env: + onefuzzDropDirectory: $(fuzzerProject)/deployment/IPAddressFuzzer + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Send IPAddressFuzzer to OneFuzz + - task: onefuzz-task@0 inputs: onefuzzOSes: 'Windows' diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs index f8fa8b7d736c0f..1fe9ee1ffb9200 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs @@ -408,7 +408,7 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth if (sslAuthenticationOptions.IsClient) { - if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !TargetHostNameHelper.IsValidAddress(sslAuthenticationOptions.TargetHost)) + if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !IPAddress.IsValid(sslAuthenticationOptions.TargetHost)) { // Similar to windows behavior, set SNI on openssl by default for client context, ignore errors. if (!Ssl.SslSetTlsExtHostName(sslHandle, sslAuthenticationOptions.TargetHost)) diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 6645daa83e4b20..d27cd18b8d56c3 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -95,7 +95,7 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) // Remarks: MUST NOT be used unless all input indexes are verified and trusted. // start must be next to '[' position, or error is reported - internal static unsafe bool IsValidStrict(TChar* name, int start, ref int end) + internal static unsafe bool IsValidStrict(TChar* name, int start, int end) where TChar : unmanaged, IBinaryInteger { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index bc973c247aa91c..b60db14144d51a 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -1,9 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + using System.Buffers; -using System.Collections.Generic; using System.Globalization; -using System.Runtime.InteropServices; namespace System.Net.Security { @@ -37,45 +36,5 @@ internal static string NormalizeHostName(string? targetHost) return targetHost; } - - // Simplified version of IPAddressParser.Parse to avoid allocations and dependencies. - // It purposely ignores scopeId as we don't really use so we do not need to map it to actual interface id. - internal static unsafe bool IsValidAddress(string? hostname) - { - if (string.IsNullOrEmpty(hostname)) - { - return false; - } - - ReadOnlySpan ipSpan = hostname.AsSpan(); - - int end = ipSpan.Length; - - if (ipSpan.Contains(':')) - { - // The address is parsed as IPv6 if and only if it contains a colon. This is valid because - // we don't support/parse a port specification at the end of an IPv4 address. - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); - } - } - else if (char.IsDigit(ipSpan[0])) - { - long tmpAddr; - - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); - } - - if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) - { - return true; - } - } - - return false; - } } } diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs index 2814de3f08bf49..9f260440cc4764 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; + namespace DotnetFuzzing; internal static class Assert @@ -18,6 +20,12 @@ static void Throw(T expected, T actual) => throw new Exception($"Expected={expected} Actual={actual}"); } + public static void True([DoesNotReturnIf(false)] bool actual) => + Equal(true, actual); + + public static void False([DoesNotReturnIf(true)] bool actual) => + Equal(false, actual); + public static void NotNull(T value) { if (value == null) @@ -26,7 +34,7 @@ public static void NotNull(T value) } static void ThrowNull() => - throw new Exception("Value is null"); + throw new Exception("Value is null"); } public static void SequenceEqual(ReadOnlySpan expected, ReadOnlySpan actual) diff --git a/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj b/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj index a392983c364e9f..195dc8d04ff993 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj +++ b/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj @@ -22,6 +22,7 @@ + diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/IPAddressFuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/IPAddressFuzzer.cs new file mode 100644 index 00000000000000..fd7df5fb9c29e3 --- /dev/null +++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/IPAddressFuzzer.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Unicode; + +namespace DotnetFuzzing.Fuzzers +{ + internal sealed class IPAddressFuzzer : IFuzzer + { + public string[] TargetAssemblies => ["System.Net.Primitives", "System.Private.Uri"]; + public string[] TargetCoreLibPrefixes => []; + + public void FuzzTarget(ReadOnlySpan bytes) + { + using var poisonedBytes = PooledBoundedMemory.Rent(bytes, PoisonPagePlacement.After); + using var poisonedChars = PooledBoundedMemory.Rent(MemoryMarshal.Cast(bytes), PoisonPagePlacement.After); + + if (IPAddress.IsValidUtf8(poisonedBytes.Span)) + { + TestValidInput(bytes: poisonedBytes.Span); + } + else + { + Assert.False(IPAddress.TryParse(poisonedBytes.Span, out _)); + } + + if (IPAddress.IsValid(poisonedChars.Span)) + { + TestValidInput(chars: poisonedChars.Span.ToString()); + } + else + { + Assert.False(IPAddress.TryParse(poisonedChars.Span, out _)); + } + + static void TestValidInput(ReadOnlySpan bytes = default, string? chars = null) + { + if (chars is null) + { + // bytes past the '%' may not be valid UTF-8: https://github.com/dotnet/runtime/issues/111288 + int percentIndex = bytes.IndexOf((byte)'%'); + Assert.True(Utf8.IsValid(bytes.Slice(0, percentIndex < 0 ? bytes.Length : percentIndex))); + + chars = Encoding.UTF8.GetString(bytes); + } + else + { + bytes = Encoding.UTF8.GetBytes(chars); + } + + Assert.True(IPAddress.IsValid(chars)); + Assert.True(IPAddress.TryParse(chars, out IPAddress? ipFromChars)); + Assert.True(IPAddress.TryParse(bytes, out IPAddress? ipFromBytes)); + + Assert.True(ipFromChars.Equals(ipFromBytes)); + Assert.True(ipFromBytes.Equals(ipFromChars)); + + Assert.True(IPAddress.IsValid(ipFromChars.ToString())); + + TestUri(chars); + } + + static void TestUri(string chars) + { + bool isIpv6 = chars.Contains(':'); + UriHostNameType hostNameType = isIpv6 ? UriHostNameType.IPv6 : UriHostNameType.IPv4; + + if (isIpv6) + { + // Remove the ScopeId + int percentIndex = chars.IndexOf('%'); + if (percentIndex >= 0) + { + chars = chars.Substring(0, percentIndex); + if (chars.StartsWith('[')) + { + chars = $"{chars}]"; + } + } + + if (!chars.StartsWith('[')) + { + chars = $"[{chars}]"; + } + + // Remove the port + int bracketIndex = chars.IndexOf(']'); + if (bracketIndex >= 0 && + bracketIndex + 1 < chars.Length && + chars[bracketIndex + 1] == ':') + { + chars = chars.Substring(0, bracketIndex + 1); + } + } + + Assert.True(Uri.TryCreate($"http://{chars}/", UriKind.Absolute, out Uri? uri)); + Assert.Equal(hostNameType, uri.HostNameType); + Assert.Equal(hostNameType, Uri.CheckHostName(chars)); + } + } + } +} diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Program.cs b/src/libraries/Fuzzing/DotnetFuzzing/Program.cs index a2e0f4d6e60a4f..243fd25f954766 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Program.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Program.cs @@ -110,15 +110,33 @@ await DownloadArtifactAsync( "https://github.com/Metalnem/libfuzzer-dotnet/releases/download/v2023.06.26.1359/libfuzzer-dotnet-windows.exe", "cbc1f510caaec01b17b5e89fc780f426710acee7429151634bbf4d0c57583458").ConfigureAwait(false); - foreach (IFuzzer fuzzer in fuzzers) + Console.WriteLine("Preparing fuzzers ..."); + + List exceptions = new(); + + Parallel.ForEach(fuzzers, fuzzer => { - Console.WriteLine(); - Console.WriteLine($"Preparing {fuzzer.Name} ..."); + try + { + PrepareFuzzer(fuzzer); + } + catch (Exception ex) + { + exceptions.Add($"Failed to prepare {fuzzer.Name}: {ex.Message}"); + } + }); + if (exceptions.Count != 0) + { + Console.WriteLine(string.Join('\n', exceptions)); + throw new Exception($"Failed to prepare {exceptions.Count} fuzzers."); + } + + void PrepareFuzzer(IFuzzer fuzzer) + { string fuzzerDirectory = Path.Combine(outputDirectory, fuzzer.Name); Directory.CreateDirectory(fuzzerDirectory); - Console.WriteLine($"Copying artifacts to {fuzzerDirectory}"); // NOTE: The expected fuzzer directory structure is currently flat. // If we ever need to support subdirectories, OneFuzzConfig.json must also be updated to use PreservePathsJobDependencies. foreach (string file in Directory.GetFiles(publishDirectory)) @@ -138,10 +156,7 @@ await DownloadArtifactAsync( InstrumentAssemblies(fuzzer, fuzzerDirectory); - Console.WriteLine("Generating OneFuzzConfig.json"); File.WriteAllText(Path.Combine(fuzzerDirectory, "OneFuzzConfig.json"), GenerateOneFuzzConfigJson(fuzzer)); - - Console.WriteLine("Generating local-run.bat"); File.WriteAllText(Path.Combine(fuzzerDirectory, "local-run.bat"), GenerateLocalRunHelperScript(fuzzer)); } @@ -195,8 +210,6 @@ private static void InstrumentAssemblies(IFuzzer fuzzer, string fuzzerDirectory) { foreach (var (assembly, prefixes) in GetInstrumentationTargets(fuzzer)) { - Console.WriteLine($"Instrumenting {assembly} {(prefixes is null ? "" : $"({prefixes})")}"); - string path = Path.Combine(fuzzerDirectory, assembly); if (!File.Exists(path)) { diff --git a/src/libraries/System.Net.HttpListener/src/System/Net/ServiceNameStore.cs b/src/libraries/System.Net.HttpListener/src/System/Net/ServiceNameStore.cs index deaf7e1851a591..ed0d541195b7f1 100644 --- a/src/libraries/System.Net.HttpListener/src/System/Net/ServiceNameStore.cs +++ b/src/libraries/System.Net.HttpListener/src/System/Net/ServiceNameStore.cs @@ -272,7 +272,7 @@ public static string[] BuildServiceNames(string uriPrefix) if (hostname == "*" || hostname == "+" || - IPAddress.TryParse(hostname, out _)) + IPAddress.IsValid(hostname)) { // for a wildcard, register the machine name. If the caller doesn't have DNS permission // or the query fails for some reason, don't add an SPN. diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index 05dee66ca23777..6122e499bd50ae 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -256,6 +256,8 @@ public IPAddress(System.ReadOnlySpan address, long scopeid) { } public static int HostToNetworkOrder(int host) { throw null; } public static long HostToNetworkOrder(long host) { throw null; } public static bool IsLoopback(System.Net.IPAddress address) { throw null; } + public static bool IsValidUtf8(System.ReadOnlySpan utf8Text) { throw null; } + public static bool IsValid(System.ReadOnlySpan ipSpan) { throw null; } public System.Net.IPAddress MapToIPv4() { throw null; } public System.Net.IPAddress MapToIPv6() { throw null; } public static short NetworkToHostOrder(short network) { throw null; } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs index cd7e281e541c52..541e96219c1c03 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -226,6 +226,14 @@ internal IPAddress(int newAddress) PrivateAddress = (uint)newAddress; } + /// Determines whether the provided span contains a valid . + /// The text to parse. + public static bool IsValid(ReadOnlySpan ipSpan) => IPAddressParser.IsValid(ipSpan); + + /// Determines whether the provided span contains a valid . + /// The text to parse. + public static bool IsValidUtf8(ReadOnlySpan utf8Text) => IPAddressParser.IsValid(utf8Text); + /// /// /// Converts an IP address string to an instance. diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 7ef1cd216e99cb..2352c4f88397d2 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -16,6 +16,24 @@ internal static class IPAddressParser internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number internal const int MaxIPv6StringLength = 65; + public static unsafe bool IsValid(ReadOnlySpan ipSpan) + where TChar : unmanaged, IBinaryInteger + { + fixed (TChar* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) + { + if (ipSpan.Contains(TChar.CreateTruncating(':'))) + { + return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ipSpan.Length); + } + else + { + int end = ipSpan.Length; + long address = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); + return address != IPv4AddressHelper.Invalid && end == ipSpan.Length; + } + } + } + internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) where TChar : unmanaged, IBinaryInteger { @@ -75,60 +93,57 @@ private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span< Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); - int end = ipSpan.Length; - bool isValid = false; fixed (TChar* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) { - isValid = IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); + if (!IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ipSpan.Length)) + { + scope = 0; + return false; + } } - scope = 0; - if (isValid || (end != ipSpan.Length)) + IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); + + if (scopeIdSpan.Length > 1) { - IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan scopeIdSpan); + bool parsedNumericScope; + scopeIdSpan = scopeIdSpan.Slice(1); - if (scopeIdSpan.Length > 1) + // scopeId is a numeric value + if (typeof(TChar) == typeof(byte)) { - bool parsedNumericScope = false; - scopeIdSpan = scopeIdSpan.Slice(1); + ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); - // scopeId is a numeric value - if (typeof(TChar) == typeof(byte)) - { - ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); + parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope); + } + else + { + ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); - parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope); - } - else if (typeof(TChar) == typeof(char)) - { - ReadOnlySpan castScopeIdSpan = MemoryMarshal.Cast(scopeIdSpan); + parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope); + } - parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope); - } + if (parsedNumericScope) + { + return true; + } + else + { + uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(scopeIdSpan); - if (parsedNumericScope) + if (interfaceIndex > 0) { - return true; + scope = interfaceIndex; + return true; // scopeId is a known interface name } - else - { - uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(scopeIdSpan); - - if (interfaceIndex > 0) - { - scope = interfaceIndex; - return true; // scopeId is a known interface name - } - } - - // scopeId is an unknown interface name } - // scopeId is not presented - return true; + // scopeId is an unknown interface name } - return false; + // scopeId is not presented + scope = 0; + return true; } internal static int FormatIPv4Address(uint address, Span addressString) diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs index 9331bf6b3b98c0..3e326de9236c9a 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs @@ -219,6 +219,8 @@ public abstract class IPAddressParsingFormatting [MemberData(nameof(ValidIpv4Addresses))] public void ParseIPv4_ValidAddress_Success(string address, string expected) { + TestIsValid(address, true); + IPAddress ip = Parse(address); // Validate the ToString of the parsed address matches the expected value @@ -443,6 +445,8 @@ public void ParseIPv4_InvalidAddress_ThrowsFormatExceptionWithInnerException(str [MemberData(nameof(ValidIpv6Addresses))] public void ParseIPv6_ValidAddress_RoundtripMatchesExpected(string address, string expected) { + TestIsValid(address, true); + IPAddress ip = Parse(address); // Validate the ToString of the parsed address matches the expected value @@ -467,6 +471,8 @@ public void ParseIPv6_ValidAddress_RoundtripMatchesExpected(string address, stri [MemberData(nameof(ValidIpv6Addresses))] public void TryParseIPv6_ValidAddress_RoundtripMatchesExpected(string address, string expected) { + TestIsValid(address, true); + Assert.True(TryParse(address, out IPAddress ip)); // Validate the ToString of the parsed address matches the expected value @@ -502,6 +508,7 @@ public void TryParseIPv6_ValidAddress_RoundtripMatchesExpected(string address, s [MemberData(nameof(ScopeIds))] public void ParseIPv6_ExtractsScopeId(string address, int expectedScopeId) { + TestIsValid(address, true); IPAddress ip = Parse(address); Assert.Equal(expectedScopeId, ip.ScopeId); } @@ -613,6 +620,8 @@ public void ParseIPv6_InvalidAddress_ThrowsFormatExceptionWithNoInnerExceptionIn private void ParseInvalidAddress(string invalidAddress, bool hasInnerSocketException) { + TestIsValid(invalidAddress, false); + FormatException fe = Assert.Throws(() => Parse(invalidAddress)); if (hasInnerSocketException) { @@ -628,5 +637,11 @@ private void ParseInvalidAddress(string invalidAddress, bool hasInnerSocketExcep Assert.False(TryParse(invalidAddress, out result)); Assert.Null(result); } + + private static void TestIsValid(string address, bool expectedValid) + { + Assert.Equal(expectedValid, IPAddress.IsValid(address)); + Assert.Equal(expectedValid, IPAddress.IsValidUtf8(Encoding.UTF8.GetBytes(address))); + } } } diff --git a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs index 32dc4cc2631925..07be627eedcba9 100644 --- a/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs +++ b/src/libraries/System.Net.Primitives/tests/UnitTests/Fakes/IPv6AddressHelper.cs @@ -11,7 +11,7 @@ internal static class IPv6AddressHelper internal static unsafe (int longestSequenceStart, int longestSequenceLength) FindCompressionRange( ReadOnlySpan numbers) => (-1, -1); internal static unsafe bool ShouldHaveIpv4Embedded(ReadOnlySpan numbers) => false; - internal static unsafe bool IsValidStrict(TChar* name, int start, ref int end) + internal static unsafe bool IsValidStrict(TChar* name, int start, int end) where TChar : unmanaged, IBinaryInteger => false; internal static unsafe bool Parse(ReadOnlySpan address, Span numbers, out ReadOnlySpan scopeId) where TChar : unmanaged, IBinaryInteger diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 840e1bdac7b0c1..95457e48584077 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -423,7 +423,7 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, // RFC 6066 forbids IP literals. // IDN mapping is handled by MsQuic. - string sni = (TargetHostNameHelper.IsValidAddress(options.ClientAuthenticationOptions.TargetHost) ? null : options.ClientAuthenticationOptions.TargetHost) ?? host ?? string.Empty; + string sni = (IPAddress.IsValid(options.ClientAuthenticationOptions.TargetHost) ? null : options.ClientAuthenticationOptions.TargetHost) ?? host ?? string.Empty; IntPtr targetHostPtr = Marshal.StringToCoTaskMemUTF8(sni); try @@ -458,7 +458,7 @@ internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, str _streamCapacityCallback = options.StreamCapacityCallback; // RFC 6066 forbids IP literals, avoid setting IP address here for consistency with SslStream - if (TargetHostNameHelper.IsValidAddress(targetHost)) + if (IPAddress.IsValid(targetHost)) { targetHost = string.Empty; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs index 0f141c0812f942..e958a482898f9e 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs @@ -255,7 +255,7 @@ private unsafe void InitializeSslContext( Interop.AndroidCrypto.SSLStreamRequestClientAuthentication(handle); } - if (!isServer && !string.IsNullOrEmpty(authOptions.TargetHost) && !TargetHostNameHelper.IsValidAddress(authOptions.TargetHost)) + if (!isServer && !string.IsNullOrEmpty(authOptions.TargetHost) && !IPAddress.IsValid(authOptions.TargetHost)) { Interop.AndroidCrypto.SSLStreamSetTargetHost(handle, authOptions.TargetHost); } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs index 7ff7b26e7a5e62..f1945c428363cd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs @@ -95,7 +95,7 @@ public SafeDeleteSslContext(SslAuthenticationOptions sslAuthenticationOptions) throw; } - if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !sslAuthenticationOptions.IsServer && !TargetHostNameHelper.IsValidAddress(sslAuthenticationOptions.TargetHost)) + if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !sslAuthenticationOptions.IsServer && !IPAddress.IsValid(sslAuthenticationOptions.TargetHost)) { Interop.AppleCrypto.SslSetTargetName(_sslContext, sslAuthenticationOptions.TargetHost); } diff --git a/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.Wasm.cs b/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.Wasm.cs index a2756eb6487396..26f47648d5c8f5 100644 --- a/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.Wasm.cs +++ b/src/libraries/System.Net.WebProxy/src/System/Net/WebProxy.Wasm.cs @@ -16,7 +16,7 @@ private static bool IsLocal(Uri host) string hostString = host.Host; return - !IPAddress.TryParse(hostString, out _) && + !IPAddress.IsValid(hostString) && !hostString.Contains('.'); } }