Introduction
This service allows public DHCP address monitoring by a service on remote clients. When the IP address changes, it is reported to a web service, and then depending on the options for the account, may send an administrative email about the change, and may update a DNS entry.
Project List
The solution is made up of multiple projects, as follows:
- Database (t-SQL): Creates the SQL database to store the data
- IP (C#, ASP.NET): A simple web site to glimpse data from the web service
- IPMSCfg (C#): Allows setting of email and DNS updating options from the computer being monitored.
- IPMCSvc (MC++): The Windows Service which monitors the IP Address of the local computer.
- IPMXP (C++, Win32): Contains SQL Server extended procedures to drop emails and update a dynamic DNS server.
- Store (C#, ASP.NET): The web service itself.
Database Project
The database project creates a SQL server database with three tables:
- IPMon - keeps information about the client computers
- Email - keeps information about email alerts
- DNS - keeps information about DNS updating
It also registers the two extended stored procedures xp_AppendDNSHostEntry
and xp_SendIPMonMailNotification
from the IMPXP project in the [master] database.
This functionality is brought together by the t-SQL AFTER UPDATE
trigger on the IPMon table. This script updates the Email and DNS tables when necessary and also calls the extended procedures to perform the external email and DNS work.
CREATE TRIGGER trigSendMail ON [dbo].[IPMon] AFTER UPDATE
AS
IF ((COLUMNS_UPDATED() & 8 = 8) AND (@@ROWCOUNT = 1))
BEGIN
DECLARE @UID uniqueidentifier
DECLARE @NewIPAddress varchar(15)
DECLARE ip cursor local for
SELECT UID, IPAddress FROM inserted
OPEN ip
FETCH NEXT FROM ip INTO @UID, @NewIPAddress
DECLARE @EmailAddress varchar(50)
DECLARE @LastUpdateIP varchar(15)
DECLARE email cursor local keyset for
SELECT TOP 1 EmailAddress, LastUpdateIP FROM Email WHERE UID = @UID
for read only
OPEN email
FETCH NEXT FROM email INTO @EmailAddress, @LastUpdateIP
IF (@@FETCH_STATUS = 0)
BEGIN
IF (@LastUpdateIP <> @NewIPAddress)
BEGIN
UPDATE Email
SET LastUpdateIP = @NewIPAddress
WHERE UID = @UID
SET @LastUpdateIP = @NewIPAddress;
EXEC master.dbo.xp_SendIPMonMailNotification
@emailaddress = @EmailAddress,
@ipaddress = @NewIPAddress
END
END
CLOSE email
DEALLOCATE email
DECLARE @HostName varchar(50)
DECLARE dnsc cursor local keyset for
SELECT TOP 1 HostName, LastUpdateIP FROM DNS WHERE UID = @UID
for read only
OPEN dnsc
FETCH NEXT FROM dnsc INTO @HostName, @LastUpdateIP
IF (@@FETCH_STATUS = 0)
BEGIN
IF (@LastUpdateIP <> @NewIPAddress)
BEGIN
UPDATE DNS
SET LastUpdateIP = @NewIPAddress
WHERE UID = @UID
SET @LastUpdateIP = @NewIPAddress;
EXEC master.dbo.xp_AppendDNSHostEntry
@hostname = @HostName,
@ipaddress = @NewIPAddress
END
END
CLOSE dnsc
DEALLOCATE dnsc
CLOSE ip
DEALLOCATE ip
END
IP Project
The IP project is a very rudimentary front end for the data from the web service. It is fairly self explanatory. I imagine that any implementation would use this as a general example and build its own custom interface.
IPMSCfg Project
The IPMSCfg project allows a simple user interface to change the DNS and email options on the database. It is dependent on the service being installed and the registry key \HKLM\Software\reeder.ws\IPMon
intact.
The program retrieves the service GUID from the registry, then uses the web service to retrieve and store information. Since the web service uses an empty string to define options not set for email and DNS settings, these must be taken into account for the GUI.
private reeder.IPMonWebService ws = new IPMCSCfg.reeder.IPMonWebService();
Microsoft.Win32.RegistryKey reg =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
("Software").OpenSubKey("reeder.ws").OpenSubKey("IPMon");
this.uid = (System.Guid)
(System.ComponentModel.TypeDescriptor.GetConverter
(this.uid).ConvertFrom(reg.GetValue("UID")));
{
this.Email = ws.GetEmailAddress(this.uid);
if (this.Email == string.Empty)
this.SendEmail = false;
else
this.SendEmail = true;
this.DNS = ws.GetDNSInfo(this.uid);
if (this.DNS == string.Empty)
this.SendDNS = false;
else
this.SendDNS = true;
}
catch{
System.Windows.Forms.MessageBox.Show(this,
"Error retrieving information from the IPMon Web Service." +
" Please ensure that you have an active Internet" +
" connection, then restart this program",
"IPMon Web Service Error",
System.Windows.Forms.MessageBoxButtons.OK);
this.Close();
return;
}
if (this.SendEmail){
this.ckEmail.Checked = true;
this.txtEmail.Enabled = true;
this.txtEmail.Text = this.Email;
}
if (this.SendDNS){
this.ckDNS.Checked = true;
this.txtDNS.Enabled = true;
this.txtDNS.Text = this.DNS;
}
After the service has been configured, the program restarts the Windows service by using an instance of the System.ServiceProcess.ServiceController
class.
System.ServiceProcess.ServiceController sc =
new System.ServiceProcess.ServiceController("IP Monitor");
if (changed && (sc.Status ==
System.ServiceProcess.ServiceControllerStatus.Running)){
sc.Stop();
sc.WaitForStatus(System.ServiceProcess.ServiceControllerStatus.Stopped);
sc.Start();
sc.WaitForStatus(System.ServiceProcess.ServiceControllerStatus.Running);
}
if (sc.Status == System.ServiceProcess.ServiceControllerStatus.Stopped){
if (System.Windows.Forms.MessageBox.Show(this,
"The IP Monitor service is not currently running." +
" Would you like to start it now?", "Service Start",
System.Windows.Forms.MessageBoxButtons.YesNo)
== System.Windows.Forms.DialogResult.Yes)
sc.Start();
}
IPMCSvc Project
The IP Monitor Client service monitors the IP addresses of the client computer, searching for the first available public IP address. When the IP address changes, or after an hour of dormancy, the client publishes the IP address to the web service.
To monitor the local computer's IP address list, the service makes heavy use of the Win32 IP Helper API. It uses two worker threads, one for updating on IP address changes, and one for updating via a timer.
void IPMCSvc::IPMCSvcWinService::IPChangeThreadStart(){
while (true){
DWORD ret = NotifyAddrChange(NULL, NULL);
if (ret == NO_ERROR){
if (this->EventLevel & 0x04)
this->EventLog->WriteEntry("Win32 IP Address changed trigger hit",
System::Diagnostics::EventLogEntryType::Information,
EVENT_IP_W32_CHANGE_EVENT);
this->CheckIPAddress();
}
}
}
void IPMCSvc::IPMCSvcWinService::IPTimerThreadStart(){
while (true){
System::Threading::Thread::Sleep(TIMER_SLEEP_SECONDS);
if (this->EventLevel & 0x04)
this->EventLog->WriteEntry("Timed check triggered",
System::Diagnostics::EventLogEntryType::Information,
EVENT_IP_TIMER);
this->CheckIPAddress();
}
The actual check for a public IP address is fairly straightforward. You retrieve the IP address list, then check each one until a non-private address is found, as shown in the following snippet:
static DWORD RegIPAddr = 0;
ULONG ipasize = IPADDRESS_BUFFER_SIZE;
PMIB_IPADDRTABLE ipa =
reinterpret_cast<PMIB_IPADDRTABLE>(malloc(IPADDRESS_BUFFER_SIZE));
GetIpAddrTable(ipa, &ipasize, FALSE);
bool AddressFound = false;
for (DWORD i = 0; i < ipa->dwNumEntries; ++i){
bool usable = true;
unsigned char* ipb =
reinterpret_cast<unsigned char*>(&(ipa->table[i].dwAddr));
if ((*ipb == 127) && (!ipb[1]) && (!ipb[2]) && (ipb[3] == 1))
usable = false;
if (*ipb == 0x0A)
usable = false;
if ((*ipb == 172) && ((ipb[1] & 0xF0) == 0x10))
usable = false;
if ((*ipb == 192) && (ipb[1] == 168))
usable = false;
IMPXP Project
The IMPXP project contains the extended stored procedures for email notification and DNS updating. Email, of course, could have been done using SQL Mail, but often, SQL Mail is not preferred because it forces all mail bearing applications from the SQL Service instance to operate from a single MAPI-based email service.
Probably the most painful thing in building an extended stored procedure for SQL Server is retrieving the parameters from the t-SQL EXEC
command. This is no near as simple as it could be, and hopefully the next version of SQL Server (which supposedly supports embedded C# statements inside t-SQL scripts) will make this much easier. Here is an example of retrieving the email address from the EXEC
command.
if (srv_paraminfo(proc, 1, &type, &mlen, &clen, NULL, &fNull) == FAIL)
return (FAIL);
if ((type != SRVCHAR) && (type != SRVVARCHAR) && (type != SRVBIGVARCHAR))
return (FAIL);
emailaddress = reinterpret_cast<BYTE*>(malloc(clen + 1));
emailaddress[clen] = '\0';
srv_paraminfo(proc, 1, &type, &mlen, &clen, emailaddress, &fNull);
Definitely not a trivial thing. But once this is traversed, everything else is fairly straightforward. I should note that the email sending functionality is dependent on the MS SMTP Service and expects the drop directory to be at C:\Inetpub\mailroot\pickup. The location can be changed by changing the value for ROOT_DROP_DIRECTORY
and recompiling.
For DNS functionality, the Win32 DNS API is used. This is dependent on the default DNS server accepting dynamic updates. Both secure and unsecure updates are attempted.
In order to allow a first time entry to succeed, an attempt to retrieve a host record is done, then the submission is dependent on the status of that retrieval.
PDNS_RECORD dnsoldlist;
DNS_STATUS oldlookupret = DnsQuery_A(reinterpret_cast<PSTR>(hostname),
DNS_TYPE_A, DNS_QUERY_BYPASS_CACHE |
DNS_QUERY_TREAT_AS_FQDN |
DNS_QUERY_DONT_RESET_TTL_VALUES,
NULL, &dnsoldlist, NULL);
DNS_RECORD rec;
rec.pNext = NULL;
rec.pName = reinterpret_cast<PSTR>(hostname);
rec.wType = DNS_TYPE_A;
rec.wDataLength = sizeof(DNS_A_DATA);
rec.Flags.DW = 0;
rec.dwTtl = 480;
ec.dwReserved = 0;
rec.Data.A.IpAddress =
inet_addr(reinterpret_cast<char*>(ipaddress));
DNS_STATUS dnsret;
if (!oldlookupret){
dnsret = DnsModifyRecordsInSet_A(&rec, dnsoldlist,
DNS_UPDATE_SECURITY_ON, NULL, NULL, NULL);
DnsRecordListFree(dnsoldlist, DnsFreeFlat);
}
else
dnsret = DnsModifyRecordsInSet_A(&rec, NULL,
DNS_UPDATE_SECURITY_ON, NULL, NULL, NULL);
Store Project
The Store project contains the Web service which provides gating functionality between the database and the clients' Windows services. It consists of the following WebMethod
s:
bool CheckUserName(string UserName)
System.Guid CreateNewUser(string UserName, string Password)
bool GetCurrentInfo(string UserName, string Password,
out string IPAddress, out System.DateTime UpdateTime)
bool StoreIPAddress(System.Guid Id, string IPAddress)
bool GetGUID(string UserName, string Password, out string Uid)
bool StoreEmailInfo(System.Guid UID, string EmailAddress)
bool DropEmailInfo(System.Guid UID)
string GetEmailAddress(System.Guid UID)
bool StoreDNSInfo(System.Guid UID, string HostName)
bool DropDNSInfo(System.Guid UID)
string GetDNSInfo(System.Guid UID)
These are all fairly straightforward.
Implementing
Implementing this isn't as simple as just deploying the executables (which is why the executables aren't included here). To implement without changes, you should do the following:
- Build the extended procedures and place them in the SQL Server's Bin directory.
- Build the database, by first adding a user account name
IPMonWS
and then using the t-SQL scripts.
- Deploy the web service to your web server. Note: You have to supply the ConnectionString to the SQL Server using the [IPMonWS] account and the [IPMon] database.
- Change all web references to your web service.
- Deploy the Windows Service (IPMCSvc) and fill the 2 registry values (
UID
and Event
) in HKLM\Software\reeder.ws\IPMon
. Optionally, you can also include the config utility.
- Optionally, you can then deploy the sample Web UI to your web server.