Wandering Code

Write Mobile Agents In .NET To Roam And Interact On Your Network

Matt Neely

This article discusses:

  • Introduction to mobile agents
  • Creating a simple mobile agent system
  • Mobile agent development patterns
  • Security implications and solutions
This article uses the following technologies:
.NET Framework

Code download available at:MobileAgents.exe(145 KB)

Contents

Introduction to Mobile Agents
A Simple Mobile Agent System
Assembly Resolution
The Traveling Pattern
The Task Pattern
The Interactive Pattern
Advanced Topics
Security and Mobile Agents
Conclusion

Recently I had the opportunity to return to school to complete a graduate degree. This experience made me realize two important things: there are some cool ideas in academia that seem to never find the light of day in the professional setting, and the academic world at large is not yet very familiar with the Microsoft® .NET Framework. Thus was born my goal of introducing lesser-known ideas to the professional masses while introducing the .NET Framework to the academic world. One particular concept has become the pivot point of this goal: mobile agents, a distributed computing paradigm.

The term agent originates in artificial intelligence and describes a logical entity that has some level of autonomy within its environment or host. A mobile agent has the added capability to move between hosts. In a computing context, a mobile agent is a combined unit of data and code that can move between different execution environments. This mobility gives mobile agents several potential advantages: reduced network traffic, decentralization, increased robustness and fault-tolerance, and an easy deployment scenario.

In this introduction to mobile agents, I'll create an example mobile agent system and develop several mobile agent applications, highlighting some of the problems that I encountered along the way as well as several ideas for overcoming them. Lastly, I'll touch on some advanced concepts that need to be addressed when creating a serious mobile agent system.

Introduction to Mobile Agents

Figure 1 Traveling Agent Hops Between Machines

Figure 1** Traveling Agent Hops Between Machines **

There are three main design patterns for mobile agents which can be combined for any implementation. These patterns are named after the agent's primary purpose: traveling, task, and interaction. A traveling agent is mainly concerned with location. A task agent is used for processing (doing work or tasks). An interaction agent collaborates or interacts with other agents

Figure 2 Task Agent Allocates Tasks

Figure 2** Task Agent Allocates Tasks **

An example of a traveling agent app could perform operations control. An agent is sent out with a list of machines on the local network it should traverse to inventory hardware and software (see Figure 1). A task agent application (see Figure 2) could be one that performs massive scientific calculations. An agent can determine what hosts are available on the network and subdivide its tasks accordingly, which essentially creates a load-balancing, parallel-processing application. To understand an interaction agent, think of a marketplace. In this environment, a buying agent can interact with various selling agents to determine the best price for some item (see Figure 3). The buying agent can then purchase from the chosen selling agent.

Figure 3 Interaction Agent

Figure 3** Interaction Agent **

