security: contain physical miner commands in Stage A
Disable physical control through legacy HTTP/M30 producers, typed admission, queue dispatch and upgraded Windows executors. Restrict diagnostic transports to exact read-only operations, with ordinal PowerShell validation before sockets. Retire unsupported pending/claimed work with atomic, idempotent security audits. Preserve prior delivery evidence and report unknown outcomes after a prior claim, including explicitly repaired pending rows. Include containment, cancellation, concurrency and malformed-payload regressions and the old-Agent cutover runbook. Validation: independent final application-security revalidation PASS. Two unchanged baseline source-assertion failures are documented as exceptions. Production release still requires native Windows validation and verified fencing/stop/drain/upgrade; server cancellation alone cannot revoke a cached command held by an old Agent.
This commit is contained in:
1 parent
2d89e80410
commit
e898f283ab
28 files changed
+2377
-2379
No files matched your search
@@ -743,61 +743,7 @@ function Invoke-MinerPlainCommand {
|
||||
|
||||
[int]$TimeoutMs = 2500
|
||||
)
|
||||
|
||||
$client = $null
|
||||
|
||||
try {
|
||||
$client = New-Object System.Net.Sockets.TcpClient
|
||||
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
|
||||
|
||||
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
|
||||
throw "Connect timeout to $IpAddress`:$Port"
|
||||
}
|
||||
|
||||
$client.EndConnect($async)
|
||||
$client.ReceiveTimeout = $TimeoutMs
|
||||
$client.SendTimeout = $TimeoutMs
|
||||
|
||||
$stream = $client.GetStream()
|
||||
$payload = [System.Text.Encoding]::ASCII.GetBytes("{`"cmd`":`"$Command`"}`n")
|
||||
$stream.Write($payload, 0, $payload.Length)
|
||||
|
||||
try {
|
||||
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
|
||||
} catch {
|
||||
}
|
||||
|
||||
$buffer = New-Object byte[] 65536
|
||||
$chunks = New-Object System.Collections.ArrayList
|
||||
$started = Get-Date
|
||||
|
||||
while (((Get-Date) - $started).TotalMilliseconds -lt $TimeoutMs) {
|
||||
if (-not $stream.DataAvailable) {
|
||||
Start-Sleep -Milliseconds 20
|
||||
continue
|
||||
}
|
||||
|
||||
$read = $stream.Read($buffer, 0, $buffer.Length)
|
||||
|
||||
if ($read -le 0) {
|
||||
break
|
||||
}
|
||||
|
||||
for ($i = 0; $i -lt $read; $i++) {
|
||||
[void]$chunks.Add([byte]$buffer[$i])
|
||||
}
|
||||
|
||||
if ($chunks.Count -gt 1048576) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [System.Text.Encoding]::UTF8.GetString([byte[]]$chunks.ToArray([byte])).Trim()
|
||||
} finally {
|
||||
if ($client -ne $null) {
|
||||
$client.Close()
|
||||
}
|
||||
}
|
||||
throw "physical_control_disabled_stage_a"
|
||||
}
|
||||
|
||||
function Get-MinerOpsApi3Credential {
|
||||
@@ -894,61 +840,7 @@ function Invoke-MinerOpsApi3Payload {
|
||||
|
||||
[int]$TimeoutMs = 10000
|
||||
)
|
||||
|
||||
$client = $null
|
||||
$started = Get-Date
|
||||
|
||||
try {
|
||||
$client = New-Object System.Net.Sockets.TcpClient
|
||||
|
||||
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
|
||||
|
||||
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
|
||||
throw "API3 connect timeout to $IpAddress`:$Port"
|
||||
}
|
||||
|
||||
$client.EndConnect($async)
|
||||
$client.ReceiveTimeout = $TimeoutMs
|
||||
$client.SendTimeout = $TimeoutMs
|
||||
|
||||
$json = $Payload | ConvertTo-Json -Depth 20 -Compress
|
||||
$requestBytes = [System.Text.Encoding]::UTF8.GetBytes($json)
|
||||
$lengthBytes = [System.BitConverter]::GetBytes([UInt32]$requestBytes.Length)
|
||||
|
||||
$stream = $client.GetStream()
|
||||
$stream.Write($lengthBytes, 0, $lengthBytes.Length)
|
||||
$stream.Write($requestBytes, 0, $requestBytes.Length)
|
||||
|
||||
$responseLengthBytes = Read-MinerOpsExactBytes -Stream $stream -Length 4 -TimeoutMs $TimeoutMs
|
||||
$responseLength = [System.BitConverter]::ToUInt32($responseLengthBytes, 0)
|
||||
|
||||
if ($responseLength -le 0 -or $responseLength -gt 1048576) {
|
||||
throw "API3 response length looked invalid: $responseLength"
|
||||
}
|
||||
|
||||
$responseBytes = Read-MinerOpsExactBytes -Stream $stream -Length ([int]$responseLength) -TimeoutMs $TimeoutMs
|
||||
$responseText = [System.Text.Encoding]::UTF8.GetString($responseBytes)
|
||||
$responseJson = $responseText | ConvertFrom-Json
|
||||
|
||||
return @{
|
||||
sent = $true
|
||||
protocol = "whatsminer_api3"
|
||||
target_ip = $IpAddress
|
||||
target_port = $Port
|
||||
duration_seconds = [math]::Round(((Get-Date) - $started).TotalSeconds, 3)
|
||||
request = @{
|
||||
cmd = $Payload.cmd
|
||||
param = $Payload.param
|
||||
account = $Payload.account
|
||||
has_token = [bool]$Payload.token
|
||||
}
|
||||
response = $responseJson
|
||||
}
|
||||
} finally {
|
||||
if ($client -ne $null) {
|
||||
$client.Close()
|
||||
}
|
||||
}
|
||||
throw "physical_control_disabled_stage_a"
|
||||
}
|
||||
|
||||
function Get-MinerOpsApi3Salt {
|
||||
@@ -989,43 +881,7 @@ function Invoke-MinerOpsApi3Service {
|
||||
[ValidateSet("start", "stop", "restart")]
|
||||
[string]$ServiceAction
|
||||
)
|
||||
|
||||
$account = Get-MinerOpsApi3Credential -Name "api3_account" -DefaultValue ""
|
||||
$password = Get-MinerOpsApi3Credential -Name "api3_password" -DefaultValue ""
|
||||
$portRaw = Get-MinerOpsApi3Credential -Name "api3_port" -DefaultValue "4433"
|
||||
|
||||
try {
|
||||
$port = [int]$portRaw
|
||||
} catch {
|
||||
$port = 4433
|
||||
}
|
||||
|
||||
$command = "set.miner.service"
|
||||
$saltInfo = Get-MinerOpsApi3Salt -IpAddress $IpAddress -Port $port
|
||||
$salt = [string]$saltInfo.salt
|
||||
$timestamp = [int64][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
$token = Get-MinerOpsApi3Token -Command $command -Password $password -Salt $salt -Timestamp $timestamp
|
||||
|
||||
$payload = @{
|
||||
cmd = $command
|
||||
param = $ServiceAction
|
||||
ts = $timestamp
|
||||
token = $token
|
||||
account = $account
|
||||
}
|
||||
|
||||
$result = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress -Port $port -Payload $payload
|
||||
|
||||
$result["salt_response"] = $saltInfo.salt_response
|
||||
$result["api3_command"] = $command
|
||||
$result["api3_param"] = $ServiceAction
|
||||
$result["api3_account"] = $account
|
||||
|
||||
if ([int]$result.response.code -ne 0) {
|
||||
throw "API3 $command $ServiceAction failed: $($result.response | ConvertTo-Json -Compress)"
|
||||
}
|
||||
|
||||
return $result
|
||||
throw "physical_control_disabled_stage_a"
|
||||
}
|
||||
|
||||
|
||||
@@ -1054,45 +910,7 @@ function Invoke-MinerOpsApi3AuthenticatedCommand {
|
||||
|
||||
[object]$Param = $null
|
||||
)
|
||||
|
||||
$account = Get-MinerOpsApi3Credential -Name "api3_account" -DefaultValue ""
|
||||
$password = Get-MinerOpsApi3Credential -Name "api3_password" -DefaultValue ""
|
||||
$portRaw = Get-MinerOpsApi3Credential -Name "api3_port" -DefaultValue "4433"
|
||||
|
||||
try {
|
||||
$port = [int]$portRaw
|
||||
} catch {
|
||||
$port = 4433
|
||||
}
|
||||
|
||||
$saltInfo = Get-MinerOpsApi3Salt -IpAddress $IpAddress -Port $port
|
||||
$salt = [string]$saltInfo.salt
|
||||
$timestamp = [int64][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
$token = Get-MinerOpsApi3Token -Command $Command -Password $password -Salt $salt -Timestamp $timestamp
|
||||
|
||||
$payload = @{
|
||||
cmd = $Command
|
||||
ts = $timestamp
|
||||
token = $token
|
||||
account = $account
|
||||
}
|
||||
|
||||
if ($null -ne $Param) {
|
||||
$payload["param"] = $Param
|
||||
}
|
||||
|
||||
$result = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress -Port $port -Payload $payload
|
||||
|
||||
$result["salt_response"] = $saltInfo.salt_response
|
||||
$result["api3_command"] = $Command
|
||||
$result["api3_param"] = $Param
|
||||
$result["api3_account"] = $account
|
||||
|
||||
if ([int]$result.response.code -ne 0) {
|
||||
throw "API3 $Command failed: $($result.response | ConvertTo-Json -Compress)"
|
||||
}
|
||||
|
||||
return $result
|
||||
throw "physical_control_disabled_stage_a"
|
||||
}
|
||||
|
||||
function Get-MinerOpsPayloadValue {
|
||||
@@ -1129,113 +947,14 @@ function Invoke-MinerOpsLegacy4028Command {
|
||||
[int] $Port = 4028,
|
||||
[int] $TimeoutMs = 7000
|
||||
)
|
||||
|
||||
$normalizedIp = Normalize-MinerOpsIpAddress -IpAddress $IpAddress
|
||||
|
||||
$client = New-Object System.Net.Sockets.TcpClient
|
||||
$connectResult = $client.BeginConnect($normalizedIp, $Port, $null, $null)
|
||||
|
||||
if (-not $connectResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
|
||||
try { $client.Close() } catch {}
|
||||
throw "Legacy 4028 connection timed out to $normalizedIp`:$Port"
|
||||
}
|
||||
|
||||
$client.EndConnect($connectResult)
|
||||
|
||||
try {
|
||||
$stream = $client.GetStream()
|
||||
$stream.ReadTimeout = $TimeoutMs
|
||||
$stream.WriteTimeout = $TimeoutMs
|
||||
|
||||
$bytes = [System.Text.Encoding]::ASCII.GetBytes($CommandJson)
|
||||
$stream.Write($bytes, 0, $bytes.Length)
|
||||
$stream.Flush()
|
||||
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
$buffer = New-Object byte[] 65535
|
||||
$builder = New-Object System.Text.StringBuilder
|
||||
|
||||
do {
|
||||
$read = $stream.Read($buffer, 0, $buffer.Length)
|
||||
|
||||
if ($read -gt 0) {
|
||||
[void]$builder.Append([System.Text.Encoding]::ASCII.GetString($buffer, 0, $read))
|
||||
}
|
||||
} while ($stream.DataAvailable)
|
||||
|
||||
$rawResponse = $builder.ToString().Trim([char]0).Trim()
|
||||
|
||||
$parsedResponse = $null
|
||||
|
||||
try {
|
||||
if ($rawResponse) {
|
||||
$parsedResponse = $rawResponse | ConvertFrom-Json
|
||||
}
|
||||
} catch {
|
||||
$parsedResponse = $null
|
||||
}
|
||||
|
||||
return @{
|
||||
success = $true
|
||||
control_method = "legacy_4028"
|
||||
port = $Port
|
||||
request_json = $CommandJson
|
||||
raw_response = $rawResponse
|
||||
parsed_response = $parsedResponse
|
||||
}
|
||||
}
|
||||
finally {
|
||||
try { $stream.Close() } catch {}
|
||||
try { $client.Close() } catch {}
|
||||
}
|
||||
throw "physical_control_disabled_stage_a"
|
||||
}
|
||||
|
||||
function Invoke-MinerOpsM30Legacy4028Test {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)] $Payload
|
||||
)
|
||||
|
||||
if ($Payload -is [string]) {
|
||||
$Payload = $Payload | ConvertFrom-Json
|
||||
}
|
||||
|
||||
$ipAddress = Get-MinerOpsPayloadValue -Payload $Payload -Name "ip_address"
|
||||
|
||||
if (-not $ipAddress) {
|
||||
$ipAddress = Get-MinerOpsPayloadValue -Payload $Payload -Name "current_ip"
|
||||
}
|
||||
|
||||
if (-not $ipAddress) {
|
||||
throw "Legacy 4028 test failed: no IP address in payload."
|
||||
}
|
||||
|
||||
$legacyJson = Get-MinerOpsPayloadValue -Payload $Payload -Name "legacy_4028_json"
|
||||
$legacyCommand = Get-MinerOpsPayloadValue -Payload $Payload -Name "legacy_4028_command"
|
||||
|
||||
if (-not $legacyJson) {
|
||||
if (-not $legacyCommand) {
|
||||
throw "Legacy 4028 test failed: no legacy command in payload."
|
||||
}
|
||||
|
||||
$legacyJson = '{"command":"' + $legacyCommand + '"}'
|
||||
}
|
||||
|
||||
Write-AgentLog "Running legacy 4028 M30 test against $ipAddress command=$legacyCommand"
|
||||
|
||||
$result = Invoke-MinerOpsLegacy4028Command -IpAddress $ipAddress -CommandJson $legacyJson -Port 4028
|
||||
|
||||
return @{
|
||||
success = $true
|
||||
action = "legacy_4028"
|
||||
test_lab = "m30"
|
||||
ip_address = $ipAddress
|
||||
legacy_4028_command = $legacyCommand
|
||||
legacy_4028_request = $legacyJson
|
||||
legacy_4028_response = $result.raw_response
|
||||
legacy_4028_parsed = $result.parsed_response
|
||||
control_method = "legacy_4028"
|
||||
}
|
||||
throw "physical_control_disabled_stage_a"
|
||||
}
|
||||
|
||||
function Run-MinerControlCommand {
|
||||
@@ -1244,105 +963,7 @@ function Run-MinerControlCommand {
|
||||
[Parameter(Mandatory=$true)]
|
||||
[object]$Payload
|
||||
)
|
||||
# MinerOps M30 legacy 4028 test path
|
||||
if ($Payload -is [string]) {
|
||||
try {
|
||||
$Payload = $Payload | ConvertFrom-Json
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
if ($Payload -and (($Payload.control_method -eq "legacy_4028") -or ($Payload.action -eq "legacy_4028"))) {
|
||||
return Invoke-MinerOpsM30Legacy4028Test -Payload $Payload
|
||||
}
|
||||
|
||||
|
||||
if ($Payload -is [string]) {
|
||||
$Payload = $Payload | ConvertFrom-Json
|
||||
}
|
||||
|
||||
if ($null -eq $Payload) {
|
||||
throw "Miner control payload was empty."
|
||||
}
|
||||
|
||||
$action = ([string]$Payload.action).ToLower()
|
||||
$minerId = [string]$Payload.miner_id
|
||||
$macAddress = [string]$Payload.mac_address
|
||||
$dryRun = [bool]$Payload.dry_run
|
||||
|
||||
$ipAddress = ([string]$Payload.ip_address).Trim()
|
||||
|
||||
if ($ipAddress.Contains("/")) {
|
||||
$ipAddress = $ipAddress.Split("/")[0]
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ipAddress)) {
|
||||
throw "Miner control payload did not include ip_address."
|
||||
}
|
||||
|
||||
$serviceAction = ""
|
||||
$api3Command = ""
|
||||
$api3Param = $null
|
||||
$performanceMode = ""
|
||||
|
||||
if ($action -eq "reboot") {
|
||||
$serviceAction = "restart"
|
||||
$api3Command = "set.miner.service"
|
||||
} elseif ($action -eq "suspend") {
|
||||
$serviceAction = "stop"
|
||||
$api3Command = "set.miner.service"
|
||||
} elseif ($action -eq "resume") {
|
||||
$serviceAction = "start"
|
||||
$api3Command = "set.miner.service"
|
||||
} elseif ($action -eq "performance_mode") {
|
||||
$performanceMode = ([string]$Payload.performance_mode).ToLower()
|
||||
|
||||
if ($performanceMode -in @("low", "normal", "high")) {
|
||||
$api3Command = "set.miner.power_mode"
|
||||
$api3Param = $performanceMode
|
||||
} else {
|
||||
throw "Invalid performance_mode: $performanceMode"
|
||||
}
|
||||
} else {
|
||||
throw "Unsupported miner control action: $action"
|
||||
}
|
||||
|
||||
Write-AgentLog "Miner control requested. Action=$action API3Command=$api3Command API3Service=$serviceAction PerformanceMode=$performanceMode IP=$ipAddress MAC=$macAddress DryRun=$dryRun"
|
||||
|
||||
if ($dryRun) {
|
||||
return @{
|
||||
ok = $true
|
||||
dry_run = $true
|
||||
action = $action
|
||||
api3_command = $api3Command
|
||||
api3_service_action = $serviceAction
|
||||
performance_mode = $performanceMode
|
||||
ip_address = $ipAddress
|
||||
miner_id = $minerId
|
||||
mac_address = $macAddress
|
||||
message = "Dry run only. No miner command sent."
|
||||
}
|
||||
}
|
||||
|
||||
if ($action -eq "performance_mode") {
|
||||
$api3Result = Invoke-MinerOpsApi3AuthenticatedCommand -IpAddress $ipAddress -Command $api3Command -Param $api3Param
|
||||
} else {
|
||||
$api3Result = Invoke-MinerOpsApi3Service -IpAddress $ipAddress -ServiceAction $serviceAction
|
||||
}
|
||||
|
||||
return @{
|
||||
ok = $true
|
||||
dry_run = $false
|
||||
action = $action
|
||||
api3_command = $api3Command
|
||||
api3_service_action = $serviceAction
|
||||
performance_mode = $performanceMode
|
||||
ip_address = $ipAddress
|
||||
miner_id = $minerId
|
||||
mac_address = $macAddress
|
||||
message = "API3 miner control command sent by Windows Agent."
|
||||
api3_result = $api3Result
|
||||
}
|
||||
throw "physical_control_disabled_stage_a"
|
||||
}
|
||||
|
||||
|
||||
@@ -1368,8 +989,8 @@ function Process-PendingCommands {
|
||||
Write-AgentLog "Command completed: $commandId"
|
||||
} elseif ($commandType -eq "miner_control") {
|
||||
$result = Run-MinerControlCommand -Payload $cmd.request_payload
|
||||
Send-AgentCommandResult -CommandId $commandId -Status "completed" -ResultPayload $result | Out-Null
|
||||
Write-AgentLog "Miner control command completed: $commandId"
|
||||
Send-AgentCommandResult -CommandId $commandId -Status "failed" -ResultPayload $result | Out-Null
|
||||
Write-AgentLog "Miner control rejected by Stage A containment: $commandId"
|
||||
|
||||
} elseif ($commandType -eq "enrich_match_discovery") {
|
||||
$payloadObj = $cmd.request_payload
|
||||
|
||||
Reference in new issue
Block a user