Files
RemLink/scripts/validation/Test-Architecture.ps1
T
qsc20001102 142e5dc7d6
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled
初版功能完成
2026-08-29 13:12:17 +08:00

110 lines
7.0 KiB
PowerShell

[CmdletBinding()]
param()
$ErrorActionPreference = "Stop"
$repository = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$goFiles = Get-ChildItem -Path $repository -Recurse -File -Filter *.go | Where-Object {
$_.FullName -notmatch '[\\/]frontend[\\/]node_modules[\\/]' -and $_.Name -notlike '*_test.go'
}
$violations = @()
foreach ($file in $goFiles) {
$relative = [System.IO.Path]::GetRelativePath($repository, $file.FullName)
$content = Get-Content -Raw $file.FullName
if ($relative -notlike 'internal\platform\windows\*' -and ($content -match 'powershell\.exe|Set-NetIPInterface|New-NetRoute|Remove-NetRoute')) {
$violations += "$relative contains a Windows network command outside internal/platform/windows"
}
if ($content -match 'New-NetNat|Get-NetNat|Remove-NetNat|Set-NetNat|IPEnableRouter|Disable-NetFirewall|Set-NetFirewallProfile|netsh\s+advfirewall') {
$violations += "$relative contains forbidden WinNAT, Windows forwarding, or firewall-disabling code"
}
if ($content -match 'S7Proxy|ModbusProxy|HTTPProxy|RDPProxy|WireGuardNT|TransitCIDR|TransitPrefix|SecondSubnetAdapter|SubnetAdapterName|\bP2P\b|\bSTUN\b|\bTURN\b|TAPDevice|EthernetFrame') {
$violations += "$relative contains a forbidden architecture symbol"
}
}
$siteConfig = Get-Content -Raw (Join-Path $repository 'internal/config/client.go')
if ($siteConfig -match '(?i)remote[_ ]?(cidr|subnet)|transit[_ ]?(cidr|prefix)') {
$violations += 'Site YAML configuration contains a forbidden Remote/Transit CIDR field'
}
$packetMux = Get-Content -Raw (Join-Path $repository 'internal/overlay/clientwg/packetmux.go')
if ($packetMux -match 'gopacket|tcpip/header|tcprelay|udprelay|pingrelay') {
$violations += 'PacketMux contains protocol parsing beyond IPv4 destination CIDR classification'
}
$serverSources = (Get-Content -Raw (Join-Path $repository 'cmd/server/main.go')) + (Get-Content -Raw (Join-Path $repository 'internal/overlay/serverwg/manager_linux.go'))
if ($serverSources -match 'wireguard/device|internal/overlay/clientwg|NewPacketMux|NewMuxTun') {
$violations += 'Server data plane imports client wireguard-go or PacketMux code'
}
$composePaths = @('deploy/docker/compose.yaml', 'deploy/docker/compose.release.yaml')
$composes = @{}
foreach ($composePath in $composePaths) {
$compose = Get-Content -Raw (Join-Path $repository $composePath)
$composes[$composePath] = $compose
if ($compose -match '(?m)^\s*privileged\s*:') { $violations += "$composePath enables privileged mode" }
if ($compose -notmatch 'NET_ADMIN') { $violations += "$composePath does not grant NET_ADMIN" }
if ($compose -notmatch '/dev/net/tun') { $violations += "$composePath does not map /dev/net/tun" }
if ($compose -match '(?m)^\s*-\s*"?(7001|6200):') { $violations += "$composePath publicly publishes an Overlay-only Control or Session port" }
if ($compose -match '(?i)MASQUERADE|\bSNAT\b') { $violations += "$composePath configures forbidden Overlay NAT" }
if ($compose -notmatch '\./data:/app/data') { $violations += "$composePath does not use the required ./data:/app/data persistence mount" }
if ($compose -notmatch '\$\{REMLINK_WG_PORT:-51820\}:\$\{REMLINK_WG_PORT:-51820\}/udp') {
$violations += "$composePath WireGuard host/container port mapping does not follow REMLINK_WG_PORT"
}
}
$compose = $composes['deploy/docker/compose.yaml']
$dockerServerConfig = Get-Content -Raw (Join-Path $repository 'deploy/docker/server.yaml')
$dockerfile = Get-Content -Raw (Join-Path $repository 'deploy/docker/Dockerfile')
if ($dockerServerConfig -notmatch '(?m)^\s*directory:\s*["'']?/app/data["'']?\s*$' -or $dockerfile -notmatch 'VOLUME \["/app/data"\]') {
$violations += 'Docker Server data directory does not target the required ./data:/app/data persistence mount'
}
$releaseDockerfile = Get-Content -Raw (Join-Path $repository 'deploy/docker/Dockerfile.release')
if ($releaseDockerfile -notmatch 'COPY linux-amd64/remlink-server' -or $releaseDockerfile -notmatch 'VOLUME \["/app/data"\]') {
$violations += 'Release Dockerfile does not package the released Server binary with persistent data'
}
$identityPathSource = Get-Content -Raw (Join-Path $repository 'internal/identity/path_windows.go')
$wintunRuntimeSource = Get-Content -Raw (Join-Path $repository 'internal/platform/windows/wintunruntime/runtime_windows.go')
if (($identityPathSource + $wintunRuntimeSource) -match 'ProgramData') {
$violations += 'Portable Windows identity or Wintun runtime still depends on ProgramData'
}
$releaseScript = Get-Content -Raw (Join-Path $repository 'scripts/build-release.ps1')
foreach ($packageName in @('RemLink-Engineer-v', 'RemLink-Site-v', 'RemLink-Server-v')) {
if ($releaseScript -notmatch [regex]::Escape($packageName)) {
$violations += "Release build does not define the independent $packageName package"
}
}
if ($releaseScript -match '\$windowsRoot') {
$violations += 'Release build still combines Engineer and Site under one Windows package root'
}
if ($releaseScript -notmatch 'go build[^\r\n]+-tags\s+"desktop,production"[^\r\n]+RemLinkEngineer\.exe') {
$violations += 'Engineer release build does not use the mandatory Wails desktop,production tags'
}
foreach ($dockerfileEntry in @{
'deploy/docker/Dockerfile' = $dockerfile
'deploy/docker/Dockerfile.release' = $releaseDockerfile
}.GetEnumerator()) {
if ($dockerfileEntry.Value -notmatch 'Acquire::Retries=5' -or
$dockerfileEntry.Value -notmatch 'Acquire::http::Timeout=30' -or
$dockerfileEntry.Value -notmatch 'APT_FORCE_IPV4' -or
$dockerfileEntry.Value -notmatch 'APT_DEBIAN_MIRROR' -or
$dockerfileEntry.Value -notmatch 'APT_SECURITY_MIRROR') {
$violations += "$($dockerfileEntry.Key) does not bound apt network waits and expose the IPv4 fallback"
}
}
$moduleText = Get-Content -Raw (Join-Path $repository 'go.mod')
$packageText = Get-Content -Raw (Join-Path $repository 'frontend/package.json')
if ($moduleText -match '@latest' -or $packageText -match '"(latest|\*)"') { $violations += 'A floating dependency version was found' }
$serverCollector = Get-Content -Raw (Join-Path $repository 'scripts/validation/Collect-ServerEvidence.sh')
if ($serverCollector -match '(?im)wg\s+show[^\r\n]*\bdump\b|show[^\r\n]*(private-key|preshared-key)') {
$violations += 'Server evidence collector may export WireGuard private or preshared keys'
}
$preflight = Get-Content -Raw (Join-Path $repository 'deploy/docker/preflight.sh')
if ($preflight -notmatch 'probe_interface="([^"]+)"') {
$violations += 'Docker preflight does not define a fixed WireGuard probe interface'
} elseif ($Matches[1].Length -gt 15) {
$violations += "Docker preflight interface '$($Matches[1])' exceeds the Linux 15-character interface-name limit"
}
if ($preflight -notmatch 'ip link error') {
$violations += 'Docker preflight suppresses the underlying ip link diagnostic'
}
if ($violations.Count -gt 0) {
$violations | ForEach-Object { Write-Error $_ }
exit 1
}
Write-Host "Architecture policy checks passed ($($goFiles.Count) production Go files inspected)."