diff options
24 files changed, 1226 insertions, 470 deletions
| diff --git a/data/templates/dns-forwarding/recursor.conf.lua.tmpl b/data/templates/dns-forwarding/recursor.conf.lua.tmpl new file mode 100644 index 000000000..e2506238d --- /dev/null +++ b/data/templates/dns-forwarding/recursor.conf.lua.tmpl @@ -0,0 +1,9 @@ +-- Autogenerated by VyOS (dns_forwarding.py) -- +-- Do not edit, your changes will get overwritten -- + +-- Load DNSSEC root keys from dns-root-data package. +dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua") + +-- Load lua from vyos-hostsd -- +dofile("recursor.vyos-hostsd.conf.lua") + diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl index 9d1e019fa..d233b8abc 100644 --- a/data/templates/dns-forwarding/recursor.conf.tmpl +++ b/data/templates/dns-forwarding/recursor.conf.tmpl @@ -12,6 +12,7 @@ log-common-errors=yes  non-local-bind=yes  query-local-address=0.0.0.0  query-local-address6=:: +lua-config-file=recursor.conf.lua  # cache-size  max-cache-entries={{ cache_size }} @@ -22,23 +23,11 @@ max-negative-ttl={{ negative_ttl }}  # ignore-hosts-file  export-etc-hosts={{ export_hosts_file }} -# listen-on -local-address={{ listen_on | join(',') }} +# listen-address +local-address={{ listen_address | join(',') }}  # dnssec  dnssec={{ dnssec }} -# forward-zones / recursion -# -# statement is only inserted if either one forwarding domain or nameserver is configured -# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own -# -{% if name_servers or domains %}forward-zones-recurse= -{%- for d in domains %} -{{ d.name }}={{ d.servers | join(";") }} -{{- ", " if not loop.last -}} -{%- endfor -%} -{%- if name_servers -%} -{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }} -{% endif %} -{% endif %} +forward-zones-file=recursor.forward-zones.conf + diff --git a/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl new file mode 100644 index 000000000..de5eaee00 --- /dev/null +++ b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl @@ -0,0 +1,28 @@ +# Autogenerated by VyOS (vyos-hostsd) +# Do not edit, your changes will get overwritten + +# dot zone (catch-all): '+' indicates recursion is desired +# (same as forward-zones-recurse) +{#- the code below ensures the order of nameservers is determined first by #} +{#- the order of tags, then by the order of nameservers within that tag #} +{%- set n = namespace(dot_zone_ns='') %} +{%- for tag in name_server_tags_recursor %} +{%- set ns = '' %} +{%- if tag in name_servers %} +{%- set ns = ns + name_servers[tag]|join(', ') %} +{%- set n.dot_zone_ns = (n.dot_zone_ns, ns)|join(', ') if n.dot_zone_ns != '' else ns %} +{%- endif %} +# {{ tag }}: {{ ns }} +{%- endfor %} + +{%- if n.dot_zone_ns %} ++.={{ n.dot_zone_ns }} +{%- endif %} + +{% if forward_zones -%} +# zones added via 'service dns forwarding domain' +{%- for zone, zonedata in forward_zones.items() %} +{% if zonedata['recursion-desired'] %}+{% endif %}{{ zone }}={{ zonedata['nslist']|join(', ') }} +{%- endfor %} +{%- endif %} + diff --git a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl new file mode 100644 index 000000000..6d1760199 --- /dev/null +++ b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl @@ -0,0 +1,24 @@ +-- Autogenerated by VyOS (vyos-hostsd) -- +-- Do not edit, your changes will get overwritten -- + +{% if hosts -%} +-- from 'system static-host-mapping' and DHCP server +{%- for tag, taghosts in hosts.items() %} +{%- for host, hostprops in taghosts.items() %} +addNTA("{{ host }}.", "{{ tag }}") +{%- for a in hostprops['aliases'] %} +addNTA("{{ a }}.", "{{ tag }} alias") +{%- endfor %} +{%- endfor %} +{%- endfor %} +{%- endif %} + +{% if forward_zones -%} +-- from 'service dns forwarding domain' +{%- for zone, zonedata in forward_zones.items() %} +{%- if zonedata['addNTA'] %} +addNTA("{{ zone }}.", "static") +{%- endif %} +{%- endfor %} +{%- endif %} + diff --git a/data/templates/vyos-hostsd/hosts.tmpl b/data/templates/vyos-hostsd/hosts.tmpl new file mode 100644 index 000000000..566f9a5dd --- /dev/null +++ b/data/templates/vyos-hostsd/hosts.tmpl @@ -0,0 +1,26 @@ +### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +# Local host +127.0.0.1       localhost +127.0.1.1       {{ host_name }}{% if domain_name %}.{{ domain_name }} {{ host_name }}{% endif %} + +# The following lines are desirable for IPv6 capable hosts +::1             localhost ip6-localhost ip6-loopback +fe00::0         ip6-localnet +ff00::0         ip6-mcastprefix +ff02::1         ip6-allnodes +ff02::2         ip6-allrouters + +{% if hosts -%} +# From 'system static-host-mapping' and DHCP server +{%- for tag, taghosts in hosts.items() %} +# {{ tag }} +{%- for host, hostprops in taghosts.items() %} +{%- if hostprops['address'] %} +{{ hostprops['address'] }}	{{ host }}{% for a in hostprops['aliases'] %} {{ a }}{% endfor %} +{%- endif %} +{%- endfor %} +{%- endfor %} +{%- endif %} + diff --git a/data/templates/vyos-hostsd/resolv.conf.tmpl b/data/templates/vyos-hostsd/resolv.conf.tmpl new file mode 100644 index 000000000..b920b2e5f --- /dev/null +++ b/data/templates/vyos-hostsd/resolv.conf.tmpl @@ -0,0 +1,26 @@ +### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +{#- the code below ensures the order of nameservers is determined first by #} +{# the order of tags, then by the order of nameservers within that tag #} + +{%- for tag in name_server_tags_system %} +{%- if tag in name_servers %} +# {{ tag }} +{%- for ns in name_servers[tag] %} +nameserver {{ ns }} +{%- endfor %} +{%- endif %} +{%- endfor %} + +{%- if domain_name %} +domain {{ domain_name }} +{%- endif %} + +{% for tag in name_server_tags_system %} +{%- if tag in search_domains %} +# {{ tag }} +search {{ search_domains[tag]|join(' ') }} +{%- endif %} +{%- endfor %} + diff --git a/debian/control b/debian/control index bf330c35c..aaaf33e2a 100644 --- a/debian/control +++ b/debian/control @@ -35,6 +35,7 @@ Depends: python3,    python3-jmespath,    python3-xmltodict,    python3-pyudev, +  python3-voluptuous,    bsdmainutils,    cron,    etherwake, diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index a308401ee..dc129cb54 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -19,3 +19,14 @@ if ! grep -q '^minion' /etc/passwd; then      adduser --quiet minion disk      adduser --quiet minion users  fi + +# add hostsd group for vyos-hostsd +if ! grep -q '^hostsd' /etc/group; then +    addgroup --quiet --system hostsd +fi + +# add dhcpd user for dhcp-server +if ! grep -q '^dhcpd' /etc/passwd; then +    adduser --quiet --system --disabled-login --no-create-home --home /run/dhcp-server dhcpd +    adduser --quiet dhcpd hostsd +fi diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in index eeaa5b2c1..3b5843b53 100644 --- a/interface-definitions/dns-domain-name.xml.in +++ b/interface-definitions/dns-domain-name.xml.in @@ -5,7 +5,7 @@      <children>        <leafNode name="name-server" owner="${vyos_conf_scripts_dir}/host_name.py">          <properties> -          <help>Domain Name Server (DNS)</help> +          <help>Domain Name Servers (DNS) used by the system (resolv.conf)</help>            <priority>400</priority>            <valueHelp>              <format>ipv4</format> @@ -22,6 +22,16 @@            </constraint>          </properties>        </leafNode> +      <leafNode name="name-servers-dhcp" owner="${vyos_conf_scripts_dir}/host_name.py"> +        <properties> +          <help>Interfaces whose DHCP client nameservers will be used by the system (resolv.conf)</help> +          <priority>400</priority> +          <completionHelp> +            <script>${vyos_completion_dir}/list_interfaces.py</script> +          </completionHelp> +          <multi/> +        </properties> +      </leafNode>        <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py">          <properties>            <help>System host name (default: vyos)</help> @@ -56,20 +66,12 @@            </leafNode>          </children>        </node> -      <leafNode name="disable-dhcp-nameservers" owner="${vyos_conf_scripts_dir}/host_name.py"> -        <properties> -          <help>Disable DHCP updates of DNS settings</help> -          <priority>300</priority> -          <valueless/> -        </properties> -      </leafNode>        <node name="static-host-mapping" owner="${vyos_conf_scripts_dir}/host_name.py">          <properties>            <help>Map host names to addresses</help>            <priority>400</priority>          </properties>          <children> -            <tagNode name="host-name">              <properties>                <help>Host name for static address mapping</help> diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index 8b89bf758..993d69fe1 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -28,7 +28,7 @@                </leafNode>                <leafNode name="dhcp">                  <properties> -                  <help>Use DNS servers received from DHCP server for specified interface</help> +                  <help>Interfaces whose DHCP client nameservers to forward requests to</help>                    <completionHelp>                      <script>${vyos_completion_dir}/list_interfaces.py</script>                    </completionHelp> @@ -89,6 +89,18 @@                        </constraint>                      </properties>                    </leafNode> +                  <leafNode name="addnta"> +                    <properties> +                      <help>Add NTA (negative trust anchor) for this domain (must be set if the domain doesn't support DNSSEC)</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="recursion-desired"> +                    <properties> +                      <help>Set the "recursion desired" bit in requests to the upstream nameserver</help> +                      <valueless/> +                    </properties> +                  </leafNode>                  </children>                </tagNode>                <leafNode name="ignore-hosts-file"> diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py index f009aba98..303b6ea47 100644 --- a/python/vyos/hostsd_client.py +++ b/python/vyos/hostsd_client.py @@ -1,15 +1,11 @@  import json -  import zmq - -SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" - +SOCKET_PATH = "ipc:///run/vyos-hostsd/vyos-hostsd.sock"  class VyOSHostsdError(Exception):      pass -  class Client(object):      def __init__(self):          try: @@ -35,35 +31,89 @@ class Client(object):          except zmq.error.Again:              raise VyOSHostsdError("Could not connect to vyos-hostsd") -    def set_host_name(self, host_name, domain_name, search_domains): -        msg = { -            'type': 'host_name', -            'op': 'set', -            'data': { -                'host_name': host_name, -                'domain_name': domain_name, -                'search_domains': search_domains -            } -        } +    def add_name_servers(self, data): +        msg = {'type': 'name_servers', 'op': 'add', 'data': data}          self._communicate(msg) -    def add_hosts(self, tag, hosts): -        msg = {'type': 'hosts', 'op': 'add',  'tag': tag, 'data': hosts} +    def delete_name_servers(self, data): +        msg = {'type': 'name_servers', 'op': 'delete', 'data': data}          self._communicate(msg) -    def delete_hosts(self, tag): -        msg = {'type': 'hosts', 'op': 'delete', 'tag': tag} +    def get_name_servers(self, tag_regex): +        msg = {'type': 'name_servers', 'op': 'get', 'tag_regex': tag_regex} +        return self._communicate(msg) + +    def add_name_server_tags_recursor(self, data): +        msg = {'type': 'name_server_tags_recursor', 'op': 'add', 'data': data} +        self._communicate(msg) + +    def delete_name_server_tags_recursor(self, data): +        msg = {'type': 'name_server_tags_recursor', 'op': 'delete', 'data': data} +        self._communicate(msg) + +    def get_name_server_tags_recursor(self): +        msg = {'type': 'name_server_tags_recursor', 'op': 'get'} +        return self._communicate(msg) + +    def add_name_server_tags_system(self, data): +        msg = {'type': 'name_server_tags_system', 'op': 'add', 'data': data} +        self._communicate(msg) + +    def delete_name_server_tags_system(self, data): +        msg = {'type': 'name_server_tags_system', 'op': 'delete', 'data': data}          self._communicate(msg) -    def add_name_servers(self, tag, servers): -        msg = {'type': 'name_servers', 'op': 'add', 'tag': tag, 'data': servers} +    def get_name_server_tags_system(self): +        msg = {'type': 'name_server_tags_system', 'op': 'get'} +        return self._communicate(msg) + +    def add_forward_zones(self, data): +        msg = {'type': 'forward_zones', 'op': 'add', 'data': data}          self._communicate(msg) -    def delete_name_servers(self, tag): -        msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag} +    def delete_forward_zones(self, data): +        msg = {'type': 'forward_zones', 'op': 'delete', 'data': data}          self._communicate(msg) -    def get_name_servers(self, tag): -        msg = {'type': 'name_servers', 'op': 'get', 'tag': tag} +    def get_forward_zones(self): +        msg = {'type': 'forward_zones', 'op': 'get'}          return self._communicate(msg) +    def add_search_domains(self, data): +        msg = {'type': 'search_domains', 'op': 'add', 'data': data} +        self._communicate(msg) + +    def delete_search_domains(self, data): +        msg = {'type': 'search_domains', 'op': 'delete', 'data': data} +        self._communicate(msg) + +    def get_search_domains(self, tag_regex): +        msg = {'type': 'search_domains', 'op': 'get', 'tag_regex': tag_regex} +        return self._communicate(msg) + +    def add_hosts(self, data): +        msg = {'type': 'hosts', 'op': 'add', 'data': data} +        self._communicate(msg) + +    def delete_hosts(self, data): +        msg = {'type': 'hosts', 'op': 'delete', 'data': data} +        self._communicate(msg) + +    def get_hosts(self, tag_regex): +        msg = {'type': 'hosts', 'op': 'get', 'tag_regex': tag_regex} +        return self._communicate(msg) + +    def set_host_name(self, host_name, domain_name): +        msg = { +            'type': 'host_name', +            'op': 'set', +            'data': { +                'host_name': host_name, +                'domain_name': domain_name, +            } +        } +        self._communicate(msg) + +    def apply(self): +        msg = {'op': 'apply'} +        return self._communicate(msg) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 692ac2456..51631dc16 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -15,49 +15,45 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os -import argparse  from sys import exit  from copy import deepcopy  from vyos.config import Config  from vyos.hostsd_client import Client as hostsd_client -from vyos.util import wait_for_commit_lock  from vyos import ConfigError -from vyos.util import call +from vyos.util import call, chown  from vyos.template import render  from vyos import airbag  airbag.enable() -parser = argparse.ArgumentParser() -parser.add_argument("--dhclient", action="store_true", -                    help="Started from dhclient-script") - -config_file = r'/run/powerdns/recursor.conf' +pdns_rec_user = pdns_rec_group = 'pdns' +pdns_rec_run_dir = '/run/powerdns' +pdns_rec_lua_conf_file = f'{pdns_rec_run_dir}/recursor.conf.lua' +pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.lua' +pdns_rec_hostsd_zones_file = f'{pdns_rec_run_dir}/recursor.forward-zones.conf' +pdns_rec_config_file = f'{pdns_rec_run_dir}/recursor.conf'  default_config_data = {      'allow_from': [],      'cache_size': 10000,      'export_hosts_file': 'yes', -    'listen_on': [], +    'listen_address': [],      'name_servers': [],      'negative_ttl': 3600, -    'domains': [], -    'dnssec': 'process-no-validate' +    'system': False, +    'domains': {}, +    'dnssec': 'process-no-validate', +    'dhcp_interfaces': []  } +hostsd_tag = 'static' -def get_config(arguments): +def get_config(conf):      dns = deepcopy(default_config_data) -    conf = Config()      base = ['service', 'dns', 'forwarding'] -    if arguments.dhclient: -        conf.exists = conf.exists_effective -        conf.return_value = conf.return_effective_value -        conf.return_values = conf.return_effective_values -      if not conf.exists(base):          return None @@ -75,53 +71,35 @@ def get_config(arguments):          dns['negative_ttl'] = negative_ttl      if conf.exists(['domain']): -        for node in conf.list_nodes(['domain']): -            servers = conf.return_values(['domain', node, 'server']) -            domain = { -                "name": node, -                "servers": bracketize_ipv6_addrs(servers) +        for domain in conf.list_nodes(['domain']): +            conf.set_level(base + ['domain', domain]) +            entry = { +                'nslist': bracketize_ipv6_addrs(conf.return_values(['server'])), +                'addNTA': conf.exists(['addnta']), +                'recursion-desired': conf.exists(['recursion-desired'])              } -            dns['domains'].append(domain) +            dns['domains'][domain] = entry + +        conf.set_level(base)      if conf.exists(['ignore-hosts-file']):          dns['export_hosts_file'] = "no"      if conf.exists(['name-server']): -        name_servers = conf.return_values(['name-server']) -        dns['name_servers'] = dns['name_servers'] + name_servers +        dns['name_servers'] = bracketize_ipv6_addrs( +                conf.return_values(['name-server']))      if conf.exists(['system']): -        conf.set_level(['system']) -        system_name_servers = [] -        system_name_servers = conf.return_values(['name-server']) -        if not system_name_servers: -            print("DNS forwarding warning: No name-servers set under 'system name-server'\n") -        else: -            dns['name_servers'] = dns['name_servers'] + system_name_servers -        conf.set_level(base) - -    dns['name_servers'] = bracketize_ipv6_addrs(dns['name_servers']) +        dns['system'] = True      if conf.exists(['listen-address']): -        dns['listen_on'] = conf.return_values(['listen-address']) +        dns['listen_address'] = conf.return_values(['listen-address'])      if conf.exists(['dnssec']):          dns['dnssec'] = conf.return_value(['dnssec']) -    # Add name servers received from DHCP      if conf.exists(['dhcp']): -        interfaces = [] -        interfaces = conf.return_values(['dhcp']) -        hc = hostsd_client() - -        for interface in interfaces: -            dhcp_resolvers = hc.get_name_servers(f'dhcp-{interface}') -            dhcpv6_resolvers = hc.get_name_servers(f'dhcpv6-{interface}') - -            if dhcp_resolvers: -                dns['name_servers'] = dns['name_servers'] + dhcp_resolvers -            if dhcpv6_resolvers: -                dns['name_servers'] = dns['name_servers'] + dhcpv6_resolvers +        dns['dhcp_interfaces'] = conf.return_values(['dhcp'])      return dns @@ -129,14 +107,14 @@ def bracketize_ipv6_addrs(addrs):      """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched."""      return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] -def verify(dns): +def verify(conf, dns):      # bail out early - looks like removal from running config      if dns is None:          return None -    if not dns['listen_on']: +    if not dns['listen_address']:          raise ConfigError( -            "Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") +            "Error: DNS forwarding requires a listen-address")      if not dns['allow_from']:          raise ConfigError( @@ -144,9 +122,22 @@ def verify(dns):      if dns['domains']:          for domain in dns['domains']: -            if not domain['servers']: -                raise ConfigError( -                    'Error: No server configured for domain {0}'.format(domain['name'])) +            if not dns['domains'][domain]['nslist']: +                raise ConfigError(( +                    f'Error: No server configured for domain {domain}')) + +    no_system_nameservers = False +    if dns['system'] and not ( +            conf.exists(['system', 'name-server']) or +            conf.exists(['system', 'name-servers-dhcp']) ): +        no_system_nameservers = True +        print(("DNS forwarding warning: No 'system name-server' or " +                "'system name-servers-dhcp' set\n")) + +    if (no_system_nameservers or not dns['system']) and not ( +            dns['name_servers'] or dns['dhcp_interfaces']): +        print(("DNS forwarding warning: No 'dhcp', 'name-server' or 'system' " +            "nameservers set. Forwarding will operate as a recursor.\n"))      return None @@ -155,29 +146,69 @@ def generate(dns):      if dns is None:          return None -    render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True, user='pdns', group='pdns') +    render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl', +            dns, user=pdns_rec_user, group=pdns_rec_group) + +    render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl', +            dns, user=pdns_rec_user, group=pdns_rec_group) + +    # if vyos-hostsd didn't create its files yet, create them (empty) +    for f in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]: +        with open(f, 'a'): +            pass +        chown(f, user=pdns_rec_user, group=pdns_rec_group) +      return None  def apply(dns):      if dns is None:          # DNS forwarding is removed in the commit          call("systemctl stop pdns-recursor.service") -        if os.path.isfile(config_file): -            os.unlink(config_file) +        if os.path.isfile(pdns_rec_config_file): +            os.unlink(pdns_rec_config_file)      else: -        call("systemctl restart pdns-recursor.service") +        ### first apply vyos-hostsd config +        hc = hostsd_client() -if __name__ == '__main__': -    args = parser.parse_args() +        # add static nameservers to hostsd so they can be joined with other +        # sources +        hc.delete_name_servers([hostsd_tag]) +        if dns['name_servers']: +            hc.add_name_servers({hostsd_tag: dns['name_servers']}) + +        # delete all nameserver tags +        hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor()) + +        ## add nameserver tags - the order determines the nameserver order! +        # our own tag (static) +        hc.add_name_server_tags_recursor([hostsd_tag]) + +        if dns['system']: +            hc.add_name_server_tags_recursor(['system']) +        else: +            hc.delete_name_server_tags_recursor(['system']) + +        # add dhcp nameserver tags for configured interfaces +        for intf in dns['dhcp_interfaces']: +            hc.add_name_server_tags_recursor(['dhcp-' + intf, 'dhcpv6-' + intf ]) + +        # hostsd will generate the forward-zones file +        # the list and keys() are required as get returns a dict, not list +        hc.delete_forward_zones(list(hc.get_forward_zones().keys())) +        if dns['domains']: +            hc.add_forward_zones(dns['domains']) -    if args.dhclient: -        # There's a big chance it was triggered by a commit still in progress -        # so we need to wait until the new values are in the running config -        wait_for_commit_lock() +        # call hostsd to generate forward-zones and its lua-config-file +        hc.apply() +        ### finally (re)start pdns-recursor +        call("systemctl restart pdns-recursor.service") + +if __name__ == '__main__':      try: -        c = get_config(args) -        verify(c) +        conf = Config() +        c = get_config(conf) +        verify(conf, c)          generate(c)          apply(c)      except ConfigError as e: diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index dbc587d7d..3e301477d 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -41,19 +41,21 @@ default_config_data = {      'domain_name': '',      'domain_search': [],      'nameserver': [], -    'no_dhcp_ns': False +    'nameservers_dhcp_interfaces': [], +    'static_host_mapping': {}  } -def get_config(): -    conf = Config() +hostsd_tag = 'system' + +def get_config(conf):      hosts = copy.deepcopy(default_config_data) -    if conf.exists("system host-name"): -        hosts['hostname'] = conf.return_value("system host-name") -        # This may happen if the config is not loaded yet, -        # e.g. if run by cloud-init -        if not hosts['hostname']: -            hosts['hostname'] = default_config_data['hostname'] +    hosts['hostname'] = conf.return_value("system host-name") + +    # This may happen if the config is not loaded yet, +    # e.g. if run by cloud-init +    if not hosts['hostname']: +        hosts['hostname'] = default_config_data['hostname']      if conf.exists("system domain-name"):          hosts['domain_name'] = conf.return_value("system domain-name") @@ -62,60 +64,50 @@ def get_config():      for search in conf.return_values("system domain-search domain"):          hosts['domain_search'].append(search) -    if conf.exists("system name-server"): -        hosts['nameserver'] = conf.return_values("system name-server") +    hosts['nameserver'] = conf.return_values("system name-server") -    if conf.exists("system disable-dhcp-nameservers"): -        hosts['no_dhcp_ns'] = True +    hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp")      # system static-host-mapping -    hosts['static_host_mapping'] = [] - -    if conf.exists('system static-host-mapping host-name'): -        for hn in conf.list_nodes('system static-host-mapping host-name'): -            mapping = {} -            mapping['host'] = hn -            mapping['address'] = conf.return_value('system static-host-mapping host-name {0} inet'.format(hn)) -            mapping['aliases'] = conf.return_values('system static-host-mapping host-name {0} alias'.format(hn)) -            hosts['static_host_mapping'].append(mapping) +    for hn in conf.list_nodes('system static-host-mapping host-name'): +        hosts['static_host_mapping'][hn] = {} +        hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet') +        hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias')      return hosts -def verify(config): -    if config is None: +def verify(conf, hosts): +    if hosts is None:          return None      # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)"      hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") -    if not hostname_regex.match(config['hostname']): -        raise ConfigError('Invalid host name ' + config["hostname"]) +    if not hostname_regex.match(hosts['hostname']): +        raise ConfigError('Invalid host name ' + hosts["hostname"])      # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" -    length = len(config['hostname']) +    length = len(hosts['hostname'])      if length < 1 or length > 63:          raise ConfigError(              'Invalid host-name length, must be less than 63 characters') -    # The search list is currently limited to six domains with a total of 256 characters. -    # https://linux.die.net/man/5/resolv.conf -    if len(config['domain_search']) > 6: -        raise ConfigError( -            'The search list is currently limited to six domains') - -    tmp = ' '.join(config['domain_search']) -    if len(tmp) > 256: -        raise ConfigError( -            'The search list is currently limited to 256 characters') - +    all_static_host_mapping_addresses = []      # static mappings alias hostname -    if config['static_host_mapping']: -        for m in config['static_host_mapping']: -            if not m['address']: -                raise ConfigError('IP address required for ' + m['host']) -            for a in m['aliases']: -                if not hostname_regex.match(a) and len(a) != 0: -                    raise ConfigError('Invalid alias \'{0}\' in mapping {1}'.format(a, m['host'])) +    for host, hostprops in hosts['static_host_mapping'].items(): +        if not hostprops['address']: +            raise ConfigError(f'IP address required for static-host-mapping "{host}"') +        if hostprops['address'] in all_static_host_mapping_addresses: +            raise ConfigError(( +                f'static-host-mapping "{host}" address "{hostprops["address"]}"' +                f'already used in another static-host-mapping')) +        all_static_host_mapping_addresses.append(hostprops['address']) +        for a in hostprops['aliases']: +            if not hostname_regex.match(a) and len(a) != 0: +                raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"') + +    # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't +    # exist or doesn't have address dhcp(v6)      return None @@ -128,24 +120,32 @@ def apply(config):          return None      ## Send the updated data to vyos-hostsd +    try: +        hc = vyos.hostsd_client.Client() -    # vyos-hostsd uses "tags" to identify data sources -    tag = "static" +        hc.set_host_name(config['hostname'], config['domain_name']) -    try: -        client = vyos.hostsd_client.Client() +        hc.delete_search_domains([hostsd_tag]) +        if config['domain_search']: +            hc.add_search_domains({hostsd_tag: config['domain_search']}) + +        hc.delete_name_servers([hostsd_tag]) +        if config['nameserver']: +            hc.add_name_servers({hostsd_tag: config['nameserver']}) -        # Check if disable-dhcp-nameservers is configured, and if yes - delete DNS servers added by DHCP -        if config['no_dhcp_ns']: -            client.delete_name_servers('dhcp-.+') +        # add our own tag's (system) nameservers and search to resolv.conf +        hc.delete_name_server_tags_system(hc.get_name_server_tags_system()) +        hc.add_name_server_tags_system([hostsd_tag]) -        client.set_host_name(config['hostname'], config['domain_name'], config['domain_search']) +        # this will add the dhcp client nameservers to resolv.conf +        for intf in config['nameservers_dhcp_interfaces']: +            hc.add_name_server_tags_system([f'dhcp-{intf}', f'dhcpv6-{intf}']) -        client.delete_name_servers(tag) -        client.add_name_servers(tag, config['nameserver']) +        hc.delete_hosts([hostsd_tag]) +        if config['static_host_mapping']: +            hc.add_hosts({hostsd_tag: config['static_host_mapping']}) -        client.delete_hosts(tag) -        client.add_hosts(tag, config['static_host_mapping']) +        hc.apply()      except vyos.hostsd_client.VyOSHostsdError as e:          raise ConfigError(str(e)) @@ -167,25 +167,14 @@ def apply(config):      if process_named_running('snmpd'):          call('systemctl restart snmpd.service') -    # restart pdns if it is used - we check for the control dir to not raise -    # an exception on system startup -    # -    #   File "/usr/lib/python3/dist-packages/vyos/configsession.py", line 128, in __run_command -    #     raise ConfigSessionError(output) -    # vyos.configsession.ConfigSessionError: [ system domain-name vyos.io ] -    # Fatal: Unable to generate local temporary file in directory '/run/powerdns': No such file or directory -    if os.path.isdir('/run/powerdns'): -        ret = run('/usr/bin/rec_control --socket-dir=/run/powerdns ping') -        if ret == 0: -            call('systemctl restart pdns-recursor.service') -      return None  if __name__ == '__main__':      try: -        c = get_config() -        verify(c) +        conf = Config() +        c = get_config(conf) +        verify(conf, c)          generate(c)          apply(c)      except ConfigError as e: diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf index ea5562ea8..24090e2a8 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf +++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf @@ -1,39 +1,44 @@ -# modified make_resolv_conf () for Vyatta system below +# modified make_resolv_conf () for VyOS  make_resolv_conf() { +    hostsd_client="/usr/bin/vyos-hostsd-client" +    hostsd_changes= +      if [ -n "$new_domain_name" ]; then -        logmsg info "Adding search-domain \"$new_domain_name\" via vyos-hostsd-client" -        /usr/bin/vyos-hostsd-client --set-host-name --search-domain $new_domain_name +        logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" +        $hostsd_client --delete-search-domains --tag "dhcp-$interface" +        logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcp-$interface\" via vyos-hostsd-client" +        $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcp-$interface" +        hostsd_changes=y      fi      if [ -n "$new_dhcp6_domain_search" ]; then -        logmsg info "Adding search-domain \"$new_dhcp6_domain_search\" via vyos-hostsd-client" -        /usr/bin/vyos-hostsd-client --set-host-name --search-domain $new_dhcp6_domain_search +        logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" +        $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" +        logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" +        $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface" +        hostsd_changes=y      fi -    if [ -n "$new_domain_name_servers" ] && ! cli-shell-api existsEffective system disable-dhcp-nameservers && [ "$new_domain_name_servers" != "$old_domain_name_servers" ] ; then +    if [ -n "$new_domain_name_servers" ]; then          logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client" -        vyos-hostsd-client --delete-name-servers --tag dhcp-$interface -        NEW_SERVERS="" -        for nameserver in $new_domain_name_servers; do -            NEW_SERVERS="$NEW_SERVERS --name-server $nameserver" -        done -        logmsg info "Adding nameservers \"$NEW_SERVERS\" with tag \"dhcp-$interface\" via vyos-hostsd-client" -        /usr/bin/vyos-hostsd-client --add-name-servers $NEW_SERVERS --tag dhcp-$interface +        $hostsd_client --delete-name-servers --tag "dhcp-$interface" +        logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcp-$interface\" via vyos-hostsd-client" +        $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcp-$interface" +        hostsd_changes=y      fi -    if [ -n "$new_dhcp6_name_servers" ] && ! cli-shell-api existsEffective system disable-dhcp-nameservers && [ "$new_dhcp6_name_servers" != "$old_dhcp6_name_servers" ] ; then +    if [ -n "$new_dhcp6_name_servers" ]; then          logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" -        vyos-hostsd-client --delete-name-servers --tag dhcpv6-$interface -        NEW_SERVERS="" -        for nameserver in $new_dhcp6_name_servers; do -            NEW_SERVERS="$NEW_SERVERS --name-server $nameserver" -        done -        logmsg info "Adding nameservers \"$NEW_SERVERS\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" -        /usr/bin/vyos-hostsd-client --add-name-servers $NEW_SERVERS --tag dhcpv6-$interface +        $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" +        logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" +        $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface" +        hostsd_changes=y      fi -    if cli-shell-api existsEffective service dns forwarding; then -        logmsg info "Enabling DNS forwarding" -        /usr/libexec/vyos/conf_mode/dns_forwarding.py --dhclient +    if [ $hostsd_changes ]; then +        logmsg info "Applying changes via vyos-hostsd-client" +        $hostsd_client --apply +    else +        logmsg info "No changes to apply via vyos-hostsd-client"      fi  } diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index 88a4d9db9..01981ad04 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -1,14 +1,24 @@ +## +## VyOS cleanup +##  # NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state +hostsd_client="/usr/bin/vyos-hostsd-client" +hostsd_changes=  if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then -    # delete dynamic nameservers from a configuration if lease was deleted +    # delete search domains and nameservers via vyos-hostsd +    logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" +    $hostsd_client --delete-search-domains --tag "dhcp-$interface"      logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client" -    vyos-hostsd-client --delete-name-servers --tag dhcp-${interface} +    $hostsd_client --delete-name-servers --tag "dhcp-${interface}" +    hostsd_changes=y +      # try to delete default ip route       for router in $old_routers; do          logmsg info "Deleting default route: via $router dev ${interface}"          ip -4 route del default via $router dev ${interface}      done +      # delete rfc3442 routes      if [ -n "$old_rfc3442_classless_static_routes" ]; then          set -- $old_rfc3442_classless_static_routes @@ -72,7 +82,17 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then  fi  if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then -    # delete dynamic nameservers from a configuration if lease was deleted +    # delete search domains and nameservers via vyos-hostsd +    logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" +    $hostsd_client --delete-search-domains --tag "dhcpv6-$interface"      logmsg info "Deleting nameservers with tag \"dhcpv6-${interface}\" via vyos-hostsd-client" -    vyos-hostsd-client --delete-name-servers --tag dhcpv6-${interface} +    $hostsd_client --delete-name-servers --tag "dhcpv6-${interface}" +    hostsd_changes=y +fi + +if [ $hostsd_changes ]; then +    logmsg info "Applying changes via vyos-hostsd-client" +    $hostsd_client --apply +else +    logmsg info "No changes to apply via vyos-hostsd-client"  fi diff --git a/src/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf index 750bc9972..158bac02b 100644 --- a/src/etc/systemd/system/pdns-recursor.service.d/override.conf +++ b/src/etc/systemd/system/pdns-recursor.service.d/override.conf @@ -2,6 +2,7 @@  WorkingDirectory=  WorkingDirectory=/run/powerdns  RuntimeDirectory= -RuntimeDirectory=/run/powerdns +RuntimeDirectory=powerdns +RuntimeDirectoryPreserve=yes  ExecStart=  ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns --socket-dir=/run/powerdns diff --git a/src/migration-scripts/dns-forwarding/2-to-3 b/src/migration-scripts/dns-forwarding/2-to-3 new file mode 100755 index 000000000..01e445b22 --- /dev/null +++ b/src/migration-scripts/dns-forwarding/2-to-3 @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +# + +# Sets the new options "addnta" and "recursion-desired" for all +# 'dns forwarding domain' as this is usually desired + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): +    # Nothing to do +    sys.exit(0) + +if config.exists(base + ['domain']): +    for domain in config.list_nodes(base + ['domain']): +        domain_base = base + ['domain', domain] +        config.set(domain_base + ['addnta']) +        config.set(domain_base + ['recursion-desired']) + +    try: +        with open(file_name, 'w') as f: +            f.write(config.to_string()) +    except OSError as e: +        print("Failed to save the modified config: {}".format(e)) +        sys.exit(1) diff --git a/src/migration-scripts/system/17-to-18 b/src/migration-scripts/system/17-to-18 new file mode 100755 index 000000000..dd2abce00 --- /dev/null +++ b/src/migration-scripts/system/17-to-18 @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +# + +# migrate disable-dhcp-nameservers (boolean) to name-servers-dhcp <interface> +# if disable-dhcp-nameservers is set, just remove it +# else retrieve all interface names that have configured dhcp(v6) address and +# add them to the new name-servers-dhcp node + +from sys import argv, exit +from vyos.ifconfig import Interface +from vyos.configtree import ConfigTree + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +config = ConfigTree(config_file) + +base = ['system'] +if not config.exists(base): +    # Nothing to do +    exit(0) + +if config.exists(base + ['disable-dhcp-nameservers']): +    config.delete(base + ['disable-dhcp-nameservers']) +else: +    dhcp_interfaces = [] + +    # go through all interfaces searching for 'address dhcp(v6)?' +    for sect in Interface.sections(): +        sect_base = ['interfaces', sect] + +        if not config.exists(sect_base): +            continue + +        for intf in config.list_nodes(sect_base): +            intf_base = sect_base + [intf] + +            # try without vlans +            if config.exists(intf_base + ['address']): +                for addr in config.return_values(intf_base + ['address']): +                    if addr in ['dhcp', 'dhcpv6']: +                        dhcp_interfaces.append(intf) + +            # try vif +            if config.exists(intf_base + ['vif']): +                for vif in config.list_nodes(intf_base + ['vif']): +                    vif_base = intf_base + ['vif', vif] +                    if config.exists(vif_base + ['address']): +                        for addr in config.return_values(vif_base + ['address']): +                            if addr in ['dhcp', 'dhcpv6']: +                                dhcp_interfaces.append(f'{intf}.{vif}') + +            # try vif-s +            if config.exists(intf_base + ['vif-s']): +                for vif_s in config.list_nodes(intf_base + ['vif-s']): +                    vif_s_base = intf_base + ['vif-s', vif_s] +                    if config.exists(vif_s_base + ['address']): +                        for addr in config.return_values(vif_s_base + ['address']): +                            if addr in ['dhcp', 'dhcpv6']: +                                dhcp_interfaces.append(f'{intf}.{vif_s}') + +                # try vif-c +                if config.exists(intf_base + ['vif-c', vif_c]): +                    for vif_c in config.list_nodes(vif_s_base + ['vif-c', vif_c]): +                        vif_c_base = vif_s_base + ['vif-c', vif_c] +                        if config.exists(vif_c_base + ['address']): +                            for addr in config.return_values(vif_c_base + ['address']): +                                if addr in ['dhcp', 'dhcpv6']: +                                    dhcp_interfaces.append(f'{intf}.{vif_s}.{vif_c}') + +    # set new config nodes +    for intf in dhcp_interfaces: +        config.set(base + ['name-servers-dhcp'], value=intf, replace=False) + +    # delete old node +    config.delete(base + ['disable-dhcp-nameservers']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) + +exit(0) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index bf5d67cfa..0079f7e5c 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -14,7 +14,201 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  # +######### +# USAGE # +######### +# This daemon listens on its socket for JSON messages. +# The received message format is:  # +# { 'type': '<message type>', +#   'op': '<message operation>', +#   'data': <data list or dict> +# } +# +# For supported message types, see below. +# 'op' can be 'add', delete', 'get', 'set' or 'apply'. +# Different message types support different sets of operations and different +# data formats. +# +# Changes to configuration made via add or delete don't take effect immediately, +# they are remembered in a state variable and saved to disk to a state file. +# State is remembered across daemon restarts but not across system reboots +# as it's saved in a temporary filesystem (/run). +# +# 'apply' is a special operation that applies the configuration from the cached +# state, rendering all config files and reloading relevant daemons (currently +# just pdns-recursor via rec-control). +# +# note: 'add' operation also acts as 'update' as it uses dict.update, if the +# 'data' dict item value is a dict. If it is a list, it uses list.append. +# +### tags +# Tags can be arbitrary, but they are generally in this format: +# 'static', 'system', 'dhcp(v6)-<intf>' or 'dhcp-server-<client ip>' +# They are used to distinguish entries created by different scripts so they can +# be removed and recreated without having to track what needs to be changed. +# They are also used as a way to control which tags settings (e.g. nameservers) +# get added to various config files via name_server_tags_(recursor|system) +# +### name_server_tags_(recursor|system) +# A list of tags whose nameservers and search domains is used to generate +# /etc/resolv.conf and pdns-recursor config. +# system list is used to generate resolv.conf. +# recursor list is used to generate pdns-rec forward-zones. +# When generating each file, the order of nameservers is as per the order of +# name_server_tags (the order in which tags were added), then the order in +# which the name servers for each tag were added. +# +#### Message types +# +### name_servers +# +# { 'type': 'name_servers', +#   'op': 'add', +#   'data': { +#       '<str tag>': ['<str nameserver>', ...], +#       ... +#     } +# } +# +# { 'type': 'name_servers', +#   'op': 'delete', +#   'data': ['<str tag>', ...] +# } +# +# { 'type': 'name_servers', +#   'op': 'get', +#   'tag_regex': '<str regex>' +# } +# response: +# { 'data': { +#       '<str tag>': ['<str nameserver>', ...], +#       ... +#     } +# } +# +### name_server_tags +# +# { 'type': 'name_server_tags', +#   'op': 'add', +#   'data':  ['<str tag>', ...] +# } +# +# { 'type': 'name_server_tags', +#   'op': 'delete', +#   'data': ['<str tag>', ...] +# } +# +# { 'type': 'name_server_tags', +#   'op': 'get', +# } +# response: +# { 'data': ['<str tag>', ...] } +# +### forward_zones +## Additional zones added to pdns-recursor forward-zones-file. +## If recursion-desired is true, '+' will be prepended to the zone line. +## If addNTA is true, a NTA will be added via lua-config-file. +# +# { 'type': 'forward_zones', +#   'op': 'add', +#   'data': { +#       '<str zone>': { +#           'nslist': ['<str nameserver>', ...], +#           'addNTA': <bool>, +#           'recursion-desired': <bool> +#         } +#       ... +#     } +# } +# +# { 'type': 'forward_zones', +#   'op': 'delete', +#   'data': ['<str zone>', ...] +# } +# +# { 'type': 'forward_zones', +#   'op': 'get', +# } +# response: +# { 'data': { +#       '<str zone>': { ... }, +#       ... +#     } +# } +# +# +### search_domains +# +# { 'type': 'search_domains', +#   'op': 'add', +#   'data': { +#       '<str tag>': ['<str domain>', ...], +#       ... +#     } +# } +# +# { 'type': 'search_domains', +#   'op': 'delete', +#   'data': ['<str tag>', ...] +# } +# +# { 'type': 'search_domains', +#   'op': 'get', +# } +# response: +# { 'data': { +#       '<str tag>': ['<str domain>', ...], +#       ... +#     } +# } +# +### hosts +# +# { 'type': 'hosts', +#   'op': 'add', +#   'data': { +#       '<str tag>': { +#           '<str host>': { +#               'address': '<str address>', +#               'aliases': ['<str alias>, ...] +#             }, +#           ... +#         }, +#       ... +#     } +# } +# +# { 'type': 'hosts', +#   'op': 'delete', +#   'data': ['<str tag>', ...] +# } +# +# { 'type': 'hosts', +#   'op': 'get' +#   'tag_regex': '<str regex>' +# } +# response: +# { 'data': { +#       '<str tag>': { +#           '<str host>': { +#               'address': '<str address>', +#               'aliases': ['<str alias>, ...] +#             }, +#           ... +#         }, +#       ... +#     } +# } +### host_name +# +# { 'type': 'host_name', +#   'op': 'set', +#   'data': { +#       'host_name': '<str hostname>' +#       'domain_name': '<str domainname>' +#     } +# }  import os  import sys @@ -25,10 +219,10 @@ import traceback  import re  import logging  import zmq -import collections - -import jinja2 -from vyos.util import popen, process_named_running +from voluptuous import Schema, MultipleInvalid, Required, Any +from collections import OrderedDict +from vyos.util import popen, chown, chmod_755, makedir, process_named_running +from vyos.template import render  debug = True @@ -43,163 +237,271 @@ if debug:  else:      logger.setLevel(logging.INFO) - -DATA_DIR = "/var/lib/vyos/" -STATE_FILE = os.path.join(DATA_DIR, "hostsd.state") - -SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" +RUN_DIR = "/run/vyos-hostsd" +STATE_FILE = os.path.join(RUN_DIR, "vyos-hostsd.state") +SOCKET_PATH = "ipc://" + os.path.join(RUN_DIR, 'vyos-hostsd.sock')  RESOLV_CONF_FILE = '/etc/resolv.conf'  HOSTS_FILE = '/etc/hosts' -hosts_tmpl_source = """ -### Autogenerated by VyOS ### -### Do not edit, your changes will get overwritten ### - -# Local host -127.0.0.1       localhost -127.0.1.1       {{ host_name }}{% if domain_name %}.{{ domain_name }} {{ host_name }}{% endif %} - -# The following lines are desirable for IPv6 capable hosts -::1             localhost ip6-localhost ip6-loopback -fe00::0         ip6-localnet -ff00::0         ip6-mcastprefix -ff02::1         ip6-allnodes -ff02::2         ip6-allrouters - -# From DHCP and "system static host-mapping" -{%- if hosts %} -{% for h in hosts -%} -{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %} -{% endfor %} -{%- endif %} -""" - -hosts_tmpl = jinja2.Template(hosts_tmpl_source) - -resolv_tmpl_source = """ -### Autogenerated by VyOS ### -### Do not edit, your changes will get overwritten ### - -# name server from static configuration -{% for ns in name_servers -%} -{%- if name_servers[ns]['tag'] == "static" %} -nameserver {{ns}} -{%- endif %} -{% endfor -%} - -{% for ns in name_servers -%} -{%- if name_servers[ns]['tag'] != "static" %} -# name server from {{name_servers[ns]['tag']}} -nameserver {{ns}} -{%- endif %} -{% endfor -%} - -{%- if domain_name %} -domain {{ domain_name }} -{%- endif %} - -{%- if search_domains %} -search {{ search_domains | join(" ") }} -{%- endif %} - -""" - -resolv_tmpl = jinja2.Template(resolv_tmpl_source) - -# The state data includes a list of name servers -# and a list of hosts entries. -# -# Name servers have the following structure: -# {"server": {"tag": <str>}} -# -# Hosts entries are similar: -# {"host": {"tag": <str>, "address": <str>, "aliases": <str list>}} -# -# The tag is either "static" or "dhcp-<intf>" -# It's used to distinguish entries created -# by different scripts so that they can be removed -# and re-created without having to track what needs -# to be changed +PDNS_REC_USER = PDNS_REC_GROUP = 'pdns' +PDNS_REC_RUN_DIR = '/run/powerdns' +PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua' +PDNS_REC_ZONES_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf' +  STATE = { -    "name_servers": collections.OrderedDict({}), +    "name_servers": {}, +    "name_server_tags_recursor": [], +    "name_server_tags_system": [], +    "forward_zones": {},      "hosts": {},      "host_name": "vyos",      "domain_name": "", -    "search_domains": []} +    "search_domains": {}, +    "changes": 0 +    } + +# the base schema that every received message must be in +base_schema = Schema({ +    Required('op'): Any('add', 'delete', 'set', 'get', 'apply'), +    'type': Any('name_servers', +        'name_server_tags_recursor', 'name_server_tags_system', +        'forward_zones', 'search_domains', 'hosts', 'host_name'), +    'data': Any(list, dict), +    'tag': str, +    'tag_regex': str +    }) + +# more specific schemas +op_schema = Schema({ +    'op': str, +    }, required=True) + +op_type_schema = op_schema.extend({ +    'type': str, +    }, required=True) + +host_name_add_schema = op_type_schema.extend({ +    'data': { +        'host_name': str, +        'domain_name': Any(str, None) +        } +    }, required=True) + +data_dict_list_schema = op_type_schema.extend({ +    'data': { +        str: [str] +        } +    }, required=True) + +data_list_schema = op_type_schema.extend({ +    'data': [str] +    }, required=True) + +tag_regex_schema = op_type_schema.extend({ +    'tag_regex': str +    }, required=True) + +forward_zone_add_schema = op_type_schema.extend({ +    'data': { +        str: { +            'nslist': [str], +            'addNTA': bool, +            'recursion-desired': bool +            } +        } +    }, required=True) + +hosts_add_schema = op_type_schema.extend({ +    'data': { +        str: { +            str: { +            'address': str, +            'aliases': [str] +                } +            } +        } +    }, required=True) + + +# op and type to schema mapping +msg_schema_map = { +    'name_servers': { +        'add': data_dict_list_schema, +        'delete': data_list_schema, +        'get': tag_regex_schema +        }, +    'name_server_tags_recursor': { +        'add': data_list_schema, +        'delete': data_list_schema, +        'get': op_type_schema +        }, +    'name_server_tags_system': { +        'add': data_list_schema, +        'delete': data_list_schema, +        'get': op_type_schema +        }, +    'forward_zones': { +        'add': forward_zone_add_schema, +        'delete': data_list_schema, +        'get': op_type_schema +        }, +    'search_domains': { +        'add': data_dict_list_schema, +        'delete': data_list_schema, +        'get': tag_regex_schema +        }, +    'hosts': { +        'add': hosts_add_schema, +        'delete': data_list_schema, +        'get': tag_regex_schema +        }, +    'host_name': { +        'set': host_name_add_schema +        }, +    None: { +        'apply': op_schema +        } +    } + +def validate_schema(data): +    base_schema(data) + +    try: +        schema = msg_schema_map[data['type'] if 'type' in data else None][data['op']] +        schema(data) +    except KeyError: +        raise ValueError(( +            'Invalid or unknown combination: ' +            f'op: "{data["op"]}", type: "{data["type"]}"')) + + +def pdns_rec_control(command): +    # pdns-r process name is NOT equal to the name shown in ps +    if not process_named_running('pdns-r/worker'): +        logger.info(f'pdns_recursor not running, not sending "{command}"') +        return +    logger.info(f'Running "rec_control {command}"') +    (ret,ret_code) = popen(( +            f"rec_control --socket-dir={PDNS_REC_RUN_DIR} {command}")) +    if ret_code > 0: +        logger.exception(( +            f'"rec_control {command}" failed with exit status {ret_code}, ' +            f'output: "{ret}"')) -def make_resolv_conf(data): -    resolv_conf = resolv_tmpl.render(data) -    logger.info("Writing /etc/resolv.conf") -    with open(RESOLV_CONF_FILE, 'w') as f: -        f.write(resolv_conf) +def make_resolv_conf(state): +    logger.info(f"Writing {RESOLV_CONF_FILE}") +    render(RESOLV_CONF_FILE, 'vyos-hostsd/resolv.conf.tmpl', state, +            user='root', group='root') -def make_hosts_file(state): -    logger.info("Writing /etc/hosts") -    hosts = hosts_tmpl.render(state) -    with open(HOSTS_FILE, 'w') as f: -        f.write(hosts) +def make_hosts(state): +    logger.info(f"Writing {HOSTS_FILE}") +    render(HOSTS_FILE, 'vyos-hostsd/hosts.tmpl', state, +            user='root', group='root') -def add_hosts(data, entries, tag): -    hosts = data['hosts'] +def make_pdns_rec_conf(state): +    logger.info(f"Writing {PDNS_REC_LUA_CONF_FILE}") -    if not entries: -        return +    # on boot, /run/powerdns does not exist, so create it +    makedir(PDNS_REC_RUN_DIR, user=PDNS_REC_USER, group=PDNS_REC_GROUP) +    chmod_755(PDNS_REC_RUN_DIR) -    for e in entries: -        host = e['host'] -        hosts[host] = {} -        hosts[host]['tag'] = tag -        hosts[host]['address'] = e['address'] -        hosts[host]['aliases'] = e['aliases'] +    render(PDNS_REC_LUA_CONF_FILE, +            'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl', +            state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) -def delete_hosts(data, tag): -    hosts = data['hosts'] -    keys_for_deletion = [] +    logger.info(f"Writing {PDNS_REC_ZONES_FILE}") +    render(PDNS_REC_ZONES_FILE, +            'dns-forwarding/recursor.forward-zones.conf.tmpl', +            state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) -    # You can't delete items from a dict while iterating over it, -    # so we build a list of doomed items first -    for h in hosts: -        if hosts[h]['tag'] == tag: -            keys_for_deletion.append(h) +def set_host_name(state, data): +    if data['host_name']: +        state['host_name'] = data['host_name'] +    if 'domain_name' in data: +        state['domain_name'] = data['domain_name'] + +def add_items_to_dict(_dict, items): +    """ +    Dedupes and preserves sort order. +    """ +    assert isinstance(_dict, dict) +    assert isinstance(items, dict) + +    if not items: +        return -    for k in keys_for_deletion: -        del hosts[k] +    _dict.update(items) -def add_name_servers(data, entries, tag): -    name_servers = data['name_servers'] +def add_items_to_dict_as_keys(_dict, items): +    """ +    Added item values are converted to OrderedDict with the value as keys +    and null values. This is to emulate a list but with inherent deduplication. +    Dedupes and preserves sort order. +    """ +    assert isinstance(_dict, dict) +    assert isinstance(items, dict) -    if not entries: +    if not items:          return -    for e in entries: -        name_servers[e] = {} -        name_servers[e]['tag'] = tag +    for item, item_val in items.items(): +        if item not in _dict: +            _dict[item] = OrderedDict({}) +        _dict[item].update(OrderedDict.fromkeys(item_val)) -def delete_name_servers(data, tag): -    name_servers = data['name_servers'] -    regex_filter = re.compile(tag) -    for ns in list(name_servers.keys()): -        if regex_filter.match(name_servers[ns]['tag']): -            del name_servers[ns] +def add_items_to_list(_list, items): +    """ +    Dedupes and preserves sort order. +    """ +    assert isinstance(_list, list) +    assert isinstance(items, list) -def set_host_name(state, data): -    if data['host_name']: -        state['host_name'] = data['host_name'] -    if 'domain_name' in data: -        state['domain_name'] = data['domain_name'] -    if 'search_domains' in data: -        state['search_domains'] = data['search_domains'] - -def get_name_servers(state, tag): -    ns = [] -    data = state['name_servers'] -    regex_filter = re.compile(tag) -    for n in data: -        if regex_filter.match(data[n]['tag']): -            ns.append(n) -    return ns +    if not items: +        return + +    for item in items: +        if item not in _list: +            _list.append(item) + +def delete_items_from_dict(_dict, items): +    """ +    items is a list of keys to delete. +    Doesn't error if the key doesn't exist. +    """ +    assert isinstance(_dict, dict) +    assert isinstance(items, list) + +    for item in items: +        if item in _dict: +            del _dict[item] + +def delete_items_from_list(_list, items): +    """ +    items is a list of items to remove. +    Doesn't error if the key doesn't exist. +    """ +    assert isinstance(_list, list) +    assert isinstance(items, list) + +    for item in items: +        if item in _list: +            _list.remove(item) + +def get_items_from_dict_regex(_dict, item_regex_string): +    """ +    Returns the items whose keys match item_regex_string. +    """ +    assert isinstance(_dict, dict) +    assert isinstance(item_regex_string, str) + +    tmp = {} +    regex = re.compile(item_regex_string) +    for item in _dict: +        if regex.match(item): +            tmp[item] = _dict[item] +    return tmp  def get_option(msg, key):      if key in msg: @@ -207,85 +509,77 @@ def get_option(msg, key):      else:          raise ValueError("Missing required option \"{0}\"".format(key)) -def handle_message(msg_json): -    msg = json.loads(msg_json) - +def handle_message(msg): +    result = None      op = get_option(msg, 'op') -    _type = get_option(msg, 'type') -    changes = 0 +    if op in ['add', 'delete', 'set']: +        STATE['changes'] += 1      if op == 'delete': -        tag = get_option(msg, 'tag') - -        if _type == 'name_servers': -            delete_name_servers(STATE, tag) -            changes += 1 -        elif _type == 'hosts': -            delete_hosts(STATE, tag) -            changes += 1 +        _type = get_option(msg, 'type') +        data = get_option(msg, 'data') +        if _type in ['name_servers', 'forward_zones', 'search_domains', 'hosts']: +            delete_items_from_dict(STATE[_type], data) +        elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: +            delete_items_from_list(STATE[_type], data)          else: -            raise ValueError("Unknown message type {0}".format(_type)) +            raise ValueError(f'Operation "{op}" unknown data type "{_type}"')      elif op == 'add': -        tag = get_option(msg, 'tag') -        entries = get_option(msg, 'data') -        if _type == 'name_servers': -            add_name_servers(STATE, entries, tag) -            changes += 1 -        elif _type == 'hosts': -            add_hosts(STATE, entries, tag) -            changes += 1 +        _type = get_option(msg, 'type') +        data = get_option(msg, 'data') +        if _type in ['name_servers', 'search_domains']: +            add_items_to_dict_as_keys(STATE[_type], data) +        elif _type in ['forward_zones', 'hosts']: +            add_items_to_dict(STATE[_type], data) +            # maybe we need to rec_control clear-nta each domain that was removed here? +        elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: +            add_items_to_list(STATE[_type], data)          else: -            raise ValueError("Unknown message type {0}".format(_type)) +            raise ValueError(f'Operation "{op}" unknown data type "{_type}"')      elif op == 'set': -        # Host name/domain name/search domain are set without a tag, -        # there can be only one anyway +        _type = get_option(msg, 'type')          data = get_option(msg, 'data')          if _type == 'host_name':              set_host_name(STATE, data) -            changes += 1          else: -            raise ValueError("Unknown message type {0}".format(_type)) +            raise ValueError(f'Operation "{op}" unknown data type "{_type}"')      elif op == 'get': -        tag = get_option(msg, 'tag') -        if _type == 'name_servers': -            result = get_name_servers(STATE, tag) +        _type = get_option(msg, 'type') +        if _type in ['name_servers', 'search_domains', 'hosts']: +            tag_regex = get_option(msg, 'tag_regex') +            result = get_items_from_dict_regex(STATE[_type], tag_regex) +        elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones']: +            result = STATE[_type]          else: -            raise ValueError("Unimplemented") -        return result -    else: -        raise ValueError("Unknown operation {0}".format(op)) +            raise ValueError(f'Operation "{op}" unknown data type "{_type}"') +    elif op == 'apply': +        logger.info(f"Applying {STATE['changes']} changes") +        make_resolv_conf(STATE) +        make_hosts(STATE) +        make_pdns_rec_conf(STATE) +        pdns_rec_control('reload-lua-config') +        pdns_rec_control('reload-zones') +        logger.info("Success") +        result = {'message': f'Applied {STATE["changes"]} changes'} +        STATE['changes'] = 0 -    make_resolv_conf(STATE) -    make_hosts_file(STATE) +    else: +        raise ValueError(f"Unknown operation {op}") -    logger.info("Saving state to {0}".format(STATE_FILE)) +    logger.debug(f"Saving state to {STATE_FILE}")      with open(STATE_FILE, 'w') as f:          json.dump(STATE, f) -    if changes > 0: -        if process_named_running("pdns_recursor"): -            (ret,return_code) = popen("sudo rec_control --socket-dir=/run/powerdns reload-zones") -            if return_code > 0: -                logger.exception("PowerDNS rec_control failed to reload") - -def exit_handler(sig, frame): -    """ Clean up the state when shutdown correctly """ -    logger.info("Cleaning up state") -    os.unlink(STATE_FILE) -    sys.exit(0) - +    return result  if __name__ == '__main__': -    signal.signal(signal.SIGTERM, exit_handler) -      # Create a directory for state checkpoints -    os.makedirs(DATA_DIR, exist_ok=True) +    os.makedirs(RUN_DIR, exist_ok=True)      if os.path.exists(STATE_FILE):          with open(STATE_FILE, 'r') as f:              try: -                data = json.load(f) -                STATE = data +                STATE = json.load(f)              except:                  logger.exception(traceback.format_exc())                  logger.exception("Failed to load the state file, using default") @@ -294,28 +588,31 @@ if __name__ == '__main__':      socket = context.socket(zmq.REP)      # Set the right permissions on the socket, then change it back -    o_mask = os.umask(0) +    o_mask = os.umask(0o007)      socket.bind(SOCKET_PATH)      os.umask(o_mask)      while True:          #  Wait for next request from client -        message = socket.recv().decode() -        logger.info("Received a configuration change request") -        logger.debug("Request data: {0}".format(message)) - -        resp = {} +        msg_json = socket.recv().decode() +        logger.debug(f"Request data: {msg_json}")          try: -            result = handle_message(message) -            resp['data'] = result +            msg = json.loads(msg_json) +            validate_schema(msg) + +            resp = {} +            resp['data'] = handle_message(msg)          except ValueError as e:              resp['error'] = str(e) +        except MultipleInvalid as e: +            # raised by schema +            resp['error'] = f'Invalid message: {str(e)}' +            logger.exception(resp['error'])          except:              logger.exception(traceback.format_exc())              resp['error'] = "Internal error" -        logger.debug("Sent response: {0}".format(resp)) -          #  Send reply back to client          socket.send(json.dumps(resp).encode()) +        logger.debug(f"Sent response: {resp}") diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index 57f492401..a062dc810 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -19,7 +19,7 @@ client_name=$2  client_ip=$3  client_mac=$4  domain=$5 -file=/etc/hosts +hostsd_client="/usr/bin/vyos-hostsd-client"  if [ -z "$client_name" ]; then      logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" @@ -36,26 +36,19 @@ fi  case "$action" in    commit) # add mapping for new lease -    grep -q "   $client_search_expr     " $file -    if [ $? == 0 ]; then -       echo host $client_fqdn_name already exists, exiting -       exit 1 -    fi -    # add host -    /usr/bin/vyos-hostsd-client --add-hosts --tag "DHCP-$client_ip" --host "$client_fqdn_name,$client_ip" +    $hostsd_client --add-hosts "$client_fqdn_name,$client_ip" --tag "dhcp-server-$client_ip" --apply +    exit 0      ;;    release) # delete mapping for released address -    # delete host -    /usr/bin/vyos-hostsd-client --delete-hosts --tag "DHCP-$client_ip" +    $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply +    exit 0      ;;    *)      logger -s -t on-dhcp-event "Invalid command \"$1\"" -    exit 1; +    exit 1      ;;  esac  exit 0 - - diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service index e13c66dc6..9aa70a7cc 100644 --- a/src/systemd/isc-dhcp-server.service +++ b/src/systemd/isc-dhcp-server.service @@ -14,10 +14,10 @@ Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhc  PIDFile=/run/dhcp-server/dhcpd.pid  ExecStartPre=/bin/sh -ec '\  touch ${LEASE_FILE}; \ -chown nobody:nogroup ${LEASE_FILE}* ; \ +chown dhcpd:nogroup ${LEASE_FILE}* ; \  chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -4 -t -T -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart=/usr/sbin/dhcpd -4 -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} +/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' +ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}  Restart=always  [Install] diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service index 8ac861d7a..1345c5fc5 100644 --- a/src/systemd/isc-dhcp-server6.service +++ b/src/systemd/isc-dhcp-server6.service @@ -16,8 +16,8 @@ ExecStartPre=/bin/sh -ec '\  touch ${LEASE_FILE}; \  chown nobody:nogroup ${LEASE_FILE}* ; \  chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -6 -t -T -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart=/usr/sbin/dhcpd -6 -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} +/usr/sbin/dhcpd -6 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' +ExecStart=/usr/sbin/dhcpd -6 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}  Restart=always  [Install] diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index 731e570c9..b77335778 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -10,6 +10,9 @@ DefaultDependencies=no  After=systemd-remount-fs.service  [Service] +WorkingDirectory=/run/vyos-hostsd +RuntimeDirectory=vyos-hostsd +RuntimeDirectoryPreserve=yes  ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd  Type=idle  KillMode=process @@ -21,7 +24,7 @@ Restart=on-failure  # Does't work in Jessie but leave it here  User=root -Group=vyattacfg +Group=hostsd  [Install] diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client index d3105c9cf..48ebc83f7 100755 --- a/src/utils/vyos-hostsd-client +++ b/src/utils/vyos-hostsd-client @@ -21,56 +21,139 @@ import argparse  import vyos.hostsd_client -parser = argparse.ArgumentParser() +parser = argparse.ArgumentParser(allow_abbrev=False)  group = parser.add_mutually_exclusive_group() -group.add_argument('--add-hosts', action="store_true") -group.add_argument('--delete-hosts', action="store_true") -group.add_argument('--add-name-servers', action="store_true") -group.add_argument('--delete-name-servers', action="store_true") -group.add_argument('--set-host-name', action="store_true") - -parser.add_argument('--host', type=str, action="append") -parser.add_argument('--name-server', type=str, action="append") -parser.add_argument('--host-name', type=str) + +group.add_argument('--add-name-servers', type=str, nargs='*') +group.add_argument('--delete-name-servers', action='store_true') +group.add_argument('--get-name-servers', type=str, const='.*', nargs='?') + +group.add_argument('--add-name-server-tags-recursor', type=str, nargs='*') +group.add_argument('--delete-name-server-tags-recursor', type=str, nargs='*') +group.add_argument('--get-name-server-tags-recursor', action='store_true') + +group.add_argument('--add-name-server-tags-system', type=str, nargs='*') +group.add_argument('--delete-name-server-tags-system', type=str, nargs='*') +group.add_argument('--get-name-server-tags-system', action='store_true') + +group.add_argument('--add-forward-zone', type=str, nargs='?') +group.add_argument('--delete-forward-zones', type=str, nargs='*') +group.add_argument('--get-forward-zones', action='store_true') + +group.add_argument('--add-search-domains', type=str, nargs='*') +group.add_argument('--delete-search-domains', action='store_true') +group.add_argument('--get-search-domains', type=str, const='.*', nargs='?') + +group.add_argument('--add-hosts', type=str, nargs='*') +group.add_argument('--delete-hosts', action='store_true') +group.add_argument('--get-hosts', type=str, const='.*', nargs='?') + +group.add_argument('--set-host-name', type=str) + +# for --set-host-name  parser.add_argument('--domain-name', type=str) -parser.add_argument('--search-domain', type=str, action="append") + +# for forward zones +parser.add_argument('--nameservers', type=str, nargs='*') +parser.add_argument('--addnta', action='store_true') +parser.add_argument('--recursion-desired', action='store_true')  parser.add_argument('--tag', type=str) +# users must call --apply either in the same command or after they're done +parser.add_argument('--apply', action="store_true") +  args = parser.parse_args()  try:      client = vyos.hostsd_client.Client() +    ops = 1 -    if args.add_hosts: +    if args.add_name_servers:          if not args.tag: -            raise ValueError("Tag is required for this operation") -        data = [] -        for h in args.host: +            raise ValueError("--tag is required for this operation") +        client.add_name_servers({args.tag: args.add_name_servers}) +    elif args.delete_name_servers: +        if not args.tag: +            raise ValueError("--tag is required for this operation") +        client.delete_name_servers([args.tag]) +    elif args.get_name_servers: +        print(client.get_name_servers(args.get_name_servers)) + +    elif args.add_name_server_tags_recursor: +        client.add_name_server_tags_recursor(args.add_name_server_tags_recursor) +    elif args.delete_name_server_tags_recursor: +        client.delete_name_server_tags_recursor(args.delete_name_server_tags_recursor) +    elif args.get_name_server_tags_recursor: +        print(client.get_name_server_tags_recursor()) + +    elif args.add_name_server_tags_system: +        client.add_name_server_tags_system(args.add_name_server_tags_system) +    elif args.delete_name_server_tags_system: +        client.delete_name_server_tags_system(args.delete_name_server_tags_system) +    elif args.get_name_server_tags_system: +        print(client.get_name_server_tags_system()) + +    elif args.add_forward_zone: +        if not args.nameservers: +            raise ValueError("--nameservers is required for this operation") +        client.add_forward_zones( +                { args.add_forward_zone: { +                    'nslist': args.nameservers, +                    'addNTA': args.addnta, +                    'recursion-desired': args.recursion_desired +                    } +                }) +    elif args.delete_forward_zones: +        client.delete_forward_zones(args.delete_forward_zones) +    elif args.get_forward_zones: +        print(client.get_forward_zones()) + +    elif args.add_search_domains: +        if not args.tag: +            raise ValueError("--tag is required for this operation") +        client.add_search_domains({args.tag: args.add_search_domains}) +    elif args.delete_search_domains: +        if not args.tag: +            raise ValueError("--tag is required for this operation") +        client.delete_search_domains([args.tag]) +    elif args.get_search_domains: +        print(client.get_search_domains(args.get_search_domains)) + +    elif args.add_hosts: +        if not args.tag: +            raise ValueError("--tag is required for this operation") +        data = {} +        for h in args.add_hosts:              entry = {}              params = h.split(",")              if len(params) < 2:                  raise ValueError("Malformed host entry") -            entry['host'] = params[0]              entry['address'] = params[1]              entry['aliases'] = params[2:] -            data.append(entry) -        client.add_hosts(args.tag, data) +            data[params[0]] = entry +        client.add_hosts({args.tag: data})      elif args.delete_hosts:          if not args.tag: -            raise ValueError("Tag is required for this operation") -        client.delete_hosts(args.tag) -    elif args.add_name_servers: -        if not args.tag: -            raise ValueError("Tag is required for this operation") -        client.add_name_servers(args.tag, args.name_server) -    elif args.delete_name_servers: -        if not args.tag: -            raise ValueError("Tag is required for this operation") -        client.delete_name_servers(args.tag) +            raise ValueError("--tag is required for this operation") +        client.delete_hosts([args.tag]) +    elif args.get_hosts: +        print(client.get_hosts(args.get_hosts)) +      elif args.set_host_name: -        client.set_host_name(args.host_name, args.domain_name, args.search_domain) +        if not args.domain_name: +            raise ValueError('--domain-name is required for this operation') +        client.set_host_name({'host_name': args.set_host_name, 'domain_name': args.domain_name}) + +    elif args.apply: +        pass      else: +        ops = 0 + +    if args.apply: +        client.apply() + +    if ops == 0:          raise ValueError("Operation required")  except ValueError as e: | 
