Windows下基于hosts的用户级网页流量黑名单实现及脚本问题排查
问题
为Windows平台MDM产品开发网页流量黑名单功能,需求是通过Python调用PowerShell脚本,传入域名和用户名参数后,以0.0.0.0映射的方式写入hosts文件实现域名阻止,且仅对指定用户生效。
当前使用的PowerShell脚本如下:
param( [string]$domainName, [string]$username ) # Get the current username $loggedInUserFull = Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty UserName $loggedInUser = $loggedInUserFull.Split('\')[-1] $currentUsername = $loggedInUser # Define log file path $logPath = "C:\Users\acer\Documents\log.txt" # Path to the hosts file $hostsPath = "C:\Windows\System32\drivers\etc\hosts" try { $hostsContent = Get-Content -Path $hostsPath -ErrorAction Stop $domainBlocked = $hostsContent | Where-Object { $_ -match "^\s*0\.0\.0\.0\s+$domainName\s*$" } $maxRetries = 3 $currentRetry = 0 while ($currentRetry -lt $maxRetries) { try { if ($currentUsername -eq $username -and -not $domainBlocked) { Add-Content -Path $hostsPath -Value "0.0.0.0 $domainName" -ErrorAction Stop Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Successfully blocked $domainName for user $username since current user is $currentUsername" break } elseif (-not ($currentUsername -eq $username) -and $domainBlocked) { $newHostsContent = $hostsContent | Where-Object { $_ -notlike "0.0.0.0 $domainName" } $newHostsContent | Set-Content -Path $hostsPath -ErrorAction Stop Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Successfully unblocked $domainName since current user is $currentUsername and rule was for user $username" break } else { break } } catch { if ($currentRetry -eq $maxRetries - 1) { Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Failed to modify hosts file after $maxRetries retries: $_" break } else { Start-Sleep -Seconds 5 $currentRetry++ Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Retry $currentRetry of ${maxRetries} for $domainName and ${username}: $_" } } } $StateChangeTrigger = Get-CimClass ` -Namespace Root/Microsoft/Windows/TaskScheduler ` -ClassName MSFT_TaskSessionStateChangeTrigger $TriggerUnlock = New-CimInstance ` -CimClass $StateChangeTrigger ` -Property @{StateChange = 8 } ` -ClientOnly # Schedule this script to run at user login for all users $taskName = "ManageHostsFileFor${username}and${domainName}" $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue if ($existingTask) { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Existing task $taskName unregistered." } $taskDescription = "Manages hosts file at user login for all users." $scriptPath = $PSCommandPath $action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument "-ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File `"$scriptPath`" -domainName $domainName -username $username" $triggerLogon = New-ScheduledTaskTrigger -AtLogOn $principal = New-ScheduledTaskPrincipal -UserID 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest # Create a new scheduled task settings object $settings = New-ScheduledTaskSettingsSet $settings.DisallowStartIfOnBatteries = $false # Allow running on battery power $settings.StartWhenAvailable = $true try { Register-ScheduledTask -TaskName $taskName -Description $taskDescription -Action $action -Trigger $triggerLogon, $TriggerUnlock -Principal $principal -Settings $settings Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Task scheduled successfully as $taskName for $username." } catch { Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') An error occurred during registration of task ${taskName}: $_" } } catch { Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') An error occurred during the process: $_" }
当前脚本存在两个核心问题:
- 需手动执行任务计划程序中的任务才能让hosts文件达到预期状态,自动触发不生效
- 偶发hosts文件被清空的情况,推测是并发写入导致
需要确认当前实现流程是否正确,同时提供无需第三方工具、可打包进MSI的改进方案或替代方案。
当前方案评估
流程逻辑缺陷
- 脚本通过
Win32_ComputerSystem获取当前登录用户,但当任务以SYSTEM权限运行时,Win32_ComputerSystem.UserName会返回空值(SYSTEM不是交互式登录用户),导致脚本无法判断目标用户,直接跳过hosts修改逻辑。 - 任务触发器设置为
AtLogOn但未指定目标用户,所有用户登录时都会执行脚本,无法实现“仅对指定用户生效”的需求。
hosts文件操作风险
- 使用
Set-Content覆盖整个hosts文件时,若读取内容后文件被其他进程修改,会导致原有配置丢失,甚至文件清空。 - 未对hosts文件加锁,并发写入时会引发文件损坏。
修复当前脚本的建议
1. 修正用户身份判断逻辑
当任务以SYSTEM权限运行时,需通过查询交互式会话获取当前登录用户:
# 替换原有的用户获取逻辑 $currentUsername = (Get-CimInstance -ClassName Win32_ComputerSystem).UserName.Split('\')[-1] # 若返回空,查询活跃交互式会话 if (-not $currentUsername) { $currentUsername = (Get-CimInstance -ClassName Win32_UserSession | Where-Object { $_.UserName -ne $null }).UserName | Select-Object -First 1 }
2. 优化hosts文件操作,避免并发问题
采用文件锁+临时文件替换的方式,防止并发写入和配置丢失:
# 替换原有的hosts修改逻辑 $tempHostsPath = "$hostsPath.tmp" try { # 打开文件并加排他锁,阻止其他进程写入 $fileStream = [System.IO.File]::Open($hostsPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) $reader = New-Object System.IO.StreamReader($fileStream) $writer = New-Object System.IO.StreamWriter($fileStream) $hostsContent = $reader.ReadToEnd() $domainRegex = [regex]"^\s*0\.0\.0\.0\s+$([regex]::Escape($domainName))\s*$" if ($currentUsername -eq $username) { # 若不存在则添加映射 if (-not $domainRegex.IsMatch($hostsContent)) { $writer.WriteLine("0.0.0.0 $domainName") Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Successfully blocked $domainName for user $username" } } else { # 若存在则删除映射 $newContent = $domainRegex.Replace($hostsContent, "") if ($newContent -ne $hostsContent) { $fileStream.SetLength(0) $writer.Write($newContent) Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Successfully unblocked $domainName for non-target user $currentUsername" } } $writer.Flush() $fileStream.Close() } catch { Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Failed to modify hosts file: $_" } finally { if (Test-Path $tempHostsPath) { Remove-Item $tempHostsPath -Force } }
3. 修正任务计划配置
- 将
AtLogOn触发器指定为目标用户,避免所有用户触发:
$triggerLogon = New-ScheduledTaskTrigger -AtLogOn -User $username
- 移除不必要的
TriggerUnlock触发器,避免重复执行 - 脚本路径使用绝对路径(Python调用时传入,替代
$PSCommandPath,防止任务计划中路径失效)
替代方案(无需修改全局hosts)
1. 用户级防火墙规则
通过Windows防火墙创建针对指定用户的出站拦截规则,无需修改hosts文件,稳定性更高:
$ruleName = "Block $domainName for $username" # 解析域名IP并创建规则 $ipList = Resolve-DnsName $domainName -Type A | Select-Object -ExpandProperty IPAddress netsh advfirewall firewall add rule name="$ruleName" dir=out action=block remoteip=$($ipList -join ",") profile=any user=$username
注意:需定期刷新IP(域名解析可能变化),可配合任务计划执行更新脚本。
2. Windows Filtering Platform (WFP)
通过WFP API或PowerShell命令创建用户级网络过滤规则,支持更精细的流量控制,无需依赖DNS解析。此方法可直接拦截域名请求,无需跟踪IP变化。
3. 组策略(域环境)
若MDM部署在域环境中,可通过组策略对象(GPO)配置用户级hosts映射或防火墙规则,实现集中管理,无需本地脚本执行。
内容的提问来源于stack exchange,提问作者RAHUL JHA
相关产品推荐
相关产品推荐

