ISA Server 2004

Developing an Application Filter for Microsoft Internet Security and Acceleration Server 2004

Yigal Edery

Code download available at:ISAServer2004.exe(110 KB)

This article assumes you're familiar with C++ and COM

SUMMARY

The beta version of Internet Security and Acceleration (ISA) Server 2004 is now publicly available. It includes a rich SDK with several extensibility mechanisms that allow third parties to integrate their specialized solutions on top of the ISA platform. In this article, the author explores the application filter extensibility mechanism, which enables you to add high-level application layer filtering capabilities to ISA Server and to provide rich content filtering solutions. He also highlights the new features of the ISA Server 2004 SDK, then moves on to describe how to develop a basic application filter that monitors all data going through the ISA Server, and how to integrate a filter into the ISA Server management console to create a seamless interface experience for your users.

Contents

Application Filters Overview
Application Filter Programming Model
Application Filter Modules
The Filter Module
Filter Startup
The Data Pumping and Filter Threading Model
Performance and Robustness
Policy Storage
Global and Per-rule Policies
Events and Alerts
Logging
Registration
The Administration Module
Wrapping It Up

Microsoft® Internet Security and Acceleration (ISA) Server 2004 has just been announced and is now available as a public beta. It is the successor to ISA Server 2000 and is a full-blown, rule-based firewall with packet-level, circuit-level, and application-level filtering capabilities. It is also a powerful HTTP proxy and caching server. It is designed to be modular and extensible through a set of application filter interfaces, and through a separate mechanism for HTTP extensibility called Web filters, which are compatible with IIS ISAPI filters.

In this article I will describe how to develop application filters that extend ISA Server 2004. I will give a general overview of the process, the programming model, reasons for developing such extensions, and some tips on best practices for application filter development. I will also highlight some of the differences between the ISA Server 2004 software development kit (SDK) and the ISA Server 2000 SDK.

Several code samples and a detailed walkthrough are available on MSDN® as part of the ISA Server 2000 SDK. This article presents code snippets from a new code sample that ships with the ISA Server 2004 SDK called DataMonitor. This new sample is a generic TCP data monitoring filter. It demonstrates the use of the main interfaces of the ISA Server 2004 SDK, as well as some of the new features introduced in this version.

Application Filters Overview

Microsoft ISA Server runs on two separate layers: kernel mode and user mode. A kernel mode packet engine handles low-level traffic and can block or allow traffic based on previously defined rules. It can also pass packets that need deeper inspection to the higher-level user mode service, where the rule-engine decision making and deeper data inspection can take place.

ISA Server application filters are implemented as in-process COM server DLLs that run in user mode, in the process space of the firewall service. They react to network events and can monitor and modify the session behavior. Figure 1 shows a simplified diagram of this architecture.

Figure 1 ISA Server Architecture

Figure 1** ISA Server Architecture **

Application filters are usually tailored to hook themselves through specific application-level protocols. In fact, application filters are high-level protocol handlers. ISA Server 2004 comes with a set of application filters to handle protocols such as FTP, SMTP, SOCKS V4, RTSP, DNS, POP3, RPC, and, of course, HTTP.

In order to better understand how an application filter operates, it is important to understand how rules are defined. ISA Server 2004 has two main rule types: access rules and publishing rules. An access rule controls outbound communication from an internal network to the outside world. Publishing rules control inbound communication to a single published server and help isolate the internal server from the outside world, using ISA Server as a proxy for this server, listening for requests, filtering them, and passing only safe and validated requests to the protected server. Note that in the ISA Server management console, those two types of rules are mixed. They only have a different wizard for creating them and differ in some of their properties.

Application filters can be associated with both types of rules, so they can filter both outgoing (access) and incoming (published) traffic. In some cases, they are more appropriate for helping a publishing rule to work properly, while in other cases a filter may focus on outgoing access rules only. The filter code is almost identical, so from a developer's perspective, handling access rules and publishing rules requires the same effort and produces almost identical code, with just a few differences in the initialization phase.

Application filters can be developed to support several different scenarios, which I will talk about next. These include protocol enablers, network address translation (NAT) support, intrusion detection, and content filtering.

Protocol enablers Application filters can extend ISA Server's ability to handle complicated protocols that require more than a single TCP connection. This is required to allow firewall traversal of such protocols. The main function of these filters is to dynamically configure the ISA Server to allow future secondary connections. A classic example of such a filter is the built-in FTP app filter that handles all aspects of configuring a firewall to automatically allow a secondary FTP data channel.

NAT support Many protocols pass the IP addresses of internal servers as part of their data. In a NAT environment, these internal IP addresses are hidden from the outside world and need to be translated to externally visible addresses. An application filter can monitor the traffic and modify the relevant fields within the message to include the correct external addresses according to existing publishing rules. If you consider the FTP example again, an FTP client sitting behind the firewall may tell an FTP server to connect back to it, passing an address and port information as part of the protocol. The FTP application filter translates this information to an externally visible listening socket, enabling the file transfer to take place.

