Auditing AD Sites Subnets and DHCP Scopes

posted in: Uncategorized | 1

I work in a large corporate, where we have hundreds of Offices (AD Sites), and we recently noticed some problems with login speed for a particular office.

The cause was the IPs being dished out to computers via DHCP not matching any of the subnets bound to the local AD Site, hence requests were not going to the in-office  Domain Controller, but rather over the VPN/MPLS to a Domain Controller in another office.

I figured there would likely be other misconfigured DHCP scopes or AD Sites/Subnets out there, so I wrote a script which interrogates Active Directory Sites and Services, as well as all of the Domains DHCP servers. After pulling the information back into a few hash tables and arrays, it compares them all and spits out the results.

Once you have the results in Excel I just add a column (I) “DHCPServerSiteMatchesADSite” with the formula =IF(ISERR(FIND(F2,A2)),”FALSE”,”TRUE”) and you get something similar to that shown in the image below.

DHCPAudit

Now a couple of things before we start. The script uses the Get-DHCPServerInDC and Get-DHCPServerv4Scope  cmdlets, which are only available with PowerShell v4. Second, my environment has multiple child domains under a forest root, I only wanted to query the Domain Controllers in one of the child Domains. If you only have a single domain, or want to do the same as I did and query one child domain, then you’ll only need to edit line 165, and change this variable:

 $DOMAINFILTER = "eu.blah.com" 

Otherwise you’re going to need to edit the script a little more to remove the filtering.

Finally, this script does have a fairly long time to run, because it is interrogating all your Domain controllers, and the results section does a cartesian products all AD Site Subnets vs all DHCP Scopes.


<# TODO
Update checkSubnetInSubnets function
Check whether the first or last IP in a subnet is within the other subnet. Currently it only checks
whether the start of the subnet is in the other subnet
#>

#region Functions

