Загрузка данных
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Domain allow-list ONLY for chosen local account(s): local filtering proxy + per-user firewall rules.
Covers every program of that user (Edge, git, npm, pip, VS Code, Python...). Admin is not affected.
.DESCRIPTION
How it works
1. A tiny allow-list proxy (Python) runs as SYSTEM on 127.0.0.1:8899 (scheduled task at startup).
Files: C:\ProgramData\DevProxy (only SYSTEM and Administrators have access).
2. Windows Firewall: the restricted user may not connect anywhere except loopback, so the only
way out is the proxy. Administrators and system services are not affected.
3. The user's profile gets proxy settings: system proxy (Edge, VS Code) and environment
variables (git, npm, pip, Python, curl). The user must sign out and in once.
Actions
Install (default) -UserName german[,other]
Status health check + test requests
Blocked hosts blocked most often (what you may want to add)
Allow -Domains a.com,b.org adds domains to allowlist.txt (reloaded automatically)
Uninstall -UserName german [-RemoveFiles]
Needs a system-wide Python (run install-dev-tools.ps1 first) or -PythonPath.
The restricted account must exist, have signed in at least once, and must NOT be an administrator.
.EXAMPLE
.\setup-user-proxy.ps1 -UserName german
.\setup-user-proxy.ps1 -UserName german,Admin2 # several restricted accounts
.\setup-user-proxy.ps1 -Action Blocked
.\setup-user-proxy.ps1 -Action Allow -Domains vuejs.org,nuxt.com
.\setup-user-proxy.ps1 -Action Status
Run in Windows PowerShell as Administrator:
powershell -ExecutionPolicy Bypass -File .\setup-user-proxy.ps1 -UserName german
#>
[CmdletBinding()]
param(
[ValidateSet('Install', 'Status', 'Blocked', 'Allow', 'Uninstall')]
[string]$Action = 'Install',
[string[]]$UserName,
[string[]]$Domains,
[int]$Port = 8899,
[string]$PythonPath,
[switch]$RemoveFiles
)
$ErrorActionPreference = 'Stop'
$Dir = Join-Path $env:ProgramData 'DevProxy'
$ProxyFile = Join-Path $Dir 'proxy.py'
$AllowFile = Join-Path $Dir 'allowlist.txt'
$LogFile = Join-Path $Dir 'proxy.log'
$TaskName = 'DevProxy'
$FwGroup = 'DevProxy'
# ---------------------------------------------------------------------
# Embedded files
# ---------------------------------------------------------------------
$ProxyPy = @'
"""DevProxy: tiny allow-list forward proxy (HTTP + HTTPS CONNECT).
Listens on 127.0.0.1 only. A request is served only if its destination host is in
allowlist.txt (the domain itself or any subdomain). Everything else gets 403.
Fail-closed: if the allowlist is missing or empty, everything is blocked.
Log format (one event per line): time|ALLOW|host|port time|BLOCK|host|port
"""
import asyncio
import ipaddress
import logging
import logging.handlers
import os
import socket
from urllib.parse import urlsplit
BASE = os.path.dirname(os.path.abspath(__file__))
ALLOWLIST = os.path.join(BASE, 'allowlist.txt')
LOGFILE = os.path.join(BASE, 'proxy.log')
HOST = '127.0.0.1'
PORT = 8899
CONNECT_PORTS = {443} # HTTPS only
HTTP_PORTS = {80} # plain HTTP only
CONNECT_TIMEOUT = 15
HEADER_TIMEOUT = 30
log = logging.getLogger('devproxy')
log.setLevel(logging.INFO)
_h = logging.handlers.RotatingFileHandler(LOGFILE, maxBytes=5_000_000, backupCount=3, encoding='utf-8')
_h.setFormatter(logging.Formatter('%(asctime)s|%(message)s', '%Y-%m-%d %H:%M:%S'))
log.addHandler(_h)
_allow = set()
_mtime = None
def load_allow():
"""(Re)load the allowlist when the file changed."""
global _allow, _mtime
try:
m = os.path.getmtime(ALLOWLIST)
except OSError:
return
if m == _mtime:
return
new = set()
try:
with open(ALLOWLIST, encoding='utf-8-sig') as f:
for line in f:
line = line.split('#', 1)[0].strip().lower()
if line.startswith('*.'):
line = line[2:]
line = line.strip('.')
if line:
new.add(line)
except OSError:
return
_allow, _mtime = new, m
log.info('INFO|allowlist loaded|%d entries', len(new))
def allowed(host):
load_allow()
host = host.lower().strip('.')
if not host:
return False
try:
ipaddress.ip_address(host.strip('[]'))
return False # IP literals are never allowed
except ValueError:
pass
labels = host.split('.')
return any('.'.join(labels[i:]) in _allow for i in range(len(labels)))
async def open_upstream(host, port):
"""Connect to a PUBLIC address of host (never loopback/private/link-local)."""
loop = asyncio.get_running_loop()
infos = await loop.getaddrinfo(host, port, type=socket.SOCK_STREAM)
last = None
for _fam, _typ, _proto, _canon, sa in infos:
try:
ip = ipaddress.ip_address(sa[0])
except ValueError:
continue
if not ip.is_global:
continue
try:
return await asyncio.wait_for(asyncio.open_connection(sa[0], port), CONNECT_TIMEOUT)
except Exception as e: # try next address
last = e
raise last or OSError('no public address for host')
def reply(code, text, body=''):
data = body.encode()
head = ('HTTP/1.1 %d %s\r\nContent-Type: text/plain; charset=utf-8\r\n'
'Content-Length: %d\r\nConnection: close\r\n\r\n' % (code, text, len(data)))
return head.encode() + data
async def pipe(r, w):
try:
while True:
data = await r.read(65536)
if not data:
break
w.write(data)
await w.drain()
except Exception:
pass
finally:
try:
w.close()
except Exception:
pass
async def tunnel(cr, cw, ur, uw):
t1 = asyncio.ensure_future(pipe(cr, uw))
t2 = asyncio.ensure_future(pipe(ur, cw))
_done, pending = await asyncio.wait({t1, t2}, return_when=asyncio.FIRST_COMPLETED)
if pending:
_done, pending = await asyncio.wait(pending, timeout=5)
for t in pending:
t.cancel()
for w in (cw, uw):
try:
w.close()
except Exception:
pass
async def handle(reader, writer):
try:
try:
head = await asyncio.wait_for(reader.readuntil(b'\r\n\r\n'), HEADER_TIMEOUT)
except Exception:
return
lines = head.decode('latin-1').split('\r\n')
try:
method, target, version = lines[0].split(' ', 2)
except ValueError:
writer.write(reply(400, 'Bad Request'))
await writer.drain()
return
if method.upper() == 'CONNECT':
host, sep, port_s = target.rpartition(':')
if not sep or not port_s.isdigit():
writer.write(reply(400, 'Bad Request'))
await writer.drain()
return
host = host.strip('[]')
port = int(port_s)
if not (allowed(host) and port in CONNECT_PORTS):
log.info('BLOCK|%s|%d', host, port)
writer.write(reply(403, 'Forbidden', 'Blocked by allowlist: %s\n' % host))
await writer.drain()
return
try:
ur, uw = await open_upstream(host, port)
except Exception as e:
log.info('FAIL|%s|%d|%s', host, port, e)
writer.write(reply(502, 'Bad Gateway'))
await writer.drain()
return
log.info('ALLOW|%s|%d', host, port)
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
await writer.drain()
await tunnel(reader, writer, ur, uw)
return
# plain HTTP request in absolute form: GET http://host/path HTTP/1.1
u = urlsplit(target)
if u.scheme != 'http' or not u.hostname:
writer.write(reply(400, 'Bad Request', 'This is a filtering proxy.\n'))
await writer.drain()
return
host = u.hostname
port = u.port or 80
if not (allowed(host) and port in HTTP_PORTS):
log.info('BLOCK|%s|%d', host, port)
writer.write(reply(403, 'Forbidden', 'Blocked by allowlist: %s\n' % host))
await writer.drain()
return
path = (u.path or '/') + ('?' + u.query if u.query else '')
kept = []
for line in lines[1:]:
if not line:
continue
name = line.split(':', 1)[0].strip().lower()
if name in ('proxy-connection', 'proxy-authorization', 'connection', 'keep-alive'):
continue
kept.append(line)
new_head = ('%s %s %s\r\n%s\r\nConnection: close\r\n\r\n'
% (method, path, version, '\r\n'.join(kept))).encode('latin-1')
try:
ur, uw = await open_upstream(host, port)
except Exception as e:
log.info('FAIL|%s|%d|%s', host, port, e)
writer.write(reply(502, 'Bad Gateway'))
await writer.drain()
return
log.info('ALLOW|%s|%d', host, port)
uw.write(new_head)
await uw.drain()
await tunnel(reader, writer, ur, uw)
except Exception as e:
log.info('ERR|%s', e)
finally:
try:
writer.close()
except Exception:
pass
async def main():
server = await asyncio.start_server(handle, HOST, PORT, limit=65536)
log.info('INFO|listening on %s:%d', HOST, PORT)
async with server:
await server.serve_forever()
if __name__ == '__main__':
load_allow()
try:
asyncio.run(main())
except Exception as exc:
log.exception('FATAL|%s', exc)
raise
'@
$DefaultAllowlist = @'
# DevProxy allow-list. One domain per line. A domain also allows ALL of its subdomains.
# Lines starting with # are comments. The file is re-read automatically (no restart needed).
# Tip: run setup-user-proxy.ps1 -Action Blocked to see what the user tried to open.
# --- docs / references ---
developer.mozilla.org
mozilla.org
mozilla.net
learn.microsoft.com
devdocs.io
w3schools.com
web.dev
caniuse.com
json.org
regex101.com
# --- languages, runtimes, package registries ---
python.org
pypi.org
pythonhosted.org
nodejs.org
npmjs.com
npmjs.org
yarnpkg.com
typescriptlang.org
go.dev
rust-lang.org
php.net
# --- frameworks and libraries ---
react.dev
vuejs.org
angular.dev
svelte.dev
nextjs.org
vite.dev
vitejs.dev
expressjs.com
tailwindcss.com
getbootstrap.com
djangoproject.com
palletsprojects.com
fastapi.tiangolo.com
# --- git, code hosting, Q&A ---
github.com
githubusercontent.com
githubassets.com
git-scm.com
stackoverflow.com
stackexchange.com
sstatic.net
# --- VS Code (extension marketplace; remove these 3 lines to freeze extensions) ---
code.visualstudio.com
marketplace.visualstudio.com
vsassets.io
# --- search ---
duckduckgo.com
# --- messengers: MAX and VK (CDN names are from memory: if pictures/video are missing, check -Action Blocked) ---
max.ru
oneme.ru
vk.com
vk.ru
userapi.com
vkuser.net
vk-cdn.net
# vkvideo.ru
# mycdn.me
# --- learning ---
freecodecamp.org
javascript.info
learn.javascript.ru
metanit.com
# --- optional (remove the # to enable) ---
# youtube.com
# ytimg.com
# googlevideo.com
# claude.ai
# chatgpt.com
# github.io
# cdn.jsdelivr.net
# unpkg.com
# cdnjs.cloudflare.com
# fonts.googleapis.com
# fonts.gstatic.com
# ocsp.digicert.com
'@
# ---------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------
function Write-Text {
param([string]$Path, [string]$Text)
[IO.File]::WriteAllText($Path, $Text, (New-Object System.Text.UTF8Encoding($false)))
}
function Step-Header { param([string]$Text) Write-Host "`n== $Text ==" -ForegroundColor Cyan }
function Get-PythonExe {
if ($PythonPath) {
if (Test-Path -LiteralPath $PythonPath) { return $PythonPath }
throw "Python not found: $PythonPath"
}
$found = @(Get-ChildItem "$env:ProgramFiles\Python3*\python.exe" -ErrorAction SilentlyContinue |
Sort-Object { [int]($_.Directory.Name -replace '\D', '') } -Descending)
if ($found.Count -gt 0) { return $found[0].FullName }
if (Get-Command py -ErrorAction SilentlyContinue) {
$p = & py -3 -c "import sys;print(sys.executable)" 2>$null
if ($p -and (Test-Path -LiteralPath $p)) { return $p }
}
throw 'System-wide Python not found. Run install-dev-tools.ps1 first or pass -PythonPath.'
}
function Get-InstalledPort {
if (Test-Path -LiteralPath $ProxyFile) {
$m = Select-String -LiteralPath $ProxyFile -Pattern '^PORT = (\d+)' | Select-Object -First 1
if ($m) { return [int]$m.Matches[0].Groups[1].Value }
}
return $Port
}
function Get-TargetUsers {
if (-not $UserName -or $UserName.Count -eq 0) {
throw 'Specify -UserName (the restricted account), e.g. -UserName german'
}
$me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$admins = @(Get-LocalGroupMember -SID 'S-1-5-32-544' -ErrorAction SilentlyContinue | ForEach-Object { $_.SID.Value })
foreach ($n in $UserName) {
$u = Get-LocalUser -Name $n -ErrorAction SilentlyContinue
if (-not $u) { throw "Local user '$n' not found (check Get-LocalUser)" }
$sid = $u.SID.Value
if ($Action -eq 'Install') {
if ($sid -eq $me) { throw "'$n' is the account you are running as - you would lock yourself out" }
if ($admins -contains $sid) { throw "'$n' is an administrator - demote it first, an admin could remove the rules" }
}
$prof = Get-CimInstance Win32_UserProfile -Filter "SID='$sid'" -ErrorAction SilentlyContinue
$path = $null
if ($prof) { $path = $prof.LocalPath }
[pscustomobject]@{ Name = $u.Name; Sid = $sid; Path = $path }
}
}
function Use-UserHive {
# runs $Body with the registry root of the user's hive (loads NTUSER.DAT if the user is not signed in)
param($U, [scriptblock]$Body)
$tmp = $null
if (Test-Path -LiteralPath "Registry::HKEY_USERS\$($U.Sid)") {
$root = "Registry::HKEY_USERS\$($U.Sid)"
}
else {
$nt = Join-Path $U.Path 'NTUSER.DAT'
if (-not (Test-Path -LiteralPath $nt)) { Write-Warning "No NTUSER.DAT for $($U.Name)"; return }
$tmp = 'DevProxyTmp_' + $U.Sid.Replace('-', '_')
& reg.exe load "HKU\$tmp" $nt | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Warning "Could not load registry hive of $($U.Name)"; return }
$root = "Registry::HKEY_USERS\$tmp"
}
try { & $Body $root }
finally {
if ($tmp) {
[gc]::Collect(); [gc]::WaitForPendingFinalizers()
& reg.exe unload "HKU\$tmp" | Out-Null
}
}
}
function Set-UserProxy {
param($U, [int]$P)
if (-not $U.Path) {
Write-Warning "'$($U.Name)' has no profile yet (never signed in). Sign in once, sign out, then rerun Install. Until then he has no internet at all."
return
}
Use-UserHive -U $U -Body {
param($root)
$proxy = "http://127.0.0.1:$P"
$envKey = "$root\Environment"
if (-not (Test-Path -LiteralPath $envKey)) { New-Item -Path $envKey -Force | Out-Null }
$vals = @{
HTTP_PROXY = $proxy; HTTPS_PROXY = $proxy; ALL_PROXY = $proxy
NO_PROXY = 'localhost,127.0.0.1,::1'; NODE_USE_ENV_PROXY = '1'
}
foreach ($k in $vals.Keys) {
New-ItemProperty -LiteralPath $envKey -Name $k -Value $vals[$k] -PropertyType String -Force | Out-Null
}
$is = "$root\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
if (-not (Test-Path -LiteralPath $is)) { New-Item -Path $is -Force | Out-Null }
New-ItemProperty -LiteralPath $is -Name 'ProxyEnable' -Value 1 -PropertyType DWord -Force | Out-Null
New-ItemProperty -LiteralPath $is -Name 'ProxyServer' -Value "127.0.0.1:$P" -PropertyType String -Force | Out-Null
New-ItemProperty -LiteralPath $is -Name 'ProxyOverride' -Value 'localhost;127.*;[::1];<local>' -PropertyType String -Force | Out-Null
}
Write-Host " proxy settings written to profile of '$($U.Name)'"
}
function Remove-UserProxy {
param($U)
if (-not $U.Path) { return }
Use-UserHive -U $U -Body {
param($root)
$envKey = "$root\Environment"
foreach ($k in 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'NODE_USE_ENV_PROXY') {
Remove-ItemProperty -LiteralPath $envKey -Name $k -ErrorAction SilentlyContinue
}
$is = "$root\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
if (Test-Path -LiteralPath $is) {
New-ItemProperty -LiteralPath $is -Name 'ProxyEnable' -Value 0 -PropertyType DWord -Force | Out-Null
Remove-ItemProperty -LiteralPath $is -Name 'ProxyServer' -ErrorAction SilentlyContinue
Remove-ItemProperty -LiteralPath $is -Name 'ProxyOverride' -ErrorAction SilentlyContinue
}
}
Write-Host " proxy settings removed from profile of '$($U.Name)'"
}
function Set-FirewallRules {
param([string[]]$Sids)
Get-NetFirewallRule -Group $FwGroup -ErrorAction SilentlyContinue | Remove-NetFirewallRule
$sddl = 'D:' + (($Sids | ForEach-Object { "(A;;CC;;;$_)" }) -join '')
# everything except loopback (127.0.0.0/8 and ::1)
$v4 = @('0.0.0.0-126.255.255.255', '128.0.0.0-255.255.255.255')
$v6 = @('::2-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff')
New-NetFirewallRule -DisplayName 'DevProxy: block direct internet (IPv4) for restricted users' -Group $FwGroup `
-Direction Outbound -Action Block -Profile Any -Protocol Any -LocalUser $sddl -RemoteAddress $v4 -Enabled True | Out-Null
try {
New-NetFirewallRule -DisplayName 'DevProxy: block direct internet (IPv6) for restricted users' -Group $FwGroup `
-Direction Outbound -Action Block -Profile Any -Protocol Any -LocalUser $sddl -RemoteAddress $v6 -Enabled True | Out-Null
}
catch {
Write-Warning "IPv6 rule was not created ($($_.Exception.Message)). IPv6 traffic of the user is NOT blocked - disable IPv6 or add the rule manually."
}
foreach ($prof in (Get-NetFirewallProfile)) {
if (-not $prof.Enabled) { Write-Warning "Firewall profile '$($prof.Name)' is OFF - the rules do nothing there. Turn it on." }
}
}
function Test-Proxy {
param([int]$P)
$curl = Join-Path $env:windir 'System32\curl.exe'
$tests = @(
@('https://developer.mozilla.org', '200 expected'),
@('https://example.com', '403 expected (blocked)')
)
foreach ($t in $tests) {
$code = & $curl -s -o NUL -m 20 -x "http://127.0.0.1:$P" -w '%{http_connect}' $t[0] 2>$null
Write-Host (" {0,-34} CONNECT -> {1} ({2})" -f $t[0], $code, $t[1])
}
}
# ---------------------------------------------------------------------
# Actions
# ---------------------------------------------------------------------
function Install-DevProxy {
$users = @(Get-TargetUsers)
$py = Get-PythonExe
Step-Header 'Files'
New-Item -ItemType Directory -Path $Dir -Force | Out-Null
& icacls $Dir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null
if ($LASTEXITCODE -ne 0) { throw "icacls failed on $Dir" }
Write-Text $ProxyFile ($ProxyPy.Replace('PORT = 8899', "PORT = $Port"))
if (Test-Path -LiteralPath $AllowFile) {
Write-Host " allowlist.txt kept as is (your edits are preserved)"
}
else {
Write-Text $AllowFile $DefaultAllowlist
Write-Host " allowlist.txt created: $AllowFile"
}
Write-Host " proxy.py written: $ProxyFile (Python: $py)"
if (-not $py.StartsWith($env:ProgramFiles, [StringComparison]::OrdinalIgnoreCase)) {
Write-Warning 'Python is not under Program Files. Make sure standard users cannot modify that folder.'
}
Step-Header 'Proxy service (scheduled task, SYSTEM)'
$pyw = Join-Path (Split-Path $py) 'pythonw.exe'
$exe = $py
if (Test-Path -LiteralPath $pyw) { $exe = $pyw }
$act = New-ScheduledTaskAction -Execute $exe -Argument "`"$ProxyFile`"" -WorkingDirectory $Dir
$trg = New-ScheduledTaskTrigger -AtStartup
$pri = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
$set = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 99 `
-RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew -StartWhenAvailable `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName $TaskName -Action $act -Trigger $trg -Principal $pri -Settings $set -Force | Out-Null
Start-ScheduledTask -TaskName $TaskName
Start-Sleep -Seconds 3
if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) {
Write-Host " proxy is listening on 127.0.0.1:$Port"
}
else {
Write-Warning "Proxy is NOT listening. Check $LogFile (or run: & '$py' '$ProxyFile' in a console)."
}
Step-Header 'Firewall rules'
Set-FirewallRules -Sids ($users | ForEach-Object { $_.Sid })
Write-Host (" direct internet blocked for: " + (($users | ForEach-Object { $_.Name }) -join ', '))
Step-Header 'User profiles'
foreach ($u in $users) { Set-UserProxy -U $u -P $Port }
Step-Header 'Self-test (as administrator, through the proxy)'
Test-Proxy -P $Port
Write-Host @"
Done. Next:
1. The restricted user signs out and in (settings are read at sign-in).
2. As that user check (cmd/PowerShell):
curl.exe -I https://developer.mozilla.org -> works (via the proxy)
curl.exe -I https://example.com -> 403 (blocked)
curl.exe --noproxy "*" -I https://developer.mozilla.org -> must FAIL (firewall)
3. Browse as that user, then on your side: .\setup-user-proxy.ps1 -Action Blocked
Add missing helper domains with: .\setup-user-proxy.ps1 -Action Allow -Domains x.com,y.org
4. Do NOT also use the old machine-wide Edge policy (setup-dev-pc.ps1 -Steps Web): not needed any more.
"@
}
function Show-Status {
$p = Get-InstalledPort
Step-Header 'DevProxy status'
$t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
$tState = 'NOT INSTALLED'
if ($t) { $tState = "$($t.State)" }
Write-Host " Task: $tState"
$listening = 'NOT LISTENING'
if (Get-NetTCPConnection -LocalPort $p -State Listen -ErrorAction SilentlyContinue) { $listening = "127.0.0.1:$p OK" }
Write-Host " Listening: $listening"
$rules = @(Get-NetFirewallRule -Group $FwGroup -ErrorAction SilentlyContinue)
Write-Host " Firewall rules: $($rules.Count)"
foreach ($r in $rules) { Write-Host (" [{0}] {1}" -f $r.Enabled, $r.DisplayName) }
if (Test-Path -LiteralPath $AllowFile) {
$n = @(Get-Content -LiteralPath $AllowFile | Where-Object { $_ -and ($_ -notmatch '^\s*#') }).Count
Write-Host " Allow-list: $n entries ($AllowFile)"
}
Write-Host ' Test requests:'
Test-Proxy -P $p
if (Test-Path -LiteralPath $LogFile) {
Write-Host ' Last log lines:'
Get-Content -LiteralPath $LogFile -Tail 8 | ForEach-Object { Write-Host " $_" }
}
}
function Show-Blocked {
if (-not (Test-Path -LiteralPath $LogFile)) { throw "No log yet: $LogFile" }
$rows = Get-Content -LiteralPath $LogFile | Where-Object { $_ -match '\|BLOCK\|' } | ForEach-Object {
$f = $_ -split '\|'
[pscustomobject]@{ Time = $f[0]; Host = $f[2] }
}
if (-not $rows) { Write-Host 'Nothing blocked in the log.'; return }
$rows | Group-Object Host | ForEach-Object {
[pscustomobject]@{ Count = $_.Count; Host = $_.Name; Last = ($_.Group | Select-Object -Last 1).Time }
} | Sort-Object Count -Descending | Select-Object -First 40 | Format-Table -AutoSize
Write-Host 'Add what is legitimate: .\setup-user-proxy.ps1 -Action Allow -Domains host1,host2'
Write-Host '(allowing a domain also allows all its subdomains)'
}
function Add-Domains {
if (-not $Domains -or $Domains.Count -eq 0) { throw 'Specify -Domains a.com,b.org' }
if (-not (Test-Path -LiteralPath $AllowFile)) { throw "Not installed yet: $AllowFile" }
$have = @(Get-Content -LiteralPath $AllowFile | ForEach-Object { ($_ -split '#')[0].Trim().ToLower() } | Where-Object { $_ })
$text = [IO.File]::ReadAllText($AllowFile)
$add = ''
foreach ($d in $Domains) {
$x = $d.Trim().ToLower() -replace '^https?://', '' -replace '/.*$', '' -replace '^\*\.', ''
if (-not $x) { continue }
if ($have -contains $x) { Write-Host " already allowed: $x"; continue }
$add += "$x`n"
Write-Host " added: $x"
}
if ($add) {
if ($text.Length -gt 0 -and -not $text.EndsWith("`n")) { $text += "`n" }
Write-Text $AllowFile ($text + $add)
Write-Host 'The proxy picks the change up automatically (no restart needed).'
}
}
function Uninstall-DevProxy {
$users = @(Get-TargetUsers)
Step-Header 'Uninstall'
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Host ' scheduled task removed'
Get-NetFirewallRule -Group $FwGroup -ErrorAction SilentlyContinue | Remove-NetFirewallRule
Write-Host ' firewall rules removed'
foreach ($u in $users) { Remove-UserProxy -U $u }
if ($RemoveFiles -and (Test-Path -LiteralPath $Dir)) {
Remove-Item -LiteralPath $Dir -Recurse -Force
Write-Host " files removed: $Dir"
}
elseif (Test-Path -LiteralPath $Dir) {
Write-Host " files kept: $Dir (allow-list and log); add -RemoveFiles to delete them"
}
Write-Host 'The user must sign out and in to pick up the change.'
}
switch ($Action) {
'Install' { Install-DevProxy }
'Status' { Show-Status }
'Blocked' { Show-Blocked }
'Allow' { Add-Domains }
'Uninstall' { Uninstall-DevProxy }
}