I was tasked recently with creating an app that would scan a network for any and all computers in it using C#. Well not exactly... It was more anything in our domain, but for the most part that should be anything on our network. Anyway, it turns out that it's extremely simple and really only took 3 steps:
- Get the list of computer names from Active Directory
- Do a DNS lookup on those names
- And report the results
That's all that I needed to do. It was especially easy because of some helper classes that I had laying around to deal with AD. A while back I posted two classes to help with AD. They're a bit out of date as I've added extra functionality since then, the newer version of the classes looks like this:
/*
Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.*/
#region Usings
using System;
using System.Collections.Generic;
using System.DirectoryServices;
#endregion
namespace Utilities.LDAP
{
/// <summary>
/// Class for helping with AD
/// </summary>
public class Directory:IDisposable
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="UserName">User name used to log in</param>
/// <param name="Password">Password used to log in</param>
/// <param name="Path">Path of the LDAP server</param>
/// <param name="Query">Query to use in the search</param>
public Directory(string Query,string UserName, string Password, string Path)
{
try
{
Entry = new DirectoryEntry(Path, UserName, Password, AuthenticationTypes.Secure);
this.Path = Path;
this.UserName = UserName;
this.Password = Password;
this.Query = Query;
Searcher = new DirectorySearcher(Entry);
Searcher.Filter = Query;
Searcher.PageSize = 1000;
}
catch { throw; }
}
#endregion
#region Public Functions
/// <summary>
/// Finds a user by his user name
/// </summary>
/// <param name="UserName">User name to search by</param>
/// <returns>The user's entry</returns>
public Entry FindUserByUserName(string UserName)
{
try
{
List<Entry> Entries = FindUsers("samAccountName=" + UserName);
if (Entries.Count > 0)
{
return Entries[0];
}
return null;
}
catch { throw; }
}
/// <summary>
/// Finds all active users
/// </summary>
/// <param name="Filter">Filter used to modify the query</param>
/// <param name="args">Additional arguments (used in string formatting</param>
/// <returns>A list of all active users' entries</returns>
public List<Entry> FindActiveUsers(string Filter, params object[] args)
{
try
{
Filter = string.Format(Filter, args);
Filter = string.Format("(&((userAccountControl:1.2.840.113556.1.4.803:=512)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(cn=*$)))({0}))", Filter);
return FindUsers(Filter);
}
catch { throw; }
}
/// <summary>
/// Finds all users
/// </summary>
/// <param name="Filter">Filter used to modify the query</param>
/// <param name="args">Additional arguments (used in string formatting</param>
/// <returns>A list of all users meeting the specified Filter</returns>
public List<Entry> FindUsers(string Filter, params object[] args)
{
try
{
Filter = string.Format(Filter, args);
Filter = string.Format("(&(objectClass=User)(objectCategory=Person)({0}))", Filter);
Searcher.Filter = Filter;
return FindAll();
}
catch { throw; }
}
/// <summary>
/// Finds all computers
/// </summary>
/// <param name="Filter">Filter used to modify the query</param>
/// <param name="args">Additional arguments (used in string formatting</param>
/// <returns>A list of all computers meeting the specified Filter</returns>
public List<Entry> FindComputers(string Filter, params object[] args)
{
try
{
Filter = string.Format(Filter, args);
Filter = string.Format("(&(objectClass=computer)({0}))", Filter);
Searcher.Filter = Filter;
return FindAll();
}
catch { throw; }
}
/// <summary>
/// Finds all active users and groups
/// </summary>
/// <param name="Filter">Filter used to modify the query</param>
/// <param name="args">Additional arguments (used in string formatting</param>
/// <returns>A list of all active groups' entries</returns>
public List<Entry> FindActiveUsersAndGroups(string Filter, params object[] args)
{
try
{
Filter = string.Format(Filter, args);
Filter = string.Format("(&((userAccountControl:1.2.840.113556.1.4.803:=512)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(cn=*$)))({0}))", Filter);
return FindUsersAndGroups(Filter);
}
catch { throw; }
}
/// <summary>
/// Finds all users and groups
/// </summary>
/// <param name="Filter">Filter used to modify the query</param>
/// <param name="args">Additional arguments (used in string formatting</param>
/// <returns>A list of all users and groups meeting the specified Filter</returns>
public List<Entry> FindUsersAndGroups(string Filter, params object[] args)
{
try
{
Filter = string.Format(Filter, args);
Filter = string.Format("(&(|(&(objectClass=Group)(objectCategory=Group))(&(objectClass=User)(objectCategory=Person)))({0}))", Filter);
Searcher.Filter = Filter;
return FindAll();
}
catch { throw; }
}
/// <summary>
/// Finds all active groups
/// </summary>
/// <param name="Filter">Filter used to modify the query</param>
/// <param name="args">Additional arguments (used in string formatting</param>
/// <returns>A list of all active groups' entries</returns>
public List<Entry> FindActiveGroups(string Filter, params object[] args)
{
try
{
Filter = string.Format(Filter, args);
Filter = string.Format("(&((userAccountControl:1.2.840.113556.1.4.803:=512)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(cn=*$)))({0}))", Filter);
return FindGroups(Filter);
}
catch { throw; }
}
/// <summary>
/// Finds all groups
/// </summary>
/// <param name="Filter">Filter used to modify the query</param>
/// <param name="args">Additional arguments (used in string formatting</param>
/// <returns>A list of all groups meeting the specified Filter</returns>
public List<Entry> FindGroups(string Filter, params object[] args)
{
try
{
Filter = string.Format(Filter, args);
Filter = string.Format("(&(objectClass=Group)(objectCategory=Group)({0}))", Filter);
Searcher.Filter = Filter;
return FindAll();
}
catch { throw; }
}
/// <summary>
/// Returns a group's list of members
/// </summary>
/// <param name="GroupName">The group's name</param>
/// <returns>A list of the members</returns>
public List<Utilities.LDAP.Entry> FindActiveGroupMembers(string GroupName)
{
try
{
List<Utilities.LDAP.Entry> Entries = this.FindGroups("cn=" + GroupName);
if (Entries.Count < 1)
return new List<Utilities.LDAP.Entry>();
return this.FindActiveUsersAndGroups("memberOf=" + Entries[0].DistinguishedName);
}
catch
{
return new List<Utilities.LDAP.Entry>();
}
}
/// <summary>
/// Finds all entries that match the query
/// </summary>
/// <returns>A list of all entries that match the query</returns>
public List<Entry> FindAll()
{
try
{
List<Entry> ReturnedResults = new List<Entry>();
SearchResultCollection Results = Searcher.FindAll();
foreach (SearchResult Result in Results)
{
ReturnedResults.Add(new Entry(Result.GetDirectoryEntry()));
}
Results.Dispose();
return ReturnedResults;
}
catch { throw; }
}
/// <summary>
/// Finds one entry that matches the query
/// </summary>
/// <returns>A single entry matching the query</returns>
public Entry FindOne()
{
try
{
SearchResult Result = Searcher.FindOne();
return new Entry(Result.GetDirectoryEntry());
}
catch { throw; }
}
/// <summary>
/// Closes the directory
/// </summary>
public void Close()
{
try
{
Entry.Close();
}
catch { throw; }
}
/// <summary>
/// Checks to see if the person was authenticated
/// </summary>
/// <returns>true if they were authenticated properly, false otherwise</returns>
public bool Authenticate()
{
try
{
if (!Entry.Guid.ToString().ToLower().Trim().Equals(""))
{
return true;
}
}
catch
{
}
return false;
}
#endregion
#region Properties
/// <summary>
/// Path of the server
/// </summary>
public string Path
{
get { return _Path; }
set
{
_Path = value;
try
{
if (Entry != null)
{
Entry.Close();
}
Entry = new DirectoryEntry(_Path, _UserName, _Password, AuthenticationTypes.Secure);
Searcher = new DirectorySearcher(Entry);
Searcher.Filter = Query;
Searcher.PageSize = 1000;
}
catch { throw; }
}
}
/// <summary>
/// User name used to log in
/// </summary>
public string UserName
{
get { return _UserName; }
set
{
_UserName = value;
try
{
if (Entry != null)
{
Entry.Close();
}
Entry = new DirectoryEntry(_Path, _UserName, _Password, AuthenticationTypes.Secure);
Searcher = new DirectorySearcher(Entry);
Searcher.Filter = Query;
Searcher.PageSize = 1000;
}
catch { throw; }
}
}
/// <summary>
/// Password used to log in
/// </summary>
public string Password
{
get { return _Password; }
set
{
_Password = value;
try
{
if (Entry != null)
{
Entry.Close();
}
Entry = new DirectoryEntry(_Path, _UserName, _Password, AuthenticationTypes.Secure);
Searcher = new DirectorySearcher(Entry);
Searcher.Filter = Query;
Searcher.PageSize = 1000;
}
catch { throw; }
}
}
/// <summary>
/// The query that is being made
/// </summary>
public string Query
{
get { return _Query; }
set
{
_Query = value;
Searcher.Filter = _Query;
}
}
/// <summary>
/// Decides what to sort the information by
/// </summary>
public string SortBy
{
get { return _SortBy; }
set
{
_SortBy = value;
Searcher.Sort.PropertyName = _SortBy;
Searcher.Sort.Direction = SortDirection.Ascending;
}
}
#endregion
#region Private Variables
private string _Path = "";
private string _UserName = "";
private string _Password = "";
private DirectoryEntry Entry = null;
private string _Query = "";
private DirectorySearcher Searcher = null;
private string _SortBy = "";
#endregion
#region IDisposable Members
public void Dispose()
{
if (Entry != null)
{
Entry.Dispose();
Entry = null;
}
if (Searcher != null)
{
Searcher.Dispose();
Searcher = null;
}
}
#endregion
}
}
/*
Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.*/
#region Usings
using System;
using System.Collections.Generic;
using System.DirectoryServices;
#endregion
namespace Utilities.LDAP
{
/// <summary>
/// Directory entry class
/// </summary>
public class Entry:IDisposable
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="DirectoryEntry">Directory entry for the item</param>
public Entry(DirectoryEntry DirectoryEntry)
{
this._DirectoryEntry = DirectoryEntry;
}
#endregion
#region Properties
/// <summary>
/// Actual base directory entry
/// </summary>
public DirectoryEntry DirectoryEntry
{
get { return _DirectoryEntry; }
set { _DirectoryEntry = value; }
}
/// <summary>
/// Email property for this entry
/// </summary>
public string Email
{
get { return (string)GetValue("mail"); }
set { SetValue("mail", value); }
}
/// <summary>
/// distinguished name property for this entry
/// </summary>
public string DistinguishedName
{
get { return (string)GetValue("distinguishedname"); }
set { SetValue("distinguishedname", value); }
}
/// <summary>
/// country code property for this entry
/// </summary>
public string CountryCode
{
get { return (string)GetValue("countrycode"); }
set { SetValue("countrycode", value); }
}
/// <summary>
/// company property for this entry
/// </summary>
public string Company
{
get { return (string)GetValue("company"); }
set { SetValue("company", value); }
}
/// <summary>
/// MemberOf property for this entry
/// </summary>
public List<string> MemberOf
{
get
{
List<string> Values = new List<string>();
PropertyValueCollection Collection = DirectoryEntry.Properties["memberof"];
foreach (object Item in Collection)
{
Values.Add((string)Item);
}
return Values;
}
}
/// <summary>
/// display name property for this entry
/// </summary>
public string DisplayName
{
get { return (string)GetValue("displayname"); }
set { SetValue("displayname", value); }
}
/// <summary>
/// initials property for this entry
/// </summary>
public string Initials
{
get { return (string)GetValue("initials"); }
set { SetValue("initials", value); }
}
/// <summary>
/// title property for this entry
/// </summary>
public string Title
{
get { return (string)GetValue("title"); }
set { SetValue("title", value); }
}
/// <summary>
/// samaccountname property for this entry
/// </summary>
public string SamAccountName
{
get { return (string)GetValue("samaccountname"); }
set { SetValue("samaccountname", value); }
}
/// <summary>
/// givenname property for this entry
/// </summary>
public string GivenName
{
get { return (string)GetValue("givenname"); }
set { SetValue("givenname", value); }
}
/// <summary>
/// cn property for this entry
/// </summary>
public string CN
{
get { return (string)GetValue("cn"); }
set { SetValue("cn", value); }
}
/// <summary>
/// name property for this entry
/// </summary>
public string Name
{
get { return (string)GetValue("name"); }
set { SetValue("name", value); }
}
/// <summary>
/// office property for this entry
/// </summary>
public string Office
{
get { return (string)GetValue("physicaldeliveryofficename"); }
set { SetValue("physicaldeliveryofficename", value); }
}
/// <summary>
/// telephone number property for this entry
/// </summary>
public string TelephoneNumber
{
get { return (string)GetValue("telephonenumber"); }
set { SetValue("telephonenumber", value); }
}
#endregion
#region Public Functions
/// <summary>
/// Saves any changes that have been made
/// </summary>
public void Save()
{
try
{
_DirectoryEntry.CommitChanges();
}
catch { throw; }
}
/// <summary>
/// Gets a value from the entry
/// </summary>
/// <param name="Property">Property you want the information about</param>
/// <returns>an object containing the property's information</returns>
public object GetValue(string Property)
{
try
{
PropertyValueCollection Collection = DirectoryEntry.Properties[Property];
if (Collection != null)
{
return Collection.Value;
}
return null;
}
catch { throw; }
}
/// <summary>
/// Sets a property of the entry to a specific value
/// </summary>
/// <param name="Property">Property of the entry to set</param>
/// <param name="Value">Value to set the property to</param>
public void SetValue(string Property,object Value)
{
try
{
PropertyValueCollection Collection = DirectoryEntry.Properties[Property];
if (Collection != null)
{
Collection.Value = Value;
}
}
catch { throw; }
}
#endregion
#region Private Variables
private DirectoryEntry _DirectoryEntry;
#endregion
#region IDisposable Members
public void Dispose()
{
if (_DirectoryEntry != null)
{
_DirectoryEntry.Dispose();
_DirectoryEntry = null;
}
}
#endregion
}
}
The two classes are Directory and Entry. Directory is what is used to connect to Active Directory and the Entry class actually holds each entry's info (also note that they are IDisposable, so make sure to dispose of them when done). Anyway, most of the code is of little use in this instance. I mean the directory class has the ability to search for users, groups, etc. There's really only one function of much use: FindComputers. This function allows us to search for any computer that fits specific criteria that we pass into it. Or in our instance, anything in our domain:
using(Utilities.LDAP.Directory TempDirectory = new Utilities.LDAP.Directory("", "USERNAME", "PASSWORD", "LDAPLOCATION"))
{
List<Utilities.LDAP.Entry> Entries = TempDirectory.FindComputers("name=*");
}
The code above uses the Directory class to find all computers within AD, giving us back a list of those entries. This isn't the most useful code at this point but it gets us pass step one. We have our list of machines. So on to step 2, doing the DNS lookup. One of the great things about .Net is that a lot of functionality is already there for you. In this case all we have to do is take the name of each entry and feed it to Dns.GetHostEntry (which is in the System.Net namespace). It in turn gives us an IPAddress class, which holds every address that the machine is using (IPv4, IPv6, etc.). However, if you've tried it in the past you will notice that if it can't find the machine, it takes forever to run (well 14 seconds anyway). And if you're searching for 5000 machines and any decent number of them are offline, that is going to, well, suck. There are options, you can either modify your registry or you can use multithreading to get the job done. I went with the second option:
using(Utilities.LDAP.Directory TempDirectory = new Utilities.LDAP.Directory("", "USERNAME", "PASSWORD", "LDAPLOCATION"))
{
List<Utilities.LDAP.Entry> Entries = TempDirectory.FindComputers("name=*");
for (int x = 0; x < Entries.Count; ++x)
{
try
{
using (Utilities.LDAP.Entry Entry = Entries[x])
{
DNSSearch TempWorker = new DNSSearch(Entry.Name);
TempWorker.Finished = new EventHandler<Utilities.Events.EventArgs.OnEndEventArgs>(FinishedSearch);
TempWorker.Start();
}
}
catch { }
}
}
The code above just adds to our AD search, adding a loop, creating a DNSSearch class for each item, passing in the name, setting an eventhandler, and calling start. The DNSSearch class looks like this:
public class DNSSearch:Worker<Machine,string>
{
public DNSSearch(string Params)
: base(Params)
{
}
protected override Machine Work(string Params)
{
Machine TempMachine = new Machine();
TempMachine.Name = Params;
TempMachine.Addresses = new List<string>();
try
{
IPHostEntry HostEntry = Dns.GetHostEntry(Params);
foreach (IPAddress Address in HostEntry.AddressList)
{
TempMachine.Addresses.Add(Address.AddressFamily + ": " + Address.ToString());
}
}
catch { }
return TempMachine;
}
}
The class inherits from a Worker class, which I showed here. Although there are some slight updates to it, so you may want to get the latest version from my utility library here. Anyway, the base class wants to know what we are getting as input and what to expect as output. In our case it's expecting a string for input and a class called Machine for output. The machine class looks like this:
public class Machine
{
public string Name { get; set; }
public List<string> Addresses { get; set; }
}
It's really just a holder for the info. So when we call Start on the DNSSearch object, back in the original function, the base class starts the thread and calls Work in DNSSearch. The Work function creates a Machine object, sets the name of the machine and calls Dns.GetHostEntry, filling out the address info for us. When it's done it returns the machine object. This gets sent to our Finished EventHandler. In the original function we set the Finished event to a function called FinishedSearch:
public static List<Machine> MachineList { get; set; }
public static void FinishedSearch(object sender, Utilities.Events.EventArgs.OnEndEventArgs e)
{
lock (typeof(List<Machine>))
{
if (MachineList == null)
MachineList = new List<Machine>();
MachineList.Add((Machine)e.Content);
}
}
The function simply locks our list of machines, sets it up if need be and adds our machine object to said list and then unlocks it. So how do we get this info back to our form, or web page, or whatever? Simple:
public static List<Machine> SearchNetwork()
{
try
{
if (MachineList == null)
{
using(Utilities.LDAP.Directory TempDirectory = new Utilities.LDAP.Directory("", "USERNAME", "PASSWORD", "LDAPLOCATION"))
{
List<Utilities.LDAP.Entry> Entries = Temp.FindComputers("name=*");
for (int x = 0; x < Entries.Count; ++x)
{
try
{
using (Utilities.LDAP.Entry Entry = Entries[x])
{
DNSSearch TempWorker = new DNSSearch(Entry.Name);
TempWorker.Finished = new EventHandler<Utilities.Events.EventArgs.OnEndEventArgs>(FinishedSearch);
TempWorker.Start();
}
}
catch { }
}
}
}
return MachineList;
}
catch { throw; }
}
By returning the MachineList, which was static, we have access to it as it's being updated. So we can do something like this in a function:
List<Machine> Machines = SearchNetwork();
lock (typeof(List<Machine>))
{
foreach (Machine TempMachine in Machines)
{
if (TempMachine.Addresses.Count > 0)
{
//The machine is online
}
else
{
//The machine is offline
}
}
}
Note that we have to lock it because the threads are going to continue to update our list, but it doesn't stop us from locking it and using it ourselves. An even better idea might be to use a list with events telling you when something is added... Sort of like the code I created here. Using something like that, you could have a list on a form update in real time as items are added to the list. Anyway, I know I threw a bunch of code up here but it's quite simple once you get down to it. Anyway, try it out, leave feedback, and happy coding.
85292968-fce6-4509-9add-326b674f9f5d|0|.0