feat: detect an agent's public IPv4 and IPv6 addresses automatically, manual values still win [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-07 18:12:17 +08:00
1 parent 9966ea5d63
commit f03988d68b
26 files changed
+517 -33

No files matched your search

@@ -0,0 +1,90 @@
using System.Net;
using System.Text.Json.Nodes;
using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
/// <summary>public_host 和 ipv6_host 自动识别:Agent 上报公网地址,手填的优先,可以撤销手填。</summary>
[Collection("probe-e2e")]
public sealed class AddressTests
{
private const string V4 = "8.8.4.4";
private const string V6 = "2606:4700:4700::1111";
private readonly BackendFixture _backend;
public AddressTests(BackendFixture backend)
{
_backend = backend;
}
private async Task<JsonNode> AgentAsync(string id) => await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + id);
private async Task<JsonNode> PatchAsync(string id, JsonObject body)
{
using HttpResponseMessage response = await _backend.Admin.PatchAsync("/api/admin/agents/" + id, BackendFixture.Json(body));
string text = await response.Content.ReadAsStringAsync();
Assert.True(response.IsSuccessStatusCode, text);
return JsonNode.Parse(text)!;
}
[Fact]
public async Task Hello_FillsPublicHostAndIpv6Host_PrivateAndTemporaryAddressesAreIgnored()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync(hello: false);
agent.Addresses = ["10.0.0.5", "192.168.1.9", "fd00::1", "fe80::1", V6, V4];
await agent.HelloAsync();
JsonNode view = null!;
await BackendFixture.EventuallyAsync(async () => (view = await AgentAsync(agent.NodeId.Value))["ipv6_host"]!.GetValue<string>() == V6);
Assert.Equal(V4, view["public_host"]!.GetValue<string>());
Assert.True(view["public_host_auto"]!.GetValue<bool>());
Assert.True(view["ipv6_host_auto"]!.GetValue<bool>());
}
[Fact]
public async Task ManualValues_WinOverAutoDetection_AndCanBeRevokedBackToAuto()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync(hello: false);
string id = agent.NodeId.Value;
agent.Addresses = [V4, V6];
await agent.HelloAsync();
await BackendFixture.EventuallyAsync(async () => (await AgentAsync(id))["ipv6_host"]!.GetValue<string>() == V6);
// 手填:标记为手动,之后 Agent 再上报别的地址也不覆盖
JsonNode manual = await PatchAsync(id, new JsonObject { ["public_host"] = "agent.example.net", ["ipv6_host"] = "" });
Assert.False(manual["public_host_auto"]!.GetValue<bool>());
Assert.False(manual["ipv6_host_auto"]!.GetValue<bool>());
agent.Addresses = ["1.1.1.1", "2606:4700:4700::1001"];
await agent.HelloAsync();
await Task.Delay(300);
JsonNode kept = await AgentAsync(id);
Assert.Equal("agent.example.net", kept["public_host"]!.GetValue<string>());
Assert.Equal("", kept["ipv6_host"]!.GetValue<string>()); // 空 = 手动关闭 IPv6
// 撤销手填:马上按最近一次上报的地址重新识别
JsonNode auto = await PatchAsync(id, new JsonObject { ["public_host_auto"] = true, ["ipv6_host_auto"] = true });
Assert.Equal("1.1.1.1", auto["public_host"]!.GetValue<string>());
Assert.Equal("2606:4700:4700::1001", auto["ipv6_host"]!.GetValue<string>());
Assert.True(auto["public_host_auto"]!.GetValue<bool>());
}
[Fact]
public async Task AutoDetectedAddress_StaysWhileItIsStillReported_SoReorderingDoesNotFlap()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync(hello: false);
string id = agent.NodeId.Value;
agent.Addresses = [V4, "9.9.9.9"];
await agent.HelloAsync();
await BackendFixture.EventuallyAsync(async () => (await AgentAsync(id))["public_host"]!.GetValue<string>() == V4);
agent.Addresses = ["1.0.0.1", V4];
await agent.HelloAsync();
await Task.Delay(300);
Assert.Equal(V4, (await AgentAsync(id))["public_host"]!.GetValue<string>());
// 原来的地址不见了才换
agent.Addresses = ["1.0.0.1"];
await agent.HelloAsync();
await BackendFixture.EventuallyAsync(async () => (await AgentAsync(id))["public_host"]!.GetValue<string>() == "1.0.0.1");
}
}
@@ -49,6 +49,10 @@ public sealed class AgentHarness : IAsyncDisposable
Result<EnrollmentResult> enrolled = await EnrollmentClient.EnrollAsync(http, backend.BaseAddress, token, name ?? backend.UniqueName("agent"), directory, AgentApp.Version, null, TimeProvider.System, CancellationToken.None);
Assert.True(enrolled.IsSuccess, enrolled.IsSuccess ? "" : enrolled.Error.ToString());
// 测试里的 Agent 只在回环上监听:手填 public_host(手动的优先于自动识别,Agent 上报本机真实地址也不会覆盖)
using HttpResponseMessage patched = await backend.Admin.PatchAsync("/api/admin/agents/" + enrolled.Value.NodeId, BackendFixture.Json(new JsonObject { ["public_host"] = "127.0.0.1" }));
Assert.True(patched.IsSuccessStatusCode, await patched.Content.ReadAsStringAsync());
var config = new AgentConfig
{
Fleet = { Server = backend.BaseAddress.ToString(), IdentityDir = directory },