summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleš Mrázek <ales.mrazek@nic.cz>2023-06-09 14:26:57 +0200
committerAleš Mrázek <ales.mrazek@nic.cz>2023-06-09 14:26:57 +0200
commit8f756fdde96076a113a11d1e0531011d21f0891e (patch)
tree9b5da2d133ee21c9fee329ec39da2033d90db8fe
parentMerge branch 'manager-docs' into 'manager' (diff)
parentmanager: forward config example (diff)
downloadknot-resolver-8f756fdde96076a113a11d1e0531011d21f0891e.tar.xz
knot-resolver-8f756fdde96076a113a11d1e0531011d21f0891e.zip
Merge branch 'manager-datamodel-improvements' into 'manager'
manager: datamodel improvements See merge request knot/knot-resolver!1313
-rw-r--r--manager/etc/knot-resolver/config.policy.dev.yml71
-rw-r--r--manager/knot_resolver_manager/datamodel/cache_schema.py37
-rw-r--r--manager/knot_resolver_manager/datamodel/config_schema.py32
-rw-r--r--manager/knot_resolver_manager/datamodel/design-notes.yml237
-rw-r--r--manager/knot_resolver_manager/datamodel/forward_schema.py58
-rw-r--r--manager/knot_resolver_manager/datamodel/forward_zone_schema.py24
-rw-r--r--manager/knot_resolver_manager/datamodel/local_data_schema.py79
-rw-r--r--manager/knot_resolver_manager/datamodel/logging_schema.py1
-rw-r--r--manager/knot_resolver_manager/datamodel/network_schema.py17
-rw-r--r--manager/knot_resolver_manager/datamodel/options_schema.py6
-rw-r--r--manager/knot_resolver_manager/datamodel/policy_schema.py20
-rw-r--r--manager/knot_resolver_manager/datamodel/stub_zone_schema.py4
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/config.lua.j229
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j24
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/forward.lua.j27
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j220
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/local_data.lua.j230
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/logging.lua.j210
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j212
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/macros/forward_macros.lua.j242
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j244
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j220
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j225
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j225
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/network.lua.j25
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/options.lua.j210
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j214
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/views.lua.j223
-rw-r--r--manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j24
-rw-r--r--manager/knot_resolver_manager/datamodel/types/__init__.py6
-rw-r--r--manager/knot_resolver_manager/datamodel/types/base_types.py3
-rw-r--r--manager/knot_resolver_manager/datamodel/types/generic_types.py33
-rw-r--r--manager/knot_resolver_manager/datamodel/types/types.py15
-rw-r--r--manager/knot_resolver_manager/datamodel/view_schema.py33
-rw-r--r--manager/knot_resolver_manager/kres_manager.py2
-rw-r--r--manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py27
-rw-r--r--manager/knot_resolver_manager/utils/modeling/__init__.py2
-rw-r--r--manager/knot_resolver_manager/utils/modeling/base_generic_type_wrapper.py9
-rw-r--r--manager/knot_resolver_manager/utils/modeling/base_schema.py33
-rw-r--r--manager/knot_resolver_manager/utils/modeling/base_value_type.py26
-rw-r--r--manager/knot_resolver_manager/utils/modeling/types.py18
-rw-r--r--manager/knot_resolver_manager/utils/requests.py2
-rw-r--r--manager/poetry.lock2
-rw-r--r--manager/pyproject.toml6
-rw-r--r--manager/scripts/_env.sh33
-rwxr-xr-xmanager/scripts/run-new-policy29
-rw-r--r--manager/tests/unit/datamodel/templates/test_common_macros.py26
-rw-r--r--manager/tests/unit/datamodel/templates/test_forward_macros.py27
-rw-r--r--manager/tests/unit/datamodel/templates/test_network_macros.py12
-rw-r--r--manager/tests/unit/datamodel/templates/test_policy_macros.py24
-rw-r--r--manager/tests/unit/datamodel/templates/test_view_macros.py57
-rw-r--r--manager/tests/unit/datamodel/test_config_schema.py2
-rw-r--r--manager/tests/unit/datamodel/test_local_data.py33
-rw-r--r--manager/tests/unit/datamodel/test_network_schema.py18
-rw-r--r--manager/tests/unit/datamodel/test_policy_schema.py4
-rw-r--r--manager/tests/unit/datamodel/types/test_custom_types.py5
-rw-r--r--manager/tests/unit/datamodel/types/test_generic_types.py56
57 files changed, 1222 insertions, 231 deletions
diff --git a/manager/etc/knot-resolver/config.policy.dev.yml b/manager/etc/knot-resolver/config.policy.dev.yml
new file mode 100644
index 00000000..b93fcceb
--- /dev/null
+++ b/manager/etc/knot-resolver/config.policy.dev.yml
@@ -0,0 +1,71 @@
+rundir: runtime
+workers: 1
+management:
+ interface: 127.0.0.1@5000
+cache:
+ storage: cache
+logging:
+ level: notice
+ groups:
+ - manager
+ - supervisord
+network:
+ listen:
+ - interface: 127.0.0.1@5353
+
+views:
+ - subnets: [127.0.0.0/24]
+ tags: [t01, t02, t03]
+ options:
+ dns64: false
+ - subnets: [ 0.0.0.0/0, "::/0" ]
+ answer: refused
+ - subnets: [10.0.10.0/24]
+ answer: allow
+
+local-data:
+ ttl: 60m
+ nodata: false
+ records: |
+ example.net. TXT "foo bar"
+ A 192.168.2.3
+ A 192.168.2.4
+ local.example.org AAAA ::1
+ subtrees:
+ - type: empty
+ tags: [ t2 ]
+ roots: [ example1.org ]
+ - type: nxdomain
+ roots: [ sub4.example.org ]
+ rpz:
+ - file: runtime/blocklist.rpz
+ tags: [t01, t02]
+
+# ttl: 1d
+# nodata: true
+# addresses:
+# foo.bar: [ 127.0.0.1, "::1" ]
+# my.pc.corp: 192.168.12.95
+# 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
+
+forward:
+ - subtree: '.'
+ options:
+ dnssec: true
+ authoritative: false
+ servers:
+ - address: [2001:148f:fffe::1, 185.43.135.1]
+ transport: tls
+ hostname: odvr.nic.cz
+ - address: [ 192.0.2.1, 192.0.2.2 ]
+ pin-sha256: ['YQ==', 'Wg==']
+ - subtree: 1.168.192.in-addr.arpa
+ options:
+ dnssec: false
+ servers: [ 192.0.2.1@5353 ]
diff --git a/manager/knot_resolver_manager/datamodel/cache_schema.py b/manager/knot_resolver_manager/datamodel/cache_schema.py
index 783b2c15..e40f1e23 100644
--- a/manager/knot_resolver_manager/datamodel/cache_schema.py
+++ b/manager/knot_resolver_manager/datamodel/cache_schema.py
@@ -1,6 +1,8 @@
-from typing import List, Optional
+from typing import List, Optional, Union
-from knot_resolver_manager.datamodel.types import Dir, DomainName, File, SizeUnit, TimeUnit
+from typing_extensions import Literal
+
+from knot_resolver_manager.datamodel.types import Dir, DomainName, File, IntNonNegative, Percent, SizeUnit, TimeUnit
from knot_resolver_manager.utils.modeling import ConfigSchema
@@ -25,23 +27,50 @@ class PrefillSchema(ConfigSchema):
raise ValueError("cache prefilling is not yet supported for non-root zones")
+class GarbageCollectorSchema(ConfigSchema):
+ """
+ Configuration options of the cache garbage collector (kres-cache-gc).
+
+ ---
+ interval: Time interval how often the garbage collector will be run.
+ threshold: Cache usage in percent that triggers the garbage collector.
+ release: Percent of used cache to be freed by the garbage collector.
+ temp_keys_space: Maximum amount of temporary memory for copied keys (0 = unlimited).
+ rw_deletes: Maximum number of deleted records per read-write transaction (0 = unlimited).
+ rw_reads: Maximum number of readed records per read-write transaction (0 = unlimited).
+ rw_duration: Maximum duration of read-write transaction (0 = unlimited).
+ rw_delay: Wait time between two read-write transactions.
+ dry_run: Run the garbage collector in dry-run mode.
+ """
+
+ interval: TimeUnit = TimeUnit("1s")
+ threshold: Percent = Percent(80)
+ release: Percent = Percent(10)
+ temp_keys_space: SizeUnit = SizeUnit(0)
+ rw_deletes: IntNonNegative = IntNonNegative(100)
+ rw_reads: IntNonNegative = IntNonNegative(200)
+ rw_duration: TimeUnit = TimeUnit(0)
+ rw_delay: TimeUnit = TimeUnit(0)
+ dry_run: bool = False
+
+
class CacheSchema(ConfigSchema):
"""
DNS resolver cache configuration.
---
- garbage_collector: Automatically use garbage collector to periodically clear cache.
storage: Cache storage of the DNS resolver.
size_max: Maximum size of the cache.
+ garbage_collector: Use the garbage collector (kres-cache-gc) to periodically clear cache.
ttl_min: Minimum time-to-live for the cache entries.
ttl_max: Maximum time-to-live for the cache entries.
ns_timeout: Time interval for which a nameserver address will be ignored after determining that it does not return (useful) answers.
prefill: Prefill the cache periodically by importing zone data obtained over HTTP.
"""
- garbage_collector: bool = True
storage: Dir = Dir("/var/cache/knot-resolver")
size_max: SizeUnit = SizeUnit("100M")
+ garbage_collector: Union[GarbageCollectorSchema, Literal[False]] = GarbageCollectorSchema()
ttl_min: TimeUnit = TimeUnit("5s")
ttl_max: TimeUnit = TimeUnit("6d")
ns_timeout: TimeUnit = TimeUnit("1000ms")
diff --git a/manager/knot_resolver_manager/datamodel/config_schema.py b/manager/knot_resolver_manager/datamodel/config_schema.py
index 9e1903a3..5df263c6 100644
--- a/manager/knot_resolver_manager/datamodel/config_schema.py
+++ b/manager/knot_resolver_manager/datamodel/config_schema.py
@@ -11,7 +11,8 @@ from knot_resolver_manager.constants import MAX_WORKERS
from knot_resolver_manager.datamodel.cache_schema import CacheSchema
from knot_resolver_manager.datamodel.dns64_schema import Dns64Schema
from knot_resolver_manager.datamodel.dnssec_schema import DnssecSchema
-from knot_resolver_manager.datamodel.forward_zone_schema import ForwardZoneSchema
+from knot_resolver_manager.datamodel.forward_schema import ForwardSchema
+from knot_resolver_manager.datamodel.local_data_schema import LocalDataSchema
from knot_resolver_manager.datamodel.logging_schema import LoggingSchema
from knot_resolver_manager.datamodel.lua_schema import LuaSchema
from knot_resolver_manager.datamodel.management_schema import ManagementSchema
@@ -19,10 +20,7 @@ from knot_resolver_manager.datamodel.monitoring_schema import MonitoringSchema
from knot_resolver_manager.datamodel.network_schema import NetworkSchema
from knot_resolver_manager.datamodel.options_schema import OptionsSchema
from knot_resolver_manager.datamodel.policy_schema import PolicySchema
-from knot_resolver_manager.datamodel.rpz_schema import RPZSchema
from knot_resolver_manager.datamodel.slice_schema import SliceSchema
-from knot_resolver_manager.datamodel.static_hints_schema import StaticHintsSchema
-from knot_resolver_manager.datamodel.stub_zone_schema import StubZoneSchema
from knot_resolver_manager.datamodel.types import IntPositive
from knot_resolver_manager.datamodel.types.files import UncheckedPath
from knot_resolver_manager.datamodel.view_schema import ViewSchema
@@ -73,9 +71,9 @@ def _cpu_count() -> Optional[int]:
return cpus
-def _default_max_worker_count() -> Optional[int]:
+def _default_max_worker_count() -> int:
c = _cpu_count()
- if c is not None:
+ if c:
return c * 10
return MAX_WORKERS
@@ -96,13 +94,11 @@ class KresConfig(ConfigSchema):
webmgmt: Configuration of legacy web management endpoint.
options: Fine-tuning global parameters of DNS resolver operation.
network: Network connections and protocols configuration.
- static_hints: Static hints for forward records (A/AAAA) and reverse records (PTR)
views: List of views and its configuration.
+ local_data: Local data for forward records (A/AAAA) and reverse records (PTR).
slices: Split the entire DNS namespace into distinct slices.
policy: List of policy rules and its configuration.
- rpz: List of Response Policy Zones and its configuration.
- stub_zones: List of Stub Zones and its configuration.
- forward_zones: List of Forward Zones and its configuration.
+ forward: List of Forward Zones and its configuration.
cache: DNS resolver cache configuration.
dnssec: Disable DNSSEC, enable with defaults or set new configuration.
dns64: Disable DNS64 (RFC 6147), enable with defaults or set new configuration.
@@ -121,13 +117,11 @@ class KresConfig(ConfigSchema):
webmgmt: Optional[WebmgmtSchema] = None
options: OptionsSchema = OptionsSchema()
network: NetworkSchema = NetworkSchema()
- static_hints: StaticHintsSchema = StaticHintsSchema()
- views: Optional[Dict[str, ViewSchema]] = None
+ views: Optional[List[ViewSchema]] = None
+ local_data: LocalDataSchema = LocalDataSchema()
slices: Optional[List[SliceSchema]] = None
policy: Optional[List[PolicySchema]] = None
- rpz: Optional[List[RPZSchema]] = None
- stub_zones: Optional[List[StubZoneSchema]] = None
- forward_zones: Optional[List[ForwardZoneSchema]] = None
+ forward: Optional[List[ForwardSchema]] = None
cache: CacheSchema = CacheSchema()
dnssec: Union[bool, DnssecSchema] = True
dns64: Union[bool, Dns64Schema] = False
@@ -146,13 +140,11 @@ class KresConfig(ConfigSchema):
webmgmt: Optional[WebmgmtSchema]
options: OptionsSchema
network: NetworkSchema
- static_hints: StaticHintsSchema
- views: Optional[Dict[str, ViewSchema]]
+ views: Optional[List[ViewSchema]]
+ local_data: LocalDataSchema
slices: Optional[List[SliceSchema]]
policy: Optional[List[PolicySchema]]
- rpz: Optional[List[RPZSchema]]
- stub_zones: Optional[List[StubZoneSchema]]
- forward_zones: Optional[List[ForwardZoneSchema]]
+ forward: Optional[List[ForwardSchema]]
cache: CacheSchema
dnssec: Union[Literal[False], DnssecSchema]
dns64: Union[Literal[False], Dns64Schema]
diff --git a/manager/knot_resolver_manager/datamodel/design-notes.yml b/manager/knot_resolver_manager/datamodel/design-notes.yml
new file mode 100644
index 00000000..fb909acc
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/design-notes.yml
@@ -0,0 +1,237 @@
+###### Working notes about configuration schema
+
+
+## TODO nit: nest one level deeper inside `dnssec`, probably
+dnssec:
+ keep-removed: 0
+ refresh-time: 10s
+ hold-down-time: 30d
+
+## TODO nit: I don't like this name, at least not for the experimental thing we have there
+network:
+ tls:
+ auto_discovery: boolean
+
+#### General questions
+Plurals: do we name attributes in plural if they're a list;
+ some of them even allow a non-list if using a single element.
+
+
+#### New-policy brainstorming
+
+dnssec:
+ # Convert to key: style instead of list?
+ # - easier to handle in API/CLI (which might be a common action on names with broken DNSSEC)
+ # - allows to supply a value - stamp for expiration of that NTA
+ # (absolute time, but I can imagine API/CLI converting from duration when executed)
+ # - syntax isn't really more difficult, mainly it forces one entry per line (seems OK)
+ negative-trust-anchors:
+ example.org:
+ my.example.net:
+
+
+view:
+ # When a client request arrives, based on the `view` class of rules we may either
+ # decide for a direct answer or for marking the request with a set of tags.
+ # The concepts of matching and actions are a very good fit for this,
+ # and that matches our old policy approach. Matching here should avoid QNAME+QTYPE;
+ # instead it's e.g. suitable for access control.
+ # RPZ files also support rules that fall into this `view` class.
+ #
+ # Selecting a single rule: the most specific client-IP prefix
+ # that also matches additional conditions.
+ - subnet: [ 0.0.0.0/0, ::/0 ]
+ answer: refused
+ # some might prefer `allow: refused` ?
+ # Also, RCODEs are customary in CAPITALS though maybe not in configs.
+
+ - subnet: [ 10.0.0.0/8, 192.168.0.0/16 ]
+ # Adding `tags` implies allowing the query.
+ tags: [ t1, t2, t3 ] # theoretically we could use space-separated string
+ options: # only some of the global options can be overridden in view
+ minimize: true
+ dns64: true
+ rate-limit: # future option, probably (optionally?) structured
+ # LATER: rulesets are a relatively unclear feature for now.
+ # Their main point is to allow prioritization and avoid
+ # intermixing rules that come from different sources.
+ # Also some properties might be specifyable per ruleset.
+ ruleset: tt
+
+ - subnet: [ 10.0.10.0/24 ] # maybe allow a single value instead of a list?
+ # LATER: special addresses?
+ # - for kresd-internal requests
+ # - shorthands for all private IPv4 and/or IPv6;
+ # though yaml's repeated nodes could mostly cover that
+ # or just copy&paste from docs
+ answer: allow
+
+# Or perhaps a more complex approach? Probably not.
+# We might have multiple conditions at once and multiple actions at once,
+# but I don't expect these to be common, so the complication is probably not worth it.
+# An advantage would be that the separation of the two parts would be more visible.
+view:
+ - match:
+ subnet: [ 10.0.0.0/8, 192.168.0.0/16 ]
+ do:
+ tags: [ t1, t2, t3 ]
+ options: # ...
+
+
+local-data: # TODO: name
+ #FIXME: tags - allow assigning them to (groups of) addresses/records.
+
+ addresses: # automatically adds PTR records and NODATA (LATER: overridable NODATA?)
+ foo.bar: [ 127.0.0.1, ::1 ]
+ my.pc.corp: 192.168.12.95
+ addresses-files: # files in /etc/hosts format (and semantics like `addresses`)
+ - /etc/hosts
+
+ # Zonefile format seems quite handy here. Details:
+ # - probably use `local-data.ttl` from model as the default
+ # - and . root to avoid confusion if someone misses a final dot.
+ records: |
+ example.net. TXT "foo bar"
+ A 192.168.2.3
+ A 192.168.2.4
+ local.example.org AAAA ::1
+
+ subtrees:
+ nodata: true # impl ATM: defaults to false, set (only) for each rule/name separately
+ # impl: options like `ttl` and `nodata` might make sense to be settable (only?) per ruleset
+
+ subtrees: # TODO: perhaps just allow in the -tagged style, if we can't avoid lists anyway?
+ - type: empty
+ roots: [ sub2.example.org ] # TODO: name it the same as for forwarding
+ tags: [ t2 ]
+ - type: nxdomain
+ # Will we need to support multiple file formats in future and choose here?
+ roots-file: /path/to/file.txt
+ - type: empty
+ roots-url: https://example.org/blocklist.txt
+ refresh: 1d
+ # Is it a separate ruleset? Optionally? Persistence?
+ # (probably the same questions for local files as well)
+
+ - type: redirect
+ roots: [ sub4.example.org ]
+ addresses: [ 127.0.0.1, ::1 ]
+
+local-data-tagged: # TODO: name (view?); and even structure seems unclear.
+ # TODO: allow only one "type" per list entry? (addresses / addresses-files / subtrees / ...)
+ - tags: [ t1, t2 ]
+ addresses: #... otherwise the same as local-data
+ - tags: [ t2 ]
+ records: # ...
+ - tags: [ t3 ]
+ subtrees: empty
+ roots: [ sub2.example.org ]
+
+local-data-tagged: # this avoids lists, so it's relatively easy to amend through API
+ "t1 t2": # perhaps it's not nice that tags don't form a proper list?
+ addresses:
+ foo.bar: [ 127.0.0.1, ::1 ]
+ t4:
+ addresses:
+ foo.bar: [ 127.0.0.1, ::1 ]
+local-data: # avoids lists and merges into the untagged `local-data` config subtree
+ tagged: # (getting quite deep, though)
+ t1 t2:
+ addresses:
+ foo.bar: [ 127.0.0.1, ::1 ]
+# or even this ugly thing:
+local-data-tagged t1 t2:
+ addresses:
+ foo.bar: [ 127.0.0.1, ::1 ]
+
+forward: # TODO: "name" is from Unbound, but @vcunat would prefer "subtree" or something.
+ - name: '.' # Root is the default so could be omitted?
+ servers: [2001:148f:fffe::1, 2001:148f:ffff::1, 185.43.135.1, 193.14.47.1]
+ # TLS forward, server authenticated using hostname and system-wide CA certificates
+ # https://knot-resolver.readthedocs.io/en/stable/modules-policy.html?highlight=forward#tls-examples
+ - name: '.'
+ servers:
+ - address: [ 192.0.2.1, 192.0.2.2@5353 ]
+ transport: tls
+ pin-sha256: Wg==
+ - address: 2001:DB8::d0c
+ transport: tls
+ hostname: res.example.com
+ ca-file: /etc/knot-resolver/tlsca.crt
+ options:
+ # LATER: allow a subset of options here, per sub-tree?
+ # Though that's not necessarily related to forwarding (e.g. TTL limits),
+ # especially implementation-wise it probably won't matter.
+
+
+# Too confusing approach, I suppose? Different from usual way of thinking but closer to internal model.
+# Down-sides:
+# - multiple rules for the same name won't be possible (future, with different tags)
+# - loading names from a file won't be possible (or URL, etc.)
+rules:
+ example.org: &fwd_odvr
+ type: forward
+ servers: [2001:148f:fffe::1, 2001:148f:ffff::1, 185.43.135.1, 193.14.47.1]
+ sub2.example.org:
+ type: empty
+ tags: [ t3, t5 ]
+ sub3.example.org:
+ type: forward-auth
+ dnssec: no
+
+
+# @amrazek: current valid config
+
+views:
+ - subnets: [ 0.0.0.0/0, "::/0" ]
+ answer: refused
+ - subnets: [ 0.0.0.0/0, "::/0" ]
+ tags: [t01, t02, t03]
+ options:
+ minimize: true # default
+ dns64: true # default
+ - subnets: 10.0.10.0/24 # can be single value
+ answer: allow
+
+local-data:
+ ttl: 1d
+ nodata: true
+ addresses:
+ foo.bar: [ 127.0.0.1, "::1" ]
+ my.pc.corp: 192.168.12.95
+ 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
+ roots: [ sub2.example.org ]
+ tags: [ t2 ]
+ - type: nxdomain
+ roots-file: /path/to/file.txt
+ - type: empty
+ roots-url: https://example.org/blocklist.txt
+ refresh: 1d
+ - type: redirect
+ roots: [ sub4.example.org ]
+ addresses: [ 127.0.0.1, "::1" ]
+
+forward:
+ - subtree: '.'
+ servers:
+ - address: [ 192.0.2.1, 192.0.2.2@5353 ]
+ transport: tls
+ pin-sha256: Wg==
+ - address: 2001:DB8::d0c
+ transport: tls
+ hostname: res.example.com
+ ca-file: /etc/knot-resolver/tlsca.crt
+ options:
+ dnssec: true # default
+ - subtree: 1.168.192.in-addr.arpa
+ servers: [ 192.0.2.1@5353 ]
+ options:
+ dnssec: false # policy.STUB? \ No newline at end of file
diff --git a/manager/knot_resolver_manager/datamodel/forward_schema.py b/manager/knot_resolver_manager/datamodel/forward_schema.py
new file mode 100644
index 00000000..df30229d
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/forward_schema.py
@@ -0,0 +1,58 @@
+from typing import List, Optional, Union
+
+from typing_extensions import Literal
+
+from knot_resolver_manager.datamodel.types import DomainName, IPAddressOptionalPort, ListOrItem
+from knot_resolver_manager.datamodel.types.files import FilePath
+from knot_resolver_manager.utils.modeling import ConfigSchema
+
+
+class ForwardServerSchema(ConfigSchema):
+ """
+ Forward server configuration options.
+
+ ---
+ address: IP address(es) of a forward server.
+ transport: Transport protocol for a forward server.
+ pin_sha256: Hash of accepted CA certificate.
+ hostname: Hostname of the Forward server.
+ ca_file: Path to CA certificate file.
+ """
+
+ address: ListOrItem[IPAddressOptionalPort]
+ transport: Optional[Literal["tls"]] = None
+ pin_sha256: Optional[ListOrItem[str]] = None
+ hostname: Optional[DomainName] = None
+ ca_file: Optional[FilePath] = None
+
+ def _validate(self) -> None:
+ if self.pin_sha256 and (self.hostname or self.ca_file):
+ ValueError("'pin-sha256' cannot be configurad together with 'hostname' or 'ca-file'")
+
+
+class ForwardOptionsSchema(ConfigSchema):
+ """
+ Configuration options for forward subtree.
+
+ ---
+ authoritative: The forwarding target is an authoritative server.
+ dnssec: Enable/disable DNSSEC.
+ """
+
+ authoritative: bool = False
+ dnssec: bool = True
+
+
+class ForwardSchema(ConfigSchema):
+ """
+ Configuration of forward subtree.
+
+ ---
+ subtree: Subtree to forward.
+ servers: Forward server configuration.
+ options: Configuration options for forward subtree.
+ """
+
+ subtree: DomainName
+ servers: Union[List[IPAddressOptionalPort], List[ForwardServerSchema]]
+ options: ForwardOptionsSchema = ForwardOptionsSchema()
diff --git a/manager/knot_resolver_manager/datamodel/forward_zone_schema.py b/manager/knot_resolver_manager/datamodel/forward_zone_schema.py
deleted file mode 100644
index 8b7973b6..00000000
--- a/manager/knot_resolver_manager/datamodel/forward_zone_schema.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from typing import List, Optional, Union
-
-from knot_resolver_manager.datamodel.policy_schema import ForwardServerSchema
-from knot_resolver_manager.datamodel.types import DomainName, IPAddressOptionalPort, PolicyFlagEnum
-from knot_resolver_manager.utils.modeling import ConfigSchema
-
-
-class ForwardZoneSchema(ConfigSchema):
- """
- Configuration of Forward Zone.
-
- ---
- name: Domain name of the zone.
- tls: Enable/disable TLS for Forward servers.
- servers: IP address of Forward server.
- views: Use this Forward Zone only for clients defined by views.
- options: Configuration flags for Forward Zone.
- """
-
- name: DomainName
- tls: bool = False
- servers: Union[List[IPAddressOptionalPort], List[ForwardServerSchema]]
- views: Optional[List[str]] = None
- options: Optional[List[PolicyFlagEnum]] = None
diff --git a/manager/knot_resolver_manager/datamodel/local_data_schema.py b/manager/knot_resolver_manager/datamodel/local_data_schema.py
new file mode 100644
index 00000000..9461362f
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/local_data_schema.py
@@ -0,0 +1,79 @@
+from typing import Dict, List, Optional
+
+from typing_extensions import Literal
+
+from knot_resolver_manager.datamodel.types import DomainName, IDPattern, IPAddress, TimeUnit
+from knot_resolver_manager.datamodel.types.files import FilePath, UncheckedPath
+from knot_resolver_manager.utils.modeling import ConfigSchema
+
+
+class SubtreeSchema(ConfigSchema):
+ """
+ Local data and configuration of subtree.
+
+ ---
+ type: Type of the 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).
+ addresses: Subtree addresses.
+ roots: Subtree roots.
+ 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"]
+ tags: Optional[List[IDPattern]] = None
+ ttl: Optional[TimeUnit] = None
+ nodata: bool = True
+ addresses: Optional[List[IPAddress]] = None
+ roots: Optional[List[DomainName]] = None
+ roots_file: Optional[UncheckedPath] = None
+ roots_url: Optional[str] = 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'")
+
+
+class RPZSchema(ConfigSchema):
+ """
+ Configuration or Response Policy Zone (RPZ).
+
+ ---
+ file: Path to the RPZ zone file.
+ tags: Tags to link with other policy rules.
+ """
+
+ file: FilePath
+ tags: Optional[List[IDPattern]] = None
+
+
+class LocalDataSchema(ConfigSchema):
+ """
+ Local data for forward records (A/AAAA) and reverse records (PTR).
+
+ ---
+ ttl: Default TTL value used for added local data/records.
+ nodata: Use NODATA synthesis. NODATA will be synthesised for matching name, but mismatching type(e.g. AAAA query when only A exists).
+ 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.
+ rpz: List of Response Policy Zones and its configuration.
+ """
+
+ ttl: Optional[TimeUnit] = None
+ nodata: bool = True
+ addresses: Optional[Dict[DomainName, List[IPAddress]]] = None
+ addresses_files: Optional[List[UncheckedPath]] = None
+ records: Optional[str] = None
+ subtrees: Optional[List[SubtreeSchema]] = None
+ rpz: Optional[List[RPZSchema]] = None
diff --git a/manager/knot_resolver_manager/datamodel/logging_schema.py b/manager/knot_resolver_manager/datamodel/logging_schema.py
index 1217db23..fb05b826 100644
--- a/manager/knot_resolver_manager/datamodel/logging_schema.py
+++ b/manager/knot_resolver_manager/datamodel/logging_schema.py
@@ -21,6 +21,7 @@ LogTargetEnum = Literal["syslog", "stderr", "stdout"]
LogGroupsEnum: TypeAlias = Literal[
"manager",
"supervisord",
+ "cache-gc",
"system",
"cache",
"io",
diff --git a/manager/knot_resolver_manager/datamodel/network_schema.py b/manager/knot_resolver_manager/datamodel/network_schema.py
index 0a177e39..2349bdc5 100644
--- a/manager/knot_resolver_manager/datamodel/network_schema.py
+++ b/manager/knot_resolver_manager/datamodel/network_schema.py
@@ -12,6 +12,7 @@ from knot_resolver_manager.datamodel.types import (
IPNetwork,
IPv4Address,
IPv6Address,
+ ListOrItem,
PortNumber,
SizeUnit,
)
@@ -84,24 +85,24 @@ class ListenSchema(ConfigSchema):
freebind: Used for binding to non-local address.
"""
- interface: Union[None, InterfaceOptionalPort, List[InterfaceOptionalPort]] = None
- unix_socket: Union[None, FilePath, List[FilePath]] = None
+ interface: Optional[ListOrItem[InterfaceOptionalPort]] = None
+ unix_socket: Optional[ListOrItem[FilePath]] = None
port: Optional[PortNumber] = None
kind: KindEnum = "dns"
freebind: bool = False
_LAYER = Raw
- interface: Union[None, InterfaceOptionalPort, List[InterfaceOptionalPort]]
- unix_socket: Union[None, FilePath, List[FilePath]]
+ interface: Optional[ListOrItem[InterfaceOptionalPort]]
+ unix_socket: Optional[ListOrItem[FilePath]]
port: Optional[PortNumber]
kind: KindEnum
freebind: bool
- def _interface(self, origin: Raw) -> Union[None, InterfaceOptionalPort, List[InterfaceOptionalPort]]:
- if isinstance(origin.interface, list):
+ def _interface(self, origin: Raw) -> Optional[ListOrItem[InterfaceOptionalPort]]:
+ if origin.interface:
port_set: Optional[bool] = None
- for intrfc in origin.interface:
+ for intrfc in origin.interface: # type: ignore[attr-defined]
if origin.port and intrfc.port:
raise ValueError("The port number is defined in two places ('port' option and '@<port>' syntax).")
if port_set is not None and (bool(intrfc.port) != port_set):
@@ -109,8 +110,6 @@ class ListenSchema(ConfigSchema):
"The '@<port>' syntax must be used either for all or none of the interface in the list."
)
port_set = bool(intrfc.port)
- elif isinstance(origin.interface, InterfaceOptionalPort) and origin.interface.port and origin.port:
- raise ValueError("The port number is defined in two places ('port' option and '@<port>' syntax).")
return origin.interface
def _port(self, origin: Raw) -> Optional[PortNumber]:
diff --git a/manager/knot_resolver_manager/datamodel/options_schema.py b/manager/knot_resolver_manager/datamodel/options_schema.py
index cee709a2..e95e5f88 100644
--- a/manager/knot_resolver_manager/datamodel/options_schema.py
+++ b/manager/knot_resolver_manager/datamodel/options_schema.py
@@ -28,7 +28,7 @@ class OptionsSchema(ConfigSchema):
---
glue_checking: Glue records scrictness checking level.
- qname_minimisation: Send minimum amount of information in recursive queries to enhance privacy.
+ minimize: Send minimum amount of information in recursive queries to enhance privacy.
query_loopback: Permits queries to loopback addresses.
reorder_rrset: Controls whether resource records within a RRSet are reordered each time it is served from the cache.
query_case_randomization: Randomize Query Character Case.
@@ -42,7 +42,7 @@ class OptionsSchema(ConfigSchema):
"""
glue_checking: GlueCheckingEnum = "normal"
- qname_minimisation: bool = True
+ minimize: bool = True
query_loopback: bool = False
reorder_rrset: bool = True
query_case_randomization: bool = True
@@ -57,7 +57,7 @@ class OptionsSchema(ConfigSchema):
_LAYER = Raw
glue_checking: GlueCheckingEnum
- qname_minimisation: bool
+ minimize: bool
query_loopback: bool
reorder_rrset: bool
query_case_randomization: bool
diff --git a/manager/knot_resolver_manager/datamodel/policy_schema.py b/manager/knot_resolver_manager/datamodel/policy_schema.py
index 3f5962ff..bbc61cd1 100644
--- a/manager/knot_resolver_manager/datamodel/policy_schema.py
+++ b/manager/knot_resolver_manager/datamodel/policy_schema.py
@@ -1,10 +1,9 @@
from typing import List, Optional, Union
+from knot_resolver_manager.datamodel.forward_schema import ForwardServerSchema
from knot_resolver_manager.datamodel.network_schema import AddressRenumberingSchema
from knot_resolver_manager.datamodel.types import (
DNSRecordTypeEnum,
- DomainName,
- File,
IPAddressOptionalPort,
PolicyActionEnum,
PolicyFlagEnum,
@@ -45,23 +44,6 @@ class AnswerSchema(ConfigSchema):
nodata: bool = False
-class ForwardServerSchema(ConfigSchema):
- """
- Configuration of Forward server.
-
- ---
- address: IP address of Forward server.
- pin_sha256: Hash of accepted CA certificate.
- hostname: Hostname of the Forward server.
- ca_file: Path to CA certificate file.
- """
-
- address: IPAddressOptionalPort
- pin_sha256: Optional[Union[str, List[str]]] = None
- hostname: Optional[DomainName] = None
- ca_file: Optional[File] = None
-
-
def _validate_policy_action(policy_action: Union["ActionSchema", "PolicySchema"]) -> None:
servers = ["mirror", "forward", "stub"]
diff --git a/manager/knot_resolver_manager/datamodel/stub_zone_schema.py b/manager/knot_resolver_manager/datamodel/stub_zone_schema.py
index 76e4d82c..b9945ecc 100644
--- a/manager/knot_resolver_manager/datamodel/stub_zone_schema.py
+++ b/manager/knot_resolver_manager/datamodel/stub_zone_schema.py
@@ -20,13 +20,13 @@ class StubZoneSchema(ConfigSchema):
Configuration of Stub Zone.
---
- name: Domain name of the zone.
+ subtree: Domain name of the zone.
servers: IP address of Stub server.
views: Use this Stub Zone only for clients defined by views.
options: Configuration flags for Stub Zone.
"""
- name: DomainName
+ subtree: DomainName
servers: Union[List[IPAddressOptionalPort], List[StubServerSchema]]
views: Optional[List[str]] = None
options: Optional[List[PolicyFlagEnum]] = None
diff --git a/manager/knot_resolver_manager/datamodel/templates/config.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/config.lua.j2
index 93a58ace..442354ab 100644
--- a/manager/knot_resolver_manager/datamodel/templates/config.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/config.lua.j2
@@ -1,5 +1,9 @@
{% if not cfg.lua.script_only %}
+-- FFI library
+ffi = require('ffi')
+local C = ffi.C
+
-- hostname
hostname('{{ cfg.hostname }}')
@@ -12,6 +16,9 @@ nsid.name('{{ cfg.nsid }}_' .. worker.id)
-- LOGGING section ----------------------------------
{% include "logging.lua.j2" %}
+-- MONITORING section -------------------------------
+{% include "monitoring.lua.j2" %}
+
-- WEBMGMT section ----------------------------------
{% include "webmgmt.lua.j2" %}
@@ -21,26 +28,23 @@ nsid.name('{{ cfg.nsid }}_' .. worker.id)
-- NETWORK section ----------------------------------
{% include "network.lua.j2" %}
--- STATIC-HINTS section -----------------------------
-{% include "static_hints.lua.j2" %}
-
-- VIEWS section ------------------------------------
{% include "views.lua.j2" %}
+-- LOCAL-DATA section -------------------------------
+{% include "local_data.lua.j2" %}
+
-- SLICES section -----------------------------------
-{% include "slices.lua.j2" %}
+{# {% include "slices.lua.j2" %} #}
-- POLICY section -----------------------------------
-{% include "policy.lua.j2" %}
+{# {% include "policy.lua.j2" %} #}
-- RPZ section --------------------------------------
-{% include "rpz.lua.j2" %}
-
--- STUB-ZONES section -------------------------------
-{% include "stub_zones.lua.j2" %}
+{# {% include "rpz.lua.j2" %} #}
--- FORWARD-ZONES section ----------------------------
-{% include "forward_zones.lua.j2" %}
+-- FORWARD section ----------------------------------
+{% include "forward.lua.j2" %}
-- CACHE section ------------------------------------
{% include "cache.lua.j2" %}
@@ -64,6 +68,3 @@ nsid.name('{{ cfg.nsid }}_' .. worker.id)
{% if cfg.lua.script %}
{{ cfg.lua.script }}
{% endif %}
-
--- manager's monitoring configuration
-{% include "monitoring.lua.j2" %} \ No newline at end of file
diff --git a/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2
index b26961bb..31a29bea 100644
--- a/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2
@@ -1,3 +1,5 @@
+{% from 'macros/common_macros.lua.j2' import boolean %}
+
{% if not cfg.dnssec %}
-- disable dnssec
trust_anchors.remove('.')
@@ -51,6 +53,6 @@ trust_anchors.set_insecure({
{% if cfg.dnssec.trust_anchors_files %}
-- dnssec.trust-anchors-files
{% for taf in cfg.dnssec.trust_anchors_files %}
-trust_anchors.add_file('{{ taf.file }}', readonly = {{ 'true' if taf.read_only else 'false' }})
+trust_anchors.add_file('{{ taf.file }}', readonly = {{ boolean(taf.read_only) }})
{% endfor %}
{% endif %} \ No newline at end of file
diff --git a/manager/knot_resolver_manager/datamodel/templates/forward.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/forward.lua.j2
new file mode 100644
index 00000000..afb2c492
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/templates/forward.lua.j2
@@ -0,0 +1,7 @@
+{% from 'macros/forward_macros.lua.j2' import policy_rule_forward_add %}
+
+{% if cfg.forward %}
+{% for fwd in cfg.forward %}
+{{ policy_rule_forward_add(fwd) }}
+{% endfor %}
+{% endif %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2
index 3a1fb4ae..26e0a9e8 100644
--- a/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2
@@ -3,7 +3,7 @@
{% if cfg.forward_zones %}
{% for zone in cfg.forward_zones %}
--- forward-zone: {{ zone.name }}
+-- forward-zone: {{ zone.subtree }}
{% if zone.views -%}
{# views set for forward-zone #}
{% for view_id in zone.views -%}
@@ -24,13 +24,13 @@
{% for tsig in view.tsig %}
{%- if options -%}
-{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }}
+{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
{% if zone.tls -%}
-{{ view_tsig(tsig|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ view_tsig(tsig|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.subtree|string))) }}
{% else %}
-{{ view_tsig(tsig|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ view_tsig(tsig|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
{% endfor %}
@@ -41,13 +41,13 @@
{% for addr in view.subnets %}
{%- if options -%}
-{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }}
+{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
{% if zone.tls -%}
-{{ view_addr(addr|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ view_addr(addr|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.subtree|string))) }}
{% else %}
-{{ view_addr(addr|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ view_addr(addr|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
{% endfor %}
@@ -58,13 +58,13 @@
{# no views set for forward-zone #}
{% if zone.options -%}
-{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.name|string))) }}
+{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
{% if zone.tls -%}
-{{ policy_add(policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ policy_add(policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.subtree|string))) }}
{% else %}
-{{ policy_add(policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ policy_add(policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
{% endif %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/local_data.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/local_data.lua.j2
new file mode 100644
index 00000000..d7e2110f
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/templates/local_data.lua.j2
@@ -0,0 +1,30 @@
+{% from 'macros/local_data_macros.lua.j2' import local_data_subtree_root, local_data_records %}
+
+{# TODO: implemented all other options/features from local_data_schema #}
+
+{# records #}
+{% if cfg.local_data.records -%}
+{{ 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(subtree.type, root, subtree.tags) }}
+{% endfor %}
+{%- elif subtree.roots_file -%}
+{# TODO: not implemented yet #}
+{%- elif subtree.roots_url -%}
+{# TODO: not implemented yet #}
+{%- endif %}
+{% endfor %}
+{%- endif %}
+
+{# rpz #}
+{% if cfg.local_data.rpz -%}
+{% for rpz in cfg.local_data.rpz %}
+{{ local_data_records(rpz.file, true, cfg.local_data.ttl, cfg.local_data.nodata, rpz.tags) }}
+{% endfor %}
+{%- endif %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2
index d7fb845d..2fb398e9 100644
--- a/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2
@@ -1,3 +1,5 @@
+{% from 'macros/common_macros.lua.j2' import boolean %}
+
-- logging.level
log_level('{{ cfg.logging.level }}')
@@ -27,15 +29,15 @@ modules.load('dnstap')
dnstap.config({
socket_path = '{{ cfg.logging.dnstap.unix_socket }}',
client = {
- log_queries = {{ 'true' if cfg.logging.dnstap.log_queries else 'false' }},
- log_responses = {{ 'true' if cfg.logging.dnstap.log_responses else 'false' }},
- log_tcp_rtt = {{ 'true' if cfg.logging.dnstap.log_tcp_rtt else 'false' }}
+ log_queries = {{ boolean(cfg.logging.dnstap.log_queries) }},
+ log_responses = {{ boolean(cfg.logging.dnstap.log_responses) }},
+ log_tcp_rtt = {{ boolean(cfg.logging.dnstap.log_tcp_rtt) }}
}
})
{%- endif %}
-- logging.debugging.assertion-abort
-debugging.assertion_abort = {{ 'true' if cfg.logging.debugging.assertion_abort else 'false' }}
+debugging.assertion_abort = {{ boolean(cfg.logging.debugging.assertion_abort) }}
-- logging.debugging.assertion-fork
debugging.assertion_fork = {{ cfg.logging.debugging.assertion_fork.millis() }}
diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2
index 1084a9b7..4c2ba11a 100644
--- a/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2
@@ -1,3 +1,15 @@
+{% macro quotes(string) -%}
+'{{ string }}'
+{%- endmacro %}
+
+{% macro boolean(val, negation=false) -%}
+{%- if negation -%}
+{{ 'false' if val else 'true' }}
+{%- else-%}
+{{ 'true' if val else 'false' }}
+{%- endif -%}
+{%- endmacro %}
+
{# Return string or table of strings #}
{% macro string_table(table) -%}
{%- if table is string -%}
diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/forward_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/forward_macros.lua.j2
new file mode 100644
index 00000000..f6777324
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/templates/macros/forward_macros.lua.j2
@@ -0,0 +1,42 @@
+{% from 'macros/common_macros.lua.j2' import boolean, string_table %}
+
+{% macro forward_options(options) -%}
+{dnssec={{ boolean(options.dnssec) }},auth={{ boolean(options.authoritative) }}}
+{%- endmacro %}
+
+{% macro forward_server(server) -%}
+{%- if server.address -%}
+{%- for addr in server.address -%}
+{'{{ addr }}',
+{%- if server.transport == 'tls' -%}
+tls=true,
+{%- else -%}
+tls=false,
+{%- endif -%}
+{%- if server.hostname -%}
+hostname='{{ server.hostname }}',
+{%- endif -%}
+{%- if server.pin_sha256 -%}
+pin_sha256={{ string_table(server.pin_sha256) }},
+{%- endif -%}
+{%- if server.ca_file -%}
+ca_file='{{ server.ca_file }}',
+{%- endif -%}
+},
+{%- endfor -%}
+{% else %}
+{'{{ server }}'},
+{%- endif -%}
+{%- endmacro %}
+
+{% macro forward_servers(servers) -%}
+{
+{%- for server in servers -%}
+{{ forward_server(server) }}
+{%- endfor -%}
+}
+{%- endmacro %}
+
+{% macro policy_rule_forward_add(forward) -%}
+policy.rule_forward_add('{{ forward.subtree }}',{{ forward_options(forward.options) }},{{ forward_servers(forward.servers) }})
+{%- endmacro %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j2
new file mode 100644
index 00000000..dde204e3
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j2
@@ -0,0 +1,44 @@
+{% from 'macros/common_macros.lua.j2' import string_table, boolean %}
+{% from 'macros/policy_macros.lua.j2' import policy_get_tagset, policy_todname %}
+
+{% macro local_data_records(input_str, is_rpz, ttl, nodata, tags=none, id='rrs') -%}
+{{ id }} = ffi.new('struct kr_rule_zonefile_config')
+{% if ttl %}
+{{ id }}.ttl = {{ ttl.millis() }}
+{% endif %}
+{% if tags %}
+{{ id }}.tags = {{ policy_get_tagset(tags) }}
+{% endif %}
+{{ id }}.nodata = {{ boolean(nodata) }}
+{{ id }}.is_rpz = {{ boolean(is_rpz) }}
+{% if is_rpz -%}
+{{ id }}.filename = '{{ input_str }}'
+{% else %}
+{{ id }}.input_str = [[
+{{ input_str }}]]
+{% endif %}
+assert(C.kr_rule_zonefile({{ id }})==0)
+{%- endmacro %}
+
+{% macro local_data_emptyzone(dname, tags) -%}
+assert(C.kr_rule_local_data_emptyzone({{ dname }},{{ tags }})==0)
+{%- endmacro %}
+
+{% macro local_data_nxdomain(dname, tags) -%}
+assert(C.kr_rule_local_data_nxdomain({{ dname }},{{ tags }})==0)
+{%- endmacro %}
+
+{% macro local_data_subtree_root(type, root, tags) -%}
+{%- if tags -%}
+{%- set get_tags = policy_get_tagset(tags) -%}
+{%- else -%}
+{%- set get_tags = '0' -%}
+{%- endif -%}
+{%- if type == 'empty' -%}
+{{ local_data_emptyzone(policy_todname(root), get_tags) }}
+{%- elif type == 'nxdomain' -%}
+{{ local_data_nxdomain(policy_todname(root), get_tags) }}
+{%- else -%}
+{# TODO: implement other possible types #}
+{%- endif -%}
+{%- endmacro %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2
index 933ecdfa..ff78fbd8 100644
--- a/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2
@@ -44,20 +44,12 @@ net.{{ interface.if_name }},
{% macro network_listen(listen) -%}
{%- if listen.unix_socket -%}
- {%- if listen.unix_socket is iterable-%}
- {% for path in listen.unix_socket -%}
- {{ net_listen_unix_socket(path, listen.kind, listen.freebind) }}
- {% endfor -%}
- {%- else -%}
- {{ net_listen_unix_socket(listen.unix_socket, listen.kind, listen.freebind) }}
- {%- endif -%}
+{% for path in listen.unix_socket %}
+{{ net_listen_unix_socket(path, listen.kind, listen.freebind) }}
+{% endfor %}
{%- elif listen.interface -%}
- {%- if listen.interface is iterable-%}
- {% for interface in listen.interface -%}
- {{ net_listen_interface(interface, listen.kind, listen.freebind, listen.port) }}
- {% endfor -%}
- {%- else -%}
- {{ net_listen_interface(listen.interface, listen.kind, listen.freebind, listen.port) }}
- {%- endif -%}
+{% for interface in listen.interface %}
+{{ net_listen_interface(interface, listen.kind, listen.freebind, listen.port) }}
+{% endfor %}
{%- endif -%}
{%- endmacro %} \ No newline at end of file
diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2
index 8ffd83a5..36ce102f 100644
--- a/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2
@@ -37,17 +37,22 @@ policy.slice_randomize_psl()
{% macro policy_flags(flags) -%}
policy.FLAGS({
-{%- if flags is string -%}
-'{{ flags.upper().replace("-", "_")|string }}'
-{%- else -%}
-{%- for flag in flags|list -%}
-'{{ flag.upper().replace("-", "_") }}',
-{%- endfor -%}
-{%- endif -%}
+{{- flags -}}
})
{%- endmacro %}
+{# Tags assign #}
+
+{% macro policy_tags_assign(tags) -%}
+policy.TAGS_ASSIGN({{ string_table(tags) }})
+{%- endmacro %}
+
+{% macro policy_get_tagset(tags) -%}
+policy.get_tagset({{ string_table(tags) }})
+{%- endmacro %}
+
+
{# Filters #}
{% macro policy_all(action) -%}
@@ -253,7 +258,11 @@ policy.TLS_FORWARD({{ tls_servers_table(servers) }})
{# Other #}
-{% macro policy_todname(names) -%}
+{% macro policy_todname(name) -%}
+todname('{{ name.punycode()|string }}')
+{%- endmacro %}
+
+{% macro policy_todnames(names) -%}
policy.todnames({
{%- if names is string -%}
'{{ names.punycode()|string }}'
diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2
index b829f2ef..efd03211 100644
--- a/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2
@@ -1,7 +1,22 @@
-{% macro view_tsig(tsig, rule) -%}
-view:tsig('{{ tsig }}',{{ rule }})
+{% macro view_insert_action(subnet, action) -%}
+assert(C.kr_view_insert_action('{{ subnet }}',{{ action }})==0)
{%- endmacro %}
-{% macro view_addr(addr, rule) -%}
-view:addr('{{ addr }}',{{ rule }})
-{%- endmacro %} \ No newline at end of file
+{% macro view_flags(options) -%}
+{% if not options.minimize -%}
+"NO_MINIMIZE",
+{%- endif %}
+{% if not options.dns64 -%}
+"DNS64_DISABLE",
+{%- endif %}
+{%- endmacro %}
+
+{% macro view_answer(answer) -%}
+{%- if answer == 'allow' -%}
+policy.TAGS_ASSIGN({})
+{%- elif answer == 'refused' -%}
+'policy.REFUSE'
+{%- elif answer == 'noanswer' -%}
+'policy.NO_ANSWER'
+{%- endif -%}
+{%- endmacro %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/network.lua.j2
index 60b09652..665ee454 100644
--- a/manager/knot_resolver_manager/datamodel/templates/network.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/network.lua.j2
@@ -1,8 +1,9 @@
+{% from 'macros/common_macros.lua.j2' import boolean %}
{% from 'macros/network_macros.lua.j2' import network_listen, http_config %}
-- network.do-ipv4/6
-net.ipv4 = {{ 'true' if cfg.network.do_ipv4 else 'false' }}
-net.ipv6 = {{ 'true' if cfg.network.do_ipv6 else 'false' }}
+net.ipv4 = {{ boolean(cfg.network.do_ipv4) }}
+net.ipv6 = {{ boolean(cfg.network.do_ipv6) }}
{% if cfg.network.out_interface_v4 %}
-- network.out-interface-v4
diff --git a/manager/knot_resolver_manager/datamodel/templates/options.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/options.lua.j2
index 4abe977b..8210fb6d 100644
--- a/manager/knot_resolver_manager/datamodel/templates/options.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/options.lua.j2
@@ -1,3 +1,5 @@
+{% from 'macros/common_macros.lua.j2' import boolean %}
+
-- options.glue-checking
mode('{{ cfg.options.glue_checking }}')
@@ -38,13 +40,13 @@ modules.unload('refuse_nord')
{% endif %}
-- options.qname-minimisation
-option('NO_MINIMIZE', {{ 'false' if cfg.options.qname_minimisation else 'true' }})
+option('NO_MINIMIZE', {{ boolean(cfg.options.minimize,true) }})
-- options.query-loopback
-option('ALLOW_LOCAL', {{ 'true' if cfg.options.query_loopback else 'false' }})
+option('ALLOW_LOCAL', {{ boolean(cfg.options.query_loopback) }})
-- options.reorder-rrset
-option('REORDER_RR', {{ 'true' if cfg.options.reorder_rrset else 'false' }})
+option('REORDER_RR', {{ boolean(cfg.options.reorder_rrset) }})
-- options.query-case-randomization
-option('NO_0X20', {{ 'false' if cfg.options.query_case_randomization else 'true' }}) \ No newline at end of file
+option('NO_0X20', {{ boolean(cfg.options.query_case_randomization,true) }}) \ No newline at end of file
diff --git a/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2
index 85483e6e..85290982 100644
--- a/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2
@@ -3,7 +3,7 @@
{% if cfg.stub_zones %}
{% for zone in cfg.stub_zones %}
--- stub-zone: {{ zone.name }}
+-- stub-zone: {{ zone.subtree }}
{% if zone.views -%}
{# views set for stub-zone #}
{% for view_id in zone.views -%}
@@ -23,10 +23,10 @@
{% for tsig in view.tsig -%}
{%- if options -%}
-{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }}
+{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
-{{ view_tsig(tsig|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ view_tsig(tsig|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.subtree|string))) }}
{% endfor %}
{%- endif -%}
@@ -35,10 +35,10 @@
{% for addr in view.subnets -%}
{%- if options -%}
-{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }}
+{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
-{{ view_addr(addr|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ view_addr(addr|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.subtree|string))) }}
{% endfor %}
{% endif %}
@@ -48,10 +48,10 @@
{# no views set for stub-zone #}
{% if zone.options -%}
-{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.name|string))) }}
+{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.subtree|string))) }}
{%- endif %}
-{{ policy_add(policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.name|string))) }}
+{{ policy_add(policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.subtree|string))) }}
{% endif %}
{% endfor %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/views.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/views.lua.j2
index fbe8617e..99c654c9 100644
--- a/manager/knot_resolver_manager/datamodel/templates/views.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/views.lua.j2
@@ -1,3 +1,22 @@
+{% from 'macros/common_macros.lua.j2' import quotes %}
+{% from 'macros/view_macros.lua.j2' import view_insert_action, view_flags, view_answer %}
+{% from 'macros/policy_macros.lua.j2' import policy_flags, policy_tags_assign %}
+
{% if cfg.views %}
-modules.load('view')
-{% endif %} \ No newline at end of file
+{% for view in cfg.views %}
+{% for subnet in view.subnets %}
+
+{% if view.tags -%}
+{{ view_insert_action(subnet, policy_tags_assign(view.tags)) }}
+{% elif view.answer %}
+{{ view_insert_action(subnet, view_answer(view.answer)) }}
+{%- endif %}
+
+{%- set flags = view_flags(view.options) -%}
+{% if flags -%}
+{{ view_insert_action(subnet, quotes(policy_flags(flags))) }}
+{%- endif %}
+
+{% endfor %}
+{% endfor %}
+{% endif %}
diff --git a/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2
index 1dd0098c..938ea8da 100644
--- a/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2
+++ b/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2
@@ -1,7 +1,9 @@
+{% from 'macros/common_macros.lua.j2' import boolean %}
+
{% if cfg.webmgmt -%}
-- webmgmt
modules.load('http')
-http.config({tls = {{ 'true' if cfg.webmgmt.tls else 'false'}},
+http.config({tls = {{ boolean(cfg.webmgmt.tls) }},
{%- if cfg.webmgmt.cert_file -%}
cert = '{{ cfg.webmgmt.cert_file }}',
{%- endif -%}
diff --git a/manager/knot_resolver_manager/datamodel/types/__init__.py b/manager/knot_resolver_manager/datamodel/types/__init__.py
index bdd22c82..33d8c90d 100644
--- a/manager/knot_resolver_manager/datamodel/types/__init__.py
+++ b/manager/knot_resolver_manager/datamodel/types/__init__.py
@@ -1,7 +1,9 @@
from .enums import DNSRecordTypeEnum, PolicyActionEnum, PolicyFlagEnum
from .files import AbsoluteDir, Dir, File, FilePath
+from .generic_types import ListOrItem
from .types import (
DomainName,
+ IDPattern,
Int0_512,
Int0_65535,
InterfaceName,
@@ -16,6 +18,7 @@ from .types import (
IPv4Address,
IPv6Address,
IPv6Network96,
+ Percent,
PortNumber,
SizeUnit,
TimeUnit,
@@ -26,6 +29,7 @@ __all__ = [
"PolicyFlagEnum",
"DNSRecordTypeEnum",
"DomainName",
+ "IDPattern",
"Int0_512",
"Int0_65535",
"InterfaceName",
@@ -40,6 +44,8 @@ __all__ = [
"IPv4Address",
"IPv6Address",
"IPv6Network96",
+ "ListOrItem",
+ "Percent",
"PortNumber",
"SizeUnit",
"TimeUnit",
diff --git a/manager/knot_resolver_manager/datamodel/types/base_types.py b/manager/knot_resolver_manager/datamodel/types/base_types.py
index 9bf78402..96c0a393 100644
--- a/manager/knot_resolver_manager/datamodel/types/base_types.py
+++ b/manager/knot_resolver_manager/datamodel/types/base_types.py
@@ -155,6 +155,9 @@ class UnitBase(IntBase):
self._value = int(val) * type(self)._units[unit]
else:
raise ValueError(f"{type(self._value)} Failed to convert: {self}")
+ elif source_value in (0, "0"):
+ self._value_orig = source_value
+ self._value = int(source_value)
elif isinstance(source_value, int):
raise ValueError(
f"number without units, please convert to string and add unit - {list(type(self)._units.keys())}",
diff --git a/manager/knot_resolver_manager/datamodel/types/generic_types.py b/manager/knot_resolver_manager/datamodel/types/generic_types.py
new file mode 100644
index 00000000..bf4e8680
--- /dev/null
+++ b/manager/knot_resolver_manager/datamodel/types/generic_types.py
@@ -0,0 +1,33 @@
+from typing import Any, List, TypeVar, Union
+
+from knot_resolver_manager.utils.modeling import BaseGenericTypeWrapper
+
+T = TypeVar("T")
+
+
+class ListOrItem(BaseGenericTypeWrapper[Union[List[T], T]]):
+ _value_orig: Union[List[T], T]
+ _list: List[T]
+
+ def __init__(self, source_value: Any, object_path: str = "/") -> None: # pylint: disable=unused-argument
+ super().__init__(source_value)
+ self._value_orig: Union[List[T], T] = source_value
+ self._list: List[T] = source_value if isinstance(source_value, list) else [source_value]
+
+ def __getitem__(self, index: Any) -> T:
+ return self._list[index]
+
+ def __int__(self) -> int:
+ raise ValueError(f"Can't convert '{type(self).__name__}' to an integer.")
+
+ def __str__(self) -> str:
+ return str(self._value_orig)
+
+ def to_std(self) -> List[T]:
+ return self._list
+
+ def __eq__(self, o: object) -> bool:
+ return isinstance(o, ListOrItem) and o._value_orig == self._value_orig
+
+ def serialize(self) -> Union[List[T], T]:
+ return self._value_orig
diff --git a/manager/knot_resolver_manager/datamodel/types/types.py b/manager/knot_resolver_manager/datamodel/types/types.py
index 08623535..f38759c8 100644
--- a/manager/knot_resolver_manager/datamodel/types/types.py
+++ b/manager/knot_resolver_manager/datamodel/types/types.py
@@ -24,6 +24,11 @@ class Int0_65535(IntRangeBase):
_max: int = 65_535
+class Percent(IntRangeBase):
+ _min: int = 0
+ _max: int = 100
+
+
class PortNumber(IntRangeBase):
_min: int = 1
_max: int = 65_535
@@ -42,14 +47,20 @@ class SizeUnit(UnitBase):
def bytes(self) -> int:
return self._value
+ def mbytes(self) -> int:
+ return self._value // 1024**2
+
class TimeUnit(UnitBase):
- _units = {"ms": 1, "s": 1000, "m": 60 * 1000, "h": 3600 * 1000, "d": 24 * 3600 * 1000}
+ _units = {"us": 1, "ms": 10**3, "s": 10**6, "m": 60 * 10**6, "h": 3600 * 10**6, "d": 24 * 3600 * 10**6}
def seconds(self) -> int:
- return self._value // 1000
+ return self._value // 1000**2
def millis(self) -> int:
+ return self._value // 1000
+
+ def micros(self) -> int:
return self._value
diff --git a/manager/knot_resolver_manager/datamodel/view_schema.py b/manager/knot_resolver_manager/datamodel/view_schema.py
index f84ab428..74bf5a11 100644
--- a/manager/knot_resolver_manager/datamodel/view_schema.py
+++ b/manager/knot_resolver_manager/datamodel/view_schema.py
@@ -1,23 +1,40 @@
from typing import List, Optional
-from knot_resolver_manager.datamodel.types import IPNetwork, PolicyFlagEnum
+from typing_extensions import Literal
+
+from knot_resolver_manager.datamodel.types import IDPattern, IPNetwork
from knot_resolver_manager.utils.modeling import ConfigSchema
+class ViewOptionsSchema(ConfigSchema):
+ """
+ Configuration options for clients identified by the view.
+
+ ---
+ minimize: Send minimum amount of information in recursive queries to enhance privacy.
+ dns64: Enable/disable DNS64.
+ """
+
+ minimize: bool = True
+ dns64: bool = True
+
+
class ViewSchema(ConfigSchema):
"""
Configuration parameters that allow you to create personalized policy rules and other.
---
subnets: Identifies the client based on his subnet.
- tsig: Identifies the client based on a TSIG key name (for testing purposes, TSIG signature is not verified!).
- options: Configuration flags for clients identified by the view.
+ tags: Tags to link with other policy rules.
+ answer: Direct approach how to handle request from clients identified by the view.
+ options: Configuration options for clients identified by the view.
"""
- subnets: Optional[List[IPNetwork]] = None
- tsig: Optional[List[str]] = None
- options: Optional[List[PolicyFlagEnum]] = None
+ subnets: List[IPNetwork]
+ tags: Optional[List[IDPattern]] = None
+ answer: Optional[Literal["allow", "refused", "noanswer"]] = None
+ options: ViewOptionsSchema = ViewOptionsSchema()
def _validate(self) -> None:
- if self.tsig is None and self.subnets is None:
- raise ValueError("'subnets' or 'rsig' must be configured")
+ if bool(self.tags) == bool(self.answer):
+ raise ValueError("only one of 'tags' and 'answer' options must be configured")
diff --git a/manager/knot_resolver_manager/kres_manager.py b/manager/knot_resolver_manager/kres_manager.py
index 36cddc13..072c73fc 100644
--- a/manager/knot_resolver_manager/kres_manager.py
+++ b/manager/knot_resolver_manager/kres_manager.py
@@ -193,7 +193,7 @@ class KresManager: # pylint: disable=too-many-instance-attributes
await self._rolling_restart(config)
await self._ensure_number_of_children(config, int(config.workers))
- if self._is_gc_running() != config.cache.garbage_collector:
+ if self._is_gc_running() != bool(config.cache.garbage_collector):
if config.cache.garbage_collector:
logger.debug("Starting cache GC")
await self._start_gc(config)
diff --git a/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py b/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py
index 758a9da5..08450739 100644
--- a/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py
+++ b/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py
@@ -48,6 +48,30 @@ class SupervisordKresID(KresID):
raise RuntimeError(f"Unexpected subprocess type {self.subprocess_type}")
+def kres_cache_gc_args(config: KresConfig) -> str:
+ args = ""
+
+ if config.logging.level == "debug" or (config.logging.groups and "cache-gc" in config.logging.groups):
+ args += " -v"
+
+ gc_config = config.cache.garbage_collector
+ if gc_config:
+ args += (
+ f" -d {gc_config.interval.millis()}"
+ f" -u {gc_config.threshold}"
+ f" -f {gc_config.release}"
+ f" -l {gc_config.rw_deletes}"
+ f" -L {gc_config.rw_reads}"
+ f" -t {gc_config.temp_keys_space.mbytes()}"
+ f" -m {gc_config.rw_duration.micros()}"
+ f" -w {gc_config.rw_delay.micros()}"
+ )
+ if gc_config.dry_run:
+ args += " -n"
+ return args
+ raise ValueError("missing configuration for the cache garbage collector")
+
+
@dataclass
class ProcessTypeConfig:
"""
@@ -66,7 +90,7 @@ class ProcessTypeConfig:
return ProcessTypeConfig( # type: ignore[call-arg]
logfile=supervisord_subprocess_log_dir(config) / "gc.log",
workdir=cwd,
- command=f"{kres_gc_executable()} -c {kresd_cache_dir(config)} -d 1000",
+ command=f"{kres_gc_executable()} -c {kresd_cache_dir(config)}{kres_cache_gc_args(config)}",
environment="",
)
@@ -152,6 +176,7 @@ async def write_config_file(config: KresConfig) -> None:
manager=ProcessTypeConfig.create_manager_config(config),
config=SupervisordConfig.create(config),
)
+ print(config_string)
await writefile(supervisord_config_file_tmp(config), config_string)
# atomically replace (we don't technically need this right now, but better safe then sorry)
os.rename(supervisord_config_file_tmp(config), supervisord_config_file(config))
diff --git a/manager/knot_resolver_manager/utils/modeling/__init__.py b/manager/knot_resolver_manager/utils/modeling/__init__.py
index c72c60c7..d16f6c12 100644
--- a/manager/knot_resolver_manager/utils/modeling/__init__.py
+++ b/manager/knot_resolver_manager/utils/modeling/__init__.py
@@ -1,8 +1,10 @@
+from .base_generic_type_wrapper import BaseGenericTypeWrapper
from .base_schema import BaseSchema, ConfigSchema
from .base_value_type import BaseValueType
from .parsing import parse_json, parse_yaml, try_to_parse
__all__ = [
+ "BaseGenericTypeWrapper",
"BaseValueType",
"BaseSchema",
"ConfigSchema",
diff --git a/manager/knot_resolver_manager/utils/modeling/base_generic_type_wrapper.py b/manager/knot_resolver_manager/utils/modeling/base_generic_type_wrapper.py
new file mode 100644
index 00000000..1f2c1767
--- /dev/null
+++ b/manager/knot_resolver_manager/utils/modeling/base_generic_type_wrapper.py
@@ -0,0 +1,9 @@
+from typing import Generic, TypeVar
+
+from .base_value_type import BaseTypeABC
+
+T = TypeVar("T")
+
+
+class BaseGenericTypeWrapper(Generic[T], BaseTypeABC): # pylint: disable=abstract-method
+ pass
diff --git a/manager/knot_resolver_manager/utils/modeling/base_schema.py b/manager/knot_resolver_manager/utils/modeling/base_schema.py
index 31cea7cc..32388816 100644
--- a/manager/knot_resolver_manager/utils/modeling/base_schema.py
+++ b/manager/knot_resolver_manager/utils/modeling/base_schema.py
@@ -7,15 +7,18 @@ import yaml
from knot_resolver_manager.utils.functional import all_matches
+from .base_generic_type_wrapper import BaseGenericTypeWrapper
from .base_value_type import BaseValueType
from .exceptions import AggregateDataValidationError, DataDescriptionError, DataValidationError
from .renaming import Renamed, renamed
from .types import (
get_generic_type_argument,
get_generic_type_arguments,
+ get_generic_type_wrapper_argument,
get_optional_inner_type,
is_dict,
is_enum,
+ is_generic_type_wrapper,
is_internal_field_name,
is_list,
is_literal,
@@ -54,6 +57,7 @@ class Serializable(ABC):
or is_literal(typ)
or is_dict(typ)
or is_list(typ)
+ or is_generic_type_wrapper(typ)
or (inspect.isclass(typ) and issubclass(typ, Serializable))
or (inspect.isclass(typ) and issubclass(typ, BaseValueType))
or (inspect.isclass(typ) and issubclass(typ, BaseSchema))
@@ -66,8 +70,11 @@ class Serializable(ABC):
if isinstance(obj, Serializable):
return obj.to_dict()
- elif isinstance(obj, BaseValueType):
- return obj.serialize()
+ elif isinstance(obj, (BaseValueType, BaseGenericTypeWrapper)):
+ o = obj.serialize()
+ # if Serializable.is_serializable(o):
+ return Serializable.serialize(o)
+ # return o
elif isinstance(obj, list):
res: List[Any] = [Serializable.serialize(i) for i in cast(List[Any], obj)]
@@ -171,6 +178,10 @@ def _describe_type(typ: Type[Any]) -> Dict[Any, Any]:
elif inspect.isclass(typ) and issubclass(typ, BaseValueType):
return typ.json_schema()
+ elif is_generic_type_wrapper(typ):
+ wrapped = get_generic_type_wrapper_argument(typ)
+ return _describe_type(wrapped)
+
elif is_none_type(typ):
return {"type": "null"}
@@ -279,11 +290,15 @@ class ObjectMapper:
inner_type = get_generic_type_argument(tp)
errs: List[DataValidationError] = []
res: List[Any] = []
- for i, val in enumerate(obj):
- try:
+
+ try:
+ for i, val in enumerate(obj):
res.append(self.map_object(inner_type, val, object_path=f"{object_path}[{i}]"))
- except DataValidationError as e:
- errs.append(e)
+ except DataValidationError as e:
+ errs.append(e)
+ except TypeError as e:
+ errs.append(DataValidationError(str(e), object_path))
+
if len(errs) == 1:
raise errs[0]
elif len(errs) > 1:
@@ -465,6 +480,12 @@ class ObjectMapper:
elif inspect.isclass(tp) and issubclass(tp, BaseValueType):
return self.create_value_type_object(tp, obj, object_path)
+ # BaseGenericTypeWrapper subclasses
+ elif is_generic_type_wrapper(tp):
+ inner_type = get_generic_type_wrapper_argument(tp)
+ obj_valid = self.map_object(inner_type, obj, object_path)
+ return tp(obj_valid, object_path=object_path) # type: ignore
+
# nested BaseSchema subclasses
elif inspect.isclass(tp) and issubclass(tp, BaseSchema):
return self._create_base_schema_object(tp, obj, object_path)
diff --git a/manager/knot_resolver_manager/utils/modeling/base_value_type.py b/manager/knot_resolver_manager/utils/modeling/base_value_type.py
index 1b07e3d4..dff4a3fe 100644
--- a/manager/knot_resolver_manager/utils/modeling/base_value_type.py
+++ b/manager/knot_resolver_manager/utils/modeling/base_value_type.py
@@ -2,18 +2,7 @@ from abc import ABC, abstractmethod # pylint: disable=[no-name-in-module]
from typing import Any, Dict, Type
-class BaseValueType(ABC):
- """
- Subclasses of this class can be used as type annotations in 'DataParser'. When a value
- is being parsed from a serialized format (e.g. JSON/YAML), an object will be created by
- calling the constructor of the appropriate type on the field value. The only limitation
- is that the value MUST NOT be `None`.
-
- There is no validation done on the wrapped value. The only condition is that
- it can't be `None`. If you want to perform any validation during creation,
- raise a `ValueError` in case of errors.
- """
-
+class BaseTypeABC(ABC):
@abstractmethod
def __init__(self, source_value: Any, object_path: str = "/") -> None:
pass
@@ -37,6 +26,19 @@ class BaseValueType(ABC):
"""
raise NotImplementedError(f"{type(self).__name__}'s' 'serialize()' not implemented.")
+
+class BaseValueType(BaseTypeABC):
+ """
+ Subclasses of this class can be used as type annotations in 'DataParser'. When a value
+ is being parsed from a serialized format (e.g. JSON/YAML), an object will be created by
+ calling the constructor of the appropriate type on the field value. The only limitation
+ is that the value MUST NOT be `None`.
+
+ There is no validation done on the wrapped value. The only condition is that
+ it can't be `None`. If you want to perform any validation during creation,
+ raise a `ValueError` in case of errors.
+ """
+
@classmethod
@abstractmethod
def json_schema(cls: Type["BaseValueType"]) -> Dict[Any, Any]:
diff --git a/manager/knot_resolver_manager/utils/modeling/types.py b/manager/knot_resolver_manager/utils/modeling/types.py
index aaeded9e..4ce9aecc 100644
--- a/manager/knot_resolver_manager/utils/modeling/types.py
+++ b/manager/knot_resolver_manager/utils/modeling/types.py
@@ -8,6 +8,8 @@ from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
from typing_extensions import Literal
+from .base_generic_type_wrapper import BaseGenericTypeWrapper
+
NoneType = type(None)
@@ -46,6 +48,11 @@ def is_literal(tp: Any) -> bool:
return getattr(tp, "__origin__", None) == Literal
+def is_generic_type_wrapper(tp: Any) -> bool:
+ orig = getattr(tp, "__origin__", None)
+ return inspect.isclass(orig) and issubclass(orig, BaseGenericTypeWrapper)
+
+
def get_generic_type_arguments(tp: Any) -> List[Any]:
default: List[Any] = []
if sys.version_info.minor == 6 and is_literal(tp):
@@ -62,6 +69,17 @@ def get_generic_type_argument(tp: Any) -> Any:
return args[0]
+def get_generic_type_wrapper_argument(tp: Type["BaseGenericTypeWrapper[Any]"]) -> Any:
+ assert hasattr(tp, "__origin__")
+ origin = getattr(tp, "__origin__")
+
+ assert hasattr(origin, "__orig_bases__")
+ orig_base: List[Any] = getattr(origin, "__orig_bases__", [])[0]
+
+ arg = get_generic_type_argument(tp)
+ return get_generic_type_argument(orig_base[arg])
+
+
def is_none_type(tp: Any) -> bool:
return tp is None or tp == NoneType
diff --git a/manager/knot_resolver_manager/utils/requests.py b/manager/knot_resolver_manager/utils/requests.py
index e406ab3b..ab95c5d2 100644
--- a/manager/knot_resolver_manager/utils/requests.py
+++ b/manager/knot_resolver_manager/utils/requests.py
@@ -1,6 +1,6 @@
import socket
-from http.client import HTTPConnection
import sys
+from http.client import HTTPConnection
from typing import Any, Optional, Union
from urllib.error import HTTPError, URLError
from urllib.request import AbstractHTTPHandler, Request, build_opener, install_opener, urlopen
diff --git a/manager/poetry.lock b/manager/poetry.lock
index 08dea305..364a19eb 100644
--- a/manager/poetry.lock
+++ b/manager/poetry.lock
@@ -2933,4 +2933,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.7"
-content-hash = "16ce34509fd2c4b4bedee9c8fc289f077bbe3057e95e7a2736ce71d994499444"
+content-hash = "c79ae903180b91d16637ed8d463843473edcae8d0ff2fbd89921b34922a56ba8"
diff --git a/manager/pyproject.toml b/manager/pyproject.toml
index 4d1f2988..7aec2a29 100644
--- a/manager/pyproject.toml
+++ b/manager/pyproject.toml
@@ -53,6 +53,7 @@ knot-resolver = 'knot_resolver_manager.__main__:run'
[tool.poe.tasks]
run = { cmd = "scripts/run", help = "Run the manager" }
+run-new-policy = { cmd = "scripts/run-new-policy", help = "Run the manager with 'new-policy' kresd" }
run-debug = { cmd = "scripts/run-debug", help = "Run the manager under debugger" }
docs = { cmd = "scripts/docs", help = "Create HTML documentation" }
test = { shell = "env PYTHONPATH=. pytest --junitxml=unit.junit.xml --cov=knot_resolver_manager --show-capture=all tests/unit/", help = "Run tests" }
@@ -173,5 +174,8 @@ implicit_reexport = false
no_implicit_optional = true
[build-system]
-requires = ["poetry-core>=1.0.0"]
+requires = [
+ "poetry-core>=1.0.0",
+ "setuptools>=67.8.0"
+]
build-backend = "poetry.core.masonry.api"
diff --git a/manager/scripts/_env.sh b/manager/scripts/_env.sh
index b1941edf..52697ca0 100644
--- a/manager/scripts/_env.sh
+++ b/manager/scripts/_env.sh
@@ -50,3 +50,36 @@ function build_kresd {
export PATH="$(realpath manager/.install_kresd)/sbin:$PATH"
popd
}
+
+function build_kresd_new_policy {
+ echo
+ echo Building Knot Resolver
+ echo ----------------------
+ echo -e "${blue}In case of an compilation error, run this command to try to fix it:${reset}"
+ echo -e "\t${blue}rm -r $(realpath .install_kresd) $(realpath .build_kresd)${reset}"
+ echo
+
+
+ pushd ..
+ rm -rf manager/.install_kresd manager/.build_kresd
+ mkdir -p manager/.build_kresd manager/.install_kresd
+
+ if [ -d "kres-new-policy" ]
+ then
+ echo updating repository...
+ cd kres-new-policy
+ git pull
+ else
+ echo cloning repository...
+ git clone -b new-policy https://gitlab.nic.cz/knot/knot-resolver.git kres-new-policy
+ cd kres-new-policy
+ git submodule update --init --recursive
+ fi
+
+ meson setup ../manager/.build_kresd --prefix=$(realpath ../manager/.install_kresd) --default-library=static --buildtype=debug
+ ninja -C ../manager/.build_kresd
+ ninja install -C ../manager/.build_kresd
+ cd ..
+ export PATH="$(realpath manager/.install_kresd)/sbin:$PATH"
+ popd
+} \ No newline at end of file
diff --git a/manager/scripts/run-new-policy b/manager/scripts/run-new-policy
new file mode 100755
index 00000000..771710c2
--- /dev/null
+++ b/manager/scripts/run-new-policy
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# ensure consistent behaviour
+src_dir="$(dirname "$(realpath "$0")")"
+source $src_dir/_env.sh
+
+build_kresd_new_policy
+
+echo
+echo Building Knot Resolver Manager native extensions
+echo ------------------------------------------------
+poetry build
+# copy native modules from build directory to source directory
+shopt -s globstar
+shopt -s nullglob
+for d in build/lib*; do
+ for f in "$d/"**/*.so; do
+ cp -v "$f" ${f#"$d/"}
+ done
+done
+shopt -u globstar
+shopt -u nullglob
+
+
+echo
+echo Knot Manager API is accessible on http://localhost:5000
+echo -------------------------------------------------------
+
+python3 -m knot_resolver_manager -c etc/knot-resolver/config.policy.dev.yml $@
diff --git a/manager/tests/unit/datamodel/templates/test_common_macros.py b/manager/tests/unit/datamodel/templates/test_common_macros.py
index 6cb24050..d730fb9d 100644
--- a/manager/tests/unit/datamodel/templates/test_common_macros.py
+++ b/manager/tests/unit/datamodel/templates/test_common_macros.py
@@ -1,5 +1,23 @@
from knot_resolver_manager.datamodel.config_schema import template_from_str
-from knot_resolver_manager.datamodel.forward_zone_schema import ForwardServerSchema
+from knot_resolver_manager.datamodel.forward_schema import ForwardServerSchema
+
+
+def test_boolean():
+ tmpl_str = """{% from 'macros/common_macros.lua.j2' import boolean %}
+{{ boolean(x) }}"""
+
+ tmpl = template_from_str(tmpl_str)
+ assert tmpl.render(x=True) == "true"
+ assert tmpl.render(x=False) == "false"
+
+
+def test_boolean_neg():
+ tmpl_str = """{% from 'macros/common_macros.lua.j2' import boolean %}
+{{ boolean(x,true) }}"""
+
+ tmpl = template_from_str(tmpl_str)
+ assert tmpl.render(x=True) == "false"
+ assert tmpl.render(x=False) == "true"
def test_string_table():
@@ -50,9 +68,9 @@ def test_servers_table():
def test_tls_servers_table():
d = ForwardServerSchema(
# the ca-file is a dummy, because it's existence is checked
- {"address": "2001:DB8::d0c", "hostname": "res.example.com", "ca-file": "/etc/passwd"}
+ {"address": ["2001:DB8::d0c"], "hostname": "res.example.com", "ca-file": "/etc/passwd"}
)
- t = [d, ForwardServerSchema({"address": "192.0.2.1", "pin-sha256": "YQ=="})]
+ t = [d, ForwardServerSchema({"address": ["192.0.2.1"], "pin-sha256": "YQ=="})]
tmpl_str = """{% from 'macros/common_macros.lua.j2' import tls_servers_table %}
{{ tls_servers_table(x) }}"""
@@ -60,5 +78,5 @@ def test_tls_servers_table():
assert tmpl.render(x=[d.address, t[1].address]) == f"{{'{d.address}','{t[1].address}',}}"
assert (
tmpl.render(x=t)
- == f"{{{{'{d.address}',hostname='{d.hostname}',ca_file='{d.ca_file}',}},{{'{t[1].address}',pin_sha256='{t[1].pin_sha256}',}},}}"
+ == f"{{{{'{d.address}',hostname='{d.hostname}',ca_file='{d.ca_file}',}},{{'{t[1].address}',pin_sha256={{'{t[1].pin_sha256}',}}}},}}"
)
diff --git a/manager/tests/unit/datamodel/templates/test_forward_macros.py b/manager/tests/unit/datamodel/templates/test_forward_macros.py
new file mode 100644
index 00000000..5f80df15
--- /dev/null
+++ b/manager/tests/unit/datamodel/templates/test_forward_macros.py
@@ -0,0 +1,27 @@
+from knot_resolver_manager.datamodel.config_schema import template_from_str
+from knot_resolver_manager.datamodel.forward_schema import ForwardSchema
+from knot_resolver_manager.datamodel.types import IPAddressOptionalPort
+
+
+def test_policy_rule_forward_add():
+ tmpl_str = """{% from 'macros/forward_macros.lua.j2' import policy_rule_forward_add %}
+{{ policy_rule_forward_add(rule) }}"""
+
+ rule = ForwardSchema(
+ {
+ "subtree": ".",
+ "servers": [{"address": ["2001:148f:fffe::1", "185.43.135.1"], "hostname": "odvr.nic.cz"}],
+ "options": {
+ "authoritative": False,
+ "dnssec": True,
+ },
+ }
+ )
+ result = "policy.rule_forward_add('.',{dnssec=true,auth=false},{{'2001:148f:fffe::1',tls=false,hostname='odvr.nic.cz',},{'185.43.135.1',tls=false,hostname='odvr.nic.cz',},})"
+
+ tmpl = template_from_str(tmpl_str)
+ assert tmpl.render(rule=rule) == result
+
+ rule.servers = [IPAddressOptionalPort("2001:148f:fffe::1"), IPAddressOptionalPort("185.43.135.1")]
+ result = "policy.rule_forward_add('.',{dnssec=true,auth=false},{{'2001:148f:fffe::1'},{'185.43.135.1'},})"
+ assert tmpl.render(rule=rule) == result
diff --git a/manager/tests/unit/datamodel/templates/test_network_macros.py b/manager/tests/unit/datamodel/templates/test_network_macros.py
index 6d6637ed..ad193d98 100644
--- a/manager/tests/unit/datamodel/templates/test_network_macros.py
+++ b/manager/tests/unit/datamodel/templates/test_network_macros.py
@@ -8,8 +8,8 @@ def test_network_listen():
tmpl = template_from_str(tmpl_str)
soc = ListenSchema({"unix-socket": "/tmp/kresd-socket", "kind": "dot"})
- assert tmpl.render(listen=soc) == "net.listen('/tmp/kresd-socket',nil,{kind='tls',freebind=false})"
- soc_list = ListenSchema({"unix-socket": [soc.unix_socket, "/tmp/kresd-socket2"], "kind": "dot"})
+ assert tmpl.render(listen=soc) == "net.listen('/tmp/kresd-socket',nil,{kind='tls',freebind=false})\n"
+ soc_list = ListenSchema({"unix-socket": [soc.unix_socket.to_std()[0], "/tmp/kresd-socket2"], "kind": "dot"})
assert (
tmpl.render(listen=soc_list)
== "net.listen('/tmp/kresd-socket',nil,{kind='tls',freebind=false})\n"
@@ -17,8 +17,8 @@ def test_network_listen():
)
ip = ListenSchema({"interface": "::1@55", "freebind": True})
- assert tmpl.render(listen=ip) == "net.listen('::1',55,{kind='dns',freebind=true})"
- ip_list = ListenSchema({"interface": [ip.interface, "127.0.0.1@5353"]})
+ assert tmpl.render(listen=ip) == "net.listen('::1',55,{kind='dns',freebind=true})\n"
+ ip_list = ListenSchema({"interface": [ip.interface.to_std()[0], "127.0.0.1@5353"]})
assert (
tmpl.render(listen=ip_list)
== "net.listen('::1',55,{kind='dns',freebind=false})\n"
@@ -26,8 +26,8 @@ def test_network_listen():
)
intrfc = ListenSchema({"interface": "eth0", "kind": "doh2"})
- assert tmpl.render(listen=intrfc) == "net.listen(net.eth0,443,{kind='doh2',freebind=false})"
- intrfc_list = ListenSchema({"interface": [intrfc.interface, "lo"], "port": 5555, "kind": "doh2"})
+ assert tmpl.render(listen=intrfc) == "net.listen(net.eth0,443,{kind='doh2',freebind=false})\n"
+ intrfc_list = ListenSchema({"interface": [intrfc.interface.to_std()[0], "lo"], "port": 5555, "kind": "doh2"})
assert (
tmpl.render(listen=intrfc_list)
== "net.listen(net.eth0,5555,{kind='doh2',freebind=false})\n"
diff --git a/manager/tests/unit/datamodel/templates/test_policy_macros.py b/manager/tests/unit/datamodel/templates/test_policy_macros.py
index 28f8d5aa..2920a206 100644
--- a/manager/tests/unit/datamodel/templates/test_policy_macros.py
+++ b/manager/tests/unit/datamodel/templates/test_policy_macros.py
@@ -16,16 +16,24 @@ def test_policy_add():
assert tmpl.render(rule=rule, postrule=True) == f"policy.add({rule},true)"
-def test_policy_flags():
- flags: List[PolicyFlagEnum] = ["no-cache", "no-edns"]
- tmpl_str = """{% from 'macros/policy_macros.lua.j2' import policy_flags %}
-{{ policy_flags(flags) }}"""
+def test_policy_tags_assign():
+ tags: List[str] = ["t01", "t02", "t03"]
+ tmpl_str = """{% from 'macros/policy_macros.lua.j2' import policy_tags_assign %}
+{{ policy_tags_assign(tags) }}"""
tmpl = template_from_str(tmpl_str)
- assert tmpl.render(flags=flags[1]) == f"policy.FLAGS({{'{flags[1].upper().replace('-', '_')}'}})"
- assert (
- tmpl.render(flags=flags) == f"policy.FLAGS({{{str(flags).upper().replace('-', '_').replace(' ', '')[1:-1]},}})"
- )
+ assert tmpl.render(tags=tags[1]) == f"policy.TAGS_ASSIGN('{tags[1]}')"
+ assert tmpl.render(tags=tags) == "policy.TAGS_ASSIGN({" + ",".join([f"'{x}'" for x in tags]) + ",})"
+
+
+def test_policy_get_tagset():
+ tags: List[str] = ["t01", "t02", "t03"]
+ tmpl_str = """{% from 'macros/policy_macros.lua.j2' import policy_get_tagset %}
+{{ policy_get_tagset(tags) }}"""
+
+ tmpl = template_from_str(tmpl_str)
+ assert tmpl.render(tags=tags[1]) == f"policy.get_tagset('{tags[1]}')"
+ assert tmpl.render(tags=tags) == "policy.get_tagset({" + ",".join([f"'{x}'" for x in tags]) + ",})"
# Filters
diff --git a/manager/tests/unit/datamodel/templates/test_view_macros.py b/manager/tests/unit/datamodel/templates/test_view_macros.py
index 32d881e2..3a3f35f9 100644
--- a/manager/tests/unit/datamodel/templates/test_view_macros.py
+++ b/manager/tests/unit/datamodel/templates/test_view_macros.py
@@ -1,22 +1,53 @@
+from typing import Any
+
+import pytest
+
from knot_resolver_manager.datamodel.config_schema import template_from_str
-from knot_resolver_manager.datamodel.types import IPAddressOptionalPort
+from knot_resolver_manager.datamodel.view_schema import ViewOptionsSchema, ViewSchema
+
+
+def test_view_insert_action():
+ subnet = "10.0.0.0/8"
+ action = "policy.DENY"
+ tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_insert_action %}
+{{ view_insert_action(subnet, action) }}"""
+
+ tmpl = template_from_str(tmpl_str)
+ assert tmpl.render(subnet=subnet, action=action) == f"assert(C.kr_view_insert_action('{ subnet }',{ action })==0)"
+
+
+def test_view_flags():
+ tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_flags %}
+{{ view_flags(options) }}"""
+
+ tmpl = template_from_str(tmpl_str)
+ options = ViewOptionsSchema({"dns64": False, "minimize": False})
+ assert tmpl.render(options=options) == '"NO_MINIMIZE","DNS64_DISABLE",'
+ assert tmpl.render(options=ViewOptionsSchema()) == ""
-def test_view_tsig():
- tsig: str = r"\5mykey"
- rule = "policy.all(policy.DENY)"
- tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_tsig %}
-{{ view_tsig(tsig, rule) }}"""
+def test_view_answer():
+ tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_options_flags %}
+{{ view_options_flags(options) }}"""
tmpl = template_from_str(tmpl_str)
- assert tmpl.render(tsig=tsig, rule=rule) == f"view:tsig('{tsig}',{rule})"
+ options = ViewOptionsSchema({"dns64": False, "minimize": False})
+ assert tmpl.render(options=options) == "policy.FLAGS({'NO_MINIMIZE','DNS64_DISABLE',})"
+ assert tmpl.render(options=ViewOptionsSchema()) == "policy.FLAGS({})"
-def test_view_addr():
- addr: IPAddressOptionalPort = IPAddressOptionalPort("10.0.0.1")
- rule = "policy.all(policy.DENY)"
- tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_addr %}
-{{ view_addr(addr, rule) }}"""
+@pytest.mark.parametrize(
+ "val,res",
+ [
+ ("allow", "policy.TAGS_ASSIGN({})"),
+ ("refused", "'policy.REFUSE'"),
+ ("noanswer", "'policy.NO_ANSWER'"),
+ ],
+)
+def test_view_answer(val: Any, res: Any):
+ tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_answer %}
+{{ view_answer(view.answer) }}"""
tmpl = template_from_str(tmpl_str)
- assert tmpl.render(addr=addr, rule=rule) == f"view:addr('{addr}',{rule})"
+ view = ViewSchema({"subnets": ["10.0.0.0/8"], "answer": val})
+ assert tmpl.render(view=view) == res
diff --git a/manager/tests/unit/datamodel/test_config_schema.py b/manager/tests/unit/datamodel/test_config_schema.py
index 50233473..31703b96 100644
--- a/manager/tests/unit/datamodel/test_config_schema.py
+++ b/manager/tests/unit/datamodel/test_config_schema.py
@@ -49,6 +49,6 @@ def test_config_json_schema():
try:
_ = json.dumps(obj)
except BaseException as e:
- raise Exception(f"failed to serialize '{path}'") from e
+ raise Exception(f"failed to serialize '{path}': {e}") from e
recser(dct)
diff --git a/manager/tests/unit/datamodel/test_local_data.py b/manager/tests/unit/datamodel/test_local_data.py
new file mode 100644
index 00000000..198bccd2
--- /dev/null
+++ b/manager/tests/unit/datamodel/test_local_data.py
@@ -0,0 +1,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.utils.modeling.exceptions import DataValidationError
+
+
+@pytest.mark.parametrize(
+ "val",
+ [
+ {"type": "empty", "roots": ["sub2.example.org"]},
+ {"type": "empty", "roots-url": "https://example.org/blocklist.txt", "refresh": "1d"},
+ {"type": "nxdomain", "roots-file": "/path/to/file.txt"},
+ {"type": "redirect", "roots": ["sub4.example.org"], "addresses": ["127.0.0.1", "::1"]},
+ ],
+)
+def test_subtree_valid(val: Any):
+ SubtreeSchema(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"},
+ ],
+)
+def test_subtree_invalid(val: Any):
+ with raises(DataValidationError):
+ SubtreeSchema(val)
diff --git a/manager/tests/unit/datamodel/test_network_schema.py b/manager/tests/unit/datamodel/test_network_schema.py
index 1a398b50..7b616f34 100644
--- a/manager/tests/unit/datamodel/test_network_schema.py
+++ b/manager/tests/unit/datamodel/test_network_schema.py
@@ -13,12 +13,12 @@ def test_listen_defaults():
assert len(o.listen) == 2
# {"ip-address": "127.0.0.1"}
- assert o.listen[0].interface == InterfaceOptionalPort("127.0.0.1")
+ assert o.listen[0].interface.to_std() == [InterfaceOptionalPort("127.0.0.1")]
assert o.listen[0].port == PortNumber(53)
assert o.listen[0].kind == "dns"
assert o.listen[0].freebind == False
# {"ip-address": "::1", "freebind": True}
- assert o.listen[1].interface == InterfaceOptionalPort("::1")
+ assert o.listen[1].interface.to_std() == [InterfaceOptionalPort("::1")]
assert o.listen[1].port == PortNumber(53)
assert o.listen[1].kind == "dns"
assert o.listen[1].freebind == True
@@ -27,11 +27,11 @@ def test_listen_defaults():
@pytest.mark.parametrize(
"listen,port",
[
- ({"unix-socket": "/tmp/kresd-socket"}, None),
- ({"interface": "::1"}, 53),
- ({"interface": "::1", "kind": "dot"}, 853),
- ({"interface": "::1", "kind": "doh-legacy"}, 443),
- ({"interface": "::1", "kind": "doh2"}, 443),
+ ({"unix-socket": ["/tmp/kresd-socket"]}, None),
+ ({"interface": ["::1"]}, 53),
+ ({"interface": ["::1"], "kind": "dot"}, 853),
+ ({"interface": ["::1"], "kind": "doh-legacy"}, 443),
+ ({"interface": ["::1"], "kind": "doh2"}, 443),
],
)
def test_listen_port_defaults(listen: Dict[str, Any], port: Optional[int]):
@@ -64,8 +64,8 @@ def test_listen_valid(listen: Dict[str, Any]):
@pytest.mark.parametrize(
"listen",
[
- {"unit-socket": "/tmp/kresd-socket", "port": "53"},
- {"interface": "::1", "unit-socket": "/tmp/kresd-socket"},
+ {"unix-socket": "/tmp/kresd-socket", "port": "53"},
+ {"interface": "::1", "unix-socket": "/tmp/kresd-socket"},
{"interface": "::1@5353", "port": 5353},
{"interface": ["127.0.0.1", "::1@5353"]},
{"interface": ["127.0.0.1@5353", "::1@5353"], "port": 5353},
diff --git a/manager/tests/unit/datamodel/test_policy_schema.py b/manager/tests/unit/datamodel/test_policy_schema.py
index ac3761b4..aeb98a71 100644
--- a/manager/tests/unit/datamodel/test_policy_schema.py
+++ b/manager/tests/unit/datamodel/test_policy_schema.py
@@ -51,7 +51,7 @@ def test_action_invalid(val: Dict[str, Any]):
{"action": "mirror", "servers": ["192.0.2.1@5353", "2001:148f:ffff::1"]},
{"action": "forward", "servers": ["192.0.2.1@5353", "2001:148f:ffff::1"]},
{"action": "stub", "servers": ["192.0.2.1@5353", "2001:148f:ffff::1"]},
- {"action": "forward", "servers": [{"address": "127.0.0.1@5353"}]},
+ {"action": "forward", "servers": [{"address": ["127.0.0.1@5353"]}]},
],
)
def test_policy_valid(val: Dict[str, Any]):
@@ -68,7 +68,7 @@ def test_policy_valid(val: Dict[str, Any]):
{"action": "pass", "reroute": [{"source": "192.0.2.0/24", "destination": "127.0.0.0"}]},
{"action": "pass", "answer": {"rtype": "AAAA", "rdata": "::1"}},
{"action": "pass", "servers": ["127.0.0.1@5353"]},
- {"action": "mirror", "servers": [{"address": "127.0.0.1@5353"}]},
+ {"action": "mirror", "servers": [{"address": ["127.0.0.1@5353"]}]},
],
)
def test_policy_invalid(val: Dict[str, Any]):
diff --git a/manager/tests/unit/datamodel/types/test_custom_types.py b/manager/tests/unit/datamodel/types/test_custom_types.py
index 5fba82ee..b9d6f567 100644
--- a/manager/tests/unit/datamodel/types/test_custom_types.py
+++ b/manager/tests/unit/datamodel/types/test_custom_types.py
@@ -56,13 +56,14 @@ def test_size_unit_invalid(val: Any):
SizeUnit(val)
-@pytest.mark.parametrize("val", ["1d", "24h", "1440m", "86400s", "86400000ms"])
+@pytest.mark.parametrize("val", ["1d", "24h", "1440m", "86400s", "86400000ms", "86400000000us"])
def test_time_unit_valid(val: str):
o = TimeUnit(val)
- assert int(o) == 86400000
+ assert int(o) == 86400000000
assert str(o) == val
assert o.seconds() == 86400
assert o.millis() == 86400000
+ assert o.micros() == 86400000000
@pytest.mark.parametrize("val", ["-1", "-24h", "1440mm", 6575, -1440])
diff --git a/manager/tests/unit/datamodel/types/test_generic_types.py b/manager/tests/unit/datamodel/types/test_generic_types.py
new file mode 100644
index 00000000..7803ed00
--- /dev/null
+++ b/manager/tests/unit/datamodel/types/test_generic_types.py
@@ -0,0 +1,56 @@
+from typing import Any, List, Optional, Union
+
+import pytest
+from pytest import raises
+
+from knot_resolver_manager.datamodel.types import ListOrItem
+from knot_resolver_manager.utils.modeling import BaseSchema
+from knot_resolver_manager.utils.modeling.exceptions import DataValidationError
+from knot_resolver_manager.utils.modeling.types import get_generic_type_wrapper_argument
+
+
+@pytest.mark.parametrize("val", [str, int])
+def test_list_or_item_inner_type(val: Any):
+ assert get_generic_type_wrapper_argument(ListOrItem[val]) == Union[List[val], val]
+
+
+@pytest.mark.parametrize(
+ "typ,val",
+ [
+ (int, [1, 65_535, 5353, 5000]),
+ (int, 65_535),
+ (str, ["string1", "string2"]),
+ (str, "string1"),
+ ],
+)
+def test_list_or_item_valid(typ: Any, val: Any):
+ class ListOrItemSchema(BaseSchema):
+ test: ListOrItem[typ]
+
+ o = ListOrItemSchema({"test": val})
+ assert o.test.serialize() == val
+ assert o.test.to_std() == val if isinstance(val, list) else [val]
+
+ i = 0
+ for item in o.test:
+ assert item == val[i] if isinstance(val, list) else val
+ i += 1
+
+
+@pytest.mark.parametrize(
+ "typ,val",
+ [
+ (str, [True, False, True, False]),
+ (str, False),
+ (bool, [1, 65_535, 5353, 5000]),
+ (bool, 65_535),
+ (int, "string1"),
+ (int, ["string1", "string2"]),
+ ],
+)
+def test_list_or_item_invalid(typ: Any, val: Any):
+ class ListOrItemSchema(BaseSchema):
+ test: ListOrItem[typ]
+
+ with raises(DataValidationError):
+ ListOrItemSchema({"test": val})