agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
1 parent
d5ebf29e57
commit
ad5efc62ff
206 files changed
+4202
-4964
No files matched your search
@@ -34,19 +34,19 @@ public sealed class AgentCounters
|
||||
public void FragmentDiscarded() => Interlocked.Increment(ref _fragmentsDiscarded);
|
||||
}
|
||||
|
||||
public sealed record PeerContext(ProbeSession Session, MonoClock Clock);
|
||||
public sealed record PeerContext(DialerSession Session, MonoClock Clock);
|
||||
|
||||
/// <summary>
|
||||
/// 一个已经认证的 Probe 会话:数据面的 <c>session_id</c> 和密钥(由点对点握手里的 ECDH 派生)、最近一次的 UDP 源地址、
|
||||
/// 一个已经认证的拨号方会话:数据面的 <c>session_id</c> 和密钥(由点对点握手里的 ECDH 派生)、最近一次的 UDP 源地址、
|
||||
/// 每个流的重放窗口。数据面的包不绑定五元组,NAT 映射变化之后自动跟随新的源地址。
|
||||
/// </summary>
|
||||
public sealed class ProbeSession : DataSession
|
||||
public sealed class DialerSession : DataSession
|
||||
{
|
||||
private readonly ConcurrentDictionary<uint, ReplayWindow> _windows = new();
|
||||
private readonly ConcurrentDictionary<ulong, byte> _nonces = new();
|
||||
private SocketAddress? _datagramSource;
|
||||
|
||||
public ProbeSession(PeerSessionInfo info)
|
||||
public DialerSession(PeerSessionInfo info)
|
||||
: base(info.Credentials.SessionIdValue, info.Credentials.Key)
|
||||
{
|
||||
Info = info;
|
||||
@@ -54,20 +54,20 @@ public sealed class ProbeSession : DataSession
|
||||
|
||||
public PeerSessionInfo Info { get; }
|
||||
|
||||
public string ProbeId => Info.PeerId;
|
||||
public string DialerId => Info.PeerId;
|
||||
|
||||
public override string DiagnosticPeerId => ProbeId;
|
||||
public override string DiagnosticPeerId => DialerId;
|
||||
|
||||
public string? CloseReason { get; set; }
|
||||
|
||||
public string IpVersion => Info.Remote is IPEndPoint endpoint && endpoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !endpoint.Address.IsIPv4MappedToIPv6 ? IpVersions.V6 : IpVersions.V4;
|
||||
|
||||
public string PathKey => ProbeId + "~" + IpVersion;
|
||||
public string PathKey => DialerId + "~" + IpVersion;
|
||||
|
||||
/// <summary>Probe 的程序版本(握手里自报,只用于展示)。</summary>
|
||||
/// <summary>拨号方 的程序版本(握手里自报,只用于展示)。</summary>
|
||||
public string? Version => Info.Version;
|
||||
|
||||
/// <summary>Probe 的远端地址(经过 NAT 之后的公网 IP)。</summary>
|
||||
/// <summary>拨号方 的远端地址(经过 NAT 之后的公网 IP)。</summary>
|
||||
public string? RemoteAddress => (Info.Remote as IPEndPoint)?.Address.ToString();
|
||||
|
||||
public long ConnectedAtUtcMs { get; init; }
|
||||
@@ -133,59 +133,59 @@ public sealed class ProbeSession : DataSession
|
||||
public void ForgetStream(uint streamId) => _windows.TryRemove(streamId, out _);
|
||||
}
|
||||
|
||||
/// <summary>每个 Probe 的 IPv4、IPv6 会话独立;重连只替换同地址族的旧会话。</summary>
|
||||
/// <summary>每个拨号方的 IPv4、IPv6 会话独立;重连只替换同地址族的旧会话。</summary>
|
||||
public sealed class SessionRegistry : IDataSessions
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<string, ProbeSession> _byProbe = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<long, ProbeSession> _bySession = [];
|
||||
private readonly Dictionary<string, DialerSession> _byDialer = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<long, DialerSession> _bySession = [];
|
||||
|
||||
public IReadOnlyList<ProbeSession> All
|
||||
public IReadOnlyList<DialerSession> All
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return [.. _byProbe.Values];
|
||||
return [.. _byDialer.Values];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ProbeSession? Get(string probeId, string ipVersion = IpVersions.V4)
|
||||
public DialerSession? Get(string dialerId, string ipVersion = IpVersions.V4)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _byProbe.GetValueOrDefault(probeId + "~" + ipVersion);
|
||||
return _byDialer.GetValueOrDefault(dialerId + "~" + ipVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>登记新会话;返回同一个 Probe 的旧会话(如果有)。</summary>
|
||||
public ProbeSession? Replace(ProbeSession session)
|
||||
/// <summary>登记新会话;返回同一个拨号方的旧会话(如果有)。</summary>
|
||||
public DialerSession? Replace(DialerSession session)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_byProbe.TryGetValue(session.PathKey, out ProbeSession? old);
|
||||
_byDialer.TryGetValue(session.PathKey, out DialerSession? old);
|
||||
if (old is not null)
|
||||
{
|
||||
_bySession.Remove(old.SessionId);
|
||||
}
|
||||
|
||||
_byProbe[session.PathKey] = session;
|
||||
_byDialer[session.PathKey] = session;
|
||||
_bySession[session.SessionId] = session;
|
||||
return old;
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(ProbeSession session)
|
||||
public void Remove(DialerSession session)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_byProbe.TryGetValue(session.PathKey, out ProbeSession? current) && ReferenceEquals(current, session))
|
||||
if (_byDialer.TryGetValue(session.PathKey, out DialerSession? current) && ReferenceEquals(current, session))
|
||||
{
|
||||
_byProbe.Remove(session.PathKey);
|
||||
_byDialer.Remove(session.PathKey);
|
||||
}
|
||||
|
||||
if (_bySession.TryGetValue(session.SessionId, out ProbeSession? bySession) && ReferenceEquals(bySession, session))
|
||||
if (_bySession.TryGetValue(session.SessionId, out DialerSession? bySession) && ReferenceEquals(bySession, session))
|
||||
{
|
||||
_bySession.Remove(session.SessionId);
|
||||
}
|
||||
@@ -236,8 +236,8 @@ public sealed class StreamInfo
|
||||
|
||||
public int? Phase { get; init; }
|
||||
|
||||
/// <summary>这个流属于哪个 Probe 会话:别的会话的包即使带着这个流编号也不会被接受。</summary>
|
||||
public ProbeSession? Owner { get; init; }
|
||||
/// <summary>这个流属于哪个拨号方会话:别的会话的包即使带着这个流编号也不会被接受。</summary>
|
||||
public DialerSession? Owner { get; init; }
|
||||
|
||||
public int Port { get; }
|
||||
|
||||
@@ -290,7 +290,7 @@ public sealed class StreamRegistry : IRecordSource
|
||||
|
||||
public bool TryGet(uint id, out StreamInfo stream) => _streams.TryGetValue(id, out stream!);
|
||||
|
||||
public StreamInfo? FindByTask(TaskId task, ProbeSession owner) => _streams.Values.FirstOrDefault(s => s.TaskId == task && ReferenceEquals(s.Owner, owner));
|
||||
public StreamInfo? FindByTask(TaskId task, DialerSession owner) => _streams.Values.FirstOrDefault(s => s.TaskId == task && ReferenceEquals(s.Owner, owner));
|
||||
|
||||
public StreamInfo? FindByTest(TestId test, int phase) => _streams.Values.FirstOrDefault(s => s.TestId == test && s.Phase == phase);
|
||||
|
||||
@@ -314,11 +314,11 @@ public sealed class AgentAdmission : IAdmission
|
||||
}
|
||||
|
||||
/// <summary>NAT 映射变化,源地址变了:上报 <c>endpoint_changed</c>。</summary>
|
||||
public Action<ProbeSession>? OnEndpointChanged { get; set; }
|
||||
public Action<DialerSession>? OnEndpointChanged { get; set; }
|
||||
|
||||
public Admission AdmitDatagram(DataSession session, in MeasurementDatagramHeader header, SocketAddress source, long nowUs)
|
||||
{
|
||||
if (session is not ProbeSession probe || !_streams.TryGet(header.StreamId, out StreamInfo? stream) || !ReferenceEquals(stream.Owner, probe) || !probe.AcceptSequence(header.StreamId, header.Seq))
|
||||
if (session is not DialerSession dialer || !_streams.TryGet(header.StreamId, out StreamInfo? stream) || !ReferenceEquals(stream.Owner, dialer) || !dialer.AcceptSequence(header.StreamId, header.Seq))
|
||||
{
|
||||
return Admission.Invalid;
|
||||
}
|
||||
@@ -328,9 +328,9 @@ public sealed class AgentAdmission : IAdmission
|
||||
return Admission.RateLimited;
|
||||
}
|
||||
|
||||
if (probe.UpdateSource(source))
|
||||
if (dialer.UpdateSource(source))
|
||||
{
|
||||
OnEndpointChanged?.Invoke(probe);
|
||||
OnEndpointChanged?.Invoke(dialer);
|
||||
}
|
||||
|
||||
return Admission.Accepted;
|
||||
|
||||
Reference in new issue
Block a user