From 0fd9b26266b6c42fbe26b91afebcdb48e3b598d9 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 09:55:15 +0100 Subject: [PATCH 01/10] SPF is now a dedicated service --- services/abstract/email.go | 25 +---------- services/spf.go | 85 +++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/services/abstract/email.go b/services/abstract/email.go index 385e989a..3d2168b4 100644 --- a/services/abstract/email.go +++ b/services/abstract/email.go @@ -116,10 +116,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Rec } if s.SPF != nil { - rc := utils.NewRecordConfig(domain, "TXT", ttl, origin) - rc.SetTargetTXT(s.SPF.String()) - - rrs = append(rrs, rc) + rrs = append(rrs, s.SPF.GenRRs(domain, ttl, origin)...) } for selector, d := range s.DKIM { @@ -184,25 +181,6 @@ func email_analyze(a *svcs.Analyzer) (err error) { } for domain, service := range services { - // Is there SPF record? - for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, Domain: domain, Contains: "v=spf1"}) { - if record.Type == "TXT" || record.Type == "SPF" { - if service.SPF == nil { - service.SPF = &svcs.SPF{} - } - - err = service.SPF.Analyze(record.GetTargetTXTJoined()) - if err != nil { - return - } - } - - err = a.UseRR(record, domain, service) - if err != nil { - return - } - } - service.DKIM = map[string]*svcs.DKIM{} // Is there DKIM record? for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, SubdomainsOf: "_domainkey." + domain}) { @@ -309,7 +287,6 @@ func init() { }, RecordTypes: []uint16{ dns.TypeMX, - dns.TypeSPF, }, Tabs: true, Restrictions: svcs.ServiceRestrictions{ diff --git a/services/spf.go b/services/spf.go index 64657ea9..c05f853c 100644 --- a/services/spf.go +++ b/services/spf.go @@ -25,7 +25,13 @@ import ( "fmt" "strings" + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + "github.com/StackExchange/dnscontrol/v4/pkg/spflib" + + "git.happydns.org/happyDomain/model" + "git.happydns.org/happyDomain/utils" ) type SPF struct { @@ -33,6 +39,23 @@ type SPF struct { Directives []string `json:"directives" happydomain:"label=Directives,placeholder=ip4:203.0.113.12"` } +func (s *SPF) GetNbResources() int { + return 1 +} + +func (s *SPF) GenComment(origin string) string { + return fmt.Sprintf("%d directives", len(s.Directives)) +} + +func (s *SPF) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + rc := utils.NewRecordConfig(domain, "TXT", ttl, origin) + rc.SetTargetTXT(s.String()) + + rrs = append(rrs, rc) + + return +} + func (t *SPF) Analyze(txt string) error { _, err := spflib.Parse(txt, nil) if err != nil { @@ -61,7 +84,65 @@ func (t *SPF) Analyze(txt string) error { return nil } -func (t *SPF) String() string { - directives := append([]string{fmt.Sprintf("v=spf%d", t.Version)}, t.Directives...) +func spf_analyze(a *Analyzer) (err error) { + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeTXT, Contains: "v=spf1"}) { + service := &SPF{} + + err = service.Analyze(record.GetTargetTXTJoined()) + if err != nil { + return + } + + err = a.UseRR(record, record.NameFQDN, service) + if err != nil { + return + } + } + + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeSPF, Contains: "v=spf1"}) { + service := &SPF{} + + err = service.Analyze(record.GetTargetTXTJoined()) + if err != nil { + return + } + + err = a.UseRR(record, record.NameFQDN, service) + if err != nil { + return + } + } + + return +} + +func (s *SPF) String() string { + directives := append([]string{fmt.Sprintf("v=spf%d", s.Version)}, s.Directives...) return strings.Join(directives, " ") } + +func init() { + RegisterService( + func() happydns.Service { + return &SPF{} + }, + spf_analyze, + ServiceInfos{ + Name: "SPF", + Description: "Sender Policy Framework, to authenticate domain name on email sending.", + Categories: []string{ + "email", + }, + RecordTypes: []uint16{ + dns.TypeTXT, + dns.TypeSPF, + }, + Restrictions: ServiceRestrictions{ + NeedTypes: []uint16{ + dns.TypeTXT, + }, + }, + }, + 1, + ) +} From 56054b23e3ad3af5b535a682afdfb1664c85eeb1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 10:13:56 +0100 Subject: [PATCH 02/10] DKIM is now a dedicated service --- services/abstract/email.go | 30 ++------------- services/dkim.go | 79 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/services/abstract/email.go b/services/abstract/email.go index 3d2168b4..94eda502 100644 --- a/services/abstract/email.go +++ b/services/abstract/email.go @@ -120,10 +120,10 @@ func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Rec } for selector, d := range s.DKIM { - rc := utils.NewRecordConfig(utils.DomainJoin(selector+"._domainkey", domain), "TXT", ttl, origin) - rc.SetTargetTXT(d.String()) - - rrs = append(rrs, rc) + rrs = append(rrs, (&svcs.DKIMRecord{ + DKIM: *d, + Selector: selector, + }).GenRRs(domain, ttl, origin)...) } if s.DMARC != nil { @@ -181,28 +181,6 @@ func email_analyze(a *svcs.Analyzer) (err error) { } for domain, service := range services { - service.DKIM = map[string]*svcs.DKIM{} - // Is there DKIM record? - for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, SubdomainsOf: "_domainkey." + domain}) { - selector := strings.TrimSuffix(record.NameFQDN, "._domainkey."+domain) - - if _, ok := service.DKIM[selector]; !ok { - service.DKIM[selector] = &svcs.DKIM{} - } - - if record.Type == "TXT" { - err = service.DKIM[selector].Analyze(record.GetTargetTXTJoined()) - if err != nil { - return - } - } - - err = a.UseRR(record, domain, service) - if err != nil { - return - } - } - // Is there DMARC record? for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, Domain: "_dmarc." + domain}) { if service.DMARC == nil { diff --git a/services/dkim.go b/services/dkim.go index 3e893e35..848a250a 100644 --- a/services/dkim.go +++ b/services/dkim.go @@ -26,6 +26,12 @@ import ( "fmt" "strconv" "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + + "git.happydns.org/happyDomain/model" + "git.happydns.org/happyDomain/utils" ) type DKIM struct { @@ -111,3 +117,76 @@ func (t *DKIM) String() string { return strings.Join(fields, ";") } + +type DKIMRecord struct { + Selector string `json:"selector" happydomain:"label=Selector,placeholder=reykjavik,required,description=Name of the key"` + DKIM +} + +func (s *DKIMRecord) GetNbResources() int { + return 1 +} + +func (s *DKIMRecord) GenComment(origin string) string { + return s.Selector +} + +func (s *DKIMRecord) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + rc := utils.NewRecordConfig(utils.DomainJoin(s.Selector+"._domainkey", domain), "TXT", ttl, origin) + rc.SetTargetTXT(s.String()) + + rrs = append(rrs, rc) + + return +} + +func dkim_analyze(a *Analyzer) (err error) { + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeTXT}) { + dkidx := strings.Index(record.NameFQDN, "._domainkey.") + if dkidx <= 0 { + continue + } + + service := &DKIMRecord{ + Selector: record.NameFQDN[:dkidx], + } + + err = service.Analyze(record.GetTargetTXTJoined()) + if err != nil { + return + } + + err = a.UseRR(record, record.NameFQDN[dkidx+12:], service) + if err != nil { + return + } + } + + return +} + +func init() { + RegisterService( + func() happydns.Service { + return &DKIMRecord{} + }, + dkim_analyze, + ServiceInfos{ + Name: "DKIM", + Description: "DomainKeys Identified Mail, authenticate outgoing emails.", + Categories: []string{ + "email", + }, + RecordTypes: []uint16{ + dns.TypeTXT, + }, + Restrictions: ServiceRestrictions{ + NearAlone: true, + NeedTypes: []uint16{ + dns.TypeTXT, + }, + }, + }, + 1, + ) +} From d69e1992d856171d01aff455899d3f49e71787ec Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 10:27:28 +0100 Subject: [PATCH 03/10] DMARC is now a dedicated service --- services/abstract/email.go | 24 +---------- services/dmarc.go | 88 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/services/abstract/email.go b/services/abstract/email.go index 94eda502..87033615 100644 --- a/services/abstract/email.go +++ b/services/abstract/email.go @@ -127,10 +127,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Rec } if s.DMARC != nil { - rc := utils.NewRecordConfig(utils.DomainJoin("_dmarc", domain), "TXT", ttl, origin) - rc.SetTargetTXT(s.DMARC.String()) - - rrs = append(rrs, rc) + rrs = append(rrs, s.DMARC.GenRRs(domain, ttl, origin)...) } if s.MTA_STS != nil { @@ -181,25 +178,6 @@ func email_analyze(a *svcs.Analyzer) (err error) { } for domain, service := range services { - // Is there DMARC record? - for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, Domain: "_dmarc." + domain}) { - if service.DMARC == nil { - service.DMARC = &svcs.DMARC{} - } - - if record.Type == "TXT" { - err = service.DMARC.Analyze(record.GetTargetTXTJoined()) - if err != nil { - return - } - } - - err = a.UseRR(record, domain, service) - if err != nil { - return - } - } - // Is there MTA-STS record? for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, Domain: "_mta-sts." + domain}) { if service.MTA_STS == nil { diff --git a/services/dmarc.go b/services/dmarc.go index 78b6873e..46619f13 100644 --- a/services/dmarc.go +++ b/services/dmarc.go @@ -26,7 +26,12 @@ import ( "strconv" "strings" + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + + "git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/services/common" + "git.happydns.org/happyDomain/utils" ) type DMARC struct { @@ -43,6 +48,45 @@ type DMARC struct { Percent uint8 `json:"pct" happydomain:"label=Policy applies on,description=Percentage of messages to which the DMARC policy is to be applied.,unit=%"` } +func (t *DMARC) GetNbResources() int { + return 1 +} + +func (t *DMARC) GenComment(origin string) string { + var b strings.Builder + + if t.ADKIM && t.ASPF { + b.WriteString("strict ") + } else if t.ADKIM { + b.WriteString("SPF relaxed ") + } else if t.ASPF { + b.WriteString("DKIM relaxed ") + } else { + b.WriteString("relaxed ") + } + + if t.Request != "" { + b.WriteString(t.Request) + b.WriteString(" ") + } + + if t.Percent < 100 { + b.WriteString(strconv.Itoa(int(t.Percent))) + b.WriteString(" %") + } + + return b.String() +} + +func (t *DMARC) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + rc := utils.NewRecordConfig(utils.DomainJoin("_dmarc", domain), "TXT", ttl, origin) + rc.SetTargetTXT(t.String()) + + rrs = append(rrs, rc) + + return +} + func analyseFields(txt string) map[string]string { ret := map[string]string{} @@ -167,3 +211,47 @@ func (t *DMARC) String() string { return strings.Join(fields, ";") } + +func dmarc_analyze(a *Analyzer) (err error) { + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeTXT, Prefix: "_dmarc"}) { + service := &DMARC{} + + err = service.Analyze(record.GetTargetTXTJoined()) + if err != nil { + return + } + + err = a.UseRR(record, strings.TrimPrefix(record.NameFQDN, "_dmarc."), service) + if err != nil { + return + } + } + + return +} + +func init() { + RegisterService( + func() happydns.Service { + return &DMARC{} + }, + dmarc_analyze, + ServiceInfos{ + Name: "DMARC", + Description: "Domain-based Message Authentication, Reporting and Conformance.", + Categories: []string{ + "email", + }, + RecordTypes: []uint16{ + dns.TypeTXT, + }, + Restrictions: ServiceRestrictions{ + NearAlone: true, + NeedTypes: []uint16{ + dns.TypeTXT, + }, + }, + }, + 1, + ) +} From 806297316978bb9b959dddaa01ca4861ded62e10 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 10:33:14 +0100 Subject: [PATCH 04/10] MTA-STS is now a dedicated service --- services/abstract/email.go | 24 +------------ services/mta-sts.go | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/services/abstract/email.go b/services/abstract/email.go index 87033615..f02a4643 100644 --- a/services/abstract/email.go +++ b/services/abstract/email.go @@ -131,10 +131,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Rec } if s.MTA_STS != nil { - rc := utils.NewRecordConfig(utils.DomainJoin("_mta-sts", domain), "TXT", ttl, origin) - rc.SetTargetTXT(s.MTA_STS.String()) - - rrs = append(rrs, rc) + rrs = append(rrs, s.MTA_STS.GenRRs(domain, ttl, origin)...) } if s.TLS_RPT != nil { @@ -178,25 +175,6 @@ func email_analyze(a *svcs.Analyzer) (err error) { } for domain, service := range services { - // Is there MTA-STS record? - for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, Domain: "_mta-sts." + domain}) { - if service.MTA_STS == nil { - service.MTA_STS = &svcs.MTA_STS{} - } - - if record.Type == "TXT" { - err = service.MTA_STS.Analyze(record.GetTargetTXTJoined()) - if err != nil { - return - } - } - - err = a.UseRR(record, domain, service) - if err != nil { - return - } - } - // Is there TLS-RPT record? for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, Domain: "_smtp._tls." + domain}) { // rfc8460: 3. records that do not begin with "v=TLSRPTv1;" are discarded diff --git a/services/mta-sts.go b/services/mta-sts.go index cc20310a..66cacfd9 100644 --- a/services/mta-sts.go +++ b/services/mta-sts.go @@ -25,6 +25,12 @@ import ( "fmt" "strconv" "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + + "git.happydns.org/happyDomain/model" + "git.happydns.org/happyDomain/utils" ) type MTA_STS struct { @@ -32,6 +38,22 @@ type MTA_STS struct { Id string `json:"id" happydomain:"label=Policy Identifier,placeholder=,description=A short string used to track policy updates."` } +func (t *MTA_STS) GetNbResources() int { + return 1 +} + +func (t *MTA_STS) GenComment(origin string) string { + return t.Id +} + +func (t *MTA_STS) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + rc := utils.NewRecordConfig(utils.DomainJoin("_mta-sts", domain), "TXT", ttl, origin) + rc.SetTargetTXT(t.String()) + + rrs = append(rrs, rc) + return +} + func (t *MTA_STS) Analyze(txt string) error { fields := strings.Split(txt, ";") @@ -68,3 +90,52 @@ func (t *MTA_STS) Analyze(txt string) error { func (t *MTA_STS) String() string { return fmt.Sprintf("v=STSv%d; id=%s", t.Version, t.Id) } + +func mtasts_analyze(a *Analyzer) (err error) { + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeTXT, Prefix: "_mta-sts."}) { + // rfc8461: 3.1 records that do not begin with "v=STSv1;" are discarded + if !strings.HasPrefix(record.GetTargetTXTJoined(), "v=STS") { + continue + } + + service := &MTA_STS{} + + err = service.Analyze(record.GetTargetTXTJoined()) + if err != nil { + return + } + + err = a.UseRR(record, strings.TrimPrefix(record.NameFQDN, "_mta-sts."), service) + if err != nil { + return + } + } + + return +} + +func init() { + RegisterService( + func() happydns.Service { + return &MTA_STS{} + }, + mtasts_analyze, + ServiceInfos{ + Name: "MTA-STS", + Description: "SMTP MTA Strict Transport Security.", + Categories: []string{ + "email", + }, + RecordTypes: []uint16{ + dns.TypeTXT, + }, + Restrictions: ServiceRestrictions{ + NearAlone: true, + NeedTypes: []uint16{ + dns.TypeTXT, + }, + }, + }, + 1, + ) +} From 70a189163ead5efc244de6ea534adac7f8dd758c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 10:45:43 +0100 Subject: [PATCH 05/10] TLS-RPT is now a dedicated service --- services/abstract/email.go | 34 +----------------- services/tls-rpt.go | 72 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/services/abstract/email.go b/services/abstract/email.go index f02a4643..7116532d 100644 --- a/services/abstract/email.go +++ b/services/abstract/email.go @@ -135,10 +135,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Rec } if s.TLS_RPT != nil { - rc := utils.NewRecordConfig(utils.DomainJoin("_smtp._tls", domain), "TXT", ttl, origin) - rc.SetTargetTXT(s.TLS_RPT.String()) - - rrs = append(rrs, rc) + rrs = append(rrs, s.TLS_RPT.GenRRs(domain, ttl, origin)...) } return } @@ -174,35 +171,6 @@ func email_analyze(a *svcs.Analyzer) (err error) { } } - for domain, service := range services { - // Is there TLS-RPT record? - for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeTXT, Domain: "_smtp._tls." + domain}) { - // rfc8460: 3. records that do not begin with "v=TLSRPTv1;" are discarded - if !strings.HasPrefix(record.GetTargetTXTJoined(), "v=TLSRPT") { - continue - } - - if service.TLS_RPT == nil { - service.TLS_RPT = &svcs.TLS_RPT{} - } else { - // rfc8460: 3. If the number of resulting records is not one, senders MUST assume the recipient domain does not implement TLSRPT. - continue - } - - if record.Type == "TXT" { - err = service.TLS_RPT.Analyze(record.GetTargetTXTJoined()) - if err != nil { - return - } - } - - err = a.UseRR(record, domain, service) - if err != nil { - return - } - } - } - return nil } diff --git a/services/tls-rpt.go b/services/tls-rpt.go index 05f3b0d9..05618425 100644 --- a/services/tls-rpt.go +++ b/services/tls-rpt.go @@ -25,6 +25,12 @@ import ( "fmt" "strconv" "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + + "git.happydns.org/happyDomain/model" + "git.happydns.org/happyDomain/utils" ) type TLS_RPT struct { @@ -32,6 +38,23 @@ type TLS_RPT struct { Rua []string `json:"rua" happydomain:"label=Aggregate Report URI,placeholder=https://example.com/path|mailto:name@example.com"` } +func (t *TLS_RPT) GetNbResources() int { + return 1 +} + +func (t *TLS_RPT) GenComment(origin string) string { + return strings.Join(t.Rua, ", ") +} + +func (t *TLS_RPT) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + rc := utils.NewRecordConfig(utils.DomainJoin("_smtp._tls", domain), "TXT", ttl, origin) + rc.SetTargetTXT(t.String()) + + rrs = append(rrs, rc) + + return +} + func (t *TLS_RPT) Analyze(txt string) error { fields := strings.Split(txt, ";") @@ -72,3 +95,52 @@ func (t *TLS_RPT) Analyze(txt string) error { func (t *TLS_RPT) String() string { return fmt.Sprintf("v=TLSRPTv%d; rua=%s", t.Version, strings.Join(t.Rua, ",")) } + +func tlsrpt_analyze(a *Analyzer) (err error) { + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeTXT, Prefix: "_smtp._tls."}) { + // rfc8460: 3. records that do not begin with "v=TLSRPTv1;" are discarded + if !strings.HasPrefix(record.GetTargetTXTJoined(), "v=TLSRPT") { + continue + } + + service := &TLS_RPT{} + + err = service.Analyze(record.GetTargetTXTJoined()) + if err != nil { + return + } + + err = a.UseRR(record, strings.TrimPrefix(record.NameFQDN, "_smtp._tls."), service) + if err != nil { + return + } + } + + return +} + +func init() { + RegisterService( + func() happydns.Service { + return &TLS_RPT{} + }, + tlsrpt_analyze, + ServiceInfos{ + Name: "TLS-RPT", + Description: "SMTP TLS Reporting.", + Categories: []string{ + "email", + }, + RecordTypes: []uint16{ + dns.TypeTXT, + }, + Restrictions: ServiceRestrictions{ + NearAlone: true, + NeedTypes: []uint16{ + dns.TypeTXT, + }, + }, + }, + 1, + ) +} From e38baf825ee2c7e2a68068938ea3bdaebd32a90f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 11:11:08 +0100 Subject: [PATCH 06/10] Deprecate Email service --- main.go | 1 + services/abstract/email.go | 78 +----------------- services/mx.go | 123 +++++++++++++++++++++++++++- services/providers/google/gsuite.go | 10 +-- 4 files changed, 130 insertions(+), 82 deletions(-) diff --git a/main.go b/main.go index fd11a3e4..b5b17b84 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ import ( "git.happydns.org/happyDomain/internal/app" "git.happydns.org/happyDomain/storage" + _ "git.happydns.org/happyDomain/services/abstract" _ "git.happydns.org/happyDomain/services/providers/google" _ "git.happydns.org/happyDomain/storage/leveldb" diff --git a/services/abstract/email.go b/services/abstract/email.go index 7116532d..ebfd99e4 100644 --- a/services/abstract/email.go +++ b/services/abstract/email.go @@ -23,15 +23,12 @@ package abstract import ( "bytes" - "fmt" - "strings" "github.com/StackExchange/dnscontrol/v4/models" "github.com/miekg/dns" "git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/services" - "git.happydns.org/happyDomain/utils" ) type EMail struct { @@ -48,38 +45,9 @@ func (s *EMail) GetNbResources() int { } func (s *EMail) GenComment(origin string) string { - poolMX := map[string]int{} - - for _, mx := range s.MX { - labels := dns.SplitDomainName(mx.Target) - nbLabel := len(labels) - - var dn string - if nbLabel <= 2 { - dn = mx.Target - } else if len(labels[nbLabel-2]) < 4 { - dn = strings.Join(labels[nbLabel-3:], ".") + "." - } else { - dn = strings.Join(labels[nbLabel-2:], ".") + "." - } - - poolMX[dn] += 1 - } - var buffer bytes.Buffer - first := true - for dn, nb := range poolMX { - if !first { - buffer.WriteString("; ") - } else { - first = !first - } - buffer.WriteString(strings.TrimSuffix(dn, "."+origin)) - if nb > 1 { - buffer.WriteString(fmt.Sprintf(" ×%d", nb)) - } - } + buffer.WriteString((&svcs.MXs{MX: s.MX}).GenComment(origin)) if s.SPF != nil { buffer.WriteString(" + SPF") @@ -106,13 +74,7 @@ func (s *EMail) GenComment(origin string) string { func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { if len(s.MX) > 0 { - for _, mx := range s.MX { - rc := utils.NewRecordConfig(domain, "MX", ttl, origin) - rc.MxPreference = mx.Preference - rc.SetTarget(utils.DomainFQDN(mx.Target, origin)) - - rrs = append(rrs, rc) - } + rrs = append(rrs, (&svcs.MXs{MX: s.MX}).GenRRs(domain, ttl, origin)...) } if s.SPF != nil { @@ -140,46 +102,12 @@ func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Rec return } -func email_analyze(a *svcs.Analyzer) (err error) { - services := map[string]*EMail{} - - // Handle only MX records - for _, record := range a.SearchRR(svcs.AnalyzerRecordFilter{Type: dns.TypeMX}) { - if record.Type == "MX" { - dn := record.NameFQDN - - if _, ok := services[dn]; !ok { - services[dn] = &EMail{} - } - - services[dn].MX = append( - services[dn].MX, - svcs.MX{ - Target: record.GetTargetField(), - Preference: record.MxPreference, - }, - ) - - err = a.UseRR( - record, - dn, - services[dn], - ) - if err != nil { - return - } - } - } - - return nil -} - func init() { svcs.RegisterService( func() happydns.Service { return &EMail{} }, - email_analyze, + nil, svcs.ServiceInfos{ Name: "E-Mail", Description: "Sends and receives e-mail with this domain.", diff --git a/services/mx.go b/services/mx.go index af60f797..e64d1001 100644 --- a/services/mx.go +++ b/services/mx.go @@ -21,9 +21,130 @@ package svcs -import () +import ( + "bytes" + "fmt" + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + + "git.happydns.org/happyDomain/model" + "git.happydns.org/happyDomain/utils" +) type MX struct { Target string `json:"target"` Preference uint16 `json:"preference,omitempty"` } + +type MXs struct { + MX []MX `json:"mx" happydomain:"label=EMail Servers,required"` +} + +func (s *MXs) GetNbResources() int { + return len(s.MX) +} + +func (s *MXs) GenComment(origin string) string { + poolMX := map[string]int{} + + for _, mx := range s.MX { + labels := dns.SplitDomainName(mx.Target) + nbLabel := len(labels) + + var dn string + if nbLabel <= 2 { + dn = mx.Target + } else if len(labels[nbLabel-2]) < 4 { + dn = strings.Join(labels[nbLabel-3:], ".") + "." + } else { + dn = strings.Join(labels[nbLabel-2:], ".") + "." + } + + poolMX[dn] += 1 + } + + var buffer bytes.Buffer + first := true + + for dn, nb := range poolMX { + if !first { + buffer.WriteString("; ") + } else { + first = !first + } + buffer.WriteString(strings.TrimSuffix(dn, "."+origin)) + if nb > 1 { + buffer.WriteString(fmt.Sprintf(" ×%d", nb)) + } + } + + return buffer.String() +} + +func (s *MXs) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + for _, mx := range s.MX { + rc := utils.NewRecordConfig(domain, "MX", ttl, origin) + rc.MxPreference = mx.Preference + rc.SetTarget(utils.DomainFQDN(mx.Target, origin)) + + rrs = append(rrs, rc) + } + + return +} + +func mx_analyze(a *Analyzer) (err error) { + service := &MXs{} + + // Handle only MX records + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeMX}) { + dn := record.NameFQDN + + service.MX = append( + service.MX, + MX{ + Target: record.GetTargetField(), + Preference: record.MxPreference, + }, + ) + + err = a.UseRR( + record, + dn, + service, + ) + if err != nil { + return + } + } + + return +} + +func init() { + RegisterService( + func() happydns.Service { + return &MXs{} + }, + mx_analyze, + ServiceInfos{ + Name: "E-Mail servers", + Description: "Receives e-mail with this domain.", + Categories: []string{ + "email", + }, + RecordTypes: []uint16{ + dns.TypeMX, + }, + Restrictions: ServiceRestrictions{ + Single: true, + NeedTypes: []uint16{ + dns.TypeMX, + }, + }, + }, + 1, + ) +} diff --git a/services/providers/google/gsuite.go b/services/providers/google/gsuite.go index e53fcac7..149c1b7d 100644 --- a/services/providers/google/gsuite.go +++ b/services/providers/google/gsuite.go @@ -29,7 +29,6 @@ import ( "git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/services" - "git.happydns.org/happyDomain/services/abstract" ) type GSuite struct { @@ -37,7 +36,7 @@ type GSuite struct { } func (s *GSuite) GenKnownSvcs() []happydns.Service { - knownSvc := &abstract.EMail{ + knownSvc := &svcs.MXs{ MX: []svcs.MX{ svcs.MX{Target: "aspmx.l.google.com.", Preference: 1}, svcs.MX{Target: "alt1.aspmx.l.google.com.", Preference: 5}, @@ -45,9 +44,6 @@ func (s *GSuite) GenKnownSvcs() []happydns.Service { svcs.MX{Target: "alt3.aspmx.l.google.com.", Preference: 10}, svcs.MX{Target: "alt4.aspmx.l.google.com.", Preference: 10}, }, - SPF: &svcs.SPF{ - Directives: []string{"include:_spf.google.com", "~all"}, - }, } if len(s.ValidationCode) > 0 { @@ -57,7 +53,9 @@ func (s *GSuite) GenKnownSvcs() []happydns.Service { }) } - return []happydns.Service{knownSvc} + return []happydns.Service{knownSvc, &svcs.SPF{ + Directives: []string{"include:_spf.google.com", "~all"}, + }} } func (s *GSuite) GetNbResources() int { From fd30989e3a7afce8f4bacbe31f78658f258bf5b8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 11:23:08 +0100 Subject: [PATCH 07/10] Update categories to be more usefull --- services/abstract/acme.go | 2 +- services/abstract/delegation.go | 2 +- services/abstract/github-site-verification.go | 2 +- services/abstract/gitlab-page-verification.go | 2 +- services/abstract/google-site-verification.go | 2 +- services/abstract/keybase.go | 2 +- services/abstract/matrix.go | 2 +- services/abstract/origin.go | 4 ++-- services/abstract/scaleway-challenge.go | 2 +- services/abstract/xmpp.go | 2 +- services/caa.go | 2 +- services/cname.go | 4 ++-- services/naptr.go | 3 +++ services/providers/google/gsuite.go | 1 - services/ptr.go | 2 +- services/tlsa.go | 2 +- 16 files changed, 19 insertions(+), 17 deletions(-) diff --git a/services/abstract/acme.go b/services/abstract/acme.go index 498c0ae3..97eac92a 100644 --- a/services/abstract/acme.go +++ b/services/abstract/acme.go @@ -75,7 +75,7 @@ func init() { Family: svcs.Abstract, Categories: []string{ "temporary", - "tls", + "verification", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/abstract/delegation.go b/services/abstract/delegation.go index 77a4d212..18067bfd 100644 --- a/services/abstract/delegation.go +++ b/services/abstract/delegation.go @@ -126,7 +126,7 @@ func init() { Description: "Delegate this subdomain to another name server", Family: svcs.Abstract, Categories: []string{ - "internal", + "domain name", }, RecordTypes: []uint16{ dns.TypeNS, diff --git a/services/abstract/github-site-verification.go b/services/abstract/github-site-verification.go index 92484278..2a762cfb 100644 --- a/services/abstract/github-site-verification.go +++ b/services/abstract/github-site-verification.go @@ -83,7 +83,7 @@ func init() { Description: "Temporary record to prove that you control the domain.", Family: svcs.Abstract, Categories: []string{ - "temporary", + "verification", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/abstract/gitlab-page-verification.go b/services/abstract/gitlab-page-verification.go index dfcd3eee..d0738886 100644 --- a/services/abstract/gitlab-page-verification.go +++ b/services/abstract/gitlab-page-verification.go @@ -80,7 +80,7 @@ func init() { Description: "Temporary record to prove that you control the domain.", Family: svcs.Abstract, Categories: []string{ - "temporary", + "verification", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/abstract/google-site-verification.go b/services/abstract/google-site-verification.go index 1c3f33c9..99b581d8 100644 --- a/services/abstract/google-site-verification.go +++ b/services/abstract/google-site-verification.go @@ -75,7 +75,7 @@ func init() { Description: "Temporary record to prove that you control the domain.", Family: svcs.Abstract, Categories: []string{ - "temporary", + "verification", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/abstract/keybase.go b/services/abstract/keybase.go index e669fce4..0eb3c6c4 100644 --- a/services/abstract/keybase.go +++ b/services/abstract/keybase.go @@ -74,7 +74,7 @@ func init() { Description: "Temporary record to prove that you control the domain.", Family: svcs.Abstract, Categories: []string{ - "temporary", + "verification", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/abstract/matrix.go b/services/abstract/matrix.go index ec3ab858..4e3b8ec7 100644 --- a/services/abstract/matrix.go +++ b/services/abstract/matrix.go @@ -122,7 +122,7 @@ func init() { Description: "Communicate on Matrix using your domain.", Family: svcs.Abstract, Categories: []string{ - "im", + "service", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/abstract/origin.go b/services/abstract/origin.go index 83b7dce3..a53940de 100644 --- a/services/abstract/origin.go +++ b/services/abstract/origin.go @@ -176,7 +176,7 @@ func init() { Description: "This is the root of your domain.", Family: svcs.Abstract, Categories: []string{ - "internal", + "domain name", }, RecordTypes: []uint16{ dns.TypeSOA, @@ -202,7 +202,7 @@ func init() { Description: "This is the root of your domain.", Family: svcs.Hidden, Categories: []string{ - "internal", + "domain name", }, RecordTypes: []uint16{ dns.TypeNS, diff --git a/services/abstract/scaleway-challenge.go b/services/abstract/scaleway-challenge.go index 799ea8de..f1c61a25 100644 --- a/services/abstract/scaleway-challenge.go +++ b/services/abstract/scaleway-challenge.go @@ -74,7 +74,7 @@ func init() { Description: "Temporary record to prove that you control the domain.", Family: svcs.Abstract, Categories: []string{ - "temporary", + "verification", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/abstract/xmpp.go b/services/abstract/xmpp.go index e3f0a568..71f6c1db 100644 --- a/services/abstract/xmpp.go +++ b/services/abstract/xmpp.go @@ -148,7 +148,7 @@ func init() { Description: "Communicate over XMPP with your domain.", Family: svcs.Abstract, Categories: []string{ - "im", + "service", }, Restrictions: svcs.ServiceRestrictions{ NearAlone: true, diff --git a/services/caa.go b/services/caa.go index 92260aa8..19984199 100644 --- a/services/caa.go +++ b/services/caa.go @@ -298,7 +298,7 @@ func init() { Name: "Certification Authority Authorization", Description: "Indicate to certificate authorities whether they are authorized to issue digital certificates for a particular domain name.", Categories: []string{ - "tls", + "security", }, RecordTypes: []uint16{ dns.TypeCAA, diff --git a/services/cname.go b/services/cname.go index 72a9b311..bb03464c 100644 --- a/services/cname.go +++ b/services/cname.go @@ -108,7 +108,7 @@ func init() { Name: "SubAlias", Description: "A service alias to another domain/service.", Categories: []string{ - "internal", + "alias", }, Restrictions: ServiceRestrictions{ NearAlone: true, @@ -128,7 +128,7 @@ func init() { Name: "Alias", Description: "Maps an alias to another (canonical) domain.", Categories: []string{ - "internal", + "alias", }, RecordTypes: []uint16{ dns.TypeCNAME, diff --git a/services/naptr.go b/services/naptr.go index 4aad7f1f..0a5aadf5 100644 --- a/services/naptr.go +++ b/services/naptr.go @@ -85,6 +85,9 @@ func init() { naptr_analyze, ServiceInfos{ Name: "Naming Authority Pointer", + Categories: []string{ + "telephony", + }, RecordTypes: []uint16{ dns.TypeNAPTR, }, diff --git a/services/providers/google/gsuite.go b/services/providers/google/gsuite.go index 149c1b7d..62122e26 100644 --- a/services/providers/google/gsuite.go +++ b/services/providers/google/gsuite.go @@ -147,7 +147,6 @@ func init() { Description: "The suite of cloud computing, productivity and collaboration tools by Google.", Family: svcs.Provider, Categories: []string{ - "cloud", "email", }, Restrictions: svcs.ServiceRestrictions{ diff --git a/services/ptr.go b/services/ptr.go index ee05e312..bc5f8c41 100644 --- a/services/ptr.go +++ b/services/ptr.go @@ -73,7 +73,7 @@ func init() { Name: "Pointer", Description: "A pointer to another domain.", Categories: []string{ - "internal", + "domain name", }, RecordTypes: []uint16{ dns.TypePTR, diff --git a/services/tlsa.go b/services/tlsa.go index f87cdfa4..fed6b98f 100644 --- a/services/tlsa.go +++ b/services/tlsa.go @@ -161,7 +161,7 @@ func init() { Name: "TLSA records", Description: "Publish TLS certificates exposed by your services.", Categories: []string{ - "tls", + "security", }, RecordTypes: []uint16{ dns.TypeTLSA, From 597bdcbcff79994c1b05e4e0d9189862abc29f7a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 11:38:29 +0100 Subject: [PATCH 08/10] server: Fix nbresources return --- services/abstract/server.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/abstract/server.go b/services/abstract/server.go index 0614999d..76ae5c37 100644 --- a/services/abstract/server.go +++ b/services/abstract/server.go @@ -41,7 +41,17 @@ type Server struct { } func (s *Server) GetNbResources() int { - return 1 + i := 0 + + if s.A != nil { + i += 1 + } + + if s.AAAA != nil { + i += 1 + } + + return i + len(s.SSHFP) } func (s *Server) GenComment(origin string) string { From ec952b50fa939a9a2296faa6b97d33e17ae305be Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 11:38:51 +0100 Subject: [PATCH 09/10] SSHFP can also be independant records --- services/abstract/server.go | 9 +--- services/sshfp.go | 88 ++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/services/abstract/server.go b/services/abstract/server.go index 76ae5c37..770afefc 100644 --- a/services/abstract/server.go +++ b/services/abstract/server.go @@ -88,13 +88,8 @@ func (s *Server) GenRRs(domain string, ttl uint32, origin string) (rrs models.Re rrs = append(rrs, rc) } - for _, sshfp := range s.SSHFP { - rc := utils.NewRecordConfig(domain, "SSHFP", ttl, origin) - rc.SshfpAlgorithm = sshfp.Algorithm - rc.SshfpFingerprint = sshfp.Type - rc.SetTarget(sshfp.FingerPrint) - - rrs = append(rrs, rc) + if len(s.SSHFP) > 0 { + rrs = append(rrs, (&svcs.SSHFPs{SSHFP: s.SSHFP}).GenRRs(domain, ttl, origin)...) } return diff --git a/services/sshfp.go b/services/sshfp.go index 8dd1982b..c2565293 100644 --- a/services/sshfp.go +++ b/services/sshfp.go @@ -21,10 +21,96 @@ package svcs -import () +import ( + "fmt" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + + "git.happydns.org/happyDomain/model" + "git.happydns.org/happyDomain/utils" +) type SSHFP struct { Algorithm uint8 `json:"algorithm"` Type uint8 `json:"type"` FingerPrint string `json:"fingerprint"` } + +type SSHFPs struct { + SSHFP []*SSHFP `json:"SSHFP,omitempty" happydomain:"label=SSH Fingerprint,description=Server's SSH fingerprint"` +} + +func (s *SSHFPs) GetNbResources() int { + return len(s.SSHFP) +} + +func (s *SSHFPs) GenComment(origin string) string { + return fmt.Sprintf("%d fingerprints", len(s.SSHFP)) +} + +func (s *SSHFPs) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + for _, sshfp := range s.SSHFP { + rc := utils.NewRecordConfig(domain, "SSHFP", ttl, origin) + rc.SshfpAlgorithm = sshfp.Algorithm + rc.SshfpFingerprint = sshfp.Type + rc.SetTarget(sshfp.FingerPrint) + + rrs = append(rrs, rc) + } + + return +} + +func sshfp_analyze(a *Analyzer) error { + pool := map[string]models.Records{} + + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeSSHFP}) { + domain := record.NameFQDN + + pool[domain] = append(pool[domain], record) + } + + for dn, rrs := range pool { + s := &SSHFPs{} + + for _, rr := range rrs { + if rr.Type == "SSHFP" { + s.SSHFP = append(s.SSHFP, &SSHFP{ + Algorithm: rr.SshfpAlgorithm, + Type: rr.SshfpFingerprint, + FingerPrint: rr.GetTargetField(), + }) + + a.UseRR(rr, dn, s) + } + } + } + + return nil +} + +func init() { + RegisterService( + func() happydns.Service { + return &SSHFPs{} + }, + sshfp_analyze, + ServiceInfos{ + Name: "SSHFP", + Description: "Store SSH key fingerprints in DNS.", + Categories: []string{ + "security", + }, + RecordTypes: []uint16{ + dns.TypeSSHFP, + }, + Restrictions: ServiceRestrictions{ + NeedTypes: []uint16{ + dns.TypeSSHFP, + }, + }, + }, + 1000, + ) +} From 0c0557bd1210db529af2b175165532a04a85835b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 10 Dec 2024 11:42:55 +0100 Subject: [PATCH 10/10] gsuite: Add SPF version Fixes: https://framagit.org/happyDomain/happyDomain/-/issues/7 --- services/providers/google/gsuite.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/providers/google/gsuite.go b/services/providers/google/gsuite.go index 62122e26..70291c77 100644 --- a/services/providers/google/gsuite.go +++ b/services/providers/google/gsuite.go @@ -54,6 +54,7 @@ func (s *GSuite) GenKnownSvcs() []happydns.Service { } return []happydns.Service{knownSvc, &svcs.SPF{ + Version: 1, Directives: []string{"include:_spf.google.com", "~all"}, }} }