diff --git a/providers/alidns/aliDnsProvider.go b/providers/alidns/aliDnsProvider.go index 13ebbada5..62e01c360 100644 --- a/providers/alidns/aliDnsProvider.go +++ b/providers/alidns/aliDnsProvider.go @@ -49,7 +49,14 @@ func init() { } type aliDnsDsp struct { - client *alidns.Client + client *alidns.Client + domainVersionCache map[string]*domainVersionInfo +} + +type domainVersionInfo struct { + versionCode string + minTTL uint32 + maxTTL uint32 } func newAliDnsDsp(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { @@ -80,7 +87,10 @@ func newAliDnsDsp(config map[string]string, metadata json.RawMessage) (providers if err != nil { return nil, err } - return &aliDnsDsp{client}, nil + return &aliDnsDsp{ + client: client, + domainVersionCache: make(map[string]*domainVersionInfo), + }, nil } // GetZoneRecords returns an array of RecordConfig structs for a zone. @@ -124,7 +134,33 @@ func deduplicateNameServerTargets(newRecs models.Records) models.Records { return deduped } +// validateRecordTTLs checks that all records in the domain config have valid TTL values +// according to the Alibaba Cloud DNS version constraints. +func (a *aliDnsDsp) validateRecordTTLs(dc *models.DomainConfig) error { + versionInfo, err := a.getDomainVersionInfo(dc.Name) + if err != nil { + return fmt.Errorf("failed to get domain version info: %w", err) + } + + for _, rec := range dc.Records { + if rec.TTL < versionInfo.minTTL { + return fmt.Errorf("record %s has TTL %d which is below the minimum %d for this domain version (%s)", + rec.GetLabelFQDN(), rec.TTL, versionInfo.minTTL, versionInfo.versionCode) + } + if rec.TTL > versionInfo.maxTTL { + return fmt.Errorf("record %s has TTL %d which exceeds the maximum %d (24 hours)", + rec.GetLabelFQDN(), rec.TTL, versionInfo.maxTTL) + } + } + return nil +} + func (a *aliDnsDsp) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) { + // Validate TTL constraints for all records + if err := a.validateRecordTTLs(dc); err != nil { + return nil, 0, err + } + var corrections []*models.Correction // Azure is a "ByRecordSet" API. diff --git a/providers/alidns/api.go b/providers/alidns/api.go index 51a1abff2..64905649a 100644 --- a/providers/alidns/api.go +++ b/providers/alidns/api.go @@ -8,6 +8,45 @@ import ( "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" ) +func (a *aliDnsDsp) getDomainVersionInfo(domain string) (*domainVersionInfo, error) { + // Check cache first + if info, ok := a.domainVersionCache[domain]; ok { + return info, nil + } + + req := alidns.CreateDescribeDomainInfoRequest() + req.DomainName = domain + + resp, err := a.client.DescribeDomainInfo(req) + if err != nil { + return nil, err + } + + // Determine minTTL based on VersionCode + var minTTL uint32 + switch resp.VersionCode { + case "version_enterprise_advanced": + minTTL = 1 // Enterprise Ultimate Edition + case "version_personal", "mianfei": + minTTL = 600 // Personal Edition and Free Edition + default: + // Use MinTtl from API if available, otherwise default to 600 + if resp.MinTtl > 0 { + minTTL = uint32(resp.MinTtl) + } else { + minTTL = 600 + } + } + + info := &domainVersionInfo{ + versionCode: resp.VersionCode, + minTTL: minTTL, + maxTTL: 86400, + } + a.domainVersionCache[domain] = info + return info, nil +} + func (a *aliDnsDsp) GetNameservers(domain string) ([]*models.Nameserver, error) { req := alidns.CreateDescribeDomainInfoRequest() req.DomainName = domain diff --git a/providers/alidns/auditrecords.go b/providers/alidns/auditrecords.go index 351ce85f3..cd669a7b9 100644 --- a/providers/alidns/auditrecords.go +++ b/providers/alidns/auditrecords.go @@ -41,21 +41,13 @@ func targetConstraint(rc *models.RecordConfig) error { return nil } -// ttlConstraint checks that TTL is within Alibaba Cloud's allowed range (600-86400 seconds). -func ttlConstraint(rc *models.RecordConfig) error { - if rc.TTL < 600 { - return errors.New("TTL must be at least 600 seconds") - } - if rc.TTL > 86400 { - return errors.New("TTL must not exceed 86400 seconds (24 hours)") - } - return nil -} - // AuditRecords returns a list of errors corresponding to the records // that aren't supported by this provider. If all records are // supported, an empty list is returned. func AuditRecords(records []*models.RecordConfig) []error { + // Note: We can't get domain version info here because AuditRecords + // is called without provider context. TTL validation will be done + // at the provider level in GetZoneRecordsCorrections. a := rejectif.Auditor{} a.Add("MX", rejectif.MxNull) // Last verified at 2025-12-03 @@ -65,7 +57,6 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified at 2025-12-03: Alibaba strips trailing spaces a.Add("TXT", rejectif.TxtHasUnpairedBackslash) // Last verified at 2025-12-03: Alibaba mishandles odd backslashes a.Add("*", labelConstraint) // Last verified at 2025-12-03: Alibaba only allows ASCII + Chinese, rejects other Unicode - a.Add("*", ttlConstraint) // Last verified at 2025-12-03: Alibaba requires TTL 600-86400 a.Add("CNAME", targetConstraint) // Last verified at 2025-12-03: CNAME target must be ASCII or Chinese a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified at 2025-12-03: SRV target must not be null a.Add("SRV", rejectif.SrvHasEmptyTarget) // Last verified at 2025-12-03: SRV target must not be empty