Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Amazon AWS Route 53 Dynamic IP Updater Windows Service

5.00/5 (2 votes)
19 Dec 2015GPL39 min read 27.1K   441  
Automatically update your dynamic IP address, supports multiple DNS Providers and IpCheckers. Project was started to update Amazon AWS Route 53.

Introduction

I have been a long term paid customer for DynDns but Amazon Route 53 has better price and more flexibile monthly billing, not to mention I am migrating the VMs to AWS VPC/EC2 which triggers me to make the switch.  Route 53 is great but the problem is that I cannot find any Route 53 windows service client to update IP automatically,  I still want to access my windows based servers at home in a realiable way, but not relying on those free IP/DNS service. This is why I started this project. 

Background

Initially the project was written for Aamzon Route 53 but the architecture and design is flexible enough for anyone to implement other DNS update providers. Visual Studio 2013 with .NET 4.5.0 is needed. 

The key features are:

  • Multiple DNS providers, multiple domains update, not limited to Amazon Route 53 and you can implement other providers easily such as (DynDns, No-IP, etc.)
  • Multiple IP checkers, you can run your own IP checker on your ISP or Amazon VPC/EC2. When one fails it automatically goes to next one
  • Custom XML configuration for easy configuration
  • Detailed Logging for all activities (not limited to File logging), you can configure to write to Event Log easily with no code change (Microsoft Enterprise Library knowledge needed)
  • Notification by email (Gmail SMTP tested) for any updated made 
  • Password encryption by Des3. This can be considered a weakness as symmetric encryption is not secure
  • Monitoring of DNS provider status change. DNS provider such as Rotute 53 update usually takes up to several minutes to propagate for all their DNS servers. AWS provides an API for checking the change status with a given ID. To ensure a change is successful, PENDING would be changed to INSYNC for completion. 
  • Force Update in x days
  • Runs as Windows Service using Open Source TopShelf project
  • No user interface :)             

Prerequisites

Visual Studio 2013 and .NET 4.5.0 was used.  The following packages are needed from NuGet restore:

  • Microsoft Enterprise Library 6.0.1304
  • Microsoft Unity 3.5.1404
  • AWS SDK 2.3.19
  • Topshelf 3.1.4

Architecture and Design

First of all, let's talk about the configuration. To support multiple domains, multiple providers and multiple IpCheckers,  the default App.config key-value pair configuration is not flexible enough, therefore we have a custom XmlConfig.

XML Configuration

XML
<xmlconfig>
  <domains>
    <domain>
      <domainname>home.yourdomain1.com</domainname>
      <providertype>AMAZON_ROUTE_53</providertype>
      <hostedzoneid>ZQ455PJ32KNK8GI2A</hostedzoneid>
      <accessid>AJUGYGYSNHYQHSJFOB</accessid>
      <secretkey>jFjF34lazdJFjdjm24FHsdjfg</secretkey>
      <minimalupdateintervalinminutes>5</minimalupdateintervalinminutes>
      <lastipaddress>1.1.1.1</lastipaddress>
      <lastupdateddatetime>2015-02-21T19:29:12.8381639Z</lastupdateddatetime>
      <changestatusid></changestatusid>
    </domain>
    <domain>
      <domainname>home.yourdomain2.com</domainname>
      <providertype>AMAZON_ROUTE_53</providertype>
      <hostedzoneid>ZJFD34IIFJ52SIF</hostedzoneid>
      <accessid>AJUGYGYSNHYQHSJFOB</accessid>
      <secretkey>jFjF34lazdJFjdjm24FHsdjfg</secretkey>
      <minimalupdateintervalinminutes>5</minimalupdateintervalinminutes>
      <lastipaddress>1.1.1.1</lastipaddress>
      <lastupdateddatetime>2015-02-21T19:29:12.8381639Z</lastupdateddatetime>
      <changestatusid></changestatusid>
    </domain>
  </domains>
    
  <providers>
    <provider>
      <providertype>AMAZON_ROUTE_53</providertype>
      <providerurl>https://route53.amazonaws.com</providerurl>
    </provider>
  </providers>

  <ipcheckers>
    <ipchecker>
      <ipcheckertype>CUSTOM</ipcheckertype>
      <clienttype>WEB_HTTP</clienttype>
      <ipcheckerurl>http://ec2.yourdomain1.com/</ipcheckerurl>
    </ipchecker>
    <ipchecker>
      <ipcheckertype>JSON_IP</ipcheckertype>
      <clienttype>WEB_HTTP</clienttype>
      <ipcheckerurl>http://www.jsonip.com/</ipcheckerurl>
    </ipchecker>
    <ipchecker>
      <ipcheckertype>DYN_DNS</ipcheckertype>
      <clienttype>WEB_HTTP</clienttype>
      <ipcheckerurl>http://checkip.dyndns.com/</ipcheckerurl>
    </ipchecker>
  </ipcheckers>
