From ed5ff58205a063187b3c6c6ca5134da55c834ea8 Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Tue, 7 Apr 2026 16:10:33 +0200 Subject: [PATCH 01/10] NEW PROVIDER: Netnod primary DNS Add a DNSControl provider for Netnod primary DNS API. Provider is loosely based on the PowerDNS provider. --- .github/workflows/pr_integration_tests.yml | 6 +- .goreleaser.yml | 2 +- OWNERS | 1 + README.md | 1 + documentation/SUMMARY.md | 1 + documentation/provider/index.md | 7 ++ documentation/provider/netnod.md | 59 +++++++++++ go.mod | 3 +- go.sum | 2 + integrationTest/profiles.json | 6 ++ pkg/providers/_all/all.go | 1 + providers/netnod/auditrecords.go | 18 ++++ providers/netnod/convert.go | 40 ++++++++ providers/netnod/convert_test.go | 67 +++++++++++++ providers/netnod/diff.go | 108 +++++++++++++++++++++ providers/netnod/dns.go | 94 ++++++++++++++++++ providers/netnod/listzones.go | 17 ++++ providers/netnod/netnodProvider.go | 92 ++++++++++++++++++ 18 files changed, 522 insertions(+), 3 deletions(-) create mode 100644 documentation/provider/netnod.md create mode 100644 providers/netnod/auditrecords.go create mode 100644 providers/netnod/convert.go create mode 100644 providers/netnod/convert_test.go create mode 100644 providers/netnod/diff.go create mode 100644 providers/netnod/dns.go create mode 100644 providers/netnod/listzones.go create mode 100644 providers/netnod/netnodProvider.go diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index e63bb95fe6..45c0aedd4c 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -65,7 +65,7 @@ jobs: Write-Host "Integration test providers: $Providers" echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT env: - PROVIDERS: "['ALIDNS', 'AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','GIDINET','HEDNS','HETZNER_V2','HUAWEICLOUD','INWX','JOKER','MIKROTIK','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP','UNIFI']" + PROVIDERS: "['ALIDNS', 'AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','GIDINET','HEDNS','HETZNER_V2','HUAWEICLOUD','INWX','JOKER','MIKROTIK','MYTHICBEASTS', 'NAMEDOTCOM','NETNOD','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP','UNIFI']" ENV_CONTEXT: ${{ toJson(env) }} VARS_CONTEXT: ${{ toJson(vars) }} SECRETS_CONTEXT: ${{ toJson(secrets) }} @@ -108,6 +108,7 @@ jobs: MIKROTIK_DOMAIN: ${{ vars.MIKROTIK_DOMAIN }} MYTHICBEASTS_DOMAIN: ${{ vars.MYTHICBEASTS_DOMAIN }} NAMEDOTCOM_DOMAIN: ${{ vars.NAMEDOTCOM_DOMAIN }} + NETNOD_DOMAIN: ${{ vars.NETNOD_DOMAIN }} NS1_DOMAIN: ${{ vars.NS1_DOMAIN }} POWERDNS_DOMAIN: ${{ vars.POWERDNS_DOMAIN }} ROUTE53_DOMAIN: ${{ vars.ROUTE53_DOMAIN }} @@ -197,6 +198,9 @@ jobs: NAMEDOTCOM_URL: ${{ secrets.NAMEDOTCOM_URL }} NAMEDOTCOM_USER: ${{ secrets.NAMEDOTCOM_USER }} # + NETNOD_APIKEY: ${{ secrets.NETNOD_APIKEY }} + NETNOD_APIURL: ${{ secrets.NETNOD_APIURL }} + # NS1_TOKEN: ${{ secrets.NS1_TOKEN }} # POWERDNS_APIKEY: ${{ secrets.POWERDNS_APIKEY }} diff --git a/.goreleaser.yml b/.goreleaser.yml index fce02ca7cd..ca99f87979 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -37,7 +37,7 @@ changelog: regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$" order: 1 - title: 'Provider-specific changes:' - regexp: "(?i)((adguardhome|akamaiedgedns|alidns|autodns|axfrddns|azure_dns|azure_private_dns|bind|bunny_dns|cloudflare|cloudflareapi|cloudns|cnr|cscglobal|desec|digitalocean|dnscale|dnsimple|dnsmadeeasy|dnsoverhttps|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi_v5|gcloud|gcore|gidinet|hedns|hetzner|hetzner_v2|hostingde|huaweicloud|infomaniak|internetbs|inwx|joker|linode|loopia|luadns|mikrotik|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|unifi|vercel|vultr).*:)+.*" + regexp: "(?i)((adguardhome|akamaiedgedns|alidns|autodns|axfrddns|azure_dns|azure_private_dns|bind|bunny_dns|cloudflare|cloudflareapi|cloudns|cnr|cscglobal|desec|digitalocean|dnscale|dnsimple|dnsmadeeasy|dnsoverhttps|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi_v5|gcloud|gcore|gidinet|hedns|hetzner|hetzner_v2|hostingde|huaweicloud|infomaniak|internetbs|inwx|joker|linode|loopia|luadns|mikrotik|mythicbeasts|namecheap|namedotcom|netcup|netlify|netnod|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|unifi|vercel|vultr).*:)+.*" order: 2 - title: 'Documentation:' regexp: "(?i)^.*(docs)[(\\w)]*:+.*$" diff --git a/OWNERS b/OWNERS index 13ab5ad029..3fe9f197bb 100644 --- a/OWNERS +++ b/OWNERS @@ -44,6 +44,7 @@ providers/namecheap @willpower232 # providers/namedotcom NEEDS VOLUNTEER providers/netcup @kordianbruck providers/netlify @SphericalKat +providers/netnod @Netnod providers/ns1 @costasd # providers/opensrs NEEDS VOLUNTEER providers/oracle @kallsyms diff --git a/README.md b/README.md index e580dc8715..435fc6627e 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Currently supported DNS providers: - Namecheap - Netcup - Netlify +- Netnod - NS1 - Oracle Cloud - OVH diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index d12f5e864b..f7ac2cbbf4 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -168,6 +168,7 @@ * [Name.com](provider/namedotcom.md) * [Netcup](provider/netcup.md) * [Netlify](provider/netlify.md) +* [Netnod](provider/netnod.md) * [NS1](provider/ns1.md) * [OpenSRS](provider/opensrs.md) * [Oracle Cloud](provider/oracle.md) diff --git a/documentation/provider/index.md b/documentation/provider/index.md index d25adf4d33..7c94809e18 100644 --- a/documentation/provider/index.md +++ b/documentation/provider/index.md @@ -71,6 +71,7 @@ Jump to a table: | [`NAMEDOTCOM`](namedotcom.md) | ❌ | ✅ | ✅ | | [`NETCUP`](netcup.md) | ❌ | ✅ | ❌ | | [`NETLIFY`](netlify.md) | ❌ | ✅ | ❌ | +| [`NETNOD`](netnod.md) | ❌ | ✅ | ❌ | | [`NS1`](ns1.md) | ❌ | ✅ | ❌ | | [`OPENSRS`](opensrs.md) | ❌ | ❌ | ✅ | | [`ORACLE`](oracle.md) | ❌ | ✅ | ❌ | @@ -138,6 +139,7 @@ Jump to a table: | [`NAMEDOTCOM`](namedotcom.md) | ❔ | ✅ | ❌ | ✅ | | [`NETCUP`](netcup.md) | ❔ | ❌ | ❌ | ❌ | | [`NETLIFY`](netlify.md) | ✅ | ❌ | ❌ | ✅ | +| [`NETNOD`](netnod.md) | ❔ | ✅ | ✅ | ✅ | | [`NS1`](ns1.md) | ✅ | ✅ | ✅ | ✅ | | [`OPENSRS`](opensrs.md) | ❔ | ❔ | ❌ | ❔ | | [`ORACLE`](oracle.md) | ❔ | ✅ | ✅ | ✅ | @@ -201,6 +203,7 @@ Jump to a table: | [`NAMEDOTCOM`](namedotcom.md) | ✅ | ❔ | ❌ | ❌ | ❔ | | [`NETCUP`](netcup.md) | ❔ | ❔ | ❌ | ❌ | ❔ | | [`NETLIFY`](netlify.md) | ✅ | ❔ | ❌ | ❌ | ❔ | +| [`NETNOD`](netnod.md) | ✅ | ❌ | ❌ | ✅ | ❌ | | [`NS1`](ns1.md) | ✅ | ✅ | ❌ | ✅ | ❔ | | [`ORACLE`](oracle.md) | ✅ | ❔ | ❔ | ✅ | ❔ | | [`OVH`](ovh.md) | ❌ | ❔ | ❔ | ❌ | ❔ | @@ -262,6 +265,7 @@ Jump to a table: | [`NAMEDOTCOM`](namedotcom.md) | ❔ | ❔ | ✅ | ❔ | | [`NETCUP`](netcup.md) | ❔ | ❔ | ✅ | ❔ | | [`NETLIFY`](netlify.md) | ❔ | ❌ | ✅ | ❔ | +| [`NETNOD`](netnod.md) | ❌ | ❌ | ✅ | ❌ | | [`NS1`](ns1.md) | ✅ | ✅ | ✅ | ✅ | | [`ORACLE`](oracle.md) | ❔ | ✅ | ✅ | ❔ | | [`OVH`](ovh.md) | ❔ | ❔ | ✅ | ❔ | @@ -322,6 +326,7 @@ Jump to a table: | [`NAMECHEAP`](namecheap.md) | ✅ | ❔ | ❔ | ❔ | ❌ | | [`NETCUP`](netcup.md) | ✅ | ❔ | ❔ | ❔ | ✅ | | [`NETLIFY`](netlify.md) | ✅ | ❔ | ❔ | ❌ | ❌ | +| [`NETNOD`](netnod.md) | ✅ | ✅ | ❔ | ❌ | ✅ | | [`NS1`](ns1.md) | ✅ | ✅ | ❔ | ❔ | ✅ | | [`ORACLE`](oracle.md) | ✅ | ❔ | ❔ | ✅ | ✅ | | [`OVH`](ovh.md) | ✅ | ❔ | ❔ | ✅ | ✅ | @@ -369,6 +374,7 @@ Jump to a table: | [`LOOPIA`](loopia.md) | ❌ | ❌ | ❌ | | [`MIKROTIK`](mikrotik.md) | ❌ | ❔ | ❌ | | [`NETLIFY`](netlify.md) | ❌ | ❔ | ❌ | +| [`NETNOD`](netnod.md) | ❌ | ❌ | ❌ | | [`NS1`](ns1.md) | ✅ | ❔ | ✅ | | [`ORACLE`](oracle.md) | ❔ | ❔ | ❌ | | [`PORKBUN`](porkbun.md) | ❌ | ❔ | ❌ | @@ -458,6 +464,7 @@ Providers in this category and their maintainers are: |[`NAMECHEAP`](namecheap.md)|@willpower232| |[`NETCUP`](netcup.md)|@kordianbruck| |[`NETLIFY`](netlify.md)|@SphericalKat| +|[`NETNOD`](netnod.md)|@Netnod| |[`NS1`](ns1.md)|@costasd| |[`OPENSRS`](opensrs.md)|@philhug| |[`ORACLE`](oracle.md)|@kallsyms| diff --git a/documentation/provider/netnod.md b/documentation/provider/netnod.md new file mode 100644 index 0000000000..565dc3e263 --- /dev/null +++ b/documentation/provider/netnod.md @@ -0,0 +1,59 @@ +## Configuration + +To use this provider, add an entry to `creds.json` with `TYPE` set to `NETNOD` along with your API URL and API Key. The API URL can be omitted to use the default value `https://primarydnsapi.netnod.se`. + +Example: + +{% code title="creds.json" %} + +```json +{ + "netnod": { + "TYPE": "NETNOD", + "apiKey": "your-key", + "apiUrl": "https://primarydnsapi.netnod.se" + } +} +``` + +{% endcode %} + +## Metadata + +The following provider metadata is available: + +{% code title="dnsconfig.js" %} + +```javascript +var DSP_NETNOD = NewDnsProvider('netnod', { + default_ns: ['a.example.com.', 'b.example.com.'], + also_notify: ['192.36.148.17', '2001:7fe::53'], + allow_transfer_keys: ['netnod-key1.'], +}); +``` + +{% endcode %} + +- `default_ns` sets the nameservers used when creating zones. +- `also_notify` sets a list of IP addresses that will receive DNS NOTIFY messages when a zone is created. This is the provider-level default and applies to all zones unless overridden per zone (see below). +- `allow_transfer_keys` sets the TSIG key IDs permitted to perform zone transfers from the distribution servers when a zone is created. + This should include all keys used for DNS secondary replication, including those used by the Netnod secondary DNS service. This is the provider-level default and applies to all zones unless overridden per zone. + +## Usage + +An example configuration: + +{% code title="dnsconfig.js" %} + +```javascript +var REG_NONE = NewRegistrar('none'); +var DSP_NETNOD = NewDnsProvider('netnod'); + +D('example.com', REG_NONE, DnsProvider(DSP_NETNOD), A('test', '1.2.3.4')); +``` + +{% endcode %} + +## Activation + +See the [Netnod DNS](https://www.netnod.se/dns/dns-enterprise-services). diff --git a/go.mod b/go.mod index 91bdcc71cf..db25c21440 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/StackExchange/dnscontrol/v4 -go 1.25.0 +go 1.26 retract v4.8.0 @@ -77,6 +77,7 @@ require ( github.com/kylelemons/godebug v1.1.0 github.com/luadns/luadns-go v0.3.0 github.com/mattn/go-isatty v0.0.20 + github.com/netnod/netnod-primary-dns-client v1.0.0 github.com/nicholas-fedor/shoutrrr v0.13.2 github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 github.com/oracle/oci-go-sdk/v65 v65.109.0 diff --git a/go.sum b/go.sum index 5f78f0fb7a..d99f4749c4 100644 --- a/go.sum +++ b/go.sum @@ -340,6 +340,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= +github.com/netnod/netnod-primary-dns-client v1.0.0 h1:ISyr7eOsQtUAZ4c1ov0sFV22I4yB09Du1kf9zy/q85U= +github.com/netnod/netnod-primary-dns-client v1.0.0/go.mod h1:MLvJf4UhbAB8WQdypLqYv9rwtOGD+3cZlAVXMwgad3c= github.com/nicholas-fedor/shoutrrr v0.13.2 h1:hfsYBIqSFYGg92pZP5CXk/g7/OJIkLYmiUnRl+AD1IA= github.com/nicholas-fedor/shoutrrr v0.13.2/go.mod h1:ZqzV3gY/Wj6AvWs1etlO7+yKbh4iptSbeL8avBpMQbA= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= diff --git a/integrationTest/profiles.json b/integrationTest/profiles.json index b20a260d29..1e05a486b8 100644 --- a/integrationTest/profiles.json +++ b/integrationTest/profiles.json @@ -285,6 +285,12 @@ "slug": "$NETLIFY_ACCOUNT_SLUG", "token": "$NETLIFY_TOKEN" }, + "NETNOD": { + "TYPE": "NETNOD", + "apiKey": "$NETNOD_APIKEY", + "apiUrl": "$NETNOD_APIURL", + "domain": "$NETNOD_DOMAIN" + }, "NS1": { "TYPE": "NS1", "api_token": "$NS1_TOKEN", diff --git a/pkg/providers/_all/all.go b/pkg/providers/_all/all.go index 621613c877..e7856aebf8 100644 --- a/pkg/providers/_all/all.go +++ b/pkg/providers/_all/all.go @@ -49,6 +49,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v4/providers/namedotcom" _ "github.com/StackExchange/dnscontrol/v4/providers/netcup" _ "github.com/StackExchange/dnscontrol/v4/providers/netlify" + _ "github.com/StackExchange/dnscontrol/v4/providers/netnod" _ "github.com/StackExchange/dnscontrol/v4/providers/ns1" _ "github.com/StackExchange/dnscontrol/v4/providers/opensrs" _ "github.com/StackExchange/dnscontrol/v4/providers/oracle" diff --git a/providers/netnod/auditrecords.go b/providers/netnod/auditrecords.go new file mode 100644 index 0000000000..3e51ce4d1a --- /dev/null +++ b/providers/netnod/auditrecords.go @@ -0,0 +1,18 @@ +package netnod + +import ( + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" +) + +// 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 { + a := rejectif.Auditor{} + + a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-11-11 + a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2023-11-11 + + return a.Audit(records) +} diff --git a/providers/netnod/convert.go b/providers/netnod/convert.go new file mode 100644 index 0000000000..af661ca67b --- /dev/null +++ b/providers/netnod/convert.go @@ -0,0 +1,40 @@ +package netnod + +import ( + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" +) + +// toRecordConfig converts a Netnod DNS Record to a RecordConfig. #rtype_variations +func toRecordConfig(domain string, r netnodPrimaryDNS.Record, ttl int, name string, rtype string) (*models.RecordConfig, error) { + // trimming trailing dot and domain from name + name = strings.TrimSuffix(name, domain+".") + name = strings.TrimSuffix(name, ".") + + rc := &models.RecordConfig{ + TTL: uint32(ttl), + Original: r, + Type: rtype, + } + rc.SetLabel(name, domain) + + switch rtype { + case "TXT": + // API accepts long TXTs without requiring to split them. + // The API then returns them as they initially came in, e.g. "averylooooooo[...]oooooongstring" or "string" "string" + // So we need to strip away " and split into multiple string + // We can't use SetTargetRFC1035Quoted, it would split the long strings into multiple parts + return rc, rc.SetTargetTXTs(parseTxt(r.Content)) + default: + return rc, rc.PopulateFromString(rtype, r.Content, domain) + } +} + +func parseTxt(content string) (result []string) { + for r := range strings.SplitSeq(content, "\" ") { + result = append(result, strings.Trim(r, "\"")) + } + return +} diff --git a/providers/netnod/convert_test.go b/providers/netnod/convert_test.go new file mode 100644 index 0000000000..8e8cd67a35 --- /dev/null +++ b/providers/netnod/convert_test.go @@ -0,0 +1,67 @@ +package netnod + +import ( + "fmt" + "strings" + "testing" + + netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" + "github.com/stretchr/testify/assert" +) + +func TestToRecordConfig(t *testing.T) { + record := netnodPrimaryDNS.Record{ + Content: "simple", + } + recordConfig, err := toRecordConfig("example.com", record, 120, "test", "TXT") + + assert.NoError(t, err) + assert.Equal(t, "test.example.com", recordConfig.NameFQDN) + assert.Equal(t, "\"simple\"", recordConfig.String()) + assert.Equal(t, uint32(120), recordConfig.TTL) + assert.Equal(t, "TXT", recordConfig.Type) + + largeContent := fmt.Sprintf("\"%s\" \"%s\"", strings.Repeat("A", 300), strings.Repeat("B", 300)) + largeRecord := netnodPrimaryDNS.Record{ + Content: largeContent, + } + recordConfig, err = toRecordConfig("example.com", largeRecord, 5, "large", "TXT") + + assert.NoError(t, err) + assert.Equal(t, "large.example.com", recordConfig.NameFQDN) + assert.Equal(t, `"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"`, + recordConfig.String()) + assert.Equal(t, uint32(5), recordConfig.TTL) + assert.Equal(t, "TXT", recordConfig.Type) + + luaRecord := netnodPrimaryDNS.Record{ + Content: "TXT \"return 'Hello, world!'\"", + } + recordConfig, err = toRecordConfig("example.com", luaRecord, 3600, "script", "LUA") + + assert.NoError(t, err) + assert.Equal(t, "script.example.com", recordConfig.NameFQDN) + assert.Equal(t, "LUA", recordConfig.Type) + assert.Equal(t, "TXT", recordConfig.LuaRType) + assert.Equal(t, "return 'Hello, world!'", recordConfig.GetTargetTXTJoined()) + assert.Equal(t, "TXT \"return 'Hello, world!'\"", recordConfig.GetTargetCombined()) + assert.Equal(t, uint32(3600), recordConfig.TTL) +} + +func TestParseText(t *testing.T) { + // short TXT record + short := parseTxt("\"simple\"") + assert.Equal(t, []string{"simple"}, short) + + // TXT record with multiple parts + multiple := parseTxt("\"simple\" \"simple2\"") + assert.Equal(t, []string{"simple", "simple2"}, multiple) + + // long TXT record + long := parseTxt(fmt.Sprintf("\"%s\"", strings.Repeat("A", 300))) + assert.Equal(t, []string{strings.Repeat("A", 300)}, long) + + // multiple long TXT record + multipleLong := parseTxt(fmt.Sprintf("\"%s\" \"%s\"", strings.Repeat("A", 300), strings.Repeat("B", 300))) + assert.Equal(t, []string{strings.Repeat("A", 300), strings.Repeat("B", 300)}, multipleLong) +} diff --git a/providers/netnod/diff.go b/providers/netnod/diff.go new file mode 100644 index 0000000000..73272279a4 --- /dev/null +++ b/providers/netnod/diff.go @@ -0,0 +1,108 @@ +package netnod + +import ( + "fmt" + "regexp" + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/fatih/color" + netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" +) + +func (dsp *netnodProvider) getDiff2DomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) { + changes, actualChangeCount, err := diff2.ByRecordSet(existing, dc, nil) + if err != nil { + return nil, 0, err + } + + var corrections []*models.Correction + var changeMsgs []string + var rrChangeSets []netnodPrimaryDNS.RRset + var deleteMsgs []string + var rrDeleteSets []netnodPrimaryDNS.RRset + + // for pretty alignment, add an empty string + changeMsgs = append(changeMsgs, color.YellowString("± BATCHED CHANGE/CREATEs for %s", dc.Name)) + deleteMsgs = append(deleteMsgs, color.RedString("- BATCHED DELETEs for %s", dc.Name)) + + for _, change := range changes { + labelName := canonical(change.Key.NameFQDN) + labelType := change.Key.Type + + switch change.Type { + case diff2.REPORT: + corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined}) + case diff2.CREATE, diff2.CHANGE: + labelTTL := int64(change.New[0].TTL) + records := buildRecordList(change) + + rrChangeSets = append(rrChangeSets, netnodPrimaryDNS.RRset{ + Name: labelName, + Type: labelType, + TTL: &labelTTL, + Records: records, + ChangeType: "REPLACE", + }) + changeMsgs = append(changeMsgs, change.MsgsJoined) + case diff2.DELETE: + rrDeleteSets = append(rrDeleteSets, netnodPrimaryDNS.RRset{ + Name: labelName, + Type: labelType, + ChangeType: "DELETE", + }) + deleteMsgs = append(deleteMsgs, change.MsgsJoined) + default: + panic(fmt.Sprintf("unhandled change.Type %s", change.Type)) + } + } + + domainVariant := dc.Name + "." + + // only append a Correction if there are any, otherwise causes an error when sending an empty rrset + if len(rrDeleteSets) > 0 { + corrections = append(corrections, &models.Correction{ + Msg: strings.Join(deleteMsgs, "\n"), + F: func() error { + return dsp.client.PatchZoneRRsets(domainVariant, rrDeleteSets) + }, + }) + } + if len(rrChangeSets) > 0 { + corrections = append(corrections, &models.Correction{ + Msg: strings.Join(changeMsgs, "\n"), + F: func() error { + return dsp.client.PatchZoneRRsets(domainVariant, rrChangeSets) + }, + }) + } + return corrections, actualChangeCount, nil +} + +// httpsParamQuoteRe matches HTTPS SVCB parameter values that are quoted but +// don't contain characters requiring quoting (+ or /). These are stripped of +// their quotes before sending to the API (e.g. alpn="h2,h3" => alpn=h2,h3). +// Values containing + or / (e.g. ECH base64 data) retain their quotes. +var httpsParamQuoteRe = regexp.MustCompile(`="([^"+/ ]*)"`) + +// buildRecordList returns a list of records for the resource record set from a change +func buildRecordList(change diff2.Change) (records []netnodPrimaryDNS.Record) { + for _, recordContent := range change.New { + record := netnodPrimaryDNS.Record{ + Content: recordContent.GetTargetCombined(), + } + if recordContent.Type == "HTTPS" { + // The API rejects double-quoted simple param values (e.g. alpn="h2,h3") + // but requires quotes around values containing + or / (e.g. ECH base64). + // Strip quotes only from values that don't contain those characters. + record.Content = httpsParamQuoteRe.ReplaceAllString(record.Content, `=$1`) + } + records = append(records, record) + } + return +} + +func canonical(fqdn string) string { + return fqdn + "." +} diff --git a/providers/netnod/dns.go b/providers/netnod/dns.go new file mode 100644 index 0000000000..e5053d159a --- /dev/null +++ b/providers/netnod/dns.go @@ -0,0 +1,94 @@ +package netnod + +import ( + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + + netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" +) + +// GetNameservers returns the nameservers for a domain. +func (dsp *netnodProvider) GetNameservers(string) ([]*models.Nameserver, error) { + var r []string + for _, j := range dsp.nameservers { + r = append(r, j.Name) + } + return models.ToNameservers(r) +} + +// GetZoneRecords gets the records of a zone and returns them in RecordConfig format. +func (dsp *netnodProvider) GetZoneRecords(dc *models.DomainConfig) (models.Records, error) { + domain := dc.Name + curRecords := models.Records{} + domainVariant := domain + "." + zone, err := dsp.client.GetZone(domainVariant) + if err != nil { + return nil, err + } + if zone == nil { + return curRecords, nil + } + + // loop over grouped records by type, called RRSet + for _, rrset := range zone.RRsets { + // Skip SOA records - they are managed by the provider + if rrset.Type == "SOA" { + continue + } + ttl := 0 + if rrset.TTL != nil { + ttl = int(*rrset.TTL) + } + // loop over single records of this group and create records + for _, record := range rrset.Records { + r, err := toRecordConfig(domain, record, ttl, rrset.Name, rrset.Type) + if err != nil { + return nil, err + } + curRecords = append(curRecords, r) + } + } + + return curRecords, nil +} + +// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. +func (dsp *netnodProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) { + corrections, actualChangeCount, err := dsp.getDiff2DomainCorrections(dc, existing) + if err != nil { + return nil, 0, err + } + + return corrections, actualChangeCount, nil +} + +// EnsureZoneExists creates a zone if it does not exist +func (dsp *netnodProvider) EnsureZoneExists(domain string, metadata map[string]string) error { + domainVariant := domain + "." + zone, err := dsp.client.GetZone(domainVariant) + if err != nil { + return err + } + if zone != nil { + return nil + } + + // Per-zone overrides take precedence over provider-level defaults. + alsoNotify := dsp.AlsoNotify + if v, ok := metadata["also_notify"]; ok { + alsoNotify = strings.Split(v, ",") + } + + allowTransferKeys := dsp.AllowTransferKeys + if v, ok := metadata["allow_transfer_keys"]; ok { + allowTransferKeys = strings.Split(v, ",") + } + + _, err = dsp.client.CreateZone(&netnodPrimaryDNS.Zone{ + Name: domainVariant, + AllowTransferKeys: allowTransferKeys, + AlsoNotify: alsoNotify, + }) + return err +} diff --git a/providers/netnod/listzones.go b/providers/netnod/listzones.go new file mode 100644 index 0000000000..ea514a48a3 --- /dev/null +++ b/providers/netnod/listzones.go @@ -0,0 +1,17 @@ +package netnod + +import "strings" + +// ListZones returns all the zones in an account +func (dsp *netnodProvider) ListZones() ([]string, error) { + zones, err := dsp.client.ListZones() + if err != nil { + return nil, err + } + + var result []string + for _, zone := range zones { + result = append(result, strings.TrimSuffix(zone.Name, ".")) + } + return result, nil +} diff --git a/providers/netnod/netnodProvider.go b/providers/netnod/netnodProvider.go new file mode 100644 index 0000000000..162a297aa5 --- /dev/null +++ b/providers/netnod/netnodProvider.go @@ -0,0 +1,92 @@ +package netnod + +import ( + "encoding/json" + "errors" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/providers" + netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" +) + +var features = providers.DocumentationNotes{ + // The default for unlisted capabilities is 'Cannot'. + // See providers/capabilities.go for the entire list of capabilities. + providers.CanAutoDNSSEC: providers.Cannot(), + providers.CanGetZones: providers.Can(), + providers.CanConcur: providers.Unimplemented(), + providers.CanUseAlias: providers.Can(), + providers.CanUseCAA: providers.Can(), + providers.CanUseDHCID: providers.Cannot(), + providers.CanUseDNAME: providers.Cannot(), + providers.CanUseDNSKEY: providers.Cannot(), + providers.CanUseDS: providers.Cannot(), + providers.CanUseHTTPS: providers.Can(), + providers.CanUseLOC: providers.Cannot(), + providers.CanUseNAPTR: providers.Cannot(), + providers.CanUseOPENPGPKEY: providers.Cannot(), + providers.CanUsePTR: providers.Can(), + providers.CanUseSOA: providers.Cannot(), + providers.CanUseSRV: providers.Can(), + providers.CanUseSSHFP: providers.Cannot(), + providers.CanUseSVCB: providers.Cannot(), + providers.CanUseTLSA: providers.Can(), + providers.DocCreateDomains: providers.Can(), + providers.DocDualHost: providers.Can(), + providers.DocOfficiallySupported: providers.Cannot(), +} + +func init() { + const providerName = "NETNOD" + const providerMaintainer = "@Netnod" + fns := providers.DspFuncs{ + Initializer: newDSP, + RecordAuditor: AuditRecords, + } + providers.RegisterDomainServiceProviderType(providerName, fns, features) + providers.RegisterMaintainer(providerName, providerMaintainer) +} + +// netnodProvider represents the netnodProvider DNSServiceProvider. +type netnodProvider struct { + client *netnodPrimaryDNS.Client + APIKey string + APIUrl string + DefaultNS []string `json:"default_ns"` + AlsoNotify []string `json:"also_notify"` + AllowTransferKeys []string `json:"allow_transfer_keys"` + + nameservers []*models.Nameserver +} + +// newDSP initializes a Netnod DNSServiceProvider. +func newDSP(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { + dsp := &netnodProvider{} + + dsp.APIKey = m["apiKey"] + if dsp.APIKey == "" { + return nil, errors.New("Netnod API Key is required") + } + + dsp.APIUrl = m["apiUrl"] + + // load js config + if len(metadata) != 0 { + err := json.Unmarshal(metadata, dsp) + if err != nil { + return nil, err + } + } + var nss []string + for _, ns := range dsp.DefaultNS { + nss = append(nss, ns[0:len(ns)-1]) + } + var err error + dsp.nameservers, err = models.ToNameservers(nss) + if err != nil { + return dsp, err + } + + dsp.client = netnodPrimaryDNS.NewClient(dsp.APIUrl, dsp.APIKey) + return dsp, nil +} From 0b7b790698048306852e67972bab581d59561819 Mon Sep 17 00:00:00 2001 From: flindeberg <11232267+flindeberg@users.noreply.github.com> Date: Fri, 8 May 2026 16:47:38 +0200 Subject: [PATCH 02/10] Resolve merge conflict in go.mod Fixed typos / artifacts from web-GUI based merge. --- go.mod | 5 ----- 1 file changed, 5 deletions(-) diff --git a/go.mod b/go.mod index 0dac12eee7..0a7e378abf 100644 --- a/go.mod +++ b/go.mod @@ -80,14 +80,9 @@ require ( github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.195 github.com/kylelemons/godebug v1.1.0 github.com/luadns/luadns-go v0.3.0 -<<<<<<< add-netnod-provider - github.com/mattn/go-isatty v0.0.20 github.com/netnod/netnod-primary-dns-client v1.0.0 - github.com/nicholas-fedor/shoutrrr v0.13.2 -======= github.com/mattn/go-isatty v0.0.22 github.com/nicholas-fedor/shoutrrr v0.15.0 ->>>>>>> main github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 github.com/oracle/oci-go-sdk/v65 v65.114.1 github.com/urfave/cli/v3 v3.8.0 From 4eb0317c0655c25c737266163fa8be285762b347 Mon Sep 17 00:00:00 2001 From: flindeberg <11232267+flindeberg@users.noreply.github.com> Date: Fri, 8 May 2026 16:48:24 +0200 Subject: [PATCH 03/10] Fix merge conflicts and update shoutrrr version Fixed web-GUI based merge artifacts. --- go.sum | 5 ----- 1 file changed, 5 deletions(-) diff --git a/go.sum b/go.sum index 10fd297fff..8ac414b26a 100644 --- a/go.sum +++ b/go.sum @@ -344,15 +344,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= -<<<<<<< add-netnod-provider github.com/netnod/netnod-primary-dns-client v1.0.0 h1:ISyr7eOsQtUAZ4c1ov0sFV22I4yB09Du1kf9zy/q85U= github.com/netnod/netnod-primary-dns-client v1.0.0/go.mod h1:MLvJf4UhbAB8WQdypLqYv9rwtOGD+3cZlAVXMwgad3c= -github.com/nicholas-fedor/shoutrrr v0.13.2 h1:hfsYBIqSFYGg92pZP5CXk/g7/OJIkLYmiUnRl+AD1IA= -github.com/nicholas-fedor/shoutrrr v0.13.2/go.mod h1:ZqzV3gY/Wj6AvWs1etlO7+yKbh4iptSbeL8avBpMQbA= -======= github.com/nicholas-fedor/shoutrrr v0.15.0 h1:4gKIev9ucsY50dy+GkkPQKyfIJdKOEwr0dnsVpGdDWk= github.com/nicholas-fedor/shoutrrr v0.15.0/go.mod h1:Z6b9KNn8q9nXl27/p39zo7iJHBmqkigzVxWnbfwcU8w= ->>>>>>> main 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= From baa2925fc5fce86d220a8bcb142c56f0f7f24636 Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Sun, 10 May 2026 23:32:13 +0200 Subject: [PATCH 04/10] fix: rename providers/netnod/diff.go > providers/netnod/records.go --- providers/netnod/{diff.go => records.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename providers/netnod/{diff.go => records.go} (100%) diff --git a/providers/netnod/diff.go b/providers/netnod/records.go similarity index 100% rename from providers/netnod/diff.go rename to providers/netnod/records.go From 973361b26c26d933740b0f14d8c837df16c15333 Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Tue, 19 May 2026 22:28:34 +0200 Subject: [PATCH 05/10] DOCS(netnod): remove old referenced to 'see below' --- documentation/provider/netnod.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/provider/netnod.md b/documentation/provider/netnod.md index 565dc3e263..86e8ac27a1 100644 --- a/documentation/provider/netnod.md +++ b/documentation/provider/netnod.md @@ -35,7 +35,7 @@ var DSP_NETNOD = NewDnsProvider('netnod', { {% endcode %} - `default_ns` sets the nameservers used when creating zones. -- `also_notify` sets a list of IP addresses that will receive DNS NOTIFY messages when a zone is created. This is the provider-level default and applies to all zones unless overridden per zone (see below). +- `also_notify` sets a list of IP addresses that will receive DNS NOTIFY messages when a zone is created. This is the provider-level default and applies to all zones unless overridden per zone. - `allow_transfer_keys` sets the TSIG key IDs permitted to perform zone transfers from the distribution servers when a zone is created. This should include all keys used for DNS secondary replication, including those used by the Netnod secondary DNS service. This is the provider-level default and applies to all zones unless overridden per zone. From 94b7ae1876f829f7b2bb6ef3ac0ba87dc235e57f Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Tue, 19 May 2026 22:29:27 +0200 Subject: [PATCH 06/10] NETNOD: update last verified date in auditrecords.go --- providers/netnod/auditrecords.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/netnod/auditrecords.go b/providers/netnod/auditrecords.go index 3e51ce4d1a..2a1a7a0c0c 100644 --- a/providers/netnod/auditrecords.go +++ b/providers/netnod/auditrecords.go @@ -11,8 +11,8 @@ import ( func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} - a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-11-11 - a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2023-11-11 + a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2026-05-19 + a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2026-05-19 return a.Audit(records) } From f873f38fe774f97d1d20673cbc12eae5044191e0 Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Tue, 19 May 2026 22:39:58 +0200 Subject: [PATCH 07/10] NETNOD: fix invalid module path since repo migration --- providers/netnod/auditrecords.go | 4 ++-- providers/netnod/convert.go | 2 +- providers/netnod/dns.go | 2 +- providers/netnod/netnodProvider.go | 4 ++-- providers/netnod/records.go | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/providers/netnod/auditrecords.go b/providers/netnod/auditrecords.go index 2a1a7a0c0c..ad879f558b 100644 --- a/providers/netnod/auditrecords.go +++ b/providers/netnod/auditrecords.go @@ -1,8 +1,8 @@ package netnod import ( - "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" + "github.com/DNSControl/dnscontrol/v4/models" + "github.com/DNSControl/dnscontrol/v4/pkg/rejectif" ) // AuditRecords returns a list of errors corresponding to the records diff --git a/providers/netnod/convert.go b/providers/netnod/convert.go index af661ca67b..b1b79fe397 100644 --- a/providers/netnod/convert.go +++ b/providers/netnod/convert.go @@ -3,7 +3,7 @@ package netnod import ( "strings" - "github.com/StackExchange/dnscontrol/v4/models" + "github.com/DNSControl/dnscontrol/v4/models" netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" ) diff --git a/providers/netnod/dns.go b/providers/netnod/dns.go index e5053d159a..7d6e74a37f 100644 --- a/providers/netnod/dns.go +++ b/providers/netnod/dns.go @@ -3,7 +3,7 @@ package netnod import ( "strings" - "github.com/StackExchange/dnscontrol/v4/models" + "github.com/DNSControl/dnscontrol/v4/models" netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" ) diff --git a/providers/netnod/netnodProvider.go b/providers/netnod/netnodProvider.go index 162a297aa5..93571f3e30 100644 --- a/providers/netnod/netnodProvider.go +++ b/providers/netnod/netnodProvider.go @@ -4,8 +4,8 @@ import ( "encoding/json" "errors" - "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/pkg/providers" + "github.com/DNSControl/dnscontrol/v4/models" + "github.com/DNSControl/dnscontrol/v4/pkg/providers" netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" ) diff --git a/providers/netnod/records.go b/providers/netnod/records.go index 73272279a4..44e79ed193 100644 --- a/providers/netnod/records.go +++ b/providers/netnod/records.go @@ -5,8 +5,8 @@ import ( "regexp" "strings" - "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/DNSControl/dnscontrol/v4/models" + "github.com/DNSControl/dnscontrol/v4/pkg/diff2" "github.com/fatih/color" netnodPrimaryDNS "github.com/netnod/netnod-primary-dns-client" ) From de822167965e8f8e50c12e556e32454cbcdf498f Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Tue, 19 May 2026 23:00:10 +0200 Subject: [PATCH 08/10] NETNOD: add RegisterCredsMetadata --- providers/netnod/netnodProvider.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/providers/netnod/netnodProvider.go b/providers/netnod/netnodProvider.go index 93571f3e30..cd17d55d7c 100644 --- a/providers/netnod/netnodProvider.go +++ b/providers/netnod/netnodProvider.go @@ -45,6 +45,28 @@ func init() { } providers.RegisterDomainServiceProviderType(providerName, fns, features) providers.RegisterMaintainer(providerName, providerMaintainer) + providers.RegisterCredsMetadata(providerName, providers.CredsMetadata{ + DisplayName: "Netnod", + Kind: providers.KindDNS, + DocsURL: "https://docs.dnscontrol.org/provider/netnod", + PortalURL: "https://www.netnod.se/dns/dns-enterprise-services", + Notes: "An API key is required. The API URL defaults to https://primarydnsapi.netnod.se and can be omitted.", + Fields: []providers.CredsField{ + { + Key: "apiKey", + Label: "API key", + Help: "API key for the Netnod Primary DNS API.", + Secret: true, + Required: true, + }, + { + Key: "apiUrl", + Label: "API URL", + Help: "Base URL of the Netnod Primary DNS API. Leave blank to use the default.", + Default: "https://primarydnsapi.netnod.se", + }, + }, + }) } // netnodProvider represents the netnodProvider DNSServiceProvider. From 75368fb0e1602931934b0bdeb93afa6213c8319f Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Thu, 21 May 2026 22:14:03 +0200 Subject: [PATCH 09/10] DOCS(netnod): Add Netnod as supported provider in README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a3e11c3894..34545b8fad 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See [Getting Started](https://docs.dnscontrol.org/getting-started/getting-starte ## Supported Providers -DNSControl supports 62 DNS providers and registrars: +DNSControl supports 63 DNS providers and registrars: | | | | | | | ----- | ----- | ----- | ----- | ----- | @@ -55,10 +55,10 @@ DNSControl supports 62 DNS providers and registrars: | [hosting.de](https://docs.dnscontrol.org/provider/hostingde)¹ | [Huawei Cloud DNS](https://docs.dnscontrol.org/provider/huaweicloud) | [Hurricane Electric DNS](https://docs.dnscontrol.org/provider/hedns) | [Infomaniak](https://docs.dnscontrol.org/provider/infomaniak) | [Internet.bs](https://docs.dnscontrol.org/provider/internetbs)² | | [INWX](https://docs.dnscontrol.org/provider/inwx)¹ | [Joker](https://docs.dnscontrol.org/provider/joker) | [Linode](https://docs.dnscontrol.org/provider/linode) | [Loopia](https://docs.dnscontrol.org/provider/loopia)¹ | [LuaDNS](https://docs.dnscontrol.org/provider/luadns) | | Windows Server DNS | [MikroTik RouterOS](https://docs.dnscontrol.org/provider/mikrotik) | [Mythic Beasts](https://docs.dnscontrol.org/provider/mythicbeasts) | [Name.com](https://docs.dnscontrol.org/provider/namedotcom)¹ | [Namecheap](https://docs.dnscontrol.org/provider/namecheap)¹ | -| [Netcup](https://docs.dnscontrol.org/provider/netcup) | [Netlify](https://docs.dnscontrol.org/provider/netlify) | [NS1](https://docs.dnscontrol.org/provider/ns1) | [OpenSRS](https://docs.dnscontrol.org/provider/opensrs)² | [Oracle Cloud](https://docs.dnscontrol.org/provider/oracle) | -| [OVH](https://docs.dnscontrol.org/provider/ovh)¹ | [Packetframe](https://docs.dnscontrol.org/provider/packetframe) | [Porkbun](https://docs.dnscontrol.org/provider/porkbun)¹ | [PowerDNS](https://docs.dnscontrol.org/provider/powerdns) | [Realtime Register](https://docs.dnscontrol.org/provider/realtimeregister)¹ | -| [RWTH DNS-Admin](https://docs.dnscontrol.org/provider/rwth) | [Sakura Cloud](https://docs.dnscontrol.org/provider/sakuracloud) | [SoftLayer](https://docs.dnscontrol.org/provider/softlayer) | [Tencent Cloud DNS](https://docs.dnscontrol.org/provider/tencentdns)¹ | [TransIP](https://docs.dnscontrol.org/provider/transip) | -| [UniFi Network](https://docs.dnscontrol.org/provider/unifi) | [Vercel](https://docs.dnscontrol.org/provider/vercel) | [Vultr](https://docs.dnscontrol.org/provider/vultr) | [Netbird](https://docs.dnscontrol.org/provider/netbird) | | +| [Netcup](https://docs.dnscontrol.org/provider/netcup) | [Netlify](https://docs.dnscontrol.org/provider/netlify) | [Netnod](https://docs.dnscontrol.org/provider/netnod) | [NS1](https://docs.dnscontrol.org/provider/ns1) | [OpenSRS](https://docs.dnscontrol.org/provider/opensrs)² | +| [Oracle Cloud](https://docs.dnscontrol.org/provider/oracle) | [OVH](https://docs.dnscontrol.org/provider/ovh)¹ | [Packetframe](https://docs.dnscontrol.org/provider/packetframe) | [Porkbun](https://docs.dnscontrol.org/provider/porkbun)¹ | [PowerDNS](https://docs.dnscontrol.org/provider/powerdns) | +| [Realtime Register](https://docs.dnscontrol.org/provider/realtimeregister)¹ | [RWTH DNS-Admin](https://docs.dnscontrol.org/provider/rwth) | [Sakura Cloud](https://docs.dnscontrol.org/provider/sakuracloud) | [SoftLayer](https://docs.dnscontrol.org/provider/softlayer) | [Tencent Cloud DNS](https://docs.dnscontrol.org/provider/tencentdns)¹ | +| [TransIP](https://docs.dnscontrol.org/provider/transip) | [UniFi Network](https://docs.dnscontrol.org/provider/unifi) | [Vercel](https://docs.dnscontrol.org/provider/vercel) | [Vultr](https://docs.dnscontrol.org/provider/vultr) | [Netbird](https://docs.dnscontrol.org/provider/netbird) | ¹also supports registrar functions ²registrar only From b53ed0abfa1576f234704b54093f06de27a51862 Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Thu, 21 May 2026 23:11:46 +0200 Subject: [PATCH 10/10] NETNOD: update supported record types --- providers/netnod/netnodProvider.go | 7 ++++--- providers/netnod/records.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/providers/netnod/netnodProvider.go b/providers/netnod/netnodProvider.go index cd17d55d7c..b847374537 100644 --- a/providers/netnod/netnodProvider.go +++ b/providers/netnod/netnodProvider.go @@ -21,15 +21,16 @@ var features = providers.DocumentationNotes{ providers.CanUseDNAME: providers.Cannot(), providers.CanUseDNSKEY: providers.Cannot(), providers.CanUseDS: providers.Cannot(), + providers.CanUseDSForChildren: providers.Can(), providers.CanUseHTTPS: providers.Can(), providers.CanUseLOC: providers.Cannot(), - providers.CanUseNAPTR: providers.Cannot(), + providers.CanUseNAPTR: providers.Can(), providers.CanUseOPENPGPKEY: providers.Cannot(), providers.CanUsePTR: providers.Can(), providers.CanUseSOA: providers.Cannot(), providers.CanUseSRV: providers.Can(), - providers.CanUseSSHFP: providers.Cannot(), - providers.CanUseSVCB: providers.Cannot(), + providers.CanUseSSHFP: providers.Can(), + providers.CanUseSVCB: providers.Can(), providers.CanUseTLSA: providers.Can(), providers.DocCreateDomains: providers.Can(), providers.DocDualHost: providers.Can(), diff --git a/providers/netnod/records.go b/providers/netnod/records.go index 44e79ed193..dcaa9a74dc 100644 --- a/providers/netnod/records.go +++ b/providers/netnod/records.go @@ -92,7 +92,7 @@ func buildRecordList(change diff2.Change) (records []netnodPrimaryDNS.Record) { record := netnodPrimaryDNS.Record{ Content: recordContent.GetTargetCombined(), } - if recordContent.Type == "HTTPS" { + if recordContent.Type == "HTTPS" || recordContent.Type == "SVCB" { // The API rejects double-quoted simple param values (e.g. alpn="h2,h3") // but requires quotes around values containing + or / (e.g. ECH base64). // Strip quotes only from values that don't contain those characters.