Add location-aware DNS suffix auto-switch for LBOOK

LBOOK is domain-joined to intern.beletage.ro, so the AD suffix was searched first for every single-label name. Off the office LAN, home names like home-ws were tried as home-ws.intern.beletage.ro, routed by NRPT to the AD DNS over VPN, stalling ~11s on timeout before falling back to .lan — slow RDP to home hosts.

scripts/dns-location-suffix.ps1 sets the global suffix search order from the physical NIC subnet (Sophos TAP excluded): intern-first on the office LAN (10.10.10.x / 10.10.40.x), lan-first everywhere else. install-dns-location-task.ps1 registers it as a SYSTEM scheduled task triggered on network-connect (NetworkProfile 10000) and logon; verify-dns-location-task.ps1 reads it back (the task is not queryable unelevated). Also adds .claude/settings.json allowlisting read-only network diagnostics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:19:16 +03:00
parent d5d6703a1a
commit 5b2e2aa417
4 changed files with 188 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"PowerShell(Get-NetAdapter)",
"PowerShell(Get-NetAdapter *)",
"PowerShell(Get-NetRoute *)",
"PowerShell(Get-VpnConnection)",
"PowerShell(Get-VpnConnection *)",
"PowerShell(Get-NetIPAddress *)",
"PowerShell(ipconfig /all)",
"PowerShell(klist)",
"PowerShell(klist *)",
"PowerShell(Clear-DnsClientCache)",
"PowerShell(ipconfig /flushdns)",
"Bash(pdftotext *)"
]
}
}
+72
View File
@@ -0,0 +1,72 @@
<#
.SYNOPSIS
Auto-switch the global DNS suffix search order based on network location.
.DESCRIPTION
Office LAN (physical NIC in 10.10.10.x / 10.10.40.x) -> intern.beletage.ro first.
Home / remote / VPN-from-anywhere -> lan first.
Rationale: LBOOK is domain-joined to intern.beletage.ro. When the AD suffix is
searched first, every single-label home name (e.g. "home-ws") is first tried as
<name>.intern.beletage.ro, which the NRPT rule routes to the AD DNS 10.10.10.2.
Off the office LAN that server is only reachable over VPN (and not at all when the
VPN is down), so the query stalls on a timeout before falling back to <name>.lan.
Putting "lan" first makes home names resolve locally (~3 ms); work names still
resolve via the NRPT rule after a fast local .lan NXDOMAIN.
The Sophos TAP adapter is excluded from detection so that starting the VPN from
home (which may assign a 10.10.x address on the tunnel) does not flip us to the
"office" ordering.
Installed as a SYSTEM scheduled task (see install-dns-location-task.ps1), triggered
on network-connect events and at logon. Idempotent: only writes when the order
actually needs to change.
#>
$ErrorActionPreference = 'Stop'
$log = 'C:\ProgramData\Beletage\dns-location.log'
$dir = Split-Path $log
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
function Write-Log($msg) {
$line = '{0} {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $msg
Add-Content -Path $log -Value $line -Encoding UTF8
}
try {
$officePrefixes = @('10.10.10.', '10.10.40.')
# Indices of VPN/tunnel adapters to ignore (Sophos SSL VPN TAP).
$vpnIdx = (Get-NetAdapter -ErrorAction SilentlyContinue |
Where-Object { $_.InterfaceDescription -like '*Sophos*' }).ifIndex
$ips = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object {
$_.IPAddress -notlike '169.254.*' -and
$_.IPAddress -ne '127.0.0.1' -and
($vpnIdx -notcontains $_.InterfaceIndex)
}).IPAddress
$atOffice = $false
foreach ($ip in $ips) {
foreach ($p in $officePrefixes) {
if ($ip.StartsWith($p)) { $atOffice = $true }
}
}
$desired = if ($atOffice) { @('intern.beletage.ro', 'lan') } else { @('lan', 'intern.beletage.ro') }
$current = (Get-DnsClientGlobalSetting).SuffixSearchList
if (($current -join ',') -ne ($desired -join ',')) {
Set-DnsClientGlobalSetting -SuffixSearchList $desired
Clear-DnsClientCache
Write-Log ('CHANGED ips=[{0}] office={1} [{2}] -> [{3}]' -f ($ips -join ' '), $atOffice, ($current -join ','), ($desired -join ','))
}
else {
Write-Log ('OK ips=[{0}] office={1} already [{2}]' -f ($ips -join ' '), $atOffice, ($desired -join ','))
}
}
catch {
Write-Log ('ERROR {0}' -f $_.Exception.Message)
}
+64
View File
@@ -0,0 +1,64 @@
<#
.SYNOPSIS
One-time elevated installer for the location-aware DNS suffix task.
.DESCRIPTION
Copies dns-location-suffix.ps1 to C:\ProgramData\Beletage and registers a SYSTEM
scheduled task that runs it on every network-connect event and at logon. Re-runnable
(idempotent) and device-independent (locates the payload via $PSScriptRoot), so the
same script installs the automation on any machine where this repo is cloned.
Run elevated:
Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','<path>\install-dns-location-task.ps1'
#>
$ErrorActionPreference = 'Stop'
$taskName = 'Beletage-DNS-Location-Suffix'
$dst = 'C:\ProgramData\Beletage'
$payload = Join-Path $dst 'dns-location-suffix.ps1'
$log = Join-Path $dst 'install.log'
New-Item -ItemType Directory -Force -Path $dst | Out-Null
Start-Transcript -Path $log -Append | Out-Null
try {
# 1. Deploy the detection script next to where SYSTEM will run it.
Copy-Item (Join-Path $PSScriptRoot 'dns-location-suffix.ps1') $payload -Force
Write-Host "Copied payload -> $payload"
# 2. Action: run the detection script hidden, policy-bypassed.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$payload`""
# 3a. Trigger: NetworkProfile "network connected" event (ID 10000).
$evtTrigger = New-CimInstance -ClientOnly `
-CimClass (Get-CimClass -ClassName MSFT_TaskEventTrigger -Namespace Root/Microsoft/Windows/TaskScheduler)
$evtTrigger.Enabled = $true
$evtTrigger.Subscription = '<QueryList><Query Id="0" Path="Microsoft-Windows-NetworkProfile/Operational"><Select Path="Microsoft-Windows-NetworkProfile/Operational">*[System[(EventID=10000)]]</Select></Query></QueryList>'
# 3b. Trigger: at any user logon (covers boot / resume).
$logonTrigger = New-ScheduledTaskTrigger -AtLogOn
# 4. Run as SYSTEM, highest privileges.
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
Register-ScheduledTask -TaskName $taskName -Force `
-Action $action -Trigger $evtTrigger, $logonTrigger -Principal $principal -Settings $settings | Out-Null
Write-Host "Registered task: $taskName"
# 5. Run once now to apply the correct order immediately.
Start-ScheduledTask -TaskName $taskName
Start-Sleep -Seconds 2
Write-Host "Triggered initial run. Current suffix order: [$((Get-DnsClientGlobalSetting).SuffixSearchList -join ', ')]"
}
catch {
Write-Host "INSTALL FAILED: $($_.Exception.Message)"
throw
}
finally {
Stop-Transcript | Out-Null
}
+34
View File
@@ -0,0 +1,34 @@
<#
.SYNOPSIS
Elevated read-out of the Beletage-DNS-Location-Suffix task (triggers, principal,
last run) to C:\ProgramData\Beletage\verify.txt. The task runs as SYSTEM with a
restrictive security descriptor, so it is not queryable from a non-elevated shell.
Run elevated:
Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','<path>\verify-dns-location-task.ps1'
#>
$ErrorActionPreference = 'Stop'
$out = 'C:\ProgramData\Beletage\verify.txt'
$name = 'Beletage-DNS-Location-Suffix'
$lines = @()
try {
$t = Get-ScheduledTask -TaskName $name
$lines += "FOUND $($t.TaskName) State=$($t.State)"
$lines += "Principal $($t.Principal.UserId) RunLevel=$($t.Principal.RunLevel) LogonType=$($t.Principal.LogonType)"
$lines += "Triggers $($t.Triggers.Count)"
foreach ($tr in $t.Triggers) {
$cn = $tr.CimClass.CimClassName
$tag = if ($tr.Subscription) { ' (event: NetworkProfile connected / EventID 10000)' } else { '' }
$lines += " - $cn$tag"
}
$info = Get-ScheduledTaskInfo -TaskName $name
$lines += "LastRun $($info.LastRunTime) LastResult=0x$('{0:X}' -f $info.LastTaskResult)"
$lines += "NextRun $($info.NextRunTime)"
}
catch {
$lines += "ERROR $($_.Exception.Message)"
}
$lines | Set-Content -Path $out -Encoding UTF8