summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Jenkinsfile178
-rw-r--r--data/templates/dns-forwarding/recursor.conf.lua.tmpl9
-rw-r--r--data/templates/dns-forwarding/recursor.conf.tmpl21
-rw-r--r--data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl28
-rw-r--r--data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl24
-rw-r--r--data/templates/vyos-hostsd/hosts.tmpl26
-rw-r--r--data/templates/vyos-hostsd/resolv.conf.tmpl26
-rw-r--r--debian/control2
-rw-r--r--debian/vyos-1x.postinst11
-rw-r--r--interface-definitions/dns-domain-name.xml.in20
-rw-r--r--interface-definitions/dns-forwarding.xml.in14
-rw-r--r--interface-definitions/ssh.xml.in2
-rw-r--r--python/setup.py2
-rw-r--r--python/vyos/hostsd_client.py100
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py167
-rwxr-xr-xsrc/conf_mode/host_name.py131
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf53
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup28
-rw-r--r--src/etc/systemd/system/pdns-recursor.service.d/override.conf3
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/2-to-351
-rwxr-xr-xsrc/migration-scripts/system/16-to-1745
-rwxr-xr-xsrc/migration-scripts/system/17-to-18105
-rwxr-xr-xsrc/op_mode/show_interfaces.py12
-rwxr-xr-xsrc/services/vyos-hostsd703
-rwxr-xr-xsrc/system/on-dhcp-event.sh19
-rw-r--r--src/systemd/isc-dhcp-server.service6
-rw-r--r--src/systemd/isc-dhcp-server6.service4
-rw-r--r--src/systemd/vyos-hostsd.service5
-rwxr-xr-xsrc/utils/vyos-hostsd-client141
29 files changed, 1266 insertions, 670 deletions
diff --git a/Jenkinsfile b/Jenkinsfile
index 7a79b0f43..7a760b40b 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,4 +1,4 @@
-// Copyright (C) 2019 VyOS maintainers and contributors
+// Copyright (C) 2020 VyOS maintainers and contributors
//
// This program is free software; you can redistribute it and/or modify
// in order to easy exprort images built to "external" world
@@ -15,174 +15,10 @@
@NonCPS
-def getGitBranchName() {
- def branch = scm.branches[0].name
- return branch.split('/')[-1]
-}
-
-def getGitRepoURL() {
- return scm.userRemoteConfigs[0].url
-}
-
-def getGitRepoName() {
- return getGitRepoURL().split('/').last()
-}
-
-// Returns true if this is a custom build launched on any project fork.
-// Returns false if this is build from git@github.com:vyos/<reponame>.
-// <reponame> can be e.g. vyos-1x.git or vyatta-op.git
-def isCustomBuild() {
- // GitHub organisation base URL
- def gitURI = 'git@github.com:vyos/' + getGitRepoName()
- def httpURI = 'https://github.com/vyos/' + getGitRepoName()
-
- return !((getGitRepoURL() == gitURI) || (getGitRepoURL() == httpURI)) || env.CHANGE_ID
-}
-
-def setDescription() {
- def item = Jenkins.instance.getItemByFullName(env.JOB_NAME)
-
- // build up the main description text
- def description = ""
- description += "<h2>VyOS individual package build: " + getGitRepoName().replace('.git', '') + "</h2>"
-
- if (isCustomBuild()) {
- description += "<p style='border: 3px dashed red; width: 50%;'>"
- description += "<b>Build not started from official Git repository!</b><br>"
- description += "<br>"
- description += "Repository: <font face = 'courier'>" + getGitRepoURL() + "</font><br>"
- description += "Branch: <font face = 'courier'>" + getGitBranchName() + "</font><br>"
- description += "</p>"
- } else {
- description += "Sources taken from Git branch: <font face = 'courier'>" + getGitBranchName() + "</font><br>"
- }
-
- item.setDescription(description)
- item.save()
-}
-
-/* Only keep the 10 most recent builds. */
-def projectProperties = [
- [$class: 'BuildDiscarderProperty',strategy: [$class: 'LogRotator', numToKeepStr: '10']],
-]
-
-properties(projectProperties)
-setDescription()
-
-node('Docker') {
- stage('Define Agent') {
- script {
- // create container name on demand
- def branchName = getGitBranchName()
- // Adjust PR target branch name so we can re-map it to the proper
- // Docker image. CHANGE_ID is set only for pull requests, so it is
- // safe to access the pullRequest global variable
- if (env.CHANGE_ID) {
- branchName = "${env.CHANGE_TARGET}".toLowerCase()
- }
- if (branchName.equals("master")) {
- branchName = "current"
- }
- env.DOCKER_IMAGE = "vyos/vyos-build:" + branchName
- }
- }
-}
-
-pipeline {
- agent {
- docker {
- args "--sysctl net.ipv6.conf.lo.disable_ipv6=0 -e GOSU_UID=1006 -e GOSU_GID=1006"
- image "${env.DOCKER_IMAGE}"
- alwaysPull true
- }
- }
- options {
- disableConcurrentBuilds()
- timeout(time: 30, unit: 'MINUTES')
- timestamps()
- }
- stages {
- stage('Fetch') {
- steps {
- script {
- dir('build') {
- checkout scm
- }
- }
- }
- }
- stage('Build') {
- steps {
- script {
- dir('build') {
- def commitId = sh(returnStdout: true, script: 'git rev-parse --short=11 HEAD').trim()
- currentBuild.description = sprintf('Git SHA1: %s', commitId[-11..-1])
-
- sh 'dpkg-buildpackage -b -us -uc -tc'
- }
- }
- }
- }
- }
- post {
- cleanup {
- deleteDir()
- }
- success {
- script {
- // archive *.deb artifact on custom builds, deploy to repo otherwise
- if ( isCustomBuild()) {
- archiveArtifacts artifacts: '*.deb', fingerprint: true
- } else {
- // publish build result, using SSH-dev.packages.vyos.net Jenkins Credentials
- sshagent(['SSH-dev.packages.vyos.net']) {
- // build up some fancy groovy variables so we do not need to write/copy
- // every option over and over again!
- def RELEASE = getGitBranchName()
- if (getGitBranchName() == "master") {
- RELEASE = 'current'
- }
-
- def VYOS_REPO_PATH = '/home/sentrium/web/dev.packages.vyos.net/public_html/repositories/' + RELEASE + '/'
- if (getGitBranchName() == "crux")
- VYOS_REPO_PATH += 'vyos/'
-
- def SSH_OPTS = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR'
- def SSH_REMOTE = 'khagen@10.217.48.113'
-
- echo "Uploading package(s) and updating package(s) in the repository ..."
-
- files = findFiles(glob: '*.deb')
- files.each { PACKAGE ->
- def ARCH = sh(returnStdout: true, script: "dpkg-deb -f ${PACKAGE} Architecture").trim()
- def SUBSTRING = sh(returnStdout: true, script: "dpkg-deb -f ${PACKAGE} Package").trim()
- def SSH_DIR = '~/VyOS/' + RELEASE + '/' + ARCH
- def ARCH_OPT = ''
- if (ARCH != 'all')
- ARCH_OPT = '-A ' + ARCH
-
- // No need to explicitly check the return code. The pipeline
- // will fail if sh returns a non 0 exit code
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "bash --login -c 'mkdir -p ${SSH_DIR}'"
- """
- sh """
- scp ${SSH_OPTS} ${PACKAGE} ${SSH_REMOTE}:${SSH_DIR}/
- """
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "uncron-add 'reprepro -v -b ${VYOS_REPO_PATH} ${ARCH_OPT} remove ${RELEASE} ${SUBSTRING}'"
- """
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "uncron-add 'reprepro -v -b ${VYOS_REPO_PATH} deleteunreferenced'"
- """
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "uncron-add 'reprepro -v -b ${VYOS_REPO_PATH} ${ARCH_OPT} includedeb ${RELEASE} ${SSH_DIR}/${PACKAGE}'"
- """
- }
- }
- }
- }
- }
- }
-}
+// Using a version specifier library, use 'current' branch. The underscore (_)
+// is not a typo! You need this underscore if the line immediately after the
+// @Library annotation is not an import statement!
+@Library('vyos-build@current')_
+// Start package build using library function from https://github.com/c-po/vyos-build
+buildPackage()
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 e7aeeda93..5e14340a8 100644
--- a/debian/control
+++ b/debian/control
@@ -6,7 +6,6 @@ Build-Depends: debhelper (>= 9),
quilt,
python3,
python3-setuptools,
- quilt,
python3-xmltodict,
python3-lxml,
python3-nose,
@@ -36,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..aaf8bb27d 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 does not 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/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in
index de926a897..4e8cf28eb 100644
--- a/interface-definitions/ssh.xml.in
+++ b/interface-definitions/ssh.xml.in
@@ -5,7 +5,7 @@
<children>
<node name="ssh" owner="${vyos_conf_scripts_dir}/ssh.py">
<properties>
- <help>Secure SHell (SSH) protocol</help>
+ <help>Secure Shell (SSH)</help>
<priority>500</priority>
</properties>
<children>
diff --git a/python/setup.py b/python/setup.py
index 61cb8cb4e..e2d28bd6b 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -10,7 +10,7 @@ def packages(directory):
setup(
name = "vyos",
- version = "1.2.0",
+ version = "1.3.0",
author = "VyOS maintainers and contributors",
author_email = "maintainers@vyos.net",
description = ("VyOS configuration libraries."),
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/16-to-17 b/src/migration-scripts/system/16-to-17
index 981149d1b..8f762c0e2 100755
--- a/src/migration-scripts/system/16-to-17
+++ b/src/migration-scripts/system/16-to-17
@@ -41,31 +41,32 @@ else:
if config.exists(base + ['netconsole']):
config.delete(base + ['netconsole'])
- for device in config.list_nodes(base + ['device']):
- dev_path = base + ['device', device]
- # remove "system console device <device> modem" (T2570)
- if config.exists(dev_path + ['modem']):
- config.delete(dev_path + ['modem'])
+ if config.exists(base + ['device']):
+ for device in config.list_nodes(base + ['device']):
+ dev_path = base + ['device', device]
+ # remove "system console device <device> modem" (T2570)
+ if config.exists(dev_path + ['modem']):
+ config.delete(dev_path + ['modem'])
- # Only continue on USB based serial consoles
- if not 'ttyUSB' in device:
- continue
+ # Only continue on USB based serial consoles
+ if not 'ttyUSB' in device:
+ continue
- # A serial console has been configured but it does no longer
- # exist on the system - cleanup
- if not os.path.exists(f'/dev/{device}'):
- config.delete(dev_path)
- continue
+ # A serial console has been configured but it does no longer
+ # exist on the system - cleanup
+ if not os.path.exists(f'/dev/{device}'):
+ config.delete(dev_path)
+ continue
- # migrate from ttyUSB device to new device in /dev/serial/by-bus
- for root, dirs, files in os.walk('/dev/serial/by-bus'):
- for usb_device in files:
- device_file = os.path.realpath(os.path.join(root, usb_device))
- # migrate to new USB device names (T2529)
- if os.path.basename(device_file) == device:
- config.copy(dev_path, base + ['device', usb_device])
- # Delete old USB node from config
- config.delete(dev_path)
+ # migrate from ttyUSB device to new device in /dev/serial/by-bus
+ for root, dirs, files in os.walk('/dev/serial/by-bus'):
+ for usb_device in files:
+ device_file = os.path.realpath(os.path.join(root, usb_device))
+ # migrate to new USB device names (T2529)
+ if os.path.basename(device_file) == device:
+ config.copy(dev_path, base + ['device', usb_device])
+ # Delete old USB node from config
+ config.delete(dev_path)
try:
with open(file_name, 'w') as f:
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/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
index ebb3508f0..46571c0c0 100755
--- a/src/op_mode/show_interfaces.py
+++ b/src/op_mode/show_interfaces.py
@@ -96,10 +96,14 @@ def split_text(text, used=0):
line = ''
for word in text.split():
- if len(line) + len(word) >= desc_len:
- yield f'{line} {word}'[1:]
- line = ''
- line = f'{line} {word}'
+ if len(line) + len(word) < desc_len:
+ line = f'{line} {word}'
+ continue
+ if line:
+ yield line[1:]
+ else:
+ line = f'{line} {word}'
+
yield line[1:]
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: