Skip to content

feat(p/MISAKA): add Misaka.io DNS provider - #4994

Merged
TomOnTime merged 8 commits into
DNSControl:mainfrom
ZeroClover:feat/misaka-provider
Oct 15, 2026
Merged

TomOnTime merged 8 commits into
DNSControl:mainfrom
ZeroClover:feat/misaka-provider

Conversation

@ZeroClover

Copy link
Copy Markdown
Contributor

Adds a DNS provider for Misaka.io Managed DNS (MISAKA), using their v3 API.

Please create the GitHub label 'provider-MISAKA'

What it does

  • diff2.ByRecordSet: the API treats a record set ({name}/{type}) as one unit, so each change is a single POST (create), PUT (replace the whole set, including values kept by IGNORE() / NO_PURGE) or DELETE.
  • Supported types: Basic8, ALIAS, DNAME, PTR, SSHFP, TLSA.
  • get-zones / ListZones, and create-domains / auto-create on push (EnsureZoneExists).
  • SOA and apex NS are managed by Misaka.io. They are hidden on read. Desired apex NS records are dropped, with a warning if they don't match the zone's nameservers (same approach as NexDNS). GetNameservers reads the apex NS set and only warns when the zone doesn't exist yet, so preview of a new zone works.
  • Record sets that use features DNSControl doesn't model are rejected on read, with an error naming the set, rather than being silently overwritten later. These are:
    • GeoDNS filters
    • alias
    • enabled: false
    • protected
    • an empty value list
    • a hostname of @
    • a non-canonical (mixed-case or trailing-dot) name
  • TXT: values are written in presentation format. On read, a value that is entirely quoted is parsed as presentation format, and anything else is treated as the raw string. This is the rule the API itself applies when it publishes a value.
  • AuditRecords rejects TTLs above 2147483647, the API's maximum.

Rate limits and retries

  • No 429 handling. No 429 or rate-limit headers appeared in four full integration runs (roughly 1,500–2,000 sequential requests each), so there is no wait-and-retry for rate limits.
  • One retry for dropped connections. The API, which sits behind Cloudflare, sometimes dropped the connection mid-request: unexpected EOF, with no HTTP status. This happened in three of the four runs, once each on a GET, a PUT and a DELETE.
    • GET, PUT and DELETE are sent once more after 1 s, with a warning.
    • POST is never repeated.
    • A 404 on the repeated DELETE counts as done.
    • HTTP error statuses are not retried.

Testing

Integration tests

go test -v ./integrationTest -args -verbose -profile MISAKA against a dedicated test zone in a separate account: PASS.

  • One opt-out was added: NS only APEX → not("MISAKA"), because apex NS records are managed by Misaka.io.
  • All other skipped groups are capability-gated (*_not_supported) or only(...) groups for other providers.
  • pager101 passes. pager601 / pager1201 are opt-in groups and were not enabled.

Result: PASS in 579.8 s - 252 test steps passed, 0 failed (the remaining groups are skipped as listed above).
The connection-drop retry fired once during this run (on a PUT) and recovered.

Full output: https://gist.github.com/ZeroClover/ea219c72183312ddb3e3eec9947ce0a7

TXT procedure (testing-txt-records.md)

Records t0–t3 were created with the exact values a console user would type: test0, test1, "test2", "test3".

  • preview reports changes for t1 and t3. t2 shows no change because Misaka.io publishes the quoted console value "test2" as the unquoted string test2, which already matches TXT("t2", "test2"). This was confirmed with dig TXT t2.<zone> @a.m1ns.net, which returns "test2".

  • After push, preview is clean, and the authoritative answers equal the strings in dnsconfig.js:

    Record Answer
    t0 "test0"
    t1 "\"test1\""
    t2 "test2"
    t3 "\"test3\""

Concurrency

go build -race, then dnscontrol preview --cmode all over six zones, twice. Both runs gathered all zones concurrently with no race report, so the provider declares CanConcur: providers.Can().

CONCURRENTLY checking for 6 zone(s)
Waiting for concurrent checking(s) to complete...DONE
CONCURRENTLY gathering records of 6 zone(s)
Waiting for concurrent gathering(s) to complete...DONE
...
Done. 0 corrections.

Other checks

  • bin/generate-all.sh: golangci-lint 0 issues, staticcheck clean, no changes to generated files.
  • modernize -fix ./...: no changes.
  • go vet ./...: clean.
  • go test ./...: passes.

Notes

  • Misaka.io labels the v3 API as alpha; the provider page says so.
  • I (@ZeroClover) will maintain the provider. I can provide a test zone and token for the CI integration tests (MISAKA_DOMAIN, MISAKA_API_TOKEN).

Add a DNS provider for Misaka.io Managed DNS (v3 API): record-set
corrections via diff2.ByRecordSet, zone listing and creation, apex NS
and SOA left to Misaka.io, record sets with unmodelled features
(GeoDNS filters, alias, disabled, protected, empty, @ hostnames,
non-canonical names) refused on read, TTL audit, golden conversion
tests, and a single retry of non-POST requests whose connection is
dropped mid-request (observed during integration runs).

@TomOnTime TomOnTime left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far! Just a few requests.

