Enumerating DFS Namespaces with PowerShell

A domain-based DFS Namespace is not like regular file share on a single server. So what to do when you need to discover domain-based shares? Let's see if we can enumerate all DFS Namespaces in an Active Directory domain

Microsoft's Distributed File System

DFS (Distributed File System) is Microsoft's name for a set of replicated file system features included in Windows Server since 2000.
DFS consists of two core components:

  • DFS-R: Replication service
  • DFS-N: Client access view

DFS Replication is fairly efficient, built on top the delta-based Remote Differential Compression algorithm. DFS-R is one of my favorite Windows Server topics, but today we're gonna look at DFS Namespaces.

The basic idea is that by organizing replicated copies of file system directories into namespaces, we can provide domain members with access to the nearest (based on site location) copy of a directory as if it was a regular file share:

\\domain.fqdn\namespace\folder_target  # DFS Namespace
\\machine.fqdn\sharename\local_folder  # Regular file share

From the user's perspective, they look and act just alike, neat!

Discovering remote folders

In the case of a regular file share, we can easily interrogate WMI on the target machine to discover shares:

Get-CimInstance Win32_Share -ComputerName machine.fqdn

or use the Get-SmbShare cmdlet:

Get-SmbShare -ComputerName machine.fqdn

But what about domain-based DFS Namespaces? As with most other AD-integrated middleware in Windows, this information is stored in the directory itself. Since namespaces are per-domain, namespace configuration is stored in the System container of the Default Naming Context partition. We can use the ActiveDirectory module to gather the relevant data:

# Grab user domain and System container DN
$DomainInfo = Get-ADDomain
$SystemDN   = $DomainInfo.SystemsContainer

More specifically, the namespace objects are located in the container CN=Dfs-Configuration,CN=System,DC=domain,DC=fqdn, and are of the objectClass msDFS-Namespacev2

$Namespaces = Get-ADObject -Filter 'objectClass -eq "msDFS-Namespacev2"' -SearchBase "CN=Dfs-Configuration,$SystemDN" -Server $DomainInfo.DNSRoot

Now all we need to do is reconstruct the UNC path to the namespace root using the name from AD:

$Namespaces |ForEach-Object {
  '\\{0}\{1}' -f $DomainInfo.DNSRoot,$_.Name
}

Finally we end up with something like:

function Get-DFSNamespacePath
{
  param(
    [Parameter(Mandatory=$False, ValueFromPipeline=$true)]
    [ValidatePattern('[\w\{\}\.\-\*]+$')]
    [string[]]$Namespace,

    [Parameter(Mandatory=$False)]
    [string]$Domain
  )

  # Check if Domain name was supplied
  $DomainSplat = @{}
  if($PSBoundParameters.ContainsKey('Domain'))
  {
    $DomainSplat['Identity'] = $Domain
  }
  
  # Grab user domain and System container DN
  $DomainInfo = Get-ADDomain @DomainSplat
  $SystemDN   = $DomainInfo.SystemsContainer

  # Create namespace clause to search LDAP by name, multiple names and wildcard filter allowed
  if($PSBoundParameters.ContainsKey('Namespace'))
  {
    $NamespaceClauses = ($Namespace |ForEach-Object { "(name=$_)" }) -join ''
    $NamespaceClause  = '(|{0})' -f $NamespaceClauses
  }

  # Define final LDAP query filter and search base
  $NamespaceSplat = @{
    LDAPFilter = '(&(objectClass=msDFS-Namespacev2){0})' -f $NamespaceClause
    SearchBase = 'CN=Dfs-Configuration,{0}' -f $SystemDN
    Server = $DomainInfo.DNSRoot
  }

  # Query directory for namespace objects, output UNC paths
  Get-ADObject @NamespaceSplat |ForEach-Object {
    '\\{0}\{1}' -f $DomainInfo.DNSRoot,$_.Name
  }
}

And that's it :-)