summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/dhcp-server/kea-dhcp-ddns.conf.j230
-rw-r--r--data/templates/dhcp-server/kea-dhcp4.conf.j213
-rw-r--r--interface-definitions/include/dhcp/ddns-dns-server.xml.i19
-rw-r--r--interface-definitions/include/dhcp/ddns-settings.xml.i172
-rw-r--r--interface-definitions/service_dhcp-server.xml.in121
-rw-r--r--python/vyos/kea.py52
-rwxr-xr-xpython/vyos/template.py70
-rw-r--r--smoketest/config-tests/basic-vyos17
-rw-r--r--smoketest/configs/basic-vyos60
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-server.py130
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py34
-rw-r--r--src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf7
12 files changed, 714 insertions, 11 deletions
diff --git a/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2 b/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2
new file mode 100644
index 000000000..7b0394a88
--- /dev/null
+++ b/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2
@@ -0,0 +1,30 @@
+{
+ "DhcpDdns": {
+ "ip-address": "127.0.0.1",
+ "port": 53001,
+ "control-socket": {
+ "socket-type": "unix",
+ "socket-name": "/run/kea/kea-ddns-ctrl-socket"
+ },
+ "tsig-keys": {{ dynamic_dns_update | kea_dynamic_dns_update_tsig_key_json }},
+ "forward-ddns" : {
+ "ddns-domains": {{ dynamic_dns_update | kea_dynamic_dns_update_domains('forward_domain') }}
+ },
+ "reverse-ddns" : {
+ "ddns-domains": {{ dynamic_dns_update | kea_dynamic_dns_update_domains('reverse_domain') }}
+ },
+ "loggers": [
+ {
+ "name": "kea-dhcp-ddns",
+ "output_options": [
+ {
+ "output": "stdout",
+ "pattern": "%-5p %m\n"
+ }
+ ],
+ "severity": "INFO",
+ "debuglevel": 0
+ }
+ ]
+ }
+}
diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2
index 8d9ffb194..d08ca0eaa 100644
--- a/data/templates/dhcp-server/kea-dhcp4.conf.j2
+++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2
@@ -36,6 +36,19 @@
"space": "ubnt"
}
],
+{% if dynamic_dns_update is vyos_defined %}
+ "dhcp-ddns": {
+ "enable-updates": true,
+ "server-ip": "127.0.0.1",
+ "server-port": 53001,
+ "sender-ip": "",
+ "sender-port": 0,
+ "max-queue-size": 1024,
+ "ncr-protocol": "UDP",
+ "ncr-format": "JSON"
+ },
+ {{ dynamic_dns_update | kea_dynamic_dns_update_main_json }}
+{% endif %}
"hooks-libraries": [
{% if high_availability is vyos_defined %}
{
diff --git a/interface-definitions/include/dhcp/ddns-dns-server.xml.i b/interface-definitions/include/dhcp/ddns-dns-server.xml.i
new file mode 100644
index 000000000..ba9f186d0
--- /dev/null
+++ b/interface-definitions/include/dhcp/ddns-dns-server.xml.i
@@ -0,0 +1,19 @@
+<!-- include start from dhcp/ddns-dns-server.xml.i -->
+<tagNode name="dns-server">
+ <properties>
+ <help>DNS server specification</help>
+ <valueHelp>
+ <format>u32:1-999999</format>
+ <description>Number for this DNS server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>DNS server number must be between 1 and 999999</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-single.xml.i>
+ #include <include/port-number.xml.i>
+ </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/include/dhcp/ddns-settings.xml.i b/interface-definitions/include/dhcp/ddns-settings.xml.i
new file mode 100644
index 000000000..3e202685e
--- /dev/null
+++ b/interface-definitions/include/dhcp/ddns-settings.xml.i
@@ -0,0 +1,172 @@
+<!-- include start from dhcp/ddns-settings.xml.i -->
+<leafNode name="send-updates">
+ <properties>
+ <help>Enable or disable updates for this scope</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable updates for this scope</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable updates for this scope</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="override-client-update">
+ <properties>
+ <help>Always update both forward and reverse DNS data, regardless of the client's request</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Force update both forward and reverse DNS records</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Respect client request settings</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="override-no-update">
+ <properties>
+ <help>Perform a DDNS update, even if the client instructs the server not to</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Force DDNS updates regardless of client request</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Respect client request settings</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="replace-client-name">
+ <properties>
+ <help>Replace client name mode</help>
+ <completionHelp>
+ <list>never always when-present when-not-present</list>
+ </completionHelp>
+ <valueHelp>
+ <format>never</format>
+ <description>Use the name the client sent. If the client sent no name, do not generate
+ one</description>
+ </valueHelp>
+ <valueHelp>
+ <format>always</format>
+ <description>Replace the name the client sent. If the client sent no name, generate one
+ for the client</description>
+ </valueHelp>
+ <valueHelp>
+ <format>when-present</format>
+ <description>Replace the name the client sent. If the client sent no name, do not
+ generate one</description>
+ </valueHelp>
+ <valueHelp>
+ <format>when-not-present</format>
+ <description>Use the name the client sent. If the client sent no name, generate one for
+ the client</description>
+ </valueHelp>
+ <constraint>
+ <regex>(never|always|when-present|when-not-present)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid replace client name mode</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="generated-prefix">
+ <properties>
+ <help>The prefix used in the generation of an FQDN</help>
+ <constraint>
+ <validator name="fqdn" />
+ </constraint>
+ <constraintErrorMessage>Invalid generated prefix</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="qualifying-suffix">
+ <properties>
+ <help>The suffix used when generating an FQDN, or when qualifying a partial name</help>
+ <constraint>
+ <validator name="fqdn" />
+ </constraint>
+ <constraintErrorMessage>Invalid qualifying suffix</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="update-on-renew">
+ <properties>
+ <help>Update DNS record on lease renew</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Update DNS record on lease renew</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Do not update DNS record on lease renew</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="conflict-resolution">
+ <properties>
+ <help>DNS conflict resolution behavior</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable DNS conflict resolution</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable DNS conflict resolution</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="ttl-percent">
+ <properties>
+ <help>Calculate TTL of the DNS record as a percentage of the lease lifetime</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-100" />
+ </constraint>
+ <constraintErrorMessage>Invalid qualifying suffix</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="hostname-char-set">
+ <properties>
+ <help>A regular expression describing the invalid character set in the host name</help>
+ </properties>
+</leafNode>
+<leafNode name="hostname-char-replacement">
+ <properties>
+ <help>A string of zero or more characters with which to replace each invalid character in
+ the host name</help>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in
index c0ab7c048..78f1cea4e 100644
--- a/interface-definitions/service_dhcp-server.xml.in
+++ b/interface-definitions/service_dhcp-server.xml.in
@@ -10,12 +10,111 @@
</properties>
<children>
#include <include/generic-disable-node.xml.i>
- <leafNode name="dynamic-dns-update">
+ <node name="dynamic-dns-update">
<properties>
<help>Dynamically update Domain Name System (RFC4702)</help>
- <valueless/>
</properties>
- </leafNode>
+ <children>
+ #include <include/dhcp/ddns-settings.xml.i>
+ <tagNode name="tsig-key">
+ <properties>
+ <help>TSIG key definition for DNS updates</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, hyphen and underscore</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="algorithm">
+ <properties>
+ <help>TSIG key algorithm</help>
+ <completionHelp>
+ <list>md5 sha1 sha224 sha256 sha384 sha512</list>
+ </completionHelp>
+ <valueHelp>
+ <format>md5</format>
+ <description>MD5 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha1</format>
+ <description>SHA1 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha224</format>
+ <description>SHA224 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha256</format>
+ <description>SHA256 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha384</format>
+ <description>SHA384 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha512</format>
+ <description>SHA512 HMAC algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key algorithm</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="secret">
+ <properties>
+ <help>TSIG key secret (base64-encoded)</help>
+ <constraint>
+ <validator name="base64"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="forward-domain">
+ <properties>
+ <help>Forward DNS domain name</help>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ <constraintErrorMessage>Invalid forward DNS domain name</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="key-name">
+ <properties>
+ <help>TSIG key name for forward DNS updates</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, numbers, hyphen and underscore</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/dhcp/ddns-dns-server.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="reverse-domain">
+ <properties>
+ <help>Reverse DNS domain name</help>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ <constraintErrorMessage>Invalid reverse DNS domain name</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="key-name">
+ <properties>
+ <help>TSIG key name for reverse DNS updates</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, numbers, hyphen and underscore</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/dhcp/ddns-dns-server.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
<node name="high-availability">
<properties>
<help>DHCP high availability configuration</help>
@@ -105,6 +204,14 @@
<constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
<children>
+ <node name="dynamic-dns-update">
+ <properties>
+ <help>Dynamically update Domain Name System (RFC4702)</help>
+ </properties>
+ <children>
+ #include <include/dhcp/ddns-settings.xml.i>
+ </children>
+ </node>
<leafNode name="authoritative">
<properties>
<help>Option to make DHCP server authoritative for this physical network</help>
@@ -132,6 +239,14 @@
#include <include/dhcp/ping-check.xml.i>
#include <include/generic-description.xml.i>
#include <include/generic-disable-node.xml.i>
+ <node name="dynamic-dns-update">
+ <properties>
+ <help>Dynamically update Domain Name System (RFC4702)</help>
+ </properties>
+ <children>
+ #include <include/dhcp/ddns-settings.xml.i>
+ </children>
+ </node>
<leafNode name="exclude">
<properties>
<help>IP address to exclude from DHCP lease range</help>
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 2b0cac7e6..5eecbbaad 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -20,6 +20,7 @@ import socket
from datetime import datetime
from datetime import timezone
+from vyos import ConfigError
from vyos.template import is_ipv6
from vyos.template import netmask_from_cidr
from vyos.utils.dict import dict_search_args
@@ -221,6 +222,9 @@ def kea_parse_subnet(subnet, config):
reservations.append(reservation)
out['reservations'] = reservations
+ if 'dynamic_dns_update' in config:
+ out.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
return out
@@ -350,6 +354,54 @@ def kea6_parse_subnet(subnet, config):
return out
+def kea_parse_tsig_algo(algo_spec):
+ translate = {
+ 'md5': 'HMAC-MD5',
+ 'sha1': 'HMAC-SHA1',
+ 'sha224': 'HMAC-SHA224',
+ 'sha256': 'HMAC-SHA256',
+ 'sha384': 'HMAC-SHA384',
+ 'sha512': 'HMAC-SHA512'
+ }
+ if algo_spec not in translate:
+ raise ConfigError(f'Unsupported TSIG algorithm: {algo_spec}')
+ return translate[algo_spec]
+
+def kea_parse_enable_disable(value):
+ return True if value == 'enable' else False
+
+def kea_parse_ddns_settings(config):
+ data = {}
+
+ if send_updates := config.get('send_updates'):
+ data['ddns-send-updates'] = kea_parse_enable_disable(send_updates)
+
+ if override_client_update := config.get('override_client_update'):
+ data['ddns-override-client-update'] = kea_parse_enable_disable(override_client_update)
+
+ if override_no_update := config.get('override_no_update'):
+ data['ddns-override-no-update'] = kea_parse_enable_disable(override_no_update)
+
+ if update_on_renew := config.get('update_on_renew'):
+ data['ddns-update-on-renew'] = kea_parse_enable_disable(update_on_renew)
+
+ if conflict_resolution := config.get('conflict_resolution'):
+ data['ddns-use-conflict-resolution'] = kea_parse_enable_disable(conflict_resolution)
+
+ if 'replace_client_name' in config:
+ data['ddns-replace-client-name'] = config['replace_client_name']
+ if 'generated_prefix' in config:
+ data['ddns-generated-prefix'] = config['generated_prefix']
+ if 'qualifying_suffix' in config:
+ data['ddns-qualifying-suffix'] = config['qualifying_suffix']
+ if 'ttl_percent' in config:
+ data['ddns-ttl-percent'] = int(config['ttl_percent']) / 100
+ if 'hostname_char_set' in config:
+ data['hostname-char-set'] = config['hostname_char_set']
+ if 'hostname_char_replacement' in config:
+ data['hostname-char-replacement'] = config['hostname_char_replacement']
+
+ return data
def _ctrl_socket_command(inet, command, args=None):
path = kea_ctrl_socket.format(inet=inet)
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 7ba85a046..d79e1183f 100755
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -859,10 +859,77 @@ def kea_high_availability_json(config):
return dumps(data)
+@register_filter('kea_dynamic_dns_update_main_json')
+def kea_dynamic_dns_update_main_json(config):
+ from vyos.kea import kea_parse_ddns_settings
+ from json import dumps
+
+ data = kea_parse_ddns_settings(config)
+
+ if len(data) == 0:
+ return ''
+
+ return dumps(data, indent=8)[1:-1] + ','
+
+@register_filter('kea_dynamic_dns_update_tsig_key_json')
+def kea_dynamic_dns_update_tsig_key_json(config):
+ from vyos.kea import kea_parse_tsig_algo
+ from json import dumps
+ out = []
+
+ if 'tsig_key' not in config:
+ return dumps(out)
+
+ tsig_keys = config['tsig_key']
+
+ for tsig_key_name, tsig_key_config in tsig_keys.items():
+ tsig_key = {
+ 'name': tsig_key_name,
+ 'algorithm': kea_parse_tsig_algo(tsig_key_config['algorithm']),
+ 'secret': tsig_key_config['secret']
+ }
+ out.append(tsig_key)
+
+ return dumps(out, indent=12)
+
+@register_filter('kea_dynamic_dns_update_domains')
+def kea_dynamic_dns_update_domains(config, type_key):
+ from json import dumps
+ out = []
+
+ if type_key not in config:
+ return dumps(out)
+
+ domains = config[type_key]
+
+ for domain_name, domain_config in domains.items():
+ domain = {
+ 'name': domain_name,
+
+ }
+ if 'key_name' in domain_config:
+ domain['key-name'] = domain_config['key_name']
+
+ if 'dns_server' in domain_config:
+ dns_servers = []
+ for dns_server_config in domain_config['dns_server'].values():
+ dns_server = {
+ 'ip-address': dns_server_config['address']
+ }
+ if 'port' in dns_server_config:
+ dns_server['port'] = int(dns_server_config['port'])
+ dns_servers.append(dns_server)
+ domain['dns-servers'] = dns_servers
+
+ out.append(domain)
+
+ return dumps(out, indent=12)
+
@register_filter('kea_shared_network_json')
def kea_shared_network_json(shared_networks):
from vyos.kea import kea_parse_options
from vyos.kea import kea_parse_subnet
+ from vyos.kea import kea_parse_ddns_settings
from json import dumps
out = []
@@ -877,6 +944,9 @@ def kea_shared_network_json(shared_networks):
'user-context': {}
}
+ if 'dynamic_dns_update' in config:
+ network.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
if 'option' in config:
network['option-data'] = kea_parse_options(config['option'])
diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos
index 4793e069e..aaf450e80 100644
--- a/smoketest/config-tests/basic-vyos
+++ b/smoketest/config-tests/basic-vyos
@@ -28,7 +28,21 @@ set protocols static arp interface eth2.200.201 address 100.64.201.20 mac '00:50
set protocols static arp interface eth2.200.202 address 100.64.202.30 mac '00:50:00:00:00:30'
set protocols static arp interface eth2.200.202 address 100.64.202.40 mac '00:50:00:00:00:40'
set protocols static route 0.0.0.0/0 next-hop 100.64.0.1
+set service dhcp-server dynamic-dns-update send-updates 'enable'
+set service dhcp-server dynamic-dns-update conflict-resolution 'enable'
+set service dhcp-server dynamic-dns-update tsig-key domain-lan-updates algorithm 'sha256'
+set service dhcp-server dynamic-dns-update tsig-key domain-lan-updates secret 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='
+set service dhcp-server dynamic-dns-update tsig-key reverse-0-168-192 algorithm 'sha256'
+set service dhcp-server dynamic-dns-update tsig-key reverse-0-168-192 secret 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='
+set service dhcp-server dynamic-dns-update forward-domain domain.lan dns-server 1 address '192.168.0.1'
+set service dhcp-server dynamic-dns-update forward-domain domain.lan dns-server 2 address '100.100.0.1'
+set service dhcp-server dynamic-dns-update forward-domain domain.lan key-name 'domain-lan-updates'
+set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa dns-server 1 address '192.168.0.1'
+set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa dns-server 2 address '100.100.0.1'
+set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa key-name 'reverse-0-168-192'
set service dhcp-server shared-network-name LAN authoritative
+set service dhcp-server shared-network-name LAN dynamic-dns-update send-updates 'enable'
+set service dhcp-server shared-network-name LAN dynamic-dns-update ttl-percent '75'
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1'
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-name 'vyos.net'
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-search 'vyos.net'
@@ -46,6 +60,9 @@ set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-map
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping TEST2-2 ip-address '192.168.0.21'
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping TEST2-2 mac '00:01:02:03:04:22'
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update send-updates 'enable'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update generated-prefix 'myhost'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update qualifying-suffix 'lan1.domain.lan'
set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 interface 'eth0'
set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 option domain-search 'vyos.net'
set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 option name-server 'fe88::1'
diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos
index a6cd3b6e1..5f7a71237 100644
--- a/smoketest/configs/basic-vyos
+++ b/smoketest/configs/basic-vyos
@@ -99,33 +99,77 @@ protocols {
}
service {
dhcp-server {
+ dynamic-dns-update {
+ send-updates enable
+ forward-domain domain.lan {
+ dns-server 1 {
+ address 192.168.0.1
+ }
+ dns-server 2 {
+ address 100.100.0.1
+ }
+ key-name domain-lan-updates
+ }
+ reverse-domain 0.168.192.in-addr.arpa {
+ dns-server 1 {
+ address 192.168.0.1
+ }
+ dns-server 2 {
+ address 100.100.0.1
+ }
+ key-name reverse-0-168-192
+ }
+ tsig-key domain-lan-updates {
+ algorithm sha256
+ secret SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ==
+ }
+ tsig-key reverse-0-168-192 {
+ algorithm sha256
+ secret VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ==
+ }
+ conflict-resolution enable
+ }
shared-network-name LAN {
authoritative
+ dynamic-dns-update {
+ send-updates enable
+ ttl-percent 75
+ }
subnet 192.168.0.0/24 {
- default-router 192.168.0.1
- dns-server 192.168.0.1
- domain-name vyos.net
- domain-search vyos.net
+ dynamic-dns-update {
+ send-updates enable
+ generated-prefix myhost
+ qualifying-suffix lan1.domain.lan
+ }
+ option {
+ default-router 192.168.0.1
+ domain-name vyos.net
+ domain-search vyos.net
+ name-server 192.168.0.1
+ }
range LANDynamic {
start 192.168.0.30
stop 192.168.0.240
}
static-mapping TEST1-1 {
ip-address 192.168.0.11
- mac-address 00:01:02:03:04:05
+ mac 00:01:02:03:04:05
}
static-mapping TEST1-2 {
+ disable
ip-address 192.168.0.12
- mac-address 00:01:02:03:04:05
+ mac 00:01:02:03:04:05
}
static-mapping TEST2-1 {
ip-address 192.168.0.21
- mac-address 00:01:02:03:04:21
+ mac 00:01:02:03:04:21
}
static-mapping TEST2-2 {
+ disable
ip-address 192.168.0.21
- mac-address 00:01:02:03:04:22
+ mac 00:01:02:03:04:22
}
+ subnet-id 1
}
}
}
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index 935be6227..e421f04d2 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -32,7 +32,10 @@ from vyos.template import inc_ip
from vyos.template import dec_ip
PROCESS_NAME = 'kea-dhcp4'
+D2_PROCESS_NAME = 'kea-dhcp-ddns'
+CTRL_PROCESS_NAME = 'kea-ctrl-agent'
KEA4_CONF = '/run/kea/kea-dhcp4.conf'
+KEA4_D2_CONF = '/run/kea/kea-dhcp-ddns.conf'
KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket'
HOSTSD_CLIENT = '/usr/bin/vyos-hostsd-client'
base_path = ['service', 'dhcp-server']
@@ -1116,6 +1119,133 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
# Check for running process
self.verify_service_running()
+ def test_dhcp_dynamic_dns_update(self):
+ shared_net_name = 'SMOKE-1DDNS'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+
+ self.cli_set(base_path + ['listen-interface', interface])
+
+ ddns = base_path + ['dynamic-dns-update']
+
+ self.cli_set(ddns + ['send-updates', 'enable'])
+ self.cli_set(ddns + ['conflict-resolution', 'enable'])
+ self.cli_set(ddns + ['override-no-update', 'enable'])
+ self.cli_set(ddns + ['override-client-update', 'enable'])
+ self.cli_set(ddns + ['replace-client-name', 'always'])
+ self.cli_set(ddns + ['update-on-renew', 'enable'])
+
+ self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'algorithm', 'sha256'])
+ self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'secret', 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='])
+ self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'algorithm', 'sha256'])
+ self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'secret', 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='])
+ self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '1', 'address', '192.168.0.1'])
+ self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '2', 'address', '100.100.0.1'])
+ self.cli_set(ddns + ['forward-domain', 'domain.lan', 'key-name', 'domain-lan-updates'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'address', '192.168.0.1'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'port', '1053'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'address', '100.100.0.1'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'port', '1153'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'key-name', 'reverse-0-168-192'])
+
+ shared = base_path + ['shared-network-name', shared_net_name]
+
+ self.cli_set(shared + ['dynamic-dns-update', 'send-updates', 'enable'])
+ self.cli_set(shared + ['dynamic-dns-update', 'conflict-resolution', 'enable'])
+ self.cli_set(shared + ['dynamic-dns-update', 'ttl-percent', '75'])
+
+ pool = shared + [ 'subnet', subnet]
+
+ self.cli_set(pool + ['subnet-id', '1'])
+
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ self.cli_set(pool + ['dynamic-dns-update', 'send-updates', 'enable'])
+ self.cli_set(pool + ['dynamic-dns-update', 'generated-prefix', 'myfunnyprefix'])
+ self.cli_set(pool + ['dynamic-dns-update', 'qualifying-suffix', 'suffix.lan'])
+ self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-set', 'xXyYzZ'])
+ self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-replacement', '_xXx_'])
+
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ d2_config = read_file(KEA4_D2_CONF)
+
+ obj = loads(config)
+ d2_obj = loads(d2_config)
+
+ # Verify global DDNS parameters in the main config file
+ self.verify_config_value(
+ obj,
+ ['Dhcp4'], 'dhcp-ddns',
+ {'enable-updates': True, 'server-ip': '127.0.0.1', 'server-port': 53001, 'sender-ip': '', 'sender-port': 0,
+ 'max-queue-size': 1024, 'ncr-protocol': 'UDP', 'ncr-format': 'JSON'})
+
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-send-updates', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-use-conflict-resolution', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-no-update', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-client-update', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-replace-client-name', 'always')
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-update-on-renew', True)
+
+ # Verify scoped DDNS parameters in the main config file
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-send-updates', True)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-use-conflict-resolution', True)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-ttl-percent', 0.75)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-send-updates', True)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-generated-prefix', 'myfunnyprefix')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-qualifying-suffix', 'suffix.lan')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-set', 'xXyYzZ')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-replacement', '_xXx_')
+
+ # Verify keys and domains configuration in the D2 config
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'tsig-keys'],
+ {'name': 'domain-lan-updates', 'algorithm': 'HMAC-SHA256', 'secret': 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='}
+ )
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'tsig-keys'],
+ {'name': 'reverse-0-168-192', 'algorithm': 'HMAC-SHA256', 'secret': 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='}
+ )
+
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'name', 'domain.lan')
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'key-name', 'domain-lan-updates')
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '192.168.0.1'}
+ )
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '100.100.0.1'}
+ )
+
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'name', '0.168.192.in-addr.arpa')
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'key-name', 'reverse-0-168-192')
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '192.168.0.1', 'port': 1053}
+ )
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '100.100.0.1', 'port': 1153}
+ )
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertTrue(process_named_running(D2_PROCESS_NAME))
+
def test_dhcp_on_interface_with_vrf(self):
self.cli_set(['interfaces', 'ethernet', 'eth1', 'address', '10.1.1.1/30'])
self.cli_set(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP'])
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index e46d916fd..99c7e6a1f 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -43,6 +43,7 @@ airbag.enable()
ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
config_file = '/run/kea/kea-dhcp4.conf'
+config_file_d2 = '/run/kea/kea-dhcp-ddns.conf'
lease_file = '/config/dhcp/dhcp4-leases.csv'
lease_file_glob = '/config/dhcp/dhcp4-leases*'
user_group = '_kea'
@@ -170,6 +171,15 @@ def get_config(config=None):
return dhcp
+def verify_ddns_domain_servers(domain_type, domain):
+ if 'dns_server' in domain:
+ invalid_servers = []
+ for server_no, server_config in domain['dns_server'].items():
+ if 'address' not in server_config:
+ invalid_servers.append(server_no)
+ if len(invalid_servers) > 0:
+ raise ConfigError(f'{domain_type} DNS servers {", ".join(invalid_servers)} in DDNS configuration need to have an IP address')
+ return None
def verify(dhcp):
# bail out early - looks like removal from running config
@@ -422,6 +432,22 @@ def verify(dhcp):
if not interface_exists(interface):
raise ConfigError(f'listen-interface "{interface}" does not exist')
+ if 'dynamic_dns_update' in dhcp:
+ ddns = dhcp['dynamic_dns_update']
+ if 'tsig_key' in ddns:
+ invalid_keys = []
+ for tsig_key_name, tsig_key_config in ddns['tsig_key'].items():
+ if not ('algorithm' in tsig_key_config and 'secret' in tsig_key_config):
+ invalid_keys.append(tsig_key_name)
+ if len(invalid_keys) > 0:
+ raise ConfigError(f'Both algorithm and secret need to be set for TSIG keys: {", ".join(invalid_keys)}')
+
+ if 'forward_domain' in ddns:
+ verify_ddns_domain_servers('Forward', ddns['forward_domain'])
+
+ if 'reverse_domain' in ddns:
+ verify_ddns_domain_servers('Reverse', ddns['reverse_domain'])
+
return None
@@ -485,6 +511,14 @@ def generate(dhcp):
user=user_group,
group=user_group,
)
+ if 'dynamic_dns_update' in dhcp:
+ render(
+ config_file_d2,
+ 'dhcp-server/kea-dhcp-ddns.conf.j2',
+ dhcp,
+ user=user_group,
+ group=user_group
+ )
return None
diff --git a/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf
new file mode 100644
index 000000000..cdfdea8eb
--- /dev/null
+++ b/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf
@@ -0,0 +1,7 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/kea-dhcp-ddns -c /run/kea/kea-dhcp-ddns.conf