package checker import ( "fmt" sdk "git.happydns.org/checker-sdk-go/checker" tlsct "git.happydns.org/checker-tls/contract" ) // 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 in // the TLS endpoint contract: the upgrade protocol name (which feeds // tlsct.TLSEndpoint.STARTTLS) and whether the protocol treats STARTTLS as // mandatory. type starttlsSpec struct { // Proto is the STARTTLS upgrade protocol name. It follows the SRV // service-name convention agreed with the TLS checker (see // checker-tls/contract) — e.g. "smtp", "imap", "xmpp-client". When two // SRV services share the same wire upgrade (submission/smtp both do // ESMTP STARTTLS), Proto is the canonical one. Proto string // Required is true when STARTTLS is mandatory per spec; false when // opportunistic (e.g. SMTP on port 25, s2s XMPP). It is mapped to // TLSEndpoint.RequireSTARTTLS so the consumer can pick the right // severity when the server does not advertise STARTTLS. Required bool } // starttlsServices enumerates SRV service names that speak plaintext on // connect and then negotiate TLS via a protocol-specific STARTTLS handshake. // // The Proto follows the tls.endpoint.v1 contract's vocabulary; the SDK // itself has no opinion on these values, they belong to checker-tls. var starttlsServices = map[string]starttlsSpec{ "submission": {"smtp", true}, // RFC 8314: STARTTLS required "smtp": {"smtp", false}, // port 25: opportunistic "imap": {"imap", true}, "pop3": {"pop3", true}, "xmpp-client": {"xmpp-client", true}, // RFC 7590 "xmpp-server": {"xmpp-server", false}, // s2s: opportunistic "ldap": {"ldap", false}, "nntp": {"nntp", false}, "ftp": {"ftp", false}, "sieve": {"sieve", true}, "postgresql": {"postgres", false}, } // DiscoverEntries is invoked right after Collect. It publishes // DiscoveryEntry records carrying the TLS endpoint contract // (tls.endpoint.v1) for: // // - direct-TLS services whose SRV service name is listed in // directTLSServices (e.g. _ldaps, _sips, _https), // - STARTTLS-capable services whose SRV service name is listed in // starttlsServices (e.g. _submission, _imap, _xmpp-client). // // Unknown service names produce no entries: we lean on the SRV naming // convention rather than guessing from the port, since a port alone // conveys no protocol semantics. func (p *srvProvider) DiscoverEntries(data any) ([]sdk.DiscoveryEntry, error) { d, ok := data.(*SRVData) if !ok { return nil, fmt.Errorf("unexpected data type %T", data) } var out []sdk.DiscoveryEntry for _, r := range d.Records { if r.IsNullTarget || r.Target == "" { continue } if directTLSServices[r.Service] { e, err := tlsct.NewEntry(tlsct.TLSEndpoint{ Host: r.Target, Port: r.Port, SNI: r.Target, }) if err != nil { return nil, fmt.Errorf("build tls entry for %s:%d: %w", r.Target, r.Port, err) } out = append(out, e) continue } if spec, ok := starttlsServices[r.Service]; ok { e, err := tlsct.NewEntry(tlsct.TLSEndpoint{ Host: r.Target, Port: r.Port, SNI: r.Target, STARTTLS: spec.Proto, RequireSTARTTLS: spec.Required, }) if err != nil { return nil, fmt.Errorf("build starttls entry for %s:%d: %w", r.Target, r.Port, err) } out = append(out, e) } } return out, nil }