]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
datamodel: network: interfaces schema changed to new listen schema #703
authorAleš <ales.mrazek@nic.cz>
Mon, 17 Jan 2022 17:53:19 +0000 (18:53 +0100)
committerAleš Mrázek <ales.mrazek@nic.cz>
Fri, 8 Apr 2022 14:17:53 +0000 (16:17 +0200)
- 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

manager/etc/knot-resolver/config.dev.yml
manager/knot_resolver_manager/datamodel/network_schema.py
manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2
manager/knot_resolver_manager/datamodel/templates/network.lua.j2
manager/knot_resolver_manager/datamodel/types.py
manager/tests/integration/config.yml
manager/tests/unit/datamodel/templates/test_network_macros.py
manager/tests/unit/datamodel/test_network_schema.py

index 2deeec790f03759a57edf529aeb8cafe4d4c1e90..a2b1292373a25911ff0dfde01087edff130c2e79 100644 (file)
@@ -5,14 +5,12 @@ logging:
   groups:
     - manager
 network:
-  interfaces:
-    - listen:
-        ip: 127.0.0.1
-        port: 5353
+  listen:
+    - ip-address: 127.0.0.1@5353
 server:
   workers: 1
   rundir: etc/knot-resolver/runtime
   management:
     listen:
       ip: 127.0.0.1
-      port: 5000
+      port: 5000
\ No newline at end of file
index d905c1fc26026cd31b358224b18574f57c4ceb32..2abb1ec53bab07ed4ff7690c789f41b7cf48119f 100644 (file)
@@ -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):
index 758fac2a76910e5d1eb5a8c657318fb7a8f8eb0d..0fa31e98e245fc4937ace8b479bc482b243bfadf 100644 (file)
@@ -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
index 9bfd0dfd528778e33ff4877c2b6b04d4d40f25a5..edcfd1771dd1d1a31ad6e044481d403e0bfca460 100644 (file)
@@ -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
index c7c3c0aa13383429227d0319cd4b07ff8c72c1d9..c97f136458ca635b91351bfade5b723f90d0dd8a 100644 (file)
@@ -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,
             )
index f2cf2feff97d026241cfd604b14c9c2a6c6cab4c..04ebf1a0264ec634a3d7723414fae5c16a3ba366 100644 (file)
@@ -1,8 +1,6 @@
 network:
-  interfaces:
-    - listen:
-        ip: 127.0.0.1
-        port: 5353
+  listen:
+    - ip-address: 127.0.0.1@5353
 server:
   workers: 1
   rundir: tests/integration/run
@@ -13,4 +11,4 @@ server:
 cache:
   storage: cache
 logging:
-  level: debug
+  level: debug
\ No newline at end of file
index c073c75e1cf0e66808075a0940e894d89ac48c08..7f302bf4f0937486cfc0179d2b3740481a72c9de 100644 (file)
@@ -1,25 +1,35 @@
 from knot_resolver_manager.datamodel.config_schema import template_from_str