</xmlconfig>

 

To understand the design, you need the basic understanding of Dependency Injection (DI) and Inversion of control (IoC)  in software engineering.

Dependecy Injection with Unity IoC container

Because the configuration is so flexible, we cannot create the instance of the object until finish reading the XmlConfig during runtime,  DI is absolutely needed. Microsoft Unity IoC container is used to resolve the instance from different interfaces.

For the interfaces, we have "IDnsProvider", "IIpAddressChecker" and "IClient". 

IDnsProviders can be AmazonRoute53DnsProvider,  DynDnsProvider, NoIpDnsProvider, etc.
IIPAddressChecker can be CustomIpAddressChecker, DynDnsIpAddressChecker, JsonIpAddressChecker, etc.
IClient can be WebHttpClient, WebSslClient, WebSoapClient, etc.  (for IpAddress Checker to reuse)

C#
public interface IDnsProvider
{
    string UpdateDns(string accessID, string secretKey, string providerUrl, string domainName, string hostZoneId, string newIPaddress);
    Meta.Enum.ChangeStatusType CheckUpdateStatus(string accessID, string secretKey, string providerUrl, string id);
}
C#
public interface IIpAddressChecker
{       
      string GetCurrentIpAddress(string IpProviderURL, IClient client);     
}
C#
public interface IClient
{
     string GetContent(string IpProviderUrl, DelegateParser parser);
}


Serialization and Object Mappings

To use the configuration,  we perform the deserialize the XML  into objects, but then flexibilty is needed to use it as in-memory objects contains all the child instances.  The goal is to have the workable objects that are decoupled  from the deserialized objects.   In this case, we make it simple by mapping the XmlConfig to 2 new models: "DomainModel" and "IpCheckerModel".  

In Multi-tier application, we often use open source AutoMapper for Data Transfer Object (DTO) but for this project, it is a bit overkill, so manual mapping was done.  

C#
public class DomainModel
{
  public string DomainName { get; set; }
  public Meta.Enum.DnsProviderType DnsProviderType { get; set; }
  public IDnsProvider DnsProvider { get; set; }
  public string ProviderUrl { get; set; }     // Flatten as one of the properties
  public string HostedZoneId { get; set; }
  public string AccessID { get; set; }
  public string SecretKey { get; set; }
  public string MinimalUpdateIntervalInMinutes { get; set; }
  public string LastIpAddress { get; set; }
  public DateTime LastUpdatedDateTime { get; set; }
  public string ChangeStatusID { get; set; }     
}
C#
public  class IpCheckerModel
{
  public Meta.Enum.IpCheckerType IpCheckerType { get; set; }
  public Meta.Enum.ClientType ClientType { get; set; }
  public string IpCheckerUrl { get; set; }
  public IClient Client { get; set; }
  public IIpAddressChecker IpAddressChecker { get; set; }
}

Run time resolving DnsProvider instance from XmlConfig and maps to DomainModel. Similar is done for IpChecker based on the configured type. I assume you got the idea already.

C#
//.......
//.......

