the shed // windows // credential relay

LLMNR, NetBIOS-NS, mDNS and WPAD are the four ways a mistyped hostname becomes a captured NTLM hash — and each one is a registry value or a per-adapter flag that quietly drifts back on. A Ruby audit that reads them through WMI and win32/registry, prints PASS/FAIL with the exact fix, and exits non-zero until the box is actually hardened.

Step through the build below:

win_nameres_audit.rb

Windows never takes “no” from DNS. When a name does not resolve — a typo, a decommissioned share, a laptop looking for its office printer from a coffee shop — the resolver falls back to asking the local segment: LLMNR, NetBIOS name service, mDNS, and a WPAD lookup for a proxy. Anyone on that segment can answer “that’s me”. The client then authenticates to the impostor with NTLM, no click required.

That is the whole trick behind Responder and ntlmrelayx, and it has been the first move in internal penetration tests for a decade because the fixes are boring registry values that nobody verifies after the GPO is written: EnableMulticast, TcpipNetbiosOptions, EnableMDNS, SMB1, SMB signing, WinHttpAutoProxySvc.

win_nameres_audit.rb reads all of them — per adapter via WMI, per policy via the registry — and prints PASS/FAIL with the exact fix, exiting 2 while any CRIT remains.

$ type win_nameres_audit.rb # win32ole + win32/registry