Comment thread providers/misaka/misakaProvider.go Outdated
// Features retains annotations for record types and interface-derived facts.
Features: providers.DocumentationNotes{
providers.CanUseSOA: providers.Cannot("The SOA record is managed by Misaka.io (zone settings)"),
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove Features. Add to SupportedTypes "SOA:Cannot:Managed by Misaka.io"

Comment thread providers/misaka/auditrecords.go Outdated
Comment on lines +26 to +33
// rejectTTLAboveMax rejects a TTL the API would refuse, instead of quietly
// lowering it to one the user did not ask for.
func rejectTTLAboveMax(rc *models.RecordConfig) error {
if rc.TTL > maxTTL {
return fmt.Errorf("%s %s: ttl %d is larger than %d, the maximum Misaka.io accepts", rc.GetLabelFQDN(), rc.Type, rc.TTL, maxTTL)
}
return nil
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of rejecting these records, silently reduce the TTL in toNative().

Comment thread providers/misaka/records.go Outdated
Comment on lines +67 to +69
if rs.Name != strings.ToLower(rs.Name) || strings.HasSuffix(rs.Name, ".") {
return fail("has a name that is not lowercase or ends with a dot; rename it in the Misaka.io console")
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than rejecting names that are not lowercase, do one of two things:

  1. Downcase them. Allow the next push to update the record.
  2. Record the original case in RecordConfig.Original for use when the original case is needed.

Resolve the semantic conflicts with DNSControl#4995 and DNSControl#4996: use the renamed
CanDualHost and OfficiallySupported fields, and declare SOA as
"SOA:Cannot:Managed by Misaka.io" in SupportedTypes instead of the
Features note that built-in providers may no longer use (also requested
in the review).
…them

A TTL above 2147483647 is now lowered to that maximum instead of
failing validation. The desired records are adjusted before the diff,
as Linode and Domainnameshop do, so the stored TTL matches the desired
one and later runs do not offer the same change again.
Misaka.io stores record-set names verbatim and matches them
case-sensitively, so a set named "Www" cannot be changed through
"www". Instead of refusing such a zone, read the name lowercased as
usual, keep the native record set in RecordConfig.Original, and change
or delete the set under its original name.
…rigin response

A regression run hit a DELETE answered with 502 "The origin web server
returned an invalid or incomplete response to Cloudflare". It is the
connection drop already handled for "unexpected EOF", seen from the
other side of Cloudflare, so it gets the same single retry of non-POST
requests. Other 5xx statuses are still not retried.
Under the integration workload the API drops responses more often than
first observed: one regression run saw seven drops, including a GET
whose retry was dropped as well. A GET, PUT or DELETE is now sent up to
three times after a dropped connection or a Cloudflare 502. A POST is
still never repeated, because the first one may have been applied; the
next push creates whatever is still missing.
@ZeroClover

Copy link
Copy Markdown
Contributor Author

@TomOnTime Thanks for the review!

The new SupportedType requires PR #4995, so it looks like I didn't pick a good time to submit my changes :P

I've resolved the conflicts and made the requested changes to the code.

I also re-ran the necessary tests to make sure that the TTL clamping and case conversion both work as expected.


providers/misaka/auditrecords.go - "Instead of rejecting these records, silently reduce the TTL in toNative()."

I do this in GetZoneRecordsCorrections before the diff (like Linode and Domainnameshop)
rather than only in toNative(). If it only happened in toNative(), the stored TTL
(2147483647) would never equal the desired one (e.g. 4294967295), and every
preview would offer the same TTL change again. The records toNative() receives
are already lowered, so the request carries the maximum. I checked this live:
TTL(4294967295) was pushed as 2147483647, and the next preview showed 0 corrections.

providers/misaka/records.go - "Rather than rejecting names that are not lowercase, do one of two things ..."

Went with option 2. Misaka.io matches record-set names case-sensitively: for a set
created as V-Case/A, GET/PUT .../v-case/A answers 404, so downcasing alone would
make the next push fail (or create a second set). Each record now carries its native
record set in RecordConfig.Original, and changes and deletes use Original.Name.
I checked this live: a set created as V-Case/A was changed through V-Case (and
v-case/A stayed 404), and a set created as Del-Case/TXT was deleted. Names
ending with a dot are still refused, since what they are relative to is unknown.

@TomOnTime

Copy link
Copy Markdown
Collaborator

Thank you for contributing to this new provider, @ZeroClover!

A few action items before we can merge this PR:

  1. By now you should have received a Github invite to join the Provider-maintainers github team, which gives you the "triage" role for this repo. Please accept the invite so we can assign bugs to you.
  2. @fm: Faisal Misle is our “liaison to maintainers”. Please send him an email from your preferred address to dnscontrol so we can stay in touch. Your email address will not be shared, only used for DNSControl communication. Please email f at faisal dot fm and CC tal at what exit dot org
  3. Please consider setting up a test account for use by our automated testing. This will mean we can test the provider every release in an automated manner. Info is here: https://docs.dnscontrol.org/developer-info/byo-secrets#donate-secrets-to-the-project (If a test account or OE&T environment isn't available, an API key restricted to a single domain is sufficient.) (This doesn't need to be part of this PR)

When I see the Github invite accepted and receive the email, I’ll merge this PR.

Thanks again!
Tom

Regenerate the provider page for the renamed "Concurrent fetching"
feature label (DNSControl#5002) and let go fix use errors.AsType, so that the
"PR: Check git status" job finds nothing to change.
@ZeroClover

Copy link
Copy Markdown
Contributor Author

Thanks, @TomOnTime

Invite accepted and Email sent to Faisal, with you in CC.

I'm happy to donate a test account. I have a separate Misaka.io account for integration testing only and sent credentials via SendSafely.

Also, I pushed one small follow-up commit (ebcca9b): after #5002 renamed the feature label, go generate updates the Misaka page ("Concurrency Verified" → "Concurrent fetching"), and go fix rewrites one errors.As into errors.AsType. Without it, the "PR: Check git status" job would fail once it runs.

@TomOnTime

Copy link
Copy Markdown
Collaborator

Great! Thanks so much!

@TomOnTime
TomOnTime merged commit b4d26af into DNSControl:main Oct 15, 2026
13 checks passed
@ZeroClover
ZeroClover deleted the feat/misaka-provider branch October 15, 2026 11:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants