Files
MoShiro/packaging/install/install.ps1
T
justme a2f88a344b
ci/woodpecker/tag/gitea-release Pipeline is pending
ci/woodpecker/tag/linux-release Pipeline failed
ci/woodpecker/tag/windows-release Pipeline was canceled
feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
Join codes need no passkey touch and carry only public values (genesis
fingerprint, owner credentials, installer hashes). Nodes pin the genesis and
owners at enrollment and only follow passkey-signed operator set versions.
The panel's server key is listed in the set; pinned nodes require its
signature and run the control link under AES-256-GCM. Password sessions carry
no capabilities. The updater adopts a next_key from a signed manifest.

Bump version to 0.2.0. [CI SKIP]
2026-10-08 03:21:44 +08:00

150 lines
8.3 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MoShiro 节点一键安装(Windows,Agent 和 Probe 共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。
# 面板给的命令先下载这份文件、核对 SHA-256(CI 构建时算出,写进签名的发布清单,也公布在发布页),一致才执行。
# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(agent 或 probe)、MOSHIRO_CODE(一次性的加入码)。
# 做的事:用 Docker Desktop 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取计算机名。
# 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)、MOSHIRO_GENESIS / MOSHIRO_CONTROLLERS(命令里带着:信任根指纹和这个节点的属主 passkey 凭证 id,都是公开值)、
# MOSHIRO_ALLOW_UNVERIFIED=1 加 MOSHIRO_IMAGE=镜像(面板还没有已签名的发布清单时,不验签、按这个镜像安装;默认拒绝)。
# 镜像:向面板取 CI 签名的发布清单,用内置的发布公钥验签,再按清单里的 digest 拉取。
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 = $env:MOSHIRO_ROLE
$server = $env:MOSHIRO_SERVER
$code = $env:MOSHIRO_CODE
$image = $env:MOSHIRO_IMAGE
$releaseKey = @'
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfw7EkNcl0FZqOETK+Vt55RMZ9K0q
YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA==
-----END PUBLIC KEY-----
'@
# 这些值来自命令行环境:只允许正常的字符,因为它们会进到 URL 和容器参数里
if ($role -notin @('agent', 'probe')) { Write-Error 'MOSHIRO_ROLE must be agent or probe'; return }
if ($server -notmatch '^https?://[A-Za-z0-9._:/@~%+\[\]-]+$') { Write-Error 'MOSHIRO_SERVER must be the panel address (http:// or https://)'; return }
if ($code -notmatch '^[A-Za-z0-9._~%+-]+$') { Write-Error 'MOSHIRO_CODE is required (copy the whole command from the panel)'; return }
$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' -or -not $image) {
Write-Error "the panel has no signed release for $role yet, so the image cannot be verified. To install anyway set `$env:MOSHIRO_ALLOW_UNVERIFIED='1' and `$env:MOSHIRO_IMAGE"
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")
# 命令里带着信任根指纹和属主(公开值):节点在注册时验证面板的操作员集合并把它们固定在本地
if ($env:MOSHIRO_GENESIS) {
if (($env:MOSHIRO_GENESIS + $env:MOSHIRO_CONTROLLERS) -notmatch '^[A-Za-z0-9_,-]+$') { Write-Error 'MOSHIRO_GENESIS or MOSHIRO_CONTROLLERS has unexpected characters'; return }
$arguments += @('-e', "MOSHIRO_GENESIS=$($env:MOSHIRO_GENESIS)", '-e', "MOSHIRO_CONTROLLERS=$($env:MOSHIRO_CONTROLLERS)")
}
$arguments += $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='<other-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