Introduction
Sometimes you might want to block a user, perhaps spamming your site, from accessing your site based on the user's IP address. This article demonstrates how to do this by validating the request IP addresses in the Global.asax, Application_BeginRequest() event. The solution has support for both IPv4 and IPv6 addresses and uses caching to avoid bottlenecks caused by reading the banned IP addresses each page request.
The same is also possible to accomplish by configuring IIS, as explained in this Microsoft Knowledge Base article: HOW TO: Restrict Site Access by IP Address or Domain Name.
However, if you have your site on shared hosting you may not have access to the IIS configuration, in such scenarios my suggested solution may come handy.
Basic functionality
The IPAddressBlocker works as following. We have a file with blocked IP addresses. The IP addresses can be given as whole addresses or as masks, for example 86.234.*. This file is read when the Application_BeginRequest() event is triggered and searched for the IP address given by HttpContext.Current.Request.UserHostAddress. If some entry in the file maches the IP address, the user is transferred to a page telling him he is banned from the site. The blocked IP address file contents is cahed in order to improve performance.
The file with blocked IP addresses can hold addresses both in IPv4 and IPv6 format, mixed with each other. Here is an example of file contents:
85.154.90.243
85.234.50.*
213.100.*
2001:0db8:85a3:0000:0000:8a2e:0370:7334
2001:cdba:0000:0000:0000:0000:*
The wildcard character * means that any numbers is accepted in the comparison with the user IP address. This allows blocking of IP address ranges with a single entry.
A few words about IP addresses
An IP address (Internet Protocol address) is a numerical label assigned to each computer (device) participating in a computer network that uses the Internet Protocol for communication. An IP address serves two principal functions: host or network interface identification and location addressing. There are two different IP address formats in common use; IPv4 and IPv6.
IPv4
IPv4 or Internet Protocol version 4 is version four of the Internet Protocol (IP) . IPv4 was the first version that was widely distributed and is the version that the internet primarily based.
An IP address in IPv4 consists of 32 pieces and limiting the protocol to 4,294,967,296 unique addresses, many of which are reserved for special purposes, such as multicast and local networks. IPv6 has been developed as a successor to IPv4, mainly due to the limited space of the available IP addresses in IPv4.
An address in IPv4 consists of 32 bits and is usually written as four bytes with a period in between, so-called dot-decimal notation. For example 207.142.131.235
IPv6
The biggest difference between IPv4 and IPv6 is the length of the addresses . IPv6 addresses are 128 bits long. IPv6 addresses are usually composed of two logical parts: a 64-bit network prefix, and a 64-bit local part. The latter part is sometimes generated automatically using the network card's MAC address .
IPv6 addresses are normally written as eight groups of four characters. The address is often accompanied by a slash, and then the length of the prefix. An example: 2001: 0db8: 85a3: 08d3: 1319: 8a2e: 0370: 7334/64 is a valid IPv6 address.
Sequences of zeros can be shortened and made more readable to humans by writing together two colons and omit the zeros in between. Leading zeros in a group of 16 bits (or 4 hexadecimal digits) can be shortened away. For example, the address 2001: fe0c: 0000: 0000: 0000: 0000: 00db: 1dc0 can be written 2001: fe0c :: db: 1dc0 . The process is described in detail in RFC 4291.
This software only supports fully expanded IPv6 addresses, and not shortened formats.
The code
Global.asax file
Fist we add the following event handler to the Global.asax file.
protected void Application_BeginRequest(Object sender, EventArgs e)
{
BlockedIpHandler biph = new BlockedIpHandler();
if (biph.IsIpBlocked(HttpContext.Current.Request.UserHostAddress))
{
Server.Transfer("~/banned.aspx");
}
}
This event is trigged when a page is loaded from the web site. If the IP address retrieved by Request.UserHostAddress
is blocked, the user is transferred to the banned.aspx page.
BlockedIpHandler.cs file
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Caching;
using System.Web.Hosting;
using System.Net;
public class BlockedIpHandler
{
private const string FILE_PATH = "~\\banned.txt";
private const double CACHE_EXPIRATION = 30.0;
private FileContents _fileContents;
public BlockedIpHandler()
{
_fileContents = ReadBannedIpListFile();
}
public bool IsIpBlocked(string ip)
{
IPAddress ipAddress;
if (IPAddress.TryParse(ip, out ipAddress)) {
if (ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) {
string[] ipParts = ip.Split('.');
foreach (string banned in _fileContents.Ipv4Masks) {
string[] blockedParts = banned.Split('.');
if (blockedParts.Length > 4) continue;
if (IsIpBlocked(ipParts, blockedParts)) {
return true;
}
}
}
else if (ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) {
string[] ipParts = ExpandIpv6Address(ipAddress).Split(':');
foreach (string banned in _fileContents.Ipv6Masks) {
string bannedIP = banned.Split('/')[0]; string[] blockedParts = bannedIP.Split(':');
if (blockedParts.Length > 8) continue;
if (IsIpBlocked(ipParts, blockedParts)) {
return true;
}
}
}
}
return false;
}
private bool IsIpBlocked(string[] ipParts, string[] blockedIpParts)
{
for (int i = 0; i < blockedIpParts.Length; i++) {
if (blockedIpParts[i] != "*") {
if (ipParts[i] != blockedIpParts[i].ToLower()) {
return false;
}
}
}
return true;
}
private string ExpandIpv6Address(IPAddress ipAddress)
{
string expanded = "", separator = "";
byte[] bytes = ipAddress.GetAddressBytes();
for (int i = 0; i < bytes.Length; i += 2) {
expanded += separator + bytes[i].ToString("x2");
expanded += bytes[i + 1].ToString("x2");
separator = ":";
}
return expanded;
}
private FileContents ReadBannedIpListFile()
{
ObjectCache cache = MemoryCache.Default;
FileContents fileContents = cache["filecontents"] as FileContents;
if (fileContents == null)
{
FileContents tempFileContents = new FileContents();
string cachedFilePath = HostingEnvironment.MapPath(FILE_PATH);
if (File.Exists(cachedFilePath))
{
List<string> filePaths = new List<string>();
filePaths.Add(cachedFilePath);
CacheItemPolicy policy = new CacheItemPolicy();
policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(CACHE_EXPIRATION);
policy.ChangeMonitors.Add(new HostFileChangeMonitor(filePaths));
List<string> tempIpv4List = new List<string>();
List<string> tempIpv6List = new List<string>();
using (StreamReader file = new StreamReader(cachedFilePath)) {
string line;
while ((line = file.ReadLine()) != null) {
if (line.Contains(".")) {
tempIpv4List.Add(line);
}
else if (line.Contains(":")) {
tempIpv6List.Add(line);
}
}
}
tempFileContents.Ipv4Masks = tempIpv4List.ToArray();
tempFileContents.Ipv6Masks = tempIpv6List.ToArray();
cache.Set("filecontents", tempFileContents, policy);
}
fileContents = tempFileContents;
}
return fileContents;
}
}
public class FileContents
{
public string[] Ipv4Masks = new string[0];
public string[] Ipv6Masks = new string[0];
}
The code is pretty straight forward, but here is a few notes. First we have constant declarations for the path to the file with the blocked IP addresses are and cache timeout. The IsIpBlocked()
method is public and called from the event handler in the Global.asax file. The method first determines if the given address is IPv4 or IPv6 and then compare the address with the address masks in the file.
The ExpandIpv6Address()
function creates an expanded version of the IPv6 address. This means all zeroes are written out in contrast to the .NET Framework IPAddress::ToString()
method which returns a compressed version of the IP address.
The ReadBannedIpListFile()
reads the file with the blocked IP addresses, if the file if found. If not, it returns an empty FileContents
object. The contents of the file is stored in the cache, so if the file has been read within the CACHE_EXPIRATION
time, the cached data will be returned. The file is at read time sorted in IPv4 and IPv6 addresses in order to make lookup faster.
The FileContents
class contains the file contents returned by the ReadBannedIpListFile()
function and is also stored in the cache.
That’s all I have to say about the code. You can download the code as a Website project and try it out on your own computer.
History
- November 15, 2014: Article first published.
- November 17, 2014: IPv6 addresses now case insensitive