diff --git a/checker/discover.go b/checker/discover.go new file mode 100644 index 0000000..b7b8a5d --- /dev/null +++ b/checker/discover.go @@ -0,0 +1,120 @@ +package checker + +import ( + "fmt" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// directTLSServices enumerates SRV service names (the "service" part of +// _service._proto.domain) that by convention mean "direct TLS on connect", +// as opposed to STARTTLS or plaintext. +// +// Matching on the service name is more authoritative than matching on the +// port: port 636 could carry anything, but _ldaps._tcp unambiguously +// designates LDAP over TLS — even on a non-standard port. +var directTLSServices = map[string]bool{ + "https": true, + "ftps": true, // FTPS implicit + "smtps": true, // SMTP over TLS (legacy port 465 semantics) + "submissions": true, // RFC 8314: SMTP submission over TLS + "imaps": true, + "pop3s": true, + "nntps": true, + "ircs": true, + "telnets": true, + "ldaps": true, + "sips": true, + "ipps": true, // IPP over TLS (printing) + "xmpps-client": true, // XMPP client over direct TLS + "xmpps-server": true, // XMPP server-to-server over direct TLS + "mqtts": true, + "coaps": true, + "stuns": true, + "turns": true, +} + +// starttlsSpec describes how to surface a STARTTLS-capable SRV service as +// a DiscoveredEndpoint: the endpoint type (which carries the protocol +// family in its suffix) and whether the protocol historically treats +// STARTTLS as mandatory or opportunistic. +type starttlsSpec struct { + Type string + Opportunistic bool +} + +// starttlsServices enumerates SRV service names that speak plaintext on +// connect and then negotiate TLS via a protocol-specific STARTTLS handshake. +// +// The Type follows the "starttls-" convention agreed with the +// future TLS checker (the consumer); the SDK itself has no opinion on +// these values. The suffix mirrors the SRV service-name vocabulary so +// producers and consumers stay naturally aligned. +// +// The Opportunistic flag is exposed to consumers as Meta["starttls"] = +// "required" | "opportunistic" so rules can pick an appropriate severity +// when the server does not advertise STARTTLS. +var starttlsServices = map[string]starttlsSpec{ + "submission": {"starttls-smtp", false}, // RFC 8314: STARTTLS required + "smtp": {"starttls-smtp", true}, // port 25: opportunistic + "imap": {"starttls-imap", false}, + "pop3": {"starttls-pop3", false}, + "xmpp-client": {"starttls-xmpp-client", false}, // RFC 7590 + "xmpp-server": {"starttls-xmpp-server", true}, // s2s: opportunistic + "ldap": {"starttls-ldap", true}, + "nntp": {"starttls-nntp", true}, + "ftp": {"starttls-ftp", true}, + "sieve": {"starttls-sieve", false}, + "postgresql": {"starttls-postgres", true}, +} + +// DiscoverEndpoints is invoked right after Collect. It declares (host, port) +// pairs worth testing by other checkers: +// +// - direct-TLS endpoints (Type="tls") whose SRV service name is listed in +// directTLSServices (e.g. _ldaps, _sips, _https), +// - STARTTLS-capable endpoints (Type="starttls-") whose SRV service +// name is listed in starttlsServices (e.g. _submission, _imap, _xmpp-client). +// +// Unknown service names produce no endpoints: we lean on the SRV naming +// convention rather than guessing from the port, since a port alone +// conveys no protocol semantics. +func (p *srvProvider) DiscoverEndpoints(data any) ([]sdk.DiscoveredEndpoint, error) { + d, ok := data.(*SRVData) + if !ok { + return nil, fmt.Errorf("unexpected data type %T", data) + } + var out []sdk.DiscoveredEndpoint + for _, r := range d.Records { + if r.IsNullTarget || r.Target == "" { + continue + } + + if directTLSServices[r.Service] { + out = append(out, sdk.DiscoveredEndpoint{ + Type: "tls", + Host: r.Target, + Port: r.Port, + SNI: r.Target, + }) + continue + } + + if spec, ok := starttlsServices[r.Service]; ok { + policy := "required" + if spec.Opportunistic { + policy = "opportunistic" + } + out = append(out, sdk.DiscoveredEndpoint{ + Type: spec.Type, + Host: r.Target, + Port: r.Port, + SNI: r.Target, + Meta: map[string]any{ + "starttls": policy, + }, + }) + } + } + return out, nil +} diff --git a/checker/provider.go b/checker/provider.go index 8fed05f..da4df90 100644 --- a/checker/provider.go +++ b/checker/provider.go @@ -1,8 +1,6 @@ package checker import ( - "fmt" - sdk "git.happydns.org/checker-sdk-go/checker" ) @@ -19,54 +17,3 @@ func (p *srvProvider) Key() sdk.ObservationKey { func (p *srvProvider) Definition() *sdk.CheckerDefinition { return Definition() } - -// directTLSPorts lists TCP ports where clients speak TLS immediately upon -// connection (as opposed to STARTTLS upgrades). A dedicated TLS checker -// consumes these endpoints to validate certificates. -var directTLSPorts = map[uint16]bool{ - 443: true, // HTTPS - 465: true, // SMTPS - 563: true, // NNTPS - 636: true, // LDAPS - 853: true, // DoT - 989: true, // FTPS data - 990: true, // FTPS control - 992: true, // Telnet/TLS - 993: true, // IMAPS - 995: true, // POP3S - 5061: true, // SIPS - 5223: true, // XMPP client TLS - 5349: true, // STUN/TURN/TLS - 6697: true, // IRCS - 8443: true, // HTTPS alt -} - -// DiscoverEndpoints is invoked by the host right after Collect. It declares -// (host, port) pairs worth testing by other checkers — here: TLS endpoints -// whose SRV target points at a well-known direct-TLS port. -// -// STARTTLS SRVs (e.g. _xmpp-server._tcp on 5269, _sips._tcp notwithstanding) -// are intentionally not emitted yet: a dedicated "smtp-starttls" / "xmpp-starttls" -// endpoint type will be defined when the TLS checker grows that capability. -func (p *srvProvider) DiscoverEndpoints(data any) ([]sdk.DiscoveredEndpoint, error) { - d, ok := data.(*SRVData) - if !ok { - return nil, fmt.Errorf("unexpected data type %T", data) - } - var out []sdk.DiscoveredEndpoint - for _, r := range d.Records { - if r.IsNullTarget || r.Target == "" { - continue - } - if !directTLSPorts[r.Port] { - continue - } - out = append(out, sdk.DiscoveredEndpoint{ - Type: "tls", - Host: r.Target, - Port: r.Port, - SNI: r.Target, - }) - } - return out, nil -}