fix public ip selfcheck logic

This commit is contained in:
ayurishchev committed 2026-08-21 11:04:49 +03:00
1 parent a6f9646da5
commit 67adc6670e
18 files changed
+209 -90

No files matched your search

+62 -8
View File
@@ -11,8 +11,12 @@ package agentcore
import (
"context"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"strings"
"time"
"cloudipvalidator/internal/apiclient"
@@ -139,19 +143,16 @@ func (a *Agent) handleSelfCheckAndRun(ctx context.Context, assignment assignment
selfCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var whoami struct {
IP string `json:"ip"`
}
_, err := a.client.Do(selfCtx, "GET", "/api/v1/whatsmyip", nil, &whoami)
success := err == nil && whoami.IP == assignment.IPAddress
detectedIP, err := a.detectPublicIP(selfCtx)
success := err == nil && detectedIP == assignment.IPAddress
detail := "matched"
if err != nil {
detail = "whatsmyip request failed: " + err.Error()
detail = "ip echo request failed: " + err.Error()
} else if !success {
detail = fmt.Sprintf("egress ip %q does not match assigned fip %q", whoami.IP, assignment.IPAddress)
detail = fmt.Sprintf("egress ip %q does not match assigned fip %q", detectedIP, assignment.IPAddress)
}
a.postSelfCheck(ctx, assignment.IPID, whoami.IP, success, detail)
a.postSelfCheck(ctx, assignment.IPID, detectedIP, success, detail)
a.postEvent(ctx, assignment.IPID, "self_check_result", fmt.Sprintf(`{"success":%t}`, success))
if !success {
@@ -161,6 +162,59 @@ func (a *Agent) handleSelfCheckAndRun(ctx context.Context, assignment assignment
a.runChecks(ctx, assignment)
}
// detectPublicIP asks each configured IP-echo URL, in order, for the
// address this validator is currently seen egressing from, returning the
// first one that answers with a parseable IP. These must be resources
// genuinely outside the cloud project (see config.SelfCheckCfg) — OpenStack
// only applies floating-IP SNAT to traffic leaving via the external
// network, so anything reachable over the project's internal network would
// report the validator's private address instead, regardless of whether
// the floating IP is correctly attached.
func (a *Agent) detectPublicIP(ctx context.Context) (string, error) {
var lastErr error
for _, url := range a.cfg.SelfCheck.IPEchoURLs {
ip, err := fetchIPEcho(ctx, url)
if err != nil {
lastErr = fmt.Errorf("%s: %w", url, err)
continue
}
return ip, nil
}
if lastErr == nil {
lastErr = fmt.Errorf("no self_check.ip_echo_urls configured")
}
return "", lastErr
}
// fetchIPEcho performs a single GET against an IP-echo endpoint that
// returns the caller's address as a bare string in the response body
// (the common contract shared by services like api.ipify.org,
// ifconfig.me/ip, icanhazip.com — and by the local stub used in
// scripts/run-local-e2e.sh).
func fetchIPEcho(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", fmt.Errorf("build request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return "", fmt.Errorf("unexpected status %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
if err != nil {
return "", fmt.Errorf("read response: %w", err)
}
ip := strings.TrimSpace(string(body))
if net.ParseIP(ip) == nil {
return "", fmt.Errorf("response is not a valid IP: %q", ip)
}
return ip, nil
}
func (a *Agent) runChecks(ctx context.Context, assignment assignmentResp) {
var results []checkResultDTO
for _, ct := range assignment.CheckConfig {