Intrusion detection Application filters can examine traffic going through the firewall and look for known attack signatures. ISA Server 2004 has two such filters included which detect known intrusion signatures for DNS and POP3.

Content filtering This family of filters is a natural candidate for use by third-party vendors and is what makes ISA Server 2004 an ideal content-filtering platform. Application filters can parse high-level application protocols, look for actual data (the payload), and apply rules and processing based on the content. Examples can include applying protocol-level syntax validation, antivirus scanning on file transfers, SOAP/XML filtering, and content categorization. The ISA Server HTTP and SMTP filters and the DataMonitor application filter both demonstrate this capability.

In all of these scenarios, the overall structure of the application filter is the same. It should typically attach itself to each firewall connection and implement the specifications and RFCs relevant to the protocols it represents in order to understand the traffic and apply rules to it. The filter should keep a session state and use it to control the data transfer through the firewall. It may modify the data flow, change the session payload, abort sessions that seem to violate the policy, or call on firewall APIs to automatically configure allow/deny rules for expected future traffic.

Note that content filtering for HTTP traffic can be achieved with less effort by developing ISAPI filters, which are called Web filters in the context of ISA Server. For HTTP-only filters, Web filters are an appropriate solution.

Application Filter Programming Model

Application filters follow an "active" data-pumping programming model, where an application filter that registers itself on a connection takes full ownership of the connection and actively pipes the data through from one side to the other. The model is very similar to that of asynchronous socket I/O, where a filter dispatches I/O requests and receives notifications upon completion of the I/O operation.

Although the firewall SDK hides the details of the worker-thread implementation from the application filter developer, it is important to be aware of how this works and to realize that I/O completions for the same connection can be called in the context of different threads.

Figure 2 Chained Filters Data Flow

Figure 2** Chained Filters Data Flow **

Application filters can be chained so that the same protocol is handled by more than one filter. This is achieved by using a "virtual socket" concept through the IFWXSocket interface. When an application filter pumps data through a socket interface, it can be a virtual socket that is connected to the next filter or it can be a real network socket that actually writes and reads data from the network. Figure 2 illustrates this data flow.

Application Filter Modules

Now let's drill down into actual implementation issues and answer the questions: what constitutes a full application-filtering solution? What are the best practices when writing such a solution, and what is expected from a fully featured filter?

An application filter will usually comprise at least two separate modules: the filter module and the administration module. The filter module is an in-process COM server that runs inside the firewall service, responding to events and performing the data pumping. The Administration module is a Microsoft Management Console (MMC) snap-in that extends the firewall management console and adds a user interface to control the filtering policy. For filters that just need to enable a protocol or apply hardcoded rules, the administration module may not be necessary.

The Filter Module

As mentioned before, the filter is an in-process COM server. It should be registered during installation in the ISA Server storage as a COM object and in the table of application filters which is held inside the ISA Server storage. Figure 3 lists the core interfaces that have to be implemented by an application filter in order to start handling the communication flow.

Figure 3 Required Interfaces

Interface Description
IFWXFilter The main entry point of the filter. It is created when the firewall starts and is destroyed when the firewall shuts down.
IFWXSessionFilter Per-session object. Session filter objects should be created by the filter in response to an IFWXFilter::AttachToSession event, which is called when a client machine first connects through the firewall using a protocol for which the filter was registered.
IFWXDataFilter Per-connection object. It should be created by the filter in response to an IFWXSessionFilter::FirewallEventHandler event, which is called every time the client connects through the firewall using a protocol for which the filter was registered. A data filter is attached to the connection using a call to IFWXConnection::AttachDataFilter.
IFWXIOCompletion Callbacks for I/O completions.

An application filter uses interfaces exposed by the firewall to communicate back with the firewall service and manage the connection. Figure 4 lists the firewall interfaces that a filter would typically have to use.

Filter Startup

When a filter is registered and the firewall service is started, the service will call the filter's IFWXFilter::FilterInit method during initialization. This call should be used by the filter as its entry point and as the opportunity to initialize all global scope objects. It also serves as the first opportunity for the filter to tell the firewall which connection events are of interest to it.

The next event that a filter will receive is IFWXFilter::AttachToSession for each new session that is established through the firewall. Note that a "session" in the context of ISA Server means a connection to a client machine and not a TCP session. In this call, a filter can modify the events it requires for this specific session, create a session object (which implements IFWXSessionFilter), and give the firewall a reference to this object.