#!/usr/bin/env ruby
# frozen_string_literal: true
#
# win_nameres_audit.rb — audit a Windows host for the name-resolution and SMB
# settings that make credential-relay attacks (Responder, ntlmrelayx) work.
#
# When DNS fails to resolve a name, Windows falls back to asking the local
# network: LLMNR, NetBIOS-NS and mDNS broadcasts, plus a WPAD lookup for a proxy.
# Anyone on the same segment can answer "that's me", collect an NTLMv2 hash or
# relay the authentication, and be a domain user before lunch. Every one of
# these fallbacks can be turned off; this script checks whether they were.
#
# Checks (severity):
#   [CRIT] LLMNR          EnableMulticast policy missing or != 0
#   [CRIT] NETBIOS        NetBIOS over TCP/IP enabled on an IP-enabled adapter
#   [CRIT] SMB1_ENABLED   SMB1 server component present / not disabled
#   [WARN] SMB_SIGN_SRV   LanmanServer RequireSecuritySignature != 1
#   [WARN] SMB_SIGN_CLI   LanmanWorkstation RequireSecuritySignature != 1
#   [WARN] MDNS           EnableMDNS != 0 (Windows 10 1703+ answers mDNS by default)
#   [WARN] WPAD_AUTO      proxy auto-detect on, or WinHttpAutoProxySvc not disabled
#   [WARN] NETBT_NODE     NodeType != 2 (P-node) so NetBIOS still broadcasts
#   [INFO] WINS           a WINS server is configured (legacy, but not exploitable by itself)
#   [INFO] DNS_NOT_LOCAL  adapter DNS servers are not RFC1918/link-local (public resolvers on a domain host)
#
# Usage:  ruby win_nameres_audit.rb [--json]      (run elevated for HKLM policy keys)
# Exit:   0 clean, 1 warnings only, 2 any CRIT
#
# Requirements: Ruby 3.x on Windows; win32ole (stdlib) and win32-registry (default gem).
# The analysis layer takes plain Hashes/Arrays, so it is unit-tested on Linux with
# fixtures — see test_win_nameres_audit.rb.
require 'optparse'
require 'json'
# --------------------------------------------------------------------------
# Data sources — the only code that touches Windows.
# --------------------------------------------------------------------------
class WindowsSources
  def initialize
    require 'win32ole'
    require 'win32/registry'
    @wmi = WIN32OLE.connect('winmgmts:\\\\.\\root\\cimv2')
  end
  # One Hash per IP-enabled adapter (WMI Win32_NetworkAdapterConfiguration).
  def adapters
    q = 'SELECT Description, IPEnabled, IPAddress, DHCPEnabled, DNSServerSearchOrder, TcpipNetbiosOptions, WINSPrimaryServer FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = TRUE'
    @wmi.ExecQuery(q).map do |a|
      { description: a.Description.to_s, ip: Array(a.IPAddress).map(&:to_s), dhcp: a.DHCPEnabled ? true : false,
        dns: Array(a.DNSServerSearchOrder).map(&:to_s), netbios: a.TcpipNetbiosOptions.to_i, wins: a.WINSPrimaryServer.to_s }
    end
  end
  # SMB1 feature state via WMI Win32_OptionalFeature (InstallState 1 = enabled, 2 = disabled)
  def smb1_feature_state
    @wmi.ExecQuery("SELECT InstallState FROM Win32_OptionalFeature WHERE Name = 'SMB1Protocol-Server'").map { |f| f.InstallState.to_i }.first
  rescue WIN32OLERuntimeError
    nil
  end
  # Registry values as a flat Hash of "HIVE\\path\\value" => data (nil when absent).
  REG_VALUES = {
    'llmnr'       => ['HKLM', 'SOFTWARE\Policies\Microsoft\Windows NT\DNSClient', 'EnableMulticast'],
    'mdns'        => ['HKLM', 'SYSTEM\CurrentControlSet\Services\Dnscache\Parameters', 'EnableMDNS'],
    'nodetype'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\NetBT\Parameters', 'NodeType'],
    'smb1'        => ['HKLM', 'SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters', 'SMB1'],
    'sign_srv'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters', 'RequireSecuritySignature'],
    'sign_cli'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters', 'RequireSecuritySignature'],
    'wpad_svc'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\WinHttpAutoProxySvc', 'Start'],
    'wpad_policy' => ['HKLM', 'SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings', 'EnableAutoProxyResultCache'],
    'wpad_user'   => ['HKCU', 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections', 'DefaultConnectionSettings']
  }.freeze
  def registry
    REG_VALUES.transform_values do |hive, path, name|
      root = hive == 'HKLM' ? Win32::Registry::HKEY_LOCAL_MACHINE : Win32::Registry::HKEY_CURRENT_USER
      root.open(path, Win32::Registry::KEY_READ | 0x0100) { |k| k[name] }
    rescue Win32::Registry::Error
      nil
    end
  end
end
# --------------------------------------------------------------------------
# Analysis — pure Ruby. adapters is an Array of Hashes, reg a Hash of the
# keys above, smb1_state an Integer or nil.
# --------------------------------------------------------------------------
module Analyzer
  Check = Struct.new(:severity, :code, :status, :detail, :fix, keyword_init: true)
  PRIVATE_DNS = [/\A10\./, /\A192\.168\./, /\A172\.(1[6-9]|2\d|3[01])\./, /\A127\./, /\A169\.254\./, /\Afe80:/i, /\Afd/i, /\A::1\z/].freeze
  def self.run(adapters, reg, smb1_state)
    c = []
    # LLMNR: policy value 0 disables. Absent = enabled (the default).
    llmnr = reg['llmnr']
    c << Check.new(severity: 'CRIT', code: 'LLMNR', status: llmnr.to_i.zero? && !llmnr.nil? ? 'PASS' : 'FAIL',
                   detail: llmnr.nil? ? 'EnableMulticast policy not set (LLMNR on by default)' : "EnableMulticast=#{llmnr}",
                   fix: 'reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" /v EnableMulticast /t REG_DWORD /d 0 /f')
    # NetBIOS over TCP/IP per adapter: 0 = via DHCP (usually on), 1 = on, 2 = off
    nb_on = adapters.select { |a| a[:netbios] != 2 }
    c << Check.new(severity: 'CRIT', code: 'NETBIOS', status: nb_on.empty? ? 'PASS' : 'FAIL',
                   detail: nb_on.empty? ? 'disabled on all IP-enabled adapters' : nb_on.map { |a| "#{a[:description]} (TcpipNetbiosOptions=#{a[:netbios]})" }.join('; '),
                   fix: 'wmic nicconfig where IPEnabled=true call SetTcpipNetbios 2   (or Set-NetAdapterBinding / DHCP option 001)')
    # mDNS: EnableMDNS 0 disables; absent = enabled on Win10 1703+
    md = reg['mdns']
    c << Check.new(severity: 'WARN', code: 'MDNS', status: !md.nil? && md.to_i.zero? ? 'PASS' : 'FAIL',
                   detail: md.nil? ? 'EnableMDNS not set (mDNS responder on by default)' : "EnableMDNS=#{md}",
                   fix: 'reg add HKLM\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters /v EnableMDNS /t REG_DWORD /d 0 /f')
    # NetBT node type: 2 = P-node (WINS only, no broadcast). 1 B, 4 M, 8 H.
    nt = reg['nodetype']
    c << Check.new(severity: 'WARN', code: 'NETBT_NODE', status: nt.to_i == 2 ? 'PASS' : 'FAIL',
                   detail: nt.nil? ? 'NodeType not set (H-node when WINS configured, else B-node broadcasts)' : "NodeType=#{nt}",
                   fix: 'reg add HKLM\SYSTEM\CurrentControlSet\Services\NetBT\Parameters /v NodeType /t REG_DWORD /d 2 /f')
    # SMB1: feature disabled (InstallState 2) or SMB1=0 in LanmanServer
    smb1 = reg['smb1']
    smb1_off = (smb1_state == 2) || (!smb1.nil? && smb1.to_i.zero?)
    c << Check.new(severity: 'CRIT', code: 'SMB1_ENABLED', status: smb1_off ? 'PASS' : 'FAIL',
                   detail: "SMB1Protocol-Server InstallState=#{smb1_state.inspect}, LanmanServer\\SMB1=#{smb1.inspect}",
                   fix: 'Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol  /  Set-SmbServerConfiguration -EnableSMB1Protocol $false')
    # SMB signing
    c << Check.new(severity: 'WARN', code: 'SMB_SIGN_SRV', status: reg['sign_srv'].to_i == 1 ? 'PASS' : 'FAIL',
                   detail: "LanmanServer RequireSecuritySignature=#{reg['sign_srv'].inspect}",
                   fix: 'Set-SmbServerConfiguration -RequireSecuritySignature $true')
    c << Check.new(severity: 'WARN', code: 'SMB_SIGN_CLI', status: reg['sign_cli'].to_i == 1 ? 'PASS' : 'FAIL',
                   detail: "LanmanWorkstation RequireSecuritySignature=#{reg['sign_cli'].inspect}",
                   fix: 'Set-SmbClientConfiguration -RequireSecuritySignature $true')
    # WPAD: service Start 4 = disabled. DefaultConnectionSettings byte 8 bit 0x08 = auto-detect on.
    svc_disabled = reg['wpad_svc'].to_i == 4
    dcs = reg['wpad_user']
    autodetect = dcs.is_a?(String) && dcs.bytesize > 8 ? (dcs.getbyte(8) & 0x08) != 0 : nil
    wpad_fail = !svc_disabled || autodetect == true
    c << Check.new(severity: 'WARN', code: 'WPAD_AUTO', status: wpad_fail ? 'FAIL' : 'PASS',
                   detail: "WinHttpAutoProxySvc Start=#{reg['wpad_svc'].inspect}#{autodetect.nil? ? '' : ", IE auto-detect=#{autodetect}"}",
                   fix: 'sc config WinHttpAutoProxySvc start= disabled; untick "Automatically detect settings"; add a wpad DNS record pointing at a sinkhole')
    # WINS / DNS hygiene (informational)
    wins = adapters.select { |a| !a[:wins].empty? }
    c << Check.new(severity: 'INFO', code: 'WINS', status: wins.empty? ? 'PASS' : 'FAIL',
                   detail: wins.empty? ? 'no WINS servers' : wins.map { |a| "#{a[:description]} -> #{a[:wins]}" }.join('; '), fix: 'remove WINS once NetBIOS is off')
    pub = adapters.flat_map { |a| a[:dns].reject { |d| PRIVATE_DNS.any? { |re| d =~ re } }.map { |d| "#{a[:description]} -> #{d}" } }
    c << Check.new(severity: 'INFO', code: 'DNS_NOT_LOCAL', status: pub.empty? ? 'PASS' : 'FAIL',
                   detail: pub.empty? ? 'all adapter DNS servers are private/link-local' : pub.join('; '), fix: 'point domain members at internal resolvers only')
    c
  end
end
def summarize(checks)
  fails = checks.reject { |k| k.status == 'PASS' }
  crit = fails.count { |k| k.severity == 'CRIT' }
  warn = fails.count { |k| k.severity == 'WARN' }
  [crit.positive? ? 'CRIT' : (warn.positive? ? 'WARN' : 'OK'), crit, warn]
end
def print_text(adapters, checks)
  status, crit, warn = summarize(checks)
  puts "win_nameres_audit  #{Time.now.strftime('%Y-%m-%d %H:%M')}  #{adapters.size} IP-enabled adapter(s)"
  puts '-' * 100
  adapters.each do |a|
    puts format('  %-40s ip=%-16s dhcp=%-5s netbios=%-1s dns=%s', a[:description][0, 40], a[:ip].first, a[:dhcp], a[:netbios], a[:dns].join(','))
  end
  puts
  puts format('%-6s %-14s %-5s %s', 'SEV', 'CHECK', 'RESULT', 'DETAIL')
  checks.each do |k|
    puts format('%-6s %-14s %-5s %s', k.severity, k.code, k.status, k.detail)
    puts format('%-6s %-14s %-5s fix: %s', '', '', '', k.fix) if k.status == 'FAIL' && k.severity != 'INFO'
  end
  puts
  puts "#{status}: #{crit} critical, #{warn} warning(s) — #{checks.count { |k| k.status == 'PASS' }}/#{checks.size} checks pass"
end
if __FILE__ == $PROGRAM_NAME
  opts = { json: false }
  OptionParser.new do |o|
    o.banner = 'Usage: win_nameres_audit.rb [--json]'
    o.on('--json', 'JSON output') { opts[:json] = true }
  end.parse!
  abort 'win_nameres_audit.rb only runs on Windows (needs win32ole + win32/registry)' unless RUBY_PLATFORM =~ /mingw|mswin|cygwin/
  src = WindowsSources.new
  adapters = src.adapters
  checks = Analyzer.run(adapters, src.registry, src.smb1_feature_state)
  status, crit, warn = summarize(checks)
  if opts[:json]
    puts JSON.pretty_generate(status: status, critical: crit, warnings: warn, adapters: adapters, checks: checks.map(&:to_h))
  else
    print_text(adapters, checks)
  end
  exit(crit.positive? ? 2 : (warn.positive? ? 1 : 0))
end

One class touches Windows; the rest is Hashes. WindowsSources#adapters queries Win32_NetworkAdapterConfiguration WHERE IPEnabled = TRUE and returns plain Hashes. #registry reads nine named values with KEY_WOW64_64KEY and returns nil for absent ones — absence is itself a finding for LLMNR and mDNS. #smb1_feature_state asks Win32_OptionalFeature.

Analyzer.run is ten explicit checks. Each produces a Check with severity, PASS/FAIL, the observed value, and a copy-pasteable fix. Severities are deliberate: LLMNR, NetBIOS and SMB1 are CRIT because each alone enables a well-known attack; signing, mDNS, node type and WPAD are WARN; WINS and public DNS servers are INFO.

WPAD needs a byte, not a value. Internet Explorer’s “Automatically detect settings” lives in the binary DefaultConnectionSettings blob — byte 8, bit 0x08. The script reads it with getbyte and also checks the WinHttpAutoProxySvc start type, because WinHTTP clients use WPAD independently of the browser.

Tested on Linux with fixtures. Because the analyzer takes plain data, the harness feeds it an unhardened workstation (expect CRIT) and the same box after the GPO (expect OK) and asserts on both.

$ ruby test_win_nameres_audit.rb # fixture harness (any OS); exit 2 because the unhardened fixture is CRIT

win_nameres_audit  2026-09-11 15:53  3 IP-enabled adapter(s)
----------------------------------------------------------------------------------------------------
  Intel(R) Ethernet Connection I219-LM     ip=10.20.5.41       dhcp=true  netbios=0 dns=10.20.0.10,10.20.0.11
  Intel(R) Wi-Fi 6 AX201 160MHz            ip=192.168.1.57     dhcp=true  netbios=0 dns=8.8.8.8,1.1.1.1
  Hyper-V Virtual Ethernet Adapter         ip=172.28.0.1       dhcp=false netbios=2 dns=
SEV    CHECK          RESULT DETAIL
CRIT   LLMNR          FAIL  EnableMulticast policy not set (LLMNR on by default)
                            fix: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" /v EnableMulticast /t REG_DWORD /d 0 /f
CRIT   NETBIOS        FAIL  Intel(R) Ethernet Connection I219-LM (TcpipNetbiosOptions=0); Intel(R) Wi-Fi 6 AX201 160MHz (TcpipNetbiosOptions=0)
                            fix: wmic nicconfig where IPEnabled=true call SetTcpipNetbios 2   (or Set-NetAdapterBinding / DHCP option 001)
WARN   MDNS           FAIL  EnableMDNS not set (mDNS responder on by default)
                            fix: reg add HKLM\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters /v EnableMDNS /t REG_DWORD /d 0 /f
WARN   NETBT_NODE     FAIL  NodeType not set (H-node when WINS configured, else B-node broadcasts)
                            fix: reg add HKLM\SYSTEM\CurrentControlSet\Services\NetBT\Parameters /v NodeType /t REG_DWORD /d 2 /f
CRIT   SMB1_ENABLED   FAIL  SMB1Protocol-Server InstallState=1, LanmanServer\SMB1=nil
                            fix: Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol  /  Set-SmbServerConfiguration -EnableSMB1Protocol $false
WARN   SMB_SIGN_SRV   FAIL  LanmanServer RequireSecuritySignature=0
                            fix: Set-SmbServerConfiguration -RequireSecuritySignature $true
WARN   SMB_SIGN_CLI   FAIL  LanmanWorkstation RequireSecuritySignature=0
                            fix: Set-SmbClientConfiguration -RequireSecuritySignature $true
WARN   WPAD_AUTO      FAIL  WinHttpAutoProxySvc Start=3, IE auto-detect=true
                            fix: sc config WinHttpAutoProxySvc start= disabled; untick "Automatically detect settings"; add a wpad DNS record pointing at a sinkhole
INFO   WINS           PASS  no WINS servers
INFO   DNS_NOT_LOCAL  FAIL  Intel(R) Wi-Fi 6 AX201 160MHz -> 8.8.8.8; Intel(R) Wi-Fi 6 AX201 160MHz -> 1.1.1.1
CRIT: 3 critical, 5 warning(s) — 1/10 checks pass
=== after hardening (GPO applied, SMB1 removed, NetBIOS off) ===
win_nameres_audit  2026-09-11 15:53  3 IP-enabled adapter(s)
----------------------------------------------------------------------------------------------------
  Intel(R) Ethernet Connection I219-LM     ip=10.20.5.41       dhcp=true  netbios=2 dns=10.20.0.10,10.20.0.11
  Intel(R) Wi-Fi 6 AX201 160MHz            ip=192.168.1.57     dhcp=true  netbios=2 dns=10.20.0.10,10.20.0.10
  Hyper-V Virtual Ethernet Adapter         ip=172.28.0.1       dhcp=false netbios=2 dns=
SEV    CHECK          RESULT DETAIL
CRIT   LLMNR          PASS  EnableMulticast=0
CRIT   NETBIOS        PASS  disabled on all IP-enabled adapters
WARN   MDNS           PASS  EnableMDNS=0
WARN   NETBT_NODE     PASS  NodeType=2
CRIT   SMB1_ENABLED   PASS  SMB1Protocol-Server InstallState=2, LanmanServer\SMB1=0
WARN   SMB_SIGN_SRV   PASS  LanmanServer RequireSecuritySignature=1
WARN   SMB_SIGN_CLI   PASS  LanmanWorkstation RequireSecuritySignature=1
WARN   WPAD_AUTO      PASS  WinHttpAutoProxySvc Start=4, IE auto-detect=false
INFO   WINS           PASS  no WINS servers
INFO   DNS_NOT_LOCAL  PASS  all adapter DNS servers are private/link-local
OK: 0 critical, 0 warning(s) — 10/10 checks pass
assertions: 6/6 passed
Get the code

Full script + README on GitHub: ruby-devops-toolkit/win-nameres-audit

01 // the problem

The fallback chain

Name resolution on Windows is a chain, not a lookup. The hosts file and DNS come first; when they fail, the OS broadcasts the question to everyone nearby using LLMNR (UDP 5355), NetBIOS name service (UDP 137) and, since Windows 10 1703, mDNS (UDP 5353). Separately, if proxy auto-detection is on, every WinHTTP client asks for a host literally named wpad — and falls through the same chain when DNS has no answer. An attacker running Responder on the segment replies to all of it, the victim connects to the attacker’s SMB or HTTP listener, and NTLM authentication happens automatically. The captured NTLMv2 response can be cracked offline, or relayed live to any server that does not require SMB signing.

Windows name-resolution fallback chain and where each audit finding cuts it

Every hop in the chain has an off switch. The audit checks whether each one is actually off.

None of this is new, and every step has a documented mitigation. The operational problem is drift: a new NIC arrives with NetBIOS set to “default” (on, via DHCP), an image predates the GPO, SMB1 gets re-enabled for a printer, a browser reset ticks “automatically detect settings” again. A checklist catches it once. A script you can schedule catches it every night.

02 // prerequisites

What you need

Requirements
  • Ruby 3.x on Windows (RubyInstaller). win32ole ships in the standard library on Windows; win32-registry is a bundled default gem (gem install win32-registry if require fails).
  • Windows 10 / Server 2016 or later. The registry paths and Win32_OptionalFeature class exist on older versions too, but the mDNS check only means something on 1703+.
  • Run elevated to read the HKLM policy and service keys reliably and to query Win32_OptionalFeature.
  • Any OS for the test harness — it never loads win32ole.
03 // the code

win_nameres_audit.rb

The full script: one WindowsSources class, one Analyzer module, a printer, and a main guarded by if __FILE__ == $PROGRAM_NAME.

win_nameres_audit.rbruby
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# win_nameres_audit.rb — audit a Windows host for the name-resolution and SMB
# settings that make credential-relay attacks (Responder, ntlmrelayx) work.
#
# When DNS fails to resolve a name, Windows falls back to asking the local
# network: LLMNR, NetBIOS-NS and mDNS broadcasts, plus a WPAD lookup for a proxy.
# Anyone on the same segment can answer "that's me", collect an NTLMv2 hash or
# relay the authentication, and be a domain user before lunch. Every one of
# these fallbacks can be turned off; this script checks whether they were.
#
# Checks (severity):
#   [CRIT] LLMNR          EnableMulticast policy missing or != 0
#   [CRIT] NETBIOS        NetBIOS over TCP/IP enabled on an IP-enabled adapter
#   [CRIT] SMB1_ENABLED   SMB1 server component present / not disabled
#   [WARN] SMB_SIGN_SRV   LanmanServer RequireSecuritySignature != 1
#   [WARN] SMB_SIGN_CLI   LanmanWorkstation RequireSecuritySignature != 1
#   [WARN] MDNS           EnableMDNS != 0 (Windows 10 1703+ answers mDNS by default)
#   [WARN] WPAD_AUTO      proxy auto-detect on, or WinHttpAutoProxySvc not disabled
#   [WARN] NETBT_NODE     NodeType != 2 (P-node) so NetBIOS still broadcasts
#   [INFO] WINS           a WINS server is configured (legacy, but not exploitable by itself)
#   [INFO] DNS_NOT_LOCAL  adapter DNS servers are not RFC1918/link-local (public resolvers on a domain host)
#
# Usage:  ruby win_nameres_audit.rb [--json]      (run elevated for HKLM policy keys)
# Exit:   0 clean, 1 warnings only, 2 any CRIT
#
# Requirements: Ruby 3.x on Windows; win32ole (stdlib) and win32-registry (default gem).
# The analysis layer takes plain Hashes/Arrays, so it is unit-tested on Linux with
# fixtures — see test_win_nameres_audit.rb.
require 'optparse'
require 'json'
# --------------------------------------------------------------------------
# Data sources — the only code that touches Windows.
# --------------------------------------------------------------------------
class WindowsSources
  def initialize
    require 'win32ole'
    require 'win32/registry'
    @wmi = WIN32OLE.connect('winmgmts:\\\\.\\root\\cimv2')
  end
  # One Hash per IP-enabled adapter (WMI Win32_NetworkAdapterConfiguration).
  def adapters
    q = 'SELECT Description, IPEnabled, IPAddress, DHCPEnabled, DNSServerSearchOrder, TcpipNetbiosOptions, WINSPrimaryServer FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = TRUE'
    @wmi.ExecQuery(q).map do |a|
      { description: a.Description.to_s, ip: Array(a.IPAddress).map(&:to_s), dhcp: a.DHCPEnabled ? true : false,
        dns: Array(a.DNSServerSearchOrder).map(&:to_s), netbios: a.TcpipNetbiosOptions.to_i, wins: a.WINSPrimaryServer.to_s }
    end
  end
  # SMB1 feature state via WMI Win32_OptionalFeature (InstallState 1 = enabled, 2 = disabled)
  def smb1_feature_state
    @wmi.ExecQuery("SELECT InstallState FROM Win32_OptionalFeature WHERE Name = 'SMB1Protocol-Server'").map { |f| f.InstallState.to_i }.first
  rescue WIN32OLERuntimeError
    nil
  end
  # Registry values as a flat Hash of "HIVE\\path\\value" => data (nil when absent).
  REG_VALUES = {
    'llmnr'       => ['HKLM', 'SOFTWARE\Policies\Microsoft\Windows NT\DNSClient', 'EnableMulticast'],
    'mdns'        => ['HKLM', 'SYSTEM\CurrentControlSet\Services\Dnscache\Parameters', 'EnableMDNS'],
    'nodetype'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\NetBT\Parameters', 'NodeType'],
    'smb1'        => ['HKLM', 'SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters', 'SMB1'],
    'sign_srv'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters', 'RequireSecuritySignature'],
    'sign_cli'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters', 'RequireSecuritySignature'],
    'wpad_svc'    => ['HKLM', 'SYSTEM\CurrentControlSet\Services\WinHttpAutoProxySvc', 'Start'],
    'wpad_policy' => ['HKLM', 'SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings', 'EnableAutoProxyResultCache'],
    'wpad_user'   => ['HKCU', 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections', 'DefaultConnectionSettings']
  }.freeze
  def registry
    REG_VALUES.transform_values do |hive, path, name|
      root = hive == 'HKLM' ? Win32::Registry::HKEY_LOCAL_MACHINE : Win32::Registry::HKEY_CURRENT_USER
      root.open(path, Win32::Registry::KEY_READ | 0x0100) { |k| k[name] }
    rescue Win32::Registry::Error
      nil
    end
  end
end
# --------------------------------------------------------------------------
# Analysis — pure Ruby. adapters is an Array of Hashes, reg a Hash of the
# keys above, smb1_state an Integer or nil.
# --------------------------------------------------------------------------
module Analyzer
  Check = Struct.new(:severity, :code, :status, :detail, :fix, keyword_init: true)
  PRIVATE_DNS = [/\A10\./, /\A192\.168\./, /\A172\.(1[6-9]|2\d|3[01])\./, /\A127\./, /\A169\.254\./, /\Afe80:/i, /\Afd/i, /\A::1\z/].freeze
  def self.run(adapters, reg, smb1_state)
    c = []
    # LLMNR: policy value 0 disables. Absent = enabled (the default).
    llmnr = reg['llmnr']
    c << Check.new(severity: 'CRIT', code: 'LLMNR', status: llmnr.to_i.zero? && !llmnr.nil? ? 'PASS' : 'FAIL',
                   detail: llmnr.nil? ? 'EnableMulticast policy not set (LLMNR on by default)' : "EnableMulticast=#{llmnr}",
                   fix: 'reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" /v EnableMulticast /t REG_DWORD /d 0 /f')
    # NetBIOS over TCP/IP per adapter: 0 = via DHCP (usually on), 1 = on, 2 = off
    nb_on = adapters.select { |a| a[:netbios] != 2 }
    c << Check.new(severity: 'CRIT', code: 'NETBIOS', status: nb_on.empty? ? 'PASS' : 'FAIL',
                   detail: nb_on.empty? ? 'disabled on all IP-enabled adapters' : nb_on.map { |a| "#{a[:description]} (TcpipNetbiosOptions=#{a[:netbios]})" }.join('; '),
                   fix: 'wmic nicconfig where IPEnabled=true call SetTcpipNetbios 2   (or Set-NetAdapterBinding / DHCP option 001)')
    # mDNS: EnableMDNS 0 disables; absent = enabled on Win10 1703+
    md = reg['mdns']
    c << Check.new(severity: 'WARN', code: 'MDNS', status: !md.nil? && md.to_i.zero? ? 'PASS' : 'FAIL',
                   detail: md.nil? ? 'EnableMDNS not set (mDNS responder on by default)' : "EnableMDNS=#{md}",
                   fix: 'reg add HKLM\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters /v EnableMDNS /t REG_DWORD /d 0 /f')
    # NetBT node type: 2 = P-node (WINS only, no broadcast). 1 B, 4 M, 8 H.
    nt = reg['nodetype']
    c << Check.new(severity: 'WARN', code: 'NETBT_NODE', status: nt.to_i == 2 ? 'PASS' : 'FAIL',
                   detail: nt.nil? ? 'NodeType not set (H-node when WINS configured, else B-node broadcasts)' : "NodeType=#{nt}",
                   fix: 'reg add HKLM\SYSTEM\CurrentControlSet\Services\NetBT\Parameters /v NodeType /t REG_DWORD /d 2 /f')
    # SMB1: feature disabled (InstallState 2) or SMB1=0 in LanmanServer
    smb1 = reg['smb1']
    smb1_off = (smb1_state == 2) || (!smb1.nil? && smb1.to_i.zero?)
    c << Check.new(severity: 'CRIT', code: 'SMB1_ENABLED', status: smb1_off ? 'PASS' : 'FAIL',
                   detail: "SMB1Protocol-Server InstallState=#{smb1_state.inspect}, LanmanServer\\SMB1=#{smb1.inspect}",
                   fix: 'Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol  /  Set-SmbServerConfiguration -EnableSMB1Protocol $false')
    # SMB signing
    c << Check.new(severity: 'WARN', code: 'SMB_SIGN_SRV', status: reg['sign_srv'].to_i == 1 ? 'PASS' : 'FAIL',
                   detail: "LanmanServer RequireSecuritySignature=#{reg['sign_srv'].inspect}",
                   fix: 'Set-SmbServerConfiguration -RequireSecuritySignature $true')
    c << Check.new(severity: 'WARN', code: 'SMB_SIGN_CLI', status: reg['sign_cli'].to_i == 1 ? 'PASS' : 'FAIL',
                   detail: "LanmanWorkstation RequireSecuritySignature=#{reg['sign_cli'].inspect}",
                   fix: 'Set-SmbClientConfiguration -RequireSecuritySignature $true')
    # WPAD: service Start 4 = disabled. DefaultConnectionSettings byte 8 bit 0x08 = auto-detect on.
    svc_disabled = reg['wpad_svc'].to_i == 4
    dcs = reg['wpad_user']
    autodetect = dcs.is_a?(String) && dcs.bytesize > 8 ? (dcs.getbyte(8) & 0x08) != 0 : nil
    wpad_fail = !svc_disabled || autodetect == true
    c << Check.new(severity: 'WARN', code: 'WPAD_AUTO', status: wpad_fail ? 'FAIL' : 'PASS',
                   detail: "WinHttpAutoProxySvc Start=#{reg['wpad_svc'].inspect}#{autodetect.nil? ? '' : ", IE auto-detect=#{autodetect}"}",
                   fix: 'sc config WinHttpAutoProxySvc start= disabled; untick "Automatically detect settings"; add a wpad DNS record pointing at a sinkhole')
    # WINS / DNS hygiene (informational)
    wins = adapters.select { |a| !a[:wins].empty? }
    c << Check.new(severity: 'INFO', code: 'WINS', status: wins.empty? ? 'PASS' : 'FAIL',
                   detail: wins.empty? ? 'no WINS servers' : wins.map { |a| "#{a[:description]} -> #{a[:wins]}" }.join('; '), fix: 'remove WINS once NetBIOS is off')
    pub = adapters.flat_map { |a| a[:dns].reject { |d| PRIVATE_DNS.any? { |re| d =~ re } }.map { |d| "#{a[:description]} -> #{d}" } }
    c << Check.new(severity: 'INFO', code: 'DNS_NOT_LOCAL', status: pub.empty? ? 'PASS' : 'FAIL',
                   detail: pub.empty? ? 'all adapter DNS servers are private/link-local' : pub.join('; '), fix: 'point domain members at internal resolvers only')
    c
  end
end
def summarize(checks)
  fails = checks.reject { |k| k.status == 'PASS' }
  crit = fails.count { |k| k.severity == 'CRIT' }
  warn = fails.count { |k| k.severity == 'WARN' }
  [crit.positive? ? 'CRIT' : (warn.positive? ? 'WARN' : 'OK'), crit, warn]
end
def print_text(adapters, checks)
  status, crit, warn = summarize(checks)
  puts "win_nameres_audit  #{Time.now.strftime('%Y-%m-%d %H:%M')}  #{adapters.size} IP-enabled adapter(s)"
  puts '-' * 100
  adapters.each do |a|
    puts format('  %-40s ip=%-16s dhcp=%-5s netbios=%-1s dns=%s', a[:description][0, 40], a[:ip].first, a[:dhcp], a[:netbios], a[:dns].join(','))
  end
  puts
  puts format('%-6s %-14s %-5s %s', 'SEV', 'CHECK', 'RESULT', 'DETAIL')
  checks.each do |k|
    puts format('%-6s %-14s %-5s %s', k.severity, k.code, k.status, k.detail)
    puts format('%-6s %-14s %-5s fix: %s', '', '', '', k.fix) if k.status == 'FAIL' && k.severity != 'INFO'
  end
  puts
  puts "#{status}: #{crit} critical, #{warn} warning(s) — #{checks.count { |k| k.status == 'PASS' }}/#{checks.size} checks pass"
end
if __FILE__ == $PROGRAM_NAME
  opts = { json: false }
  OptionParser.new do |o|
    o.banner = 'Usage: win_nameres_audit.rb [--json]'
    o.on('--json', 'JSON output') { opts[:json] = true }
  end.parse!
  abort 'win_nameres_audit.rb only runs on Windows (needs win32ole + win32/registry)' unless RUBY_PLATFORM =~ /mingw|mswin|cygwin/
  src = WindowsSources.new
  adapters = src.adapters
  checks = Analyzer.run(adapters, src.registry, src.smb1_feature_state)
  status, crit, warn = summarize(checks)
  if opts[:json]
    puts JSON.pretty_generate(status: status, critical: crit, warnings: warn, adapters: adapters, checks: checks.map(&:to_h))
  else
    print_text(adapters, checks)
  end
  exit(crit.positive? ? 2 : (warn.positive? ? 1 : 0))
end
win_nameres_audit.rb architecture

WMI for adapter state, the registry for policy, one pure-Ruby analyzer.
04 // walkthrough

How it works, step by step

1. Adapters via WMI

Win32_NetworkAdapterConfiguration has one instance per adapter, including dozens of virtual and disconnected ones, so the query filters on IPEnabled = TRUE. The properties that matter: TcpipNetbiosOptions (0 = use DHCP setting, which almost always means on; 1 = on; 2 = off), DNSServerSearchOrder, DHCPEnabled and WINSPrimaryServer. WMI returns arrays as nil when empty, hence Array(...) everywhere.

2. Nine registry values, read defensively

REG_VALUES maps a short key to [hive, path, value]. Each is opened read-only with 0x0100 (KEY_WOW64_64KEY) so a 32-bit Ruby is not redirected into WOW6432Node, and any Win32::Registry::Error — key missing, value missing, access denied — becomes nil. That matters because for EnableMulticast and EnableMDNS a missing value means the feature is on; the analyzer treats nil as FAIL for those and says so in the detail string.

3. The checks

LLMNR passes only when the policy value exists and is 0. NETBIOS passes only when every IP-enabled adapter reports 2; the detail lists the offenders by name. MDNS mirrors LLMNR. NETBT_NODE wants P-node (2), the only node type that never broadcasts. SMB1_ENABLED passes if the optional feature reports InstallState 2 (disabled) or LanmanServer\SMB1 is 0 — either is sufficient. SMB_SIGN_SRV/CLI check RequireSecuritySignature on both sides, because relay works in either direction. WPAD_AUTO fails if the WinHttpAutoProxySvc start type is not 4 (disabled) or the IE auto-detect bit is set. WINS and DNS_NOT_LOCAL are informational.

4. Decoding DefaultConnectionSettings

The “Automatically detect settings” checkbox has no DWORD of its own. It is bit 3 of byte 8 in a binary blob under Internet Settings\Connections; the other bits in that byte are “direct” (0x01), “use proxy” (0x02) and “use auto-config script” (0x04). dcs.getbyte(8) & 0x08 is the whole decode. win32/registry returns REG_BINARY values as a Ruby String, which is why the check guards on is_a?(String) and length.

5. Summarise, print, exit

summarize counts failing checks by severity and returns a status. The text printer shows the adapters, then one line per check and — only for failing CRIT/WARN checks — a fix: line with the reg add / PowerShell command. The exit code is the usual 0/1/2 contract so a scheduled task or RMM can alert on it.

05 // example output

The harness: before and after the GPO

The script refuses to run on non-Windows, so it ships with a stub harness that hands Analyzer.run what WMI and the registry would have returned. The first fixture is a typical domain-joined laptop that nobody hardened (LLMNR default, NetBIOS via DHCP on two NICs, SMB1 still installed, signing off, WPAD auto-detect on, public DNS on the Wi-Fi adapter). The second is the same machine after the GPO and SMB1 removal.

test_win_nameres_audit.rbruby
# test_win_nameres_audit.rb — stub harness: feeds Analyzer realistic fixtures
# (what WMI and the registry would return) so the scoring runs on any OS.
require_relative 'win_nameres_audit'
# A typical domain-joined workstation that nobody has hardened.
adapters = [
  { description: 'Intel(R) Ethernet Connection I219-LM', ip: ['10.20.5.41', 'fe80::1c2a:3b4c:5d6e:7f80'], dhcp: true,
    dns: ['10.20.0.10', '10.20.0.11'], netbios: 0, wins: '' },
  { description: 'Intel(R) Wi-Fi 6 AX201 160MHz', ip: ['192.168.1.57'], dhcp: true,
    dns: ['8.8.8.8', '1.1.1.1'], netbios: 0, wins: '' },
  { description: 'Hyper-V Virtual Ethernet Adapter', ip: ['172.28.0.1'], dhcp: false,
    dns: [], netbios: 2, wins: '' }
]
# IE DefaultConnectionSettings blob: byte 8 = 0x09 -> direct (0x01) + auto-detect (0x08)
dcs = [0x46, 0, 0, 0, 0x2a, 0, 0, 0, 0x09, 0, 0, 0].pack('C*')
reg_unhardened = { 'llmnr' => nil, 'mdns' => nil, 'nodetype' => nil, 'smb1' => nil, 'sign_srv' => 0, 'sign_cli' => 0,
                   'wpad_svc' => 3, 'wpad_policy' => nil, 'wpad_user' => dcs }
checks = Analyzer.run(adapters, reg_unhardened, 1)
print_text(adapters, checks)
status, crit, warn = summarize(checks)
raise "expected CRIT, got #{status}" unless status == 'CRIT'
raise 'LLMNR should fail when policy absent' unless checks.find { |c| c.code == 'LLMNR' }.status == 'FAIL'
raise 'NETBIOS should list two adapters' unless checks.find { |c| c.code == 'NETBIOS' }.detail.scan('TcpipNetbiosOptions').size == 2
raise 'WPAD should detect auto-detect bit' unless checks.find { |c| c.code == 'WPAD_AUTO' }.detail.include?('auto-detect=true')
raise 'public DNS should be reported' unless checks.find { |c| c.code == 'DNS_NOT_LOCAL' }.detail.include?('8.8.8.8')
puts
puts '=== after hardening (GPO applied, SMB1 removed, NetBIOS off) ==='
hardened = adapters.map { |a| a.merge(netbios: 2, dns: a[:dns].map { |d| d.start_with?('10.') ? d : '10.20.0.10' }) }
reg_hardened = { 'llmnr' => 0, 'mdns' => 0, 'nodetype' => 2, 'smb1' => 0, 'sign_srv' => 1, 'sign_cli' => 1,
                 'wpad_svc' => 4, 'wpad_policy' => nil, 'wpad_user' => [0x46, 0, 0, 0, 0x2b, 0, 0, 0, 0x01, 0, 0, 0].pack('C*') }
checks2 = Analyzer.run(hardened, reg_hardened, 2)
print_text(hardened, checks2)
status2, = summarize(checks2)
raise "expected OK after hardening, got #{status2}" unless status2 == 'OK'
puts 'assertions: 6/6 passed'
exit(crit.positive? ? 2 : (warn.positive? ? 1 : 0))
ruby test_win_nameres_audit.rb (exit 2)
win_nameres_audit 2026-09-11 15:53 3 IP-enabled adapter(s)
—————————————————————————————————-
Intel(R) Ethernet Connection I219-LM ip=10.20.5.41 dhcp=true netbios=0 dns=10.20.0.10,10.20.0.11
Intel(R) Wi-Fi 6 AX201 160MHz ip=192.168.1.57 dhcp=true netbios=0 dns=8.8.8.8,1.1.1.1
Hyper-V Virtual Ethernet Adapter ip=172.28.0.1 dhcp=false netbios=2 dns=
SEV CHECK RESULT DETAIL
CRIT LLMNR FAIL EnableMulticast policy not set (LLMNR on by default)
fix: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" /v EnableMulticast /t REG_DWORD /d 0 /f
CRIT NETBIOS FAIL Intel(R) Ethernet Connection I219-LM (TcpipNetbiosOptions=0); Intel(R) Wi-Fi 6 AX201 160MHz (TcpipNetbiosOptions=0)
fix: wmic nicconfig where IPEnabled=true call SetTcpipNetbios 2 (or Set-NetAdapterBinding / DHCP option 001)
WARN MDNS FAIL EnableMDNS not set (mDNS responder on by default)
fix: reg add HKLM\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters /v EnableMDNS /t REG_DWORD /d 0 /f
WARN NETBT_NODE FAIL NodeType not set (H-node when WINS configured, else B-node broadcasts)
fix: reg add HKLM\SYSTEM\CurrentControlSet\Services\NetBT\Parameters /v NodeType /t REG_DWORD /d 2 /f
CRIT SMB1_ENABLED FAIL SMB1Protocol-Server InstallState=1, LanmanServer\SMB1=nil
fix: Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol / Set-SmbServerConfiguration -EnableSMB1Protocol $false
WARN SMB_SIGN_SRV FAIL LanmanServer RequireSecuritySignature=0
fix: Set-SmbServerConfiguration -RequireSecuritySignature $true
WARN SMB_SIGN_CLI FAIL LanmanWorkstation RequireSecuritySignature=0
fix: Set-SmbClientConfiguration -RequireSecuritySignature $true
WARN WPAD_AUTO FAIL WinHttpAutoProxySvc Start=3, IE auto-detect=true
fix: sc config WinHttpAutoProxySvc start= disabled; untick "Automatically detect settings"; add a wpad DNS record pointing at a sinkhole
INFO WINS PASS no WINS servers
INFO DNS_NOT_LOCAL FAIL Intel(R) Wi-Fi 6 AX201 160MHz -> 8.8.8.8; Intel(R) Wi-Fi 6 AX201 160MHz -> 1.1.1.1
CRIT: 3 critical, 5 warning(s) — 1/10 checks pass
=== after hardening (GPO applied, SMB1 removed, NetBIOS off) ===
win_nameres_audit 2026-09-11 15:53 3 IP-enabled adapter(s)
—————————————————————————————————-
Intel(R) Ethernet Connection I219-LM ip=10.20.5.41 dhcp=true netbios=2 dns=10.20.0.10,10.20.0.11
Intel(R) Wi-Fi 6 AX201 160MHz ip=192.168.1.57 dhcp=true netbios=2 dns=10.20.0.10,10.20.0.10
Hyper-V Virtual Ethernet Adapter ip=172.28.0.1 dhcp=false netbios=2 dns=
SEV CHECK RESULT DETAIL
CRIT LLMNR PASS EnableMulticast=0
CRIT NETBIOS PASS disabled on all IP-enabled adapters
WARN MDNS PASS EnableMDNS=0
WARN NETBT_NODE PASS NodeType=2
CRIT SMB1_ENABLED PASS SMB1Protocol-Server InstallState=2, LanmanServer\SMB1=0
WARN SMB_SIGN_SRV PASS LanmanServer RequireSecuritySignature=1
WARN SMB_SIGN_CLI PASS LanmanWorkstation RequireSecuritySignature=1
WARN WPAD_AUTO PASS WinHttpAutoProxySvc Start=4, IE auto-detect=false
INFO WINS PASS no WINS servers
INFO DNS_NOT_LOCAL PASS all adapter DNS servers are private/link-local
OK: 0 critical, 0 warning(s) — 10/10 checks pass
assertions: 6/6 passed
10
checks
3
CRIT-class findings
6/6
harness assertions
06 // troubleshooting

When it does not behave

Common issues
  • NETBIOS fails on an adapter you do not care about. Hyper-V, WSL and VPN adapters are IP-enabled and often default to NetBIOS on. Either set them to 2 as well (there is no downside) or add a description filter to adapters.
  • LLMNR passes but Responder still gets hits. Check mDNS and NetBIOS — Responder listens on all three. Also confirm the policy is applied (gpresult /h), not just present in a GPO nobody linked.
  • SMB1_ENABLED reports InstallState=nil. Win32_OptionalFeature needs elevation and is absent on some Server Core builds. The LanmanServer\SMB1 value is the fallback; set it to 0 explicitly if you want the check to pass without the feature query.
  • WPAD_AUTO fails after you disabled the service. The IE auto-detect bit is per-user and the script reads HKCU for the account running it. Deploy the setting via user GPO, or accept WARN for the service account.
  • WIN32OLERuntimeError: Access is denied. Run elevated; WMI’s DCOM permissions block standard users from some classes.
  • Testing note. Windows APIs are not available on Linux, so WindowsSources was reviewed against the documented win32ole / Win32::Registry interfaces but not executed while writing this article. The complete analysis layer (Analyzer.run, summarize, print_text) was executed via the harness above on Ruby 3.4 — unhardened fixture CRIT, hardened fixture OK, six assertions passing. Run it once by hand on a Windows host before scheduling.
07 // extending

Where to take it next

Ideas
  • More relay-surface checks. LDAP signing and channel binding on domain controllers (LDAPServerIntegrity, LdapEnforceChannelBinding), NTLM restrictions (LmCompatibilityLevel, RestrictSendingNTLMTraffic), and IPv6 router-discovery for mitm6 all follow the same read-a-value-compare pattern.
  • Fleet mode over WMI. WIN32OLE.connect("winmgmts://#{host}/root/cimv2") reaches remote adapters; pair it with Win32::Registry‘s RegConnectRegistry equivalent or a remote PowerShell call for the policy values.
  • Auto-remediate with a flag. Every check already carries its fix string; an --apply mode that runs them (elevated, with a log) is a small addition — keep NETBIOS changes last, they briefly disrupt SMB sessions.
  • Baseline diff. Store --json per host and alert on transitions from PASS to FAIL, which is the “it drifted back” signal this whole script exists for.
  • Linux twin. avahi-daemon (mDNS) and systemd-resolved‘s LLMNR= and MulticastDNS= settings are the same attack surface on the other side of the office.