Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

WCF Service Behavior Example: IPFilter - Allow/Deny Access by IP Address

4.92/5 (17 votes)
15 Jun 2009CPOL7 min read 115.5K   2.3K  
WCF Service Behavior Example: IPFilter - Allow/Deny Access by IP Address

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.10x100007F
24.1.5.1 0x1050118
192.168.0.230x1700A8C0

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:

C#
[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.

C#
public bool IsMatch(uint address)
        {
            if (_mode == Mode.Class)
            {
                // Check the mask.
                if ((address & _addressMask) != _addressMask)
                {
                    return false;
                }

                // Check for zeros in mask
                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;

            }
            // Shift over the host identifier and so we are only 
            // comparing the network identifier portion of the ip address
            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:

C#
public static IPRange Parse(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value");
            }

            // Check if is a wildcard.
            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");
                }

                // Remove any bits that are not apart of the network identifier
                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.

C#
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.

C#
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 IPRanges and associates Allow/Deny behavior with them. This allows us to validate one IP address against multiple IPRanges. These are evaluated in top down order. The default behavior is returned when no match is found.

C#
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:

XML
<?xml version="1.0" encoding="utf-8" ?>
<!-- Example configuration -->
<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>

      <!-- A filter than only allows traffic from local network -->
      <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>

      <!-- A filter than denies traffic from local network -->
      <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>

      <!-- A filter than only allows traffic from loopback -->
      <add Name="LoopbackOnly">
        <allow hosts="127.0.0.1/8" />
        <deny hosts="*" />
      </add>

      <!-- A filter than denies traffic from loopback -->
      <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:

C#
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.

C#
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.

C#
public object AfterReceiveRequest(ref Message request, 
	IClientChannel channel, InstanceContext instanceContext)
        {
            // RemoteEndpointMessageProperty new in 3.5 allows us 
            // to get the remote endpoint address.
            RemoteEndpointMessageProperty remoteEndpoint = request.Properties
		[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;

            // The address is a string so we have to parse to get as a number
            IPAddress address = IPAddress.Parse(remoteEndpoint.Address);

            // If IP address is denied clear the request message 
            // so service method does not get execute
            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.

C#
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.

XML
...
   <extensions>
     <behaviorExtensions>
       <add
         FilterName="LocalOnly"
         type="IPFiltering.IPFilterBehaviorExtension, IPFilter, 
		Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
        />
     </behaviorExtensions>
   </extensions>
...
C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)