Skip to content

feat(M365_BUILDER)!: rewrite in Go and update the records to Microsoft's current requirements - #4936

Merged
TomOnTime merged 3 commits into
DNSControl:mainfrom
jonathan8devs:m365-builder-go
Sep 26, 2026
Merged

TomOnTime merged 3 commits into
DNSControl:mainfrom
jonathan8devs:m365-builder-go

Conversation

@jonathan8devs

@jonathan8devs jonathan8devs commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4809.

Problem

D("lbl.com") with {label: "test", initialDomain: "ex.onmicrosoft.com", skypeForBusiness: true, mdm: true} produces:

test                    MX     0 lbl-com.mail.protection.outlook.com.
autodiscover            CNAME  autodiscover.outlook.com.
selector1._domainkey    CNAME  selector1-lbl-com._domainkey.ex.onmicrosoft.com.
selector2._domainkey    CNAME  selector2-lbl-com._domainkey.ex.onmicrosoft.com.
lyncdiscover            CNAME  webdir.online.lync.com.
sip                     CNAME  sipdir.online.lync.com.
_sip._tls               SRV    100 1 443 sipdir.online.lync.com.
_sipfederationtls._tcp  SRV    100 1 5061 sipfed.online.lync.com.
enterpriseregistration  CNAME  enterpriseregistration.windows.net.
enterpriseenrollment    CNAME  enterpriseenrollment.manage.microsoft.com.

Only the MX moved. The token is lbl-com, not test-lbl-com. sipdir.online.lync.com and webdir.online.lync.com no longer resolve.

Root cause

if ((value.label = "@")) in helpers.js assigns instead of comparing, so the label is overwritten on every call and domainGUID is derived from the first argument alone. The Skype for Business and device management targets are the ones Microsoft published when the builder was written. The options object is also modified in place, so one object shared between D() blocks gives every later domain the first domain's MX and DKIM targets.

Fix

Rewritten in Go against Microsoft's current documentation.

  • models/b_m365.go: the new builder. Every record has an on/off switch and exactly one option that replaces its target, which covers the sovereign clouds, DANE and the May 2025 DKIM format without a cloud-specific code path. New options: mxPriority, verificationToken, mxTarget, autodiscoverTarget, dkimSelector1Target, dkimSelector2Target, sipFederationTarget, mdmEnrollmentTarget, mdmRegistrationTarget.
  • pkg/js/helpers.js: m365Options() passes the options object as an argument instead of through record.metas, because the metadata path formats values with %s. M365_BUILDER() returns its modifier in an array, so the M365_BUILDER(...).concat() form keeps working.
  • models/builders.go and RecordBuilderFn are unchanged.

Behavior changes

Of the 27 calls I ran through both versions, 7 produce byte-identical records, 5 fail in both, and 15 change. The migration table in M365_BUILDER.md lists every one. The changes that break a working config:

  • label applies to every record, not only to the MX.
  • lyncdiscover, sip and _sip._tls are gone, _sipfederationtls._tcp stays.
  • mdm uses enterpriseenrollment-s.manage.microsoft.com.; mdmEnrollmentTarget restores the old value.
  • The first argument must be the name the records are created under. Unknown options, wrong types and targets without a trailing dot are errors now, and an internationalized domain name needs domainGUID.

Tests

  • models/b_m365_test.go: table-driven, the record cases and every error with its exact wording, plus one options object shared between two domains.
  • pkg/js/parse_tests/068-m365builder.js: apex, label, D_EXTEND(), the DKIM targets Microsoft assigns to domains created since May 2025, DANE, and a TTL() on the call.
  • pkg/js/js_test.go: the error cases, including a record modifier passed to the call.

M365_BUILDER.md is rewritten, and cookbook.md documented a metadata parameter that RecordBuilderFn does not have and a helper that does not exist (mustbe.TagetHostWithSubdoman). Both corrected.

Not in this PR

  • models.mergeMetas formats values with %s, which destroys booleans and numbers on the metadata path.
  • mustbe.Uint16 validates an int16 without returning it.
  • BuilderLOC does not set RecordConfig.SubDomain, and has no unit test.
  • Builder errors carry no file position: models.ImportRawRecords wraps every other branch with at %s but returns the builder error unchanged.

…t's current requirements

Issue DNSControl#4809 asks for M365_BUILDER to be moved to Go and for its records to be
brought up to date. This is not a bug-for-bug port. The JavaScript version
generates records that no longer work, and carrying those into Go would mean
shipping a new builder that is already wrong on the day it lands.

The builder now lives in models/b_m365.go and follows the RecordBuilderFn
signature. Builders receive record.args only, so the options object is moved
out of the metadata list in pkg/js/helpers.js (m365Options), the same way CAA
and R53_ALIAS use the third parameter of rawrecordBuilder. models/builders.go
is unchanged.

Every generated record now has an on/off switch and exactly one option that
replaces its target, which is what makes the sovereign clouds, DANE-enabled
tenants and the DKIM format of May 2025 configurable.

BREAKING CHANGE: label now applies to every generated record, not just the MX.
The Skype for Business records are reduced to the SIP federation SRV. The
device management CNAME uses enterpriseenrollment-s. A TTL() modifier on the
call is applied instead of ignored. Unknown options, wrong types and a first
argument that is not the name the records are created under are errors. Nine
option names that used to be ignored now take effect. See the migration table in
documentation/language-reference/domain-modifiers/M365_BUILDER.md.
@TomOnTime

Copy link
Copy Markdown
Collaborator

Looks great! Thanks for porting this to Go. The testing in models/b_m365_test.go is what's going to make the Go builders so much more reliable. Thanks!

@TomOnTime
TomOnTime merged commit 9710715 into DNSControl:main Sep 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

META: Review and modernize Microsoft 365 support

2 participants