# 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:
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):
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.
"""
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
-{% 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' }
{{ 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 #}
{%- 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 %}
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 %}
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)