// Load Domains from Config, Resolve the DnsProvider instance based on configuration
_domainModelList = new List<DomainModel>();
foreach (Domain domain in xmlConfig.Domains)
{
    DomainModel domainModel = new DomainModel();
    domainModel.DomainName = domain.DomainName;
    domainModel.DnsProviderType = domain.ProviderType;
    // Resolve DnsProvider
    domainModel.DnsProvider = _container.Resolve<IDnsProvider>(domain.ProviderType.ToString());  
    // Find the matching Provider (XmlConfig object) and get the URL, flatten it for this Model                        
    domainModel.ProviderUrl = ((Provider)xmlConfig.Providers.FirstOrDefault(x => x.ProviderType == domain.ProviderType)).ProviderUrl;  
    domainModel.HostedZoneId = domain.HostedZoneId;
    domainModel.AccessID = domain.AccessID;

    // Decrypt the string if it the data is encrypted
    if (ConfigHelper.EnablePasswordEncryption)
        domainModel.SecretKey = Des3.Decrypt(domain.SecretKey, ConfigHelper.EncryptionKey);
    else
        domainModel.SecretKey = domain.SecretKey;
               
    domainModel.MinimalUpdateIntervalInMinutes = domain.MinimalUpdateIntervalInMinutes;
    domainModel.LastIpAddress = domain.LastIpAddress;
    domainModel.LastUpdatedDateTime = domain.LastUpdatedDateTime;

    if (domainModel.DnsProvider == null)
    {
        Logger.Write("Cannot resolve Provider, misconfiguration in XML config file in Domain section.", Meta.Enum.LogCategoryType.CONFIGURATION.ToString());
        throw new ArgumentNullException();
    }

    if (String.IsNullOrWhiteSpace(domain.ChangeStatusID))
        domainModel.ChangeStatusID = null;
    else
        domainModel.ChangeStatusID = domain.ChangeStatusID;

    _domainModelList.Add(domainModel);
}

// Load IpCheckers from Config, Resolve the instance
_ipCheckerModelList = new List<IpCheckerModel>();
foreach (IpChecker ipChecker in xmlConfig.IpCheckers)
{
    // Mapping from Config to Model
    IpCheckerModel model = new IpCheckerModel();
    model.IpCheckerType = ipChecker.IpCheckerType;
    model.IpCheckerUrl = ipChecker.IpCheckerUrl;
    model.ClientType = ipChecker.ClientType;
    // Resolve Client
    model.Client = _container.Resolve<IClient>(ipChecker.ClientType.ToString());      
    // Resolve IpAddressChecker
    model.IpAddressChecker = _container.Resolve<IIpAddressChecker>(ipChecker.IpCheckerType.ToString());  

    if ((model.Client == null) || (model.IpAddressChecker == null))
    {
        Logger.Write("Cannot resolve Checker or Client, misconfiguration in XML config file in IpChecker section.", Meta.Enum.LogCategoryType.CONFIGURATION.ToString());
        throw new ArgumentNullException();
    }

    _ipCheckerModelList.Add(model);
}

//.......
//.......

XPath vs Re-serialization (Debatable)

When the update operation is completed, we need to update the XML file, information includes    LastUpdatedDateTime, LastIpAddress  and ChangeStatusID are updated using XPath. But it is debatable as you can re-serialize the XML and save the whole document.             

C#
/// <summary>
/// Update LastIpAddress +  LastUpdatedDateTime in XmlConfig using XPath  (Called by Update Dns)
/// </summary>
/// <param name="domainName"></param>
/// <param name="dateTimeinUTC"></param>
/// <returns></returns>
public static bool UpdateLastUpdatedInformation(string domainName, string ipAddress, DateTime dateTimeinUTC)
{
    // Use Xpath to update (debatable - not deserialize the xml?)
    XmlDocument xmlDocument = new XmlDocument();
    xmlDocument.Load(AppDomain.CurrentDomain.BaseDirectory + XmlConfigFileName);
    XmlNode root = xmlDocument.DocumentElement;

    // Find the matching domain name using Xpath and update the datetime
    XmlNode node = root.SelectSingleNode("//Domains/Domain[DomainName=\"" + domainName + "\"]");

    if (node != null)
    {
        node["LastIpAddress"].InnerText = ipAddress;
        node["LastUpdatedDateTime"].InnerText = dateTimeinUTC.ToString("o");    // UTC timestamp in ISO 8601 format
           
        // Need to use this to fix carriage return problem if InnerText is an empty string
        XmlWriterSettings settings = new XmlWriterSettings { Indent = true };               
        using (XmlWriter writer = XmlWriter.Create(AppDomain.CurrentDomain.BaseDirectory + XmlConfigFileName, settings))
        {
            xmlDocument.Save(writer);
        }


        return true;
    }
    else
        return false;
}

Implementation of DnsProviders and IpCheckers

Here comes the actual implementation of the core dns updating and ip checking function. In the future, say Microsoft Azure launches a new DNS service, you can implement MicrosoftAzureProvider : IDnsProvider easily. 

