Domain DNS Hooks

2 views Markdown

The ten hooks setting where a domain points: name servers, DNS records, DNSSEC and child name servers.

Overview

What these four surfaces share: they all work live. Reads and writes alike send a real request to the provider, and our record is only a mirror.

Learn the pattern once and all four follow it: a gate in front of a write, an event behind it, and on the read side a filter handing you the list by reference.

Reference

Stopping a name server write

gatedomain.nameservers_save
AdminServices::save_nameservers() ClientDomains:197 from both paths

Runs before the name server list is written to the provider, with the input already checked. Both paths, panel and customer panel, pass through here.

Parameters 2
$servicearrayThe domain service record.
$dnsarrayThe name server list about to be written. The non-empty ns1..ns4 values, in order.
Return 1
stringA non-empty string stops the action; the text is thrown as the error and the provider is never called. null or an empty string lets it carry on.
Listener
Hook::add('gate:domain.nameservers_save', 10, function ($service, $dns) {
    // Changing name servers mid-transfer breaks the transfer.
    if (($service['status'] ?? '') === 'transfer')
        return 'Name servers cannot change while a transfer runs.';

    return null;
});

Following a name server change

actiondomain.nameservers_saved
AdminServices::save_nameservers() on success only

Runs after the provider call succeeded, the values landed on the service and the activity was recorded.

Parameters 2
$servicearrayThe domain service record.
$dnsarrayThe list that was written to the provider.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.nameservers_saved', 10, function ($service, $dns) {
    DnsMonitor::resync($service['name'] ?? '', $dns);
});

Stopping a DNS record change

gatedomain.dns_record_save
AdminServices::save_dns_record() AdminServices::delete_dns_record() three actions, one hook

Runs before a DNS record changes at the provider. Adding, updating and deleting all three pass here, and the third parameter says which.

Parameters 3
$servicearrayThe domain service record.
$recordarrayThe record being worked on: type, name, value, identity. On an update also ttl and priority.
$taskstringThe action: create, update or delete.
Return 1
stringA non-empty string stops the action; the text is thrown as the error and the provider is never called. null or an empty string lets it carry on.
Listener
Hook::add('gate:domain.dns_record_save', 10, function ($service, $record, $task) {
    $type = strtoupper((string) ($record['type'] ?? ''));

    // Stop a delete that cuts the mail flow; adds and updates carry on.
    if ($task === 'delete' && $type === 'MX')
        return 'An MX record cannot be deleted; change the mail settings first.';

    return null;
});

Following a DNS record change

actiondomain.dns_record_saved
AdminServices::save_dns_record() on success only

Runs after the change at the provider finished successfully.

Parameters 3
$servicearrayThe domain service record.
$recordarrayThe record that was worked on.
$taskstringThe action that ran.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.dns_record_saved', 10, function ($service, $record, $task) {
    Audit::dns($service['name'] ?? '', $task, $record);
});

Stopping a DNSSEC record action

gatedomain.dnssec_save
AdminServices create and delete

Runs before a DS record is created or deleted at the provider.

Parameters 3
$servicearrayThe domain service record.
$recordarrayThe DS record: digest, key_tag, digest_type, algorithm. On a delete also identity.
$verbstringcreate or delete.
Return 1
stringA non-empty string stops the action; the text is thrown as the error and the provider is never called. null or an empty string lets it carry on.
Listener
Hook::add('gate:domain.dnssec_save', 10, function ($service, $record, $verb) {
    // Deleting the last DS record switches validation off entirely.
    if ($verb === 'delete' && Acme::dsCount((int) ($service['id'] ?? 0)) <= 1)
        return 'The last DNSSEC record cannot be deleted.';

    return null;
});

Following a DNSSEC change

actiondomain.dnssec_saved
AdminServices on success only

Runs after the DS record landed at the provider.

Parameters 3
$servicearrayThe domain service record.
$recordarrayThe DS record that was handled.
$verbstringThe action that ran.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.dnssec_saved', 10, function ($service, $record, $verb) {
    Audit::dnssec($service['name'] ?? '', $verb, $record['key_tag'] ?? '');
});

Stopping a child name server action

gatedomain.child_ns_save
ClientDomains create and delete

Runs before a child name server is added at the provider or removed from it.

Parameters 4
$servicearrayThe domain service record.
$hoststringThe fully qualified name, such as ns1.example.com.
$ipstringThe address behind the name. It can arrive empty on a delete.
$verbstringcreate or delete.
Return 1
stringA non-empty string stops the action; the text is thrown as the error and the provider is never called. null or an empty string lets it carry on.
Listener
Hook::add('gate:domain.child_ns_save', 10, function ($service, $host, $ip, $verb) {
    if ($verb === 'delete') return null;

    // Do not glue a name to an address outside your own block.
    if (!Acme::ownsIp($ip)) return 'That address is not ours.';

    return null;
});

Following a child name server change

actiondomain.child_ns_saved
ClientDomains on success only

Runs after the child name server landed at the provider.

Parameters 4
$servicearrayThe domain service record.
$hoststringThe name that was handled.
$ipstringThe address behind it.
$verbstringThe action that ran.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.child_ns_saved', 10, function ($service, $host, $ip, $verb) {
    Ipam::glue($verb, $host, $ip);
});

Changing the child name server list

filterdomain.child_ns_list
Hook::runRefs by reference

Runs after the list read from the provider was normalised, before it reaches the screen.

Parameters 2
$listarrayrefThe normalised list: every row carries ns and ip. Keep the shape; the screen draws to this contract.
$servicearrayThe domain service record.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.child_ns_list', 10, function (&$list, $service) {
    // Sort without breaking the shape: the screen wants ns and ip keys.
    usort($list, fn ($a, $b) => strcmp($a['ns'] ?? '', $b['ns'] ?? ''));
});

Changing the DNSSEC list

filterdomain.dnssec_records
Hook::runRefs by reference

Runs after the DS records read from the provider were normalised.

Parameters 2
$recordsarrayrefThe normalised records: identity, digest, key_tag, digest_type, algorithm. A delete sends identity back; do not drop it.
$servicearrayThe domain service record.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.dnssec_records', 10, function (&$records, $service) {
    // KEEP the identity field: a delete targets the record with it.
    $records = array_values(array_filter($records,
        fn ($r) => (int) ($r['algorithm'] ?? 0) !== 5));   // hide the old algorithm
});

Pitfalls

One gate carries three actions

The DNS record gate is the same hook for adding, updating and deleting. Without reading the third parameter, a rule you wrote to "stop deletes" stops adds as well. The same holds for the DNSSEC and child name server gates, where the action sits in the fourth parameter.

Keep the shape in list filters

Read filters hand you the array the screen draws directly. Renaming a key or dropping identity breaks more than the list: it leaves the delete call without a target, because the record is found by that field.

The gate is on both paths

The name server gate sits on the panel path and the customer path alike. A rule written as "the customer should not" stops the operator too. To tell the paths apart, look at the calling context rather than at the service record.

Propagation is not instant

The event says the provider accepted the change, not that the world sees it. Propagation takes minutes. A listener acting on the new value gets the old answer where it checks straight away.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.