Next, the firewall service will start sending notifications to the filter (through the IFWXSessionFilter::FirewallEventHandler) about the specific connection events the filter requested. In response to these events the filter can decide to hook on the actual data transfer and gain control over the data pumping. This is accomplished by creating an instance of an IFWXDataFilter object and attaching it to the connection using a call to IFWXConnection::AttachDataFilter. The filter will then get the two sockets of the connection in a call to IFWXDataFilter::SetSockets, and can start pumping data through.

Figure 5 shows the implementation of some data filter startup code that is based on the data monitor filter (please note that it's a slightly simplified version of the actual sample). CDMFilter implements the IFWXFilter interface, CDMSessionFilter implements the IFWXSessionFilter interface, and CDMDataFilter uses the IFWXDataFilter and IFWXIOCompletion interfaces.

Figure 5 Data Filter Startup

CDMFilter::CDMFilter() { m_FwxFilterHookEvents.dwGlobalEvents = DWORD( fwx_Connect_Tcp | fwx_AcceptedConnection | FWX_ALL_SOURCES); } // IFWXFilter::FilterInit implementation STDMETHODIMP CDMFilter::FilterInit(IFWXFirewall * pIFWXFirewall, FwxFilterHookEvents * pFilterHookEvents) { // Tell the firewall what events you want *pFilterHookEvents = m_FwxFilterHookEvents; return S_OK; } // IFWXFilter::AttachToSession implementation // Create the session filter object and attach it to the new session STDMETHODIMP CDMFilter::AttachToSession(IFWXSession *piSession, IFWXSessionFilter ** piSessionFilter, PFwxFilterHookEvents pFilterHookEvents) { HRESULT hr = S_OK; //Create an instance of the session filter CComObject<CDMSessionFilter> *pSessionFilter; hr = CComObject<CDMSessionFilter>::CreateInstance(&pSessionFilter); if (FAILED(hr)) { return hr; } pSessionFilter->AddRef(); // Give back the required events and the session filter object *pFilterHookEvents = m_FwxFilterHookEvents; *piSessionFilter = pSessionFilter; return S_OK; } // Helper function to create a data filter object and attach it // to the current connection HRESULT CDMSessionFilter::CreateDataFilter(IFWXConnection* pConnection) { // Create a data filter for this connection HRESULT hr; CComObject<CDMDataFilter>* pDataFilter; hr = CComObject<CDMDataFilter>::CreateInstance(&pDataFilter); if (FAILED(hr)) { return hr; } pDataFilter->AddRef(); // Attach to the connection object hr = pConnection->AttachDataFilter(pDataFilter,fwx_dfpc_Middle,NULL); if (FAILED(hr)) { DBGTRACEF(("AttachDataFilter failed, error = %08x\n",hr)); } // The connection now owns the DataFilter ref-count, so you can // release it. (or if failed, you still have to release it to avoid // leaks) pDataFilter->Release(); return hr; } // IFWXSessionFilter::FirewallEventHandler implementation // Accepts firewall events and dispatches data filter objects based on the // event type STDMETHODIMP CDMSessionFilter::FirewallEventHandler(const FwxFirewallEvent *pProxyEvent ) { HRESULT hr = S_OK; IFWXConnection* pConnection = NULL; switch (pProxyEvent->EventType) { case fwx_Connect_Tcp: { pConnection = pProxyEvent->Parameters.Connect.piConnection; break; } case fwx_AcceptedConnection: { pConnection = pProxyEvent >Parameters.Accept.piConnectionAccepted; break; } // Unexpected EventType default: ATLASSERT(false); return E_FAIL; } hr = CreateDataFilter(pConnection); return hr; }

It is important to understand that once a filter has attached itself to a connection, the filter has full control over it. If the filter doesn't start socket send/receive operations, the connection will time-out and die. The firewall expects the filter to own the data pump and does not actively initiate any data transfer.

The Data Pumping and Filter Threading Model

All filter I/O is performed asynchronously. The filter dispatches SEND/RECV requests using a call to the IFWXSocket objects provided by the firewall, and it will get back notification and I/O buffers via the IFWXIOCompletion interface.

Thus, a pure data pump filter with no filtering at all can start a two-way pumping loop very easily. It just sends a RECV on both sides, and for each I/O completion event passes the buffer to the other socket and issues a new RECV, continuing with this sequence until the connection is terminated.

Of course, a transparent filter that does nothing is a waste of good development time, so maybe it's better to actually do something with the data. This is what all the filter logic is used for. The filter has to actually understand all the data it sees, modify the buffers according to its policies, call different firewall interfaces, and so on. Typically, a filter would be required to implement a state machine that handles the specific protocol and conform to one or more protocol specifications or RFCs. There are several samples in the ISA Server SDK that demonstrate this.

Figure 6 shows the data pump part of the data monitor sample. The data monitor sample acts as a transparent two-way data pump, but also logs all data into a file in a human-readable format (that is not shown here, but is implemented by the DumpBuffer method). The data pump is started at the call to SetSockets, and continues at the subsequent calls to CompleteAsyncIO. Note that the filter always takes a reference to the socket objects in a synchronized way because Detach may be called at any point, so the sockets may be freed in the case where the connection was dropped. Last, note how the context of the I/O operation is passed between the calls to Send and CompleteAsyncIO using the UserData parameter.

Figure 6 Data Pump

/// SetSockets called by the firewall when the data filter is attached // to a connection, and is given the connection sockets STDMETHODIMP CDMDataFilter::SetSockets(IFWXSocket *piInternalSocket, IFWXSocket *piExternalSocket, IFWXConnection *piConnection, IUnknown *punkFilterContext) { HRESULT hr; Lock(); if (!m_fDetached) // for example, if not detached before you got the // lock { // Take reference to the two sockets m_spInternalSocket = piInternalSocket; m_spExternalSocket = piExternalSocket; } Unlock(); // Kick-off the I/O data pump hr = StartIO(); if (FAILED(hr)) { CloseSockets(true); } return hr; } // CompleteAsyncIO is called by the firewall for each send/recv operation // that has completed (either successfully or failed). // It is the main point for driving the filter data pump and analyzing // the connection data. STDMETHODIMP CDMDataFilter::CompleteAsyncIO(BOOL fSuccess, DWORD Win32ErrorCode, IFWXIOBuffer * pIOBuffer, UserContextType UserData, LPSOCKADDR ExternalAddress, INT ExternalAddressLength) { // You did not ask for notifications on completion of send ATLASSERT((UserData == ocReadFromInternal) || (UserData == ocReadFromExternal)); HRESULT hr; // Handle failures on receive if ((!fSuccess) || (pIOBuffer == NULL)) { CloseSockets(true); return S_OK; } // Dump the data and drive the data pump BYTE* pBuffer = NULL; DWORD dwBuffSize = 0; hr = pIOBuffer->GetBufferAndSize(&pBuffer,&dwBuffSize); if (SUCCEEDED(hr) && (dwBuffSize > 0)) { DumpBuffer(pIOBuffer, (UserData == ocReadFromInternal)); if (UserData == ocReadFromInternal) { hr = WriteToExternal(pIOBuffer); if SUCCEEDED(hr) { hr = ReadFromInternal(); } } else { hr = WriteToInternal(pIOBuffer); if (SUCCEEDED(hr)) { hr = ReadFromExternal(); } } if (FAILED(hr)) { // Close the two sockets on any failure CloseSockets(true); } } else { // If you got a zero-sized buffer, than socket has no // more data and you should end the data pump CloseSockets(false); } return S_OK; } // Kick-off the data pump by receiving data on both sides HRESULT CDMDataFilter::StartIO() { HRESULT hr; hr = ReadFromInternal(); if (FAILED(hr)) { return hr; } hr = ReadFromExternal(); if (FAILED(hr)) { return hr; } return S_OK; }; // Send a buffer to the internal socket // Note: CDMDataFilter::WriteToExternal not shown here, but similar HRESULT CDMDataFilter::WriteToInternal(IFWXIOBuffer* pBuffer) { HRESULT hr; CComPtr<IFWXSocket> spSocket; GetInternalSocket(&spSocket); if (spSocket) { hr = spSocket->Send(pBuffer,NULL,ocWriteToInternal); if (FAILED(hr)) { return hr; } } return S_OK; } // Recv a buffer from the internal socket // Note: CDMDataFilter::ReadFromExternal not shown here, but similar HRESULT CDMDataFilter::ReadFromInternal() { HRESULT hr; CComPtr<IFWXSocket> spSocket; GetInternalSocket(&spSocket); if (spSocket) { hr = spSocket->Recv(NULL, this, ocReadFromInternal); if (FAILED(hr)) { return hr; } } return S_OK; } // Get a reference to the internal socket, synchronized // Note: CDMDataFilter::GetExternalSocket not shown here, but similar. void CDMDataFilter::GetInternalSocket(IFWXSocket** ppSocket) { Lock(); (*ppSocket) = m_spInternalSocket; if (*ppSocket != NULL) (*ppSocket)->AddRef(); Unlock(); } // Close the two sockets void CDMDataFilter::CloseSockets(bool fAbortive) { IFWXSocket* pSocket; GetInternalSocket(&pSocket); if (pSocket) { pSocket->Close(fAbortive); pSocket->Release(); } GetExternalSocket(&pSocket); if (pSocket) { pSocket->Close(fAbortive); pSocket->Release(); } }

You should note two things about the implementation. First, a filter can implement both the IFWXIOCompletion and IFWXDataFilter in the same class. This usually makes the filter code simpler and makes it easier to handle the asynchronous I/O callbacks because they occur within the same object that is responsible for a single connection. However, the filter must be aware of and handle threading issues. This is an asynchronous model, so multiple callback events can be triggered simultaneously on different threads for the same connection and even the order of their arrival is unpredictable.

Second, a simplified model can be implemented, as in the data monitor example, by dispatching only a single I/O request on each socket at any given time (in a one-way alternate data pump, this would be the side that is expected to send traffic next). This will make almost all threading issues trivial because the filter will never receive simultaneous I/O completion events. However, even in this case, the filter should always be ready to handle an asynchronous IFWXDataFilter::Detach event that may be called by the firewall in order to signal an aborted connection. The Detach event can be called at any time, even during a call to IFWXDataFilter::SetSockets, therefore access to the socket objects supplied by the firewall should always be synchronized.

Performance and Robustness

As mentioned earlier, an application filter runs inside the process of the firewall service and fully controls the data pumping for each connection it owns. It isn't uncommon for the firewall service to operate under heavy stress conditions, serving thousands of simultaneous connections, so the performance and robustness of the firewall largely depends on good implementation of the application filters that are installed.

A good application filter should always check for error returns, including low memory situations, and handle failures gracefully. If a filter doesn't do this, it may cause the firewall to become unstable and present a security risk.

The application filter should never trust the data coming over the connection and should never assume that the data is well formatted. Failure to check correct bounds might allow buffer overflows inside the firewall service, which can be exploited for attacks against the firewall itself, not to mention causing simple crashes of the firewall service. In fact, one of the tasks of an application filter, especially in a server publishing scenario, is to validate the protocol data, ensuring that it is well formed, and preventing malformed packets from reaching the destination server.

Last, when parsing the data it is important to take performance considerations into account. It is much more efficient to parse data on the fly, chunk by chunk, and avoid unnecessary buffering of data or copying of large memory blocks. Always try to do all of the protocol parsing work on the I/O buffers supplied during I/O completion and send the same buffers to the other side in cases in which they were not modified. Implementing these techniques and paying attention to other performance issues will enable the firewall to scale better and serve more simultaneous connections.

Policy Storage

Assuming you've implemented all the filter protocol parsing logic, you will probably want to add some configuration data to control the filter operation. This is accomplished using the ISA Server's storage extension, which lets a filter add its own configuration data to the firewall storage. This provides easy access to the storage from both the filter and the administration module using COM interfaces, and lets you propagate this configuration information to other members in a firewall array when using an array configuration (in ISA Server Enterprise Edition).

Think of the ISA Server storage model as a hierarchical tree of typed nodes. Each node can serve as a container for child nodes and can also have its own properties. At the root of this tree there is the single Array object, under which you will find containers for policies, policy elements, application filters, and so on.

ISA Server 2000 storage was implemented over the registry or Active Directory®, depending on the type of installation (standalone or array, each of which are described in the ISA Server product and SDK documentation). ISA Server 2004 storage is implemented over a combination of the registry for small values and file system for large objects. The ISA SDK administration COM model hides these implementation details from the developer, so all access to storage must be performed through these COM interfaces.

The COM model of ISA Server enables access to this hierarchical storage and represents each node with a matching COM interface. Through each of these interfaces, you can enumerate all child nodes and get to the storage point you're interested in. These COM objects are also scriptable, so you can use scripting languages to access the ISA storage and fully control the firewall behavior.

For extensibility purposes, each of the ISA Server objects inherits from the IFPCPersist interface, enabling you to attach your own information to each and every node.

Extending the storage is achieved through the use of a mechanism called VendorParametersSets. Each node can have a collection of third-party (Vendor) properties. This is implemented as a bi-level storage hierarchy. First, an application filter attaches a VendorParametersSet with a unique ID (typically a GUID) to the VendorParametersSets list of the node. This VendorParametersSet is basically a key/value list that can be used to store all the extra information the filter requires.

From an implementation perspective, both the filter module and the administration module will probably require access to the storage with minor differences (for example, the administration module will probably require read-write access, while the filter requires read-only access). Because of this common nature of the policy access, it is usually more desirable to implement all of the policy storage access code in some common library that will be used by both modules.

Also you should note that since the filter is executed in the context of the firewall service, which runs as a network service account on Windows Server™ 2003, it does not have write permissions to the storage. It is only able to read existing policies and must have built-in hardcoded defaults that will be used in case the configurable policy is missing from the storage.

Global and Per-rule Policies

In ISA Server 2000, a filter could only implement global policies. This means that for all connections being handled by an application filter, the same set of configuration settings would be used. In ISA Server 2004, a filter can also implement per-rule policies, allowing for a more flexible configuration with different settings for different rules.

The decision to implement global or per-rule policy should be based on the nature of the filter's functionality. In some cases it is more natural to implement global policies, where in other cases a per-rule policy would make more sense. For example, in the data monitor code sample, a combination of both is implemented. A global setting allows configuration of the name of the log file, while a per-rule setting allows you to limit the size of data chunks written into this log file, making it possible to log traffic that was allowed by some rules and not by others.

From a storage point of view, global policies should be stored as a single VendorParametersSet under the application filter's object. Per-rule policies of an application filter should be stored under the storage node of each rule.

When the administrator changes the configuration of the firewall, he will typically apply those new settings. The result of this is that the new policy should immediately take effect. In order to support this, the ISA SDK includes several mechanisms for storage change notifications.

For per-rule policies, there is a specialized interface in the SDK for handling most aspects of policy changes. Implementing per-rule policies is a two-step process. First, a filter is notified whenever any rule is changed, using a callback defined in the IFWXPerRuleData interface. A filter that implements this interface is called for each rule during startup, and then every time the rule is changed. The filter should load its policies attached to the given rule (for example, read the VendorParametersSet), and return to the firewall a reference to a pre-loaded filter policy object that implements reference counting (IUnknown).

Next, for each session event in IFWXSessionFilter::FirewallEventHandler, the event structure contains a pointer to the previously loaded policy object, so the filter can take a reference to it and give it to the data filter object. This policy should be used for the lifetime of the data filter object, which usually means the lifetime of a single connection.

The code in Figure 7 shows the per-rule policy implementation of the DataMonitor sample. Note that IFWXPerRuleData is implemented by the CDMFilter class and that the per-rule processing was added to the FirewallEventHandler method and passed to the data filter object.

Figure 7 Handling Per-rule Policies

// Header file declaration of CDMFilter class ATL_NO_VTABLE CDMFilter : public CComObjectRootEx<CComMultiThreadModel>, public CComCoClass<CDMFilter, &CLSID_DMFilter>, public IFWXFilter, public IFWXPerRuleData { public: ... BEGIN_COM_MAP(CDMFilter) COM_INTERFACE_ENTRY(IFWXFilter) COM_INTERFACE_ENTRY(IFWXPerRuleData) END_COM_MAP() ... // IFWXPerRuleData STDMETHOD(PrepareRulesData)(IN IFPCPolicyRule *pPolicyRule, OUT IFWXPerRuleDataplugin **ProcessedRulesData); ... }; // For each rule callback, load the per-rule policy object and give a // reference to it back to the firewall STDMETHODIMP CDMFilter::PrepareRulesData(IN IFPCPolicyRule *pPolicyRule, OUT IFWXPerRuleDataplugin **ProcessedRulesData) { HRESULT hr; // Allocate a per-rule policy object for this rule CComPtr<CDMPerRulePolicy> spPolicy = new CComObject<CDMPerRulePolicy>; if (spPolicy == NULL) { return E_OUTOFMEMORY; } // Load the rule from the rule storage hr = spPolicy->Load(pPolicyRule); if (FAILED(hr)) { DBGTRACEF(("No policy, using default (hr=%08x)\n",bstrRuleName,hr)); } // Return a reference of the loaded policy to the firewall *ProcessedRulesData = spPolicy.Detach(); return S_OK; } // The modified version of FirewallEventHandler with per-rule policies // handling STDMETHODIMP CDMSessionFilter::FirewallEventHandler(const FwxFirewallEvent *pProxyEvent ) { HRESULT hr = S_OK; IFWXConnection* pConnection = NULL; CDMPerRulePolicy* pPerRulePolicy = NULL; switch (pProxyEvent->EventType) { case fwx_Connect_Tcp: { pConnection = pProxyEvent->Parameters.Connect.piConnection; pPerRulePolicy = (CDMPerRulePolicy*)pProxyEvent-> Parameters.Connect.PerRuleProcessedData; break; } case fwx_AcceptedConnection: { pConnection = pProxyEvent >Parameters.Accept.piConnectionAccepted; pPerRulePolicy = (CDMPerRulePolicy*)pProxyEvent-> Parameters.Accept.PerRuleProcessedData; break; } // Unexpected EventType default: ATLASSERT(false); return E_FAIL; } hr = CreateDataFilter(pConnection,pPerRulePolicy); return hr; }

If a filter also has global policies, it should typically register on the node (or nodes) where this policy is stored and wait for configuration changes. In most cases, this would be the application filter node, but VendorParametersSet blocks can be added to any other node in the firewall storage. The filter should register for configuration change notifications using a call to IFPCVendorParametersSet::WaitForChanges, which inherits from the IFPCPersist interface. The filter should then start a thread that will wait on the change notification event. The notification event will be signaled by the firewall once the parameters have changed.

When the filter gets a notification about such a change it should immediately start acting on it. If you want to follow the same logic as the filters that ISA provides, then the general rule of thumb is that new connections should use the new policy while existing connections can continue to use the old policy. In some cases, even existing connections should react to these changes and modify their behavior. It is up to the developer of the filter to decide which is more appropriate.

One design pattern for implementing such policy-responding logic is to create a synchronized, reference-counted tear-off policy object. Each new connection can take a reference to the currently active policy and release it once the connection dies. When a policy changes, new connections will get a reference to and use the new policy object while existing live connections will keep using the old policy until they are closed.

If the filter needs to change the behavior of live connections, it can implement some synchronization points at which each of those live connections polls for a change of configuration and then takes a reference to the new policy object.

Events and Alerts

An application filter can communicate information back to the firewall administrator through a mechanism of predefined events and administrator-configurable alerts. An ISA Server event can be signaled by the application filter in response to some critical events it detects, such as intrusion detection, policy violation, internal errors, and so on.

These events are collected by the ISA Server event manager and are matched against alerts, which are a set of conditions that dictate when to inform the administrator about the events being signaled. For example, a filter can raise an event whenever it detects a malformed packet. A single malformed packet may be something that can (and probably should) be ignored, to avoid inundating the administrator with irrelevant information. However, if this event repeats itself, a real attack may be underway. Therefore, an alert can be defined so that if more than 1,000 malformed packet events are raised within a specified time limit, say 60 seconds, an administrative action will be required.

The result is that the firewall administrator sees a list of active alerts in the firewall management console and can respond to them by taking the appropriate action, marking them as handled.

When you're designing an application filter, you should consider events that the filter may want to raise. Such filter events should be registered within the firewall storage during filter installation. This registration can be accomplished through the use of the FPCEventDefinitions interface.

You should also install some predefined default alerts for the events provided by the filter. This can save the administrator the time it would take to understand and define his own alerts. She can always edit those alert definitions later, disable or remove them, or even add her own alerts.

At run time, when the filter detects a situation that requires an event to be signaled, it makes a call to FPCAlertNotification::SignalEvent. When the filter raises an event, it also has to be registered as a Windows® event source and must provide the matching Windows event. This is to help the firewall report the event to the application event log in addition to the firewall management console.

Logging

Unfortunately, there is no built-in extensibility mechanism for activity logging in the ISA Server SDK. A filter that needs to log its own operations has to handle this task itself.

It is highly recommended that you include some kind of logging mechanism in your application filter in order to assist in the debugging of communication flow problems when the filter is active and, of course, to always analyze the logs and produce reports using external tools.

Registration

A filter should handle its own installation and registration. There is no user interface provided in ISA Server to enable manual installation by the firewall administrator.

Installation can be performed using scriptable objects or the filter can be designed to register itself. Built-in filters provided by Microsoft with ISA Server 2004 take the self-registration approach in the DllInstall exported function, called when you use regsvr32 with the /I parameter. It is up to the developer to decide which forms of registration fit their needs and coding style. In all cases, registration is performed using a set of exposed COM interfaces. Figure 8 lists those interfaces.

Figure 8 Interfaces for Filter Registration

Interface Description
IFWXFilterAdmin Filter registration object
FPCEventDefintions Registration of filter events
FPCAlerts Registration of alerts
FPCApplicationFilter Access to the filter's VendorParametersSet in order to initialize the default policy storage

Earlier versions of ISA Server had an enterprise mode in which ISA Server policies were managed for the enterprise using Active Directory. In some cases, such as when a filter added new policy elements, the registration process had to include the registration of enterprise-level objects. ISA Server 2004 (which is currently available in standard edition only) does not require this special enterprise registration code.

In addition to registering the application filter module, a filter should also handle the registration of any other storage objects it may use (such as its default policy, events it may raise, default alerts, and so on). Figure 9 shows a typical filter registration implementation. In order to just register the filter, IFWXFilterAdmin is all that is needed. However, in order to access the storage to add VendorParametersSets and events, the FPC object should be used, as demonstrated in the Initialize method.

Figure 9 Filter Registration

// Entry point for registering the application filter HRESULT CFilterReg::RegisterFilter () { HRESULT hr; hr = Initialize(); if (FAILED(hr)) { return hr; } CComBSTR bstrFilterName("DataMonitor Sample"); CComBSTR bstrFilterDescription("Monitors data flow through ISA Server"); CComBSTR bstrFilterVendor("Microsoft"); CComBSTR bstrFilterVersion("1.0"); // Protocols to hook on by default GUID ProtocolsToFilter[] = { SMTP_PROTOCOL_GUID_BIN, SMTP_SERVER_PROTOCOL_GUID_BIN }; hr = m_spFWXFilterAdmin->InstallFilter(CLSID_DMFilter, // GUID bstrFilterName, // Name bstrFilterDescription, // Description bstrFilterVendor, // Vendor bstrFilterVersion, // Version NULL, // Reserved parameter, // must be NULL ProtocolsToFilter, sizeof(ProtocolsToFilter) / sizeof(ProtocolsToFilter[0])); //OK if already installed if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) hr = S_OK; if (FAILED(hr)) { DBGTRACEF(("Failed to register filter, error = %08x\n",hr)); return hr; } hr = RegisterEvents(); if (FAILED(hr)) { return hr; } DBGTRACE("filter registered successfully\n"); return S_OK; } // Local members initialization // Creates the COM objects needed for the registration process HRESULT CFilterReg::Initialize() { HRESULT hr; hr = CoCreateInstance(CLSID_FWXFilterAdmin, NULL, CLSCTX_INPROC_SERVER, IID_IFWXFilterAdmin, (LPVOID *) &m_spFWXFilterAdmin); if (FAILED(hr)) { return hr; } CComPtr<IFPC> spFPC; hr = CoCreateInstance(CLSID_FPC, NULL, CLSCTX_INPROC_SERVER, IID_IFPC, (LPVOID *)&spFPC); if (FAILED(hr)) { return hr; } // Get containing array hr = spFPC->GetContainingArray(&m_spFPCArray); if (FAILED(hr)) { return hr; } return S_OK; } // Perform registration of filter event HRESULT CFilterReg::RegisterEvents() { // First, create an event object that the filter will signal in case // of failure to open the log file CComBSTR bstrEventGuid = EVENT_DM_LOGERROR_STR; CComBSTR bstrEventName = "DataMonitor Log Error"; CComBSTR bstrEventDescription = "DataMonitor failed to open log file"; HRESULT hr = S_OK; // Open rule elements container CComPtr<IFPCRuleElements> spFPCRuleElements; hr = m_spFPCArray->get_RuleElements(&spFPCRuleElements); if (FAILED(hr)) { return hr; } // Open event definitions CComPtr<IFPCEventDefinitions> spFPCEventDefinitions; hr = spFPCRuleElements->get_EventDefinitions(&spFPCEventDefinitions); if (FAILED(hr)) { return hr; } // Add the new event CComPtr<IFPCEventDefinition> spFPCEventDefinition; hr = spFPCEventDefinitions->Add(bstrEventName, bstrEventGuid, NULL, &spFPCEventDefinition); if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) { DBGTRACE("Event already exists. Skipping event registration\n"); } else if (FAILED(hr)) { DBGTRACEF(("RegisterEvent failed to add event [%S], hr=0%08X\n", bstrEventName, hr)); return hr; } else { // Set event's description and save it hr = spFPCEventDefinition->put_Description(bstrEventDescription); if (FAILED(hr)) { return hr; } hr = spFPCEventDefinitions->Save(); if (FAILED(hr)) { return hr; } } // ... Continue with creation of more events and alerts as needed return S_OK; }

The Administration Module

ISA Server 2004 administration is implemented as an Microsoft Management Console snap-in. It uses all of the MMC services for user interface management and presents the policies to the user as a hierarchical tree of nodes representing information such as firewall rules, real-time monitoring data, Virtual Private Network (VPN) settings, and configuration information. This management snap-in can be extended using the MMC SDK, which also allows you to add user interface capabilities.

As described earlier, ISA Server 2004 also exposes many administration objects, which are designed to allow external access to the firewall storage. These objects are accessible through COM interfaces and are also scriptable. The storage itself is extensible, enabling a new application filter solution to add its own storage anywhere in the firewall storage.

A filter admin module ties the MMC and the storage together. It is a snap-in extension to the main ISA Server snap-in, adding the capability to configure the filter, which means writing to the ISA storage from the ISA management console. The integration is usually simple, requiring only minimal interaction with the firewall management console. A filter management module typically just adds some property pages to the policy rules and to the application filter object property sheet.

Creating MMC snap-in extensions is a widely discussed topic and is covered thoroughly in the ISA Server SDK. It is also demonstrated in the DataMonitor code sample within the data monitor admin module. The SDK includes a header file called FpcNodesGuids.h, which lists the GUIDS of all the extensible nodes of the ISA main snap-in.

Wrapping It Up

By developing application filters for Microsoft ISA Server 2004, third-party vendors can extend ISA Server's ability to better support proprietary protocols, offer increased security with application-level understanding of protocol commands and communication flow, and add content-filtering capabilities to protect both servers and clients from external attacks.

The Microsoft ISA Server platform provides easier deployment of solutions on an existing robust and reliable firewall infrastructure, and mitigates issues involved with scaling solutions, load balancing multiple servers, and managing an array of servers.

For related articles see:
TechNet Webcast: ISA Server 2004

For background information see:
Microsoft Internet Security and Acceleration Server 2000

Yigal Edery is the program manager for the Microsoft ISA Server SDK and application filters. He has more than 15 years of experience in the software industry, with expertise in the application layer security space. He can be reached at yigale@microsoft.com.