//----------------------------------------------------------------------------- // // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). // You may not use this file except in compliance with the License. // A copy of the License is located at // // http://aws.amazon.com/apache2.0 // // or in the "license" file accompanying this file. This file 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.Linq; using System.Net; using System.Net.Sockets; using System.Threading; using Amazon.Runtime.Internal.Util; namespace Amazon.XRay.Recorder.Core.Internal.Utils { /// /// Represents a endpoint on some network. /// The represented endpoint is identified by a hostname. /// /// Internally resolves and caches an ip for the hostname. /// The ip is cached to keep the normal path speedy and non-blocking. /// public class HostEndPoint { private readonly int _cacheTtl; //Seconds to consider a cached dns response valid private IPEndPoint _ipCache; private DateTime? _timestampOfLastIPCacheUpdate; private static readonly Logger _logger = Logger.GetLogger(typeof(HostEndPoint)); private readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim(); /// /// Create a HostEndPoint. /// /// /// /// public HostEndPoint(string host, int port, int cacheTtl = 60) { if (host == "") { _logger.InfoFormat("Warning: Using an empty host will always resolve to the address of the local host."); } Host = host; Port = port; _cacheTtl = cacheTtl; } /// /// Get the hostname that identifies the endpoint. /// public string Host { get; } /// /// Get the port of the endpoint. /// public int Port { get; } /// /// Check to see if the cache is valid. /// A lock with at least read access MUST be held when calling this method! /// /// true if the cache is valid, false otherwise. private CacheState IPCacheIsValid() { if (_ipCache == null) { return CacheState.Invalid; } if (!(_timestampOfLastIPCacheUpdate is DateTime lastTimestamp)) { return CacheState.Invalid; } if (DateTime.Now.Subtract(lastTimestamp).TotalSeconds < _cacheTtl) { return CacheState.Valid; } return CacheState.Invalid; } /// /// Checks to see if the cache is valid. /// This method is essentially a wrapper around that acquires the required lock. /// /// true if the cache is valid, false otherwise. private CacheState LockedIPCacheIsValid() { // If !entered => another thread holds a write lock, i.e. updating the cache if (!cacheLock.TryEnterReadLock(0)) return CacheState.Updating; try { return IPCacheIsValid(); } finally { cacheLock.ExitReadLock(); } } /// /// Returns a cached ip resolved from the hostname. /// If the cached address is invalid this method will try to update it. /// The IP address returned is never guaranteed to be valid. /// An IP address may be invalid to due to several factors, including but not limited to: /// * DNS record is incorrect, /// * DNS record might have changed since last update. /// The returned IPEndPoint may also be null if no cache update has been successful. /// /// set to true if an update was performed, false otherwise /// the cached IPEndPoint, may be null public IPEndPoint GetIPEndPoint(out bool updatePerformed) { // LockedIPCacheIsValid and UpdateCache will in unison perform // a double checked locked to ensure: // 1. UpdateCache only is called when it appears that the cache is invalid // 2. The cache will not be updated when not necessary if (LockedIPCacheIsValid() == CacheState.Invalid) { updatePerformed = UpdateCache(); } else { updatePerformed = false; } cacheLock.EnterReadLock(); try { return _ipCache; } finally { cacheLock.ExitReadLock(); } } /// /// Updates the cache if invalid. /// Utilises an upgradable read lock, meaning only one thread at a time can enter this method. /// private bool UpdateCache() { if (!cacheLock.TryEnterUpgradeableReadLock(0)) { // Another thread is already performing an update => bail and use potentially dirty cache return false; } try { // We hold a UpgradableReadLock so when may call IPCacheIsValid if (IPCacheIsValid() != CacheState.Invalid) { // Cache no longer invalid, i.e. another thread performed the update after us seeing it invalid // and before now. return false; } // We have confirmed that the cache still is invalid and needs updating. // We know that we are the only ones that may update it because we hold an UpgradeableReadLock // Only one thread may hold such lock at a time, see: // https://docs.microsoft.com/en-gb/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.7.2#remarks var ipEntries = Dns.GetHostAddresses(Host); var newIP = ipEntries.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork); if (newIP == null) { _logger.InfoFormat( "IP cache invalid: DNS responded with no IP addresses for {1}. Cached IP address not updated!.", _ipCache, Host); return false; } // Upgrade our read lock to write mode cacheLock.EnterWriteLock(); try { _timestampOfLastIPCacheUpdate = DateTime.Now; _ipCache = new IPEndPoint(newIP, Port); _logger.InfoFormat("IP cache invalid: updated ip cache for {0} to {1}.", Host, newIP); return true; } //Error catching for IPEndPoint creation catch (ArgumentNullException) { _logger.InfoFormat("IP cache invalid: resolved IP address for hostname {0} null. Cached IP address not updated!", Host); } catch (ArgumentOutOfRangeException) { _logger.InfoFormat("IP cache invalid: either the port {0} or IP address {1} is out of range. Cached IP address not updated!", Port, newIP); } finally { // Downgrade back to read mode cacheLock.ExitWriteLock(); } } //Error catching for DNS resolve catch (ArgumentNullException) { _logger.InfoFormat( "IP cache invalid: failed to resolve DNS due to host being null. Cached IP address not updated!"); } catch (ArgumentOutOfRangeException) { _logger.InfoFormat( "IP cache invalid: failed to resolve DNS due to host being longer than 255 characters. ({0}) Cached IP address not updated!", Host); } catch (SocketException) { _logger.InfoFormat("IP cache invalid: failed to resolve DNS. ({0}) Cached IP address not updated!", Host); } catch (ArgumentException) { _logger.InfoFormat("IP cache invalid: failed to update cache due to {0} not being a valid IP. Cached IP address not updated!", Host); } finally { cacheLock.ExitUpgradeableReadLock(); } return false; } private enum CacheState { Valid, Invalid, Updating } } }