diff options
author | Aleš <ales.mrazek@nic.cz> | 2022-01-17 18:53:19 +0100 |
---|---|---|
committer | Aleš Mrázek <ales.mrazek@nic.cz> | 2022-04-08 16:17:53 +0200 |
commit | 0b1a7286082d712998d8c586f86bc129407f6d38 (patch) | |
tree | fdd3536a1e56aacfdc0a6d9ffa277a1ba82c8c99 /manager/knot_resolver_manager | |
parent | Merge branch 'manager-systemd-improvements' into 'manager' (diff) | |
download | knot-resolver-0b1a7286082d712998d8c586f86bc129407f6d38.tar.xz knot-resolver-0b1a7286082d712998d8c586f86bc129407f6d38.zip |
datamodel: network: interfaces schema changed to new listen schema #703
- list of ip-address, interface and unix-socket can be used
- <ip-address/interface>@<port> syntax to specify the port number
- config files and unit tests updated
Diffstat (limited to 'manager/knot_resolver_manager')
4 files changed, 195 insertions, 57 deletions
diff --git a/manager/knot_resolver_manager/datamodel/network_schema.py b/manager/knot_resolver_manager/datamodel/network_schema.py index d905c1fc..2abb1ec5 100644 --- a/manager/knot_resolver_manager/datamodel/network_schema.py +++ b/manager/knot_resolver_manager/datamodel/network_schema.py @@ -1,42 +1,20 @@ -from typing import List, Optional +from typing import List, Optional, Union from typing_extensions import Literal from knot_resolver_manager.datamodel.types import ( CheckedPath, + InterfacePort, IPAddress, + IPAddressPort, IPNetwork, IPv4Address, IPv6Address, - Listen, SizeUnit, ) from knot_resolver_manager.utils import SchemaNode -KindEnum = Literal["dns", "xdp", "dot", "doh"] - - -class InterfaceSchema(SchemaNode): - class Raw(SchemaNode): - listen: Listen - kind: KindEnum = "dns" - freebind: bool = False - - _PREVIOUS_SCHEMA = Raw - - listen: Listen - kind: KindEnum - freebind: bool - - def _listen(self, origin: Raw) -> Listen: - if not origin.listen.port: - if origin.kind == "dot": - origin.listen.port = 853 - elif origin.kind == "doh": - origin.listen.port = 443 - else: - origin.listen.port = 53 - return origin.listen +KindEnum = Literal["dns", "xdp", "dot", "doh2"] class EdnsBufferSizeSchema(SchemaNode): @@ -64,6 +42,55 @@ class TLSSchema(SchemaNode): raise ValueError("'padding' must be number in range<0-512>") +class ListenSchema(SchemaNode): + class Raw(SchemaNode): + unix_socket: Union[None, CheckedPath, List[CheckedPath]] = None + ip_address: Union[None, IPAddressPort, List[IPAddressPort]] = None + interface: Union[None, InterfacePort, List[InterfacePort]] = None + port: Optional[int] = None + kind: KindEnum = "dns" + freebind: bool = False + + _PREVIOUS_SCHEMA = Raw + + unix_socket: Union[None, CheckedPath, List[CheckedPath]] + ip_address: Union[None, IPAddressPort, IPAddressPort, List[IPAddressPort]] + interface: Union[None, InterfacePort, List[InterfacePort]] + port: Optional[int] + kind: KindEnum + freebind: bool + + def _port(self, origin: Raw) -> Optional[int]: + if origin.port: + return origin.port + elif origin.ip_address or origin.interface: + if origin.kind == "dot": + return 853 + elif origin.kind == "doh2": + return 443 + return 53 + return None + + def _validate(self) -> None: + present = { + "ip_address" if self.ip_address is not None else ..., + "unix_socket" if self.unix_socket is not None else ..., + "interface" if self.interface is not None else ..., + } + if not (present == {"ip_address", ...} or present == {"unix_socket", ...} or present == {"interface", ...}): + raise ValueError( + "Listen configuration contains multiple incompatible options at once. " + "Only one of 'ip-address', 'interface' and 'unix-socket' optionscan be configured at once." + ) + if self.port and self.unix_socket: + raise ValueError( + "'unix-socket' and 'port' are not compatible options. " + "Port configuration can only be used with 'ip-address' or 'interface'." + ) + if self.port and not 0 <= self.port <= 65_535: + raise ValueError(f"Port value {self.port} out of range of usual 2-byte port value") + + class NetworkSchema(SchemaNode): do_ipv4: bool = True do_ipv6: bool = True @@ -74,9 +101,9 @@ class NetworkSchema(SchemaNode): edns_buffer_size: EdnsBufferSizeSchema = EdnsBufferSizeSchema() address_renumbering: Optional[List[AddressRenumberingSchema]] = None tls: TLSSchema = TLSSchema() - interfaces: List[InterfaceSchema] = [ - InterfaceSchema({"listen": {"ip": "127.0.0.1", "port": 53}}), - InterfaceSchema({"listen": {"ip": "::1", "port": 53}, "freebind": True}), + listen: List[ListenSchema] = [ + ListenSchema({"ip-address": "127.0.0.1"}), + ListenSchema({"ip-address": "::1", "freebind": True}), ] def _validate(self): 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 758fac2a..0fa31e98 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 @@ -1,17 +1,64 @@ -{% macro net_listen(interface) -%} -net.listen( -{%- if interface.listen.ip -%} -'{{ interface.listen.ip|string }}', -{%- if interface.listen.port -%} -{{ interface.listen.port|int }}, +{% macro listen_interface(interface) -%} +net.{{ interface }} +{%- endmacro %} + + +{% macro listen_kind(kind) -%} +'{{ 'tls' if kind == 'dot' else kind }}' +{%- endmacro %} + + +{% macro net_listen_unix_socket(socket, kind, freebind) -%} +net.listen('{{ socket }}',nil,{kind={{ listen_kind(kind) }},freebind={{ 'true' if freebind else 'false'}}}) +{%- endmacro %} + + +{% macro net_listen_ip_address(ip_address, kind, freebind, port) -%} +net.listen('{{ ip_address.addr }}', +{%- if ip_address.port -%} +{{ ip_address.port }}, +{%- else -%} +{{ port }}, {%- endif -%} -{%- elif interface.listen.unix_socket -%} -'{{ interface.listen.unix_socket|string }}',nil, -{%- elif interface.listen.interface -%} -net.{{ interface.listen.interface|string }}, -{%- if interface.listen.port -%} -{{ interface.listen.port|int }}, +{kind={{ listen_kind(kind) }},freebind={{ 'true' if freebind else 'false'}}}) +{%- endmacro %} + + +{% macro net_listen_interface(interface, kind, freebind, port) -%} +net.listen({{ listen_interface(interface.intrfc) }}, +{%- if interface.port -%} +{{ interface.port }}, +{%- else -%} +{{ port }}, {%- endif -%} +{kind={{ listen_kind(kind) }},freebind={{ 'true' if freebind else 'false'}}}) +{%- endmacro %} + + +{% macro network_listen(listen) -%} +{%- if listen.unix_socket -%} + {%- if listen.unix_socket is iterable-%} + {% for socket in listen.unix_socket -%} + {{ net_listen_unix_socket(socket, listen.kind, listen.freebind) }} + {% endfor -%} + {%- else -%} + {{ net_listen_unix_socket(listen.unix_socket, listen.kind, listen.freebind) }} + {%- endif -%} +{%- elif listen.ip_address -%} + {%- if listen.ip_address is iterable-%} + {% for address in listen.ip_address -%} + {{ net_listen_ip_address(address, listen.kind, listen.freebind, listen.port) }} + {% endfor -%} + {%- else -%} + {{ net_listen_ip_address(listen.ip_address, listen.kind, listen.freebind, listen.port) }} + {%- endif -%} +{%- 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 -%} {%- endif -%} -{kind='{{ 'tls' if interface.kind == 'dot' else interface.kind }}',freebind={{ 'true' if interface.freebind else 'false'}}}) {%- endmacro %}
\ No newline at end of file diff --git a/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 index 9bfd0dfd..edcfd177 100644 --- a/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 @@ -1,4 +1,4 @@ -{% from 'macros/network_macros.lua.j2' import net_listen %} +{% from 'macros/network_macros.lua.j2' import network_listen %} -- network.do-ipv4/6 net.ipv4 = {{ 'true' if cfg.network.do_ipv4 else 'false' }} @@ -65,7 +65,7 @@ renumber.config = { } {% endif %} --- network.interfaces -{% for interface in cfg.network.interfaces %} -{{ net_listen(interface) }} +-- network.listen +{% for listen in cfg.network.listen %} +{{ network_listen(listen) }} {% endfor %}
\ No newline at end of file diff --git a/manager/knot_resolver_manager/datamodel/types.py b/manager/knot_resolver_manager/datamodel/types.py index c7c3c0aa..c97f1364 100644 --- a/manager/knot_resolver_manager/datamodel/types.py +++ b/manager/knot_resolver_manager/datamodel/types.py @@ -353,30 +353,94 @@ class DomainName(CustomValueType): } +class InterfacePort(CustomValueType): + intrfc: str + port: Optional[int] = None + + def __init__(self, source_value: Any, object_path: str = "/") -> None: + super().__init__(source_value) + if isinstance(source_value, str): + if "@" in source_value: + sep = source_value.split("@", 1) + try: + self.port = int(sep[1]) + except ValueError as e: + raise SchemaException("Failed to parse port.", object_path) from e + + if not 0 <= self.port <= 65_535: + raise SchemaException( + f"Port value '{self.port}' out of range of usual 2-byte port value", object_path + ) + self.intrfc = sep[0] + else: + self.intrfc = source_value + self._value = source_value + + else: + raise SchemaException( + f"Unexpected value for a '<interface>[@<port>]'. Expected string, got '{source_value}'" + f" with type '{type(source_value)}'", + object_path, + ) + + def to_std(self) -> str: + return self._value + + def __str__(self) -> str: + return self._value + + def __int__(self) -> int: + raise ValueError("Can't convert InterfacePort to an integer") + + def __eq__(self, o: object) -> bool: + """ + Two instances of InterfacePort are equal when they represent same string. + """ + return isinstance(o, InterfacePort) and str(o._value) == str(self._value) + + def serialize(self) -> Any: + return str(self._value) + + @classmethod + def json_schema(cls: Type["InterfacePort"]) -> Dict[Any, Any]: + return { + "type": "string", + } + + class IPAddressPort(CustomValueType): + addr: Union[ipaddress.IPv4Address, ipaddress.IPv6Address] + port: Optional[int] = None + def __init__(self, source_value: Any, object_path: str = "/") -> None: super().__init__(source_value) if isinstance(source_value, str): - addr = source_value if "@" in source_value: sep = source_value.split("@", 1) - addr = sep[0] try: - port = int(sep[1]) + self.port = int(sep[1]) except ValueError as e: raise SchemaException("Failed to parse port.", object_path) from e - if not 0 <= port <= 65_535: - raise SchemaException(f"Port value '{port}' out of range of usual 2-byte port value", object_path) - try: - ipaddress.ip_address(addr) - except ValueError as e: - raise SchemaException("Failed to parse IP address.", object_path) from e + if not 0 <= self.port <= 65_535: + raise SchemaException( + f"Port value '{self.port}' out of range of usual 2-byte port value", object_path + ) + + try: + self.addr = ipaddress.ip_address(sep[0]) + except ValueError as e: + raise SchemaException("Failed to parse IP address.", object_path) from e + else: + try: + self.addr = ipaddress.ip_address(source_value) + except ValueError as e: + raise SchemaException("Failed to parse IP address.", object_path) from e + self._value = source_value - self._value: str = source_value else: raise SchemaException( - f"Unexpected value for a '<ip-address>@<port>'. Expected string, got '{source_value}'" + f"Unexpected value for a '<ip-address>[@<port>]'. Expected string, got '{source_value}'" f" with type '{type(source_value)}'", object_path, ) |