Public/Get-VBmDNSRecord.ps1

function Get-VBmDNSRecord {
<#
.SYNOPSIS
    Discover mDNS services for an IP using dns-sd.exe (Layer 9).
 
.DESCRIPTION
    Runs dns-sd.exe -B for each of 5 service types, captures output for 5 seconds
    per type, and attempts to match results to the supplied IP address. A matching
    service name is returned as MDNSServiceType and MDNSServiceName.
 
    Service types probed:
        _pdl-datastream._tcp -- printers (raw/JetDirect over TCP)
        _ipp._tcp -- IPP printers
        _scanner._tcp -- scanners
        _http._tcp -- generic HTTP services (NAS, cameras, APs)
        _afpovertcp._tcp -- Apple Filing Protocol (Macs, NAS)
 
    mDNS is VLAN-bound. If the probe machine and target are on different VLANs,
    results will always be empty -- this is expected, not a bug. The context report
    states this explicitly.
 
    dns-sd.exe -B lists services by name, not by IP. The function resolves each
    discovered service name via dns-sd.exe -L to get its IP and matches against
    the target. This is inherently slow (5 s listen per type) and best run on
    the same VLAN as the target devices.
 
    Prerequisites: $Context.mDNSAvailable must be $true (dns-sd.exe on PATH).
 
.PARAMETER IPAddress
    The RFC1918 / CGNAT / link-local IP address to match.
 
.PARAMETER Context
    Environment context from Get-VBEnrichmentContext. Provides mDNSAvailable.
 
.OUTPUTS
    [PSCustomObject] -- base layer result fields plus:
        MDNSServiceType [string] e.g. '_ipp._tcp'
        MDNSServiceName [string] service instance name
 
.EXAMPLE
    $ctx = Get-VBEnrichmentContext
    Get-VBmDNSRecord -IPAddress '192.168.1.50' -Context $ctx
 
.EXAMPLE
    '192.168.1.50' | Get-VBmDNSRecord -Context $ctx
 
.NOTES
    Version: 1.0.0
    MinPSVersion: 5.1
    Author: VB
    ChangeLog:
        1.0.0 -- 2026-05-11 -- Initial release
#>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$IPAddress,

        [Parameter()]
        [PSCustomObject]$Context
    )

    begin {
        $LAYER_NUM  = 9
        $LAYER_NAME = 'mDNS'

        if (-not $Context) {
            Write-Warning "[$LAYER_NAME] No context provided -- running without prerequisite validation."
        }

        $ServiceTypes = @(
            '_pdl-datastream._tcp'
            '_ipp._tcp'
            '_scanner._tcp'
            '_http._tcp'
            '_afpovertcp._tcp'
        )

        # One-shot cache per run: browse all service types once and store results
        # keyed by IP so multiple IPs share the same dns-sd browse results
        $CacheTTLMinutes = 60
        if ($null -ne $Script:VBmDNSCache) {
            $ageMin = ((Get-Date) - $Script:VBmDNSCacheBuiltAt).TotalMinutes
            if ($ageMin -gt $CacheTTLMinutes) {
                Write-Verbose "[$LAYER_NAME] mDNS cache is $([int]$ageMin) min old (TTL $CacheTTLMinutes min) -- rebuilding"
                $Script:VBmDNSCache      = $null
                $Script:VBmDNSCacheBuilt = $false
            }
        }

        if ($null -eq $Script:VBmDNSCache) {
            $Script:VBmDNSCache      = @{}
            $Script:VBmDNSCacheBuilt = $false

            if ($Context -and $Context.mDNSAvailable) {
                Write-Verbose "[$LAYER_NAME] Building mDNS service cache (one-shot browse)..."
                $Script:VBmDNSCache      = Invoke-VBmDNSBrowse -ServiceTypes $ServiceTypes
                $Script:VBmDNSCacheBuilt = $true
                $Script:VBmDNSCacheBuiltAt = Get-Date
                Write-Verbose "[$LAYER_NAME] mDNS cache built: $($Script:VBmDNSCache.Count) IP entries"
            }
        }
    }

    process {
        $sw = [System.Diagnostics.Stopwatch]::StartNew()

        if ($Context -and -not $Context.mDNSAvailable) {
            $sw.Stop()
            return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                -Status 'Skipped' -ExecutionMs $sw.ElapsedMilliseconds `
                -SkipReason 'BonjourNotInstalled' `
                -Impact 'Printers, scanners, and Apple devices using mDNS only may not be identified'
        }

        try {
            $entry = $Script:VBmDNSCache[$IPAddress]

            if ($null -eq $entry) {
                $sw.Stop()
                return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                    -Status 'NoResult' -ExecutionMs $sw.ElapsedMilliseconds
            }

            $sw.Stop()
            New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                -Status 'Success' -ExecutionMs $sw.ElapsedMilliseconds `
                -ExtraFields @{
                    MDNSServiceType = $entry.ServiceType
                    MDNSServiceName = $entry.ServiceName
                }
        }
        catch {
            $sw.Stop()
            New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                -Status 'Failed' -ExecutionMs $sw.ElapsedMilliseconds `
                -ErrorDetail $_.Exception.Message
        }
    }
}