mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
fix(dynadot): read an apex MX that Dynadot returns typed "stealth"
Dynadot's records endpoint returns suchshop.lol's apex MX with record_type "stealth", the mail host in record_value1 and the preference in record_value2. Sending that record back, which any preserve-unmanaged write has to do, is rejected by Dynadot with "Please enter a valid stealth forwarding url looks like this: http://subdomain.example.com". The POST replaces the whole zone, so the rejection takes every other record in the batch with it. Six DMARC report authorizations could not be published for exactly this reason, and no amount of correctness in the manifest would have helped. Confirmed rather than inferred. The apex returns three records, stealth/txt/a, and the apex serves three records, MX/TXT/A. Matching them one to one gives stealth = MX 10 mail.suchshop.lol; the txt and a records match their live values exactly, and there is no fourth record and no forwarding configured. Three separate reads across two days returned the same thing. The correction is narrow because the two shapes cannot overlap. A genuine stealth forward stores a URL in value1 and leaves value2 empty, which is the format Dynadot's own error message asks for. A hostname in value1 with an integer preference in value2 is not a valid stealth record in any configuration, so the reader treats that combination, and only that combination, as MX. The end-to-end read test already carried a copy of this zone and asserted the bogus type was preserved verbatim, calling it "an unusual type is still a record". That assertion described the bug. It now requires the apex to read as MX with its preference intact.
This commit is contained in:
@@ -50,6 +50,34 @@ DYNADOT_API_ORIGINS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_mislabelled_apex_mx(record_type: str, value1: str | None, value2: str | None) -> bool:
|
||||||
|
"""Dynadot returns an apex MX record typed "stealth".
|
||||||
|
|
||||||
|
Observed on suchshop.lol across three reads on two days: the zone's three
|
||||||
|
apex records come back as stealth/txt/a, and comparing them one to one with
|
||||||
|
what the apex actually serves gives stealth = MX 10 mail.suchshop.lol. There
|
||||||
|
is no fourth record and no forwarding configured. Writing that record back
|
||||||
|
with the type Dynadot supplied is refused by Dynadot itself, with "Please
|
||||||
|
enter a valid stealth forwarding url", so a zone carrying an apex MX could
|
||||||
|
not be updated at all: the whole POST fails and no record in it is written.
|
||||||
|
|
||||||
|
The two shapes do not overlap, which is what makes this safe to correct
|
||||||
|
rather than guess at. A genuine stealth forward stores a URL in value1 and
|
||||||
|
leaves value2 empty; Dynadot's own error names the format it requires. A
|
||||||
|
hostname in value1 with an integer preference in value2 is not a stealth
|
||||||
|
record in any valid configuration.
|
||||||
|
"""
|
||||||
|
if record_type != "STEALTH" or value1 is None or value2 is None:
|
||||||
|
return False
|
||||||
|
if "://" in value1:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
preference = int(value2)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return 0 <= preference <= 65535
|
||||||
|
|
||||||
|
|
||||||
def dynadot_signature(
|
def dynadot_signature(
|
||||||
api_key: str,
|
api_key: str,
|
||||||
api_secret: str,
|
api_secret: str,
|
||||||
@@ -282,6 +310,8 @@ class DynadotDNSProvider:
|
|||||||
value2 = _record_value(item, "record_value2")
|
value2 = _record_value(item, "record_value2")
|
||||||
if value2 is None:
|
if value2 is None:
|
||||||
value2 = _record_value(item, "value2")
|
value2 = _record_value(item, "value2")
|
||||||
|
if _is_mislabelled_apex_mx(record_type, value1, value2):
|
||||||
|
record_type = "MX"
|
||||||
if not name or value1 is None:
|
if not name or value1 is None:
|
||||||
raise RuntimeError("Dynadot returned an incomplete DNS record")
|
raise RuntimeError("Dynadot returned an incomplete DNS record")
|
||||||
mx_pref = None
|
mx_pref = None
|
||||||
|
|||||||
@@ -490,7 +490,14 @@ class DynadotDNSTests(unittest.TestCase):
|
|||||||
self.assertEqual("v=DKIM1;k=rsa;p=AAAA", by_name[("modoboa._domainkey", "TXT")].value)
|
self.assertEqual("v=DKIM1;k=rsa;p=AAAA", by_name[("modoboa._domainkey", "TXT")].value)
|
||||||
self.assertEqual("mail.example.test", by_name[("mail", "MX")].value)
|
self.assertEqual("mail.example.test", by_name[("mail", "MX")].value)
|
||||||
self.assertEqual(10, by_name[("mail", "MX")].mx_pref)
|
self.assertEqual(10, by_name[("mail", "MX")].mx_pref)
|
||||||
self.assertIn(("@", "STEALTH"), by_name, "an unusual type is still a record")
|
# This fixture is a copy of suchshop.lol, whose apex record Dynadot types
|
||||||
|
# "stealth" while the apex serves MX 10 mail.suchshop.lol. Preserving that
|
||||||
|
# type verbatim, which this test used to require, is exactly what made a
|
||||||
|
# preserve-unmanaged write impossible: Dynadot rejects its own spelling on
|
||||||
|
# the way back in, and takes the whole batch down with it.
|
||||||
|
self.assertNotIn(("@", "STEALTH"), by_name)
|
||||||
|
self.assertEqual("mail.example.test", by_name[("@", "MX")].value)
|
||||||
|
self.assertEqual(10, by_name[("@", "MX")].mx_pref)
|
||||||
|
|
||||||
def test_the_write_spelling_is_still_accepted(self):
|
def test_the_write_spelling_is_still_accepted(self):
|
||||||
# Kept so the two spellings cannot quietly become one.
|
# Kept so the two spellings cannot quietly become one.
|
||||||
@@ -527,3 +534,54 @@ class DynadotDNSTests(unittest.TestCase):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
from ampg import dynadot_dns # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class MislabelledApexMxTest(unittest.TestCase):
|
||||||
|
"""Dynadot types an apex MX record "stealth" and then refuses it back.
|
||||||
|
|
||||||
|
suchshop.lol carries MX 10 mail.suchshop.lol at the apex. Dynadot's records
|
||||||
|
endpoint returns it as record_type "stealth" with the host in value1 and the
|
||||||
|
preference in value2. Echoing that back, which a preserve-unmanaged write has
|
||||||
|
to do, is rejected with "Please enter a valid stealth forwarding url" and the
|
||||||
|
whole POST fails, so not one record in the batch is written. Six DMARC report
|
||||||
|
authorizations were blocked by it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_a_hostname_with_a_preference_is_an_mx_record(self) -> None:
|
||||||
|
self.assertTrue(
|
||||||
|
dynadot_dns._is_mislabelled_apex_mx("STEALTH", "mail.suchshop.lol", "10")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_real_stealth_forward_is_left_alone(self) -> None:
|
||||||
|
# value1 is a URL and value2 is empty: the shape Dynadot documents.
|
||||||
|
self.assertFalse(
|
||||||
|
dynadot_dns._is_mislabelled_apex_mx(
|
||||||
|
"STEALTH", "http://subdomain.example.com", None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
dynadot_dns._is_mislabelled_apex_mx(
|
||||||
|
"STEALTH", "https://shop.example.com/path", "0"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nothing_else_is_touched(self) -> None:
|
||||||
|
for record_type in ("TXT", "A", "MX", "CNAME", "FORWARD"):
|
||||||
|
with self.subTest(record_type=record_type):
|
||||||
|
self.assertFalse(
|
||||||
|
dynadot_dns._is_mislabelled_apex_mx(
|
||||||
|
record_type, "mail.suchshop.lol", "10"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_preference_outside_the_range_is_not_assumed(self) -> None:
|
||||||
|
for value2 in ("-1", "65536", "ten", "", "1.5"):
|
||||||
|
with self.subTest(value2=value2):
|
||||||
|
self.assertFalse(
|
||||||
|
dynadot_dns._is_mislabelled_apex_mx(
|
||||||
|
"STEALTH", "mail.suchshop.lol", value2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user