Enable SSH and TLS handshake on prober for TCP22 and TCP443 ports
This commit is contained in:
1 parent
af3453f19f
commit
550ce3fec4
11 files changed
+308
-20
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
118e09f98944ac59593af603c04a1d5f0e7cafb6549b5a562300bc31d9c769da control-api
|
||||
48c9b99fa88be751d9badfba8b7d80f894e326a743a90e85478f1d2251ce4ab6 validator-agent
|
||||
8877cf7c6d2bdd58faf70865be602b3db5f18508f4607c8942bc8899ce0fb1e6 prober
|
||||
c7b4c2464a5974237b1f98f1565884f09b1ee91a1efadb4b8e1e7d9aaf0b9fe2 prober
|
||||
b0339b9b341d9ab2d4f6835fda5d9f927a18ed8a831ec9f24dba05624664a417 admin-dashboard
|
||||
BIN
Binary file not shown.
+12
@@ -262,8 +262,10 @@ USAGE.md про состояния площадки).
|
||||
{
|
||||
"results": [
|
||||
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-22", "success": true, "latency_ms": 12, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
|
||||
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "ssh", "success": true, "latency_ms": 15, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
|
||||
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-80", "success": true, "latency_ms": 9, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
|
||||
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-443", "success": true, "latency_ms": 10, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
|
||||
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tls-443", "success": true, "latency_ms": 34, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
|
||||
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-8080","success": false,"latency_ms": 0, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
|
||||
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "icmp", "success": true, "latency_ms": 5, "checked_at": "2026-08-21T09:15:01Z", "complete": true}
|
||||
]
|
||||
@@ -275,6 +277,16 @@ USAGE.md про состояния площадки).
|
||||
IP на данном проходе". До этого момента control-api не будет считать
|
||||
данные с этой площадки завершёнными.
|
||||
|
||||
Порты `22` и `443` в `ports` — особый случай: если они присутствуют в
|
||||
списке, `prober` дополнительно к базовой `tcp-<port>`-проверке доступности
|
||||
выполняет **настоящий** протокольный чек — `ssh` (обмен SSH-банером на
|
||||
порту 22) и `tls-443` (полноценный TLS-хендшейк на порту 443)
|
||||
соответственно. Это не отдельно настраиваемые типы проверок — они
|
||||
включаются/выключаются автоматически вместе с самим портом, без
|
||||
дополнительного флага. Сертификат в `tls-443` не валидируется
|
||||
(`InsecureSkipVerify`) — проверяется только сам факт TLS-хендшейка, не
|
||||
доверие к серверу.
|
||||
|
||||
Ответ: `{"ok": true}`.
|
||||
|
||||
## Служебные и административные методы
|
||||
|
||||
@@ -324,6 +324,16 @@ curl -s -X PUT http://<control-api>:8080/api/v1/admin/config/inbound-checks \
|
||||
паузой перед self-check (см. ниже): текстовое поле с портами через запятую
|
||||
и чекбокс ICMP.
|
||||
|
||||
Порты `22` и `443` в этом списке трактуются особо: помимо базового
|
||||
TCP-connect (`tcp-22`/`tcp-443`) пробер дополнительно выполняет настоящий
|
||||
обмен SSH-банером (`ssh`) и настоящий TLS-хендшейк (`tls-443`) —
|
||||
голого открытого TCP-порта недостаточно, чтобы считать SSH/HTTPS
|
||||
рабочими. Отдельного переключателя для этих доп.проверок нет: они
|
||||
включаются и выключаются вместе с самим портом в `ports`. Если на порту
|
||||
22/443 у площадки на самом деле слушает что-то, кроме SSH/HTTPS,
|
||||
доп.проверка будет закономерно проваливаться — заведите для такого сервиса
|
||||
другой порт.
|
||||
|
||||
> Правки через `orchestrator.inbound_checks` в `control-api.yaml` тоже
|
||||
> поддерживаются, но только как bootstrap пустой базы данных при самом
|
||||
> первом старте — как только в БД есть эта настройка (а она появляется
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package checkrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TLSHandshake performs a real TLS handshake (not just a TCP connect)
|
||||
// against host:port and reports success if it completes within timeout.
|
||||
// Certificate validation is intentionally skipped: the target is a bare
|
||||
// candidate IP under test before any DNS/hostname is attached to it, so
|
||||
// there's neither a hostname to validate the cert against nor any reason
|
||||
// to expect a signed cert yet. This checks "is there a real TLS listener
|
||||
// here", not "is its certificate valid" — same scope-limiting principle
|
||||
// SSHBanner already applies (banner only, no auth handshake).
|
||||
func TLSHandshake(host string, port int, timeout time.Duration) func(ctx context.Context) Result {
|
||||
target := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
checkType := "tls-" + strconv.Itoa(port)
|
||||
return run(checkType, target, func(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
d := tls.Dialer{
|
||||
NetDialer: &net.Dialer{Timeout: timeout},
|
||||
Config: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
conn, err := d.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package checkrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func splitHostPortInt(t *testing.T, addr string) (string, int) {
|
||||
t.Helper()
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("split host port %q: %v", addr, err)
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
t.Fatalf("parse port %q: %v", portStr, err)
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
// TestTLSHandshakeSucceedsAgainstTLSServer confirms a real TLS listener
|
||||
// (self-signed cert, InsecureSkipVerify accepts it) passes the handshake.
|
||||
func TestTLSHandshakeSucceedsAgainstTLSServer(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(nil)
|
||||
defer ts.Close()
|
||||
|
||||
host, port := splitHostPortInt(t, ts.Listener.Addr().String())
|
||||
res := TLSHandshake(host, port, time.Second)(context.Background())
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got failure: %s", res.Detail)
|
||||
}
|
||||
wantType := "tls-" + strconv.Itoa(port)
|
||||
if res.CheckType != wantType {
|
||||
t.Fatalf("expected check type %q, got %q", wantType, res.CheckType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLSHandshakeFailsAgainstPlainTCP confirms a bare TCP listener (no TLS
|
||||
// on top) fails the handshake rather than being mistaken for success —
|
||||
// this is exactly the gap a plain TCPConnect check can't catch.
|
||||
func TestTLSHandshakeFailsAgainstPlainTCP(t *testing.T) {
|
||||
ts := httptest.NewServer(nil)
|
||||
defer ts.Close()
|
||||
|
||||
host, port := splitHostPortInt(t, ts.Listener.Addr().String())
|
||||
res := TLSHandshake(host, port, time.Second)(context.Background())
|
||||
if res.Success {
|
||||
t.Fatalf("expected failure against a plain TCP listener, got success")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLSHandshakeFailsOnConnectionRefused confirms a closed port reports
|
||||
// failure rather than hanging or panicking.
|
||||
func TestTLSHandshakeFailsOnConnectionRefused(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
_, port := splitHostPortInt(t, l.Addr().String())
|
||||
l.Close() // close immediately so the port refuses connections
|
||||
|
||||
res := TLSHandshake("127.0.0.1", port, time.Second)(context.Background())
|
||||
if res.Success {
|
||||
t.Fatalf("expected failure against a closed port, got success")
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,7 @@ func TestEndToEndHTTPFlow(t *testing.T) {
|
||||
resp, body = fc.do(http.MethodPost, "/api/v1/probers/"+site+"/results", proberResultsRequest{
|
||||
Results: []proberResultDTO{
|
||||
{IPID: assignments[0].IPID, IPAddress: "1.2.3.4", CheckType: "tcp-22", Success: true, CheckedAt: now},
|
||||
{IPID: assignments[0].IPID, IPAddress: "1.2.3.4", CheckType: "ssh", Success: true, CheckedAt: now},
|
||||
{IPID: assignments[0].IPID, IPAddress: "1.2.3.4", CheckType: "tcp-80", Success: true, CheckedAt: now},
|
||||
{IPID: assignments[0].IPID, IPAddress: "1.2.3.4", CheckType: "icmp", Success: true, CheckedAt: now, Complete: true},
|
||||
},
|
||||
|
||||
@@ -531,6 +531,11 @@ func (o *Orchestrator) expectedCheckCount(ctx context.Context) (int, error) {
|
||||
if inbound.ICMP {
|
||||
inboundPerSite++
|
||||
}
|
||||
for _, p := range inbound.Ports {
|
||||
if p == 443 || p == 22 {
|
||||
inboundPerSite++
|
||||
}
|
||||
}
|
||||
return egress + inboundPerSite*len(sites), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestHappyPath(t *testing.T) {
|
||||
|
||||
// 4. inbound results from all 3 sites
|
||||
for site := 1; site <= 3; site++ {
|
||||
for _, ct := range []string{"tcp-22", "tcp-80", "icmp"} {
|
||||
for _, ct := range []string{"tcp-22", "ssh", "tcp-80", "icmp"} {
|
||||
if err := o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(site), CheckType: ct, Target: ip.IPAddress,
|
||||
@@ -558,7 +558,7 @@ func TestInboundChecksPartialSites(t *testing.T) {
|
||||
t.Fatalf("expected still checking (site-1 pending), got %s", ip.State)
|
||||
}
|
||||
|
||||
for _, ct := range []string{"tcp-22", "tcp-80", "icmp"} {
|
||||
for _, ct := range []string{"tcp-22", "ssh", "tcp-80", "icmp"} {
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(1), CheckType: ct, Target: ip.IPAddress, Success: true, CheckedAt: db.Now(),
|
||||
@@ -585,7 +585,7 @@ func TestExpectedCheckCountReflectsInboundChecksConfigChange(t *testing.T) {
|
||||
sites := []config.SiteConfig{{SiteID: "site-1", Index: 1}}
|
||||
o, d, mock := newTestOrchestratorWithSites(t, 180, sites)
|
||||
// newTestOrchestratorWithSites seeds Inbound: {Ports: [22, 80], ICMP: true}
|
||||
// (3 inbound checks expected per site).
|
||||
// (4 inbound checks expected per site: tcp-22, ssh, tcp-80, icmp).
|
||||
mock.Seed("fip-1", "1.2.3.4", "svc-project")
|
||||
_ = d.RegisterValidator(ctx, "validator-1", "host-1", "port-1", "v0.1")
|
||||
_ = d.SeedQueue(ctx, []string{"1.2.3.4"})
|
||||
@@ -603,16 +603,19 @@ func TestExpectedCheckCountReflectsInboundChecksConfigChange(t *testing.T) {
|
||||
_ = o.MarkEgressComplete(ctx, ip.ID)
|
||||
|
||||
// Shrink the inbound check config to a single TCP port, no ICMP — an
|
||||
// admin API change made between assignment and aggregation.
|
||||
// admin API change made between assignment and aggregation. Port 22
|
||||
// still auto-triggers the extra "ssh" check (see expectedCheckCount).
|
||||
if err := d.SetInboundChecks(ctx, []int{22}, false); err != nil {
|
||||
t.Fatalf("set inbound checks: %v", err)
|
||||
}
|
||||
|
||||
// Report only the one now-expected inbound check.
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(1), CheckType: "tcp-22", Target: ip.IPAddress, Success: true, CheckedAt: db.Now(),
|
||||
})
|
||||
// Report only the two now-expected inbound checks.
|
||||
for _, ct := range []string{"tcp-22", "ssh"} {
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(1), CheckType: ct, Target: ip.IPAddress, Success: true, CheckedAt: db.Now(),
|
||||
})
|
||||
}
|
||||
_ = o.MarkSiteComplete(ctx, ip.ID, 1)
|
||||
|
||||
o.Tick(ctx)
|
||||
@@ -652,7 +655,7 @@ func TestFourSitesAllMustReportBeforeAggregation(t *testing.T) {
|
||||
_ = o.MarkEgressComplete(ctx, ip.ID)
|
||||
|
||||
for _, site := range []int{1, 2, 3} {
|
||||
for _, ct := range []string{"tcp-22", "tcp-80", "icmp"} {
|
||||
for _, ct := range []string{"tcp-22", "ssh", "tcp-80", "icmp"} {
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(site), CheckType: ct, Target: ip.IPAddress, Success: true, CheckedAt: db.Now(),
|
||||
@@ -669,7 +672,7 @@ func TestFourSitesAllMustReportBeforeAggregation(t *testing.T) {
|
||||
t.Fatalf("expected still checking (site-4 pending), got %s", ip.State)
|
||||
}
|
||||
|
||||
for _, ct := range []string{"tcp-22", "tcp-80", "icmp"} {
|
||||
for _, ct := range []string{"tcp-22", "ssh", "tcp-80", "icmp"} {
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(4), CheckType: ct, Target: ip.IPAddress, Success: true, CheckedAt: db.Now(),
|
||||
@@ -686,3 +689,102 @@ func TestFourSitesAllMustReportBeforeAggregation(t *testing.T) {
|
||||
t.Fatalf("expected pass, got %s", ip.OverallResult)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExpectedCheckCountIncludesTLSAndSSHForPorts443And22 confirms
|
||||
// expectedCheckCount counts the auto-triggered tls-443/ssh checks (in
|
||||
// addition to the base tcp-443/tcp-22) once those ports are configured —
|
||||
// reporting exactly that full set must be enough to aggregate as pass.
|
||||
func TestExpectedCheckCountIncludesTLSAndSSHForPorts443And22(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sites := []config.SiteConfig{{SiteID: "site-1", Index: 1}}
|
||||
o, d, mock := newTestOrchestratorWithSites(t, 180, sites)
|
||||
mock.Seed("fip-1", "1.2.3.4", "svc-project")
|
||||
_ = d.RegisterValidator(ctx, "validator-1", "host-1", "port-1", "v0.1")
|
||||
_ = d.SeedQueue(ctx, []string{"1.2.3.4"})
|
||||
|
||||
o.Tick(ctx)
|
||||
ip, _ := d.GetIPByAddress(ctx, "1.2.3.4")
|
||||
_ = o.SelfCheckResult(ctx, "validator-1", ip.ID, true, "ok")
|
||||
ip, _ = d.GetIP(ctx, ip.ID)
|
||||
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
ValidatorID: "validator-1", Source: db.SourceEgress, CheckType: "https",
|
||||
Target: "https://example.test", Success: true, CheckedAt: db.Now(),
|
||||
})
|
||||
_ = o.MarkEgressComplete(ctx, ip.ID)
|
||||
|
||||
if err := d.SetInboundChecks(ctx, []int{22, 443}, false); err != nil {
|
||||
t.Fatalf("set inbound checks: %v", err)
|
||||
}
|
||||
|
||||
for _, ct := range []string{"tcp-22", "ssh", "tcp-443", "tls-443"} {
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(1), CheckType: ct, Target: ip.IPAddress, Success: true, CheckedAt: db.Now(),
|
||||
})
|
||||
}
|
||||
_ = o.MarkSiteComplete(ctx, ip.ID, 1)
|
||||
|
||||
o.Tick(ctx)
|
||||
ip, _ = d.GetIP(ctx, ip.ID)
|
||||
if ip.State != db.IPDone {
|
||||
t.Fatalf("expected done once tcp+tls+ssh all reported, got %s", ip.State)
|
||||
}
|
||||
if ip.OverallResult != db.ResultPass {
|
||||
t.Fatalf("expected pass, got %s", ip.OverallResult)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAggregationWaitsForTLSAndSSHResults confirms that reporting only the
|
||||
// base tcp-22/tcp-443 checks (without the auto-triggered ssh/tls-443
|
||||
// checks) is not enough to aggregate as pass — expectedCheckCount must
|
||||
// actually have grown, not just failed to break.
|
||||
func TestAggregationWaitsForTLSAndSSHResults(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sites := []config.SiteConfig{{SiteID: "site-1", Index: 1}}
|
||||
o, d, mock := newTestOrchestratorWithSites(t, 180, sites)
|
||||
mock.Seed("fip-1", "1.2.3.4", "svc-project")
|
||||
_ = d.RegisterValidator(ctx, "validator-1", "host-1", "port-1", "v0.1")
|
||||
_ = d.SeedQueue(ctx, []string{"1.2.3.4"})
|
||||
|
||||
o.Tick(ctx)
|
||||
ip, _ := d.GetIPByAddress(ctx, "1.2.3.4")
|
||||
_ = o.SelfCheckResult(ctx, "validator-1", ip.ID, true, "ok")
|
||||
ip, _ = d.GetIP(ctx, ip.ID)
|
||||
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
ValidatorID: "validator-1", Source: db.SourceEgress, CheckType: "https",
|
||||
Target: "https://example.test", Success: true, CheckedAt: db.Now(),
|
||||
})
|
||||
_ = o.MarkEgressComplete(ctx, ip.ID)
|
||||
|
||||
if err := d.SetInboundChecks(ctx, []int{22, 443}, false); err != nil {
|
||||
t.Fatalf("set inbound checks: %v", err)
|
||||
}
|
||||
|
||||
// Only the base TCP checks report — ssh/tls-443 never arrive, but the
|
||||
// site still (incorrectly, from the operator's point of view) claims
|
||||
// completion. Force the checking window to have elapsed so aggregation
|
||||
// runs anyway, same as TestPartialResult.
|
||||
for _, ct := range []string{"tcp-22", "tcp-443"} {
|
||||
_ = o.RecordCheck(ctx, db.Check{
|
||||
IPID: ip.ID, IPAddress: ip.IPAddress, AttemptNumber: ip.AttemptNumber,
|
||||
Source: db.InboundSource(1), CheckType: ct, Target: ip.IPAddress, Success: true, CheckedAt: db.Now(),
|
||||
})
|
||||
}
|
||||
_ = o.MarkSiteComplete(ctx, ip.ID, 1)
|
||||
|
||||
o.Cfg.CheckingWindowSeconds = 0
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
o.Tick(ctx)
|
||||
|
||||
ip, _ = d.GetIP(ctx, ip.ID)
|
||||
if ip.State != db.IPDone {
|
||||
t.Fatalf("expected done, got %s", ip.State)
|
||||
}
|
||||
if ip.OverallResult != db.ResultPartial {
|
||||
t.Fatalf("expected partial (missing ssh/tls-443 counted against it), got %s", ip.OverallResult)
|
||||
}
|
||||
}
|
||||
@@ -109,26 +109,46 @@ func (p *Prober) pollOnce(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// extraChecksForPort returns checks to run in addition to the base
|
||||
// TCPConnect for ports where a bare TCP handshake doesn't actually prove
|
||||
// the expected service is behind it: a real TLS handshake on 443, a real
|
||||
// SSH banner exchange on 22. nil for every other port.
|
||||
func extraChecksForPort(host string, port int, timeout time.Duration) []func(ctx context.Context) checkrunner.Result {
|
||||
switch port {
|
||||
case 443:
|
||||
return []func(ctx context.Context) checkrunner.Result{
|
||||
checkrunner.TLSHandshake(host, port, timeout),
|
||||
}
|
||||
case 22:
|
||||
return []func(ctx context.Context) checkrunner.Result{
|
||||
checkrunner.SSHBanner(host, timeout),
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Prober) probeOne(ctx context.Context, a assignment) {
|
||||
tcpTimeout := time.Duration(p.cfg.Checks.TCPTimeoutSeconds) * time.Second
|
||||
icmpTimeout := time.Duration(p.cfg.Checks.ICMPTimeoutSeconds) * time.Second
|
||||
|
||||
var results []resultDTO
|
||||
for _, port := range a.Ports {
|
||||
res := checkrunner.TCPConnect(a.IPAddress, port, tcpTimeout)(ctx)
|
||||
record := func(res checkrunner.Result) {
|
||||
results = append(results, resultDTO{
|
||||
IPID: a.IPID, IPAddress: a.IPAddress, CheckType: res.CheckType,
|
||||
Success: res.Success, LatencyMS: res.LatencyMS, Detail: res.Detail,
|
||||
CheckedAt: res.CheckedAt.Format(time.RFC3339Nano),
|
||||
})
|
||||
}
|
||||
|
||||
for _, port := range a.Ports {
|
||||
record(checkrunner.TCPConnect(a.IPAddress, port, tcpTimeout)(ctx))
|
||||
for _, extra := range extraChecksForPort(a.IPAddress, port, tcpTimeout) {
|
||||
record(extra(ctx))
|
||||
}
|
||||
}
|
||||
if a.ICMP {
|
||||
res := checkrunner.ICMPEcho(a.IPAddress, p.cfg.Checks.ICMPCount, icmpTimeout)(ctx)
|
||||
results = append(results, resultDTO{
|
||||
IPID: a.IPID, IPAddress: a.IPAddress, CheckType: res.CheckType,
|
||||
Success: res.Success, LatencyMS: res.LatencyMS, Detail: res.Detail,
|
||||
CheckedAt: res.CheckedAt.Format(time.RFC3339Nano),
|
||||
})
|
||||
record(checkrunner.ICMPEcho(a.IPAddress, p.cfg.Checks.ICMPCount, icmpTimeout)(ctx))
|
||||
}
|
||||
if len(results) > 0 {
|
||||
results[len(results)-1].Complete = true
|
||||
|
||||
@@ -94,3 +94,36 @@ func TestPollOnceSkipsAssignmentsWhenHeartbeatFails(t *testing.T) {
|
||||
t.Fatalf("expected only [heartbeat] (assignments skipped on heartbeat failure), got %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtraChecksForPort confirms only ports 443 (TLS) and 22 (SSH) get an
|
||||
// extra protocol check beyond the baseline TCPConnect every port gets.
|
||||
func TestExtraChecksForPort(t *testing.T) {
|
||||
// Use a host nothing is listening on so the extra check's own network
|
||||
// call fails fast — only its CheckType is asserted here, the
|
||||
// port->check mapping, not real connectivity.
|
||||
closedHost := "127.0.0.1"
|
||||
|
||||
cases := []struct {
|
||||
port int
|
||||
wantLen int
|
||||
wantCheckType string
|
||||
}{
|
||||
{port: 443, wantLen: 1, wantCheckType: "tls-443"},
|
||||
{port: 22, wantLen: 1, wantCheckType: "ssh"},
|
||||
{port: 80, wantLen: 0},
|
||||
{port: 8080, wantLen: 0},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
extra := extraChecksForPort(closedHost, tc.port, 100*time.Millisecond)
|
||||
if len(extra) != tc.wantLen {
|
||||
t.Fatalf("port %d: expected %d extra checks, got %d", tc.port, tc.wantLen, len(extra))
|
||||
}
|
||||
if tc.wantLen == 0 {
|
||||
continue
|
||||
}
|
||||
res := extra[0](context.Background())
|
||||
if res.CheckType != tc.wantCheckType {
|
||||
t.Fatalf("port %d: expected check type %q, got %q", tc.port, tc.wantCheckType, res.CheckType)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user