jarld checks the reverse DNS of the public zones' MX and SPF addresses as the internet sees it, every 6 hours (Exposure page, jarl exposure mail, jarl_mail_reverse_dns_ok). The Server options page, jarl option generated and GET /options/generated list what Jarl writes into named.conf by itself; stats.rate_limit_log turns off the query-errors logging. Option lists of three or more elements are shown one per line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.1 KiB
Go
109 lines
4.1 KiB
Go
package mailcheck
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
// fakeResolver answers from a fixed set of records, as a recursive
|
|
// resolver would, with the reverse zone's SOA in negative answers.
|
|
func fakeResolver(t *testing.T, records ...string) string {
|
|
t.Helper()
|
|
var rrs []dns.RR
|
|
for _, s := range records {
|
|
rr, err := dns.NewRR(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rrs = append(rrs, rr)
|
|
}
|
|
soa, _ := dns.NewRR("2.0.192.in-addr.arpa. 3600 IN SOA ns1.linode.com. hostmaster.linode.com. 1 3600 900 1209600 300")
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := &dns.Server{PacketConn: pc, Handler: dns.HandlerFunc(func(w dns.ResponseWriter, q *dns.Msg) {
|
|
m := new(dns.Msg)
|
|
m.SetReply(q)
|
|
qn := strings.ToLower(q.Question[0].Name)
|
|
for _, rr := range rrs {
|
|
if strings.ToLower(rr.Header().Name) == qn && (rr.Header().Rrtype == q.Question[0].Qtype || rr.Header().Rrtype == dns.TypeCNAME) {
|
|
m.Answer = append(m.Answer, rr)
|
|
}
|
|
}
|
|
if len(m.Answer) == 0 && dns.IsSubDomain(soa.Header().Name, qn) {
|
|
if q.Question[0].Qtype == dns.TypeSOA && qn == soa.Header().Name {
|
|
m.Answer = append(m.Answer, soa)
|
|
} else {
|
|
m.Ns = append(m.Ns, soa)
|
|
}
|
|
}
|
|
w.WriteMsg(m)
|
|
})}
|
|
go srv.ActivateAndServe()
|
|
t.Cleanup(func() { srv.Shutdown() })
|
|
return pc.LocalAddr().String()
|
|
}
|
|
|
|
func TestCheck(t *testing.T) {
|
|
st := model.NewState()
|
|
ext := &model.View{ID: 1, Name: "external", MatchClients: []string{"any"}}
|
|
in := &model.View{ID: 2, Name: "internal", MatchClients: []string{"lan"}}
|
|
st.Views[1], st.Views[2] = ext, in
|
|
z := &model.Zone{ID: 10, Name: "example.com.", ViewID: 1, Type: model.ZonePrimary}
|
|
zi := &model.Zone{ID: 11, Name: "example.com.", ViewID: 2, Type: model.ZonePrimary}
|
|
rev := &model.Zone{ID: 12, Name: "27.2.0.192.in-addr.arpa.", ViewID: 2, Type: model.ZonePrimary}
|
|
st.Zones[10], st.Zones[11], st.Zones[12] = z, zi, rev
|
|
rec := func(id, zone int64, name, typ, data string) {
|
|
st.Records[id] = &model.Record{ID: id, ZoneID: zone, Name: name, Type: typ, Data: data}
|
|
}
|
|
rec(1, 10, "example.com.", "MX", "10 mail.example.com.")
|
|
rec(2, 10, "example.com.", "TXT", `"v=spf1 mx ip4:192.0.2.26 ip4:192.0.2.27" " ip4:198.51.100.0/24 -all"`)
|
|
rec(3, 10, "www.example.com.", "MX", "10 ignored.example.com.") // not the apex
|
|
rec(4, 11, "example.com.", "MX", "10 internal.example.com.") // private view
|
|
rec(5, 10, "example.com.", "TXT", `"google-site-verification=x"`)
|
|
rec(6, 10, "example.com.", "MX", "20 backup.example.com.") // not on the internet
|
|
|
|
res := Resolver{Servers: []string{fakeResolver(t,
|
|
"mail.example.com. 300 IN A 192.0.2.25",
|
|
"25.2.0.192.in-addr.arpa. 300 IN PTR mail.example.com.",
|
|
"27.2.0.192.in-addr.arpa. 300 IN PTR li-27.members.linode.com.",
|
|
"li-27.members.linode.com. 300 IN A 192.0.2.99",
|
|
)}}
|
|
public := func(id int64) bool { return id == 1 }
|
|
rep := Check(context.Background(), st, public, res)
|
|
if len(rep.Errors) != 1 || !strings.Contains(rep.Errors[0], "backup.example.com (example.com, MX backup.example.com) has no address") {
|
|
t.Fatalf("errors %v", rep.Errors)
|
|
}
|
|
got := map[string]Result{}
|
|
for _, r := range rep.Results {
|
|
got[r.Address] = r
|
|
}
|
|
if len(got) != 3 {
|
|
t.Fatalf("results %+v", rep.Results)
|
|
}
|
|
if r := got["192.0.2.25"]; !r.OK || r.Level != "ok" || r.Roles[0] != "MX mail.example.com" || r.ManagedBy != "" && r.ManagedBy != "linode.com" {
|
|
t.Errorf("mail: %+v", r)
|
|
}
|
|
if r := got["192.0.2.26"]; r.OK || r.Level != "warning" || !strings.Contains(r.Message, "no PTR record") || r.ManagedBy != "linode.com" || r.ReverseZone != "2.0.192.in-addr.arpa" {
|
|
t.Errorf("no PTR: %+v", r)
|
|
}
|
|
if r := got["192.0.2.27"]; r.OK || !strings.Contains(r.Message, "doesn't resolve back") || r.LocalZone != "internal/27.2.0.192.in-addr.arpa" {
|
|
t.Errorf("not forward-confirmed: %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestManagedBy(t *testing.T) {
|
|
for in, want := range map[string]string{"ns1.linode.com.": "linode.com", "dns0.zen.co.uk.": "zen.co.uk", "ns1.members.linode.com.": "linode.com", "localhost.": "localhost"} {
|
|
if got := managedBy(in); got != want {
|
|
t.Errorf("managedBy(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|