checker: implement ShareKey to mutualise TLS probes across targets
A TLS probe result depends only on the set of endpoints actually dialed and the probe knobs, never on which domain or service published them: the observation is a map keyed by each endpoint's contract Ref (host|port|effective SNI|STARTTLS|require). Implement sdk.ObservationSharer so the host dials a host:port once and serves every target that resolves to the same endpoint set, instead of re-handshaking per record. This is the highest-value case among the checkers, since dane, xmpp, srv, dav, … all funnel their endpoints into this single checker. The share key sorts the endpoint Refs and folds in the probe timeout and the cipher-enumeration flag, since both change what is collected (a tighter timeout can fail a slow handshake; enumeration adds the Enum block). An empty or unparseable entry set yields "" so the host falls back to per-target caching.
This commit is contained in:
parent
7c2f4bfbb5
commit
03af5615ae
4 changed files with 131 additions and 4 deletions
|
|
@ -2,8 +2,12 @@ package checker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -83,3 +87,43 @@ dispatch:
|
||||||
CollectedAt: time.Now(),
|
CollectedAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShareKey implements sdk.ObservationSharer. A TLS probe result depends only on
|
||||||
|
// the set of endpoints actually dialed and the probe knobs, never on which
|
||||||
|
// domain or service published them: the observation is a map keyed by each
|
||||||
|
// endpoint's contract Ref (host|port|effective SNI|STARTTLS|require), so two
|
||||||
|
// targets that resolve to the same endpoint set produce identical probes. This
|
||||||
|
// lets the host dial a host:port once and serve every target that points at it
|
||||||
|
// instead of re-handshaking per record — the highest-value case here, since
|
||||||
|
// dane, xmpp, srv, dav, … all funnel endpoints into this single checker.
|
||||||
|
//
|
||||||
|
// The probe timeout and cipher-enumeration flag are folded in because they
|
||||||
|
// change what is collected (a tighter timeout can fail a slow handshake;
|
||||||
|
// enumeration adds the Enum block). Inputs that yield no probable endpoint
|
||||||
|
// return "" so the host falls back to the default per-target caching.
|
||||||
|
func (p *tlsProvider) ShareKey(opts sdk.CheckerOptions) (string, error) {
|
||||||
|
raw, ok := sdk.GetOption[[]sdk.DiscoveryEntry](opts, OptionEndpoints)
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, _ := contract.ParseEntries(raw)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
refs := make([]string, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
refs = append(refs, e.Ref)
|
||||||
|
}
|
||||||
|
sort.Strings(refs)
|
||||||
|
|
||||||
|
timeoutMs := sdk.GetIntOption(opts, OptionProbeTimeoutMs, DefaultProbeTimeoutMs)
|
||||||
|
if timeoutMs <= 0 {
|
||||||
|
timeoutMs = DefaultProbeTimeoutMs
|
||||||
|
}
|
||||||
|
enumerate := sdk.GetBoolOption(opts, OptionEnumerateCiphers, false)
|
||||||
|
|
||||||
|
h := sha256.Sum256(fmt.Appendf(nil, "%d|%t|%s", timeoutMs, enumerate, strings.Join(refs, ",")))
|
||||||
|
return "tls:" + hex.EncodeToString(h[:8]), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
81
checker/sharekey_test.go
Normal file
81
checker/sharekey_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
"git.happydns.org/checker-tls/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustEntry(t *testing.T, ep contract.TLSEndpoint) sdk.DiscoveryEntry {
|
||||||
|
t.Helper()
|
||||||
|
e, err := contract.NewEntry(ep)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewEntry(%v): %v", ep, err)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShareKey_StableRegardlessOfOrder(t *testing.T) {
|
||||||
|
p := &tlsProvider{}
|
||||||
|
e1 := mustEntry(t, contract.TLSEndpoint{Host: "a.example.net", Port: 443})
|
||||||
|
e2 := mustEntry(t, contract.TLSEndpoint{Host: "b.example.net", Port: 25, STARTTLS: "smtp"})
|
||||||
|
|
||||||
|
a, err := p.ShareKey(sdk.CheckerOptions{OptionEndpoints: []sdk.DiscoveryEntry{e1, e2}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, _ := p.ShareKey(sdk.CheckerOptions{OptionEndpoints: []sdk.DiscoveryEntry{e2, e1}})
|
||||||
|
if a != b {
|
||||||
|
t.Fatalf("key must be order-independent: %q vs %q", a, b)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(a, "tls:") {
|
||||||
|
t.Fatalf("missing prefix: %q", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShareKey_DiffersByEndpointSet(t *testing.T) {
|
||||||
|
p := &tlsProvider{}
|
||||||
|
e1 := mustEntry(t, contract.TLSEndpoint{Host: "a.example.net", Port: 443})
|
||||||
|
e2 := mustEntry(t, contract.TLSEndpoint{Host: "a.example.net", Port: 8443})
|
||||||
|
|
||||||
|
a, _ := p.ShareKey(sdk.CheckerOptions{OptionEndpoints: []sdk.DiscoveryEntry{e1}})
|
||||||
|
b, _ := p.ShareKey(sdk.CheckerOptions{OptionEndpoints: []sdk.DiscoveryEntry{e2}})
|
||||||
|
if a == b {
|
||||||
|
t.Fatalf("different endpoints must yield different keys")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShareKey_DiffersByProbeKnobs(t *testing.T) {
|
||||||
|
p := &tlsProvider{}
|
||||||
|
e1 := mustEntry(t, contract.TLSEndpoint{Host: "a.example.net", Port: 443})
|
||||||
|
base := func(extra map[string]any) sdk.CheckerOptions {
|
||||||
|
o := sdk.CheckerOptions{OptionEndpoints: []sdk.DiscoveryEntry{e1}}
|
||||||
|
for k, v := range extra {
|
||||||
|
o[k] = v
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
def, _ := p.ShareKey(base(nil))
|
||||||
|
timeout, _ := p.ShareKey(base(map[string]any{OptionProbeTimeoutMs: float64(1000)}))
|
||||||
|
enum, _ := p.ShareKey(base(map[string]any{OptionEnumerateCiphers: true}))
|
||||||
|
|
||||||
|
if def == timeout {
|
||||||
|
t.Fatalf("probe timeout must affect the key")
|
||||||
|
}
|
||||||
|
if def == enum {
|
||||||
|
t.Fatalf("cipher enumeration must affect the key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShareKey_EmptyWhenNoEndpoints(t *testing.T) {
|
||||||
|
p := &tlsProvider{}
|
||||||
|
if sk, err := p.ShareKey(sdk.CheckerOptions{}); err != nil || sk != "" {
|
||||||
|
t.Fatalf("expected empty key, got %q (err=%v)", sk, err)
|
||||||
|
}
|
||||||
|
if sk, _ := p.ShareKey(sdk.CheckerOptions{OptionEndpoints: []sdk.DiscoveryEntry{}}); sk != "" {
|
||||||
|
t.Fatalf("expected empty key for empty entry set, got %q", sk)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
go.mod
6
go.mod
|
|
@ -2,12 +2,14 @@ module git.happydns.org/checker-tls
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require git.happydns.org/checker-sdk-go v1.5.0
|
require (
|
||||||
|
git.happydns.org/checker-sdk-go v1.11.0
|
||||||
|
github.com/refraction-networking/utls v1.8.2
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
github.com/klauspost/compress v1.17.4 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,5 +1,5 @@
|
||||||
git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY=
|
git.happydns.org/checker-sdk-go v1.11.0 h1:+hs8OpcgvRMAoWyfqxih/Q0KYYUI1R8oFxO4RKioMLk=
|
||||||
git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
|
git.happydns.org/checker-sdk-go v1.11.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
|
||||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue