Introduction
Today a colleague of mine asked for some help on an assignment he is working on. He needs the ability to identify if a client is in an IP block. He is using data from blockacountry.com and wants a service to perform different actions based on the client IP.
In our example solution, we will create a reusable WCF service behavior that can be used to deny or allow use of a service based on the client IP address. The service behavior allows us to declaratively add our IPFilter
via configuration to any service. We will also create configuration section to support allow/deny by IP, similar to the authorize configuration element in ASP.NET.
Blocking IP addresses should not be done at the application level. If possible, you should use a firewall. If your service/application is hosted in IIS, you can use the built in IP filtering it provides. You can learn more about the IIS IP restrictions here. If you are hosting a WCF service outside of IIS or developing a socket based application, this might be useful to you too.
Background
We first need to go over some IP Address basics. IP addresses are numbers used to uniquely identify computers in a network. IP version 4 address are 32 bits wide but will soon be replaced by IP version 6 which uses 128-bit number for addresses. Today we are going to look at IPV4 only. Even though an IP address is a 32-bit number writing the number as a decimal and separating each byte of the number with a period is the most common way of writing it. This is called dot decimal notation.
Dot Decimal | Hex |
127.0.0.1 | 0x100007F |
24.1.5.1 | 0x1050118 |
192.168.0.23 | 0x1700A8C0 |
An IP address in not only an identifier for a particular host/computer but also an identifier for the network the host is on. IPV4 allows us to segment traffic into smaller groups called subnets. The start of an address is the group/subnet identifier and the end of the address is the host identifier. Each IPV4 address has a corresponding subnet mask or netmask. A netmask is a bitmask used to identify the subnet identifier portion of an IP address.
The netmask can be used to determine how many hosts can be in a subnet. The subnet 255.255.255.0 or 0xFFFFFF00 in leaves us 1 byte or 256 possible addresses (including zero). Some of these are reserved broadcast addresses and cannot be used.
A more efficient way of describing a subnet without including the netmask is CIDR (Classless Inter-Domain Routing) notation. Since the subnet identifier is always at the start of the IP address and must be contiguous, we just need to know the number of bits. Using CIDR notation, we write out the subnet identifier portion of the IP address in dot decimal notation and then a forward slash and the number of bits used by the subnet identifier.
CIDR is not just a notation. Back in the olden days (pre 1993), an IP address network and host identifiers could only be segmented along 8 bit boundaries called classes. The Classless in CIDR comes from the fact that you are not bound by 8 bit classes and can split the bits however you like. You can learn more about it here.
IP Validation Code
Our new code is going to center around a struct
called IPRange
. This struct
will describe the IP ranges. The code for this struct
is given below:
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 8)]
public struct IPRange
{
private static readonly char[] _wildcardChars = new char[] { '*', 'x', 'X' };
private readonly uint _addressMask;
private readonly byte _maskData;
private readonly Mode _mode;
private IPRange(uint mask, byte maskData, Mode mode)
{
_addressMask = mask;
_maskData = maskData;
_mode = mode;
}
...
We need to support wildcards in the middle of an address. An example would be 192.168.*.11 or 10.*.11.*. My colleague said this is a requirement so we had to implement it. Because of this late addition, I introduced a mode flag to switch between "Class" mode and Classless mode.
Testing for a Match
We test for matches differently depending on the mode. In classless, we simply shift over the host identifier portion of the IP address so we only have the network identifier left and compare. Because class modes wildcards are not tied to the network and host identifiers (ex 192.168.*.1) we need to compare them differently. In class mode we use a bitmask but we also have to check for zeros in the address. Another possible approach is to enumerate through each bit, check to see if it is in a network/wildcard mask, and compare bits if it is. This would give us a uniform solution.
public bool IsMatch(uint address)
{
if (_mode == Mode.Class)
{
if ((address & _addressMask) != _addressMask)
{
return false;
}
IPClass ipClasses = (IPClass)_maskData;
if ((ipClasses & IPClass.A) != IPClass.A &&
(0xFF & _addressMask) == 0 && (0xFF & address) != 0)
{
return false;
}
if ((ipClasses & IPClass.B) != IPClass.B &&
(0xFF00 & _addressMask) == 0 && (0xFF00 & address) != 0)
{
return false;
}
if ((ipClasses & IPClass.C) != IPClass.C &&
(0xFF0000 & _addressMask) == 0 && (0xFF0000 & address) != 0)
{
return false;
}
if ((ipClasses & IPClass.D) != IPClass.D &&
(0xFF000000 & _addressMask) == 0 && (0xFF000000 & address) != 0)
{
return false;
}
return true;
}
int shift = 32 - _maskData;
return (_addressMask << shift) == (address << shift);
}
Parsing Wildcards and CIDR Addresses
Below is our code to parse the wildcards and CIDR addresses:
public static IPRange Parse(string value)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("value");
}
if (IsWildCard(value))
{
return new IPRange(0, 0xF, Mode.Class);
}
int index = value.IndexOf('/');
Mode mode;
byte classData;
uint mask;
if (index > -1)
{
ParseAddress(value.Substring(0, index), out classData, out mask);
mode = Mode.Classless;
if (classData != 0)
{
throw new ArgumentException(
string.Format("You can use a CIDR notation and wildcards.
'{0}' is invalid'.", value),
"value");
}
if (!byte.TryParse(value.Substring(index + 1), out classData))
{
throw new ArgumentException(
string.Format("The '{0}' is invalid'.", value),
"value");
}
if (classData > 31)
{
throw new ArgumentException
("The subnet mask length must be less than 32.", "value");
}
mask &= (uint)((1 << classData) - 1);
}
else
{
ParseAddress(value, out classData, out mask);
mode = Mode.Class;
}
return new IPRange(mask, classData, mode);
}
private static void ParseAddress(string value,
out byte ipClassWildcards, out uint address)
{
string[] values = value.Split('.');
if (values.Length != 4)
{
throw new ArgumentException(string.Format
("The ip address is in invalid format {0}", value), "value");
}
byte ipValue;
byte classIndex = 1;
int maskIndex = 0;
address = 0;
ipClassWildcards = 0;
for (int i = 0; i < values.Length; i++)
{
if (IsWildCard(values[i]))
{
ipClassWildcards |= classIndex;
}
else
{
if (!byte.TryParse(values[i], out ipValue))
{
throw new ArgumentException(string.Format
("The ip address is in invalid format {0}", value), "value");
}
address |= (uint)(ipValue << maskIndex);
}
maskIndex += 8;
classIndex <<= 1;
}
}
Available Addresses
Below is the code we use to get the number of available addresses in the subnet. We count the available host bits, create number with all these bits, and add one for zero.
public int Count
{
get
{
if (_mode == Mode.Classless)
{
return (1 << (32 - _maskData));
}
int count = 0;
for (int i = 0; i < 4; i++)
{
if (((1 << i) & _maskData) != 0)
{
count += 1;
}
}
return (1 << (count * 8));
}
}
To get all possible IPAddresses
in the IPRange
, we need to identify the wildcard bits and enumerate through all possible values. In classless mode, we get the count of addresses and loop through these shifting over the index and doing a bitwise OR with the mask. Class mode is a bit more code since the wildcards can be anywhere. In class mode, we enumerate the possible values for each class and do a bitwise OR to produce the final result.
public IEnumerable<uint> GetAddressValues()
{
if (_mode == Mode.Class)
{
IPClass classWildcard = (IPClass)_maskData;
int aStart, aEnd, bStart, bEnd, cStart, cEnd, dStart, dEnd;
GetClassRange(IPClass.A, out aStart, out aEnd);
GetClassRange(IPClass.B, out bStart, out bEnd);
GetClassRange(IPClass.C, out cStart, out cEnd);
GetClassRange(IPClass.D, out dStart, out dEnd);
for (int a = aStart; a <= aEnd; a++)
{
for (int b = bStart; b <= bEnd; b++)
{
for (int c = cStart; c <= cEnd; c++)
{
for (int d = dStart; d <= dEnd; d++)
{
yield return (uint)(a | b << 8 | c << 16 | d << 24);
}
}
}
}
}
else
{
int maxValue = Count;
for (int i = 0; i < maxValue; i++)
{
yield return (uint)(((uint)i << _maskData) | _addressMask);
}
}
}
private void GetClassRange(IPClass ipClass, out int start, out int end)
{
if ((ipClass & (IPClass)_maskData) == ipClass)
{
start = 0;
end = 255;
}
else
{
int shift = ((byte)ipClass - 1) * 8;
start = end = (_maskData >> shift) & 0xFF;
}
}
IPFilter
The IPFilter
class takes multiple IPRange
s and associates Allow/Deny behavior with them. This allows us to validate one IP address against multiple IPRange
s. These are evaluated in top down order. The default behavior is returned when no match is found.
public class IPFilter
{
private string _name;
private IList<IPFilterItem> _items;
private IPFilterType _defaultBehavior;
...
public enum IPFilterType
{
NoMatch = 0,
Deny = 1,
Allow = 2
}
public class IPFilterItem
{
private IList<IPRange> _ranges;
private IPFilterType _result;
...
The IPFilter
is also configurable. Below is some example configuration:
="1.0"="utf-8"
<configuration>
<configSections>
<section name="IPFilter"
type="IPFilter.Configuration.IPFilterConfiguration,IPFilter"/>
</configSections>
<IPFilter>
<HttpModule FilterName="Default" />
<Filters>
<add Name="Default" DefaultBehavior="Deny">
<deny hosts="192.168.11.12,192.168.1.*" />
<allow hosts="192.168.0.0/16" />
<deny hosts="*" />
</add>
<add Name="LocalOnly">
<allow hosts="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/8" />
<deny hosts="*" />
</add>
<add Name="DenyLocal">
<deny hosts="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/8" />
<allow hosts="*" />
</add>
<add Name="LoopbackOnly">
<allow hosts="127.0.0.1/8" />
<deny hosts="*" />
</add>
<add Name="DenyLoopback">
<deny hosts="127.0.0.1/8" />
<allow hosts="*" />
</add>
</Filters>
</IPFilter>
</configuration>
WCF Service Behavior
A service behavior allows us to hook in to various parts of a WCF service and modify its behavior. Below is the IServiceBehavior
interface:
public interface IServiceBehavior
{
void AddBindingParameters(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters);
void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase);
void Validate(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase);
}
We are going to create a service behavior that will insert a IDispatchMessageInspector
. To do this, we need to implement the ApplyDispatchBehavior
method.
public void ApplyDispatchBehavior
(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher channelDispatcher
in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher endpointDispatcher
in channelDispatcher.Endpoints)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
}
}
The IDispatchMessageInspector
has only two methods, AfterReceiveRequest
and BeforeSendReply
. AfterReceiveRequest
fires when a message first comes in. The message is passed in by ref allowing to replace or set it to null
. When the message is set to null
, the service method is not invoked. We are going to check the IP Address and if it is in our deny list, we will set the message to null
. This method returns an object that is passed in to BeforeSendReply
after the service method is invoked allowing us to persist some state between the two methods.
public object AfterReceiveRequest(ref Message request,
IClientChannel channel, InstanceContext instanceContext)
{
RemoteEndpointMessageProperty remoteEndpoint = request.Properties
[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
IPAddress address = IPAddress.Parse(remoteEndpoint.Address);
if (_verifier.CheckAddress(address) == IPFilterType.Deny)
{
request = null;
return (channel.LocalAddress.Uri.Scheme.Equals(Uri.UriSchemeHttp) ||
channel.LocalAddress.Uri.Scheme.Equals(Uri.UriSchemeHttps)) ?
_httpAccessDeined : _accessDenied;
}
return null;
}
We are not really doing anything in our BeforeSendReply
method. If the channel is http
, then we set a 401
status code.
public void BeforeSendReply(ref Message reply, object correlationState)
{
if (correlationState == _httpAccessDeined)
{
HttpResponseMessageProperty responseProperty =
new HttpResponseMessageProperty();
responseProperty.StatusCode = (HttpStatusCode)401;
reply.Properties["httpResponse"] = responseProperty;
}
}
Service Behaviors can be applied via code but the ideal way is application configuration. WCF allows us to create strongly typed configuration section for our service behaviors. These configuration sections should inherit from BehaviorExtensionElement
. It inherits from ServiceModelExtensionElement
which in turn inherits from ConfigurationElement
. BehaviorExtensionElement
has an abstract
method and property that we need to implement. The property returns the type of the class that implements IServiceBehavior
. The abstract
method should return a new instance of this object. BehaviorExtensionElement
sections can be nested under the extensions element in the system.serviceModel
configuration section.
Below is some example configuration and our BehaviorExtensionElement
in its entirety.
...
<extensions>
<behaviorExtensions>
<add
FilterName="LocalOnly"
type="IPFiltering.IPFilterBehaviorExtension, IPFilter,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
/>
</behaviorExtensions>
</extensions>
...
public class IPFilterBehaviorExtension : BehaviorExtensionElement
{
[ConfigurationProperty("filterName", IsRequired = true)]
public string FilterName
{
get
{
return this["filterName"] as string;
}
set
{
this["providerName"] = value;
}
}
public override Type BehaviorType
{
get
{
return typeof(IPFilterServiceBehavior);
}
}
protected override object CreateBehavior()
{
return new IPFilterServiceBehavior(this.FilterName);
}
}
ASP.NET Module
The sample code also includes an ASP.NET module that uses the IPFilter
class. Again IIS has built in IP filtering that you should use if possible.
Links
History
- 15th June, 2009: Initial post