#Function checks whether an IP is in a subnet, takes CIDR or IP/Netmask notation
#Modified version of the below script, have added netmask option and changed comparison to use shr rather than Bitwise and
#### Note: Band doesn't work correctly because PowerShell does not pad out numbers to 32 bits before performing the bitwise and.
#### Hence using ($unetwork -shr $subnetlen) -eq ($uip -shr $subnetlen) instead of $unetwork -eq ($mask -band $uip)
#http://www.padisetty.com/2014/05/powershell-bit-manipulation-and-network.html
function checkIPInSubnet ()
{
param
(
[string]$SubNet,
[string]$IP,
[string]$NetMask = "0"
)
if($NetMask -eq "0")
{
$network, [int]$subnetlen = $subnet.Split('/'');
} else {
$subnetlen = netmaskToSlashNotation $NetMask;
$network = $subnet;
}

if($subnetlen -eq 32)
{
return $ip -eq $network;
}

$a = [uint32[]]$network.split('.');
[uint32] $unetwork = ($a[0] -shl 24) + ($a[1] -shl 16) + ($a[2] -shl 8) + $a[3];

$a = [uint32[]]$ip.split('.');
[uint32] $uip = ($a[0] -shl 24) + ($a[1] -shl 16) + ($a[2] -shl 8) + $a[3];

return ($unetwork -shr (32-$subnetlen)) -eq ($uip -shr (32-$subnetlen));

}

#Converts a netmask (255.255.255.192) to CIDR/Slash notation (/26). Returns -1 on failure.
#Note: Should only return -1 if input is 0 or $null
function netmaskToSlashNotation ([string]$netmask)
{
$a = [uint32[]]$netmask.split('.')
[uint32] $unetmask = ($a[0] -shl 24) + ($a[1] -shl 16) + ($a[2] -shl 8) + $a[3]
[UInt32] $slashMask = 1
$slash = 32
while ((($unetmask -band $slashMask) -eq 0) -and ($slash -ge 0))
{
$slashMask = $slashMask -shl 1
$slash--
}
$slash
}

function canResolveHost([string]$hostname)
{
try {
$a = [System.Net.Dns]::GetHostAddresses($hostname);
} catch {
return $false;
}
return $true;
}

#Checks whether a subnet - or rather the first IP of a subnet - is in any of the global subnets, if it is, adds to array.
function checkSubnetInSubnets ([string]$subnetIPcidr, [string]$siteName)
{
$subnetFirstIP = $subnetIPcidr.Split("/")[0]
foreach($row in $GlobalSubnetSiteHash.GetEnumerator())
{
#Check if it's looking at itself or the same site
if($row.key -eq $subnetIPcidr -or $row.value -eq $siteName)
{
continue;
}

if(checkIPInSubnet -subnet $row.key -ip $subnetFirstIP)
{
$global:subnetInOtherSubnet += New-ContainedSiteObject $siteName $subnetIPcidr ($row.value) ($row.key)
}
}
}

#endregion

#region ObjectFunctions

#Object used with the checkSubnetInSubnets function
#Used purely to make the output pretty, don't actually need the object otherwise
function New-ContainedSiteObject
{
Param
(
[string]$containedSite,
[string]$containedSubnet,
[string]$containingSite,
[string]$containingSubnet
)

New-Object -TypeName System.Object |
Add-Member -Name ContainedSite -Value $containedSite -MemberType NoteProperty -PassThru |
Add-Member -Name ContainedSubnet -Value $containedSubnet -MemberType NoteProperty -PassThru |
Add-Member -Name ContainingSite -Value $containingSite -MemberType NoteProperty -PassThru |
Add-Member -Name ContainingSubnet -Value $containingSubnet -MemberType NoteProperty -PassThru;
}

#Object containing one Scope on a DHCP server.
function New-ScopeObject
{
Param
(
[string]$scopeID,
[string]$subnetMask,
[string]$startRange,
[string]$endRange,
[string]$siteName,
[string]$DHCPServer
)

New-Object -TypeName System.Object |
Add-Member -Name ScopeID -Value $scopeID -MemberType NoteProperty -PassThru |
Add-Member -Name SubnetMask -Value $subnetMask -MemberType NoteProperty -PassThru |
Add-Member -Name StartRange -Value $StartRange -MemberType NoteProperty -PassThru |
Add-Member -Name EndRange -Value $EndRange -MemberType NoteProperty -PassThru |
Add-Member -Name SiteName -Value $siteName -MemberType NoteProperty -PassThru |
Add-Member -Name DHCPServer -Value $DHCPServer -MemberType NoteProperty -PassThru;
}
#Object containing the matching AD Site Subnet, and DHCP Scopes
function New-SiteSubnetScopeObject
{
Param
(
[string]$ADSite,
[string]$ADSubnet,
[string]$Scope,
[string]$ScopeSite,
[string]$DHCPServer,
[string]$Note
)

New-Object -TypeName System.Object |
Add-Member -Name ADSite -Value $ADSite -MemberType NoteProperty -PassThru |
Add-Member -Name ADSubnet -Value $ADSubnet -MemberType NoteProperty -PassThru |
Add-Member -Name Scope -Value $Scope -MemberType NoteProperty -PassThru |
Add-Member -Name ScopeSite -Value $ScopeSite -MemberType NoteProperty -PassThru |
Add-Member -Name DHCPServer -Value $DHCPServer -MemberType NoteProperty -PassThru |
Add-Member -Name Note -Value $Note -MemberType NoteProperty -PassThru;
}

#endregion
#region DataCollection

#My environment has multiple child domains, I only wanted to run this against one of them
$DOMAINFILTER = "eu.blah.com"

#Working variables
$GlobalSubnetSiteHash = @{} #HashTable with the key value pair: Subnet and AD Site name. E.G "10.10.10.10/24", "Alaska"
$EUServerADSitesHash = @{} #HashTable with the key value pair: Server, AD Site Name. E.G. "USAKDC001", "Alaska"
$DHCPScopeArr = @() # Array of DHCP Scope object.

#Final Result variables
$SiteSubnetScopeArr = @()
$global:subnetInOtherSubnet = @()
#Start of data collection:

$sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites

$euSites = $sites | ? { $_.Domains | ? {$_.Name -eq $DOMAINFILTER } }

#Get Subnet for each site (Global - Could make this EU only)
foreach ($site in $sites)
{
foreach($subnet in $site.Subnets)
{
$GlobalSubnetSiteHash.Add($subnet.Name,$site.Name)
}
}

#Get server list for each AD Site
#Having a seperate loop for this may look slower, but it's not, enumerating servers is SLOW.
foreach($site in $euSites)
{
foreach($server in $site.Servers)
{
if($server.Domain -eq $null)
{ continue; }
if($server.Domain.ToString() -eq $DOMAINFILTER)
{
$EUServerADSitesHash.Add($server.Name,$site.Name)
}
}
}

Import-Module DHCPServer

$DHCPServers = Get-DHCPServerInDC | ? { $_.DNSName.Contains($DOMAINFILTER) }

foreach($DHCPServer in $DHCPServers)
{
try
{
$scopes = Get-DHCPServerv4Scope -ComputerName $DHCPServer.DnsName | ? { $_.State -eq "Active" }
foreach($scope in $scopes)
{
$DHCPScopeArr += New-ScopeObject -ScopeID $scope.ScopeID.ToString() -SubnetMask $scope.SubnetMask.ToString() `
-StartRange $scope.StartRange.ToString() -EndRange $scope.EndRange.ToString() -SiteName $EUServerADSitesHash[$DHCPServer.DnsName] -DHCPServer $DHCPServer.DnsName
}
} catch {
Write-Output "Get DHCP Scopes Failure + $($DHCPServer.DnsName) "# Do Catching stuff;
$_
}

}

#endregion
#region DataProcessingAndOutput

#AD Site Subnets, which are members of other site subnets.
foreach($row in $GlobalSubnetSiteHash.GetEnumerator())
{
CheckSubnetInSubnets $row.key $row.value #This function writes to $global:subnetInOtherSubnet, hence no return here.

foreach($scope in $DHCPScopeArr)
{
$start = checkIPInSubnet -IP $Scope.ScopeID -SubNet $row.key
$end = checkIPInSubnet -IP $Scope.EndRange -SubNet $row.key

if($start -and $end)
{
$SiteSubnetScopeArr += New-SiteSubnetScopeObject -ADSite $row.value -ADSubnet $row.key -Scope "$($scope.ScopeID) $($scope.SubnetMask) $(netmaskToSlashNotation $scope.SubnetMask)" -ScopeSite $Scope.SiteName -DHCPServer $Scope.DHCPServer -Note "ScopeCompletelyWithinSiteSubnet";
} elseif ($start) {
$SiteSubnetScopeArr += New-SiteSubnetScopeObject -ADSite $row.value -ADSubnet $row.key -Scope "$($scope.ScopeID) $($scope.SubnetMask) $(netmaskToSlashNotation $scope.SubnetMask)" -ScopeSite $Scope.SiteName -DHCPServer $Scope.DHCPServer -Note "StartOfScopeInSiteSubnet";
} elseif ($end) {
$SiteSubnetScopeArr += New-SiteSubnetScopeObject -ADSite $row.value -ADSubnet $row.key -Scope "$($scope.ScopeID) $($scope.SubnetMask) $(netmaskToSlashNotation $scope.SubnetMask)" -ScopeSite $Scope.SiteName -DHCPServer $Scope.DHCPServer -Note "EndOfScopeInSiteSubnet";
}
}
}

$global:subnetInOtherSubnet | Export-Csv "C:\Temp\SubnetsInOtherSubnets$(get-date -Format "yyyyMMdd-HHmmss").csv" -NoTypeInformation
$SiteSubnetScopeArr | Export-Csv "C:\Temp\SiteSubnetScopes$(get-date -Format "yyyyMMdd-HHmmss").csv" -NoTypeInformation

#endregion