Compare commits

...

10 Commits

26 changed files with 651 additions and 239 deletions

View File

@ -38,6 +38,7 @@ import (
"git.happydns.org/happyDomain/internal/app" "git.happydns.org/happyDomain/internal/app"
"git.happydns.org/happyDomain/storage" "git.happydns.org/happyDomain/storage"
_ "git.happydns.org/happyDomain/services/abstract"
_ "git.happydns.org/happyDomain/services/providers/google" _ "git.happydns.org/happyDomain/services/providers/google"
_ "git.happydns.org/happyDomain/storage/leveldb" _ "git.happydns.org/happyDomain/storage/leveldb"

View File

@ -75,7 +75,7 @@ func init() {
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"temporary", "temporary",
"tls", "verification",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -126,7 +126,7 @@ func init() {
Description: "Delegate this subdomain to another name server", Description: "Delegate this subdomain to another name server",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"internal", "domain name",
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeNS, dns.TypeNS,

View File

@ -23,15 +23,12 @@ package abstract
import ( import (
"bytes" "bytes"
"fmt"
"strings"
"github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/models"
"github.com/miekg/dns" "github.com/miekg/dns"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/services" "git.happydns.org/happyDomain/services"
"git.happydns.org/happyDomain/utils"
) )
type EMail struct { type EMail struct {
@ -48,38 +45,9 @@ func (s *EMail) GetNbResources() int {
} }
func (s *EMail) GenComment(origin string) string { 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 var buffer bytes.Buffer
first := true
for dn, nb := range poolMX { buffer.WriteString((&svcs.MXs{MX: s.MX}).GenComment(origin))
if !first {
buffer.WriteString("; ")
} else {
first = !first
}
buffer.WriteString(strings.TrimSuffix(dn, "."+origin))
if nb > 1 {
buffer.WriteString(fmt.Sprintf(" ×%d", nb))
}
}
if s.SPF != nil { if s.SPF != nil {
buffer.WriteString(" + SPF") buffer.WriteString(" + SPF")
@ -106,200 +74,40 @@ func (s *EMail) GenComment(origin string) string {
func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { func (s *EMail) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) {
if len(s.MX) > 0 { if len(s.MX) > 0 {
for _, mx := range s.MX { rrs = append(rrs, (&svcs.MXs{MX: s.MX}).GenRRs(domain, ttl, origin)...)
rc := utils.NewRecordConfig(domain, "MX", ttl, origin)
rc.MxPreference = mx.Preference
rc.SetTarget(utils.DomainFQDN(mx.Target, origin))
rrs = append(rrs, rc)
}
} }
if s.SPF != nil { if s.SPF != nil {
rc := utils.NewRecordConfig(domain, "TXT", ttl, origin) rrs = append(rrs, s.SPF.GenRRs(domain, ttl, origin)...)
rc.SetTargetTXT(s.SPF.String())
rrs = append(rrs, rc)
} }
for selector, d := range s.DKIM { for selector, d := range s.DKIM {
rc := utils.NewRecordConfig(utils.DomainJoin(selector+"._domainkey", domain), "TXT", ttl, origin) rrs = append(rrs, (&svcs.DKIMRecord{
rc.SetTargetTXT(d.String()) DKIM: *d,
Selector: selector,
rrs = append(rrs, rc) }).GenRRs(domain, ttl, origin)...)
} }
if s.DMARC != nil { if s.DMARC != nil {
rc := utils.NewRecordConfig(utils.DomainJoin("_dmarc", domain), "TXT", ttl, origin) rrs = append(rrs, s.DMARC.GenRRs(domain, ttl, origin)...)
rc.SetTargetTXT(s.DMARC.String())
rrs = append(rrs, rc)
} }
if s.MTA_STS != nil { if s.MTA_STS != nil {
rc := utils.NewRecordConfig(utils.DomainJoin("_mta-sts", domain), "TXT", ttl, origin) rrs = append(rrs, s.MTA_STS.GenRRs(domain, ttl, origin)...)
rc.SetTargetTXT(s.MTA_STS.String())
rrs = append(rrs, rc)
} }
if s.TLS_RPT != nil { if s.TLS_RPT != nil {
rc := utils.NewRecordConfig(utils.DomainJoin("_smtp._tls", domain), "TXT", ttl, origin) rrs = append(rrs, s.TLS_RPT.GenRRs(domain, ttl, origin)...)
rc.SetTargetTXT(s.TLS_RPT.String())
rrs = append(rrs, rc)
} }
return 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
}
}
}
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}) {
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 {
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 {
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
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
}
func init() { func init() {
svcs.RegisterService( svcs.RegisterService(
func() happydns.Service { func() happydns.Service {
return &EMail{} return &EMail{}
}, },
email_analyze, nil,
svcs.ServiceInfos{ svcs.ServiceInfos{
Name: "E-Mail", Name: "E-Mail",
Description: "Sends and receives e-mail with this domain.", Description: "Sends and receives e-mail with this domain.",
@ -309,7 +117,6 @@ func init() {
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeMX, dns.TypeMX,
dns.TypeSPF,
}, },
Tabs: true, Tabs: true,
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{

View File

@ -83,7 +83,7 @@ func init() {
Description: "Temporary record to prove that you control the domain.", Description: "Temporary record to prove that you control the domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"temporary", "verification",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -80,7 +80,7 @@ func init() {
Description: "Temporary record to prove that you control the domain.", Description: "Temporary record to prove that you control the domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"temporary", "verification",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -75,7 +75,7 @@ func init() {
Description: "Temporary record to prove that you control the domain.", Description: "Temporary record to prove that you control the domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"temporary", "verification",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -74,7 +74,7 @@ func init() {
Description: "Temporary record to prove that you control the domain.", Description: "Temporary record to prove that you control the domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"temporary", "verification",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -122,7 +122,7 @@ func init() {
Description: "Communicate on Matrix using your domain.", Description: "Communicate on Matrix using your domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"im", "service",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -176,7 +176,7 @@ func init() {
Description: "This is the root of your domain.", Description: "This is the root of your domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"internal", "domain name",
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeSOA, dns.TypeSOA,
@ -202,7 +202,7 @@ func init() {
Description: "This is the root of your domain.", Description: "This is the root of your domain.",
Family: svcs.Hidden, Family: svcs.Hidden,
Categories: []string{ Categories: []string{
"internal", "domain name",
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeNS, dns.TypeNS,

View File

@ -74,7 +74,7 @@ func init() {
Description: "Temporary record to prove that you control the domain.", Description: "Temporary record to prove that you control the domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"temporary", "verification",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -41,7 +41,17 @@ type Server struct {
} }
func (s *Server) GetNbResources() int { 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 { func (s *Server) GenComment(origin string) string {
@ -78,13 +88,8 @@ func (s *Server) GenRRs(domain string, ttl uint32, origin string) (rrs models.Re
rrs = append(rrs, rc) rrs = append(rrs, rc)
} }
for _, sshfp := range s.SSHFP { if len(s.SSHFP) > 0 {
rc := utils.NewRecordConfig(domain, "SSHFP", ttl, origin) rrs = append(rrs, (&svcs.SSHFPs{SSHFP: s.SSHFP}).GenRRs(domain, ttl, origin)...)
rc.SshfpAlgorithm = sshfp.Algorithm
rc.SshfpFingerprint = sshfp.Type
rc.SetTarget(sshfp.FingerPrint)
rrs = append(rrs, rc)
} }
return return

View File

@ -148,7 +148,7 @@ func init() {
Description: "Communicate over XMPP with your domain.", Description: "Communicate over XMPP with your domain.",
Family: svcs.Abstract, Family: svcs.Abstract,
Categories: []string{ Categories: []string{
"im", "service",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{
NearAlone: true, NearAlone: true,

View File

@ -298,7 +298,7 @@ func init() {
Name: "Certification Authority Authorization", Name: "Certification Authority Authorization",
Description: "Indicate to certificate authorities whether they are authorized to issue digital certificates for a particular domain name.", Description: "Indicate to certificate authorities whether they are authorized to issue digital certificates for a particular domain name.",
Categories: []string{ Categories: []string{
"tls", "security",
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeCAA, dns.TypeCAA,

View File

@ -108,7 +108,7 @@ func init() {
Name: "SubAlias", Name: "SubAlias",
Description: "A service alias to another domain/service.", Description: "A service alias to another domain/service.",
Categories: []string{ Categories: []string{
"internal", "alias",
}, },
Restrictions: ServiceRestrictions{ Restrictions: ServiceRestrictions{
NearAlone: true, NearAlone: true,
@ -128,7 +128,7 @@ func init() {
Name: "Alias", Name: "Alias",
Description: "Maps an alias to another (canonical) domain.", Description: "Maps an alias to another (canonical) domain.",
Categories: []string{ Categories: []string{
"internal", "alias",
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeCNAME, dns.TypeCNAME,

View File

@ -26,6 +26,12 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/miekg/dns"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/utils"
) )
type DKIM struct { type DKIM struct {
@ -111,3 +117,76 @@ func (t *DKIM) String() string {
return strings.Join(fields, ";") 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,
)
}

View File

@ -26,7 +26,12 @@ import (
"strconv" "strconv"
"strings" "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/services/common"
"git.happydns.org/happyDomain/utils"
) )
type DMARC struct { 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=%"` 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 { func analyseFields(txt string) map[string]string {
ret := map[string]string{} ret := map[string]string{}
@ -167,3 +211,47 @@ func (t *DMARC) String() string {
return strings.Join(fields, ";") 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,
)
}

View File

@ -25,6 +25,12 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "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 { 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."` 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 { func (t *MTA_STS) Analyze(txt string) error {
fields := strings.Split(txt, ";") fields := strings.Split(txt, ";")
@ -68,3 +90,52 @@ func (t *MTA_STS) Analyze(txt string) error {
func (t *MTA_STS) String() string { func (t *MTA_STS) String() string {
return fmt.Sprintf("v=STSv%d; id=%s", t.Version, t.Id) 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,
)
}

View File

@ -21,9 +21,130 @@
package svcs 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 { type MX struct {
Target string `json:"target"` Target string `json:"target"`
Preference uint16 `json:"preference,omitempty"` 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,
)
}

View File

@ -85,6 +85,9 @@ func init() {
naptr_analyze, naptr_analyze,
ServiceInfos{ ServiceInfos{
Name: "Naming Authority Pointer", Name: "Naming Authority Pointer",
Categories: []string{
"telephony",
},
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeNAPTR, dns.TypeNAPTR,
}, },

View File

@ -29,7 +29,6 @@ import (
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/services" "git.happydns.org/happyDomain/services"
"git.happydns.org/happyDomain/services/abstract"
) )
type GSuite struct { type GSuite struct {
@ -37,7 +36,7 @@ type GSuite struct {
} }
func (s *GSuite) GenKnownSvcs() []happydns.Service { func (s *GSuite) GenKnownSvcs() []happydns.Service {
knownSvc := &abstract.EMail{ knownSvc := &svcs.MXs{
MX: []svcs.MX{ MX: []svcs.MX{
svcs.MX{Target: "aspmx.l.google.com.", Preference: 1}, svcs.MX{Target: "aspmx.l.google.com.", Preference: 1},
svcs.MX{Target: "alt1.aspmx.l.google.com.", Preference: 5}, 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: "alt3.aspmx.l.google.com.", Preference: 10},
svcs.MX{Target: "alt4.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 { if len(s.ValidationCode) > 0 {
@ -57,7 +53,10 @@ func (s *GSuite) GenKnownSvcs() []happydns.Service {
}) })
} }
return []happydns.Service{knownSvc} return []happydns.Service{knownSvc, &svcs.SPF{
Version: 1,
Directives: []string{"include:_spf.google.com", "~all"},
}}
} }
func (s *GSuite) GetNbResources() int { func (s *GSuite) GetNbResources() int {
@ -149,7 +148,6 @@ func init() {
Description: "The suite of cloud computing, productivity and collaboration tools by Google.", Description: "The suite of cloud computing, productivity and collaboration tools by Google.",
Family: svcs.Provider, Family: svcs.Provider,
Categories: []string{ Categories: []string{
"cloud",
"email", "email",
}, },
Restrictions: svcs.ServiceRestrictions{ Restrictions: svcs.ServiceRestrictions{

View File

@ -73,7 +73,7 @@ func init() {
Name: "Pointer", Name: "Pointer",
Description: "A pointer to another domain.", Description: "A pointer to another domain.",
Categories: []string{ Categories: []string{
"internal", "domain name",
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypePTR, dns.TypePTR,

View File

@ -25,7 +25,13 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/miekg/dns"
"github.com/StackExchange/dnscontrol/v4/pkg/spflib" "github.com/StackExchange/dnscontrol/v4/pkg/spflib"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/utils"
) )
type SPF struct { type SPF struct {
@ -33,6 +39,23 @@ type SPF struct {
Directives []string `json:"directives" happydomain:"label=Directives,placeholder=ip4:203.0.113.12"` 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 { func (t *SPF) Analyze(txt string) error {
_, err := spflib.Parse(txt, nil) _, err := spflib.Parse(txt, nil)
if err != nil { if err != nil {
@ -61,7 +84,65 @@ func (t *SPF) Analyze(txt string) error {
return nil return nil
} }
func (t *SPF) String() string { func spf_analyze(a *Analyzer) (err error) {
directives := append([]string{fmt.Sprintf("v=spf%d", t.Version)}, t.Directives...) 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, " ") 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,
)
}

View File

@ -21,10 +21,96 @@
package svcs 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 { type SSHFP struct {
Algorithm uint8 `json:"algorithm"` Algorithm uint8 `json:"algorithm"`
Type uint8 `json:"type"` Type uint8 `json:"type"`
FingerPrint string `json:"fingerprint"` 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,
)
}

View File

@ -25,6 +25,12 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "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 { 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"` 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 { func (t *TLS_RPT) Analyze(txt string) error {
fields := strings.Split(txt, ";") fields := strings.Split(txt, ";")
@ -72,3 +95,52 @@ func (t *TLS_RPT) Analyze(txt string) error {
func (t *TLS_RPT) String() string { func (t *TLS_RPT) String() string {
return fmt.Sprintf("v=TLSRPTv%d; rua=%s", t.Version, strings.Join(t.Rua, ",")) 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,
)
}

View File

@ -161,7 +161,7 @@ func init() {
Name: "TLSA records", Name: "TLSA records",
Description: "Publish TLS certificates exposed by your services.", Description: "Publish TLS certificates exposed by your services.",
Categories: []string{ Categories: []string{
"tls", "security",
}, },
RecordTypes: []uint16{ RecordTypes: []uint16{
dns.TypeTLSA, dns.TypeTLSA,