-from knot_resolver_manager.datamodel.network_schema import InterfaceSchema
+from knot_resolver_manager.datamodel.network_schema import ListenSchema
 
 
-def test_net_listen():
-    ip = InterfaceSchema({"listen": {"ip": "::1", "port": 53}, "freebind": True})
-    soc = InterfaceSchema({"listen": {"unix-socket": "/tmp/kresd-socket"}, "kind": "dot"})
-    infc = InterfaceSchema({"listen": {"interface": "eth0"}, "kind": "doh"})
-
-    tmpl_str = """{% from 'macros/network_macros.lua.j2' import net_listen %}
-{{ net_listen(interface) }}"""
-
+def test_network_listen():
+    tmpl_str = """{% from 'macros/network_macros.lua.j2' import network_listen %}
+{{ network_listen(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(interface=ip)
-        == f"net.listen('{ip.listen.ip}',{ip.listen.port},{{kind='dns',freebind={str(ip.freebind).lower()}}})"
+        tmpl.render(listen=soc_list)
+        == "net.listen('/tmp/kresd-socket',nil,{kind='tls',freebind=false})\n"
+        + "net.listen('/tmp/kresd-socket2',nil,{kind='tls',freebind=false})\n"
     )
+
+    ip = ListenSchema({"ip-address": "::1", "freebind": True})
+    assert tmpl.render(listen=ip) == "net.listen('::1',53,{kind='dns',freebind=true})"
+    ip_list = ListenSchema({"ip-address": [ip.ip_address, "127.0.0.1@5353"]})
     assert (
-        tmpl.render(interface=soc)
-        == f"net.listen('{soc.listen.unix_socket}',nil,{{kind='tls',freebind={str(soc.freebind).lower()}}})"
+        tmpl.render(listen=ip_list)
+        == "net.listen('::1',53,{kind='dns',freebind=false})\n"
+        + "net.listen('127.0.0.1',5353,{kind='dns',freebind=false})\n"
     )
+
+    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@5353"], "port": 5555, "kind": "doh2"})
     assert (
-        tmpl.render(interface=infc)
-        == f"net.listen(net.{infc.listen.interface},443,{{kind='doh',freebind={str(soc.freebind).lower()}}})"
+        tmpl.render(listen=intrfc_list)
+        == "net.listen(net.eth0,5555,{kind='doh2',freebind=false})\n"
+        + "net.listen(net.lo,5353,{kind='doh2',freebind=false})\n"
     )
index 6470ff701e3949e37266bf0f52e06f90867456b3..be41206f1ed3ff23d6de7c85937d510abe6af924 100644 (file)
@@ -1,20 +1,44 @@
-import ipaddress
+from pytest import raises
 
-from knot_resolver_manager.datamodel.network_schema import NetworkSchema
-from knot_resolver_manager.datamodel.types import IPv4Address, IPv6Address
+from knot_resolver_manager.datamodel.network_schema import ListenSchema, NetworkSchema
+from knot_resolver_manager.datamodel.types import IPAddressPort
+from knot_resolver_manager.exceptions import KresManagerException
 
 
-def test_interfaces_default():
+def test_listen_defaults():
     o = NetworkSchema()
 
-    assert len(o.interfaces) == 2
-    # {"listen": {"ip": "127.0.0.1", "port": 53}}
-    assert o.interfaces[0].listen.ip == IPv4Address("127.0.0.1")
-    assert o.interfaces[0].listen.port == 53
-    assert o.interfaces[0].kind == "dns"
-    assert o.interfaces[0].freebind == False
-    # {"listen": {"ip": "::1", "port": 53}, "freebind": True}
-    assert o.interfaces[1].listen.ip == IPv6Address("::1")
-    assert o.interfaces[1].listen.port == 53
-    assert o.interfaces[1].kind == "dns"
-    assert o.interfaces[1].freebind == True
+    assert len(o.listen) == 2
+    # {"ip-address": "127.0.0.1"}
+    assert o.listen[0].ip_address == IPAddressPort("127.0.0.1")
+    assert o.listen[0].port == 53
+    assert o.listen[0].kind == "dns"
+    assert o.listen[0].freebind == False
+    # {"ip-address": "::1", "freebind": True}
+    assert o.listen[1].ip_address == IPAddressPort("::1")
+    assert o.listen[1].port == 53
+    assert o.listen[1].kind == "dns"
+    assert o.listen[1].freebind == True
+
+
+def test_listen_kind_port_defaults():
+    soc = ListenSchema({"unix-socket": "/tmp/kresd-socket"})
+    dns = ListenSchema({"ip-address": "::1"})
+    dot = ListenSchema({"ip-address": "::1", "kind": "dot"})
+    doh2 = ListenSchema({"ip-address": "::1", "kind": "doh2"})
+
+    assert soc.port == None
+    assert dns.port == 53
+    assert dot.port == 853
+    assert doh2.port == 443
+
+
+def test_listen_validation():
+    with raises(KresManagerException):
+        ListenSchema({"ip-address": "::1", "port": -10})
+    with raises(KresManagerException):
+        ListenSchema({"ip-address": "::1", "interface": "eth0"})
+    with raises(KresManagerException):
+        ListenSchema({"ip-address": "::1", "unit-socket": "/tmp/kresd-socket"})
+    with raises(KresManagerException):
+        ListenSchema({"unit-socket": "/tmp/kresd-socket", "port": "53"})