]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
datamodel: local-data/rules: additional config
authorAleš Mrázek <ales.mrazek@nic.cz>
Fri, 25 Aug 2023 13:59:38 +0000 (15:59 +0200)
committerVladimír Čunát <vladimir.cunat@nic.cz>
Tue, 12 Sep 2023 10:12:55 +0000 (12:12 +0200)
ttl, nodata and tags are configurable for hostname and address(es) pairs

manager/etc/knot-resolver/config.dev.yaml
manager/knot_resolver_manager/datamodel/local_data_schema.py
manager/knot_resolver_manager/datamodel/templates/local_data.lua.j2
manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j2
manager/tests/unit/datamodel/test_local_data.py

index fc7d608a3361e8ef8e012e29011c4f76f7e31420..9288b1d0be83a92492f110b757f8de74a8eaf5e1 100644 (file)
@@ -31,21 +31,29 @@ local-data:
   # root-fallback-addresses-files: root.custom
   addresses:
     foo.bar: 127.0.0.1
-  # addresses-files: hosts.custom
+  addresses-files:
+    - /etc/hosts
   records: |
     example.net. TXT "foo bar"
      A 192.168.2.3
      A 192.168.2.4
     local.example.org AAAA ::1
-  subtrees:
-    - type: empty
+  rules:
+    - name: custom.example
+      address: 1.2.3.4
+      nodata: true
+      tags: [t01]
+    - file: hosts.custom
+      tags: [t02]
+      ttl: 10m
+    - subtree: empty
       tags: [ t2 ]
-      roots: [ example1.org ]
-    - type: nxdomain
-      roots: [ sub4.example.org ]
-  rpz:
-    - file: runtime/blocklist.rpz
-      tags: [t01, t02]
+      name: [ example1.org ]
+    - subtree: nxdomain
+      name: [ sub4.example.org ]
+  rpz:
+    - file: runtime/blocklist.rpz
+      tags: [t01, t02]
 forward:
   - subtree: '.'
     options:
