diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index cfd17cf57..67203e20c 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -372,7 +372,6 @@ func makeTests() []*TestGroup { // RFC 7505 NullMX at Apex testgroup("NullMXApex", not( - "ALIDNS", // ALIDNS does not support NullMX. "TRANSIP", // TRANSIP is slow and doesn't support NullMX. Skip to save time. ), tc("create", // Install a Null MX. @@ -596,6 +595,7 @@ func makeTests() []*TestGroup { // SOFTLAYER: fails at direct internationalization, punycode works, of course. tc("Internationalized name", a("ööö", "1.2.3.4")), tc("Change IDN", a("ööö", "2.2.2.2")), + tc("Chinese label", a("中文", "1.2.3.4")), tc("Internationalized CNAME Target", cname("a", "ööö.com.")), ), testgroup("IDNAs in CNAME targets", diff --git a/pkg/rejectif/audit.go b/pkg/rejectif/audit.go index a92eaee17..1efdbc0bb 100644 --- a/pkg/rejectif/audit.go +++ b/pkg/rejectif/audit.go @@ -32,12 +32,20 @@ func (aud *Auditor) Audit(records models.Records) (errs []error) { // For each record, call the checks for that type, gather errors. for _, rc := range records { + // First, run type-specific checks for _, f := range aud.checksFor[rc.Type] { e := f(rc) if e != nil { errs = append(errs, e) } } + // Then, run wildcard checks that apply to all record types + for _, f := range aud.checksFor["*"] { + e := f(rc) + if e != nil { + errs = append(errs, e) + } + } } return errs diff --git a/providers/alidns/auditrecords.go b/providers/alidns/auditrecords.go index 60d79e54b..24c54edda 100644 --- a/providers/alidns/auditrecords.go +++ b/providers/alidns/auditrecords.go @@ -1,10 +1,46 @@ package alidns import ( + "errors" + "unicode" + "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" ) +// isValidAliDNSString checks if a string contains only ASCII or Chinese characters. +// Alibaba Cloud DNS allows: a-z, A-Z, 0-9, -, _, ., *, @, and Chinese characters (汉字). +func isValidAliDNSString(s string) bool { + for _, r := range s { + if r > unicode.MaxASCII { + // Allow CJK Unified Ideographs (Chinese characters): U+4E00 to U+9FFF + // and CJK Extension A: U+3400 to U+4DBF + if (r >= 0x4E00 && r <= 0x9FFF) || (r >= 0x3400 && r <= 0x4DBF) { + continue + } + return false + } + } + return true +} + +// labelConstraint detects labels that contain non-ASCII characters except Chinese characters. +func labelConstraint(rc *models.RecordConfig) error { + if !isValidAliDNSString(rc.GetLabel()) { + return errors.New("label contains non-ASCII characters (only Chinese is allowed)") + } + return nil +} + +// targetConstraint detects target values that contain non-ASCII characters except Chinese characters. +// This applies to CNAME, MX, NS, SRV targets. +func targetConstraint(rc *models.RecordConfig) error { + if !isValidAliDNSString(rc.GetTargetField()) { + return errors.New("target contains non-ASCII characters (only Chinese is allowed)") + } + 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. @@ -17,5 +53,7 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified at 2025-12-03: Alibaba strips quotes 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("CNAME", targetConstraint) // Last verified at 2025-12-03: CNAME target must be ASCII or Chinese return a.Audit(records) } diff --git a/providers/alidns/convert.go b/providers/alidns/convert.go index e8df17489..4cbd094de 100644 --- a/providers/alidns/convert.go +++ b/providers/alidns/convert.go @@ -6,6 +6,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" + "golang.org/x/net/idna" ) // nativeToRecord converts an Alibaba Cloud DNS record to a RecordConfig. @@ -14,7 +15,11 @@ func nativeToRecord(r *alidns.Record, domain string) (*models.RecordConfig, erro TTL: uint32(r.TTL), Original: r, } - rc.SetLabel(r.RR, domain) + label, err := idna.ToASCII(r.RR) + if err != nil { + return nil, fmt.Errorf("failed to convert label to ASCII: %w", err) + } + rc.SetLabel(label, domain) // Normalize CNAME, MX, NS records with trailing dot to be consistent with FQDN format. value := r.Value