ci/woodpecker/push/main-amd64-build Pipeline was successful
The hash a node reports about itself cannot be trusted (a swapped binary can copy the old hash), so the runtime binding only caught files that were replaced by something that did not lie. Provenance stays where it can be verified: the signed release manifest (digest) and the signed SHA256SUMS. - Remove BinaryBindingService, the node_bindings table (schema v15 drops it), BinaryPolicy, the handshake hash/manifest fields and the admin binary column. - Drop the per-platform "binaries" list from the release manifest, the hash scripts and the updater's installed.manifest copy. - ReleaseService.PublishAsync now verifies the release signature, so the admin POST /releases route is no longer an unsigned path; tests sign with a test key. - Docs: section 16 explains why the runtime check is gone.
101 lines
4.1 KiB
C#
101 lines
4.1 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Fleet.Core;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Measurement;
|
|
|
|
/// <summary>
|
|
/// 节点进程和宿主机更新器之间的文件交换(容器里 <c>/etc/moshiro</c>,也就是数据目录):
|
|
/// 节点把面板发来的清单和签名写进 <c>update/</c>,把「更新」请求写成 <c>update/apply.request</c>;更新器验签并更新,状态由宿主更新器写在只读挂载的 <c>update-status/status.json</c>。
|
|
/// 节点进程不执行任何命令,也不信任清单的内容(验签在宿主机上)。没有安装更新器时目录写不了或没人读,调用只是返回 false。
|
|
/// </summary>
|
|
public sealed class UpdateBridge
|
|
{
|
|
private const int MaxManifestBytes = 4096;
|
|
private const int MaxSignatureBytes = 512;
|
|
private const int MaxStatusBytes = 4096;
|
|
|
|
private readonly string _updateDirectory;
|
|
private readonly string _statusFile;
|
|
private readonly Action<string, byte[]> _write;
|
|
private readonly INodeLog _log;
|
|
|
|
/// <param name="write">写数据目录里的文件(相对路径,原子替换):由节点传入 <c>ConfigStore.Write</c>——常驻进程里只有它可以写文件。</param>
|
|
public UpdateBridge(string dataDirectory, Action<string, byte[]> write, INodeLog log)
|
|
{
|
|
_updateDirectory = Path.Combine(dataDirectory, "update");
|
|
_statusFile = Path.Combine(dataDirectory, "update-status", "status.json");
|
|
_write = write;
|
|
_log = log;
|
|
}
|
|
|
|
/// <summary>放下面板发来的清单和签名(和已有的一样就不动,免得每次重连都惊动更新器)。先写清单、后写签名:更新器监视的是签名文件。</summary>
|
|
public bool WriteOffer(UpdateOfferParams offer)
|
|
{
|
|
byte[] manifest = Encoding.UTF8.GetBytes(offer.Manifest);
|
|
byte[] signature = Encoding.UTF8.GetBytes(offer.Signature);
|
|
if (manifest.Length is 0 or > MaxManifestBytes || signature.Length is 0 or > MaxSignatureBytes)
|
|
{
|
|
_log.Warn("ignoring an update offer of an unexpected size");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
string manifestPath = Path.Combine(_updateDirectory, "offer.manifest");
|
|
string signaturePath = Path.Combine(_updateDirectory, "offer.sig");
|
|
if (Same(manifestPath, manifest) && Same(signaturePath, signature))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_write("update/offer.manifest", manifest);
|
|
_write("update/offer.sig", signature);
|
|
_log.Info("update offer received");
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
_log.Warn("could not store the update offer (is the updater installed?)", ("err", ex.Message));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>请求更新:写一个空的 <c>apply.request</c>(更新器验签通过、清单更新才会真的动手)。</summary>
|
|
public bool RequestApply()
|
|
{
|
|
try
|
|
{
|
|
_write("update/apply.request", []);
|
|
_log.Info("update requested");
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
_log.Warn("could not request the update (is the updater installed?)", ("err", ex.Message));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>更新器的状态;没有安装更新器、文件不存在或内容不对时返回 null。</summary>
|
|
public UpdateStatus? ReadStatus()
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(_statusFile) || new FileInfo(_statusFile).Length > MaxStatusBytes)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return JsonSerializer.Deserialize(File.ReadAllBytes(_statusFile), MoShiroJsonContext.Default.UpdateStatus);
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static bool Same(string path, byte[] content) => File.Exists(path) && File.ReadAllBytes(path).AsSpan().SequenceEqual(content);
|
|
}
|