checker: implement ShareKey to mutualise SSH probes across targets
An SSH probe (reachability, banner, KEX/host-key algorithm posture, host keys) depends only on the set of addresses and ports dialed and the probe knobs, never on which domain name points at the server: SSH has no SNI, so the same daemon answers identically behind every name. Implement sdk.ObservationSharer so the host can probe an address set once and serve every target (of the same user) that points at it, instead of re-connecting per record. The share key sorts the resolved addresses and ports and folds in the probe timeout, the auth-probe flag, and the declared SSHFP fingerprints — the latter live in the observation and drive the SSHFP-match rule, so two services with the same endpoints but different SSHFP must not share a verdict. The host/Domain label is intentionally excluded, mirroring the ping checker's exclusion of which domain the addresses belong to: it does not change reachability, the negotiated algorithms, the host keys, or the SSHFP comparison. Inputs with no probable address yield "" so the host falls back to per-target caching.
This commit is contained in:
parent
16c82bbe16
commit
258d799a97
2 changed files with 188 additions and 0 deletions
115
checker/sharekey_test.go
Normal file
115
checker/sharekey_test.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
happydns "git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services/abstract"
|
||||
)
|
||||
|
||||
func serverOpts(t *testing.T, domain string, srv abstract.Server, extra map[string]any) sdk.CheckerOptions {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(srv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
opts := sdk.CheckerOptions{
|
||||
OptionService: happydns.ServiceMessage{
|
||||
ServiceMeta: happydns.ServiceMeta{
|
||||
Type: "abstract.Server",
|
||||
Domain: domain,
|
||||
},
|
||||
Service: raw,
|
||||
},
|
||||
OptionDomainName: "example.org",
|
||||
}
|
||||
for k, v := range extra {
|
||||
opts[k] = v
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func TestShareKey_StableAndPrefixed(t *testing.T) {
|
||||
p := &sshProvider{}
|
||||
srv := abstract.Server{A: &dns.A{A: net.ParseIP("192.0.2.1").To4()}}
|
||||
|
||||
a, err := p.ShareKey(serverOpts(t, "host", srv, nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, _ := p.ShareKey(serverOpts(t, "host", srv, nil))
|
||||
if a != b {
|
||||
t.Fatalf("non-deterministic key: %q vs %q", a, b)
|
||||
}
|
||||
if !strings.HasPrefix(a, "ssh:") {
|
||||
t.Fatalf("missing prefix: %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
// The host/Domain label must not change the key: the same server answers
|
||||
// identically behind every name (SSH has no SNI).
|
||||
func TestShareKey_IgnoresDomainLabel(t *testing.T) {
|
||||
p := &sshProvider{}
|
||||
srv := abstract.Server{A: &dns.A{A: net.ParseIP("192.0.2.1").To4()}}
|
||||
|
||||
a, _ := p.ShareKey(serverOpts(t, "alpha", srv, nil))
|
||||
b, _ := p.ShareKey(serverOpts(t, "beta", srv, nil))
|
||||
if a != b {
|
||||
t.Fatalf("domain label must not affect the key: %q vs %q", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShareKey_DiffersByAddress(t *testing.T) {
|
||||
p := &sshProvider{}
|
||||
a, _ := p.ShareKey(serverOpts(t, "host", abstract.Server{A: &dns.A{A: net.ParseIP("192.0.2.1").To4()}}, nil))
|
||||
b, _ := p.ShareKey(serverOpts(t, "host", abstract.Server{A: &dns.A{A: net.ParseIP("192.0.2.2").To4()}}, nil))
|
||||
if a == b {
|
||||
t.Fatalf("different addresses must yield different keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShareKey_DiffersByPortsAndKnobs(t *testing.T) {
|
||||
p := &sshProvider{}
|
||||
srv := abstract.Server{A: &dns.A{A: net.ParseIP("192.0.2.1").To4()}}
|
||||
|
||||
def, _ := p.ShareKey(serverOpts(t, "host", srv, nil))
|
||||
ports, _ := p.ShareKey(serverOpts(t, "host", srv, map[string]any{OptionPorts: "22,2222"}))
|
||||
auth, _ := p.ShareKey(serverOpts(t, "host", srv, map[string]any{OptionIncludeAuthProbe: false}))
|
||||
timeout, _ := p.ShareKey(serverOpts(t, "host", srv, map[string]any{OptionProbeTimeoutMs: float64(1000)}))
|
||||
|
||||
for name, got := range map[string]string{"ports": ports, "authProbe": auth, "timeout": timeout} {
|
||||
if got == def {
|
||||
t.Fatalf("%s must affect the key", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SSHFP fingerprints live in the observation and drive the SSHFP-match rule, so
|
||||
// they must be part of the key.
|
||||
func TestShareKey_DiffersBySSHFP(t *testing.T) {
|
||||
p := &sshProvider{}
|
||||
bare := abstract.Server{A: &dns.A{A: net.ParseIP("192.0.2.1").To4()}}
|
||||
withFP := abstract.Server{
|
||||
A: &dns.A{A: net.ParseIP("192.0.2.1").To4()},
|
||||
SSHFP: []*dns.SSHFP{{Algorithm: 4, Type: 2, FingerPrint: "abcdef"}},
|
||||
}
|
||||
|
||||
a, _ := p.ShareKey(serverOpts(t, "host", bare, nil))
|
||||
b, _ := p.ShareKey(serverOpts(t, "host", withFP, nil))
|
||||
if a == b {
|
||||
t.Fatalf("declared SSHFP must affect the key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShareKey_EmptyWhenNoAddress(t *testing.T) {
|
||||
p := &sshProvider{}
|
||||
if sk, err := p.ShareKey(sdk.CheckerOptions{}); err != nil || sk != "" {
|
||||
t.Fatalf("expected empty key, got %q (err=%v)", sk, err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue