253 lines
9.6 KiB
C#
253 lines
9.6 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
using Fleet.Core;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Measurement;
|
|
|
|
/// <summary>
|
|
/// 数据面会话:<c>session_id</c> 和密钥(由点对点握手里的 ECDH 派生)。两端的数据面包都用它认证。
|
|
/// Agent 的会话还带重放窗口等状态,所以是可以继承的。
|
|
/// </summary>
|
|
public class DataSession
|
|
{
|
|
public DataSession(long sessionId, byte[] key)
|
|
{
|
|
SessionId = sessionId;
|
|
Key = key;
|
|
}
|
|
|
|
public long SessionId { get; }
|
|
|
|
public byte[] Key { get; }
|
|
|
|
/// <summary>对端最近一次通过认证的 UDP 源地址(监听方把单向数据发往这里;拨号方不需要)。</summary>
|
|
public virtual IPEndPoint? UdpEndpoint => null;
|
|
|
|
/// <summary>TCP 首帧的随机数只能用一次。拨号方不接受连接,所以默认放行。</summary>
|
|
public virtual bool UseNonce(ReadOnlySpan<byte> nonce) => true;
|
|
}
|
|
|
|
public interface IDataSessions
|
|
{
|
|
DataSession? Find(long sessionId);
|
|
}
|
|
|
|
/// <summary>只有一个会话(拨号方:Probe 与某个 Agent 的会话)。</summary>
|
|
public sealed class SingleSession : IDataSessions
|
|
{
|
|
private readonly DataSession _session;
|
|
|
|
public SingleSession(DataSession session)
|
|
{
|
|
_session = session;
|
|
}
|
|
|
|
public DataSession? Find(long sessionId) => sessionId == _session.SessionId ? _session : null;
|
|
}
|
|
|
|
public enum Admission
|
|
{
|
|
Accepted,
|
|
Invalid,
|
|
RateLimited,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 监听方的准入策略(安全边界):重放窗口、令牌桶限速、源地址学习。积木本身不含任何策略;
|
|
/// 拨号方不需要,只做 MAC 校验。任何一步失败都是静默丢弃并计数,不回复。
|
|
/// </summary>
|
|
public interface IAdmission
|
|
{
|
|
/// <summary>UDP 包:<paramref name="source"/> 是认证通过的源地址(实现可以据此学习 NAT 重绑定)。</summary>
|
|
Admission AdmitPacket(DataSession session, in UdpHeader header, SocketAddress source, long nowUs);
|
|
|
|
/// <summary>TCP 回显的一条消息(只限速)。</summary>
|
|
bool AdmitMessage(DataSession session, uint streamId, long nowUs);
|
|
}
|
|
|
|
/// <summary>数据面计数(只有计数,没有主机信息):用于心跳和面板上的节点状态。</summary>
|
|
public sealed class DataPlaneCounters
|
|
{
|
|
private long _packetsIn;
|
|
private long _packetsOut;
|
|
private long _invalid;
|
|
private long _rateLimited;
|
|
|
|
public long PacketsIn => Interlocked.Read(ref _packetsIn);
|
|
|
|
public long PacketsOut => Interlocked.Read(ref _packetsOut);
|
|
|
|
public long DroppedInvalid => Interlocked.Read(ref _invalid);
|
|
|
|
public long DroppedRateLimited => Interlocked.Read(ref _rateLimited);
|
|
|
|
public void PacketIn() => Interlocked.Increment(ref _packetsIn);
|
|
|
|
public void PacketOut() => Interlocked.Increment(ref _packetsOut);
|
|
|
|
public void DropInvalid() => Interlocked.Increment(ref _invalid);
|
|
|
|
public void DropRateLimited() => Interlocked.Increment(ref _rateLimited);
|
|
}
|
|
|
|
/// <summary>一个列块属于线路任务还是按需测试的某个阶段。</summary>
|
|
public sealed record FragmentScope(TaskId? Task, TestId? Test, int? Phase);
|
|
|
|
/// <summary>
|
|
/// 积木交出一侧的行的地方。Probe:放进发给 Agent 的发件箱(列块);Agent 自己运行的积木(目标任务):直接盖章上传。
|
|
/// </summary>
|
|
public interface IFragmentSink
|
|
{
|
|
void Ship(string kind, FragmentScope scope, IReadOnlyList<RawStream> streams, IReadOnlyDictionary<string, JsonElement>? parameters, long startUtcMs, int durationMs, int port);
|
|
}
|
|
|
|
public enum FlushMode
|
|
{
|
|
/// <summary>只交出已经稳定的行(回显类等过宽限期的)。</summary>
|
|
Settled,
|
|
|
|
/// <summary>交出全部(阶段结束或角色停止)。</summary>
|
|
All,
|
|
}
|
|
|
|
/// <summary>积木无法继续(例如监听方等不到对端的地址):由编排者决定整个测试怎么结束。</summary>
|
|
public sealed class BlockFaultException : Exception
|
|
{
|
|
public BlockFaultException(string message)
|
|
: base(message)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 一个运行中的积木角色。同一种采集方法的两端是一对角色(拨号方 = Probe,监听方 = Agent),写在同一个文件里,
|
|
/// 共用包的构造、校验、行的记录;它们只产生<b>原始列</b>,不做任何统计,也不知道上层的方案或协议。
|
|
/// </summary>
|
|
public interface IBlock
|
|
{
|
|
/// <summary>运行到取消(回显、保持)或到自己结束(单向数据流的发送端)。</summary>
|
|
Task RunAsync(CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// 拨号方把已完成的行切成列块交给 <see cref="IFragmentSink"/>,返回块数;监听方返回 0(它的行留在 <see cref="Records"/> 里等合并)。
|
|
/// <paramref name="peerProduced"/>:对端是行的主导方时,对端产生的行数(单向数据流的发送包数)。
|
|
/// </summary>
|
|
int Flush(FlushMode mode, long peerProduced = 0);
|
|
|
|
/// <summary>这一侧作为主导方产生的行数(发出的包数)。</summary>
|
|
long Produced { get; }
|
|
|
|
/// <summary>这一侧为合并保留的按键记录;没有则为 null。</summary>
|
|
RowStore? Records(string streamName);
|
|
}
|
|
|
|
/// <summary>运行一个积木所需的全部参数:Kind、已经校验并补全默认值的参数、流编号、端口、列块归属。</summary>
|
|
public sealed class BlockSpec
|
|
{
|
|
public BlockSpec(KindSpec kind, ParamValues parameters, uint streamId, int port, FragmentScope scope)
|
|
{
|
|
Kind = kind;
|
|
Params = parameters;
|
|
StreamId = streamId;
|
|
Port = port;
|
|
Scope = scope;
|
|
}
|
|
|
|
public KindSpec Kind { get; }
|
|
|
|
public ParamValues Params { get; }
|
|
|
|
public uint StreamId { get; }
|
|
|
|
public int Port { get; }
|
|
|
|
public FragmentScope Scope { get; }
|
|
|
|
public string KindId => Kind.Id;
|
|
|
|
public long Int(string name) => Params.GetInt(name);
|
|
|
|
public bool Bool(string name) => Params.GetBool(name);
|
|
|
|
/// <summary>参数或端口变化时调和器重启这个积木(不含流编号:Agent 的流编号每次重建都会重新分配)。</summary>
|
|
public string ConfigSignature => KindId + "|" + Port + "|" +
|
|
string.Join(',', Params.Values.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Key + "=" + p.Value.GetRawText()));
|
|
|
|
/// <summary>参数、端口或流编号变化时重启:Probe 据此判断 Agent 下发的任务有没有变(Agent 重启后流编号会变)。</summary>
|
|
public string Signature => StreamId + "|" + ConfigSignature;
|
|
|
|
/// <summary>校验参数(补全默认值)后构造;Kind 不存在或参数非法返回 null。</summary>
|
|
public static BlockSpec? Create(string kindId, IReadOnlyDictionary<string, JsonElement> parameters, uint streamId, int port, FragmentScope scope)
|
|
{
|
|
KindSpec? kind = Kinds.Find(kindId);
|
|
if (kind is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
ParamResolution resolved = ParamResolver.Resolve(kind.Params, parameters);
|
|
return resolved.IsValid ? new BlockSpec(kind, resolved.Values!, streamId, port, scope) : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>积木运行的环境:时钟、套接字、对端地址、列块出口。拨号方和监听方各自组装自己用得到的部分。</summary>
|
|
public sealed class BlockEnv
|
|
{
|
|
public required MonoClock Clock { get; init; }
|
|
|
|
public required INodeLog Log { get; init; }
|
|
|
|
/// <summary>当前的数据面会话(拨号方固定;监听方是 Agent 当前的 Probe 会话,可能没有)。</summary>
|
|
public required Func<DataSession?> Session { get; init; }
|
|
|
|
public UdpMux? Udp { get; init; }
|
|
|
|
/// <summary>监听方的准入策略(限速等);拨号方为 null。</summary>
|
|
public IAdmission? Admission { get; init; }
|
|
|
|
public DataPlaneCounters Counters { get; init; } = new();
|
|
|
|
/// <summary>监听方:接受 TCP 数据连接。</summary>
|
|
public TcpAcceptor? TcpListen { get; init; }
|
|
|
|
/// <summary>拨号方:对端(Agent,或目标任务的目标地址)。</summary>
|
|
public IPAddress? PeerAddress { get; init; }
|
|
|
|
/// <summary>拨号方:对端 TLS 证书的 SPKI 指纹;null 表示不校验证书(目标任务,目标是任意地址)。</summary>
|
|
public byte[]? PeerSpkiSha256 { get; init; }
|
|
|
|
/// <summary>拨号方:和对端(Agent)的配对敲门密钥;每条 TCP 连接建立后先发敲门包。null 表示对端不要求敲门(目标任务的任意地址)。</summary>
|
|
public byte[]? KnockKey { get; init; }
|
|
|
|
/// <summary>拨号方:列块出口。</summary>
|
|
public IFragmentSink? Fragments { get; init; }
|
|
|
|
/// <summary>本地硬上限:批量传输(tcp_bulk)发送端的码率(Mbit/s)。批量传输会尽可能占满链路,这是防止把小机器打满的保险。</summary>
|
|
public double MaxBulkMbps { get; init; } = 100;
|
|
|
|
/// <summary>本地硬上限:单向数据流接收缓冲的包速率上界。</summary>
|
|
public long MaxPacketsPerSecond { get; init; } = 20_000;
|
|
|
|
public TimeProvider Time => Clock.Time;
|
|
|
|
/// <summary>同一套环境,但绑定到一个固定的数据面会话(Agent 为每个 Probe 会话各起一组监听方积木)。</summary>
|
|
public BlockEnv WithSession(DataSession session) =>
|
|
new()
|
|
{
|
|
Clock = Clock,
|
|
Log = Log,
|
|
Session = () => session,
|
|
Udp = Udp,
|
|
Admission = Admission,
|
|
Counters = Counters,
|
|
TcpListen = TcpListen,
|
|
PeerAddress = PeerAddress,
|
|
PeerSpkiSha256 = PeerSpkiSha256,
|
|
KnockKey = KnockKey,
|
|
Fragments = Fragments,
|
|
MaxBulkMbps = MaxBulkMbps,
|
|
MaxPacketsPerSecond = MaxPacketsPerSecond,
|
|
};
|
|
}
|