Flatten your CNAME / NS records.
Many DNS providers (Tencent Cloud, Aliyun, ...) gate native CNAME flattening
behind paid plan upgrades. Without flattening, a CNAME on the apex (@)
conflicts with coexisting records such as MX, making it unusable. flatns
solves this the simple way: it resolves the CNAME chain to its terminal A/AAAA
records and keeps your target subdomain in sync automatically.
- Resolve the configured
sourceCNAME chain to all reachable A (and optionally AAAA) records, using custom recursive DNS servers or the system resolver. - Compare the resolved addresses against the records flatns manages on the
target
domain/sub_domain. - Create / update / delete records so the target matches, then repeat on a timer.
Every record flatns creates is tagged with a remark of the form
flatns-managed:<source> (or flatns-managed:i=<instance>:<source> when an
instance is set). flatns only ever touches records carrying its own
marker, so your manually-created records are never modified or deleted.
Ownership is scoped by the configured instance (plus the target
domain/subdomain and record type). If source changes, the same instance adopts
its previously managed records, updates their values in place, and refreshes
the remark to the new source instead of creating a second record set.
When a flattened CNAME returns geo-specific results, a single host can only
serve a subset of the IPs. Set a unique instance per deployment (global or
per-entry) so several flatns instances can flatten the same source while
each manages only its own records — they never claim or clobber each other.
Leave instance empty for a single deployment.
main.go Entrypoint: tiny Init/Cleanup wiring + signal loop
internal/infra/config viper YAML singleton; SIGHUP hot reload; OnReload hooks
internal/infra/logger zap logger: console + JSON files with lumberjack rotation
internal/resolver CNAME chain resolution; custom NS or system resolver
internal/provider Vendor-neutral Provider interface + factory registry
internal/provider/tencent Tencent Cloud DNSPod implementation
internal/provider/aliyun Alibaba Cloud AliDNS implementation (V2.0 SDK)
internal/flattener Per-entry reconcile loop + Supervisor (lifecycle/reload)
The provider.Provider interface is the extension point. To add a new vendor:
- Implement
ListRecords / CreateRecord / UpdateRecord / DeleteRecord. - Call
provider.Register("<name>", factory)from the packageinit(). - Add a blank import of the package in
main.go.
No other code needs to change.
Requires Go 1.26+.
go build -o flatns .Copy config.example.yaml to config.yaml and edit it. Highlights:
log: zap + lumberjack.debugtoggles the level;file.all/file.errset the JSON log files (empty disables a sink; stdout is always written);max_size/max_backups/max_age/compresscontrol rotation.instance: deployment identifier for multi-instance setups (see above). Empty by default. Per-entryinstanceoverrides the global value.resolvers: global recursive DNS servers (hostorhost:port, port defaults to 53). Empty = system resolver. Per-entryresolversoverride this.providers: credentials keyed by an alias you choose; reused across entries.flattens: a list of jobs, each reconciled in its own goroutine. Fields:source,domain,sub_domain(@= apex),ttl,interval(e.g.30s,5m),ipv4andipv6(independent address-family switches), optionalmax_records/max_records_total(see below), and an optionalinstance.
ipv4 controls A records and ipv6 controls AAAA records. They default to
true and false, respectively, so older configurations keep their previous
behavior. At least one family must be enabled.
Some providers' plans cap how many values a single sub-domain may resolve to —
DNSPod's free tier, for example, allows only a couple of records per host.
Flattening a source with more addresses than that quota would make the provider
reject the surplus CreateRecord calls. Two optional per-entry knobs bound the
managed record set so this never happens:
max_records: per-type cap. At most this many A records and this many AAAA records are kept ("V4/V6 each N").0(default) means unlimited.max_records_total: combined cap across A and AAAA ("N in total"), for providers that count both types against one quota. The budget is split evenly between the enabled types, and either type's unused share flows to the other (IPv4 gets the odd slot).0(default) means unlimited.
Both may be set at once; each is enforced independently. Surplus addresses are dropped deterministically (the resolver's sorted order is stable, so the cap never causes create/delete churn) and the dropped values are logged at warn level.
flatns reads config.yaml from the working directory (no flags).
./flatns- Send
SIGHUP(kill -HUP <pid>) to hot-reload the config. Workers are gracefully torn down and respawned with the new config; an invalid edit is logged and ignored, leaving the previous config running. SIGINT/SIGTERMshut down cleanly.
| Provider | type |
Credentials |
|---|---|---|
| Tencent Cloud | tencent |
secret_id = SecretId, secret_key = SecretKey |
| Alibaba Cloud | aliyun |
secret_id = AccessKeyId, secret_key = AccessKeySecret |