Let's focus on Route 53 for now, AWS SDK has some good example online for updating DNS,  the only problem is that they use an infinite WHILE loop and SLEEP to check the Status which I do not quite agree using in real production despite we know that the documentation is for educational purpose only.

Ref: http://docs.aws.amazon.com/AWSSdkDocsNET/latest/DeveloperGuide/route53-recordset.html

Therefore we have another method for CheckUpdateStatus which is called by a separated Timer. AmazonRoute53Provider class is designed to perform both DNS update as well as checking the status. Here is the implementation:

C#
public class AmazonRoute53DnsProvider : IDnsProvider
{
  // ........
  // ........
  // ... Constructor/variables not shown here (download source code)
  // ........
  // ........

public string UpdateDns(string accessID, string secretKey, string providerUrl, string domainName, string hostZoneId, string newIPaddress)
{

    string changeRequestId = null;
            
    // Assign parameters
    _accessID = accessID;
    _secretKey = secretKey;
    _providerUrl = providerUrl;
            
    // Create a resource record set change batch
    ResourceRecordSet recordSet = new ResourceRecordSet()
    {
        Name = domainName,
        TTL = 60,
        Type = RRType.A,
        ResourceRecords = new List<ResourceRecord> { new ResourceRecord { Value = newIPaddress } }
    };

    Change change1 = new Change()
    {
        ResourceRecordSet = recordSet,
        Action = ChangeAction.UPSERT        // Note: UPSERT is used
    };

    ChangeBatch changeBatch = new ChangeBatch()
    {
        Changes = new List<Change> { change1 }
    };

    // Update the zone's resource record sets
    ChangeResourceRecordSetsRequest recordsetRequest = new ChangeResourceRecordSetsRequest()
    {
        HostedZoneId = hostZoneId,
        ChangeBatch = changeBatch
    };

    ChangeResourceRecordSetsResponse recordsetResponse = AmazonRoute53Client.ChangeResourceRecordSets(recordsetRequest);
    changeRequestId = recordsetResponse.ChangeInfo.Id;

    return changeRequestId;

} // method



/// <summary>
///  AmazonRoute53 takes several minutes to propagate through all the DNS servers, Status is Pending after submit
/// </summary>
/// <param name="id"></param>
public Meta.Enum.ChangeStatusType CheckUpdateStatus(string accessID, string secretKey, string providerUrl, string id)
{
    if (String.IsNullOrEmpty(id))
        return Meta.Enum.ChangeStatusType.INSYNC;

    // Assign parameters
    _accessID = accessID;
    _secretKey = secretKey;
    _providerUrl = providerUrl;

    // Monitor the change status
    GetChangeRequest changeRequest = new GetChangeRequest(id);

    if (AmazonRoute53Client.GetChange(changeRequest).ChangeInfo.Status == ChangeStatus.PENDING)
        return Meta.Enum.ChangeStatusType.PENDING;
    else
        return Meta.Enum.ChangeStatusType.INSYNC;        
            
} // method
        
}

As for IpChecker, each IpAddressChecker can return a different format, so we need a parser to parse the HTML for checkip.dyndns.com or a Json parser to parse results from JsonIp.com. To reuse IClient, we pass the Parser as Delegate.

C#
/// <summary>
/// Web Http Client with Delegate Parser
/// </summary>
public class WebHttpClient : IClient
{
    /// <summary>
    /// Get the content of the Html as string
    /// </summary>
    /// <param name="IpProviderUrl"></param>
    /// <returns></returns>
    public string GetContent(string IpProviderUrl, DelegateParser parser)
    {
        string content = null;
        int timeoutInMilliSeconds = Convert.ToInt32(ConfigHelper.ClientTimeoutInMinutes) * 60 *1000;

        // Use IDisposable webclient to get the page of content of existing IP
        using (TimeoutWebClient client = new TimeoutWebClient((timeoutInMilliSeconds)))
        {
            content = client.DownloadString(IpProviderUrl);
        }

        if (content != null)
            return parser(content);
        else
            return null;
    }

    /// <summary>
    /// To support timeout value, alternatively you can use HttpWebRequest and Stream
    /// </summary>
    public class TimeoutWebClient : WebClient
    {
        public int Timeout { get; set; }

        public TimeoutWebClient(int timeout)
        {
            Timeout = timeout;
        }

        protected override WebRequest GetWebRequest(Uri address)
        {
            WebRequest request = base.GetWebRequest(address);
            request.Timeout = Timeout;
            return request;
        }
    }


}

