////////////////////////////////////////////////////////////////////////////////////////////////////////
// DynSix.cs
//
// Requires C# 5 or newer.
// Determines IPv6 address and propagates it to a dynamic DNS server.
// Must be run as an MS Windows service.
// Needs configuration file DynSix.cfg in the programme directory (see class DSConfig).
//
// Compile code: "%WINDIR%\Microsoft.NET\Framework\v4.0.30319\csc" DynSix.cs
// Install service: "%WINDIR%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe" "%CD%\DynSix.exe"
// Uninstall service: "%WINDIR%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe" /u "%CD%\DynSix.exe"
//
// Additional hints:
// Service installation may need elevation (Command Prompt -> Run as administrator).
// How to enable ICMP (ping reply) on MS Windows: netsh firewall set icmpsetting type=all mode=enable
////////////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Security;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.ServiceProcess;
using System.Timers;
[assembly: AssemblyTitle("DynSix")]
[assembly: AssemblyDescription("DynSix - IPv6 Address Propagator")]
[assembly: AssemblyCompany("ACME")]
[assembly: AssemblyProduct("DynSix")]
[assembly: AssemblyCopyright("Free to the world")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
namespace DynSix
{
class DSMain
{
// The main entry point for the application.
static void Main()
{
ServiceBase.Run(new DSService());
}
}
class DSService : ServiceBase
{
private Timer workTimer = new Timer();
public DSService()
{
this.ServiceName = "DynSix";
}
protected override void OnStart(string[] args)
{
DSLog.Info("Starting Service ...");
workTimer.Elapsed += WorkTimerElapsedHandler;
workTimer.Interval = 60 * 1000; // every minute
workTimer.Start();
DSLog.Info("Starting Service done");
}
protected override void OnStop()
{
DSLog.Info("Stopping Service");
}
private void WorkTimerElapsedHandler(object sender, ElapsedEventArgs e)
{
try
{
DSWorker.Work();
}
catch (Exception ex)
{
DSLog.Error(ex.ToString());
}
}
}
[RunInstaller(true)]
public class DSServiceInstaller : Installer
{
public DSServiceInstaller()
{
this.Installers.Add(new ServiceProcessInstaller
{
Account = ServiceAccount.LocalSystem,
Username = null,
Password = null
});
this.Installers.Add(new ServiceInstaller
{
ServiceName = "DynSix",
DisplayName = "DynSix",
Description = "DynSix - IPv6 Address Propagator"
});
}
}
class DSWorker
{
// DSWorker.Work() is executed periodically.
// It checks for an IPv6 address change and propagates it to the dynamic DNS server.
private static Boolean configInitialized = false;
private static List<DSSession> sessions = null;
public static void Work()
{
if (!configInitialized)
{
// Read configuration
sessions = DSConfigReader.Read();
configInitialized = true;
}
foreach (DSSession session in sessions)
{
// Find IPv6 address that can be used to connect the internet
IPAddress address = FindPublicIpv6Address(session.MacAddress);
if (session.IpMask != null) address = DSUtil.IPAddress_ApplyMask(address, session.IpMask);
if ((session.LastPropagatedIpAddress != null) && (session.LastPropagatedIpAddress.Equals(address)))
{
DSLog.Debug("IPv6 address of session \"" + session.Name + "\" is unchanged.");
}
else
{
if (session.LastPropagatedIpAddress != null) DSLog.Info("IPv6 address of session \"" + session.Name + "\" has changed from " + session.LastPropagatedIpAddress.ToString() + " to " + address.ToString());
// Publish IPv6 address using dynamic DNS
String url = session.DynDnsUrlTemplate.Replace("<ip6addr>", address.ToString());
DSLog.Info("Session \"" + session.Name + "\", Request: " + url);
String response = DSWeb.PerformRequest(new Uri(url), session.DynDnsUsername, session.DynDnsPassword);
DSLog.Info("Session \"" + session.Name + "\", Response: " + response);
session.LastPropagatedIpAddress = address;
DSLog.Info("Ok, propagated IPv6 address of session \"" + session.Name + "\" is now " + session.LastPropagatedIpAddress.ToString());
}
}
}
// Find IPv6 address that can be used to connect the internet
private static IPAddress FindPublicIpv6Address(string macAddress)
{
PhysicalAddress DesiredPhysicalAddress = PhysicalAddress.Parse(macAddress);
// Find network interface identified by MAC address
NetworkInterface ni = NetworkInterface.GetAllNetworkInterfaces().FirstOrDefault(x => (x.GetPhysicalAddress().Equals(DesiredPhysicalAddress)));
if (ni == null) throw new Exception("Network interface with MAC address " + DesiredPhysicalAddress.ToString() + " not found.");
DSLog.Debug("Found network interface: " + ni.Name);
// Find IPv6 address of this network interface.
// It shall be able to connect to the internet (not IsIPv6LinkLocal, PrefixOrigin = RouterAdvertisement)
// and not be changed periodically by the IPv6 privacy extensions (SuffixOrigin = LinkLayerAddress).
// In addition, it must not be deprecated (DuplicateAddressDetectionState = Preferred).
IPAddressInformation ia = ni.GetIPProperties().UnicastAddresses.FirstOrDefault(x =>
x.Address.AddressFamily == AddressFamily.InterNetworkV6 &&
!x.Address.IsIPv6LinkLocal &&
x.PrefixOrigin == PrefixOrigin.RouterAdvertisement &&
x.SuffixOrigin == SuffixOrigin.LinkLayerAddress &&
x.DuplicateAddressDetectionState == DuplicateAddressDetectionState.Preferred);
if (ia == null) throw new Exception("Network interface \"" + ni.Name + "\" has no appropriate IPv6 address.");
DSLog.Debug("Found IPv6 address: " + ia.Address.ToString());
return ia.Address;
}
}
class DSSession
{
public string Name = null;
public string MacAddress = null;
public IPAddress IpMask = null;
public string DynDnsUrlTemplate = null;
public string DynDnsUsername = null;
public string DynDnsPassword = null;
public IPAddress LastPropagatedIpAddress = null;
}
class DSConfigReader
{
// DynSix configuration reader.
//
// Configuration file must be named "DynSix.cfg" an be located in the programme directory.
// Key "DynDnsUrlTemplate" must contain a placeholder "<ip6addr>" for the IPv6 address.
//
// Example content:
// --------------------------------------------------------------
// # Example DynSix.cfg
// # Session is required only if more than one session is used
// # IPMask is optional
//
// Session = fasel
// MacAddress = 00-0C-39-A2-50-11
// IPMask = ffff:ffff:ffff:ffff::
// DynDns.UrlTemplate = https://dyndns.service.com/?myip=<ip6addr>
// DynDns.Username = baffoon
// DynDns.Password = topsecret
// --------------------------------------------------------------
public static List<DSSession> Read()
{
List<DSSession> sessions = new List<DSSession>();
{
DSSession session = null;
String configFilename = AppDomain.CurrentDomain.BaseDirectory + "DynSix.cfg";
using (StreamReader rdr = new StreamReader(configFilename))
{
String line;
while ((line = rdr.ReadLine()) != null)
{
line = line.Trim();
if ((line.Length != 0) && !line.StartsWith("#"))
{
int p = line.IndexOf('=');
if (p != -1)
{
String key = line.Substring(0, p).Trim();
String value = line.Substring(p + 1).Trim();
if (key == "Session")
{
if (session != null) sessions.Add(session);
session = new DSSession() { Name = value };
}
else
{
if (session == null) session = new DSSession() { Name = "default" };
try
{
switch (key)
{
case "MacAddress": session.MacAddress = value; break;
case "IpMask": session.IpMask = IPAddress.Parse(value); break;
case "DynDns.UrlTemplate": session.DynDnsUrlTemplate = value; break;
case "DynDns.Username": session.DynDnsUsername = value; break;
case "DynDns.Password": session.DynDnsPassword = value; break;
default: throw new Exception("Keyword is unknown.");
}
}
catch (Exception ex)
{
throw new Exception("Invalid configuration at keyword \"" + key + "\" in Session \"" + session.Name + "\": " + ex.Message);
}
}
}
else
{
throw new Exception("Invalid configuration: Invalid line \"" + line + "\".");
}
}
}
}
if (session != null) sessions.Add(session); // last open session
}
foreach (DSSession session in sessions)
{
if (session.MacAddress == null) throw new Exception("Invalid configuration: Key \"MacAddress\" in Session \"" + session.Name + "\" is not defined.");
if (session.DynDnsUrlTemplate == null) throw new Exception("Invalid configuration: Key \"DynDns.UrlTemplate\" in Session \"" + session.Name + "\" is not defined.");
if (session.DynDnsUsername == null) throw new Exception("Invalid configuration: Key \"DynDns.Username\" in Session \"" + session.Name + "\" is not defined.");
if (session.DynDnsPassword == null) throw new Exception("Invalid configuration: Key \"DynDns.Password\" in Session \"" + session.Name + "\" is not defined.");
}
return sessions;
}
}
class DSWeb
{
private static Boolean initialized = false;
private static void Init()
{
if (initialized) return;
// accept arbitrary SSL certifikate
ServicePointManager.ServerCertificateValidationCallback +=
(object sender, X509Certificate certification, X509Chain chain, SslPolicyErrors sslPolicyErrors) =>
{ return true; };
initialized = true;
}
// Perform an HTTP or HTTPS request using username and password
public static String PerformRequest(Uri uri, String username, String password)
{
Init();
String result = "?";
CredentialCache credentialCache = new CredentialCache();
credentialCache.Add(uri, "Basic", new NetworkCredential(username, password));
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
webRequest.UserAgent = "ACME - DynSix - 1.0";
webRequest.ContentType = "text/plain";
webRequest.ContentLength = 0;
webRequest.Credentials = credentialCache;
webRequest.PreAuthenticate = true;
using (WebResponse response = webRequest.GetResponse())
{
Stream dataStream = response.GetResponseStream();
StreamReader reader = new StreamReader(dataStream);
string responseFromServer = reader.ReadToEnd();
result = responseFromServer;
}
return result;
}
}
class DSUtil
{
public static IPAddress IPAddress_ApplyMask(IPAddress address, IPAddress mask)
{
Byte[] addressBytes = address.GetAddressBytes();
Byte[] maskBytes = mask.GetAddressBytes();
for (int i = 0; i < addressBytes.Length; i++) addressBytes[i] &= maskBytes[i];
return new IPAddress(addressBytes);
}
}
class DSLog
{
// Simple logging
public static void Debug(String text)
{
//Log('D', text);
}
public static void Info(String text)
{
Log('I', text);
}
public static void Error(String text)
{
Log('E', "ERROR: " + text);
}
private static void Log(Char level, String text)
{
DateTime now = DateTime.Now;
File.AppendAllText(
Path.GetTempPath() + "/DynSix_" + now.ToString("yyyyMMdd") + ".log",
"[" + now.ToString("yyyy-MM-dd HH:mm:ss") + " " + level + "] " + text + Environment.NewLine);
}
}
}