You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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: $_"
}

当前脚本存在两个核心问题:

  1. 需手动执行任务计划程序中的任务才能让hosts文件达到预期状态,自动触发不生效
  2. 偶发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 20:44:51