JsonIp.com is a great free service and it is very fast, it returns your current IP in JSON format, so we need to parse it. This the implementation:

C#
public class JsonIpAddressChecker : IIpAddressChecker
{

    public string GetCurrentIpAddress(string ipProviderURL, IClient client)
    {
        // Pass the parser as function to the client
        DelegateParser handler = Parse;
        return client.GetContent(ipProviderURL, handler);

    }

    private string Parse(string jsonString)
    {
        string ipString = null;

        // format: {"ip":"x.x.x.x","about":"/about","Pro!":"http://getjsonip.com"}

        var jsonSerializer = new JavaScriptSerializer();
        Dictionary<string, string> data = jsonSerializer.Deserialize<Dictionary<string, string>>(jsonString);
        ipString = data["ip"];

        // Validate if this is a valid IPV4 address
        if (IpHelper.IpAddressV4Validator(ipString))
            return ipString;
        else
            return null;
    }

}

public class DynDnsIpAddressChecker : IIpAddressChecker
{
  // Implementation (download source code)
}

public class CustomIpAddressChecker : IIpAddressChecker
{
  // Implementation (download source code)
}

If you have Amazon VPC/EC2 with IIS or you have a favorite ISP which runs traditional ASPX page, you can simply use the CustomIpAddressChecker and put this ASPX code as Default.aspx:

C#
<%@ Page Language="C#" %>
<%
// In case your server is behind proxy
string ip = Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

// If not, then use the traditional remote_addr
if (ip == null) 
    ip = Request.ServerVariables["REMOTE_ADDR"];
Response.Write(ip);
%>


Time based Update and Monitoring Workflow

Amazon Route 53 is dfferent from other DNS updater provider, after you call the Route 53 API for an IP update, the status for that domain/sub-domain changes to PENDING and a change status ID is returned. It takes up to several minutes to propagate for all their DNS servers, but it is acceptable for updating home IP address.  Route 53 provides another API you can call to check the update status. When  the operation is completed, it returns INSYNC.

To perform a time-based update, either Thread or Timer can be used. Since an update can happen after another, there is no need to implement  concurrent threading. Two seperated Timers are used:

  1. First timer (For Update) runs every 5 min (default) to check the for update (calling IPCheckers and Parse the IP from the result), if it is less than MinimalUpdateIntervalInMinutes (per domain settings to prevent abuse) or IP has not been changed then continues.  Otherwise, calls the Dns Provider to perform an update to the current IP, then updates the in-memory objects as well as XmlConfig.xml for the LastIpAddress, LastUpdatedDateTime and ChangeStatusID.  Repeat for all the domains in the configurations.  
     
  2. Second timer (For Monitoring)  runs every 1 minute (default)  to check if the there is any ChangeStatusID needs to be monitored for all the domains.  When DNS Provider API returns INSYNC  (using Amazon terminology),  ChangeStatusID is set to empty and an email notification is sent out.

We define the core "Worker" class which contains the 2 timers, and we register all the type mappings in the Unity container. Then resolves them after reading the config (as explained above already).
 

C#
public class Worker
{
    // Timer for interval update and monitor status
    private Timer _updateTimer;
    private Timer _montiorTimer;

    // IoC for flexbile configuration
    private IUnityContainer _container = new UnityContainer();

    // XmlConfig (XML serialization objects) maps to Model Collection
    private List<IpCheckerModel> _ipCheckerModelList;
    private List<DomainModel> _domainModelList;


