refactor(edge): unify dynamic IP detection and prioritize IPv4 reporting

- Align agent, relay, and flared to dynamically resolve IP during heartbeat using the nodeip package (when not manually configured).
- Update GeoIP outbound IP strategy to prefer IPv4 HTTP client lookup using tcp4 dialer and fall back to dual-stack tcp.
- Optimize agent profile fingerprinting to exclude dynamic UptimeSeconds and ReportedAtUnix fields, preventing redundant updates.
- Refactor unit tests to prevent outbound network queries during tests.
This commit is contained in:
ryan
2026-06-22 15:06:39 +08:00
parent 346979344f
commit deb232d840
8 changed files with 84 additions and 6 deletions
+6
View File
@@ -211,6 +211,7 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) {
AccessToken: "agent-token",
NodeName: "edge-01",
NodeIP: "10.0.0.8",
NodeIPConfigured: true,
Version: config.Version,
ExtVersion: "1.27.1.2",
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
@@ -264,6 +265,7 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) {
AccessToken: "agent-token",
NodeName: "edge-01",
NodeIP: "10.0.0.8",
NodeIPConfigured: true,
Version: config.Version,
ExtVersion: "1.27.1.2",
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
@@ -322,6 +324,7 @@ func TestRunnerReportsOpenrestyHealthAndExecutesRestart(t *testing.T) {
AccessToken: "agent-token",
NodeName: "edge-01",
NodeIP: "10.0.0.8",
NodeIPConfigured: true,
Version: config.Version,
ExtVersion: "1.27.1.2",
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
@@ -375,6 +378,7 @@ func TestRunnerHeartbeatPayloadIncludesObservabilityExtensions(t *testing.T) {
Config: &config.Config{
NodeName: "edge-observe-1",
NodeIP: "10.0.0.51",
NodeIPConfigured: true,
Version: config.Version,
ExtVersion: "1.27.1.2",
DataDir: tempDir,
@@ -458,6 +462,7 @@ func TestRunnerReplaysBufferedObservabilityAfterHeartbeatRecovery(t *testing.T)
AccessToken: "agent-token",
NodeName: "edge-buffer-01",
NodeIP: "10.0.0.52",
NodeIPConfigured: true,
Version: config.Version,
ExtVersion: "1.27.1.2",
DataDir: tempDir,
@@ -537,6 +542,7 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) {
DiscoveryToken: cfg.DiscoveryToken,
NodeName: cfg.NodeName,
NodeIP: cfg.NodeIP,
NodeIPConfigured: cfg.NodeIPConfigured,
Version: config.Version,
ExtVersion: "1.27.1.2",
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
+2
View File
@@ -67,6 +67,7 @@ type Config struct {
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
RequestTimeout MillisecondDuration `json:"request_timeout"`
configPath string `json:"-"`
NodeIPConfigured bool `json:"-"`
}
type configFile struct {
@@ -129,6 +130,7 @@ func Load(path string) (*Config, error) {
}
cfg.configPath = path
applyEnvOverrides(cfg)
cfg.NodeIPConfigured = cfg.NodeIP != ""
applyDefaults(cfg, filepath.Dir(path))
if err = validate(cfg); err != nil {
return nil, err
+8 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"github.com/Rain-kl/Wavelet/internal/apps/agent/updater"
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
)
// SyncService is the interface used by Cycle to sync active configuration and WAF IP groups.
@@ -97,10 +98,16 @@ func (c *Cycle) NodePayload(ctx context.Context, nodeID string) protocol.NodePay
metricSnapshot := observability.BuildSnapshot(c.Config, c.StateStore)
openrestyObservation := observability.BuildOpenrestyObservation(managedOpenRestyMetrics)
healthEvents := observability.BuildHealthEvents(snapshot)
ip := c.Config.NodeIP
if !c.Config.NodeIPConfigured {
ip = nodeip.DetectWithContext(ctx)
}
payload := protocol.NodePayload{
NodeID: nodeID,
Name: c.Config.NodeName,
IP: c.Config.NodeIP,
IP: ip,
Version: c.Config.Version,
ExtVersion: c.Config.ExtVersion,
CurrentVersion: snapshot.CurrentVersion,
@@ -148,7 +148,10 @@ func collectProfile(cfg *config.Config) *protocol.NodeSystemProfile {
}
func fingerprintProfile(profile *protocol.NodeSystemProfile) string {
raw, err := json.Marshal(profile)
cloned := *profile
cloned.UptimeSeconds = 0
cloned.ReportedAtUnix = 0
raw, err := json.Marshal(&cloned)
if err != nil {
return ""
}
+2
View File
@@ -35,6 +35,7 @@ type Config struct {
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
RequestTimeout MillisecondDuration `json:"request_timeout"`
configPath string
NodeIPConfigured bool
}
// Load reads configuration from path, applying environment overrides and defaults.
@@ -54,6 +55,7 @@ func Load(path string) (*Config, error) {
}
cfg.configPath = path
applyEnvOverrides(cfg)
cfg.NodeIPConfigured = cfg.NodeIP != ""
applyDefaults(cfg, filepath.Dir(path))
if err = validate(cfg); err != nil {
return nil, err
+8 -1
View File
@@ -6,6 +6,7 @@ import (
"log/slog"
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
"github.com/Rain-kl/Wavelet/internal/apps/relay/config"
"github.com/Rain-kl/Wavelet/internal/apps/relay/frps"
"github.com/Rain-kl/Wavelet/internal/apps/relay/httpclient"
@@ -46,6 +47,12 @@ func (s *Service) doHeartbeat(ctx context.Context) {
slog.Debug("sending heartbeat")
runtimeStatus := s.frpsManager.GetRuntimeStatus()
ip := s.config.NodeIP
if !s.config.NodeIPConfigured {
ip = nodeip.DetectWithContext(ctx)
}
payload := service.RelayHeartbeatPayload{
Version: config.Version,
ExtVersion: s.frpsManager.GetVersion(ctx),
@@ -55,7 +62,7 @@ func (s *Service) doHeartbeat(ctx context.Context) {
FrpsClientCount: runtimeStatus.ClientCount,
FrpsProxies: runtimeStatus.Proxies,
Name: s.config.NodeName,
IP: s.config.NodeIP,
IP: ip,
Profile: observability.BuildProfile(s.config, s.stateStore),
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
HealthEvents: observability.BuildHealthEvents(runtimeStatus),