fix public ip selfcheck logic
This commit is contained in:
1 parent
a6f9646da5
commit
67adc6670e
18 files changed
+208
-89
No files matched your search
+3
-3
@@ -1,3 +1,3 @@
|
||||
9a6e8dd5d9be684cd4b9c26dde273c3ce863fc9bf869b2e5811a22e14e7bdc98 control-api
|
||||
8793491ae048eb57fb4d901a762e8518fc8253e6cecbf371425cbe9d83c46db9 validator-agent
|
||||
3e9c8b4878aed09e4e946a1c700f6c52b8c5395da33199197097661a4a758bf3 prober
|
||||
a7c481f3c8421dc8b3985d7c3eda13a0ad1e5d37c7a58a4aaf37c1b18254ff54 control-api
|
||||
2e34dce04889a25c564bbd0dd9dfc26499c9e63486f8419ff622ca34f7a9bed8 validator-agent
|
||||
e6ec2444854f624b11f917b7d3cf9fa32f9ee49aff0da637fe2e59140e02133c prober
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,17 @@ poll_interval_seconds: 5
|
||||
|
||||
self_check:
|
||||
timeout_seconds: 10
|
||||
# Must be a resource genuinely outside the cloud project — OpenStack only
|
||||
# applies floating-IP SNAT to traffic leaving via the external network,
|
||||
# so anything reachable over the project's internal network (including
|
||||
# control-api itself, if it's on the same internal network) would report
|
||||
# this validator's private address instead, regardless of whether the
|
||||
# floating IP is correctly attached. Tried in order; falls through to the
|
||||
# next URL only on error/timeout, never on a genuine mismatch. Defaults
|
||||
# to these two public services if omitted.
|
||||
ip_echo_urls:
|
||||
- "https://api.ipify.org"
|
||||
- "https://ifconfig.me/ip"
|
||||
|
||||
checks:
|
||||
https_timeout_seconds: 10
|
||||
|
||||
+14
-16
@@ -113,8 +113,15 @@ JSON, базовый префикс прикладных методов — `/ap
|
||||
|
||||
Отчёт о результате self-check — подтверждение, что исходящий трафик
|
||||
валидатора действительно идёт через только что назначенный FIP. Агент
|
||||
определяет это, вызвав `GET /api/v1/whatsmyip` и сравнив ответ с
|
||||
`ip_address` из задания.
|
||||
определяет это **сам**, обращаясь к внешнему (снаружи облака) IP-echo
|
||||
сервису (`self_check.ip_echo_urls` в `validator-agent.yaml`, например
|
||||
`api.ipify.org`) и сравнивая ответ с `ip_address` из задания — control-api
|
||||
в этом определении не участвует. Важно, что ресурс должен быть именно
|
||||
внешним: OpenStack применяет SNAT через Floating IP только к трафику,
|
||||
уходящему через внешнюю сеть, поэтому обращение к чему-либо внутри
|
||||
проекта (в том числе к самому control-api, если он в той же внутренней
|
||||
сети) покажет приватный адрес валидатора независимо от того, правильно
|
||||
ли привязан FIP.
|
||||
|
||||
Запрос:
|
||||
```json
|
||||
@@ -247,17 +254,6 @@ IP на данном проходе". До этого момента control-api
|
||||
Проверка живости процесса. Ответ: `{"ok": true}`. Используется в systemd/
|
||||
внешних системах мониторинга.
|
||||
|
||||
### `GET /api/v1/whatsmyip`
|
||||
|
||||
Возвращает IP-адрес, с которого пришёл TCP-запрос (без учёта заголовков
|
||||
`X-Forwarded-For` — специально, чтобы self-check нельзя было подделать).
|
||||
Это основа механизма self-check.
|
||||
|
||||
Ответ:
|
||||
```json
|
||||
{"ip": "203.0.113.10"}
|
||||
```
|
||||
|
||||
### `GET /api/v1/admin/status`
|
||||
|
||||
Сводка по очереди — сколько IP в каком состоянии.
|
||||
@@ -348,9 +344,11 @@ curl -s -X POST "$BASE/api/v1/agents/register" \
|
||||
curl -s "$BASE/api/v1/agents/validator_01/assignment"
|
||||
# => {"ip_id":1,"ip_address":"203.0.113.10","phase":"awaiting_self_check","check_config":[...]}
|
||||
|
||||
# 3. Self-check: спросить у control-api, каким адресом мы к нему пришли
|
||||
curl -s "$BASE/api/v1/whatsmyip"
|
||||
# => {"ip":"203.0.113.10"} (в реальном стенде это и есть проверка через FIP)
|
||||
# 3. Self-check: спросить у ВНЕШНЕГО (вне облака) IP-echo сервиса, каким
|
||||
# адресом мы наружу выглядим — это делает сам агент, control-api тут
|
||||
# ни при чём (см. self_check.ip_echo_urls в validator-agent.yaml)
|
||||
curl -s "https://api.ipify.org"
|
||||
# => 203.0.113.10 (в реальном стенде это и есть проверка через FIP)
|
||||
|
||||
curl -s -X POST "$BASE/api/v1/agents/validator_01/self-check" \
|
||||
-d '{"ip_id":1,"detected_egress_ip":"203.0.113.10","success":true,"detail":"matched"}'
|
||||
|
||||
+25
-16
@@ -38,7 +38,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph CAPI["control-api (управляющая машина, 1 экземпляр)"]
|
||||
HTTP["HTTP API<br/>/api/v1/agents/*<br/>/api/v1/probers/*<br/>/api/v1/admin/*<br/>/healthz · /whatsmyip"]
|
||||
HTTP["HTTP API<br/>/api/v1/agents/*<br/>/api/v1/probers/*<br/>/api/v1/admin/*<br/>/healthz"]
|
||||
ORCH["Оркестратор: Tick раз в<br/>poll_interval_seconds<br/>claim → associate FIP →<br/>ожидание self-check →<br/>checking → aggregate → release<br/>+ lease sweep + heartbeat sweep"]
|
||||
DB[("SQLite<br/>validators / ip_queue<br/>checks / events")]
|
||||
OSCLIENT["OpenStack-клиент<br/>(mode: mock | real)"]
|
||||
@@ -85,8 +85,9 @@ flowchart LR
|
||||
AGENT["validator-agent"]
|
||||
end
|
||||
|
||||
CAPI["control-api"]
|
||||
FIP(["Floating IP<br/>203.0.113.10<br/>(адрес под проверкой,<br/>привязан оркестратором заранее)"])
|
||||
IPECHO["Внешний IP-echo сервис<br/>(api.ipify.org и т.п.,<br/>вне облака)"]
|
||||
CAPI["control-api"]
|
||||
|
||||
subgraph TARGETS["Целевые серверы (targets из конфига)"]
|
||||
T1["hub.docker.com"]
|
||||
@@ -94,25 +95,33 @@ flowchart LR
|
||||
T3["packages.ubuntu.com"]
|
||||
end
|
||||
|
||||
AGENT -->|"1 GET /api/v1/whatsmyip<br/>(self-check)"| CAPI
|
||||
CAPI -.->|"2 наблюдаемый исходящий IP"| AGENT
|
||||
AGENT ==>|"3 весь исходящий трафик ВМ<br/>идёт через FIP (SNAT облака)"| FIP
|
||||
AGENT ==>|"1 self-check: исходящий трафик<br/>ВМ идёт через FIP (SNAT облака)"| FIP
|
||||
FIP ==>|"1 GET"| IPECHO
|
||||
IPECHO -.->|"2 наблюдаемый исходящий IP"| AGENT
|
||||
AGENT -->|"3 POST self-check {success}"| CAPI
|
||||
AGENT ==>|"4 проверки: тоже через FIP"| FIP
|
||||
FIP ==>|"4 HTTPS GET"| T1
|
||||
FIP ==>|"4 HTTPS GET"| T2
|
||||
FIP ==>|"4 ICMP echo"| T3
|
||||
```
|
||||
|
||||
**Пояснение.** Шаги 1–2 — самопроверка (self-check): агент обращается к
|
||||
`control-api` и сравнивает адрес, с которого пришёл его собственный
|
||||
запрос, с адресом, который ему назначен. Поскольку весь исходящий трафик
|
||||
ВМ реально идёт через привязанный Floating IP (шаг 3, SNAT на стороне
|
||||
облака), совпадение подтверждает, что назначение применилось корректно —
|
||||
только после этого агент переходит к шагу 4 и выполняет проверки из
|
||||
`check_config` (HTTPS/ICMP/опционально SSH) против целей из конфига.
|
||||
Каждый результат отправляется обратно в control-api сразу после
|
||||
выполнения (см. диаграмму телеметрии ниже) — сам этот data-plane трафик
|
||||
(шаги 3–4) в control-api не проходит и им не наблюдается напрямую,
|
||||
control-api видит только заявленный агентом результат.
|
||||
**Пояснение.** Шаги 1–2 — самопроверка (self-check): агент сам (без
|
||||
участия control-api) обращается к внешнему IP-echo сервису и сравнивает
|
||||
адрес, с которого пришёл его собственный запрос, с адресом, который ему
|
||||
назначен. Ресурс для сравнения обязан быть вне облака: OpenStack
|
||||
применяет SNAT через Floating IP только к трафику, уходящему через
|
||||
внешнюю сеть — обращение к чему-либо внутри проекта (включая сам
|
||||
control-api, если он в той же внутренней сети) показало бы приватный
|
||||
адрес валидатора независимо от корректности привязки FIP. Итог
|
||||
самопроверки агент затем сообщает control-api отдельным вызовом (шаг 3) —
|
||||
это уже управляющий, а не проверяемый трафик. Только после успешного
|
||||
self-check агент переходит к шагу 4 и выполняет проверки из
|
||||
`check_config` (HTTPS/ICMP/опционально SSH) против целей из конфига —
|
||||
тем же путём, через тот же FIP. Каждый результат отправляется обратно в
|
||||
control-api сразу после выполнения (см. диаграмму телеметрии ниже) — сам
|
||||
этот data-plane трафик (шаги 1 и 4) в control-api не проходит и им не
|
||||
наблюдается напрямую, control-api видит только заявленный агентом
|
||||
результат.
|
||||
|
||||
### Дополнительно: обратное направление (пробер к валидатору)
|
||||
|
||||
|
||||
+19
-15
@@ -8,31 +8,35 @@ real OpenStack cloud and no real internet access:
|
||||
synthetic floating IP per configured address (see
|
||||
`cmd/control-api/main.go`'s `newOpenStackClient`).
|
||||
- **1 validator-agent** (`validator_01`), started with
|
||||
`-stub-ports 12022,18081,18443,18888` — trivial accept-and-close TCP
|
||||
`-stub-ports 22022,28081,28443,28888` — trivial accept-and-close TCP
|
||||
listeners standing in for the base-minimum services (22/80/443/8080) a
|
||||
real validator would run. ICMP needs no stub: the kernel answers echo
|
||||
requests to any local address (127.0.0.0/8) on its own.
|
||||
- **3 probers** (`site-1`/`site-2`/`site-3`), all probing `127.0.0.1`.
|
||||
- **3 `httpstub` instances** (`scripts/httpstub`) standing in for the real
|
||||
outbound targets (hub.docker.com / github.com / packages.ubuntu.com),
|
||||
always returning `200 OK`.
|
||||
- **3 `httpstub` instances** (`scripts/httpstub`) — `/` always returns
|
||||
`200 OK`, standing in for the real outbound egress targets (hub.docker.com
|
||||
/ github.com / packages.ubuntu.com); `/ip` echoes the caller's remote
|
||||
address as plain text, standing in for the external IP-echo service the
|
||||
validator-agent's self-check normally queries in production (see
|
||||
`internal/agentcore.detectPublicIP` and `config.SelfCheckCfg.IPEchoURLs`).
|
||||
|
||||
The generated config uses a short `lease_ttl_seconds: 8` and
|
||||
`checking_window_seconds: 15` so the whole run finishes in well under a
|
||||
minute instead of using the (much longer) production defaults.
|
||||
|
||||
**Why only one IP address (`127.0.0.1`), and why it must be exactly that
|
||||
one:** the self-check mechanism (`GET /whatsmyip`, see
|
||||
`internal/httpapi/server.go`'s `remoteIP`) compares the TCP source address
|
||||
the validator-agent's own outbound connection to control-api arrives with
|
||||
against the address it was just assigned. In a real deployment, Neutron
|
||||
actually SNATs the validator's egress traffic through whichever floating
|
||||
IP is attached, so any configured address self-checks correctly. This
|
||||
offline harness has no real network-level SNAT — the agent's traffic to
|
||||
control-api always really originates from `127.0.0.1` — so only that
|
||||
literal loopback address can ever pass self-check here. This is a
|
||||
limitation of the harness's fidelity, not of the self-check mechanism
|
||||
itself.
|
||||
one:** self-check compares the source address the validator-agent's own
|
||||
request to the IP-echo target arrives with against the address it was
|
||||
just assigned. In a real deployment that target is a real external
|
||||
IP-echo service, and Neutron actually SNATs the validator's egress
|
||||
traffic through whichever floating IP is attached, so any configured
|
||||
address self-checks correctly. This offline harness points
|
||||
`self_check.ip_echo_urls` at the local `httpstub`'s `/ip` route instead
|
||||
(see `validator-agent.yaml` generated by the script) — there's no real
|
||||
network-level SNAT here, the agent's traffic to that stub always really
|
||||
originates from `127.0.0.1`, so only that literal loopback address can
|
||||
ever pass self-check in this harness. This is a limitation of the
|
||||
harness's fidelity, not of the self-check mechanism itself.
|
||||
|
||||
## Running it
|
||||
|
||||
|
||||
@@ -367,6 +367,13 @@ curl -s http://<control-api>:8080/api/v1/admin/validators | python3 -m json.tool
|
||||
- `validator-agent` → интернет: HTTPS/ICMP до целей из `targets` конфига
|
||||
(по умолчанию hub.docker.com, github.com, packages.ubuntu.com) — именно
|
||||
через floating IP, который в данный момент привязан к валидатору.
|
||||
- `validator-agent` → внешние IP-echo сервисы из `self_check.ip_echo_urls`
|
||||
(по умолчанию `api.ipify.org`, `ifconfig.me`) — **обязательно вне
|
||||
облака**: это и есть механизм self-check (см.
|
||||
[DIAGRAMS.md](DIAGRAMS.md#2-поток-данных-от-валидатора-к-целевому-серверу-egress-проверка)).
|
||||
Если валидатор не может достучаться ни до одного из этих адресов,
|
||||
self-check никогда не пройдёт и IP будет бесконечно возвращаться в
|
||||
очередь — см. [USAGE.md](USAGE.md#частые-проблемы-и-что-с-ними-делать).
|
||||
- `control-api` → OpenStack Keystone/Neutron API (`OS_AUTH_URL` и далее по
|
||||
каталогу сервисов).
|
||||
|
||||
|
||||
+20
-2
@@ -230,14 +230,32 @@ curl -s http://<control-api>:8080/api/v1/admin/validators | python3 -m json.tool
|
||||
`server.listen_addr`) и что процесс `validator-agent` вообще запущен
|
||||
(`systemctl status validator-agent`, `journalctl -u validator-agent`).
|
||||
|
||||
**Адрес постоянно проваливает self-check.**
|
||||
**Адрес не выходит из `awaiting_self_check` (статус не меняется вообще).**
|
||||
Self-check запрашивает внешние (вне облака) сервисы из
|
||||
`self_check.ip_echo_urls` в конфиге валидатора (по умолчанию
|
||||
`api.ipify.org`, `ifconfig.me`) — если у ВМ-валидатора нет исходящего
|
||||
доступа в интернет к этим адресам, запрос не проходит вообще, и агент
|
||||
даже не может *сообщить* результат control-api (ни успешный, ни
|
||||
неуспешный) — тогда статус реально зависает до истечения
|
||||
`orchestrator.lease_ttl_seconds`, после чего адрес возвращается в
|
||||
`queued` и цикл повторяется. Проверьте связность до
|
||||
`self_check.ip_echo_urls` прямо с ВМ-валидатора (`curl -s
|
||||
https://api.ipify.org`) и логи `journalctl -u validator-agent` на предмет
|
||||
`ip echo request failed`.
|
||||
|
||||
**Адрес постоянно проваливает self-check (не зависает, а именно
|
||||
возвращается в очередь снова и снова).**
|
||||
Смотрите `events` по адресу (`GET /api/v1/admin/ips/{ip}`) — в детали
|
||||
события `self_check_result` будет указан обнаруженный исходящий адрес.
|
||||
Если он не совпадает с ожидаемым — вероятно, на ВМ-валидаторе есть другой
|
||||
маршрут наружу (не через назначенный Floating IP), либо привязка FIP на
|
||||
стороне OpenStack не применилась. Проверьте вручную в OpenStack
|
||||
(`openstack floating ip show <адрес>`), что `port_id` совпадает с портом
|
||||
валидатора.
|
||||
валидатора. Обратите внимание: адрес для сравнения обязан быть **вне**
|
||||
облака (см. `self_check.ip_echo_urls`) — запрос к чему-либо внутри
|
||||
проекта (в том числе к самому control-api, если он в той же внутренней
|
||||
сети) покажет приватный адрес валидатора независимо от того, правильно
|
||||
ли привязан FIP, и всегда будет давать ложный провал.
|
||||
|
||||
**Площадка (`site-N`) никогда не отчитывается (`SiteNComplete` всегда
|
||||
`false`).**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -175,8 +175,18 @@ type ValidatorAgent struct {
|
||||
Checks AgentChecks `yaml:"checks"`
|
||||
}
|
||||
|
||||
// SelfCheckCfg configures how the agent confirms its egress actually flows
|
||||
// through the newly assigned floating IP. This must query a resource
|
||||
// genuinely outside the cloud project: OpenStack only applies floating-IP
|
||||
// SNAT to traffic leaving via the external/provider network, so any
|
||||
// in-project resource (including control-api, if it's reachable over the
|
||||
// project's internal network) would see the validator's private address
|
||||
// instead — a false negative that never changes. IPEchoURLs are tried in
|
||||
// order (falling through to the next on error/timeout, not on a genuine
|
||||
// mismatch) until one returns a parseable IP.
|
||||
type SelfCheckCfg struct {
|
||||
TimeoutSeconds int `yaml:"timeout_seconds"`
|
||||
IPEchoURLs []string `yaml:"ip_echo_urls"`
|
||||
}
|
||||
|
||||
type AgentChecks struct {
|
||||
@@ -202,6 +212,9 @@ func LoadValidatorAgent(path string) (*ValidatorAgent, error) {
|
||||
if c.SelfCheck.TimeoutSeconds == 0 {
|
||||
c.SelfCheck.TimeoutSeconds = 10
|
||||
}
|
||||
if len(c.SelfCheck.IPEchoURLs) == 0 {
|
||||
c.SelfCheck.IPEchoURLs = []string{"https://api.ipify.org", "https://ifconfig.me/ip"}
|
||||
}
|
||||
if c.Checks.HTTPSTimeoutSeconds == 0 {
|
||||
c.Checks.HTTPSTimeoutSeconds = 10
|
||||
}
|
||||
|
||||
@@ -139,7 +139,3 @@ func (s *Server) handleAgentComplete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
||||
}
|
||||
|
||||
func (s *Server) handleWhatsMyIP(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ip": remoteIP(r)})
|
||||
}
|
||||
@@ -123,13 +123,10 @@ func TestEndToEndHTTPFlow(t *testing.T) {
|
||||
t.Fatalf("expected 1.2.3.4, got %s", assignment.IPAddress)
|
||||
}
|
||||
|
||||
// Self-check via /whatsmyip: in the real deployment this would equal
|
||||
// the FIP; here we just exercise the endpoint and always report success.
|
||||
resp, body = fc.do(http.MethodGet, "/api/v1/whatsmyip", nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("whatsmyip: status=%d body=%s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
// Self-check: in the real deployment the agent queries an external
|
||||
// IP-echo service (see internal/agentcore.detectPublicIP) and compares
|
||||
// the result to the assigned FIP; the HTTP layer here just accepts
|
||||
// whatever outcome the caller reports.
|
||||
resp, body = fc.do(http.MethodPost, "/api/v1/agents/validator-1/self-check", selfCheckRequest{
|
||||
IPID: assignment.IPID, DetectedEgress: "1.2.3.4", Success: true, Detail: "matched",
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import "net/http"
|
||||
|
||||
func (s *Server) routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /healthz", s.handleHealthz)
|
||||
mux.HandleFunc("GET /api/v1/whatsmyip", s.handleWhatsMyIP)
|
||||
|
||||
mux.HandleFunc("POST /api/v1/agents/register", s.handleAgentRegister)
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/heartbeat", s.handleAgentHeartbeat)
|
||||
|
||||
@@ -8,7 +8,6 @@ package httpapi
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"cloudipvalidator/internal/db"
|
||||
@@ -57,15 +56,3 @@ func readJSON(r *http.Request, v interface{}) error {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
// remoteIP returns the caller's source IP with any port stripped. Used by
|
||||
// /whatsmyip — the validator-agent's self-check mechanism relies on this
|
||||
// being the actual TCP peer address (as SNAT'd by the newly associated
|
||||
// FIP), never a client-supplied header.
|
||||
func remoteIP(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
@@ -1,12 +1,24 @@
|
||||
// Command httpstub is a trivial "always 200 OK" HTTP server used only by
|
||||
// scripts/run-local-e2e.sh, standing in for the real internet targets
|
||||
// (hub.docker.com, github.com, packages.ubuntu.com) so the offline
|
||||
// end-to-end harness needs no real internet access.
|
||||
// Command httpstub is a trivial local HTTP server used only by
|
||||
// scripts/run-local-e2e.sh, standing in for two kinds of real internet
|
||||
// resources so the offline end-to-end harness needs no real internet
|
||||
// access:
|
||||
// - "/" always returns 200 OK — stands in for the real outbound egress
|
||||
// targets (hub.docker.com, github.com, packages.ubuntu.com).
|
||||
// - "/ip" echoes the caller's remote address as plain text — stands in
|
||||
// for the external IP-echo service the validator-agent's self-check
|
||||
// queries in production (see internal/agentcore.detectPublicIP and
|
||||
// config.SelfCheckCfg.IPEchoURLs). Because httpstub runs locally, this
|
||||
// only produces a meaningful self-check signal in the harness because
|
||||
// the "floating IP" under test is itself the loopback address the
|
||||
// caller genuinely connects from — it is not a stand-in for OpenStack's
|
||||
// SNAT behavior.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -18,6 +30,14 @@ func main() {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("stub ok\n"))
|
||||
})
|
||||
http.HandleFunc("/ip", func(w http.ResponseWriter, r *http.Request) {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, host)
|
||||
})
|
||||
log.Printf("httpstub listening on %s", *addr)
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
@@ -102,6 +102,12 @@ control_api_url: "http://127.0.0.1:28080"
|
||||
poll_interval_seconds: 1
|
||||
self_check:
|
||||
timeout_seconds: 5
|
||||
# Points at the local httpstub's /ip echo route rather than a real
|
||||
# internet IP-echo service — self-check only produces a meaningful
|
||||
# signal here because the "floating IP" under test (127.0.0.1) is
|
||||
# genuinely the address this process connects from; there's no real
|
||||
# OpenStack SNAT involved in this offline harness. See docs/LOCAL_E2E.md.
|
||||
ip_echo_urls: ["http://127.0.0.1:29091/ip"]
|
||||
checks:
|
||||
https_timeout_seconds: 5
|
||||
icmp_timeout_seconds: 3
|
||||
|
||||
Reference in new issue
Block a user