    /// <summary>
    /// Constructor
    /// </summary>
    public Worker()
    {
        try
        {

            // Init the enterprise log
            Logger.SetLogWriter(new LogWriterFactory().Create());
            Logger.Write("Updater worker Initialized.", Meta.Enum.LogCategoryType.WIN_SERVICE.ToString());

            // Mappings for Unity container
            _container.RegisterType<IDnsProvider, AmazonRoute53DnsProvider>(Meta.Enum.DnsProviderType.AMAZON_ROUTE_53.ToString());
            _container.RegisterType<IClient, WebHttpClient>(Meta.Enum.ClientType.WEB_HTTP.ToString());
            _container.RegisterType<IIpAddressChecker, CustomIpAddressChecker>(Meta.Enum.IpCheckerType.CUSTOM.ToString());
            _container.RegisterType<IIpAddressChecker, DynDnsIpAddressChecker>(Meta.Enum.IpCheckerType.DYN_DNS.ToString());
            _container.RegisterType<IIpAddressChecker, JsonIpAddressChecker>(Meta.Enum.IpCheckerType.JSON_IP.ToString());
            _container.RegisterType<INotification, EmailNotification>(Meta.Enum.NotificationType.EMAIL.ToString());

            // Read the XML config file for all the Domains/Providers/IpCheckers and Map them to Model
            MappingToModel(ConfigHelper.LoadConfig());


            // Configure the Timers and handlers
            _updateTimer = new Timer(Convert.ToDouble(ConfigHelper.UpdateIntervalInMinutes) * 1000 * 60);
            _updateTimer.Elapsed += new ElapsedEventHandler(UpdateTimerElapsed);

            _montiorTimer = new Timer(Convert.ToDouble(ConfigHelper.MonitorStatusInMinutes) * 1000 * 60);
            _montiorTimer.Elapsed += new ElapsedEventHandler(MonitorTimerElapsed);

        }
        catch (Exception ex)
        {
            Logger.Write(String.Format("FATAL ERROR, Exception={0}", ex.ToString()), Meta.Enum.LogCategoryType.WIN_SERVICE.ToString());
        }

    }


    /// <summary>
    /// Start the timer
    /// </summary>
    public void Start()
    {
        _updateTimer.Start();
        _montiorTimer.Start();
        Logger.Write("Service has started.", Meta.Enum.LogCategoryType.WIN_SERVICE.ToString());

        // First time running it no waiting 
        this.RunUpdate();
    }

    /// <summary>
    /// Stop the timer
    /// </summary>
    public void Stop()
    {
        _updateTimer.Stop();
        _montiorTimer.Stop();
        this.CleanUp();
        Logger.Write("Service has stopped.", Meta.Enum.LogCategoryType.WIN_SERVICE.ToString());
    }


    
    /// <summary>
    /// Event hook for the update job
    /// </summary>
    private void UpdateTimerElapsed(object sender, ElapsedEventArgs e)
    {
        this.RunUpdate();
    }


    /// <summary>
    /// Monitor the update status after submit
    /// </summary>
    private void MonitorTimerElapsed(object sender, ElapsedEventArgs e)
    {
        this.RunMonitor();
    }

    
    
    /// <summary>
    /// Loop through the domains and update the IP
    /// </summary>
    public void RunUpdate()
    {        
        // ...............
        // ...............  Details Implementation (download source code)
        // ...............
    }
   
    /// <summary>
    /// Monitor the DNS update status after submitted to the provider
    /// </summary>
    public void RunMonitor()
    {
        // ...............
        // ...............  Details Implementation (download source code)
        // ...............

    }

}

Windows Service and TopShelf

Open source TopShelf is used because it is a very good windows service framework, it makes code debuggable and makes the application more configuable as well as install.  It creates the new instance of Worker() and calls the Start() when the service is started and calls the Stop() when service is stopped. 

C#
public class Program
{
    public static void Main()
    {
        // Using Open Source project "Topshelf" to handle the run as windows service
        // Ref: http://topshelf-project.com/

        HostFactory.Run(x =>
        {
            x.Service<Worker>(s =>
            {
                s.ConstructUsing(name => new Worker());
                s.WhenStarted(tc => tc.Start());
                s.WhenStopped(tc => tc.Stop());
            });
            x.RunAsLocalSystem();

            x.SetDescription("Update current IP address supports multiple DNS providers");
            x.SetDisplayName("Dynamic DNS Updater Service");
            x.SetServiceName("DynamicDnsUpdater");
        });               
            
    } // main

} //  program

Sample Log file in Action

In this example, first we backdated the LastUpdatedDateTime more than 1 month on the XmlConfig,  launched the application and IP has been updated due to 30 days passed and ForceUpdate kicked in.  At round 10 minutes mark, we manually changed our IpChecker (on VPC/EC2) to 192.168.0.1 to simulate a change in IP.  If you take a closer look, on the first update 1 notification was set only. But on second update, 2 notifications were sent sepreately. The reason was the 1 minute monitoring interval,  Route 53 completed the change for the first domain but second domain was still PENDING.

