Merge branch 'main' into branch_allrecs

This commit is contained in:
Tom Limoncelli 2025-12-04 11:07:10 -05:00 committed by GitHub
commit c2971663ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 945 additions and 12 deletions

View file

@ -52,7 +52,7 @@ jobs:
Write-Host "Integration test providers: $Providers"
echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
env:
PROVIDERS: "['AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','HEDNS','HETZNER_V2','HEXONET','HUAWEICLOUD','INWX','JOKER','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']"
PROVIDERS: "['ALIDNS', 'AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','HEDNS','HETZNER_V2','HEXONET','HUAWEICLOUD','INWX','JOKER','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']"
ENV_CONTEXT: ${{ toJson(env) }}
VARS_CONTEXT: ${{ toJson(vars) }}
SECRETS_CONTEXT: ${{ toJson(secrets) }}
@ -73,6 +73,7 @@ jobs:
# PROVIDER DOMAIN LIST
# These providers will be tested if the env variable is set.
# Set it to the domain name to use during the test.
ALIDNS_DOMAIN: ${{ vars.ALIDNS_DOMAIN }}
AXFRDDNS_DOMAIN: ${{ vars.AXFRDDNS_DOMAIN }}
AXFRDDNS_DNSSEC_DOMAIN: ${{ vars.AXFRDDNS_DNSSEC_DOMAIN }}
AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }}
@ -103,6 +104,9 @@ jobs:
# The above providers have additional env variables they
# need for credentials and such.
#
ALIDNS_ACCESS_KEY_ID: ${{ secrets.ALIDNS_ACCESS_KEY_ID }}
ALIDNS_ACCESS_KEY_SECRET: ${{ secrets.ALIDNS_ACCESS_KEY_SECRET }}
#
AXFRDDNS_MASTER: ${{ secrets.AXFRDDNS_MASTER }}
AXFRDDNS_NAMESERVERS: ${{ secrets.AXFRDDNS_NAMESERVERS }}
AXFRDDNS_TRANSFER_KEY: ${{ secrets.AXFRDDNS_TRANSFER_KEY }}

View file

@ -39,7 +39,7 @@ changelog:
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
order: 1
- title: 'Provider-specific changes:'
regexp: "(?i)((adguardhome|akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cnr|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi|gcloud|gcore|hedns|hetzner|hetznerv2|hexonet|hostingde|huaweicloud|inwx|joker|linode|loopia|luadns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|vercel|vultr).*:)+.*"
regexp: "(?i)((adguardhome|akamaiedge|autodns|axfrd|azure|azure_private_dns|alidns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cnr|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi|gcloud|gcore|hedns|hetzner|hetznerv2|hexonet|hostingde|huaweicloud|inwx|joker|linode|loopia|luadns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|vercel|vultr).*:)+.*"
order: 2
- title: 'Documentation:'
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"

1
OWNERS
View file

@ -1,5 +1,6 @@
providers/adguardhome @ishanjain28
providers/akamaiedgedns @edglynes
providers/alidns @bytemain
providers/autodns @arnoschoon
providers/axfrddns @hnrgrgr
providers/azuredns @vatsalyagoel

View file

@ -17,6 +17,7 @@ Currently supported DNS providers:
- AdGuard Home
- Akamai Edge DNS
- Alibaba Cloud DNS (ALIDNS)
- AutoDNS
- AWS Route 53
- AXFR+DDNS

View file

