Skip to content

Commit f75cfc6

Browse files
authored
Add new IANA-allocated options from draft-muks-dns-filtering (#1269)
1 parent e72bddf commit f75cfc6

3 files changed

Lines changed: 216 additions & 0 deletions

File tree

dns/edns.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ class OptionType(dns.enum.IntEnum):
5959
EDE = 15
6060
#: REPORTCHANNEL
6161
REPORTCHANNEL = 18
62+
#: EDE-EXTRA-TEXT-LANGUAGE (Extended DNS Error EXTRA-TEXT language)
63+
EDE_EXTRA_TEXT_LANGUAGE = 22
64+
#: FILTERING-CONTACT
65+
FILTERING_CONTACT = 23
66+
#: FILTERING-ORGANIZATION
67+
FILTERING_ORGANIZATION = 24
68+
#: FILTERING-DB
69+
FILTERING_DB = 25
6270

6371
@classmethod
6472
def _maximum(cls):
@@ -513,12 +521,140 @@ def from_wire_parser(
513521
return cls(parser.get_name())
514522

515523

524+
class EDEExtraTextLanguageOption(Option):
525+
"""Extended DNS Error EXTRA-TEXT language (EDE-EXTRA-TEXT-LANGUAGE)"""
526+
527+
def __init__(self, language: str):
528+
"""Initialize an EDEExtraTextLanguageOption.
529+
530+
:param language: The language of EXTRA-TEXT in the EDE option.
531+
:type language: str
532+
"""
533+
534+
super().__init__(OptionType.EDE_EXTRA_TEXT_LANGUAGE)
535+
self.language = language
536+
537+
def to_wire(self, file: Any = None) -> bytes | None:
538+
if file:
539+
file.write(self.language.encode("utf8"))
540+
return None
541+
else:
542+
return self.language
543+
544+
def to_text(self) -> str:
545+
return f"EDE-EXTRA-TEXT-LANGUAGE {self.language}"
546+
547+
@classmethod
548+
def from_wire_parser(
549+
cls, otype: OptionType | str, parser: dns.wire.Parser
550+
) -> Option:
551+
return cls(parser.get_remaining().decode("utf8"))
552+
553+
554+
class FilteringContactOption(Option):
555+
"""Filtering contact (FILTERING-CONTACT)"""
556+
557+
def __init__(self, contact: str):
558+
"""Initialize a FilteringContactOption.
559+
560+
:param contact: A filtering contact URI as a string.
561+
:type contact: str
562+
"""
563+
564+
super().__init__(OptionType.FILTERING_CONTACT)
565+
self.contact = contact
566+
567+
def to_wire(self, file: Any = None) -> bytes | None:
568+
if file:
569+
file.write(self.contact.encode("utf8"))
570+
return None
571+
else:
572+
return self.contact
573+
574+
def to_text(self) -> str:
575+
return f"FILTERING-CONTACT {self.contact}"
576+
577+
@classmethod
578+
def from_wire_parser(
579+
cls, otype: OptionType | str, parser: dns.wire.Parser
580+
) -> Option:
581+
return cls(parser.get_remaining().decode("utf8"))
582+
583+
584+
class FilteringOrganizationOption(Option):
585+
"""Filtering organization (FILTERING-ORGANIZATION)"""
586+
587+
def __init__(self, organization: str):
588+
"""Initialize a FilteringOrganizationOption.
589+
590+
:param organization: The filtering organization.
591+
:type organization: str
592+
"""
593+
594+
super().__init__(OptionType.FILTERING_ORGANIZATION)
595+
self.organization = organization
596+
597+
def to_wire(self, file: Any = None) -> bytes | None:
598+
if file:
599+
file.write(self.organization.encode("utf8"))
600+
return None
601+
else:
602+
return self.organization
603+
604+
def to_text(self) -> str:
605+
return f"FILTERING-ORGANIZATION {self.organization}"
606+
607+
@classmethod
608+
def from_wire_parser(
609+
cls, otype: OptionType | str, parser: dns.wire.Parser
610+
) -> Option:
611+
return cls(parser.get_remaining().decode("utf8"))
612+
613+
614+
class FilteringDBOption(Option):
615+
"""Filtering DB (FILTERING-DB)"""
616+
617+
def __init__(self, db: str):
618+
"""Initialize a FilteringDBOption.
619+
620+
:param db: The filtering database containing the identifier,
621+
name, or description of the filtering database
622+
against which a matched query caused the filtering to
623+
occur.
624+
:type db: str
625+
626+
"""
627+
628+
super().__init__(OptionType.FILTERING_DB)
629+
self.db = db
630+
631+
def to_wire(self, file: Any = None) -> bytes | None:
632+
if file:
633+
file.write(self.db.encode("utf8"))
634+
return None
635+
else:
636+
return self.db
637+
638+
def to_text(self) -> str:
639+
return f"FILTERING-DB {self.db}"
640+
641+
@classmethod
642+
def from_wire_parser(
643+
cls, otype: OptionType | str, parser: dns.wire.Parser
644+
) -> Option:
645+
return cls(parser.get_remaining().decode("utf8"))
646+
647+
516648
_type_to_class: dict[OptionType, Any] = {
517649
OptionType.ECS: ECSOption,
518650
OptionType.EDE: EDEOption,
519651
OptionType.NSID: NSIDOption,
520652
OptionType.COOKIE: CookieOption,
521653
OptionType.REPORTCHANNEL: ReportChannelOption,
654+
OptionType.EDE_EXTRA_TEXT_LANGUAGE: EDEExtraTextLanguageOption,
655+
OptionType.FILTERING_CONTACT: FilteringContactOption,
656+
OptionType.FILTERING_ORGANIZATION: FilteringOrganizationOption,
657+
OptionType.FILTERING_DB: FilteringDBOption,
522658
}
523659

524660

@@ -596,5 +732,9 @@ def register_type(implementation: Any, otype: OptionType) -> None:
596732
CHAIN = OptionType.CHAIN
597733
EDE = OptionType.EDE
598734
REPORTCHANNEL = OptionType.REPORTCHANNEL
735+
EDE_EXTRA_TEXT_LANGUAGE = OptionType.EDE_EXTRA_TEXT_LANGUAGE
736+
FILTERING_CONTACT = OptionType.FILTERING_CONTACT
737+
FILTERING_ORGANIZATION = OptionType.FILTERING_ORGANIZATION
738+
FILTERING_DB = OptionType.FILTERING_DB
599739

600740
### END generated OptionType constants

doc/message-edns.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ will create a :py:class:`dns.edns.ECSOption` object to represent it.
4545
.. autoclass:: dns.edns.ReportChannelOption
4646
:members:
4747

48+
.. autoclass:: dns.edns.EDEExtraTextLanguageOption
49+
:members:
50+
51+
.. autoclass:: dns.edns.FilteringContactOption
52+
:members:
53+
54+
.. autoclass:: dns.edns.FilteringOrganizationOption
55+
:members:
56+
57+
.. autoclass:: dns.edns.FilteringDBOption
58+
:members:
59+
4860
.. autofunction:: dns.edns.get_option_class
4961
.. autofunction:: dns.edns.option_from_wire_parser
5062
.. autofunction:: dns.edns.option_from_wire

tests/test_edns.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,66 @@ def testReportChannelOption(self):
266266
)
267267
self.assertEqual(opt2.agent_domain, agent_domain)
268268

269+
def testEDEExtraTextLanguageOption(self):
270+
language = "en"
271+
expected_wire = bytes(language, "utf8")
272+
opt = dns.edns.EDEExtraTextLanguageOption(language)
273+
io = BytesIO()
274+
opt.to_wire(io)
275+
data = io.getvalue()
276+
self.assertEqual(data, expected_wire)
277+
self.assertEqual(str(opt), "EDE-EXTRA-TEXT-LANGUAGE en")
278+
opt2 = dns.edns.option_from_wire(
279+
dns.edns.OptionType.EDE_EXTRA_TEXT_LANGUAGE,
280+
expected_wire, 0, len(expected_wire)
281+
)
282+
self.assertEqual(opt2.language, language)
283+
284+
def testFilteringContactOption(self):
285+
contact = "mailto:support@example.com"
286+
expected_wire = bytes(contact, "utf8")
287+
opt = dns.edns.FilteringContactOption(contact)
288+
io = BytesIO()
289+
opt.to_wire(io)
290+
data = io.getvalue()
291+
self.assertEqual(data, expected_wire)
292+
self.assertEqual(str(opt), "FILTERING-CONTACT mailto:support@example.com")
293+
opt2 = dns.edns.option_from_wire(
294+
dns.edns.OptionType.FILTERING_CONTACT,
295+
expected_wire, 0, len(expected_wire)
296+
)
297+
self.assertEqual(opt2.contact, contact)
298+
299+
def testFilteringOrganizationOption(self):
300+
organization = "The Example Organization"
301+
expected_wire = bytes(organization, "utf8")
302+
opt = dns.edns.FilteringOrganizationOption(organization)
303+
io = BytesIO()
304+
opt.to_wire(io)
305+
data = io.getvalue()
306+
self.assertEqual(data, expected_wire)
307+
self.assertEqual(str(opt), "FILTERING-ORGANIZATION The Example Organization")
308+
opt2 = dns.edns.option_from_wire(
309+
dns.edns.OptionType.FILTERING_ORGANIZATION,
310+
expected_wire, 0, len(expected_wire)
311+
)
312+
self.assertEqual(opt2.organization, organization)
313+
314+
def testFilteringDBOption(self):
315+
db = "Government Anti-Piracy Policies #1"
316+
expected_wire = b"Government Anti-Piracy Policies #1"
317+
opt = dns.edns.FilteringDBOption(db)
318+
io = BytesIO()
319+
opt.to_wire(io)
320+
data = io.getvalue()
321+
self.assertEqual(data, expected_wire)
322+
self.assertEqual(str(opt), "FILTERING-DB Government Anti-Piracy Policies #1")
323+
opt2 = dns.edns.option_from_wire(
324+
dns.edns.OptionType.FILTERING_DB,
325+
expected_wire, 0, len(expected_wire)
326+
)
327+
self.assertEqual(opt2.db, db)
328+
269329
def test_option_registration(self):
270330
U32OptionType = 9999
271331

@@ -312,3 +372,7 @@ def test_to_generic(self):
312372

313373
generic = dns.edns.GenericOption(12345, "foo")
314374
assert generic.to_generic() is generic
375+
376+
377+
if __name__ == "__main__":
378+
unittest.main()

0 commit comments

Comments
 (0)