2015-03-04 12:04:49 WIN_SERVICE -1 Updater worker Initialized.
2015-03-04 12:04:49 WIN_SERVICE -1 Service has started.
2015-03-04 12:04:50 IP_CHECKER -1 IpChecker=http://checkip.yourdomain.com/, IP=74.125.226.111
2015-03-04 12:04:50 DNS_UPDATE -1 Domain=home.yourdomain1.com - FORCE UPDATED provider successfully from IP=74.125.226.111 to IP=74.125.226.111 with ID=/change/C1XBHGFNFIHU8A, passed 30 days
2015-03-04 12:04:50 DNS_UPDATE -1 Domain=home.yourdomain1.com - UPDATED XML configuration LastIpAddress=74.125.226.111, LastUpdatedDateTime=3/4/2015 5:04:50 PM
2015-03-04 12:04:50 DNS_UPDATE -1 Domain=home.yourdomain2.com - FORCE UPDATED provider successfully from IP=74.125.226.111 to IP=74.125.226.111 with ID=/change/C4L2T7HJFSOI4B, passed 30 days
2015-03-04 12:04:50 DNS_UPDATE -1 Domain=home.yourdomain2.com - UPDATED XML configuration LastIpAddress=74.125.226.111, LastUpdatedDateTime=3/4/2015 5:04:50 PM
2015-03-04 12:05:49 STATUS_MONITOR -1 Domain=home.yourdomain1.com - ChangeStatus=PENDING
2015-03-04 12:05:50 STATUS_MONITOR -1 Domain=home.yourdomain2.com - ChangeStatus=PENDING
2015-03-04 12:06:50 STATUS_MONITOR -1 Domain=home.yourdomain1.com - XML configuration ChangeStatusType Updated to INSYNC
2015-03-04 12:06:50 STATUS_MONITOR -1 Domain=home.yourdomain2.com - XML configuration ChangeStatusType Updated to INSYNC
2015-03-04 12:06:53 NOTIFICATION -1 Notification has been sent successfully
2015-03-04 12:11:02 IP_CHECKER -1 IpChecker=http://checkip.yourdomain.com/, IP=74.125.226.111
2015-03-04 12:11:17 DNS_UPDATE -1 Domain=home.yourdomain1.com - NOT UPDATED because IP=74.125.226.111 has not been changed
2015-03-04 12:11:20 DNS_UPDATE -1 Domain=home.yourdomain2.com - NOT UPDATED because IP=74.125.226.111 has not been changed
2015-03-04 12:16:59 IP_CHECKER -1 IpChecker=http://checkip.yourdomain.com/, IP=192.168.0.1
2015-03-04 12:17:00 DNS_UPDATE -1 Domain=home.yourdomain1.com - UPDATED provider successfully from IP=74.125.226.111 to IP=192.168.0.1 with ID=/change/C47J0BHCS33NAD
2015-03-04 12:17:00 DNS_UPDATE -1 Domain=home.yourdomain1.com - UPDATED XML configuration LastIpAddress=192.168.0.1, LastUpdatedDateTime=3/4/2015 5:17:00 PM
2015-03-04 12:17:10 DNS_UPDATE -1 Domain=home.yourdomain2.com - UPDATED provider successfully from IP=74.125.226.111 to IP=192.168.0.1 with ID=/change/C5YDKABPC145AM
2015-03-04 12:17:10 DNS_UPDATE -1 Domain=home.yourdomain2.com - UPDATED XML configuration LastIpAddress=192.168.0.1, LastUpdatedDateTime=3/4/2015 5:17:10 PM
2015-03-04 12:17:55 STATUS_MONITOR -1 Domain=home.yourdomain1.com - ChangeStatus=PENDING
2015-03-04 12:17:55 STATUS_MONITOR -1 Domain=home.yourdomain2.com - ChangeStatus=PENDING
2015-03-04 12:18:55 STATUS_MONITOR -1 Domain=home.yourdomain1.com - XML configuration ChangeStatusType Updated to INSYNC
2015-03-04 12:18:55 STATUS_MONITOR -1 Domain=home.yourdomain2.com - ChangeStatus=PENDING
2015-03-04 12:18:57 NOTIFICATION -1 Notification has been sent successfully
2015-03-04 12:19:55 STATUS_MONITOR -1 Domain=home.yourdomain2.com - XML configuration ChangeStatusType Updated to INSYNC
2015-03-04 12:19:57 NOTIFICATION -1 Notification has been sent successfully
2015-03-04 12:22:52 IP_CHECKER -1 IpChecker=http://checkip.yourdomain.com/, IP=192.168.0.1
2015-03-04 12:22:52 DNS_UPDATE -1 Domain=home.yourdomain1.com - NOT UPDATED because IP=192.168.0.1 has not been changed
2015-03-04 12:22:53 DNS_UPDATE -1 Domain=home.yourdomain2.com - NOT UPDATED because IP=192.168.0.1 has not been changed