@ -167,6 +167,12 @@ func GetZone(args GetZoneArgs) error {
return fmt.Errorf("failed GetZone CDP: %w", err)
}
// Get the actual provider type name from creds.json or args
providerType := args.ProviderName
if providerType == "" || providerType == "-" {
providerType = providerConfigs[args.CredName][pproviderTypeFieldName]
}
// decide which zones we need to convert
zones := args.ZoneNames
if len(args.ZoneNames) == 1 && args.ZoneNames[0] == "all" {
@ -253,6 +259,13 @@ func GetZone(args GetZoneArgs) error {
if defaultTTL == 0 {
defaultTTL = prettyzone.MostCommonTTL(recs)
}
// If provider has a registered default TTL and no records exist or MostCommonTTL returns 0,
// use the provider's default TTL
if defaultTTL == 0 || defaultTTL == models.DefaultTTL {
if providerDefaultTTL := providers.GetDefaultTTL(providerType); providerDefaultTTL > 0 {
defaultTTL = providerDefaultTTL
}
}
if defaultTTL != models.DefaultTTL && defaultTTL != 0 {
o = append(o, fmt.Sprintf("DefaultTTL(%d)", defaultTTL))
}

View file

@ -114,6 +114,7 @@
* [Supported providers](provider/index.md)
* [AdGuard Home](provider/adguardhome.md)
* [Akamai Edge DNS](provider/akamaiedgedns.md)
* [Alibaba Cloud DNS (ALIDNS)](provider/alidns.md)
* [Amazon Route 53](provider/route53.md)
* [AutoDNS](provider/autodns.md)
* [AXFR+DDNS](provider/axfrddns.md)

View file

@ -0,0 +1,126 @@
## Configuration
This provider is for [Alibaba Cloud DNS](https://www.alibabacloud.com/product/dns) (also known as ALIDNS). To use this provider, add an entry to `creds.json` with `TYPE` set to `ALIDNS` along with your API credentials.
Example:
{% code title="creds.json" %}
```json
{
"alidns": {
"TYPE": "ALIDNS",
"access_key_id": "YOUR_ACCESS_KEY_ID",
"access_key_secret": "YOUR_ACCESS_KEY_SECRET"
}
}
```
{% endcode %}
Optionally, you can specify a `region_id`:
{% code title="creds.json" %}
```json
{
"alidns": {
"TYPE": "ALIDNS",
"access_key_id": "YOUR_ACCESS_KEY_ID",
"access_key_secret": "YOUR_ACCESS_KEY_SECRET",
"region_id": "cn-hangzhou"
}
}
```
{% endcode %}
Note: The `region_id` defaults to `"cn-hangzhou"`. The region value does not affect DNS management (DNS is global), but Alibaba's SDK requires a region to be provided.
## Usage
An example configuration:
{% code title="dnsconfig.js" %}
```javascript
var REG_NONE = NewRegistrar("none");
var DSP_ALIDNS = NewDnsProvider("alidns");
D("example.com", REG_NONE, DnsProvider(DSP_ALIDNS),
A("test", "1.2.3.4"),
CNAME("www", "example.com."),
MX("@", 10, "mail.example.com."),
);
```
{% endcode %}
## Activation
DNSControl depends on an Alibaba Cloud [RAM user](https://www.alibabacloud.com/help/en/ram/user-guide/overview-of-ram-users) with permissions to manage DNS records.
### Creating RAM User and Access Keys
1. Log in to the [RAM console](https://ram.console.aliyun.com/)
2. Create a new RAM user or use an existing one
3. Generate an AccessKey ID and AccessKey Secret for the user
4. Attach the `AliyunDNSFullAccess` policy to the user
The minimum required permissions are:
```json
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"alidns:DescribeDomains",
"alidns:DescribeDomainRecords",
"alidns:DescribeDomainInfo",
"alidns:AddDomainRecord",
"alidns:UpdateDomainRecord",
"alidns:DeleteDomainRecord"
],
"Resource": "*"
}
]
}
```
## Important Notes
### TTL Constraints
Alibaba Cloud DNS has different TTL constraints depending on your DNS edition:
- **Enterprise Ultimate Edition**: TTL can be as low as 1 second (1-86400)
- **Personal Edition / Free Edition**: Minimum TTL is 600 seconds (600-86400)
DNSControl will automatically validate TTL values based on your domain's edition. If you attempt to use a TTL below the minimum for your edition, you will receive an error.
### Chinese Domain Name Support
ALIDNS supports Chinese domain names (IDN with Chinese characters). However:
- **Supported**: ASCII characters and Chinese characters (CJK Unified Ideographs)
- **Not supported**: Other Unicode characters (e.g., German umlauts, Arabic script)
DNSControl will automatically convert between punycode and unicode as needed.
### Record Type Support
The following record types are supported:
- A, AAAA, CNAME, MX, TXT, NS
- CAA (requires quoted values: `0 issue "letsencrypt.org"`)
- SRV
### TXT Record Constraints
Alibaba Cloud DNS has specific constraints for TXT records:
- Cannot be empty
- Maximum length: 512 bytes
- Cannot contain unescaped double quotes
- Cannot have trailing spaces
- Cannot have unpaired backslashes (odd number of consecutive backslashes)
DNSControl will audit and reject records that violate these constraints.
## New Domains
If a domain does not exist in your Alibaba Cloud account, you must create it manually through the Alibaba Cloud console. DNSControl does not automatically create new domains for this provider.

View file

@ -27,6 +27,7 @@ Jump to a table:
| ------------- | ---------------- | ------------ | --------- |
| [`ADGUARDHOME`](adguardhome.md) | ❌ | ✅ | ❌ |
| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ❌ | ✅ | ❌ |
| [`ALIDNS`](alidns.md) | ❌ | ✅ | ❌ |
| [`AUTODNS`](autodns.md) | ❌ | ✅ | ✅ |
| [`AXFRDDNS`](axfrddns.md) | ❌ | ✅ | ❌ |
| [`AZURE_DNS`](azure_dns.md) | ✅ | ✅ | ❌ |
@ -90,6 +91,7 @@ Jump to a table:
| ------------- | -------------------------------------------------------------------- | ---------------------------------------------- | -------------- | --------- |
| [`ADGUARDHOME`](adguardhome.md) | ❔ | ❔ | ❌ | ❌ |
| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ❔ | ✅ | ✅ | ✅ |
| [`ALIDNS`](alidns.md) | ✅ | ❌ | ❌ | ✅ |
| [`AUTODNS`](autodns.md) | ✅ | ❌ | ❌ | ✅ |
| [`AXFRDDNS`](axfrddns.md) | ✅ | ❌ | ❌ | ❌ |
| [`AZURE_DNS`](azure_dns.md) | ✅ | ✅ | ✅ | ✅ |
@ -152,6 +154,7 @@ Jump to a table:
| ------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ |
| [`ADGUARDHOME`](adguardhome.md) | ✅ | ❔ | ❔ | ❔ | ❔ |
| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ✅ | ❔ | ✅ | ✅ | ❌ |
| [`ALIDNS`](alidns.md) | ❌ | ❔ | ❔ | ❌ | ❔ |
| [`AUTODNS`](autodns.md) | ✅ | ❔ | ❔ | ✅ | ❔ |
| [`AXFRDDNS`](axfrddns.md) | ❌ | ✅ | ✅ | ✅ | ❌ |
| [`AZURE_DNS`](azure_dns.md) | ❌ | ❔ | ❌ | ✅ | ❔ |
@ -208,6 +211,7 @@ Jump to a table:
| Provider name | [`DHCID`](../language-reference/domain-modifiers/DHCID.md) | [`NAPTR`](../language-reference/domain-modifiers/NAPTR.md) | [`SRV`](../language-reference/domain-modifiers/SRV.md) | [`SVCB`](../language-reference/domain-modifiers/SVCB.md) |
| ------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- |
| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ❔ | ✅ | ✅ | ❔ |
| [`ALIDNS`](alidns.md) | ❔ | ❌ | ✅ | ❔ |
| [`AUTODNS`](autodns.md) | ❔ | ❔ | ✅ | ❔ |
| [`AXFRDDNS`](axfrddns.md) | ✅ | ✅ | ✅ | ✅ |
| [`AZURE_DNS`](azure_dns.md) | ❔ | ❌ | ✅ | ❔ |
@ -263,6 +267,7 @@ Jump to a table:
| Provider name | [`CAA`](../language-reference/domain-modifiers/CAA.md) | [`HTTPS`](../language-reference/domain-modifiers/HTTPS.md) | [`SMIMEA`](../language-reference/domain-modifiers/SMIMEA.md) | [`SSHFP`](../language-reference/domain-modifiers/SSHFP.md) | [`TLSA`](../language-reference/domain-modifiers/TLSA.md) |
| ------------- | ------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------- |
| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ✅ | ❔ | ❔ | ✅ | ✅ |
| [`ALIDNS`](alidns.md) | ✅ | ❔ | ❔ | ❌ | ❌ |
| [`AUTODNS`](autodns.md) | ✅ | ❔ | ❔ | ❌ | ❌ |
| [`AXFRDDNS`](axfrddns.md) | ✅ | ✅ | ❔ | ✅ | ✅ |
| [`AZURE_DNS`](azure_dns.md) | ✅ | ❔ | ❔ | ❌ | ❌ |
@ -316,6 +321,7 @@ Jump to a table:
| Provider name | [`AUTODNSSEC`](../language-reference/domain-modifiers/AUTODNSSEC_ON.md) | [`DNSKEY`](../language-reference/domain-modifiers/DNSKEY.md) | [`DS`](../language-reference/domain-modifiers/DS.md) |
| ------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- |
| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ✅ | ❔ | ❌ |
| [`ALIDNS`](alidns.md) | ❌ | ❔ | ❔ |
| [`AUTODNS`](autodns.md) | ❔ | ❔ | ❌ |
| [`AXFRDDNS`](axfrddns.md) | ✅ | ❌ | ✅ |
| [`BIND`](bind.md) | ✅ | ✅ | ✅ |
@ -391,6 +397,7 @@ Providers in this category and their maintainers are:
|Name|Maintainer|
|---|---|
|[`ADGUARDHOME`](adguardhome.md)|@ishanjain28|
|[`ALIDNS`](alidns.md)|@bytemain|
|[`AZURE_PRIVATE_DNS`](azure_private_dns.md)|@matthewmgamble|
|[`AKAMAIEDGEDNS`](akamaiedgedns.md)|@edglynes|
|[`AXFRDDNS`](axfrddns.md)|@hnrgrgr|
@ -445,7 +452,6 @@ code to support this provider, we'd be glad to help in any way.
*(The list below is sorted alphabetically.)*
* [1984 Hosting](https://github.com/StackExchange/dnscontrol/issues/1251) (#1251)
* [Alibaba Cloud DNS](https://github.com/StackExchange/dnscontrol/issues/420)(#420)
* [BookMyName](https://github.com/StackExchange/dnscontrol/issues/3451) (#3451)
* [Constellix (DNSMadeEasy)](https://github.com/StackExchange/dnscontrol/issues/842) (#842)
* [CoreDNS](https://github.com/StackExchange/dnscontrol/issues/1284) (#1284)

View file

@ -144,3 +144,13 @@ Vercel does not allow the record type to be changed after creation. If you try t
### Minimum TTL
Vercel enforces a minimum TTL of 60 seconds (1 minute) for all records. We will always silently override the TTL to 60 seconds if you try to set a lower TTL.
### HTTPS Record ECH Base64 Validation
Currently, Vercel does implements IETF's "Bootstrapping TLS Encrypted ClientHello with DNS Service Bindings" draft. However, Vercel also implements a validation process for the `ech` parameter in the `HTTPS` records, and will reject the request with the following error message if Vercel considers the `ech` value is invalid:
```
Invalid base64 string: [input] (key: ech)
```
The detail of Vercel's validation process is unknown, thus we can not support static validation for `dnscontrol check` or `dnscontrol preview`. You should use `ech=` with caution.

3
go.mod
View file

@ -59,6 +59,7 @@ require (
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
github.com/G-Core/gcore-dns-sdk-go v0.3.3
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.18
github.com/failsafe-go/failsafe-go v0.9.2
@ -127,6 +128,7 @@ require (
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
@ -134,6 +136,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect

48
go.sum
View file

@ -5,6 +5,7 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
@ -30,18 +31,23 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7 h1:AJKJCKcb/psppPl/9CUiQQnTG+Bce0/cIweD5w5Q7aQ=
github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI=
github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI=
github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/TomOnTime/utfutil v1.0.0 h1:/0Ivgo2OjXJxo8i7zgvs7ewSFZMLwCRGm3P5Umowb90=
github.com/TomOnTime/utfutil v1.0.0/go.mod h1:l9lZmOniizVSuIliSkEf87qivMRlSNzbdBFKjuLRg1c=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY=
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@ -137,6 +143,7 @@ github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNY
github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
@ -144,6 +151,7 @@ github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-gandi/go-gandi v0.7.0 h1:gsP33dUspsN1M+ZW9HEgHchK9HiaSkYnltO73RHhSZA=
github.com/go-gandi/go-gandi v0.7.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -176,9 +184,11 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -201,6 +211,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@ -248,12 +259,17 @@ github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
@ -324,6 +340,7 @@ github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nicholas-fedor/shoutrrr v0.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8=
github.com/nicholas-fedor/shoutrrr v0.12.1/go.mod h1:64qWuPpvTUv9ZppEoR6OdroiFmgf9w11YSaR0h9KZGg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE=
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw=
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
@ -332,6 +349,8 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/oracle/oci-go-sdk/v65 v65.105.0 h1:VN3IkW4kwyOOIrjrg7Lh1QGG/sou54c8dqTZB2THeTE=
github.com/oracle/oci-go-sdk/v65 v65.105.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
@ -413,6 +432,10 @@ github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/transip/gotransip/v6 v6.26.1 h1:MeqIjkTBBsZwWAK6giZyMkqLmKMclVHEuTNmoBdx4MA=
github.com/transip/gotransip/v6 v6.26.1/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@ -456,6 +479,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
@ -463,6 +488,7 @@ go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -477,12 +503,22 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -531,6 +567,7 @@ golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -590,12 +627,15 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@ -610,8 +650,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@ -644,6 +688,7 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@ -662,12 +707,15 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View file

@ -300,6 +300,18 @@ func makeTests() []*TestGroup {
testgroup("Ech",
requires(providers.CanUseHTTPS),
not(
// Last tested in 2025-12-04. Turns out that Vercel implements an unknown validation
// on the `ech` parameter, and our dummy base64 string are being rejected with:
//
// Invalid base64 string: [our base64] (key: ech)
//
// Since Vercel's validation process is unknown and not documented, we can't implement
// a rejectif within auditrecord to reject them statically.
//
// Let's just ignore ECH test for Vercel for now.
"VERCEL",
),
tc("Create a HTTPS record", https("@", 1, "example.com.", "alpn=h2,h3")),
tc("Add an ECH key", https("@", 1, "example.com.", "alpn=h2,h3 ech=some+base64+encoded+value///")),
tc("Ignore the ECH key while changing other values", https("@", 1, "example.net.", "port=80 ech=IGNORE")),
@ -600,6 +612,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",

View file

@ -16,6 +16,12 @@
"group_id": "$AED_GROUP_ID",
"host": "$AED_HOST"
},
"ALIDNS": {
"TYPE": "ALIDNS",
"access_key_id": "$ALIDNS_ACCESS_KEY_ID",
"access_key_secret": "$ALIDNS_ACCESS_KEY_SECRET",
"domain": "$ALIDNS_DOMAIN"
},
"AUTODNS": {
"TYPE": "AUTODNS",
"context": "$AUTODNS_CONTEXT",

View file

@ -81,10 +81,9 @@ func parseExternalDNSTxtLabel(label string, customPrefix string) *externalDNSMan
if customPrefix != "" {
if strings.HasPrefix(strings.ToLower(workingLabel), strings.ToLower(customPrefix)) {
workingLabel = workingLabel[len(customPrefix):]
} else {
// Custom prefix specified but not found - this might be a legacy record
// Continue with original label
}
// else: Custom prefix specified but not found - this might be a legacy record
// Continue with original label
}
// Standard prefixes used by external-dns

View file

@ -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

View file

@ -18,6 +18,29 @@ func TxtHasBackslash(rc *models.RecordConfig) error {
return nil
}
// TxtHasUnpairedBackslash audits TXT records for strings that contain an odd number of consecutive backslashes.
// Some providers strip single backslashes or convert odd consecutive backslashes to even.
// e.g., "1back\slash" -> "1backslash", "3back\\\slash" -> "3back\\slash"
func TxtHasUnpairedBackslash(rc *models.RecordConfig) error {
txt := rc.GetTargetTXTJoined()
i := 0
for i < len(txt) {
if txt[i] == '\\' {
count := 0
for i < len(txt) && txt[i] == '\\' {
count++
i++
}
if count%2 == 1 {
return errors.New("txtstring contains unpaired backslash (odd count)")
}
} else {
i++
}
}
return nil
}
// TxtHasBackticks audits TXT records for strings that contain backticks.
func TxtHasBackticks(rc *models.RecordConfig) error {
if strings.Contains(rc.GetTargetTXTJoined(), "`") {

View file

@ -5,6 +5,7 @@ import (
// Define all known providers here. They should each register themselves with the providers package via init function.
_ "github.com/StackExchange/dnscontrol/v4/providers/adguardhome"
_ "github.com/StackExchange/dnscontrol/v4/providers/akamaiedgedns"
_ "github.com/StackExchange/dnscontrol/v4/providers/alidns"
_ "github.com/StackExchange/dnscontrol/v4/providers/autodns"
_ "github.com/StackExchange/dnscontrol/v4/providers/axfrddns"
_ "github.com/StackExchange/dnscontrol/v4/providers/azuredns"

View file

@ -0,0 +1,249 @@
package alidns
import (
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
)
var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Cannot(),
providers.CanGetZones: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Cannot(),
providers.CanUseNAPTR: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanConcur: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
providers.DocDualHost: providers.Can("Alibaba Cloud DNS allows full management of apex NS records"),
providers.DocCreateDomains: providers.Cannot(),
providers.CanUseRoute53Alias: providers.Cannot(),
}
func init() {
const providerName = "ALIDNS"
const providerMaintainer = "@bytemain"
fns := providers.DspFuncs{
Initializer: newAliDNSDsp,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType(providerName, fns, features)
providers.RegisterMaintainer(providerName, providerMaintainer)
// Register default TTL of 600 seconds (10 minutes) for Alibaba Cloud DNS
// This is the minimum TTL for free/personal edition domains
providers.RegisterDefaultTTL(providerName, 600)
}
type aliDNSDsp struct {
client *alidns.Client
domainVersionCache map[string]*domainVersionInfo
cacheMu sync.Mutex
}
type domainVersionInfo struct {
versionCode string
minTTL uint32
maxTTL uint32
}
func newAliDNSDsp(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
accessKeyID := config["access_key_id"]
if accessKeyID == "" {
return nil, fmt.Errorf("creds.json: access_key_id must not be empty")
}
accessKeySecret := config["access_key_secret"]
if accessKeySecret == "" {
return nil, fmt.Errorf("creds.json: access_key_secret must not be empty")
}
// Region ID defaults to "cn-hangzhou". The region value does not affect
// DNS management (DNS is global) but Alibaba's SDK/examples require a
// region to be provided — their docs/examples use Hangzhou:
// https://www.alibabacloud.com/help/en/dns/quick-start-1
region := config["region_id"]
if region == "" {
region = "cn-hangzhou"
}
client, err := alidns.NewClientWithAccessKey(
region,
accessKeyID,
accessKeySecret,
)
if err != nil {
return nil, err
}
return &aliDNSDsp{
client: client,
domainVersionCache: make(map[string]*domainVersionInfo),
}, nil
}
func (a *aliDNSDsp) GetNameservers(domain string) ([]*models.Nameserver, error) {
nsStrings, err := a.getNameservers(domain)
if err != nil {
return nil, err
}
return models.ToNameserversStripTD(nsStrings)
}
// GetZoneRecords returns an array of RecordConfig structs for a zone.
func (a *aliDNSDsp) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
// Fetch all pages of domain records.
records, err := a.describeDomainRecordsAll(domain)
if err != nil {
return nil, err
}
out := models.Records{}
for _, r := range records {
if r.Status != "ENABLE" {
continue
}
rc, err := nativeToRecord(r, domain)
if err != nil {
return nil, err
}
out = append(out, rc)
}
// Alibaba Cloud's DescribeDomainRecords API doesn't return NS records at the apex.
// We need to fetch them separately using getNameservers and add them to the records.
nameservers, err := a.getNameservers(domain)
if err != nil {
return nil, err
}
for _, ns := range nameservers {
rc := nativeToRecordNS(ns, domain)
out = append(out, rc)
}
return out, nil
}
func (a *aliDNSDsp) ListZones() ([]string, error) {
return a.describeDomainsAll()
}
func removeTrailingDot(record string) string {
return strings.TrimSuffix(record, ".")
}
func deduplicateNameServerTargets(newRecs models.Records) models.Records {
dedupedMap := make(map[string]bool)
var deduped models.Records
for _, rec := range newRecs {
if !dedupedMap[rec.GetTargetField()] {
dedupedMap[rec.GetTargetField()] = true
deduped = append(deduped, rec)
}
}
return deduped
}
// PrepDesiredRecords munges any records to best suit this provider.
func (a *aliDNSDsp) PrepDesiredRecords(dc *models.DomainConfig) {
versionInfo, err := a.getDomainVersionInfo(dc.Name)
if err != nil {
return
}
recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
// If TTL is 0 (not set), use the minimum TTL as default
if rec.TTL == 0 {
rec.TTL = versionInfo.minTTL
}
if rec.TTL < versionInfo.minTTL {
if rec.Type != "NS" {
printer.Warnf("record %s has TTL %d which is below the minimum %d for this domain version (%s)\n",
rec.GetLabelFQDN(), rec.TTL, versionInfo.minTTL, versionInfo.versionCode)
}
rec.TTL = versionInfo.minTTL
}
if rec.TTL > versionInfo.maxTTL {
printer.Warnf("record %s has TTL %d which exceeds the maximum %d\n",
rec.GetLabelFQDN(), rec.TTL, versionInfo.maxTTL)
rec.TTL = versionInfo.maxTTL
}
recordsToKeep = append(recordsToKeep, rec)
}
dc.Records = recordsToKeep
}
func (a *aliDNSDsp) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
// Prepare desired records first to normalize TTLs and avoid warnings
a.PrepDesiredRecords(dc)
var corrections []*models.Correction
// Alibaba Cloud DNS is a "ByRecord" API.
changes, actualChangeCount, err := diff2.ByRecord(existingRecords, dc, nil)
if err != nil {
return nil, 0, err
}
for _, change := range changes {
// Copy all param values to local variables to avoid overwrites
msgs := change.MsgsJoined
dcn := dc.Name
chaKey := change.Key
if change.Type == diff2.CHANGE || change.Type == diff2.CREATE {
if chaKey.Type == "NS" && dcn == removeTrailingDot(change.Key.NameFQDN) {
change.New = deduplicateNameServerTargets(change.New)
}
}
switch change.Type {
case diff2.REPORT:
corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined})
case diff2.CREATE:
changeNew := change.New
corrections = append(corrections, &models.Correction{
Msg: msgs,
F: func() error {
return a.createRecordset(changeNew, dcn)
},
})
case diff2.CHANGE:
changeNew := change.New
changeExisting := change.Old
corrections = append(corrections, &models.Correction{
Msg: msgs,
F: func() error {
return a.updateRecordset(changeExisting, changeNew, dcn)
},
})
case diff2.DELETE:
corrections = append(corrections, &models.Correction{
Msg: msgs,
F: func() error {
return a.deleteRecordset(change.Old, dcn)
},
})
default:
panic(fmt.Sprintf("unhandled change.Type %s", change.Type))
}
}
return corrections, actualChangeCount, nil
}

187
providers/alidns/api.go Normal file
View file

@ -0,0 +1,187 @@
package alidns
import (
"fmt"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
)
func (a *aliDNSDsp) getDomainVersionInfo(domain string) (*domainVersionInfo, error) {
// Check cache first
a.cacheMu.Lock()
info, ok := a.domainVersionCache[domain]
a.cacheMu.Unlock()
if 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.cacheMu.Lock()
a.domainVersionCache[domain] = info
a.cacheMu.Unlock()
return info, nil
}
// GetNameservers returns the nameservers for a domain.
func (a *aliDNSDsp) getNameservers(domain string) ([]string, error) {
req := alidns.CreateDescribeDomainInfoRequest()
req.DomainName = domain
resp, err := a.client.DescribeDomainInfo(req)
if err != nil {
return nil, err
}
// Add trailing dot to each nameserver to make them FQDNs
nameservers := make([]string, len(resp.DnsServers.DnsServer))
for i, ns := range resp.DnsServers.DnsServer {
if ns != "" && ns[len(ns)-1] != '.' {
nameservers[i] = ns + "."
} else {
nameservers[i] = ns
}
}
return nameservers, nil
}
func (a *aliDNSDsp) deleteRecordset(records []*models.RecordConfig, domainName string) error {
for _, r := range records {
req := alidns.CreateDeleteDomainRecordRequest()
original, ok := r.Original.(*alidns.Record)
if !ok {
return fmt.Errorf("deleteRecordset: record original is not of type *alidns.Record")
}
req.RecordId = original.RecordId
_, err := a.client.DeleteDomainRecord(req)
if err != nil {
return err
}
}
return nil
}
func (a *aliDNSDsp) createRecordset(records []*models.RecordConfig, domainName string) error {
for _, r := range records {
req := alidns.CreateAddDomainRecordRequest()
req.DomainName = domainName
req.RR = r.Name
req.Type = r.Type
req.TTL = requests.Integer(fmt.Sprintf("%d", r.TTL))
req.Value = recordToNativeContent(r)
// Set priority for MX and SRV records
if r.Type == "MX" || r.Type == "SRV" {
req.Priority = requests.Integer(fmt.Sprintf("%d", recordToNativePriority(r)))
}
_, err := a.client.AddDomainRecord(req)
if err != nil {
return err
}
}
return nil
}
func (a *aliDNSDsp) updateRecordset(existing, desired []*models.RecordConfig, domainName string) error {
// Strategy: Delete all existing records, then create all desired records.
// This is the simplest and most reliable approach because:
// 1. The number of records in a recordset may change
// 2. There's no guaranteed 1:1 mapping between existing and desired records
// 3. Alibaba Cloud API requires RecordId for updates, which desired records don't have
// Delete all existing records first
if err := a.deleteRecordset(existing, domainName); err != nil {
return err
}
// Then create all desired records
return a.createRecordset(desired, domainName)
}
// describeDomainRecordsAll fetches all domain records for 'domain', handling
// pagination transparently. It returns the slice of *alidns.Record or an error.
func (a *aliDNSDsp) describeDomainRecordsAll(domain string) ([]*alidns.Record, error) {
// The SDK returns a slice of value Records (not pointers). We fetch pages
// as values and then convert to pointers before returning.
fetch := func(pageNumber, pageSize int) ([]alidns.Record, int, error) {
req := alidns.CreateDescribeDomainRecordsRequest()
req.Status = "Enable"
req.DomainName = domain
req.PageNumber = requests.NewInteger(pageNumber)
req.PageSize = requests.NewInteger(pageSize)
resp, err := a.client.DescribeDomainRecords(req)
if err != nil {
return nil, 0, err
}
total := int(resp.TotalCount)
return resp.DomainRecords.Record, total, nil
}
vals, err := paginateAll(fetch, 500)
if err != nil {
return nil, err
}
out := make([]*alidns.Record, 0, len(vals))
for i := range vals {
out = append(out, &vals[i])
}
return out, nil
}
func (a *aliDNSDsp) describeDomainsAll() ([]string, error) {
// describeDomainsAll fetches all domains in the account, handling pagination.
fetch := func(pageNumber, pageSize int) ([]string, int, error) {
req := alidns.CreateDescribeDomainsRequest()
req.PageNumber = requests.NewInteger(pageNumber)
req.PageSize = requests.NewInteger(pageSize)
resp, err := a.client.DescribeDomains(req)
if err != nil {
return nil, 0, err
}
domains := make([]string, 0, len(resp.Domains.Domain))
for _, d := range resp.Domains.Domain {
domains = append(domains, d.DomainName)
}
total := int(resp.TotalCount)
return domains, total, nil
}
return paginateAll(fetch, 100)
}

View file

@ -0,0 +1,64 @@
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.
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
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified at 2025-12-03
a.Add("TXT", rejectif.TxtLongerThan(512)) // Last verified at 2025-12-03: 511 bytes OK, 764 bytes failed
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
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
return a.Audit(records)
}

110
providers/alidns/convert.go Normal file
View file

@ -0,0 +1,110 @@
package alidns
import (
"fmt"
"strings"
"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.
func nativeToRecord(r *alidns.Record, domain string) (*models.RecordConfig, error) {
rc := &models.RecordConfig{
TTL: uint32(r.TTL),
Original: r,
}
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
if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
if value != "" && value != "." && !strings.HasSuffix(value, ".") {
value = value + "."
}
}
switch r.Type {
case "MX":
if err := rc.SetTargetMX(uint16(r.Priority), value); err != nil {
return nil, fmt.Errorf("unparsable MX record received from ALIDNS: %w", err)
}
case "SRV":
// SRV records in Alibaba Cloud: Value contains "priority weight port target"
// e.g., "1 1 5060 www.cloud-example.com."
// Parse the parts and normalize the target
parts := strings.Fields(r.Value)
if len(parts) != 4 {
return nil, fmt.Errorf("invalid SRV format from ALIDNS: %s", r.Value)
}
target := parts[3]
// Ensure target has trailing dot for FQDN
if target != "" && target != "." && !strings.HasSuffix(target, ".") {
target = target + "."
}
// Reconstruct with normalized target and let PopulateFromString handle it
srvValue := fmt.Sprintf("%s %s %s %s", parts[0], parts[1], parts[2], target)
if err := rc.PopulateFromString(r.Type, srvValue, domain); err != nil {
return nil, fmt.Errorf("unparsable SRV record received from ALIDNS: %w", err)
}
case "CAA":
// Alibaba Cloud CAA format: "0 issue \"letsencrypt.org\""
if err := rc.SetTargetCAAString(r.Value); err != nil {
return nil, fmt.Errorf("unparsable CAA record received from ALIDNS: %w", err)
}
case "TXT":
if err := rc.SetTargetTXT(r.Value); err != nil {
return nil, fmt.Errorf("unparsable TXT record received from ALIDNS: %w", err)
}
default:
rc.Type = r.Type
if err := rc.SetTarget(value); err != nil {
return nil, fmt.Errorf("unparsable record received from ALIDNS: %w", err)
}
}
return rc, nil
}
// recordToNativeContent converts a RecordConfig to the Value format expected by Alibaba Cloud DNS API.
func recordToNativeContent(r *models.RecordConfig) string {
switch r.Type {
case "SRV":
return fmt.Sprintf("%d %d %d %s", r.SrvPriority, r.SrvWeight, r.SrvPort, r.GetTargetField())
case "CAA":
return fmt.Sprintf("%d %s \"%s\"", r.CaaFlag, r.CaaTag, r.GetTargetField())
case "TXT":
return r.GetTargetTXTJoined()
default:
return r.GetTargetField()
}
}
// recordToNativePriority returns the priority value for MX and SRV records.
func recordToNativePriority(r *models.RecordConfig) int64 {
switch r.Type {
case "MX":
return int64(r.MxPreference)
case "SRV":
return int64(r.SrvPriority)
default:
return 0
}
}
// nativeToRecordNS takes a NS record from DNS and returns a native RecordConfig struct.
func nativeToRecordNS(ns string, origin string) *models.RecordConfig {
rc := &models.RecordConfig{
Type: "NS",
TTL: 600,
}
rc.SetLabel("@", origin)
rc.MustSetTarget(ns)
return rc
}

View file

@ -0,0 +1,27 @@
package alidns
// paginateAll is a small generic paginator helper. The caller provides a
// fetch function that requests a single page (pageNumber,pageSize) and
// returns the items for that page, the total number of items available,
// and an error if any. paginateAll will iterate pages until it has
// collected all items or an error occurs.
func paginateAll[T any](fetch func(pageNumber, pageSize int) ([]T, int, error), maxPageSize int) ([]T, error) {
page := 1
pageSize := maxPageSize
var out []T
for {
items, total, err := fetch(page, pageSize)
if err != nil {
return nil, err
}
out = append(out, items...)
// If we've collected all items, or the page returned nothing, stop.
if len(out) >= total || len(items) == 0 {
break
}
page++
}
return out, nil
}

View file

@ -83,6 +83,20 @@ func RegisterMaintainer(
ProviderMaintainers[providerName] = gitHubUsername
}
// ProviderDefaultTTLs stores the default TTL for each provider.
var ProviderDefaultTTLs = map[string]uint32{}
// RegisterDefaultTTL registers a default TTL for a provider.
// This is used by get-zones to determine the DefaultTTL when generating output.
func RegisterDefaultTTL(providerName string, defaultTTL uint32) {
ProviderDefaultTTLs[providerName] = defaultTTL
}
// GetDefaultTTL returns the default TTL for a provider, or 0 if not registered.
func GetDefaultTTL(providerName string) uint32 {
return ProviderDefaultTTLs[providerName]
}
// CreateRegistrar initializes a registrar instance from given credentials.
func CreateRegistrar(rType string, config map[string]string) (Registrar, error) {
var err error

View file

@ -89,12 +89,13 @@ func (c *vercelProvider) ListDNSRecords(ctx context.Context, domain string) ([]D
type httpsRecord struct {
Priority int64 `json:"priority"`
Target string `json:"target"`
Params string `json:"params,omitempty"`
Params string `json:"params"`
}
// createDNSRecordRequest embeds the official SDK request but adds HTTPS support
type createDNSRecordRequest struct {
vercelClient.CreateDNSRecordRequest
Value *string `json:"value,omitempty"`
HTTPS *httpsRecord `json:"https,omitempty"`
}

View file

@ -317,7 +317,7 @@ func toVercelCreateRequest(domain string, rc *models.RecordConfig) (createDNSRec
}
req.Name = name
req.Type = rc.Type
req.Value = rc.GetTargetField()
req.Value = ptrString(rc.GetTargetField())
req.TTL = int64(rc.TTL)
req.Comment = ""
@ -331,17 +331,24 @@ func toVercelCreateRequest(domain string, rc *models.RecordConfig) (createDNSRec
Port: int64(rc.SrvPort),
Target: rc.GetTargetField(),
}
req.Value = "" // SRV uses the SRV struct, not Value
// When dealing with SRV records, we must not set the Value fields,
// otherwise the API throws an error:
// bad_request - Invalid request: should NOT have additional property `value`
req.Value = nil
case "TXT":
req.Value = rc.GetTargetTXTJoined()
req.Value = ptrString(rc.GetTargetTXTJoined())
case "HTTPS":
req.HTTPS = &httpsRecord{
Priority: int64(rc.SvcPriority),
Target: rc.GetTargetField(),
Params: rc.SvcParams,
}
// When dealing with HTTPS records, we must not set the Value fields,
// otherwise the API throws an error:
// bad_request - Invalid request: should NOT have additional property `value`.
req.Value = nil
case "CAA":
req.Value = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
req.Value = ptrString(fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField()))
}
return req, nil
@ -373,7 +380,10 @@ func toVercelUpdateRequest(rc *models.RecordConfig) (updateDNSRecordRequest, err
Port: ptrInt64(int64(rc.SrvPort)),
Target: &value,
}
req.Value = nil // SRV uses the SRV struct, not Value
// When dealing with SRV records, we must not set the Value fields,
// otherwise the API throws an error:
// bad_request - Invalid request: should NOT have additional property `value`
req.Value = nil
case "TXT":
txtValue := rc.GetTargetTXTJoined()
req.Value = &txtValue
@ -383,6 +393,10 @@ func toVercelUpdateRequest(rc *models.RecordConfig) (updateDNSRecordRequest, err
Target: rc.GetTargetField(),
Params: rc.SvcParams,
}
// When dealing with HTTPS records, we must not set the Value fields,
// otherwise the API throws an error:
// bad_request - Invalid request: should NOT have additional property `value`.
req.Value = nil
case "CAA":
value := fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
req.Value = &value
@ -395,3 +409,7 @@ func toVercelUpdateRequest(rc *models.RecordConfig) (updateDNSRecordRequest, err
func ptrInt64(v int64) *int64 {
return &v
}
func ptrString(v string) *string {
return &v
}