index c9a73593dca4189723741e15deb2abd4917deb06..5e81c8cb1773ca9790971e73887f20b07798d3a4 100644 (file)
@@ -14,46 +14,37 @@ from knot_resolver_manager.datamodel.types import (
 from knot_resolver_manager.utils.modeling import ConfigSchema
 
 
-class SubtreeSchema(ConfigSchema):
+class RuleSchema(ConfigSchema):
     """
-    Local data and configuration of subtree.
+    Local data rule configuration.
 
     ---
-    type: Type of the subtree.
+    name: Hostname(s).
+    address: Address(es) to pair with hostname(s).
+    file: Path to file(s) with hostname and IP address(es) pairs in '/etc/hosts' like format.
+    subtree: Type of subtree.
     tags: Tags to link with other policy rules.
-    ttl: Default TTL value used for added local subtree.
-    nodata: Use NODATA synthesis. NODATA will be synthesised for matching name, but mismatching type(e.g. AAAA query when only A exists).
-    roots: Subtree roots.
+    ttl: Optional, TTL value used for these answers.
+    nodata: Optional, use NODATA synthesis. NODATA will be synthesised for matching name, but mismatching type(e.g. AAAA query when only A exists).
     """
 
-    # addresses: Subtree addresses.
-    # roots_file: Subtree roots from given file.
-    # roots_url: Subtree roots form given URL.
-    # refresh: Refresh time to update data from 'roots-file' or 'roots-url'.
-
-    type: Literal["empty", "nxdomain", "redirect"]
+    name: Optional[ListOrItem[DomainName]] = None
+    address: Optional[ListOrItem[IPAddress]] = None
+    subtree: Optional[Literal["empty", "nxdomain", "redirect"]] = None
+    file: Optional[ListOrItem[File]] = None
     tags: Optional[List[IDPattern]] = None
     ttl: Optional[TimeUnit] = None
-    nodata: bool = True
-    roots: Optional[List[DomainName]] = None
-
-    # # These aren't implemented yet.
-    # addresses: Optional[List[IPAddress]] = None
-    # roots_file: Optional[File] = None
-    # roots_url: Optional[EscapedStr] = None
-    # refresh: Optional[TimeUnit] = None
-
-    # def _validate(self) -> None:
-    #    options_sum = sum([bool(self.roots), bool(self.roots_file), bool(self.roots_url)])
-    #    if options_sum > 1:
-    #        raise ValueError("only one of, 'roots', 'roots-file' or 'roots-url' can be configured")
-    #    elif options_sum < 1:
-    #        raise ValueError("one of, 'roots', 'roots-file' or 'roots-url' must be configured")
-    #    if self.refresh and not (self.roots_file or self.roots_url):
-    #        raise ValueError("'refresh' can be only configured with 'roots-file' or 'roots-url'")
+    nodata: Optional[bool] = None
+
     def _validate(self) -> None:
-        if self.roots is None:
-            raise ValueError("'roots' is missing")
+        options_sum = sum([bool(self.address), bool(self.subtree), bool(self.file)])
+        if options_sum > 1:
+            raise ValueError("only one of 'address', 'subtree' or 'file' can be configured")
+        elif options_sum < 1:
+            raise ValueError("one of 'address', 'subtree' or 'file' must be configured")
+
+        if bool(self.file) == bool(self.name):
+            raise ValueError("one of 'file' or 'name' must be configured")
 
 
 class RPZSchema(ConfigSchema):
@@ -81,7 +72,7 @@ class LocalDataSchema(ConfigSchema):
     addresses: Direct addition of hostname and IP addresses pairs.
     addresses_files: Direct addition of hostname and IP addresses pairs from files in '/etc/hosts' like format.
     records: Direct addition of records in DNS zone file format.
-    subtrees: Direct addition of subtrees.
+    rules: Local data rules.
     rpz: List of Response Policy Zones and its configuration.
     """
 
@@ -92,5 +83,5 @@ class LocalDataSchema(ConfigSchema):
     addresses: Optional[Dict[DomainName, ListOrItem[IPAddress]]] = None
     addresses_files: Optional[List[File]] = None
     records: Optional[EscapedStr] = None
-    subtrees: Optional[List[SubtreeSchema]] = None
+    rules: Optional[List[RuleSchema]] = None
     rpz: Optional[List[RPZSchema]] = None
index 8908a9cdf96c57d382bd87be64ea072ebd5038f0..31b0f78b6c704d7d481a7ffde1fc25b43564054e 100644 (file)
@@ -1,4 +1,4 @@
-{% from 'macros/local_data_macros.lua.j2' import local_data_subtree_root, local_data_records, local_data_root_fallback_addresses, local_data_root_fallback_addresses_files, local_data_addresses, local_data_addresses_files %}
+{% from 'macros/local_data_macros.lua.j2' import local_data_rules, local_data_records, local_data_root_fallback_addresses, local_data_root_fallback_addresses_files, local_data_addresses, local_data_addresses_files %}
 {% from 'macros/common_macros.lua.j2' import boolean %}
 
 modules = { 'hints > iterate' }
@@ -28,19 +28,9 @@ modules = { 'hints > iterate' }
 {{ local_data_records(cfg.local_data.records, false, cfg.local_data.ttl, cfg.local_data.nodata) }}
 {%- endif %}
 
-{# subtrees #}
-{% if cfg.local_data.subtrees -%}
-{% for subtree in cfg.local_data.subtrees %}
-{% if subtree.roots -%}
-{% for root in subtree.roots %}
-{{ local_data_subtree_root(root, subtree.type, subtree.ttl or cfg.local_data.ttl, subtree.tags) }}
-{% endfor %}
-{%- elif subtree.roots_file -%}
-{# TODO: not implemented yet #}
-{%- elif subtree.roots_url -%}
-{# TODO: not implemented yet #}
-{%- endif %}
-{% endfor %}
+{# rules #}
+{% if cfg.local_data.rules -%}
+{{ local_data_rules(cfg.local_data.rules, cfg.local_data.nodata, cfg.local_data.ttl) }}
 {%- endif %}
 
 {# rpz #}
index 5b6081bf35978803374bc1d4f99525b1d4dbbc7c..64f35088198594ab7fb85bb5dceb91a790523b07 100644 (file)
@@ -25,20 +25,31 @@ hints.root_file('{{ file }}')
 {%- endif -%}
 {%- endmacro -%}
 
-{% macro local_data_addresses(pairs, nodata, ttl, tags) -%}
-{%- for name, addresses in pairs.items() -%}
-{% for address in addresses %}
+
+{% macro kr_rule_local_address(name, address, nodata, ttl, tags=none) -%}
 assert(C.kr_rule_local_address('{{ name }}', '{{ address }}',
-       {{ boolean(nodata) }}, {{ local_data_ttl(ttl)}}, {{ policy_get_tagset(tags) }}) == 0)
+    {{ boolean(nodata) }}, {{ local_data_ttl(ttl)}}, {{ policy_get_tagset(tags) }}) == 0)
+{%- endmacro -%}
+
+
+{% macro local_data_addresses(pairs, nodata, ttl) -%}
+{% for name, addresses in pairs.items() %}
+{% for address in obj %}
+{{ kr_rule_local_address(name, address, nodata, ttl) }}
 {% endfor %}
-{%- endfor -%}
+{% endfor%}
+{%- endmacro %}
+
+
+{% macro kr_rule_local_hosts(file, nodata, ttl, tags=none) -%}
+assert(C.kr_rule_local_hosts('{{ file }}', {{ boolean(nodata) }},
+    {{ local_data_ttl(ttl)}}, {{ policy_get_tagset(tags) }}) == 0)
 {%- endmacro %}
 
 
 {% macro local_data_addresses_files(files, nodata, ttl, tags) -%}
 {% for file in files %}
-assert(C.kr_rule_local_hosts('{{ file }}', {{ boolean(nodata) }},
-       {{ local_data_ttl(ttl)}}, {{ policy_get_tagset(tags) }}) == 0)
+{{ kr_rule_local_hosts(file, nodata, ttl, tags) }}
 {% endfor %}
 {%- endmacro %}
 
@@ -59,7 +70,29 @@ assert(C.kr_rule_local_hosts('{{ file }}', {{ boolean(nodata) }},
 assert(C.kr_rule_zonefile({{ id }})==0)
 {%- endmacro %}
 
-{% macro local_data_subtree_root(root, type, ttl, tags) -%}
-assert(C.kr_rule_local_subtree(todname('{{ root }}'),
-       C.KR_RULE_SUB_{{ type.upper() }}, {{ local_data_ttl(ttl) }}, {{ policy_get_tagset(tags) }}) == 0)
+
+{% macro kr_rule_local_subtree(name, type, ttl, tags=none) -%}
+assert(C.kr_rule_local_subtree(todname('{{ name }}'),
+    C.KR_RULE_SUB_{{ type.upper() }}, {{ local_data_ttl(ttl) }}, {{ policy_get_tagset(tags) }}) == 0)
+{%- endmacro %}
+
+
+{% macro local_data_rules(items, nodata, ttl) -%}
+{% for item in items %}
+{% if item.name %}
+{% for name in item.name %}
+{% if item.address %}
+{% for address in item.address %}
+{{ kr_rule_local_address(name, address, nodata if item.nodata is none else item.nodata, item.ttl or ttl, item.tags) }}
+{% endfor %}
+{% elif item.subtree %}
+{{ kr_rule_local_subtree(name, item.subtree, item.ttl or ttl, item.tags) }}
+{% endif %}
+{% endfor %}
+{% elif item.file %}
+{% for file in item.file %}
+{{ kr_rule_local_hosts(file, nodata if item.nodata is none else item.nodata, item.ttl or ttl, item.tags) }}
+{% endfor %}
+{% endif %}
+{% endfor %}
 {%- endmacro %}
index 3ad6dd7dc74df056bc7065f084944a61e22c453c..710fac153c405dc6874f8c8441d7193af9f64559 100644 (file)
@@ -3,32 +3,33 @@ from typing import Any
 import pytest
 from pytest import raises
 
-from knot_resolver_manager.datamodel.local_data_schema import LocalDataSchema, SubtreeSchema
+from knot_resolver_manager.datamodel.local_data_schema import RuleSchema
 from knot_resolver_manager.utils.modeling.exceptions import DataValidationError
 
 
 @pytest.mark.parametrize(
     "val",
     [
-        {"type": "empty", "roots": ["sub2.example.org"]},
-        {"type": "nxdomain", "roots": ["sub3.example.org", "sub5.example.net."], "ttl": "1h"},
-        # {"type": "empty", "roots-url": "https://example.org/blocklist.txt", "refresh": "1d"},
-        # {"type": "nxdomain", "roots-file": "/etc/hosts"},  # must be an existing file or validation will fail
-        {"type": "redirect", "roots": ["sub4.example.org"]},
+        {"name": ["sub2.example.org"], "subtree": "empty"},
+        {"name": ["sub3.example.org", "sub5.example.net."], "subtree": "nxdomain", "ttl": "1h"},
+        {"name": ["sub4.example.org"], "subtree": "redirect"},
+        {"name": ["sub5.example.org"], "address": ["127.0.0.1"]},
+        {"file": "/etc/hosts", "ttl": "20m", "nodata": True},
     ],
 )
 def test_subtree_valid(val: Any):
-    SubtreeSchema(val)
+    RuleSchema(val)
 
 
 @pytest.mark.parametrize(
     "val",
     [
-        {"type": "empty"},
-        {"type": "empty", "roots": ["sub2.example.org"], "roots-url": "https://example.org/blocklist.txt"},
-        {"type": "redirect", "roots": ["sub4.example.org"], "refresh": "1d"},
+        {"subtree": "empty"},
+        {"name": ["sub2.example.org"], "file": "/etc/hosts"},
+        {"name": ["sub4.example.org"], "address": ["127.0.0.1"], "subtree": "empty"},
+        {"name": ["sub4.example.org"], "subtree": "redirect", "file": "/etc/hosts"},
     ],
 )
 def test_subtree_invalid(val: Any):
     with raises(DataValidationError):
-        SubtreeSchema(val)
+        RuleSchema(val)