diff --git a/Common.sln b/Common.sln index 91849e5..96da917 100644 --- a/Common.sln +++ b/Common.sln @@ -37,6 +37,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Test", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Net", "src\Steeltoe.Common.Net\Steeltoe.Common.Net.csproj", "{5C2D53DB-2980-4393-BCA1-B4C51004E4F7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Security", "src\Steeltoe.Common.Security\Steeltoe.Common.Security.csproj", "{8D322AD5-ED49-4D40-952C-B38FD97D21EF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Security.Test", "test\Steeltoe.Common.Security.Test\Steeltoe.Common.Security.Test.csproj", "{6E2CBAC4-3FF1-4DC9-9B9B-D0ADFE80218D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,6 +75,14 @@ Global {5C2D53DB-2980-4393-BCA1-B4C51004E4F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C2D53DB-2980-4393-BCA1-B4C51004E4F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C2D53DB-2980-4393-BCA1-B4C51004E4F7}.Release|Any CPU.Build.0 = Release|Any CPU + {8D322AD5-ED49-4D40-952C-B38FD97D21EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D322AD5-ED49-4D40-952C-B38FD97D21EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D322AD5-ED49-4D40-952C-B38FD97D21EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D322AD5-ED49-4D40-952C-B38FD97D21EF}.Release|Any CPU.Build.0 = Release|Any CPU + {6E2CBAC4-3FF1-4DC9-9B9B-D0ADFE80218D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E2CBAC4-3FF1-4DC9-9B9B-D0ADFE80218D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E2CBAC4-3FF1-4DC9-9B9B-D0ADFE80218D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E2CBAC4-3FF1-4DC9-9B9B-D0ADFE80218D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -83,6 +95,8 @@ Global {7FF75564-423A-4EE6-B7AD-9D35C428757F} = {CC77ED1F-BC03-4B9D-A07A-186C9A13042B} {101DF01E-FDE9-4B97-9666-C3777CC308D0} = {CC77ED1F-BC03-4B9D-A07A-186C9A13042B} {5C2D53DB-2980-4393-BCA1-B4C51004E4F7} = {D9798FDE-76F4-4848-8AE0-95249C0101F0} + {8D322AD5-ED49-4D40-952C-B38FD97D21EF} = {D9798FDE-76F4-4848-8AE0-95249C0101F0} + {6E2CBAC4-3FF1-4DC9-9B9B-D0ADFE80218D} = {CC77ED1F-BC03-4B9D-A07A-186C9A13042B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4A85F9DA-2C2D-48E9-A28C-9B35C473C150} diff --git a/config/versions-dev.props b/config/versions-dev.props index cfe3ff2..3fca7cb 100644 Binary files a/config/versions-dev.props and b/config/versions-dev.props differ diff --git a/src/Steeltoe.Common.Security/CertificateOptions.cs b/src/Steeltoe.Common.Security/CertificateOptions.cs new file mode 100644 index 0000000..9f83038 --- /dev/null +++ b/src/Steeltoe.Common.Security/CertificateOptions.cs @@ -0,0 +1,25 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Security.Cryptography.X509Certificates; + +namespace Steeltoe.Common.Security +{ + public class CertificateOptions : ICertificateOptions + { + public string Name { get; set; } + + public X509Certificate2 Certificate { get; set; } + } +} diff --git a/src/Steeltoe.Common.Security/ICertificateOptions.cs b/src/Steeltoe.Common.Security/ICertificateOptions.cs new file mode 100644 index 0000000..74ab856 --- /dev/null +++ b/src/Steeltoe.Common.Security/ICertificateOptions.cs @@ -0,0 +1,25 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Security.Cryptography.X509Certificates; + +namespace Steeltoe.Common.Security +{ + public interface ICertificateOptions + { + string Name { get; } + + X509Certificate2 Certificate { get; } + } +} diff --git a/src/Steeltoe.Common.Security/PemCertificateProvider.cs b/src/Steeltoe.Common.Security/PemCertificateProvider.cs new file mode 100644 index 0000000..a32f50d --- /dev/null +++ b/src/Steeltoe.Common.Security/PemCertificateProvider.cs @@ -0,0 +1,77 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; + +namespace Steeltoe.Common.Security +{ + public class PemCertificateProvider : ConfigurationProvider + { + private IConfigurationRoot _certFileProvider; + private IConfigurationRoot _keyFileProvider; + + public PemCertificateProvider(IConfigurationRoot certFileProvider, IConfigurationRoot keyFileProvider) + { + _certFileProvider = certFileProvider; + _keyFileProvider = keyFileProvider; + _certFileProvider.GetReloadToken().RegisterChangeCallback(NotifyCertChanged, null); + _keyFileProvider.GetReloadToken().RegisterChangeCallback(NotifyKeyChanged, null); + } + + public override void Load() + { + } + + public override void Set(string key, string value) + { + throw new InvalidOperationException(); + } + + public override bool TryGet(string key, out string value) + { + value = _certFileProvider[key]; + if (!string.IsNullOrEmpty(value)) + { + return true; + } + + value = _keyFileProvider[key]; + if (!string.IsNullOrEmpty(value)) + { + return true; + } + + return false; + } + + public override IEnumerable GetChildKeys(IEnumerable earlierKeys, string parentPath) + { + return base.GetChildKeys(earlierKeys, parentPath); + } + + private void NotifyCertChanged(object state) + { + OnReload(); + _certFileProvider.GetReloadToken().RegisterChangeCallback(NotifyCertChanged, null); + } + + private void NotifyKeyChanged(object state) + { + OnReload(); + _keyFileProvider.GetReloadToken().RegisterChangeCallback(NotifyKeyChanged, null); + } + } +} diff --git a/src/Steeltoe.Common.Security/PemCertificateSource.cs b/src/Steeltoe.Common.Security/PemCertificateSource.cs new file mode 100644 index 0000000..e81bf4b --- /dev/null +++ b/src/Steeltoe.Common.Security/PemCertificateSource.cs @@ -0,0 +1,102 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Configuration; +using System.IO; + +namespace Steeltoe.Common.Security +{ + public class PemCertificateSource : IConfigurationSource + { + private string _certFilePath; + private string _keyFilePath; + + public PemCertificateSource(string certFilePath, string keyFilePath) + { + _certFilePath = Path.GetFullPath(certFilePath); + _keyFilePath = Path.GetFullPath(keyFilePath); + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + var certSource = new FileSource("certificate") + { + FileProvider = null, + Path = Path.GetFileName(_certFilePath), + Optional = false, + ReloadOnChange = true, + ReloadDelay = 1000 + }; + + var keySource = new FileSource("privateKey") + { + FileProvider = null, + Path = Path.GetFileName(_keyFilePath), + Optional = false, + ReloadOnChange = true, + ReloadDelay = 1000, + }; + + var certProvider = new ConfigurationBuilder() + .SetBasePath(Path.GetDirectoryName(_certFilePath)) + .Add(certSource) + .Build(); + + var keyProvider = new ConfigurationBuilder() + .SetBasePath(Path.GetDirectoryName(_keyFilePath)) + .Add(keySource) + .Build(); + + return new PemCertificateProvider(certProvider, keyProvider); + } + } + +#pragma warning disable SA1402 // File may only contain a single class + internal class FileSource : FileConfigurationSource + { + internal string Key { get; } + + public FileSource(string key) + : base() + { + Key = key; + } + + public override IConfigurationProvider Build(IConfigurationBuilder builder) + { + EnsureDefaults(builder); + return new FileProvider(this); + } + } + + internal class FileProvider : FileConfigurationProvider + { + public FileProvider(FileConfigurationSource source) + : base(source) + { + } + + public override void Load(Stream stream) + { + var source = Source as FileSource; + string key = source.Key; + using (var reader = new StreamReader(stream)) + { + string value = reader.ReadToEnd(); + Data[key] = value; + } + } + } +#pragma warning restore SA1402 // File may only contain a single class +} diff --git a/src/Steeltoe.Common.Security/PemConfigurationExtensions.cs b/src/Steeltoe.Common.Security/PemConfigurationExtensions.cs new file mode 100644 index 0000000..4befe4d --- /dev/null +++ b/src/Steeltoe.Common.Security/PemConfigurationExtensions.cs @@ -0,0 +1,44 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; +using System; + +namespace Steeltoe.Common.Security +{ + public static class PemConfigurationExtensions + { + public static IConfigurationBuilder AddPemFiles(this IConfigurationBuilder builder, string certFilePath, string keyFilePath) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (string.IsNullOrEmpty(certFilePath)) + { + throw new ArgumentException(nameof(certFilePath)); + } + + if (string.IsNullOrEmpty(keyFilePath)) + { + throw new ArgumentException(nameof(keyFilePath)); + } + + builder.Add(new PemCertificateSource(certFilePath, keyFilePath)); + return builder; + } + } +} diff --git a/src/Steeltoe.Common.Security/PemConfigureCertificateOptions.cs b/src/Steeltoe.Common.Security/PemConfigureCertificateOptions.cs new file mode 100644 index 0000000..9bee622 --- /dev/null +++ b/src/Steeltoe.Common.Security/PemConfigureCertificateOptions.cs @@ -0,0 +1,130 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.X509; +using System; +using System.IO; +using System.Text; +using MS = System.Security.Cryptography.X509Certificates; + +namespace Steeltoe.Common.Security +{ + public class PemConfigureCertificateOptions : IConfigureNamedOptions + { + private IConfiguration _config; + private ILogger _logger; + + public PemConfigureCertificateOptions(IConfiguration config, ILogger logger = null) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + _config = config; + _logger = logger; + } + + public void Configure(string name, CertificateOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Name = name; + + var pemCert = _config["certificate"]; + var pemKey = _config["privateKey"]; + + if (string.IsNullOrEmpty(pemCert) || string.IsNullOrEmpty(pemKey)) + { + return; + } + + var certBytes = Encoding.Default.GetBytes(pemCert); + var keyBytes = Encoding.Default.GetBytes(pemKey); + + X509Certificate cert = ReadCertificate(certBytes); + AsymmetricCipherKeyPair keys = ReadKeys(keyBytes); + + var pfxBytes = CreatePfxContainer(cert, keys); + options.Certificate = new MS.X509Certificate2(pfxBytes); + } + + public void Configure(CertificateOptions options) + { + Configure(Options.DefaultName, options); + } + + internal byte[] CreatePfxContainer(X509Certificate cert, AsymmetricCipherKeyPair keys) + { + var certEntry = new X509CertificateEntry(cert); + + var pkcs12Store = new Pkcs12StoreBuilder() + .SetUseDerEncoding(true) + .Build(); + var keyEntry = new AsymmetricKeyEntry(keys.Private); + pkcs12Store.SetKeyEntry("ServerInstance", keyEntry, new X509CertificateEntry[] { certEntry }); + + using (MemoryStream stream = new MemoryStream()) + { + pkcs12Store.Save(stream, null, new SecureRandom()); + var bytes = stream.ToArray(); + return Pkcs12Utilities.ConvertToDefiniteLength(bytes); + } + } + + internal AsymmetricCipherKeyPair ReadKeys(byte[] keyBytes) + { + try + { + using (var reader = new StreamReader(new MemoryStream(keyBytes))) + { + return new PemReader(reader).ReadObject() as AsymmetricCipherKeyPair; + } + } + catch (Exception e) + { + _logger?.LogError(e, "Unable to read PEM encoded keys"); + } + + return null; + } + + internal X509Certificate ReadCertificate(byte[] certBytes) + { + try + { + using (var reader = new StreamReader(new MemoryStream(certBytes))) + { + return new PemReader(reader).ReadObject() as X509Certificate; + } + } + catch (Exception e) + { + _logger?.LogError(e, "Unable to read PEM encoded certificate"); + } + + return null; + } + } +} diff --git a/src/Steeltoe.Common.Security/Steeltoe.Common.Security.csproj b/src/Steeltoe.Common.Security/Steeltoe.Common.Security.csproj new file mode 100644 index 0000000..6e2f92b --- /dev/null +++ b/src/Steeltoe.Common.Security/Steeltoe.Common.Security.csproj @@ -0,0 +1,44 @@ + + + + + + Steeltoe Common Security Library + $(SteeltoeVersion) + $(VersionSuffix) + Pivotal;dtillman + + netstandard2.0 + Steeltoe.Common.Security + Steeltoe.Common.Security + NET Core;NET Framework + https://steeltoe.io/images/transparent.png + https://steeltoe.io + http://www.apache.org/licenses/LICENSE-2.0 + + + bin\$(Configuration)\$(TargetFramework)\Steeltoe.Common.Security.xml + SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591 + + + + + + + + + + + + All + + + + + + stylecop.json + Always + + + + \ No newline at end of file diff --git a/src/Steeltoe.Common/LoadBalancer/ILoadBalancer.cs b/src/Steeltoe.Common/LoadBalancer/ILoadBalancer.cs index 44822a4..a633c15 100644 --- a/src/Steeltoe.Common/LoadBalancer/ILoadBalancer.cs +++ b/src/Steeltoe.Common/LoadBalancer/ILoadBalancer.cs @@ -19,8 +19,21 @@ namespace Steeltoe.Common.LoadBalancer { public interface ILoadBalancer { + /// + /// Evaluates a Uri for a host name that can be resolved into a service instance + /// + /// A Uri containing a service name that can be resolved into one or more service instances + /// The original Uri, with serviceName replaced by the host:port of a service instance Task ResolveServiceInstanceAsync(Uri request); + /// + /// A mechanism for tracking statistics for service instances + /// + /// The original request Uri + /// The Uri resolved by the load balancer + /// The amount of time taken for a remote call to complete + /// Any exception called during calls to a resolved service instance + /// A task Task UpdateStatsAsync(Uri originalUri, Uri resolvedUri, TimeSpan responseTime, Exception exception); } } diff --git a/src/Steeltoe.Common/Net/HostInfo.cs b/src/Steeltoe.Common/Net/HostInfo.cs new file mode 100644 index 0000000..72b5fb2 --- /dev/null +++ b/src/Steeltoe.Common/Net/HostInfo.cs @@ -0,0 +1,34 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Steeltoe.Common.Net +{ + public class HostInfo + { + public string Hostname { get; set; } + + public string IpAddress { get; set; } + + public bool Override { get; set; } + + public HostInfo() + { + } + + public HostInfo(string hostname) + { + Hostname = hostname; + } + } +} diff --git a/src/Steeltoe.Common/Net/INetOptions.cs b/src/Steeltoe.Common/Net/INetOptions.cs new file mode 100644 index 0000000..7ae16ee --- /dev/null +++ b/src/Steeltoe.Common/Net/INetOptions.cs @@ -0,0 +1,54 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; + +namespace Steeltoe.Common.Net +{ + public class InetOptions + { + public const string PREFIX = "spring:cloud:inet"; + + public string DefaultHostname { get; set; } = "localhost"; + + public string DefaultIpAddress { get; set; } = "127.0.0.1"; + + public string IgnoredInterfaces { get; set; } + + public bool UseOnlySiteLocalInterfaces { get; set; } = false; + + public string PreferredNetworks { get; set; } + + internal IEnumerable GetIgnoredInterfaces() + { + if (string.IsNullOrEmpty(IgnoredInterfaces)) + { + return new List(); + } + + return IgnoredInterfaces.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + internal IEnumerable GetPreferredNetworks() + { + if (string.IsNullOrEmpty(PreferredNetworks)) + { + return null; + } + + return PreferredNetworks.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + } +} diff --git a/src/Steeltoe.Common/Net/InetUtils.cs b/src/Steeltoe.Common/Net/InetUtils.cs new file mode 100644 index 0000000..b67d7fa --- /dev/null +++ b/src/Steeltoe.Common/Net/InetUtils.cs @@ -0,0 +1,269 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text.RegularExpressions; + +namespace Steeltoe.Common.Net +{ + public class InetUtils + { + private readonly InetOptions _options; + private readonly ILogger _logger; + + public InetUtils(InetOptions options, ILogger logger = null) + { + _options = options; + _logger = logger; + } + + public HostInfo FindFirstNonLoopbackHostInfo() + { + var address = FindFirstNonLoopbackAddress(); + if (address != null) + { + return ConvertAddress(address); + } + + HostInfo hostInfo = new HostInfo(); + hostInfo.Hostname = _options.DefaultHostname; + hostInfo.IpAddress = _options.DefaultIpAddress; + return hostInfo; + } + + public IPAddress FindFirstNonLoopbackAddress() + { + IPAddress result = null; + try + { + int lowest = int.MaxValue; + var ifaces = NetworkInterface.GetAllNetworkInterfaces(); + for (int i = 0; i < ifaces.Length; i++) + { + var ifc = ifaces[i]; + + if (ifc.OperationalStatus == OperationalStatus.Up && !ifc.IsReceiveOnly) + { + _logger?.LogTrace("Testing interface: {name}, {id}", ifc.Name, ifc.Id); + + var props = ifc.GetIPProperties(); + var ipprops = props.GetIPv4Properties(); + + if (ipprops.Index < lowest || result == null) + { + lowest = ipprops.Index; + } + else if (result != null) + { + continue; + } + + if (!IgnoreInterface(ifc.Name)) + { + foreach (var addressInfo in props.UnicastAddresses) + { + var address = addressInfo.Address; + if (IsInet4Address(address) + && !IsLoopbackAddress(address) + && IsPreferredAddress(address)) + { + _logger?.LogTrace("Found non-loopback interface: {name}", ifc.Name); + result = address; + } + } + } + } + } + } + catch (Exception ex) + { + _logger?.LogError(ex, "Cannot get first non-loopback address"); + } + + if (result != null) + { + return result; + } + + string localHost = GetHostAddress(); + if (!string.IsNullOrEmpty(localHost)) + { + return IPAddress.Parse(localHost); + } + + return null; + } + + internal bool IsInet4Address(IPAddress address) + { + return address.AddressFamily == AddressFamily.InterNetwork; + } + + internal bool IsLoopbackAddress(IPAddress address) + { + return IPAddress.IsLoopback(address); + } + + internal bool IsPreferredAddress(IPAddress address) + { + if (_options.UseOnlySiteLocalInterfaces) + { + bool siteLocalAddress = IsSiteLocalAddress(address); + if (!siteLocalAddress) + { + _logger?.LogTrace("Ignoring address: {address} ", address.ToString()); + } + + return siteLocalAddress; + } + + IEnumerable preferredNetworks = _options.GetPreferredNetworks(); + if (preferredNetworks == null) + { + return true; + } + + foreach (string regex in preferredNetworks) + { + string hostAddress = address.ToString(); + var matcher = new Regex(regex); + if (matcher.IsMatch(hostAddress) || hostAddress.StartsWith(regex)) + { + return true; + } + } + + _logger?.LogTrace("Ignoring address: {address}", address.ToString()); + return false; + } + + internal bool IgnoreInterface(string interfaceName) + { + if (string.IsNullOrEmpty(interfaceName)) + { + return false; + } + + foreach (string regex in _options.GetIgnoredInterfaces()) + { + var matcher = new Regex(regex); + if (matcher.IsMatch(interfaceName)) + { + _logger?.LogTrace("Ignoring interface: {name}", interfaceName); + return true; + } + } + + return false; + } + + internal HostInfo ConvertAddress(IPAddress address) + { + HostInfo hostInfo = new HostInfo(); + string hostname; + try + { + var hostEntry = Dns.GetHostEntry(address); + hostname = hostEntry.HostName; + } + catch (Exception e) + { + _logger?.LogInformation(e, "Cannot determine local hostname"); + hostname = "localhost"; + } + + hostInfo.Hostname = hostname; + hostInfo.IpAddress = address.ToString(); + return hostInfo; + } + + internal string ResolveHostAddress(string hostName) + { + string result = null; + try + { + var results = Dns.GetHostAddresses(hostName); + if (results != null && results.Length > 0) + { + foreach (var addr in results) + { + if (addr.AddressFamily.Equals(AddressFamily.InterNetwork)) + { + result = addr.ToString(); + break; + } + } + } + } + catch (Exception e) + { + _logger?.LogWarning(e, "Unable to resolve host address"); + } + + return result; + } + + internal string ResolveHostName() + { + string result = null; + try + { + result = Dns.GetHostName(); + if (!string.IsNullOrEmpty(result)) + { + var response = Dns.GetHostEntry(result); + if (response != null) + { + return response.HostName; + } + } + } + catch (Exception e) + { + _logger?.LogWarning(e, "Unable to resolve hostname"); + } + + return result; + } + + internal string GetHostName() + { + return ResolveHostName(); + } + + internal string GetHostAddress() + { + string hostName = GetHostName(); + if (!string.IsNullOrEmpty(hostName)) + { + return ResolveHostAddress(hostName); + } + + return null; + } + + internal bool IsSiteLocalAddress(IPAddress address) + { + string addr = address.ToString(); + return addr.StartsWith("10.") || + addr.StartsWith("172.16.") || + addr.StartsWith("192.168."); + } + } +} diff --git a/test/Steeltoe.Common.Security.Test/PemConfigurationExtensionsTest.cs b/test/Steeltoe.Common.Security.Test/PemConfigurationExtensionsTest.cs new file mode 100644 index 0000000..b5ad76f --- /dev/null +++ b/test/Steeltoe.Common.Security.Test/PemConfigurationExtensionsTest.cs @@ -0,0 +1,104 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.IO; +using System.Threading; +using Xunit; + +namespace Steeltoe.Common.Security.Test +{ + public class PemConfigurationExtensionsTest + { + [Fact] + public void AddPemFiles_ThrowsOnNulls() + { + Assert.Throws(() => PemConfigurationExtensions.AddPemFiles(null, null, null)); + Assert.Throws(() => PemConfigurationExtensions.AddPemFiles(new ConfigurationBuilder(), null, null)); + Assert.Throws(() => PemConfigurationExtensions.AddPemFiles(new ConfigurationBuilder(), "foobar", null)); + } + + [Fact] + public void AddPemFiles_ReadsFiles() + { + var config = new ConfigurationBuilder() + .AddPemFiles("instance.crt", "instance.key") + .Build(); + Assert.NotNull(config["certificate"]); + Assert.NotNull(config["privateKey"]); + } + + [Fact] + public void AddPemFiles_ReloadsOnChange() + { + var tempFile1 = CreateTempFile("cert"); + var tempFile2 = CreateTempFile("key"); + + var config = new ConfigurationBuilder() + .AddPemFiles(tempFile1, tempFile2) + .Build(); + + Assert.Equal("cert", config["certificate"]); + Assert.Equal("key", config["privateKey"]); + + File.WriteAllText(tempFile1, "cert2"); + Thread.Sleep(2000); + Assert.Equal("cert2", config["certificate"]); + Assert.Equal("key", config["privateKey"]); + } + + [Fact] + public void AddPemFiles_NotifiesOnChange() + { + var tempFile1 = CreateTempFile("cert"); + var tempFile2 = CreateTempFile("key"); + + var config = new ConfigurationBuilder() + .AddPemFiles(tempFile1, tempFile2) + .Build(); + + bool changeCalled = false; + var token = config.GetReloadToken(); + token.RegisterChangeCallback((o) => changeCalled = true, "state"); + Assert.Equal("cert", config["certificate"]); + Assert.Equal("key", config["privateKey"]); + + File.WriteAllText(tempFile1, "barfoo"); + Thread.Sleep(2000); + Assert.Equal("barfoo", config["certificate"]); + Assert.Equal("key", config["privateKey"]); + Assert.True(changeCalled); + + token = config.GetReloadToken(); + token.RegisterChangeCallback((o) => changeCalled = true, "state"); + + changeCalled = false; + File.WriteAllText(tempFile2, "barbar"); + Thread.Sleep(2000); + Assert.Equal("barfoo", config["certificate"]); + Assert.Equal("barbar", config["privateKey"]); + Assert.True(changeCalled); + } + + private static string CreateTempFile(string contents) + { + var tempFile = Path.GetTempFileName(); + File.WriteAllText(tempFile, contents); + return tempFile; + } + } +} diff --git a/test/Steeltoe.Common.Security.Test/PemConfigureCertificateOptionsTest.cs b/test/Steeltoe.Common.Security.Test/PemConfigureCertificateOptionsTest.cs new file mode 100644 index 0000000..cf5a509 --- /dev/null +++ b/test/Steeltoe.Common.Security.Test/PemConfigureCertificateOptionsTest.cs @@ -0,0 +1,39 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Steeltoe.Common.Security.Test +{ + public class PemConfigureCertificateOptionsTest + { + [Fact] + public void AddPemFiles_ReadsFiles_CreatesCertificate() + { + var config = new ConfigurationBuilder() + .AddPemFiles("instance.crt", "instance.key") + .Build(); + Assert.NotNull(config["certificate"]); + Assert.NotNull(config["privateKey"]); + var pemConfig = new PemConfigureCertificateOptions(config); + CertificateOptions opts = new CertificateOptions(); + pemConfig.Configure(opts); + Assert.NotNull(opts.Certificate); + Assert.Equal(Options.DefaultName, opts.Name); + Assert.True(opts.Certificate.HasPrivateKey); + } + } +} diff --git a/test/Steeltoe.Common.Security.Test/Steeltoe.Common.Security.Test.csproj b/test/Steeltoe.Common.Security.Test/Steeltoe.Common.Security.Test.csproj new file mode 100644 index 0000000..263a688 --- /dev/null +++ b/test/Steeltoe.Common.Security.Test/Steeltoe.Common.Security.Test.csproj @@ -0,0 +1,51 @@ + + + + + + netcoreapp2.0;netcoreapp2.1;net461 + + + + + Always + + + Always + + + PreserveNewest + + + + + SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591 + + + + + + + + + + + + + + + + All + + + + + + stylecop.json + Always + + + + + + diff --git a/test/Steeltoe.Common.Security.Test/instance.crt b/test/Steeltoe.Common.Security.Test/instance.crt new file mode 100644 index 0000000..36ef479 --- /dev/null +++ b/test/Steeltoe.Common.Security.Test/instance.crt @@ -0,0 +1,44 @@ +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgIQSrLsvLyESbpjMDWmkPr+0TANBgkqhkiG9w0BAQsFADAy +MTAwLgYDVQQDEydEaWVnbyBJbnN0YW5jZSBJZGVudGl0eSBJbnRlcm1lZGlhdGUg +Q0EwHhcNMTkwMjI0MjAxMzAwWhcNMTkwMjI1MjAxMzAwWjCByDGBnjA4BgNVBAsT +MW9yZ2FuaXphdGlvbjozOTBjYmI3Zi0wMWYyLTQ2MGUtOTViMi1jNjhmOGQ0YTk0 +YjgwMQYDVQQLEypzcGFjZTpjYzY4MmUyNi0yMTU1LTQ2NDktYjk0OC00ZjBkMDJm +ODg3ZGEwLwYDVQQLEyhhcHA6M2ZlYzJmYjktNmIzYi00Yzc1LTkxYTktY2YwNDRi +ZWIxOGU3MSUwIwYDVQQDExxjZmMyYmM0ZC1lODMxLTQ1ODgtNzY0My01NDQ0MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt2O/qEPoA0jQFrI23NRIzd3E +FhsNwuTpZ4DoSVIZJQsjG0gjzoj0/YZHp6sbNRlWTHz1eXJ714+0rkYvpYjOp3jo +EOJhsrx3Hs9eN0yDz+YqB+J3DSYt+t+nrae7wDBUAMFZXjqhrDhuFgANjsxF3Xtl +1REgcRIdW9LGydAEAGlyMKAGBmQf9zofYCR+4Sw96KhQXBOTJiI+9p9pw3ndptfB +hYCBdiifcHi6+e3nsOidqjrDP4PA6OfVMCxvzDkJLU2Bq1cjqHFomNB5Tc5AshyA +lvDOi2+xVWBX7Z0+UujlWBYMeQl+FSz4N1cPYQ3SSNCDadkZHx/0f+qvRGX5eQID +AQABo4GBMH8wDgYDVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggr +BgEFBQcDATAfBgNVHSMEGDAWgBTBVXXc35U/Jlb+VYWm3AsyiodCETAtBgNVHREE +JjAkghxjZmMyYmM0ZC1lODMxLTQ1ODgtNzY0My01NDQ0hwQK/0ilMA0GCSqGSIb3 +DQEBCwUAA4IBAQAYBwmgU99XJ/bFrkWYXKpb4PZClxbFk8eTGWuFQiREDtum13lU +uxsWGWETLewJ7xEQzoEhWTbhOu8Kc0qILc+/dgoE+ENl59quwpFiWJNAu8gJYLga +8JYaPL2RnCwx+7MBuAWkhNoX9fBWCrNo5NcJpyuaFJZPUCpmspDjQNf53ez/lnoV +8hgCdTcBRFJgmbInSHIyy//k9Tv+6wisdZUsQf7y6O5HByKNNwSFbaEfKrSY38gb +5lc9GBFsHxGgwJhy8y0y9ccMCHCMsEfKIJZPgJvyqjQqR3Y2AZiQCCexTi50f+lW +ul0sbc6nZtCbml31604CjeVYA4jBCV3i6+T6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTTCCAjWgAwIBAgIUYnMnLAdY1eziifHiE6IDh958SnwwDQYJKoZIhvcNAQEL +BQAwKjEoMCYGA1UEAxMfRGllZ28gSW5zdGFuY2UgSWRlbnRpdHkgUm9vdCBDQTAe +Fw0xODExMTIyMDE3MzVaFw0yMDExMTEyMDE3MzVaMDIxMDAuBgNVBAMTJ0RpZWdv +IEluc3RhbmNlIElkZW50aXR5IEludGVybWVkaWF0ZSBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKzttSjhIkimTDKEwCRWFeez/S/bFP23QIjHz+Y5 +dl7sdX8YXwdfWbqciS2EhJ6s1QCHwMToqLb6czRtzUz1TLdyMs9wuz9XzzgJHdNj +yIb9B5aSbRudhTzGIAkY6HtjSA9lSD7gn66YBt/G5b+YHeZtFzc+Ao+Su5zJZ8AD +7pWm0LP/q2Mahu+Pp3yx6dhkWV4MPDNJH6YBscQEZf08Hhew86b5adxGHmpR6ckU +CpZUGHNOtE386eY6MgoN273cEj0UL7BH+YHGEeKGVrft7QYd5T/k9YC+Z7vmE9kw +Ak5420Z7D3bg06y4qlguYHhqbxrlFW7HHjjrryc0Wfc7VRMCAwEAAaNjMGEwHQYD +VR0OBBYEFMFVddzflT8mVv5VhabcCzKKh0IRMA4GA1UdDwEB/wQEAwICBDAfBgNV +HSMEGDAWgBTFkeVheTtTImn4MXKxnOhKSBHcMzAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4IBAQBigPJqRSy13SP+vT9/dWGADoqTZSVE/JDvX4vbKuyg +mOmV4hA3RbDa0vVBzDaMXLMjA4H/RFMCA3j99bec0RZXQT0sCnaYbFIU5msUEiP7 +11TY8advyq5GnZaGTXaEGGkmbWTsPKlLulnawtPdEtUQwZxYnh1nc6O0LqmbniwK +zuHG478KmH8r779c6KCiwkA9K6OmMpT6wxJW0N88/tQ6EnJ2VSCGaJDhB+XbwJam +s+pAhGCIOOuCD3fPYlz0QQ2UN/QwRAKDV1QTzGqYBesdZTRZtnDp1q/lZd7ETt8U +iF9yZCtnfUN5lTLNs7ZCxy/3/I/tW90ssLVPY/+tfX/8 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/Steeltoe.Common.Security.Test/instance.key b/test/Steeltoe.Common.Security.Test/instance.key new file mode 100644 index 0000000..27cfe57 --- /dev/null +++ b/test/Steeltoe.Common.Security.Test/instance.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAt2O/qEPoA0jQFrI23NRIzd3EFhsNwuTpZ4DoSVIZJQsjG0gj +zoj0/YZHp6sbNRlWTHz1eXJ714+0rkYvpYjOp3joEOJhsrx3Hs9eN0yDz+YqB+J3 +DSYt+t+nrae7wDBUAMFZXjqhrDhuFgANjsxF3Xtl1REgcRIdW9LGydAEAGlyMKAG +BmQf9zofYCR+4Sw96KhQXBOTJiI+9p9pw3ndptfBhYCBdiifcHi6+e3nsOidqjrD +P4PA6OfVMCxvzDkJLU2Bq1cjqHFomNB5Tc5AshyAlvDOi2+xVWBX7Z0+UujlWBYM +eQl+FSz4N1cPYQ3SSNCDadkZHx/0f+qvRGX5eQIDAQABAoIBAQCK2Bh4+sCkC/KP +3GmxE3/zbR1SZzUqA0m7NVuod2HWK/Jua1XAvuxNLeb+SIuWzhIKYukvA8BDWee/ +sh/MwiFDpkR81AiH3CyLxRBd6a46LtZPlePwrqFNORuoXD/HqE9RKxHQR6+zxh2C +xpN9M6cJoq1cfVUEhmR36sLadIUzEVmGKVUhUv56kdrbwd3AEGrKkcWBl8N8ukCd +1qV4IU7REwPjZBbfXSequ5goSKzzMG9MbKXLAj51btkCnMtLzfHfY3sSyfQIYO62 +7KJouNvdg3omNlygBsAuj4hM5nlg9uzuJQLrNEwnqOZXVGvkTXurZ+CrAN13Q5qf +Xj4VyF0BAoGBAMkv+XKxH3Ck6KHsFENR3A2jb6UuvXHW77wzrN+pvXWPBZ0DhmxL +PII/9clBnH4+EzINei4uhs0f0OL8+d0tqoOT0Qz6vOzhFKY/2/Cjk1ifff9uI1i3 +BpLjfPJyywy4HiVqjrTuDZjpp4Fj5v8jrI5Eg6Z5Xg4q3+qeAHR22ahpAoGBAOla +c3r048V30LQf45Cg79uZaOtNNxCZwzcX0DZWqih0buwKfCEufhQ2ctpBbi9NeOt3 +9KEe2z5PT3B+uNzPBzkUGQ/6A/+aK9JVBEtNWqJI1nR6qMXB5t+d9iQrbIf56NHA +1o3e1sAtXOvkVgh0o/ojwR//N+VMzQaDdlIvbiaRAoGBAJETukK9fRmCoYqaLeZ5 +skBXedvYv53Gy6uga+oBgfCzCO43q4iOHH0kWD4fxRS3+KmgVFnXDTf/2GbG2/tl +wc8OGbLNYM1EZdqYtCZsHoXKxVYbevuvR9tGlkRTCR8L6hk7JNtNyppY64R/oQSd +GgKhX3n9jRiUTFHoTBWv2rb5AoGAbUTNjmXdwjm4oJ/OD4tMxaewWX5uqndV0hZ0 +iP1L8GWVCzJdrav3nb9hSJIa5kuAs8IX6tpoD2VT7XlpVvwahb/DfJe2B5pJqtPk +jt5J8nPo9+H35aJGWa+98nHjAEklnBKQZR5TsOmM+WiSYKM9pYPYiwMXSWgNGV+1 +qAZNrgECgYBoka0S/7jQt9TwQSRAagdmyBIo26JDQ6igqLB/lBrKgYAbqcTRmy2X +bQZH1aCB/3B/NmIsqlzGOl/Uz3V6hW9Ae9X7BrXU4ug7SRH5BSff/eBY2j9YCR3e +z9elUvEYUa59oaBtEJGXCOxjsypdjaR25LnW7/wVI3iBAiw6fAqlsw== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/test/Steeltoe.Common.Security.Test/xunit.runner.json b/test/Steeltoe.Common.Security.Test/xunit.runner.json new file mode 100644 index 0000000..4c62fd7 --- /dev/null +++ b/test/Steeltoe.Common.Security.Test/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "maxParallelThreads": 1, + "parallelizeTestCollections": false +} \ No newline at end of file diff --git a/test/Steeltoe.Common.Test/Net/InetUtilsTest.cs b/test/Steeltoe.Common.Test/Net/InetUtilsTest.cs new file mode 100644 index 0000000..851eb97 --- /dev/null +++ b/test/Steeltoe.Common.Test/Net/InetUtilsTest.cs @@ -0,0 +1,128 @@ +// Copyright 2017 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Net; +using Xunit; + +namespace Steeltoe.Common.Net.Test +{ + public class InetUtilsTest + { + [Fact] + public void TestGetFirstNonLoopbackHostInfo() + { + InetUtils utils = new InetUtils(new InetOptions()); + Assert.NotNull(utils.FindFirstNonLoopbackHostInfo()); + } + + [Fact] + public void TestGetFirstNonLoopbackAddress() + { + InetUtils utils = new InetUtils(new InetOptions()); + Assert.NotNull(utils.FindFirstNonLoopbackAddress()); + } + + [Fact] + public void TestConvert() + { + InetUtils utils = new InetUtils(new InetOptions()); + Assert.NotNull(utils.ConvertAddress(Dns.GetHostEntry("localhost").AddressList[0])); + } + + [Fact] + public void TestHostInfo() + { + InetUtils utils = new InetUtils(new InetOptions()); + HostInfo info = utils.FindFirstNonLoopbackHostInfo(); + Assert.NotNull(info.IpAddress); + } + + [Fact] + public void TestIgnoreInterface() + { + InetOptions properties = new InetOptions() + { + IgnoredInterfaces = "docker0,veth.*" + }; + + InetUtils inetUtils = new InetUtils(properties); + + Assert.True(inetUtils.IgnoreInterface("docker0")); + Assert.True(inetUtils.IgnoreInterface("vethAQI2QT")); + Assert.False(inetUtils.IgnoreInterface("docker1")); + } + + [Fact] + public void TestDefaultIgnoreInterface() + { + InetUtils inetUtils = new InetUtils(new InetOptions()); + Assert.False(inetUtils.IgnoreInterface("docker0")); + } + + [Fact] + public void TestSiteLocalAddresses() + { + InetOptions properties = new InetOptions() + { + UseOnlySiteLocalInterfaces = true + }; + + InetUtils utils = new InetUtils(properties); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("192.168.0.1"))); + Assert.False(utils.IsPreferredAddress(IPAddress.Parse("5.5.8.1"))); + } + + [Fact] + public void TestPreferredNetworksRegex() + { + InetOptions properties = new InetOptions() + { + PreferredNetworks = "192.168.*,10.0.*" + }; + + InetUtils utils = new InetUtils(properties); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("192.168.0.1"))); + Assert.False(utils.IsPreferredAddress(IPAddress.Parse("5.5.8.1"))); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("10.0.10.1"))); + Assert.False(utils.IsPreferredAddress(IPAddress.Parse("10.255.10.1"))); + } + + [Fact] + public void TestPreferredNetworksSimple() + { + InetOptions properties = new InetOptions() + { + PreferredNetworks = "192,10.0" + }; + + InetUtils utils = new InetUtils(properties); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("192.168.0.1"))); + Assert.False(utils.IsPreferredAddress(IPAddress.Parse("5.5.8.1"))); + Assert.False(utils.IsPreferredAddress(IPAddress.Parse("10.255.10.1"))); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("10.0.10.1"))); + } + + [Fact] + public void TestPreferredNetworksListIsEmpty() + { + InetOptions properties = new InetOptions(); + + InetUtils utils = new InetUtils(properties); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("192.168.0.1"))); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("5.5.8.1"))); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("10.255.10.1"))); + Assert.True(utils.IsPreferredAddress(IPAddress.Parse("10.0.10.1"))); + } + } +}