Merge branch 'main' into feat/add-alidns

This commit is contained in:
Tom Limoncelli 2025-12-04 10:36:22 -05:00 committed by GitHub
commit 75c545b89a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 364 additions and 182 deletions

View file

@ -1773,6 +1773,13 @@ declare function IGNORE(labelSpec: string, typeSpec?: string, targetSpec?: strin
*
* ## Caveats
*
* ### One per domain
*
* Only one `IGNORE_EXTERNAL_DNS()` should be used per domain. If you call it multiple
* times, the last prefix wins. If you have multiple external-dns instances with
* different prefixes managing the same zone, use `IGNORE()` patterns for additional
* prefixes.
*
* ### TXT Registry Format
*
* This feature relies on external-dns's [TXT registry](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/registry/txt.md),

View file

@ -22,13 +22,18 @@ Example:
{
"bind": {
"TYPE": "BIND",
"directory": "myzones",
"filenameformat": "%U.zone"
"directory": "myzones"
}
}
```
{% endcode %}
As of v4.2.0 `dnscontrol push` will create subdirectories along the path to
the filename. This includes both the portion of the path created by the
`directory` setting and the `filenameformat` setting. For security reasons, the
automatic creation of subdirectories is disabled if `dnscontrol` is running as
root. (Running DNSControl as root is not recommended in general.)
## Meta configuration
This provider accepts some optional metadata in the `NewDnsProvider()` call.
@ -85,43 +90,59 @@ DNSControl does not handle special serial number math such as "looping through z
# filenameformat
The `filenameformat` parameter specifies the file name to be used when
writing the zone file. The default (`%U.zone`) is acceptable in most cases: the
writing the zone file. The default (`%c.zone`) is acceptable in most cases: the
file name is the name as specified in the `D()` function plus ".zone".
The filenameformat is a string with a few printf-like `%` verbs:
* The domain name without tag (the `example.com` part of `example.com!tag`):
* `%D` as specified in `D()` (no IDN conversion, but downcased)
* `%I` converted to IDN/Punycode (`xn--...`) and downcased.
* `%N` converted to Unicode (downcased first)
* `%T` the split horizon tag, or "" (the `tag` part of `example.com!tag`)
* `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable but is usually `!`.
* `%U` short for "%I%?!%T". This is the universal, canonical, name for the domain used for comparisons within DNSControl. This is best for filenames which is why it is used in the default.
* `%%` `%`
* ordinary characters (not `%`) are copied unchanged to the output stream
* FYI: format strings must not end with an incomplete `%` or `%?`
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
| ------- | ------------------------------------------------- | ------------- | ------------------- | -------------------- |
| `%T` | the tag | "" (null) | `myTag` | `myTag` |
| `%c` | canonical name, globally unique and comparable | `example.com` | `example.com!myTag` | `xn--p1ai.com!myTag` |
| `%a` | ASCII domain (Punycode, downcased) | `example.com` | `example.com` | `xn--p1ai.com` |
| `%u` | Unicode domain (non-Unicode parts downcased) | `example.com` | `example.com` | `рф.com` |
| `%r` | Raw (unmodified) Domain from `D()` (risky!) | `EXAMple.com` | `EXAMple.com` | `рф.com` |
| `%f` | like `%c` but better for filenames (`%a%?_%T`) | `example.com` | `example.com_myTag` | `xn--p1ai.com_myTag` |
| `%F` | like `%f` but reversed order (`%T%?_%a`) | `example.com` | `myTag_example.com` | `myTag_xn--p1ai.com` |
| `%?x` | returns `x` if tag exists, otherwise "" | "" (null) | `x` | `x` |
| `%%` | a literal percent sign | `%` | `%` | `%` |
| `a-Z./` | other printable characters are copied exactly | `a-Z./` | `a-Z./` | `a-Z./` |
| `%U` | (deprecated, use `%c`) Same as `%D%?!%T` (risky!) | `example.com` | `example.com!myTag` | `рф.com!myTag` |
| `%D` | (deprecated, use `%r`) mangles Unicode (risky!) | `example.com` | `example.com` | `рф.com` |
Typical values:
* `%?x` is typically used to generate an optional `!` or `_` if there is a tag.
* `%r` is considered "risky" because it can produce a domain name that is not
canonical. For example, if you use `D("FOO.com")` and later change it to `D("foo.com")`, your file names will change.
* Format strings must not end with an incomplete `%` or `%?`
* Generating a filename without a tag is risky. For example, if the same
`dnsconfig.js` has `D("example.com!inside", DSP_BIND)` and
`D("example.com!outside", DSP_BIND)`, both will use the same filename.
DNSControl will write both zone files to the same file, flapping between the
two. No error or warning will be output.
* `%U.zone` (The default)
* `example.com.zone` or `example.com!tag.zone`
* `%T%?_%I.zone` (optional tag and `_` + domain + `.zone`)
* `tag_example.com.zone` or `example.com.zone`
* `db_%T%?_%D`
* `db_inside_example.com` or `db_example.com`
Useful examples:
{% hint style="warning" %}
**Warning** DNSControl will not warn you if two zones generate the same
filename. Instead, each will write to the same place. The content would end up
flapping back and forth between the two. The best way to prevent this is to
always include the tag (`%T`) or use `%U` which includes the tag.
{% endhint %}
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
| ------------ | ----------------------------------- | ------------------ | ------------------------ | ------------------------- |
| `%c.zone` | Default format (v4.28 and later) | `example.com.zone` | `example.com!myTag.zone` | `xn--p1ai.com!myTag.zone` |
| `%U.zone` | Default format (pre-v4.28) (risky!) | `example.com.zone` | `example.com!myTag.zone` | `рф.com!myTag.zone` |
| `db_%f` | Recommended in a popular DNS book | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
| `db_%a%?_%T` | same as above but using `%?_` | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
(new in v4.2.0) `dnscontrol push` will create subdirectories along the path to
the filename. This includes both the portion of the path created by the
`directory` setting and the `filenameformat` setting. The automatic creation of
subdirectories is disabled if `dnscontrol` is running as root for security
reasons.
Compatibility notes:
* `%D` should not be used. It downcases the string in a way that is probably
incompatible with Unicode characters. It is retained for compatibility with
pre-v4.28 releases. If your domain has capital Unicode characters, backwards
compatibility is not guaranteed. Use `%r` instead.
* `%U` relies on `%D` which is deprecated. Use `%c` instead.
* As of v4.28 the default format string changed from `%U.zone` to `%c.zone`. This
should only matter if your `D()` statements included non-ASCII (Unicode)
runes that were capitalized.
* If you are using pre-v4.28 releases the above table is slightly misleading
because uppercase ASCII letters do not always work. If you are using
pre-v4.28 releases, assume the above table lists `example.com` instead
of `EXAMpl.com`.
# FYI: get-zones
@ -132,7 +153,8 @@ any files named `*.zone` and assumes they are zone files.
dnscontrol get-zones --format=nameonly - BIND all
```
If `filenameformat` is defined, `dnscontrol` makes a guess at which
filenames are zones but doesn't try to hard to get it right, which is
mathematically impossible in some cases. Feel free to file an issue if
your format string doesn't work. I love a challenge!
If `filenameformat` is defined, `dnscontrol` makes a guess at which filenames
are zones by reversing the logic of the format string. It doesn't try very hard
to get this right, as getting it right in all situations is mathematically
impossible. Feel free to file an issue if find a situation where it doesn't
work. I love a challenge!

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.

View file

@ -292,6 +292,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")),
@ -2034,11 +2046,6 @@ func makeTests() []*TestGroup {
),
),
// This MUST be the last test.
testgroup("final",
tc("final", txt("final", `TestDNSProviders was successful!`)),
),
testgroup("SMIMEA",
requires(providers.CanUseSMIMEA),
tc("SMIMEA record", smimea("_443._tcp", 3, 1, 1, sha256hash)),
@ -2061,6 +2068,12 @@ func makeTests() []*TestGroup {
// every quarter. There may be library updates, API changes,
// etc.
// This SHOULD be the last test. We do this so that we always
// leave zones with a single TXT record exclaming our success.
// Nothing depends on this record existing or should depend on it.
testgroup("final",
tc("final", txt("final", `TestDNSProviders was successful!`)),
),
}
return tests

View file

@ -13,7 +13,7 @@ const (
DomainTag = "dnscontrol_tag" // A copy of DomainConfig.Tag
DomainUniqueName = "dnscontrol_uniquename" // A copy of DomainConfig.UniqueName
DomainNameRaw = "dnscontrol_nameraw" // A copy of DomainConfig.NameRaw
DomainNameIDN = "dnscontrol_nameidn" // A copy of DomainConfig.NameIDN
DomainNameASCII = "dnscontrol_nameascii" // A copy of DomainConfig.NameASCII
DomainNameUnicode = "dnscontrol_nameunicode" // A copy of DomainConfig.NameUnicode
)
@ -40,8 +40,8 @@ type DomainConfig struct {
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"` // IGNORE()
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"` // DISABLE_IGNORE_SAFETY_CHECK
IgnoreExternalDNS bool `json:"ignore_external_dns,omitempty"` // IGNORE_EXTERNAL_DNS
ExternalDNSPrefix string `json:"external_dns_prefix,omitempty"` // IGNORE_EXTERNAL_DNS prefix
IgnoreExternalDNS bool `json:"ignore_external_dns,omitempty"` // IGNORE_EXTERNAL_DNS
ExternalDNSPrefix string `json:"external_dns_prefix,omitempty"` // IGNORE_EXTERNAL_DNS prefix
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
// DNSSEC bool `json:"dnssec,omitempty"`
@ -74,7 +74,7 @@ func (dc *DomainConfig) PostProcess() {
// Turn the user-supplied name into the fixed forms.
ff := domaintags.MakeDomainFixForms(dc.Name)
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameIDN, ff.NameUnicode, ff.UniqueName
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameASCII, ff.NameUnicode, ff.UniqueName
// Store the FixForms is Metadata so we don't have to change the signature of every function that might need them.
// This is a bit ugly but avoids a huge refactor. Please avoid using these to make the future refactor easier.
@ -82,7 +82,7 @@ func (dc *DomainConfig) PostProcess() {
dc.Metadata[DomainTag] = dc.Tag
}
//dc.Metadata[DomainNameRaw] = dc.NameRaw
//dc.Metadata[DomainNameIDN] = dc.Name
//dc.Metadata[DomainNameASCII] = dc.Name
//dc.Metadata[DomainNameUnicode] = dc.NameUnicode
dc.Metadata[DomainUniqueName] = dc.UniqueName
}

11
package-lock.json generated
View file

@ -6,7 +6,7 @@
"": {
"dependencies": {
"@umbrelladocs/linkspector": "^0.3.13",
"prettier": "^3.6.2"
"prettier": "^3.7.4"
}
},
"node_modules/@babel/code-frame": {
@ -715,7 +715,8 @@
"version": "0.0.1367902",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
@ -2221,9 +2222,9 @@
"license": "ISC"
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"

View file

@ -1,6 +1,6 @@
{
"dependencies": {
"@umbrelladocs/linkspector": "^0.3.13",
"prettier": "^3.6.2"
"prettier": "^3.7.4"
}
}

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

@ -266,8 +266,8 @@ func Test_ignore_external_dns(t *testing.T) {
domain,
existing,
desired,
nil, // absences
nil, // unmanagedConfigs
nil, // absences
nil, // unmanagedConfigs
false, // unmanagedSafely
false, // noPurge
true, // ignoreExternalDNS
@ -352,11 +352,11 @@ func Test_ignore_external_dns_custom_prefix(t *testing.T) {
domain,
existing,
desired,
nil, // absences
nil, // unmanagedConfigs
false, // unmanagedSafely
false, // noPurge
true, // ignoreExternalDNS
nil, // absences
nil, // unmanagedConfigs
false, // unmanagedSafely
false, // noPurge
true, // ignoreExternalDNS
"extdns-", // externalDNSPrefix
)
if err != nil {

View file

@ -9,7 +9,7 @@ import (
// DomainFixedForms stores the various fixed forms of a domain name and tag.
type DomainFixedForms struct {
NameRaw string // "originalinput.com" (name as input by the user, lowercased (no tag))
NameIDN string // "punycode.com"
NameASCII string // "punycode.com"
NameUnicode string // "unicode.com" (converted to downcase BEFORE unicode conversion)
UniqueName string // "punycode.com!tag"
@ -25,7 +25,7 @@ type DomainFixedForms struct {
// * .UniqueName: "example.com!tag" unique across the entire config.
func MakeDomainFixForms(n string) DomainFixedForms {
var err error
var tag, nameRaw, nameIDN, nameUnicode, uniqueName string
var tag, nameRaw, nameASCII, nameUnicode, uniqueName string
var hasBang bool
// Split tag from name.
@ -38,42 +38,45 @@ func MakeDomainFixForms(n string) DomainFixedForms {
hasBang = false
}
nameRaw = strings.ToLower(p[0])
nameRaw = p[0]
if strings.HasPrefix(n, nameRaw) {
// Avoid pointless duplication.
nameRaw = n[0:len(nameRaw)]
}
nameIDN, err = idna.ToASCII(nameRaw)
nameASCII, err = idna.ToASCII(nameRaw)
if err != nil {
nameIDN = nameRaw // Fallback to raw name on error.
nameASCII = nameRaw // Fallback to raw name on error.
} else {
nameASCII = strings.ToLower(nameASCII)
// Avoid pointless duplication.
if nameIDN == nameRaw {
nameIDN = nameRaw
if strings.HasPrefix(n, nameASCII) {
// Avoid pointless duplication.
nameASCII = n[0:len(nameASCII)]
}
}
nameUnicode, err = idna.ToUnicode(nameRaw)
nameUnicode, err = idna.ToUnicode(nameASCII) // We use nameASCII since it is already lowercased.
if err != nil {
nameUnicode = nameRaw // Fallback to raw name on error.
} else {
// Avoid pointless duplication.
if nameUnicode == nameRaw {
nameUnicode = nameRaw
if strings.HasPrefix(n, nameUnicode) {
// Avoid pointless duplication.
nameUnicode = n[0:len(nameUnicode)]
}
}
if hasBang {
uniqueName = nameIDN + "!" + tag
uniqueName = nameASCII + "!" + tag
} else {
uniqueName = nameIDN
uniqueName = nameASCII
}
return DomainFixedForms{
Tag: tag,
NameRaw: nameRaw,
NameIDN: nameIDN,
NameASCII: nameASCII,
NameUnicode: nameUnicode,
UniqueName: uniqueName,
HasBang: hasBang,

View file

@ -10,7 +10,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input string
wantTag string
wantNameRaw string
wantNameIDN string
wantNameASCII string
wantNameUnicode string
wantUniqueName string
wantHasBang bool
@ -20,7 +20,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com",
wantHasBang: false,
@ -30,7 +30,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com!mytag",
wantTag: "mytag",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!mytag",
wantHasBang: true,
@ -40,7 +40,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com!",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!",
wantHasBang: true,
@ -50,7 +50,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "उदाहरण.com",
wantTag: "",
wantNameRaw: "उदाहरण.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com",
wantHasBang: false,
@ -60,7 +60,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "उदाहरण.com!mytag",
wantTag: "mytag",
wantNameRaw: "उदाहरण.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
wantHasBang: true,
@ -70,17 +70,29 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "xn--p1b6ci4b4b3a.com",
wantTag: "",
wantNameRaw: "xn--p1b6ci4b4b3a.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com",
wantHasBang: false,
},
{
// Unicode chars should be left alone (as far as case folding goes)
// Here are some Armenian characters https://tools.lgm.cl/lettercase.html
name: "mixed case unicode",
input: "fooԷէԸըԹ.com!myTag",
wantTag: "myTag",
wantNameRaw: "fooԷէԸըԹ.com",
wantNameASCII: "xn--foo-b7dfg43aja.com",
wantNameUnicode: "fooԷէԸըԹ.com",
wantUniqueName: "xn--foo-b7dfg43aja.com!myTag",
wantHasBang: true,
},
{
name: "punycode domain with tag",
input: "xn--p1b6ci4b4b3a.com!mytag",
wantTag: "mytag",
wantNameRaw: "xn--p1b6ci4b4b3a.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
wantHasBang: true,
@ -89,8 +101,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
name: "mixed case domain",
input: "Example.COM",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameRaw: "Example.COM",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com",
wantHasBang: false,
@ -99,12 +111,34 @@ func Test_MakeDomainFixForms(t *testing.T) {
name: "mixed case domain with tag",
input: "Example.COM!MyTag",
wantTag: "MyTag",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameRaw: "Example.COM",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!MyTag",
wantHasBang: true,
},
// This is used in the documentation for the BIND provider, thus we test
// it to make sure we got it right.
{
name: "BIND example 1",
input: "рф.com!myTag",
wantTag: "myTag",
wantNameRaw: "рф.com",
wantNameASCII: "xn--p1ai.com",
wantNameUnicode: "рф.com",
wantUniqueName: "xn--p1ai.com!myTag",
wantHasBang: true,
},
{
name: "BIND example 2",
input: "рф.com",
wantTag: "",
wantNameRaw: "рф.com",
wantNameASCII: "xn--p1ai.com",
wantNameUnicode: "рф.com",
wantUniqueName: "xn--p1ai.com",
wantHasBang: false,
},
}
for _, tt := range tests {
@ -116,8 +150,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
if got.NameRaw != tt.wantNameRaw {
t.Errorf("MakeDomainFixForms() gotNameRaw = %v, want %v", got.NameRaw, tt.wantNameRaw)
}
if got.NameIDN != tt.wantNameIDN {
t.Errorf("MakeDomainFixForms() gotNameIDN = %v, want %v", got.NameIDN, tt.wantNameIDN)
if got.NameASCII != tt.wantNameASCII {
t.Errorf("MakeDomainFixForms() gotNameASCII = %v, want %v", got.NameASCII, tt.wantNameASCII)
}
if got.NameUnicode != tt.wantNameUnicode {
t.Errorf("MakeDomainFixForms() gotNameUnicode = %v, want %v", got.NameUnicode, tt.wantNameUnicode)

View file

@ -24,8 +24,8 @@ func CompilePermitList(s string) PermitList {
continue
}
ff := MakeDomainFixForms(l)
if ff.HasBang && ff.NameIDN == "" { // Treat empty name as wildcard.
ff.NameIDN = "*"
if ff.HasBang && ff.NameASCII == "" { // Treat empty name as wildcard.
ff.NameASCII = "*"
}
sl.items = append(sl.items, ff)
}
@ -65,24 +65,24 @@ func (pl *PermitList) Permitted(domToCheck string) bool {
// Now that we know the tag matches, we can focus on the name.
// `*!tag` or `*` matches everything.
if filterItem.NameIDN == "*" {
if filterItem.NameASCII == "*" {
return true
}
// If the name starts with "*." then match the suffix.
if strings.HasPrefix(filterItem.NameIDN, "*.") {
if strings.HasPrefix(filterItem.NameASCII, "*.") {
// example.com matches *.example.com
if domToCheckFF.NameIDN == filterItem.NameIDN[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
if domToCheckFF.NameASCII == filterItem.NameASCII[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
return true
}
// foo.example.com matches *.example.com
if strings.HasSuffix(domToCheckFF.NameIDN, filterItem.NameIDN[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
if strings.HasSuffix(domToCheckFF.NameASCII, filterItem.NameASCII[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
return true
}
}
// No wildcards? Exact match.
if filterItem.NameIDN == domToCheckFF.NameIDN || filterItem.NameUnicode == domToCheckFF.NameUnicode {
if filterItem.NameASCII == domToCheckFF.NameASCII || filterItem.NameUnicode == domToCheckFF.NameUnicode {
return true
}
}

View file

@ -866,15 +866,15 @@ function locStringBuilder(record, args) {
(args.alt < -100000
? -100000
: args.alt > 42849672.95
? 42849672.95
: args.alt.toString()) + 'm';
? 42849672.95
: args.alt.toString()) + 'm';
precisionbuffer +=
' ' +
(args.siz > 90000000
? 90000000
: args.siz < 0
? 0
: args.siz.toString()) +
? 0
: args.siz.toString()) +
'm';
precisionbuffer +=
' ' +
@ -914,8 +914,8 @@ function locDMSBuilder(record, args) {
record.localtitude > 4294967295
? 4294967295
: record.localtitude < 0
? 0
: record.localtitude;
? 0
: record.localtitude;
// Size
record.locsize = getENotationInt(args.siz);
// Horizontal Precision

View file

@ -1,52 +1,52 @@
{
"registrars": [],
"dns_providers": [],
"domains": [
{
"name": "extdns-default.com",
"registrar": "none",
"dnsProviders": {},
"ignore_external_dns": true,
"meta": {
"dnscontrol_uniquename": "extdns-default.com"
},
"name": "extdns-default.com",
"records": [],
"ignore_external_dns": true
"registrar": "none"
},
{
"name": "extdns-custom.com",
"registrar": "none",
"dnsProviders": {},
"external_dns_prefix": "extdns-",
"ignore_external_dns": true,
"meta": {
"dnscontrol_uniquename": "extdns-custom.com"
},
"name": "extdns-custom.com",
"records": [],
"ignore_external_dns": true,
"external_dns_prefix": "extdns-"
"registrar": "none"
},
{
"name": "extdns-combined.com",
"registrar": "none",
"dnsProviders": {},
"ignore_external_dns": true,
"meta": {
"dnscontrol_uniquename": "extdns-combined.com"
},
"name": "extdns-combined.com",
"records": [
{
"type": "CNAME",
"name": "api",
"ttl": 300,
"filepos": "[line:12:5]",
"target": "www.extdns-combined.com."
"name": "api",
"target": "www.extdns-combined.com.",
"ttl": 300,
"type": "CNAME"
},
{
"type": "A",
"name": "www",
"ttl": 300,
"filepos": "[line:11:5]",
"target": "1.2.3.4"
"name": "www",
"target": "1.2.3.4",
"ttl": 300,
"type": "A"
}
],
"ignore_external_dns": true
"registrar": "none"
}
]
],
"registrars": []
}

View file

@ -69,7 +69,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
api.directory = "zones"
}
if api.filenameformat == "" {
api.filenameformat = "%U.zone"
api.filenameformat = "%c.zone"
}
if len(providermeta) != 0 {
err := json.Unmarshal(providermeta, api)
@ -171,7 +171,7 @@ func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (mo
ff := domaintags.DomainFixedForms{
Tag: meta[models.DomainTag],
NameRaw: meta[models.DomainNameRaw],
NameIDN: domain,
NameASCII: domain,
NameUnicode: meta[models.DomainNameUnicode],
UniqueName: meta[models.DomainUniqueName],
}
@ -282,7 +282,7 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
domaintags.DomainFixedForms{
Tag: dc.Tag,
NameRaw: dc.NameRaw,
NameIDN: dc.Name,
NameASCII: dc.Name,
NameUnicode: dc.NameUnicode,
UniqueName: dc.UniqueName,
},

View file

@ -13,11 +13,6 @@ import (
// makeFileName uses format to generate a zone's filename. See the
func makeFileName(format string, ff domaintags.DomainFixedForms) string {
//fmt.Printf("DEBUG: makeFileName(%q, %+v)\n", format, ff)
nameRaw := ff.NameRaw
nameIDN := ff.NameIDN
nameUnicode := ff.NameUnicode
uniquename := ff.UniqueName
tag := ff.Tag
if format == "" {
panic("BUG: makeFileName called with null format")
}
@ -40,16 +35,31 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
pos++
tok = tokens[pos]
switch tok {
case "D":
b.WriteString(nameRaw)
case "T":
b.WriteString(tag)
case "U":
b.WriteString(uniquename)
case "I":
b.WriteString(nameIDN)
case "N":
b.WriteString(nameUnicode)
// v4.28 names
case "r": // NameRaw "originalinput.com" (i for input)
b.WriteString(ff.NameRaw)
case "a": // NameASCII "punycode.com" (a for ascii)
b.WriteString(ff.NameASCII)
case "u": // NameUnicode "unicode.com" (u for unicode)
b.WriteString(ff.NameUnicode)
case "c": // UniqueName "punycode.com!tag" or "punycode.com" if no tag (c for canonical)
b.WriteString(ff.UniqueName)
case "f": //
b.WriteString(ff.NameASCII)
if ff.Tag != "" {
b.WriteString("_")
b.WriteString(ff.Tag)
}
case "F": //
if ff.Tag != "" {
b.WriteString(ff.Tag)
b.WriteString("_")
}
b.WriteString(ff.NameASCII)
case "T": // Tag The tag portion of `example.com!tag`
b.WriteString(ff.Tag)
case "%":
b.WriteString("%")
case "?":
@ -59,9 +69,20 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
}
pos++
tok = tokens[pos]
if tag != "" {
if ff.Tag != "" {
b.WriteString(tok)
}
// Legacy names kept for compatibility
case "U": // the domain name as specified in `D()`
b.WriteString(strings.ToLower(ff.NameRaw))
if ff.Tag != "" {
b.WriteString("!")
b.WriteString(ff.Tag)
}
case "D": // domain (without tag) as specified in D() (no IDN conversion, but downcased)
b.WriteString(strings.ToLower(ff.NameRaw))
default:
fmt.Fprintf(&b, "%%(unknown %%verb %%%s)", tok)
}
@ -156,14 +177,14 @@ func makeExtractor(format string) (string, error) {
pos++
tok = tokens[pos]
switch tok {
case "D":
case "D", "a", "u", "r":
b.WriteString(`(.*)`)
case "T":
if pass == 0 {
// On the second pass, nothing is generated.
b.WriteString(`.*`)
}
case "U":
case "U", "c":
if pass == 0 {
b.WriteString(`(.*)!.+`)
} else {

View file

@ -8,29 +8,28 @@ import (
)
func Test_makeFileName(t *testing.T) {
ff := domaintags.DomainFixedForms{
NameRaw: "raw",
NameIDN: "idn",
NameUnicode: "unicode",
UniqueName: "unique!taga",
Tag: "tagb",
}
tagless := domaintags.DomainFixedForms{
NameRaw: "raw",
NameIDN: "idn",
NameUnicode: "unicode",
UniqueName: "unique",
Tag: "",
}
fmtDefault := "%U.zone"
fmtBasic := "%U - %T - %D"
fmtBk1 := "db_%U" // Something I've seen in books on DNS
fmtBk2 := "db_%T_%D" // Something I've seen in books on DNS
fmtFancy := "%T%?_%D.zone" // Include the tag_ only if there is a tag
fmtErrorPct := "literal%"
fmtErrorOpt := "literal%?"
fmtErrorUnk := "literal%o" // Unknown % verb
ff := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique!taga",
Tag: "tagy",
}
tagless := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique",
Tag: "",
}
type args struct {
format string
ff domaintags.DomainFixedForms
@ -40,25 +39,15 @@ func Test_makeFileName(t *testing.T) {
args args
want string
}{
// Test corner cases and common cases.
{"literal", args{"literal", ff}, "literal"},
{"middle", args{"mid%Dle", ff}, "midrawle"},
{"D", args{"%D", ff}, "raw"},
{"I", args{"%I", ff}, "idn"},
{"N", args{"%N", ff}, "unicode"},
{"T", args{"%T", ff}, "tagb"},
{"x1", args{"XX%?xYY", ff}, "XXxYY"},
{"x2", args{"AA%?xBB", tagless}, "AABB"},
{"U", args{"%U", ff}, "unique!taga"},
{"percent", args{"%%", ff}, "%"},
//
{"default", args{fmtDefault, ff}, "unique!taga.zone"},
{"basic", args{fmtBasic, ff}, "unique!taga - tagb - raw"},
{"front", args{"%Daaa", ff}, "rawaaa"},
{"tail", args{"bbb%D", ff}, "bbbraw"},
{"bk1", args{fmtBk1, ff}, "db_unique!taga"},
{"bk2", args{fmtBk2, ff}, "db_tagb_raw"},
{"fanWI", args{fmtFancy, ff}, "tagb_raw.zone"},
{"fanWO", args{fmtFancy, tagless}, "raw.zone"},
{"basic", args{fmtBasic, ff}, "domy!tagy - tagy - domy"},
{"solo", args{"%D", ff}, "domy"},
{"front", args{"%Daaa", ff}, "domyaaa"},
{"tail", args{"bbb%D", ff}, "bbbdomy"},
{"def", args{fmtDefault, ff}, "domy!tagy.zone"},
{"fanWI", args{fmtFancy, ff}, "tagy_domy.zone"},
{"fanWO", args{fmtFancy, tagless}, "domy.zone"},
{"errP", args{fmtErrorPct, ff}, "literal%(format may not end in %)"},
{"errQ", args{fmtErrorOpt, ff}, "literal%(format may not end in %?)"},
{"errU", args{fmtErrorUnk, ff}, "literal%(unknown %verb %o)"},
@ -73,6 +62,66 @@ func Test_makeFileName(t *testing.T) {
}
}
func Test_makeFileName_2(t *testing.T) {
ff1 := domaintags.MakeDomainFixForms(`EXAMple.com`)
ff2 := domaintags.MakeDomainFixForms(`EXAMple.com!myTag`)
ff3 := domaintags.MakeDomainFixForms(`рф.com!myTag`)
tests := []struct {
name string
format string
want1 string
want2 string
want3 string
descr string // Not used in test, just for documentation generation
}{
// NOTE: "Domain" in these descriptions means the domain name without any split horizon tag. Technically the "Zone".
{`T`, `%T`, ``, `myTag`, `myTag`, `the tag`},
{`c`, `%c`, `example.com`, `example.com!myTag`, `xn--p1ai.com!myTag`, `canonical name, globally unique and comparable`},
{`a`, `%a`, `example.com`, `example.com`, `xn--p1ai.com`, `ASCII domain (Punycode, downcased)`},
{`u`, `%u`, `example.com`, `example.com`, `рф.com`, `Unicode domain (non-Unicode parts downcased)`},
{`r`, `%r`, `EXAMple.com`, `EXAMple.com`, `рф.com`, "Raw (unmodified) Domain from `D()` (risky!)"},
{`f`, `%f`, `example.com`, `example.com_myTag`, `xn--p1ai.com_myTag`, "like `%c` but better for filenames (`%a%?_%T`)"},
{`F`, `%F`, `example.com`, `myTag_example.com`, `myTag_xn--p1ai.com`, "like `%f` but reversed order (`%T%?_%a`)"},
{`%?x`, `%?x`, ``, `x`, `x`, "returns `x` if tag exists, otherwise \"\""},
{`%`, `%%`, `%`, `%`, `%`, `a literal percent sign`},
// Pre-v4.28 names kept for compatibility (note: pre v4.28 did not permit mixed case domain names, we downcased them here for the tests)
{`U`, `%U`, `example.com`, `example.com!myTag`, `рф.com!myTag`, "(deprecated, use `%c`) Same as `%D%?!%T` (risky!)"},
{`D`, `%D`, `example.com`, `example.com`, `рф.com`, "(deprecated, use `%r`) mangles Unicode (risky!)"},
{`%T%?_%D.zone`, `%T%?_%D.zone`, `example.com.zone`, `myTag_example.com.zone`, `myTag_рф.com.zone`, `mentioned in the docs`},
{`db_%T%?_%D`, `db_%T%?_%D`, `db_example.com`, `db_myTag_example.com`, `db_myTag_рф.com`, `mentioned in the docs`},
{`db_%D`, `db_%D`, `db_example.com`, `db_example.com`, `db_рф.com`, `mentioned in the docs`},
// Examples used in the documentation for the BIND provider
{`%c.zone`, `%c.zone`, "example.com.zone", "example.com!myTag.zone", "xn--p1ai.com!myTag.zone", "Default format (v4.28 and later)"},
{`%U.zone`, `%U.zone`, `example.com.zone`, `example.com!myTag.zone`, `рф.com!myTag.zone`, "Default format (pre-v4.28) (risky!)"},
{`db_%f`, `db_%f`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "Recommended in a popular DNS book"},
{`db_a%?_%T`, `db_%a%?_%T`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "same as above but using `%?_`"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1 := makeFileName(tt.format, ff1)
if got1 != tt.want1 {
t.Errorf("makeFileName(%q) = ff1 %q, want %q", tt.format, got1, tt.want1)
}
got2 := makeFileName(tt.format, ff2)
if got2 != tt.want2 {
t.Errorf("makeFileName(%q) = ff2 %q, want %q", tt.format, got2, tt.want2)
}
got3 := makeFileName(tt.format, ff3)
if got3 != tt.want3 {
t.Errorf("makeFileName(%q) = ff3 %q, want %q", tt.format, got3, tt.want3)
}
//Uncomment to regenerate lines used in documentation/provider/bind.md 's table:
// fmt.Print(strings.ReplaceAll(fmt.Sprintf("| `%s` | %s | `%s` | `%s` | `%s` |\n", tt.format, tt.descr, got1, got2, got3), "``", "`\"\"` (null)"))
//Uncomment to regenerate the above test cases:
// fmt.Printf("{`%s`, `%s`, `%s`, `%s`, `%s`, %q},\n", tt.name, tt.format, got1, got2, got3, tt.descr)
})
}
}
func Test_makeExtractor(t *testing.T) {
type args struct {
format string
@ -83,6 +132,10 @@ func Test_makeExtractor(t *testing.T) {
want string
wantErr bool
}{
{"c", args{"%c.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"a", args{"%a.zone"}, `(.*)\.zone`, false},
{"u", args{"%u.zone"}, `(.*)\.zone`, false},
{"r", args{"%r.zone"}, `(.*)\.zone`, false},
// TODO: Add test cases.
{"u", args{"%U.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"d", args{"%D.zone"}, `(.*)\.zone`, false},

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
}