# MoShiro 节点一键安装(Windows,Agent 和 Probe 共用同一份)。面板在 /install.ps1?code=… 生成这份脚本,把下面四个值填好: # irm 'https://panel/install.ps1?code=jt_…' | iex # 镜像:向面板取 CI 签名的发布清单,用内置的发布公钥验签,再按清单里的 digest 拉取。面板还没有签名的清单时,设 $env:MOSHIRO_ALLOW_UNVERIFIED='1' 才会按标签安装。 # 做的事:用 Docker Desktop 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取计算机名。 # 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)。 function Test-MoShiroSignature([byte[]]$data, [byte[]]$derSignature, [string]$pem) { # 发布公钥是 PEM 的 SPKI(P-256):最后 64 字节是 X、Y;签名是 DER,.NET 要 r||s $b64 = ($pem -split "`n" | Where-Object { $_ -notmatch '^-----' }) -join '' $spki = [Convert]::FromBase64String($b64.Trim()) $xy = $spki[($spki.Length - 64)..($spki.Length - 1)] $blob = [byte[]]@(0x45, 0x43, 0x53, 0x31, 0x20, 0, 0, 0) + $xy $i = 2 if ($derSignature[1] -band 0x80) { $i = 2 + ($derSignature[1] -band 0x7f) } $parts = @() for ($n = 0; $n -lt 2; $n++) { $len = $derSignature[$i + 1] $part = [byte[]]$derSignature[($i + 2)..($i + 1 + $len)] while ($part.Length -gt 32 -and $part[0] -eq 0) { $part = [byte[]]$part[1..($part.Length - 1)] } if ($part.Length -gt 32) { return $false } $parts += , ([byte[]](New-Object byte[] (32 - $part.Length)) + $part) $i += 2 + $len } $key = [Security.Cryptography.CngKey]::Import($blob, [Security.Cryptography.CngKeyBlobFormat]::EccPublicBlob) try { $ecdsa = New-Object Security.Cryptography.ECDsaCng($key) return $ecdsa.VerifyData($data, [byte[]]($parts[0] + $parts[1]), [Security.Cryptography.HashAlgorithmName]::SHA256) } finally { $key.Dispose() } } function Install-MoShiro { $ErrorActionPreference = 'Stop' $role = '@@ROLE@@' $server = '@@SERVER@@' $code = '@@CODE@@' $image = '@@IMAGE@@' $releaseKey = @' @@RELEASE_KEY@@ '@ $name = if ($env:MOSHIRO_NAME) { $env:MOSHIRO_NAME } else { $env:COMPUTERNAME } $name = $name -replace '[^A-Za-z0-9._-]', '-' if ($name.Length -gt 64) { $name = $name.Substring(0, 64) } if (-not $name) { $name = 'node' } if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { Write-Error 'Docker Desktop is required: https://docs.docker.com/desktop/setup/install/windows-install/' return } docker info *> $null if ($LASTEXITCODE -ne 0) { Write-Error 'Docker is installed but not running: start Docker Desktop and run this again' return } $container = "moshiro-$role" $volume = "moshiro-$role-config" docker inspect $container *> $null if ($LASTEXITCODE -eq 0) { docker start $container | Out-Null Write-Host "$container is already installed and running." Write-Host "reinstall: docker rm -f $container; docker volume rm $volume (then run the install command again)" return } # 验发布清单:签名对不上、组件不对、digest 格式不对都直接拒绝,不装 $client = New-Object System.Net.WebClient $manifest = $null try { $manifest = $client.DownloadData("$server/install/manifest?code=$code") $signature = [Convert]::FromBase64String(($client.DownloadString("$server/install/manifest.sig?code=$code")).Trim()) } catch { $manifest = $null } if ($null -eq $manifest) { if ($env:MOSHIRO_ALLOW_UNVERIFIED -ne '1') { Write-Error "the panel has no signed release for $role yet, so the image cannot be verified. Run again with `$env:MOSHIRO_ALLOW_UNVERIFIED='1' to install $image without verification" return } Write-Host "warning: installing $image without signature verification" } else { if (-not (Test-MoShiroSignature $manifest $signature $releaseKey)) { Write-Error 'the release signature does not verify against the built-in release key; refusing to install' return } $release = [Text.Encoding]::UTF8.GetString($manifest) | ConvertFrom-Json if ($release.component -ne $role) { Write-Error "the signed release is for '$($release.component)', not '$role'"; return } if ($release.digest -notmatch '^sha256:[0-9a-f]{64}$') { Write-Error 'the signed release has a malformed digest'; return } if ($release.image -notmatch '^[A-Za-z0-9._/:-]+$') { Write-Error 'the signed release has a malformed image name'; return } $image = "$($release.image)@$($release.digest)" Write-Host "verified release $($release.version) ($($release.tag)): $image" } Write-Host "pulling $image" docker pull $image | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "could not pull $image (private registry? run: docker login $($image.Split('/')[0]))" return } $arguments = @('run', '-d', '--name', $container, '--restart', 'unless-stopped', '--read-only', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', '--cap-drop', 'all', '--security-opt', 'no-new-privileges', '-v', "${volume}:/etc/moshiro") # Docker Desktop 没有宿主网络:Agent 把端口发布出来(面板里的地址需要指向这台机器的公网地址) if ($role -eq 'agent') { $arguments += @('-p', '7443:7443/tcp', '-p', '7443:7443/udp', '-p', '7444:7444/tcp') } $arguments += @('-e', "MOSHIRO_SERVER=$server", '-e', "MOSHIRO_JOIN=$code", '-e', "MOSHIRO_NAME=$name", $image) Write-Host "starting $container as '$name'" docker @arguments | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error 'docker run failed' return } # 等注册完成:第一次启动会用上面的令牌注册,成功之后才算装好 for ($i = 0; $i -lt 30; $i++) { $running = docker inspect -f '{{.State.Running}}' $container 2>$null $logs = (docker logs $container 2>&1) -join "`n" if ($logs -match 'link up') { Write-Host "done: $role '$name' is online. It shows up in the panel now." Write-Host "logs: docker logs -f $container" return } if ($running -ne 'true') { ($logs -split "`n" | Select-Object -Last 8) -join "`n" | Write-Host Write-Error "$container stopped. If the name '$name' is taken, run again with `$env:MOSHIRO_NAME=''. To start over: docker rm -f $container; docker volume rm $volume" return } Start-Sleep -Seconds 1 } Write-Host "$container is running, but has not reported online yet. Check: docker logs $container" } Install-MoShiro