Friday, April 29, 2011

Finding what AD site an IP address is in.

In environments where there are a large number of subnets defined in Active Directory, sometimes it can be difficult to find what site an IP belongs to. You can always run nltest /dsgetsite on the system, but that requires access to the machine and is not very efficient. There is a good command line tool for this called atsn produced by joeware.net, but I thought it would be nice to have something in powershell for this.

This script takes the IP address of the machine as a mandatory argument (ipv4 only). You can provide either a network mask, or a mask length. If no length is provided, then the system will search all subnets from a /32 downwards. The script will search for any less specific subnets that exist in AD and return the first site name that it finds containing this IP.



#get-site-byIP, ipv4


Param(
[Parameter(Mandatory=$true,HelpMessage="IP Address")][validatepattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')]$ip,
[Parameter(Mandatory=$false,HelpMessage="Netmask")][validatepattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')]$netmask,
[Parameter(Mandatory=$false,HelpMessage="Mask length")][validaterange(0,32)][int]$masklength
)



function check-subnetformat([string]$subnet) {

$octetsegments = $subnet.split(".")
#Check each octet from last to first. If an octet does not contain 0, check to see
#if it is valid octet value for subnet masks. Then check to make sure that all preceeding
#octets are 255
$foundmostsignficant = $false
for ($i = 3; $i -ge 0; $i--) {
if ($octetsegments[$i] -ne 0) {
if ($foundmostsignificant -eq $true -and $octetsegments[$i] -ne 255) {
Write-Error "The subnet mask has an invalid value"
return $false
} else {
if ((255,254,252,248,240,224,192,128) -contains $octetsegments[$i]) {
$foundmostsignficant = $true
} else {
Write-Error "The subnet mask has an invalid value"
return $false
}

}
}
}
return $true

}


function get-subnetMask-byLength ([int]$length) {

switch ($length) {
"32" { return "255.255.255.255" }
"31" { return "255.255.255.254" }
"30" { return "255.255.255.252" }
"29" { return "255.255.255.248" }
"28" { return "255.255.255.240" }
"27" { return "255.255.255.224" }
"26" { return "255.255.255.192" }
"25" { return "255.255.255.128" }
"24" { return "255.255.255.0" }
"23" { return "255.255.254.0" }
"22" { return "255.255.252.0" }
"21" { return "255.255.248.0" }
"20" { return "255.255.240.0" }
"19" { return "255.255.224.0" }
"18" { return "255.255.192.0" }
"17" { return "255.255.128.0" }
"16" { return "255.255.0.0" }
"15" { return "255.254.0.0" }
"14" { return "255.252.0.0" }
"13" { return "255.248.0.0" }
"12" { return "255.240.0.0" }
"11" { return "255.224.0.0" }
"10" { return "255.192.0.0" }
"9" { return "255.128.0.0" }
"8" { return "255.0.0.0" }
"7" { return "254.0.0.0"}
"6" { return "252.0.0.0"}
"5" { return "248.0.0.0"}
"4" { return "240.0.0.0"}
"3" { return "224.0.0.0"}
"2" { return "192.0.0.0"}
"1" { return "128.0.0.0"}
"0" { return "0.0.0.0"}

}

}

function get-MaskLength-bySubnet ([string]$subnet) {

switch ($subnet) {
"255.255.255.255" {return 32}
"255.255.255.254" {return 31}
"255.255.255.252" {return 30}
"255.255.255.248" {return 29}
"255.255.255.240" {return 28}
"255.255.255.224" {return 27}
"255.255.255.192" {return 26}
"255.255.255.128" {return 25}
"255.255.255.0" {return 24}
"255.255.254.0" {return 23}
"255.255.252.0" {return 22}
"255.255.248.0" {return 21}
"255.255.240.0" {return 20}
"255.255.224.0" {return 19}
"255.255.192.0" {return 18}
"255.255.128.0" {return 17}
"255.255.0.0" {return 16}
"255.254.0.0" {return 15}
"255.252.0.0" {return 14}
"255.248.0.0" {return 13}
"255.240.0.0" {return 12}
"255.224.0.0" {return 11}
"255.192.0.0" {return 10}
"255.128.0.0" {return 9}
"255.0.0.0" {return 8}
"254.0.0.0" {return 7}
"252.0.0.0" {return 6}
"248.0.0.0" {return 5}
"240.0.0.0" {return 4}
"224.0.0.0" {return 3}
"192.0.0.0" {return 2}
"128.0.0.0" {return 1}
"0.0.0.0" {return 0}

}

}

function get-networkID ([string]$ipaddr, [string]$subnetmask) {
$ipoctets = $ipaddr.split(".")
$subnetoctets = $subnetmask.split(".")
$result = ""

for ($i = 0; $i -lt 4; $i++) {
$result += $ipoctets[$i] -band $subnetoctets[$i]
$result += "."
}
$result = $result.substring(0,$result.length -1)
return $result

}

$startMaskLength = 32

#we can take network masks in both length and full octet format. We need to use both. LDAP searches
#use length, and network ID generation is by full octet format.

if ($netmask -ne $null) {
if (-not(&check-subnetformat $netmask)) {
Write-Error "Subnet provided is not a valid subnet"
exit
} else {
$startmasklength = &get-MaskLength-bySubnet $netmask
}
}



$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$mytopleveldomain = $forest.schema.name
$mytopleveldomain = $mytopleveldomain.substring($mytopleveldomain.indexof("DC="))
$mytopleveldomain = "LDAP://cn=subnets,cn=sites,cn=configuration," + $mytopleveldomain
$de = New-Object directoryservices.DirectoryEntry($mytopleveldomain)
$ds = New-Object directoryservices.DirectorySearcher($de)
$ds.propertiestoload.add("cn") > $null
$ds.propertiestoLoad.add("siteobject") > $null


for ($i = $startMaskLength; $i -ge 0; $i--) {
#loop through netmasks from /32 to /0 looking for a subnet match in AD

#Go through all masks from longest to shortest
$mask = &get-subnetMask-byLength $i
$netwID = &get-networkID $ip $mask

#ldap search for the network
$ds.filter = "(&(objectclass=subnet)(objectcategory=subnet)(cn=" + $netwID + "/" + $i + "))"
$fu = $ds.findone()
if ($fu -ne $null) {

#if a match is found, return it since it is the longest length (closest match)
Write-Verbose "Found Subnet in AD at site:"
return $fu.properties.siteobject
}
$fu = $null
}

#if we have arrived at this point, the subnet does not exist in AD

return "Subnet_NOT_Assigned"