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
@@ -64,12 +64,12 @@ public sealed partial class NodeInfoTests
|
||||
var time = new FakeTimeProvider(DateTimeOffset.Parse("2026-10-06T00:00:00Z"));
|
||||
var service = new NodeInfoService(store, geo, time);
|
||||
|
||||
await service.ObserveAddressAsync("prb_1", NodeKinds.Probe, "10.0.0.5", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("prb_1", NodeKinds.Probe, "127.0.0.1", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "10.0.0.5", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "127.0.0.1", CancellationToken.None);
|
||||
Assert.Empty(geo.Calls);
|
||||
|
||||
await service.ObserveAddressAsync("prb_1", NodeKinds.Probe, "223.73.168.45", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("prb_1", NodeKinds.Probe, "223.73.168.45", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "223.73.168.45", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "223.73.168.45", CancellationToken.None);
|
||||
Assert.Single(geo.Calls);
|
||||
|
||||
NodeInfo info = Assert.Single(await service.ListAsync(CancellationToken.None));
|
||||
@@ -79,10 +79,10 @@ public sealed partial class NodeInfoTests
|
||||
Assert.DoesNotMatch(Ipv4(), info.ToString());
|
||||
|
||||
// 地址变了才重新查;过了一周也重新查
|
||||
await service.ObserveAddressAsync("prb_1", NodeKinds.Probe, "223.5.5.5", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "223.5.5.5", CancellationToken.None);
|
||||
Assert.Equal(2, geo.Calls.Count);
|
||||
time.Advance(TimeSpan.FromDays(8));
|
||||
await service.ObserveAddressAsync("prb_1", NodeKinds.Probe, "223.5.5.5", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "223.5.5.5", CancellationToken.None);
|
||||
Assert.Equal(3, geo.Calls.Count);
|
||||
}
|
||||
|
||||
@@ -93,11 +93,11 @@ public sealed partial class NodeInfoTests
|
||||
var time = new FakeTimeProvider(DateTimeOffset.Parse("2026-10-06T00:00:00Z"));
|
||||
var service = new NodeInfoService(new MemoryStore(), geo, time);
|
||||
|
||||
await service.ObserveAddressAsync("agt_1", NodeKinds.Agent, "8.8.8.8", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", NodeKinds.Agent, "8.8.8.8", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "8.8.8.8", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "8.8.8.8", CancellationToken.None);
|
||||
Assert.Single(geo.Calls);
|
||||
time.Advance(TimeSpan.FromMinutes(11));
|
||||
await service.ObserveAddressAsync("agt_1", NodeKinds.Agent, "8.8.8.8", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "8.8.8.8", CancellationToken.None);
|
||||
Assert.Equal(2, geo.Calls.Count);
|
||||
Assert.Null(Assert.Single(await service.ListAsync(CancellationToken.None)).Country);
|
||||
}
|
||||
@@ -109,13 +109,13 @@ public sealed partial class NodeInfoTests
|
||||
var service = new NodeInfoService(new MemoryStore(), new FakeGeo(null), time);
|
||||
|
||||
Assert.True(service.ShouldRetryGeo("agt_1"));
|
||||
await service.ObserveAddressAsync("agt_1", NodeKinds.Agent, "", CancellationToken.None);
|
||||
await service.ObserveAddressAsync("agt_1", "", CancellationToken.None);
|
||||
Assert.False(service.ShouldRetryGeo("agt_1"));
|
||||
time.Advance(TimeSpan.FromMinutes(11));
|
||||
Assert.True(service.ShouldRetryGeo("agt_1"));
|
||||
|
||||
var found = new NodeInfoService(new MemoryStore(), new FakeGeo(new GeoInfo("jp", 1, "Example", false)), time);
|
||||
await found.ObserveAddressAsync("agt_2", NodeKinds.Agent, "8.8.8.8", CancellationToken.None);
|
||||
await found.ObserveAddressAsync("agt_2", "8.8.8.8", CancellationToken.None);
|
||||
time.Advance(TimeSpan.FromMinutes(11));
|
||||
Assert.False(found.ShouldRetryGeo("agt_2"));
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public sealed partial class NodeInfoTests
|
||||
{
|
||||
var store = new MemoryStore();
|
||||
var first = new NodeInfoService(store, new FakeGeo(null), TimeProvider.System);
|
||||
await first.SetPlatformAsync("agt_1", NodeKinds.Agent, new PlatformInfo { Os = "linux", Name = "Debian 12", Arch = "arm64" }, CancellationToken.None);
|
||||
await first.SetPlatformAsync("agt_1", new PlatformInfo { Os = "linux", Name = "Debian 12", Arch = "arm64" }, CancellationToken.None);
|
||||
|
||||
var second = new NodeInfoService(store, new FakeGeo(null), TimeProvider.System);
|
||||
NodeInfo info = Assert.Single(await second.ListAsync(CancellationToken.None));
|
||||
@@ -190,7 +190,7 @@ public sealed partial class NodeInfoTests
|
||||
|
||||
/// <summary>
|
||||
/// 访客能拿到的东西里不能有 IP 等敏感信息:公开接口的整个响应里不出现 IP 形状的字符串,也没有 ip、city、asn、经纬度这些字段。
|
||||
/// Agent 和 Probe 这里是真的连上了(Probe 的来源地址是 127.0.0.1),所以如果哪里把地址带出来了,这里一定能发现。
|
||||
/// 监听侧和拨号侧 Agent 这里是真的连上了(Dialer 的来源地址是 127.0.0.1),所以如果哪里把地址带出来了,这里一定能发现。
|
||||
/// </summary>
|
||||
[Trait("Category", "Slow")]
|
||||
public sealed partial class NodePrivacyTests : IClassFixture<BackendFixture>
|
||||
@@ -210,31 +210,29 @@ public sealed partial class NodePrivacyTests : IClassFixture<BackendFixture>
|
||||
await _backend.SiteMustBePublicAsync();
|
||||
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
|
||||
await agent.OverrideLineTasksAsync();
|
||||
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
|
||||
await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.Id))["probe_connected"]!.GetValue<bool>());
|
||||
await using DialerHarness dialer = await DialerHarness.StartAsync(_backend);
|
||||
await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.Id))["dialer_connected"]!.GetValue<bool>());
|
||||
|
||||
// 两端都自报了系统和架构(Agent 在 hello 里,Probe 随列表同步)
|
||||
// 两个 Agent 都通过各自的 Backend hello 上报系统和架构
|
||||
JsonNode? agentNode = null;
|
||||
JsonNode? probeNode = null;
|
||||
JsonNode? dialerNode = null;
|
||||
await BackendFixture.EventuallyAsync(
|
||||
async () =>
|
||||
{
|
||||
JsonNode nodes = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/nodes");
|
||||
agentNode = nodes["items"]!.AsArray().FirstOrDefault(n => n!["id"]!.GetValue<string>() == agent.Id);
|
||||
probeNode = nodes["items"]!.AsArray().FirstOrDefault(n => n!["id"]!.GetValue<string>() == probe.ProbeId);
|
||||
return agentNode?["os"] is not null && probeNode?["os"] is not null && agentNode["arch"] is not null && probeNode["arch"] is not null;
|
||||
dialerNode = nodes["items"]!.AsArray().FirstOrDefault(n => n!["id"]!.GetValue<string>() == dialer.DialerId);
|
||||
return agentNode?["os"] is not null && dialerNode?["os"] is not null && agentNode["arch"] is not null && dialerNode["arch"] is not null;
|
||||
},
|
||||
TimeSpan.FromSeconds(40),
|
||||
"the agent (hello) and the probe (agent list sync) both report their platform");
|
||||
Assert.Equal("agent", agentNode!["kind"]!.GetValue<string>());
|
||||
Assert.Equal("probe", probeNode!["kind"]!.GetValue<string>());
|
||||
"both agents report their platform through their own Backend hello");
|
||||
|
||||
string[] paths =
|
||||
[
|
||||
"/api/public/nodes",
|
||||
"/api/public/hosts",
|
||||
"/api/public/overview?range=1h&metrics=rtt.p50",
|
||||
$"/api/public/lines/{Uri.EscapeDataString(agent.Id + "~" + probe.ProbeId)}",
|
||||
$"/api/public/lines/{Uri.EscapeDataString(agent.Id + "~" + dialer.DialerId)}",
|
||||
"/api/public/catalog/plans",
|
||||
];
|
||||
foreach (string path in paths)
|
||||
|
||||
Reference in new issue
Block a user