2026-06-14 01:07:48 +08:00
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (a *App) handleDNSRecords(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
domain, err := a.domainByID(r.Context(), chi.URLParam(r, "id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, http.StatusNotFound, "domain not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
respondJSON(w, http.StatusOK, map[string]any{"items": a.dnsRecordsFor(domain)})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) handleDNSCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
domain, err := a.domainByID(r.Context(), chi.URLParam(r, "id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, http.StatusNotFound, "domain not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
result := a.checkDNS(r.Context(), domain)
|
|
|
|
|
now := a.now().UTC().Format(time.RFC3339Nano)
|
|
|
|
|
_, _ = a.db.ExecContext(r.Context(), `UPDATE domains SET dns_status=?, dns_checked_at=?, updated_at=? WHERE id=?`, result.Status, now, now, domain.ID)
|
|
|
|
|
respondJSON(w, http.StatusOK, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) dnsRecordsFor(d *Domain) []DNSRecord {
|
|
|
|
|
name := strings.TrimSuffix(d.Name, ".")
|
2026-06-16 11:07:45 +08:00
|
|
|
// MX 建议使用 mail.<域名> 标准格式,而不是直接用 PublicHostname
|
|
|
|
|
// 避免 PublicHostname 等于域名本身时出现 MX 指向自己的问题
|
|
|
|
|
mxHost := "mail." + name + "."
|
2026-06-14 01:07:48 +08:00
|
|
|
return []DNSRecord{
|
2026-06-16 11:07:45 +08:00
|
|
|
{Type: "MX", Name: name, Value: fmt.Sprintf("10 %s", mxHost), TTL: 300},
|
2026-06-14 01:07:48 +08:00
|
|
|
{Type: "TXT", Name: name, Value: "v=spf1 mx -all", TTL: 300},
|
|
|
|
|
{Type: "TXT", Name: d.DKIMSelector + "._domainkey." + name, Value: "v=DKIM1; k=rsa; p=" + d.DKIMPublicKey, TTL: 300},
|
|
|
|
|
{Type: "TXT", Name: "_dmarc." + name, Value: "v=DMARC1; p=quarantine; rua=mailto:postmaster@" + name, TTL: 300},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) checkDNS(ctx context.Context, d *Domain) DNSCheckResult {
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
resolver := net.DefaultResolver
|
|
|
|
|
checks := map[string]DNSCheckStatus{}
|
|
|
|
|
|
|
|
|
|
mx, err := resolver.LookupMX(ctx, d.Name)
|
|
|
|
|
if err != nil || len(mx) == 0 {
|
|
|
|
|
checks["mx"] = DNSCheckStatus{OK: false, Message: "未找到 MX 记录"}
|
|
|
|
|
} else {
|
|
|
|
|
found := make([]string, 0, len(mx))
|
|
|
|
|
ok := false
|
|
|
|
|
for _, item := range mx {
|
|
|
|
|
entry := fmt.Sprintf("%d %s", item.Pref, strings.TrimSuffix(item.Host, "."))
|
|
|
|
|
found = append(found, entry)
|
|
|
|
|
if strings.EqualFold(strings.TrimSuffix(item.Host, "."), strings.TrimSuffix(a.cfg.PublicHostname, ".")) {
|
|
|
|
|
ok = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
checks["mx"] = DNSCheckStatus{OK: ok, Message: boolMessage(ok, "MX 指向正确", "MX 未指向当前邮件主机"), Found: found}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rootTXT, _ := resolver.LookupTXT(ctx, d.Name)
|
|
|
|
|
checks["spf"] = txtContains(rootTXT, "v=spf1", "SPF 记录存在", "未找到 SPF 记录")
|
|
|
|
|
|
|
|
|
|
dkimName := d.DKIMSelector + "._domainkey." + d.Name
|
|
|
|
|
dkimTXT, _ := resolver.LookupTXT(ctx, dkimName)
|
|
|
|
|
checks["dkim"] = txtContains(dkimTXT, "v=DKIM1", "DKIM 记录存在", "未找到 DKIM 记录")
|
|
|
|
|
|
|
|
|
|
dmarcTXT, _ := resolver.LookupTXT(ctx, "_dmarc."+d.Name)
|
|
|
|
|
checks["dmarc"] = txtContains(dmarcTXT, "v=DMARC1", "DMARC 记录存在", "未找到 DMARC 记录")
|
|
|
|
|
|
|
|
|
|
status := "ok"
|
|
|
|
|
for _, c := range checks {
|
|
|
|
|
if !c.OK {
|
|
|
|
|
status = "error"
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return DNSCheckResult{Domain: d.Name, Status: status, Checks: checks}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func txtContains(records []string, needle, okMsg, failMsg string) DNSCheckStatus {
|
|
|
|
|
found := append([]string{}, records...)
|
|
|
|
|
for _, item := range records {
|
|
|
|
|
if strings.Contains(strings.ToLower(item), strings.ToLower(needle)) {
|
|
|
|
|
return DNSCheckStatus{OK: true, Message: okMsg, Found: found}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return DNSCheckStatus{OK: false, Message: failMsg, Found: found}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func boolMessage(ok bool, yes, no string) string {
|
|
|
|
|
if ok {
|
|
|
|
|
return yes
|
|
|
|
|
}
|
|
|
|
|
return no
|
|
|
|
|
}
|