So what does .NET have to offer mobile agents? First, having an object-oriented framework makes construction of a mobile agent application much more intuitive. Next, there are built-in services that facilitate the componentization and mobility of code, namely object remoting and serialization. (Sending an object over the wire is a snap with .NET. Name that object Agent and you're close to having a mobile agent application.) In addition, threading and synchronization are crucial, as each agent is supposedly autonomous. And finally, the security features of the .NET Framework will be a great boon to the mobile agent environment. As I'll discuss later, downloading and running unknown code is far from a secure thing to do.

A Simple Mobile Agent System

Let me first say that my particular design of a mobile agent system is not a standard, absolute, or ideal version. It is merely one implementation of many. That disclaimer aside, I began my mobile agent application as a solution in Visual Studio® with three projects: a server process console application, a client process console application, and a common class library to be used by the first two projects. In the class library, I start out with two classes: Agent and AgentHost.

The Agent class is essentially going to be the interface that all agents must adhere to so that the host knows how to run them. Because I want to include some actual common functionality to all agent subclasses, I'm making the Agent class an abstract class and not merely an interface. The Agent class represents a mobile agent and so must have at least two areas of functionality: executing code and moving. As such, I gave the Agent class two brilliantly named methods: Run and Move. The Run method is marked abstract and therefore must be overridden in agent subclasses. Run will be invoked by an AgentHost once an Agent arrives there. The second method, Move, contains the internal communication code for this system, which I'll discuss later.

The AgentHost class (shown in Figure 4) needs only one method to start with to host an Agent class. I've called it simply "HostAgent." It has a single parameter, agent, of type Agent. When this method is invoked, it will call the Run method on the Agent class.

Figure 4 AgentHost Class

public class AgentHost : MarshalByRefObject { public static void Initialize(int port, string objectUri) { //Create a new TcpChannel on the given port and register it // with the runtime TcpServerChannel c = new TcpServerChannel("MyAgentHostChannel", port); ChannelServices.RegisterChannel(c); //Register the agent host type with the remoting runtime RemotingConfiguration.RegisterWellKnownServiceType( typeof(AgentHost), objectUri, WellKnownObjectMode.SingleCall); } public void HostAgent(Agent agent) { agent.Run(); } }

The communication mechanism that I'm using is .NET remoting. However, it is important to keep in mind that a mobile agent system can be created on top of any communication layer. Now that the Windows Communication Foundation (formerly code-named "Indigo") is available in beta, you might consider using that instead.

In order to implement the remoting capability, I added a method to the AgentHost class called Initialize, which will properly set up a TcpChannel on the given port with the given uniform resource identifier (URI). It will then register the AgentHost type with the remoting runtime so that the host will be externally available.

The client-side code for communication (which resides within the Move method of the Agent class) is even easier, thanks to the Activator type. Activator has a GetObject method overload that takes a URL. This method will return a proxy to an AgentHost object on the server. The Agent uses the proxy to call the HostAgent method, giving itself as the Agent parameter (see Figure 5).

Figure 5 Agent Class

[Serializable] public abstract class Agent { public event System.EventHandler AgentMoved; public Agent() { } public abstract void Run(); public void Move(string urlOfHostToMoveTo) { AgentHost h = (AgentHost)Activator.GetObject( typeof(AgentHost), urlOfHostToMoveTo); h.HostAgent(this); } }

You can find the complete code for the server process that will host the AgentHost class in Figure 6. Also, the following code snippet shows the relevant client process code, which references a yet-to-be-discussed agent subclass and sends it to a host at a given remoting endpoint:

MyFirstAgent agent = new MyFirstAgent(); agent.Move("tcp://localhost:10000/MyAgentSample");

Figure 6 Agent Server Process

class AgentServer { static void Main(string[] args) { int port = 10000; if (args.Length > 0) port = int.Parse(args[0]); MobileAgents.AgentHost.Initialize(port, "MyAgentSample"); Console.Out.WriteLine("Press enter to stop..."); Console.In.ReadLine(); } }

Now, the only thing that is missing is an actual Agent implementation. The MyFirstAgent class (see Figure 7) gives a very simple implementation of a mobile agent. On creation, it merely records the name of the process that it was created in. When it runs, it gets the name of the process it now runs in. It outputs the two of them to the console in a formatted string. Note that any Agent subclass should also have the Serializable attribute in order for the runtime to properly serialize it.

Figure 7 MyFirstAgent Class

[Serializable] class MyFirstAgent : MobileAgents.Agent { string _startingProcess = string.Empty; public MyFirstAgent() { using(Process p = Process.GetCurrentProcess()) _startingProcess = p.ProcessName; } public override void Run() { using(Process p = Process.GetCurrentProcess()) Console.WriteLine( "I started in '{0}' but now am in '{1}'!", _startingProcess, p.ProcessName); } }

In order to run this mobile agent system, you need to start a host with the server process executable. This will establish the system's communication to the outside world so that it can begin to accept mobile agents for hosting. Once the host is ready, the client process is run to create a MyFirstAgent instance and then move it to the host.

Assembly Resolution

If you try to run this seemingly complete example, you get an assembly load exception. The problem is that even though the AgentHost only needs the Agent base class for compilation (and that is in the same assembly), at run time it will also need to access the assembly of the derived base types. Without this information, types can not be correctly laid out in memory and any inheritance functionality would break.

So the problem becomes, "how do I get the client-side, custom-defined agent assembly's bits to the host in order for correct .NET assembly resolution?" This problem can be broken into two parts, assembly transfer and assembly management. Assembly transfer deals with how you get the assemblies that define Agent subclasses into the realm of the hosting AppDomain. The solution depends on the scope of your mobile agent system and what you can assume. For example, your system may be architected such that it knows of a file share, Web, or FTP site from which to download the assemblies. Precluding the existence of a commonly known area for assembly download, the client will have to hand over the bits directly to the server. This is the approach I took in this example as it minimizes any other dependency or deployment headache. However, it also presents some security concerns, to be discussed later on in this article.

To facilitate the transfer of assemblies on the host side, I added two methods to the AgentHost class: IsAssemblyInstalled and UploadAssembly. The first method takes an assembly's full name ("mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") and returns a Boolean indicating whether the assembly is installed on the system. The second method will actually save the assembly's bits in a unique location for easy loading. These two methods are closely linked to assembly management.

On the client side of the communication channel, I altered the Agent's Move method to gather information about the agent assembly's references (see Figure 8). If the agent assembly is not installed on the host (determined by a call to the host's IsAssemblyInstalled method) then the code will recursively walk the agent assembly's dependency graph and track which assemblies need to be uploaded to the host. For each assembly found, a call to IsAssemblyInstalled is made. The assembly file will be uploaded (via the host's UploadAssembly method) if IsAssemblyInstalled returns false. This process is simple with the combined power of the System.Reflection and System.IO namespaces. (In a robust system, care should be taken to not upload different versions of core .NET runtime files. For example, if the Agent subclass depends on the Visual Studio 2005 runtime, you should probably not try to upload and install Visual Studio 2005 assemblies via this mechanism.)

Figure 8 Updated Agent with Reflection

[Serializable] public abstract class Agent { public event System.EventHandler AgentMoved; public Agent() { } public abstract void Run(); public void Move(string urlOfHostToMoveTo) { AgentHost h = (AgentHost)Activator.GetObject ( typeof(AgentHost), urlOfHostToMoveTo); TransferAgentAssembliesToHost(h); h.HostAgent(this); // move to the host } private void TransferAgentAssembliesToHost(AgentHost h) { //Get the full name of the assembly this agent is defined in. AssemblyName thisAssemblyName = GetType().Assembly.GetName(); //If this hasn't been installed yet, walk the //assembly dependency graph to see what assemblies are needed if (!h.IsAssemblyInstalled(thisAssemblyName.FullName)) { Dictionary<string, AssemblyName> assembliesFound = new Dictionary<string, AssemblyName>(); FindReferencedAssemblies(assembliesFound, GetType().Assembly.GetName()); foreach (AssemblyName an in assembliesFound.Values) { if (!h.IsAssemblyInstalled(an.FullName)) h.UploadAssembly(an.FullName, GetRawAssembly(Assembly.Load(an))); } } } private void FindReferencedAssemblies( Dictionary<string, AssemblyName> assembliesFound, AssemblyName assemblyToSearch) { //Check to see that we haven't encountered this assembly yet. if (!assembliesFound.ContainsKey(assemblyToSearch.FullName)) { assembliesFound.Add(assemblyToSearch.FullName, assemblyToSearch); AssemblyName[] references = Assembly.Load( assemblyToSearch).GetReferencedAssemblies(); foreach (AssemblyName reference in references) FindReferencedAssemblies(assembliesFound, reference); } } ... }

Another thing to note is that this mechanism does not allow for in-memory assemblies or unreferenced assemblies loaded via reflection. Therefore, avoid such programming constructs in an agent implementation. In fact, I would recommend denying agents the ability to use most reflection APIs for security and robustness reasons. I'll discuss security later in this article.

As mentioned before, assembly management is the one sub-problem of assembly resolution. Now that the AgentHost has been given the bits of the assemblies, what does it do with them? Again, this depends on your implementation. If your system requires that you only accept strong-named assemblies, then you can save the bits to disk and install them into the Global Assembly Cache (GAC). However, there may be circumstances where you do not want to do this. For example, one such circumstance is if you need to use weakly named assemblies or else just want greater control and flexibility. You might also want to totally separate the downloaded assemblies from the rest of the system. Putting an assembly in the GAC makes it globally accessible. This has considerable robustness and security implications.

Another management possibility is to keep the assemblies totally in memory. This can be a benefit to startup performance and security (nothing is directly stored or accessed to disk) but once the Application Domain is unloaded, all assemblies go with it.

If you create your own assembly management infrastructure you will need to find a place to store assemblies. You will also need to be able to resolve failed assembly loads to your assembly store. To find a place to store the assemblies, I chose a directory location that would be unique to my application. The base directory is returned by a call to Environment.SpecialFolders with the Environment.SpecialFolders.CommonApplicationData enum value as the parameter. Subdirectories are then created with the company ("MSDN" in this example), the application name, and the application version. This should ensure that I'll have a unique segment of the file system to call my own.

Now that I have a unique spot for storing assemblies, there's another problem I need to address—the Windows® operating system and the .NET Framework identify uniqueness differently. You can only have one file of any given name in a directory. However, assembly uniqueness has four parts: name, version, culture, and public key. This means that you'll have to store your assemblies in a way that ensures this uniqueness. Since you're essentially creating a local assembly cache, it makes sense to look at what the GAC does. The GAC creates a file structure based around the four uniquely identifying parts of the assembly. I merely copied this structure, as shown in Figure 9. Now each assembly should be located in a unique place that is easily discovered.

Figure 9 File Hierarchy for an Assembly

Figure 9** File Hierarchy for an Assembly **

There is still one last caveat, however. If you do allow weakly named assemblies, it is still possible for there to be identification problems. This is because weakly named assemblies do not have a public key and therefore only have three attributes (name, version, culture) to uniquely identify them. Given that the assemblies that the host will be receiving all contain Agent implementations, it is a good assumption that you'll see lots of assemblies named MyAgent.dll or the like, all with the neutral culture and version 1.0.0.0. This name collision (in addition to security issues, discussed later) is a good reason to not support weakly named assemblies in a robust system.

In order to facilitate this assembly management, I created a helper class appropriately called AssemblyManager (see Figure 10). In its static constructor, it hooks into the current AppDomain's AssemblyResolve event. This event will be fired any time the .NET runtime is unable to load an assembly via established methods. See How the Runtime Locates Assemblies for information on how the common language runtime (CLR) resolves assemblies. The ResolveEventArgs class is the second parameter to this method (the first being the AppDomain object that was unable to resolve the assembly). This class has a Name property that gives the full name of the assembly the runtime couldn't find. Using the AssemblyName(string) constructor, I can obtain the four parts I mentioned earlier—name, version, culture, and public key token. Knowing these allows me to construct a path to the given assembly and then load it.

Figure 10 AssemblyManager

public class AssemblyManager { private static string _assemblyStoreBaseDir = string.Empty; static AssemblyManager() { //Create root directory for agent assemblies _assemblyStoreBaseDir = Path.Combine(Environment.GetFolderPath( Environment.SpecialFolder.CommonApplicationData), @"MSDN\MobileAgentsSample\v1.0"); if (!Directory.Exists(_assemblyStoreBaseDir)) Directory.CreateDirectory(_assemblyStoreBaseDir); //Hook AppDomain for assembly resolution failures AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); } private static Assembly CurrentDomain_AssemblyResolve( object sender, ResolveEventArgs args) { string fileName, dirPath; GetDirectoryAndFileForAssembly(args.Name, out dirPath, out fileName); string fullPath = Path.Combine(dirPath, fileName); if (File.Exists(fullPath)) return Assembly.LoadFrom(fullPath); return null; } private static void GetDirectoryAndFileForAssembly( string assemblyFullName, out string directoryName, out string fileName) { fileName = string.Empty; directoryName = string.Empty; //Parse the assembly name string name, version, culture, publicKeyToken; ParseAssemblyFullName(assemblyFullName, out name, out version, out culture, out publicKeyToken); fileName = name + ".dll"; string relPath = string.Format(@"{0}\{1}\{2}\{3}", name, culture, version, publicKeyToken); directoryName = Path.Combine(_assemblyStoreBaseDir, relPath); } private static void ParseAssemblyFullName(string assemblyFullName, out string name, out string version, out string culture, out string publicKeyToken) { name = version = culture = publicKeyToken = string.Empty; AssemblyName an = new AssemblyName(assemblyFullName); name = an.Name; version = an.Version.ToString(); culture = an.CultureInfo.NativeName; publicKeyToken = ConvertBytesToHexString(an.GetPublicKeyToken()); } private static string ConvertBytesToHexString(byte[] bits) { StringBuilder sb = new StringBuilder(bits.Length*2); foreach (byte b in bits) sb.AppendFormat("{0:X2}", b); return sb.ToString(); } internal static void SaveAssemblyBits(string assemblyFullName, byte[] assemblyBits) { //Get the directory this assembly should go in. string fileName, dirPath; GetDirectoryAndFileForAssembly(assemblyFullName, out dirPath, out fileName); if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); //Create the assembly file if it doesn't already exist. string filePath = Path.Combine(dirPath, fileName); if (!File.Exists(filePath)) { using(FileStream fs = File.OpenWrite(filePath)) fs.Write(assemblyBits, 0, assemblyBits.Length); } } internal static bool IsAssemblyInstalled(string assemblyFullName) { try { AssemblyName an = new AssemblyName(assemblyFullName); Assembly asm = Assembly.Load(an); if (asm != null) return true; } catch (FileNotFoundException) { } return false; } }

The AssemblyManager class also has two internal methods that are called by the AgentHost class to facilitate assembly transfer: IsAssemblyInstalled and SaveAssemblyBits. The IsAssemblyInstalled method will simply attempt to load the assembly given the full name passed in as the single parameter. If a failure occurs, the assembly must not be installed yet. The second method will save the assembly's bits to the appropriate location by again parsing the full name of the assembly.

The Traveling Pattern

To exemplify a traveling agent, I wrote a simple scenario where an agent would have an itinerary and would go to each host listed in it. Once at each host, it will merely output a message on the host process' console saying that it has arrived (in lieu of actual work) and then move to the next host. The implementation is straight- forward. I create a class MyTravelingAgent (see Figure 11) derived from Agent. I give it a private System.Collections.Queue<string> class, _destinations, for its itinerary. I then add another method, AddDestination, which will add a URL to the queue. The implementation of the Run override method merely outputs to the console, sleeps a bit, dequeues the next URL from the queue, and then moves there.

Figure 11 MyTravelingAgent

[Serializable] class MyTravelingAgent : MobileAgents.Agent { Queue<string> _destinations = new Queue<string>(); public void AddDestination(string url) { _destinations.Enqueue(url); } protected override void Run() { Console.WriteLine("I'm here now: {0}", DateTime.Now); System.Threading.Thread.Sleep(5000); if (_destinations.Count > 0) { string nextDestination = _destinations.Dequeue(); this.Move(nextDestination); } } }

When an agent moves to a new location, it has a synchronous connection to the remote host and won't relinquish it until the Agent is moved and finishes executing. In a traveling agent pattern, part of the execution involves moving to another host to execute there. This operation also performs synchronously. Thus, the originating call to Agent.Move waits for the traveling agent to move to its final destination and then finishes executing. Needless to say, this is not a good design. One could easily imagine a traveling agent that has no final destination but has a continual route that it travels performing useful work.

Although there are ways to address this via Remoting, I want a protocol-agnostic solution. The System.Threading namespace offers an easy workaround in the form of ThreadPool. I'll use the ThreadPool.QueueUserWorkItem static method to forward the incoming Agent to another thread which will then execute it. This action allows the caller to return immediately:

public void HostAgent(Agent agent) { ThreadPool.QueueUserWorkItem(this.RunAgent, agent); } private void RunAgent(object context) { ((Agent)context).Run(); }

The Task Pattern

The task pattern-based agent application will use a type of task pattern known as the parent-child pattern. In this scenario, one agent is the parent or controller agent and it spawns or communicates with child agents that do its bidding. My implementation of this pattern merely creates a parent agent and tells it on which hosts to perform work. When run, the parent agent will create a child agent for each host to visit and then send the child agent there. Each child agent will then perform some valuable work, like enforcing strong access control lists (ACLs) on all file shares or clearing the Recycle Bin. My child agent will merely list the system's logical drives (with a call to Directory.GetLogicalDrives) and again output the results to the host's console (see Figure 12).

Figure 12 Creating Parent and Child Agents

[Serializable] class MyParentTaskAgent : MobileAgents.Agent { List<string> _hostsToCheck = new List<string>(); public void AddHost(string url) { _hostsToCheck.Add(url); } protected override void Run() { foreach (string hostUrl in _hostsToCheck) { AgentProxy proxy = Agent.CreateAgent( typeof(MyChildTaskAgent)); proxy.Move(hostUrl); } } } [Serializable] class MyChildTaskAgent : MobileAgents.Agent { protected override void Run() { foreach (string drive in Directory.GetLogicalDrives()) { Console.WriteLine("Found drive: '{0}'", drive); } } }

The Interactive Pattern

The interactive agent application consists of selling and buying agents. Sellers will set up shop on a host. The buying agent will then visit the shops that it knows about, looking for the best deal for some particular item. For this application I created two Agent subclasses: MySellingAgent and MyBuyingAgent.

MySellingAgent is not too complex. When a selling agent gets to a host to set up shop, it will register itself with a "registry," so to speak. This registry is really a static dictionary that will store the agent's ID (a simple integer in this scenario) mapping to the selling agent instance itself. MySellingAgent has a static dictionary, _sellers, a static constructor that initializes the dictionary, and two public static methods for buyers to find seller instances: GetSellersOnHost and GetSellerById. The first will return an array of MySellingAgent objects, the second will return a single instance of a MySellingAgent object.Generic Interaction Mechanisms

I'm sure that many of you see another shortcoming of the interactive buyer-seller scenario. That is, the buyer needs compile-time knowledge of the seller. Unless you code all agents involved in an interaction, you can't control this. If you can't control it, a generic mechanism needs to exist to allow for discovery and use of other agents' services. The AgentProxy class I described gets a step closer as you can invoke methods without compile-time knowledge. However, there is no discovery mechanism to see what services or functionality an agent provides.

Although it is fun to reinvent the wheel, you can take advantage of some protocols that currently exist for this functionality, for example, Universal Description, Discovery, and Integration (UDDI), Web Services Description Language (WSDL), and SOAP (see www.w3c.org for descriptions of these protocols). I can envision a world where each host acts as a UDDI server giving agents the ability to find other agents to use their services and communicate. Once an agent is found, WSDL can be used to dynamically discover the communication protocols and SOAP can be used for the actual communication. This essentially makes these mobile agents mobile Web services. That is a powerful, albeit complex, paradigm.

On the instance level, the selling agent has an ID (again, merely an integer) and a dictionary of items for sale. The dictionary maps the item's name to an internal class ItemForSale, which holds the item's name and price:

[Serializable] public class ItemForSale { public ItemForSale(string name, int price) { this.ItemName = name; this.ItemPrice = price; } public string ItemName = string.Empty; public int ItemPrice = 0; }

The selling agent's instance constructor merely copies the parameters to member variables. Three public methods exist for interaction with a buyer: IsItemForSale, GetItemPrice, and BuyItem. In the overridden Run method, the seller registers itself on the host by adding itself to the static _sellers dictionary. It then waits to be called by a buying agent.

MyBuyingAgent, shown in Figure 13, is a bit more complex as it needs to maintain more state. The buying agent is given the name of an item to buy, a value for the maximum amount it can spend for that item, and an array of host URLs that the agent needs to visit in order to find the item at the lowest price.

Figure 13 MyBuyingAgent

[Serializable] class MyBuyingAgent : MobileAgents.Agent { ... public MyBuyingAgent( string itemToBuy, int maximumPriceForItem, string[] urlsToVisit) { this._itemToBuy = itemToBuy; this._maxPriceForItem = maximumPriceForItem; foreach (string url in urlsToVisit) _sitesToVisit.Enqueue(url); } protected override void Run() { Console.WriteLine("Buying agent is here."); System.Threading.Thread.Sleep(2000); if (this._currentHost == this._winnerHost) BuyFromWinner(); else { FindItem(); if (_sitesToVisit.Count == 0) this.GoToMarketplace(this._winnerHost); else { string nextHost = this._sitesToVisit.Dequeue(); this.GoToMarketplace(nextHost); } } } private void BuyFromWinner() { MySellingAgent seller = MySellingAgent.GetSellerById(this._winnerId); if (seller != null) { seller.BuyItem(this._itemToBuy); Console.WriteLine( "We bought '{0}' from seller {1} at {2} for ${3}!", _itemToBuy, _winnerId, _winnerHost, _winnerPrice); } } private void FindItem() { MySellingAgent[] sellers = MySellingAgent.GetSellersOnHost(); foreach (MySellingAgent seller in sellers) { if (seller.IsItemForSale(this._itemToBuy)) { int sellersPrice = seller.GetItemPrice(this._itemToBuy); if (sellersPrice <= this._maxPriceForItem) { if (_winnerId == -1 || sellersPrice < _winnerPrice) { this._winnerHost = this._currentHost; this._winnerId = seller.Id; this._winnerPrice = sellersPrice; } } } } } public void GoToMarketplace(string hostUrl) { this._currentHost = hostUrl; this.Move(this._currentHost); } }

As the buying agent travels, it will need to enumerate each seller on each host, determine if that seller has the item desired, and then find the item's price. Since the agent wants to find the best price, it will have to track which seller it is going to buy from. I call this the winning seller. The agent will have to store the location of the winning seller, the ID of the winning seller, and the price the winning seller is offering for the desired item.

Finally, the buying agent will need one more piece of information—its current location. Since there is currently no facility in the AgentHost to give this information, I'll have to cheat. Instead of directly calling the base class's Move method directly, I created a wrapper function called GoToMarketplace. This will set a member variable to the URL of the host that it is about to move to and then it will call Move:

public void GoToMarketplace(string hostUrl) { this._currentHost = hostUrl; this.Move(this._currentHost); }

When the agent appears at the destination host, it will have its current host member set to where it is located.

The logic in the overridden Run method is as follows. If the current host is the same as the winning host, you have already determined that you are going to buy from a seller here. If so, get that seller by calling the static MySellingAgent.GetSellerById method. The object instance returned can now be invoked, calling its BuyItem method. If the current host is not the winning host, the agent will try looking for it on the local host. After this I check for any more host URLs to move to. If there are none, then the agent will move to the winning host to purchase the item. If there are more host URLs, the agent simply moves to the next host and continues.

Advanced Topics

Developers creating a mobile agent system need to manage local agent object references. As I'm sure some of you have noticed, merely calling the Move method doesn't do anything to the local object instance. It still exists. It can be invoked and run without the knowledge or consent of its clone that you just sent across the network. Just keeping a reference to the object will keep it in memory and prevent it from being garbage collected. In effect, the supposedly autonomous agent is at the mercy of anyone who has a reference to it.

Most agent implementations (especially in managed environments like that provided by the .NET Framework) use an agent proxy and never give out direct references to the agents themselves. A possible way to create such a proxy system is to implement an agent factory pattern. Instead of returning an agent, it will return a proxy to the agent. By using a basic eventing mechanism, all proxies to an agent can be notified when that agent moves. Once notified, they can ensure that no one can use the agent from that point on. Furthermore, the class implementing the proxy can use a WeakReference object to allow for greater agent object independence. (See WeakReference Class for a description of the WeakReference class.) Figure 14 shows the simple AgentProxy implementation based on the code download available from the MSDN®Magazine Web site. The creation and use of an agent would then look like this:

AgentProxy proxy = Agent.CreateAgent(typeof(MyTravelingAgent)); proxy.InvokeMethod("AddDestination", "tcp://localhost:10000/MyAgentSample"); proxy.InvokeMethod("AddDestination", "tcp://localhost:10002/MyAgentSample"); proxy.Move("tcp://localhost:10001/MyAgentSample");

Figure 14 AgentProxy Class

[Serializable] public class AgentProxy { private WeakReference _agent = null; private bool _agentLeft = false; internal AgentProxy(Agent agentToProxy) { _agent = new WeakReference(agentToProxy); agentToProxy.AgentMoved += agentToProxy_AgentMoved; } public void Move(string hostUrl) { if (_agent.IsAlive && !_agentLeft) { Agent agent = (Agent)_agent.Target; agent.Move(hostUrl); } else throw new Exception("The agent no longer exists here."); } public object InvokeMethod(string methodName, params object[] methodArguments) { if (_agent.IsAlive && !_agentLeft) { Agent a = (Agent)_agent.Target; Type agentType = a.GetType(); object retVal = agentType.InvokeMember(methodName, BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.Instance, null, a, methodArguments); return retVal; } else throw new Exception("The agent no longer exists here."); } private void agentToProxy_AgentMoved(object sender, EventArgs e) { _agentLeft = true; _agent.Target = null; } }

For bonus points, some implementations of agent systems also use proxies as a location-agnostic way of interfacing with agents. With this design, if some object has a proxy to agent A on machine X and A moves to machine Y, the user of the proxy doesn't care. It can call to agent A's proxy as before and the proxy takes care of all the details, making sure that A (now on Y) gets the message.

Unfortunately, this object reference problem is not merely due to external handles to the agent. One of the shortcomings of building an agent system on the .NET Framework is the inability to stop thread execution, store stack frames and registers, and then truly resume execution on the destination host. Because of this, there still exists the potential for an agent to misuse itself. If an agent makes a call to this.Move but then continues processing, it is violating the intention for one agent to exist in only one place at one time. Therefore, agent implementers need to think of a call to this.Move as if it is the final word in processing (exempting object cleanup, of course).

Agents should also have some internal mechanism to make sure that during race conditions they are not moved twice by accident. This goes mostly for the host side. By giving each agent instance a unique identifier, a host can add rules to make sure agents are not moved to it multiple times.

Another topic for advanced systems is the support given by the host. As seen in the buying agent example, there was no means for an agent to know where it was unless I engineered an ugly solution. Shouldn't the host itself provide this information? What services should the host give executing agents? Maybe the host will allocate file or database space. Perhaps the host will provide persistence services for long-running agents. The more services a host provides, the more interaction between agent and host, the more complexities arise. A production system will need more careful design than the samples given here.

The code samples available in the download provide a simple context class, HostContext, that each agent can utilize by simply accessing the static property HostContext.Current. The HostContext in the code exposes only the current host location, but other services can be added here to give agents a more functional playground in which to execute. (For details on using the AgentProxy class to invoke methods without compile-time knowledge, see the sidebar "Generic Interaction Mechanisms.")

Security and Mobile Agents

When I describe the mobile agent paradigm to people, their first reaction is usually that it sounds like a virus. Surely, allowing the download, installation, and execution of unknown code has significant security implications. A thorough understanding of security issues and a comprehensive threat model should be a minimum requirement in any production system.

I'll briefly discuss some different threats unique to a mobile agent system and how .NET facilities can help mitigate the risks. First though, I must make the disclaimer that the threats described here may or may not apply (nor are the threats I describe the only ones that exist), based on the particular function of the mobile agent system designed. For example, the threats for a publicly available interactive mobile agent system greatly differ from an internal, task-based mobile agent system. Don't forget that threat model. To determine the threats inherent to a mobile agent system, you need to look at the protagonists. As such, the system needs to be secure from malicious clients, agents, and hosts. (The downloadable code implements several security enhancements.)

The first possible threat is a malicious client attempting to harm an established host. Here I am defining a client as any program that communicates with a host via its outward-facing interface. A lot of the mitigation for this threat depends on your communication protocol. Will you allow anonymous calls? Will you authenticate? How? Securing the communication protocol is beyond the scope of this article, but it is very important and should be thoroughly analyzed during the design phase. I recommend a protocol that can both authenticate and encrypt the requests. Doing so will hinder many types of attacks on the host. This is where a framework like that provided by Windows Communication Foundation comes in extremely handy.

As for the host itself, a good rule of thumb in any security design is to reduce the attack surface, so be very careful of what functionality you choose to expose to the outside world (see Attack Surface: Mitigate Security Risks by Minimizing the Code You Expose to Untrusted Users for more information about attack surface reduction). The .NET Framework 2.0 has a new facility called Transparency, which can be used in external-facing code to always run with the caller's permissions (see Are You in the Know? Find Out What's New with Code Access Security in the .NET Framework 2.0).

The AgentHost class presented here has three externally visible methods: IsAssemblyInstalled, UploadAssembly, and HostAgent. Although it might seem innocuous, IsAssemblyInstalled could pose a security risk if it gives information away to a potential hacker. For example, if there is a known security issue in a particular version of a core .NET assembly, malicious code merely has to query the host to see if that assembly (and thus, vulnerability) is available for attack. Furthermore, the current implementation attempts to see if an assembly exists by loading it. If an assembly that poses a security risk is loaded, it is loaded into the host's application domain. So much for isolation. The code download contains a slightly different solution where the host creates a separate AppDomain for the sole purpose of loading assemblies during the call to IsAssemblyInstalled.

UploadAssembly needs caution because it is how a malicious user would upload malicious code onto the host machine. In the mobile agent system this article presents, the assembly bits are saved to disk at a known location and then loaded later if assembly resolution fails. Because the assemblies are not loaded directly into memory, you can delay the problem of uploading malicious agents. That is, you can delay this problem only until the call to HostAgent. However, once an assembly is uploaded and saved to storage, it is possible to inspect it for security purposes. Here you can enforce such rules as requiring a strong name. Strong-named assemblies help reduce risk in several ways, such as proving that the assembly hasn't be tampered with post build.

Of course, you should only trust the builder within certain limitations—you shouldn't trust an assembly just because it has a strong name. Finally, remember that assemblies that are loaded from a local hard disk are full-trust assemblies by default. I'll address that in a moment.

The HostAgent method is where execution of the uploaded agent code will begin, which brings me to a discussion of agent-host threats. When an agent is to be hosted for the first time, the CLR looks for the agent's assembly. Because I store the assemblies in an obscure location, default assembly resolution will fail. It is then up to the AppDomain.AssemblyResolve event handler to load the appropriate assembly. It would be a very good idea to place those assemblies in a sandboxed environment before you load and use them.

In the code download, I have bolstered the security of the system by creating separate application domains to separate agents from the host domain and other agents. I chose to allow the agent to specify the name of the application it is a part of. Each application will have its own app domain. This architecture is more suitable for interactive agent scenarios where agents from different sources can mingle more freely. You can segregate agents however you want. Your scenario may prompt you to launch an application domain for each agent type or even each agent instance that is hosted. The built-in separation given with the AppDomain construct allows you to unload these assemblies if you either determine they are a threat or if activity wanes and you need to reclaim some memory.

For creating an AppDomain, the .NET Framework 2.0 has some new APIs that allow you to specify the code access security (CAS) policy that you desire. New AppDomain.CreateDomain overloads allow you to pass in the default Evidence, PermissionSet, and the names of the full-trust assemblies for that new application domain. If you choose to go this route, I recommend you read Shawn Farkas's blog at blogs.msdn.com/shawnfa on creating custom AppDomainManagers and HostSecurityManager subclasses as well as his recent MSDN Magazine article concerning hosting add-ins at Do You Trust It? Discover Techniques for Safely Hosting Untrusted Add-Ins with the .NET Framework 2.0.

In the download code, I took another approach. My approach alters the CAS policy for the local machine. Using the SecurityManager class, I added a new CodeGroup with a restricted permission set (derived from the Internet-named permission set, then altered). The membership condition for this CodeGroup is a URL condition that points to the base file location where the assemblies were saved. This is set up and checked every time an agent AppDomain is created. And the best part is that the settings are now machine-wide. If any other process loads these assemblies they will have the same restrictive permission set. (See Code Access Security for more information on CAS in the CLR.)

Finally, I'll touch on the threat of a malicious host towards your agent. If you send data in the form of an agent to a host, that host has full control and use of the agent. So I'd recommend not storing things like credit card or social security numbers in them. Since you can't control what a malicious host might do to your agent, the best course of action would be to prevent your agent from going to such a host. Again, your communication protocol could take this on with mutual authentication. Remember, security is based on trust. If you don't trust a host, stay away from it.

Conclusion

There are many ways to engineer a networked application. Mobile agents have their uses and their pros and cons. The autonomous and mobile nature of mobile agents can lead to reduced network traffic, decentralization, increased robustness and fault-tolerance, and easy deployment. Now that you know a bit of what mobile agents can do, let's look at these advantages.

A mobile agent system has the capability of reducing network traffic by taking the processing local to the resource being used. If you are doing a text search of files across the network, you can dramatically improve performance by doing the search locally and then returning only the results.

A mobile agent system can be decentralized for increased robustness. Since the mobile agent contains code, you can implant into it any fault-tolerance and error mechanisms you want. Think of searching text files across the network again. If the machine that originally dispatched the mobile agents to the remote hosts crashes, processing can continue. The mobile agents can even camp out on remote hosts until they can determine that it is safe to return. It is also possible to survive a denial-of-service attack this way.

Deployment is made easier since mobile agents essentially deploy themselves. The only prerequisite is that an agent host is installed and configured on that machine. Once that condition is met, you're open for business.

Unfortunately, mobile agents do have a downside. They present a paradigm that is different from other development paradigms and can be more difficult to comprehend. Also, security is a much bigger issue with the download and execution of code. Search for more information about mobile agents on MSN® and look at the various implementations that currently exist. Then look at how many even attempt to tackle the issue of security—most don't. Downloading and executing unknown code is not a good way to control security.

I believe that mobile agents are a very useful paradigm that should receive more attention than they have been getting in the realm of academia. The features that are provided by the .NET Framework should allow mobile agents to proliferate to the full extent of their capabilities.

Matt Neely is a Software Design Engineer at Microsoft where he spends most of his time looking busy when the manager walks by. He enjoys playing the guitar and spending time with his wife, four children, three cats, and a puppy.