Important Notes

Production Notes:  If you ever manually edit the XmlConfig.xml file, you have to RESTART the windows service to take effect. Otherwise it will continue to use the in-memory data.

Debug Notes:  As I mentioned open source TopShelf is so good that you can actually launch the code in Visual Studio and debug the windows service!  Note that when you run in debug mode, it creates the   DynamicDnsUpdater.Service.config and XmlConfig.xml in the /bin/debug folder.  The application will NEVER change the App.config and Xml.Config.xml in your source folder. 

Compile and Installation

  1. Visual Studio 2013 - Enable NuGet manager to restore all the library packages, Compile
  2. In /BIN folder, delete all unnecessary files such as *.pdb, *.manifest, *.application (optional)
  3. Copy the files to the server such as C:\Program Files (x86)\DnsUpdaterService\
  4. On the server, make sure you have FULL version of .NET Framework 4.5 (Not client profile version)
  5. On the server prompt, type  > DynamicDnsUpdater.Service.exe install
  6. EDIT DynamicDnsUpdater.Service.config file (this is App.config) and change
  7.   - Notification email/username/password/smtp
  8.   - Logging Path:  c:\Logs\DnsUpdaterService.log and c:\Logs\DnsUpdaterError.log
  9. EDIT XmlConfig.xml (Domains/Providers config) and update your Domains/Providers
  10. GO to Windows Service,  look for the "Dynamic DNS Update Service", START.  

Encrypt Password/SecretKey

By default, smtp password in App.config and SecretKey in XmlConfig.xml are not encrypted, if you want you can encrypt the both by using the default DES3 encryption method.  DES3 is symmetric, therefore it is not very secure but it's better than nothing.  To do this:

  1. You can set the flag "EnablePasswordEncryption" to true in App.config
  2. Update your own encryption key in ConfigHelper.EncryptionKey
  3. Use Des3.Encrypt() to encrypt your plaintext Password/SecretKey

Tighten IAM Security on AWS

You may want to tighten the security of the user created in IAM (Identity and Access Management), because by default when creating a user, it may assign Power User group which can access everything in your AWS account (VPC/EC2, etc.) In order to limit the user to have access to Route 53 particular zone only, edit your IAM user to the following policy below. In this example, ZQ455PJ32KNK8GI2A is the Hosted Zone ID for my first domain, ZJFD34IIFJ52SIF is for my second domain.  This is just a simple JSON document and it is self explanatory. 

JSON
{
  "Version": "2012-10-17",
  "Statement":[
      {
         "Action":[
            "route53:ChangeResourceRecordSets",
            "route53:GetHostedZone",
            "route53:ListResourceRecordSets",
            "route53:GetChange" 
         ],
         "Effect":"Allow",
         "Resource":[
            "arn:aws:route53:::hostedzone/ZQ455PJ32KNK8GI2A",
            "arn:aws:route53:::hostedzone/ZJFD34IIFJ52SIF",
            "arn:aws:route53:::change/*"
         ]
      },
      {
         "Action":[
            "route53:ListHostedZones"
         ],
         "Effect":"Allow",
         "Resource":[
            "*"
         ]
      }
   ]
}

At this point of writing the article, Amazon AWS DOES NOT support domain/sub-domain name level security, it only supports zone level security. In order words, IAM user is able to change everything within a zone. 

I hope you find article useful!  

Open Source at GitHub

View, Download or Contribute at GitHub: https://github.com/riniboo/DynamicDnsUpdater.Service

History

2015-03-04 - First published

2015-03-05 - Fixing typos and add more detailed description

2015-03-07 - Adding more code to explain the design 

2015-12-19 (V1.0.0.2)
- Minor bug fix on notification time out due to object not disposed properly
- Add new status of "Forced" or "Changed" in both log and email notification
- Updated Amazon AWS API, Topshelf and Unity

 

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)