diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/dns_forwarding.py | 167 | ||||
-rwxr-xr-x | src/conf_mode/host_name.py | 131 | ||||
-rw-r--r-- | src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf | 53 | ||||
-rw-r--r-- | src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup | 28 | ||||
-rw-r--r-- | src/etc/systemd/system/pdns-recursor.service.d/override.conf | 3 | ||||
-rwxr-xr-x | src/migration-scripts/dns-forwarding/2-to-3 | 51 | ||||
-rwxr-xr-x | src/migration-scripts/system/17-to-18 | 105 | ||||
-rwxr-xr-x | src/services/vyos-hostsd | 703 | ||||
-rwxr-xr-x | src/system/on-dhcp-event.sh | 19 | ||||
-rw-r--r-- | src/systemd/isc-dhcp-server.service | 6 | ||||
-rw-r--r-- | src/systemd/isc-dhcp-server6.service | 4 | ||||
-rw-r--r-- | src/systemd/vyos-hostsd.service | 5 | ||||
-rwxr-xr-x | src/utils/vyos-hostsd-client | 141 |
13 files changed, 997 insertions, 419 deletions
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: |