Introduction
For those who have a capped download broadband package and have children, this article may be a Godsend by showing a way to limit users' download ability by setting a daily download allowance.
Background
So there I was, lecturing my children on the excess download charges our ISP had made again, when it occured to me that what I need is someway of limiting their download capacity. After searching the Web and asking in the CP forums, I came up with the idea for this little program.
Images
The actual program, when running, creates a permanent, very small window on the user's desktop informing them of how much download capacity they have remaining.
Once the limit has been reached, the program informs the user that their limit has been reached and that their Internet access is now disabled.
Using the Code
I've tried to keep the program as simple as possible, and therefore the code itself has a relatively simple mode of operation. We use a performance counter (see http://msdn.microsoft.com/en-us/library/system.diagnostics.performancecounter(VS.90).aspx for more information) to gather information on the number of bytes a network adapter has received.
downloadCounter = New PerformanceCounter("Network Interface", "Bytes Received/sec", nicName)
We then keep a running total, and if the running total exceeds a predetermined limit, we disable the user's network connection until the following day. It's as simple as that!
So, how do we implement all of this? How do we disable the user's internet connection I can hear you asking. Easy! We stop the DHCP service on the user's computer. Now, here's the rub: if you don't use dynamically allocated IP addresses from a DHCP server, stopping the client's DHCP service isn't going to make any difference; it won't disable their connection :-(
Private Sub StopService(ByVal service As String)
Dim sc As ServiceController = New ServiceController(service)
If sc.CanStop And sc.Status <> ServiceControllerStatus.Stopped Then
sc.Stop()
sc.WaitForStatus(ServiceControllerStatus.Stopped)
End If
End Sub
To be able to gather the downloaded bytes and keep a running total, we create an event that is triggered every second and updates the latest download total. To create the trigger, we create a one second timer:
Dim myTimer As System.Timers.Timer = New System.Timers.Timer(1000)
We then create an Event Handler and point this to our Delegated collection Sub:
AddHandler myTimer.Elapsed, New ElapsedEventHandler(AddressOf timer_Elapsed)
This is where we get the update from:
Private Sub timer_Elapsed(ByVal sender As Object, ByVal e As ElapsedEventArgs)
downloadBytes = downloadCounter.NextSample().RawValue
regKey.SetValue("PreviouslyDownloadedBytes", downloadBytes, RegistryValueKind.DWord)
End Sub
Note: we also save a running total to the Registry. This is explained later.
We also use the timer.tick
event to see if we have reached our download limit:
Private Sub TimerCounter_Tick(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles TimerCounter.Tick
If Not disabled Then
lblDownloadBytes.Text = ((tempDownloadLimit - downloadBytes) /
(1024 ^ 2)).ToString("###0.###") & " MBytes remaining"
If downloadBytes > tempDownloadLimit Then
regKey.SetValue("Expired", True, RegistryValueKind.DWord)
regKey.SetValue("PreviouslyDownloadedBytes", 0, RegistryValueKind.DWord)
disabled = True
StopService(service)
lblDownloadBytes.Text = "Download Limit Reached, Internet Access Disabled."
End If
Else
TimerCounter.Stop()
RemoveHandler myTimer.Elapsed, AddressOf timer_Elapsed
End If
End Sub
As the program stands, as described, it would be pretty trivial to circumvent and regain network access. So, we put in a few checks to help thwart circumvention. One of the first checks we perform is to see if the computer has been restarted thus resetting our performance counter.
As can be seen from the previous code block, we set a couple of Registry entries. One of these entries is 'Expired'. This is a boolean value that, if true, indicates that the download limit has been reached. We check to see if we have 'expired' on startup, and if we have, we disable the DHCP service straight away.
Another Registry setting we save, is the 'PreviouslyDownloadedBytes'. This is used in the event that the user restarts their computer and hasn't exhausted their download limit for that day. We subtract the PreviouslyDownloadedBytes
figure from the limit we set, and this becomes the new download limit for the remainder of the day.
Case DateComparisonResult.TheSame
If downloadLimitReached Then
lblDownloadBytes.Text = "Download Limit Reached, Internet Access Disabled."
disabled = True
StopService(service)
Else
tempDownloadLimit = maxDownloadLimit - previouslyDownloadedBytes
End If
Another check performed is to see if the user has moved the system clock forward thus resetting the download counter. One of our Registry entries saves the date on which we last ran, we then compare this date to the system's current date on startup to determine if we should give the user internet access. So that we can accurately determine if the system's clock is in fact correct, we query an NTP time server. DaveyM69 created a very detailed and useful CP article on querying time servers; see: http://www.codeproject.com/KB/datetime/SNTPClient.aspx for more information. I have modified Dave's original SNTPClient
code to suit my own needs, and used this code to query a time server.
If Not IsServiceRunning(service) Then
StartService(service)
End If
If My.Computer.Network.IsAvailable Then
ntpServerDate = SntpClient.GetNow().ToShortDateString
End If
If currentDate <> ntpServerDate Then
SntpClient.UpdateLocalDateTime = True
SntpClient.GetNow()
End If
The last check we make is to monitor the state of the IP address assigned to the NIC. If we have disabled the DHCP service and thus disabled the connection, and the NIC's IP address will be non-existent. We set up an event handler which notifies us if the NIC's IP address changes.
AddHandler NetworkChange.NetworkAddressChanged, AddressOf AddressChanged
We then use our delegated sub to disable the DHCP service again if the user re-enabled it.
Private Sub AddressChanged(ByVal sender As Object, ByVal e As EventArgs)
If disabled Then
StopService(service)
End If
End Sub
So, there you have it ladies and gentlemen; how to simply disable a network connection after a predetermined number of bytes have been downloaded.
Points of Interest
Because the instantiation of the performance counter requires the name of the NIC, we use a WMI routine to get the NIC's name.
Private Function GetNetworkAdaptorName() As String
Dim nicName As String = ""
Dim query As ManagementObjectSearcher = New ManagementObjectSearcher _
("SELECT * FROM Win32_NetworkAdapterConfiguration " +
"WHERE IPEnabled = TRUE")
Dim queryCollection As ManagementObjectCollection = query.Get()
Dim dhcpAddress() As String
For Each mo As ManagementObject In queryCollection
nicName = mo("Description").ToString.Trim
dhcpAddress = CType(mo("IPAddress"), String())
If dhcpAddress(0) <> "" Or dhcpAddress(0) <> "255.255.255.255" Then
If nicName.Contains("/"c) Then
nicName = nicName.Replace("/"c, "_"c)
End If
Exit For
End If
Next
Return nicName
End Function
Although we try and filter the number of NICs returned by our WMI query by using the:
WHERE IPEnabled = TRUE
clause, we can sometimes get back more than one NIC. In this case, we check to see if the returned NIC has a DHCP server address associated with it.
We also use in the program a routine to start the DHCP service. One of the problems I found was that if you start the DHCP service and then try and use any network function immediately, the function would fail even though we wait explicitly for the service to start by using:
sc.WaitForStatus(ServiceControllerStatus.Running)
This is because, even though the service is running, it takes time for the DHCP server to allocate an IP address to the NIC. To keep things simple, I just added a 5 second delay before we use any network function.
Private Sub StartService(ByVal service As String)
Dim sc As ServiceController = New ServiceController(service)
If sc.Status <> ServiceControllerStatus.Running Then
Try
sc.Start()
sc.WaitForStatus(ServiceControllerStatus.Running)
Thread.Sleep(5000)
Catch ex As Exception
lblDownloadBytes.Text = "Could Not Start DHCP Service"
End Try
End If
End Sub
Running the Program
To run the program on the user's system, put the executable out of the way somewhere, say 'Windows\system32', and then create a Registry entry in the HKLM\Software\Microsoft\Windows\CurrentVersion\Run Registry key to start the program.
Caveats
Although the program works well on Windows XP and Windows 7, the user must be a member of the Administrators group. Additionally, on Vista, even if UAC is not disabled, UAC prompts will appear when starting and stopping the DHCP service and trying to change the system time.
To Do
I think, to overcome the running as admin and UAC problems, everything that doesn't display information to the user should become a Windows service with a separate GUI app that communicates with the service to display to the user their current download usage.
One other thing: if the user stops our app in the Task Manager, obviously we can't monitor download usage. I've been thinking about adding to the form closing event a routine that shuts down the user's computer if they stop our app. Also, it might be an idea integrating our app in such a way that if it's not running at startup, the NIC itself can't start; possibly by relying on our app to, say, start the NIC's driver.
History
- 4th July 2010 - Initial version.
- 5th July 2010 - Bug fix.