From c684388153f61601ee7cb740a7513d80429c5ec9 Mon Sep 17 00:00:00 2001 From: 77-QiQi <87052253+77-QiQi@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:07:23 +0800 Subject: [PATCH] Improve: Delete the corresponding DNS record if the IPv4/IPv6 address is empty. --- pkg/ddns/ddns.go | 86 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/pkg/ddns/ddns.go b/pkg/ddns/ddns.go index 7759df54..77779c0b 100644 --- a/pkg/ddns/ddns.go +++ b/pkg/ddns/ddns.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/netip" + "strings" "time" "github.com/libdns/libdns" @@ -54,10 +55,13 @@ func (provider *Provider) updateDomain(ctx context.Context, domain string) error return err } - // 独立处理 IPv4 更新 + // 独立处理 IPv4 更新或删除 if *provider.DDNSProfile.EnableIPv4 { if provider.IPAddrs.IPv4Addr == "" { - log.Printf("NEZHA>> Skip IPv4 update for domain %s: IPv4 address is empty", domain) + log.Printf("NEZHA>> IPv4 address is empty for domain %s, deleting A record...", domain) + if err = provider.deleteDomainRecord(ctx, "A"); err != nil { + return fmt.Errorf("failed to delete IPv4 record: %w", err) + } } else { if err = provider.addDomainRecord(ctx, "A", provider.IPAddrs.IPv4Addr); err != nil { return err @@ -65,10 +69,13 @@ func (provider *Provider) updateDomain(ctx context.Context, domain string) error } } - // 独立处理 IPv6 更新 + // 独立处理 IPv6 更新或删除 if *provider.DDNSProfile.EnableIPv6 { if provider.IPAddrs.IPv6Addr == "" { - log.Printf("NEZHA>> Skip IPv6 update for domain %s: IPv6 address is empty", domain) + log.Printf("NEZHA>> IPv6 address is empty for domain %s, deleting AAAA record...", domain) + if err = provider.deleteDomainRecord(ctx, "AAAA"); err != nil { + return fmt.Errorf("failed to delete IPv6 record: %w", err) + } } else { if err = provider.addDomainRecord(ctx, "AAAA", provider.IPAddrs.IPv6Addr); err != nil { return err @@ -96,6 +103,77 @@ func (provider *Provider) addDomainRecord(ctx context.Context, recType, addr str return err } +// deleteDomainRecord 用于删除指定类型的解析记录 +func (provider *Provider) deleteDomainRecord(ctx context.Context, recType string) error { + // 检查 libdns.RecordGetter 和 libdns.RecordDeleter + getter, ok := provider.Setter.(libdns.RecordGetter) + if !ok { + return fmt.Errorf("dns provider does not support record getting") + } + + deleter, ok := provider.Setter.(libdns.RecordDeleter) + if !ok { + return fmt.Errorf("dns provider does not support record deletion") + } + + // 获取 DNS 记录 + allRecords, err := getter.GetRecords(ctx, provider.zone) + if err != nil { + return fmt.Errorf("failed to get DNS records: %w", err) + } + + cleanName := func(name string) string { + return strings.ToLower(strings.TrimSuffix(name, ".")) + } + + cleanPrefix := cleanName(provider.prefix) + cleanZone := cleanName(provider.zone) + targetRecType := strings.ToUpper(recType) + + // 筛选和匹配 + var targetRecords []libdns.Record + for _, rec := range allRecords { + rr := rec.RR() + recName := rr.Name + currentType := strings.ToUpper(rr.Type) + + if currentType != targetRecType { + continue + } + + relName := libdns.RelativeName(recName, provider.zone) + cleanRel := cleanName(relName) + cleanRRName := cleanName(recName) + + var matchedName bool + if cleanPrefix == "" { + matchedName = cleanRel == "" || cleanRel == "@" || cleanRRName == cleanZone + } else { + matchedName = cleanRel == cleanPrefix || cleanRRName == cleanPrefix + } + + // 保留原始的 rec 接口(包含 Cloudflare 所需的 ProviderData 元数据),用于精准删除 + if matchedName { + targetRecords = append(targetRecords, rec) + } + } + + // 若未找到对应记录,则直接返回成功(视为已删除或不存在) + if len(targetRecords) == 0 { + log.Printf("NEZHA>> No matching %s record found for deletion under zone %s", recType, provider.zone) + return nil + } + + // 执行删除操作 + _, err = deleter.DeleteRecords(ctx, provider.zone, targetRecords) + if err != nil { + return fmt.Errorf("deleter.DeleteRecords failed: %w", err) + } + + log.Printf("NEZHA>> Successfully deleted %d matching %s record(s) for %s.%s", len(targetRecords), recType, provider.prefix, provider.zone) + return nil +} + func (provider *Provider) splitDomainSOA(ctx context.Context, domain string) (prefix string, zone string, err error) { c := &dns.Client{Timeout: dnsTimeOut}