diff --git a/supervisor/api/dns.py b/supervisor/api/dns.py index 6255ad2a4..a31421eb1 100644 --- a/supervisor/api/dns.py +++ b/supervisor/api/dns.py @@ -48,7 +48,7 @@ class APICoreDNS(CoreSysAttributes): ATTR_UPDATE_AVAILABLE: self.sys_plugins.dns.need_update, ATTR_HOST: str(self.sys_docker.network.dns), ATTR_SERVERS: self.sys_plugins.dns.servers, - ATTR_LOCALS: self.sys_host.network.dns_servers, + ATTR_LOCALS: self.sys_plugins.dns.locals, } @api_process diff --git a/supervisor/data/coredns.tmpl b/supervisor/data/coredns.tmpl deleted file mode 100644 index 33e3bbdaa..000000000 --- a/supervisor/data/coredns.tmpl +++ /dev/null @@ -1,34 +0,0 @@ -.:53 { - log - errors - loop - {% if debug %}debug{% endif %} - hosts /config/hosts { - fallthrough - } - template ANY AAAA local.hass.io hassio { - rcode NOERROR - } - mdns - forward . {{ locals | join(" ") }} dns://127.0.0.1:5553 { - except local.hass.io - policy sequential - health_check 5s - } - fallback REFUSED . dns://127.0.0.1:5553 - fallback SERVFAIL . dns://127.0.0.1:5553 - fallback NXDOMAIN . dns://127.0.0.1:5553 - cache 10 -} - -.:5553 { - log - errors - {% if debug %}debug{% endif %} - forward . tls://1.1.1.1 tls://1.0.0.1 { - tls_servername cloudflare-dns.com - except local.hass.io - health_check 10s - } - cache 30 -} diff --git a/supervisor/data/hosts.tmpl b/supervisor/data/hosts.tmpl index 3224da3b2..b8b09a1de 100644 --- a/supervisor/data/hosts.tmpl +++ b/supervisor/data/hosts.tmpl @@ -1,2 +1,3 @@ -$supervisor hassio supervisor.local.hass.io hassio.local.hass.io -$homeassistant homeassistant homeassistant.local.hass.io home-assistant.local.hass.io +{% for entry in entries %} +{{- entry.ip_address }} {{ entry.names | join(" ") }} +{% endfor %} diff --git a/supervisor/docker/dns.py b/supervisor/docker/dns.py index 9ddbd76c3..0289c5cd6 100644 --- a/supervisor/docker/dns.py +++ b/supervisor/docker/dns.py @@ -46,7 +46,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes): detach=True, environment={ENV_TIME: self.sys_config.timezone}, volumes={ - str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "ro"} + str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "rw"} }, ) diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 49093e979..3a1cad508 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -292,11 +292,8 @@ class Tasks(CoreSysAttributes): return _LOGGER.warning("Watchdog found a problem with CoreDNS plugin!") - # Reset of failed - if await self.sys_plugins.dns.is_failed(): - _LOGGER.error("CoreDNS plugin is in failed state, resetting configuration") - await self.sys_plugins.dns.reset() - await self.sys_plugins.dns.loop_detection() + # Detect loop + await self.sys_plugins.dns.loop_detection() try: await self.sys_plugins.dns.start() diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index 58d9ac1ef..eae23896e 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -18,15 +18,16 @@ from ..const import ATTR_IMAGE, ATTR_SERVERS, ATTR_VERSION, DNS_SUFFIX, LogLevel from ..coresys import CoreSys, CoreSysAttributes from ..docker.dns import DockerDNS from ..docker.stats import DockerStats -from ..exceptions import CoreDNSError, CoreDNSUpdateError, DockerError -from ..utils.json import JsonConfig +from ..exceptions import CoreDNSError, CoreDNSUpdateError, DockerError, JsonFileError +from ..resolution.const import ContextType, IssueType, SuggestionType +from ..utils.json import JsonConfig, write_json_file from ..validate import dns_url from .const import FILE_HASSIO_DNS from .validate import SCHEMA_DNS_CONFIG _LOGGER: logging.Logger = logging.getLogger(__name__) -COREDNS_TMPL: Path = Path(__file__).parents[1].joinpath("data/coredns.tmpl") +HOSTS_TMPL: Path = Path(__file__).parents[1].joinpath("data/hosts.tmpl") RESOLV_TMPL: Path = Path(__file__).parents[1].joinpath("data/resolv.tmpl") HOST_RESOLV: Path = Path("/etc/resolv.conf") @@ -49,22 +50,35 @@ class CoreDNS(JsonConfig, CoreSysAttributes): super().__init__(FILE_HASSIO_DNS, SCHEMA_DNS_CONFIG) self.coresys: CoreSys = coresys self.instance: DockerDNS = DockerDNS(coresys) - self.coredns_template: Optional[jinja2.Template] = None self.resolv_template: Optional[jinja2.Template] = None + self.hosts_template: Optional[jinja2.Template] = None self._hosts: List[HostEntry] = [] self._loop: bool = False - @property - def corefile(self) -> Path: - """Return Path to corefile.""" - return Path(self.sys_config.path_dns, "corefile") - @property def hosts(self) -> Path: """Return Path to corefile.""" return Path(self.sys_config.path_dns, "hosts") + @property + def coredns_config(self) -> Path: + """Return Path to coredns config file.""" + return Path(self.sys_config.path_dns, "coredns.json") + + @property + def locals(self) -> List[str]: + """Return list of local system DNS servers.""" + servers: List[str] = [] + for server in self.sys_host.network.dns_servers: + if server in servers: + continue + with suppress(vol.Invalid): + dns_url(server) + servers.append(server) + + return servers + @property def servers(self) -> List[str]: """Return list of DNS servers.""" @@ -140,22 +154,18 @@ class CoreDNS(JsonConfig, CoreSysAttributes): self.save_data() # Initialize CoreDNS Template - try: - self.coredns_template = jinja2.Template(COREDNS_TMPL.read_text()) - except OSError as err: - _LOGGER.error("Can't read coredns.tmpl: %s", err) try: self.resolv_template = jinja2.Template(RESOLV_TMPL.read_text()) except OSError as err: _LOGGER.error("Can't read resolve.tmpl: %s", err) + try: + self.hosts_template = jinja2.Template(HOSTS_TMPL.read_text()) + except OSError as err: + _LOGGER.error("Can't read hosts.tmpl: %s", err) # Run CoreDNS - # If running, restart to update config/hosts - # this get shipped with Supervisor with suppress(CoreDNSError): - if await self.instance.is_running(): - await self.restart() - else: + if not await self.instance.is_running(): await self.start() # Update supervisor @@ -215,7 +225,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes): async def restart(self) -> None: """Restart CoreDNS plugin.""" - self._write_corefile() + self._write_config() _LOGGER.info("Restarting CoreDNS plugin") try: await self.instance.restart() @@ -225,7 +235,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes): async def start(self) -> None: """Run CoreDNS.""" - self._write_corefile() + self._write_config() # Start Instance _LOGGER.info("Starting CoreDNS plugin") @@ -268,47 +278,48 @@ class CoreDNS(JsonConfig, CoreSysAttributes): if b"plugin/loop: Loop" in log: _LOGGER.error("Detected a DNS loop in local Network!") self._loop = True + self.sys_resolution.create_issue( + IssueType.DNS_LOOP, + ContextType.PLUGIN, + reference=self.slug, + suggestions=[SuggestionType.EXECUTE_RESET], + ) else: self._loop = False - def _write_corefile(self) -> None: + def _write_config(self) -> None: """Write CoreDNS config.""" + debug: bool = self.sys_config.logging == LogLevel.DEBUG dns_servers: List[str] = [] - local_dns: List[str] = [] - servers: List[str] = [] + dns_locals: List[str] = [] # Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback if not self._loop: - local_dns = self.sys_host.network.dns_servers or ["dns://127.0.0.11"] - servers = self.servers + local_dns + dns_servers = self.servers + dns_locals = self.locals else: _LOGGER.warning("Ignoring user DNS settings because of loop") # Print some usefully debug data _LOGGER.debug( - "config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT", - self.servers, - local_dns, - ) - - # Make sure, they are valid - for server in servers: - try: - dns_url(server) - if server not in dns_servers: - dns_servers.append(server) - except vol.Invalid: - _LOGGER.warning("Ignoring invalid DNS Server: %s", server) - - # Generate config file - data = self.coredns_template.render( - locals=dns_servers, debug=self.sys_config.logging == LogLevel.DEBUG + "config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT / debug: %s", + dns_servers, + dns_locals, + debug, ) + # Write config to plugin try: - self.corefile.write_text(data) - except OSError as err: - _LOGGER.error("Can't update corefile: %s", err) + write_json_file( + self.coredns_config, + { + "servers": dns_servers, + "locals": dns_locals, + "debug": debug, + }, + ) + except JsonFileError as err: + _LOGGER.error("Can't update coredns config: %s", err) raise CoreDNSError() from err def _init_hosts(self) -> None: @@ -328,12 +339,13 @@ class CoreDNS(JsonConfig, CoreSysAttributes): def write_hosts(self) -> None: """Write hosts from memory to file.""" + # Generate config file + data = self.hosts_template.render(entries=self._hosts) + try: - with self.hosts.open("w") as hosts: - for entry in self._hosts: - hosts.write(f"{entry.ip_address!s} {' '.join(entry.names)}\n") + self.hosts.write_text(data) except OSError as err: - _LOGGER.error("Can't write hosts file: %s", err) + _LOGGER.error("Can't update hosts: %s", err) raise CoreDNSError() from err def add_host(self, ipv4: IPv4Address, names: List[str], write: bool = True) -> None: @@ -428,7 +440,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes): def _write_resolv(self, resolv_conf: Path) -> None: """Update/Write resolv.conf file.""" - nameservers = [f"{self.sys_docker.network.dns!s}", "127.0.0.11"] + nameservers = [str(self.sys_docker.network.dns), "127.0.0.11"] # Read resolv config data = self.resolv_template.render(servers=nameservers) diff --git a/supervisor/resolution/const.py b/supervisor/resolution/const.py index 33ab9b4cf..696ef0065 100644 --- a/supervisor/resolution/const.py +++ b/supervisor/resolution/const.py @@ -41,6 +41,7 @@ class IssueType(str, Enum): UPDATE_FAILED = "update_failed" UPDATE_ROLLBACK = "update_rollback" FATAL_ERROR = "fatal_error" + DNS_LOOP = "dns_loop" class SuggestionType(str, Enum): @@ -50,3 +51,4 @@ class SuggestionType(str, Enum): CREATE_FULL_SNAPSHOT = "create_full_snapshot" EXECUTE_UPDATE = "execute_update" EXECUTE_REPAIR = "execute_repair" + EXECUTE_RESET = "execute_reset"