diff options
58 files changed, 1379 insertions, 756 deletions
diff --git a/.github/workflows/trigger-pr-mirror-repo-sync.yml b/.github/workflows/trigger-pr-mirror-repo-sync.yml index f74895987..978be0582 100644 --- a/.github/workflows/trigger-pr-mirror-repo-sync.yml +++ b/.github/workflows/trigger-pr-mirror-repo-sync.yml @@ -6,6 +6,11 @@ on: branches: - current +permissions: + pull-requests: write + contents: write + issues: write + jobs: call-trigger-mirror-pr-repo-sync: if: github.repository_owner == 'vyos' && github.event.pull_request.merged == true @@ -7,7 +7,7 @@ LIBS := -lzmq CFLAGS := BUILD_ARCH := $(shell dpkg-architecture -q DEB_BUILD_ARCH) J2LINT := $(shell command -v j2lint 2> /dev/null) -PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts) +PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts src/services) LIBVYOSCONFIG_BUILD_PATH := /tmp/libvyosconfig/_build/libvyosconfig.so LIBVYOSCONFIG_STATUS := $(shell git submodule status) diff --git a/data/templates/dhcp-client/ipv6.override.conf.j2 b/data/templates/dhcp-client/ipv6.override.conf.j2 index b0c0e0544..d270a55fc 100644 --- a/data/templates/dhcp-client/ipv6.override.conf.j2 +++ b/data/templates/dhcp-client/ipv6.override.conf.j2 @@ -4,6 +4,9 @@ [Unit] ConditionPathExists={{ dhcp6_client_dir }}/dhcp6c.%i.conf +{% if ifname.startswith('pppoe') %} +After=ppp@{{ ifname }}.service +{% endif %} [Service] ExecStart= diff --git a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 deleted file mode 100644 index b37cf4798..000000000 --- a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Control-agent": { -{% if high_availability is vyos_defined %} - "http-host": "{{ high_availability.source_address }}", - "http-port": 647, - "control-sockets": { - "dhcp4": { - "socket-type": "unix", - "socket-name": "/run/kea/dhcp4-ctrl-socket" - } - } -{% endif %} - } -} 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 2e10d58e0..d08ca0eaa 100644 --- a/data/templates/dhcp-server/kea-dhcp4.conf.j2 +++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2 @@ -25,20 +25,6 @@ }, "option-def": [ { - "name": "rfc3442-static-route", - "code": 121, - "type": "record", - "array": true, - "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8" - }, - { - "name": "windows-static-route", - "code": 249, - "type": "record", - "array": true, - "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8" - }, - { "name": "wpad-url", "code": 252, "type": "string" @@ -50,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 %} { @@ -69,6 +68,16 @@ }, {% endif %} { + "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_ping_check.so", + "parameters": { + "enable-ping-check" : false, + "min-ping-requests" : 1, + "reply-timeout" : 100, + "ping-cltt-secs" : 60, + "ping-channel-threads" : 0 + } + }, + { "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so", "parameters": {} } diff --git a/data/templates/ids/fastnetmon.j2 b/data/templates/ids/fastnetmon.j2 deleted file mode 100644 index f6f03d0db..000000000 --- a/data/templates/ids/fastnetmon.j2 +++ /dev/null @@ -1,121 +0,0 @@ -# enable this option if you want to send logs to local syslog facility -logging:logging_level = debug -logging:local_syslog_logging = on - -# list of all your networks in CIDR format -networks_list_path = /run/fastnetmon/networks_list - -# list networks in CIDR format which will be not monitored for attacks -white_list_path = /run/fastnetmon/excluded_networks_list - -# Enable/Disable any actions in case of attack -enable_ban = on -enable_ban_ipv6 = on - -## How many packets will be collected from attack traffic -ban_details_records_count = 500 - -## How long (in seconds) we should keep an IP in blocked state -## If you set 0 here it completely disables unban capability -{% if ban_time is vyos_defined %} -ban_time = {{ ban_time }} -{% endif %} - -# Check if the attack is still active, before triggering an unban callback with this option -# If the attack is still active, check each run of the unban watchdog -unban_only_if_attack_finished = on - -# enable per subnet speed meters -# For each subnet, list track speed in bps and pps for both directions -enable_subnet_counters = off - -{% if mode is vyos_defined('mirror') %} -mirror_afpacket = on -{% elif mode is vyos_defined('sflow') %} -sflow = on -{% if sflow.port is vyos_defined %} -sflow_port = {{ sflow.port }} -{% endif %} -{% if sflow.listen_address is vyos_defined %} -sflow_host = {{ sflow.listen_address }} -{% endif %} -{% endif %} - - -process_incoming_traffic = {{ 'on' if direction is vyos_defined and 'in' in direction else 'off' }} -process_outgoing_traffic = {{ 'on' if direction is vyos_defined and 'out' in direction else 'off' }} - -{% if threshold is vyos_defined %} -{% if threshold.general is vyos_defined %} -# General threshold -{% for thr, thr_value in threshold.general.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_flows = on -threshold_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_bandwidth = on -threshold_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_pps = on -threshold_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% if threshold.tcp is vyos_defined %} -# TCP threshold -{% for thr, thr_value in threshold.tcp.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_tcp_flows = on -threshold_tcp_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_tcp_bandwidth = on -threshold_tcp_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_tcp_pps = on -threshold_tcp_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% if threshold.udp is vyos_defined %} -# UDP threshold -{% for thr, thr_value in threshold.udp.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_udp_flows = on -threshold_udp_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_udp_bandwidth = on -threshold_udp_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_udp_pps = on -threshold_udp_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% if threshold.icmp is vyos_defined %} -# ICMP threshold -{% for thr, thr_value in threshold.icmp.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_icmp_flows = on -threshold_icmp_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_icmp_bandwidth = on -threshold_icmp_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_icmp_pps = on -threshold_icmp_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% endif %} - -{% if listen_interface is vyos_defined %} -interfaces = {{ listen_interface | join(',') }} -{% endif %} - -{% if alert_script is vyos_defined %} -notify_script_path = {{ alert_script }} -{% endif %} diff --git a/data/templates/ids/fastnetmon_excluded_networks_list.j2 b/data/templates/ids/fastnetmon_excluded_networks_list.j2 deleted file mode 100644 index c88a1c527..000000000 --- a/data/templates/ids/fastnetmon_excluded_networks_list.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% if excluded_network is vyos_defined %} -{% for net in excluded_network %} -{{ net }} -{% endfor %} -{% endif %} diff --git a/data/templates/ids/fastnetmon_networks_list.j2 b/data/templates/ids/fastnetmon_networks_list.j2 deleted file mode 100644 index 0a0576d2a..000000000 --- a/data/templates/ids/fastnetmon_networks_list.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% if network is vyos_defined %} -{% for net in network %} -{{ net }} -{% endfor %} -{% endif %} diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index 3a9af2c94..cf0865c88 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -68,8 +68,19 @@ rekey_packets = 0 rekey_time = 0s {% endif %} - local_ts = 0.0.0.0/0,::/0 - remote_ts = 0.0.0.0/0,::/0 +{# set default traffic-selectors #} +{% set local_ts = '0.0.0.0/0,::/0' %} +{% set remote_ts = '0.0.0.0/0,::/0' %} +{% if peer_conf.vti.traffic_selector is vyos_defined %} +{% if peer_conf.vti.traffic_selector.local is vyos_defined and peer_conf.vti.traffic_selector.local.prefix is vyos_defined %} +{% set local_ts = peer_conf.vti.traffic_selector.local.prefix | join(',') %} +{% endif %} +{% if peer_conf.vti.traffic_selector.remote is vyos_defined and peer_conf.vti.traffic_selector.remote.prefix is vyos_defined %} +{% set remote_ts = peer_conf.vti.traffic_selector.remote.prefix | join(',') %} +{% endif %} +{% endif %} + local_ts = {{ local_ts }} + remote_ts = {{ remote_ts }} updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}" {# The key defaults to 0 and will match any policies which similarly do not have a lookup key configuration. #} {# Thus we simply shift the key by one to also support a vti0 interface #} diff --git a/debian/control b/debian/control index 4186dfb3b..ffa21f840 100644 --- a/debian/control +++ b/debian/control @@ -41,8 +41,8 @@ Pre-Depends: libpam-runtime [amd64], libnss-tacplus [amd64], libpam-tacplus [amd64], - libpam-radius-auth (= 1.5.0-cl3u7) [amd64], - libnss-mapuser (= 1.1.0-cl3u3) [amd64], + vyos-libpam-radius-auth, + vyos-libnss-mapuser, tzdata (>= 2025b) Depends: ## Fundamentals @@ -196,7 +196,6 @@ Depends: ddclient (>= 3.11.1), # End "service dns dynamic" # # For "service ids" - fastnetmon [amd64], suricata, suricata-update, # End "service ids" diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index fde58651a..798ecaa1b 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -221,11 +221,9 @@ fi # Remove unwanted daemon files from /etc # conntackd # pmacct -# fastnetmon # ntp DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd /etc/default/pmacctd /etc/pmacct - /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf /etc/ntp.conf /etc/default/ssh /etc/avahi/avahi-daemon.conf /etc/avahi/hosts /etc/powerdns /etc/default/pdns-recursor /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns" 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/include/dhcp/option-v4.xml.i b/interface-definitions/include/dhcp/option-v4.xml.i index bd6fc6043..08fbcca4a 100644 --- a/interface-definitions/include/dhcp/option-v4.xml.i +++ b/interface-definitions/include/dhcp/option-v4.xml.i @@ -59,6 +59,18 @@ <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> </properties> </leafNode> + <leafNode name="capwap-controller"> + <properties> + <help>IP address of CAPWAP access controller (Option 138)</help> + <valueHelp> + <format>ipv4</format> + <description>CAPWAP AC controller</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> <leafNode name="default-router"> <properties> <help>IP address of default router</help> diff --git a/interface-definitions/include/dhcp/option-v6.xml.i b/interface-definitions/include/dhcp/option-v6.xml.i index e1897f52d..202843ddf 100644 --- a/interface-definitions/include/dhcp/option-v6.xml.i +++ b/interface-definitions/include/dhcp/option-v6.xml.i @@ -7,6 +7,18 @@ #include <include/dhcp/captive-portal.xml.i> #include <include/dhcp/domain-search.xml.i> #include <include/name-server-ipv6.xml.i> + <leafNode name="capwap-controller"> + <properties> + <help>IP address of CAPWAP access controller (Option 52)</help> + <valueHelp> + <format>ipv6</format> + <description>CAPWAP AC controller</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> <leafNode name="nis-domain"> <properties> <help>NIS domain name for client to use</help> diff --git a/interface-definitions/include/dhcp/ping-check.xml.i b/interface-definitions/include/dhcp/ping-check.xml.i new file mode 100644 index 000000000..a506f68e4 --- /dev/null +++ b/interface-definitions/include/dhcp/ping-check.xml.i @@ -0,0 +1,8 @@ +<!-- include start from dhcp/ping-check.xml.i --> +<leafNode name="ping-check"> + <properties> + <help>Sends ICMP Echo request to the address being assigned</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/ids-version.xml.i b/interface-definitions/include/version/ids-version.xml.i index 9133be02b..6d4e92c21 100644 --- a/interface-definitions/include/version/ids-version.xml.i +++ b/interface-definitions/include/version/ids-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ids-version.xml.i --> -<syntaxVersion component='ids' version='1'></syntaxVersion> +<syntaxVersion component='ids' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in index 9a194de4f..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> @@ -112,6 +219,7 @@ </properties> </leafNode> #include <include/dhcp/option-v4.xml.i> + #include <include/dhcp/ping-check.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> <tagNode name="subnet"> @@ -128,8 +236,17 @@ </properties> <children> #include <include/dhcp/option-v4.xml.i> + #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/interface-definitions/service_ids_ddos-protection.xml.in b/interface-definitions/service_ids_ddos-protection.xml.in deleted file mode 100644 index 3ef2640b3..000000000 --- a/interface-definitions/service_ids_ddos-protection.xml.in +++ /dev/null @@ -1,167 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="service"> - <children> - <node name="ids"> - <properties> - <help>Intrusion Detection System</help> - </properties> - <children> - <node name="ddos-protection" owner="${vyos_conf_scripts_dir}/service_ids_ddos-protection.py"> - <properties> - <help>FastNetMon detection and protection parameters</help> - <priority>731</priority> - </properties> - <children> - <leafNode name="alert-script"> - <properties> - <help>Path to fastnetmon alert script</help> - </properties> - </leafNode> - <leafNode name="ban-time"> - <properties> - <help>How long we should keep an IP in blocked state</help> - <valueHelp> - <format>u32:1-4294967294</format> - <description>Time in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967294"/> - </constraint> - </properties> - <defaultValue>1900</defaultValue> - </leafNode> - <leafNode name="direction"> - <properties> - <help>Direction for processing traffic</help> - <completionHelp> - <list>in out</list> - </completionHelp> - <constraint> - <regex>(in|out)</regex> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="excluded-network"> - <properties> - <help>Specify IPv4 and IPv6 networks which are going to be excluded from protection</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 prefix(es) to exclude</description> - </valueHelp> - <valueHelp> - <format>ipv6net</format> - <description>IPv6 prefix(es) to exclude</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - <validator name="ipv6-prefix"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="listen-interface"> - <properties> - <help>Listen interface for mirroring traffic</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - <multi/> - </properties> - </leafNode> - <leafNode name="mode"> - <properties> - <help>Traffic capture mode</help> - <completionHelp> - <list>mirror sflow</list> - </completionHelp> - <valueHelp> - <format>mirror</format> - <description>Listen to mirrored traffic</description> - </valueHelp> - <valueHelp> - <format>sflow</format> - <description>Capture sFlow flows</description> - </valueHelp> - <constraint> - <regex>(mirror|sflow)</regex> - </constraint> - </properties> - </leafNode> - <node name="sflow"> - <properties> - <help>Sflow settings</help> - </properties> - <children> - #include <include/listen-address-ipv4-single.xml.i> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>6343</defaultValue> - </leafNode> - </children> - </node> - <leafNode name="network"> - <properties> - <help>Specify IPv4 and IPv6 networks which belong to you</help> - <valueHelp> - <format>ipv4net</format> - <description>Your IPv4 prefix(es)</description> - </valueHelp> - <valueHelp> - <format>ipv6net</format> - <description>Your IPv6 prefix(es)</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - <validator name="ipv6-prefix"/> - </constraint> - <multi/> - </properties> - </leafNode> - <node name="threshold"> - <properties> - <help>Attack limits thresholds</help> - </properties> - <children> - <node name="general"> - <properties> - <help>General threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - <node name="tcp"> - <properties> - <help>TCP threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - <node name="udp"> - <properties> - <help>UDP threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - <node name="icmp"> - <properties> - <help>ICMP threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - </children> - </node> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 0cf526fad..873a4f882 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -1244,6 +1244,63 @@ <children> #include <include/ipsec/bind.xml.i> #include <include/ipsec/esp-group.xml.i> + <node name="traffic-selector"> + <properties> + <help>Traffic-selectors parameters</help> + </properties> + <children> + <node name="local"> + <properties> + <help>Local parameters for interesting traffic</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>Local IPv4 or IPv6 prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>Local IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>Local IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="remote"> + <properties> + <help>Remote parameters for interesting traffic</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>Remote IPv4 or IPv6 prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>Remote IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>Remote IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> </children> </node> </children> diff --git a/libvyosconfig b/libvyosconfig -Subproject 58dbb42e827e3d326c6e0e9470334d4d5c7c396 +Subproject 1dedc69476d707718031c45b53b626da8badf86 diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index b9ef8f48e..91e1c93ef 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -17,19 +17,6 @@ </properties> <command>SYSTEMD_COLORS=false grc journalctl --no-hostname --follow --boot</command> </node> - <node name="ids"> - <properties> - <help>Monitor Intrusion Detection System log</help> - </properties> - <children> - <leafNode name="ddos-protection"> - <properties> - <help>Monitor last lines of DDOS protection</help> - </properties> - <command>journalctl --no-hostname --follow --boot --unit fastnetmon.service</command> - </leafNode> - </children> - </node> <leafNode name="certbot"> <properties> <help>Monitor last lines of certbot log</help> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 5ee7c973f..ee2e2bf70 100755 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -50,6 +50,39 @@ </properties> <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e heartbeat -e cl_status -e mach_down -e ha_log</command> </leafNode> + <node name="conntrack"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(NEW|UPDATE|DESTROY)\]'</command> + <children> + <node name="event"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <children> + <leafNode name="new"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(NEW)\]'</command> + </leafNode> + <leafNode name="update"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(UPDATE)\]'</command> + </leafNode> + <leafNode name="destroy"> + <properties> + <help>Show log for Conntrack Events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(DESTROY)\]'</command> + </leafNode> + </children> + </node> + </children> + </node> <leafNode name="conntrack-sync"> <properties> <help>Show log for Conntrack-sync</help> @@ -62,19 +95,6 @@ </properties> <command>journalctl --no-hostname --boot --unit conserver-server.service</command> </leafNode> - <node name="ids"> - <properties> - <help>Show log for for Intrusion Detection System</help> - </properties> - <children> - <leafNode name="ddos-protection"> - <properties> - <help>Show log for DDOS protection</help> - </properties> - <command>journalctl --no-hostname --boot --unit fastnetmon.service</command> - </leafNode> - </children> - </node> <node name="dhcp"> <properties> <help>Show log for Dynamic Host Control Protocol (DHCP)</help> @@ -139,7 +159,7 @@ <properties> <help>Show log for Firewall</help> </properties> - <command>journalctl --no-hostname --boot -k | egrep "(ipv[46]|bri)-(FWD|INP|OUT|NAM)"</command> + <command>journalctl --no-hostname --boot -k --grep='(ipv[46]|bri)-(FWD|INP|OUT|NAM)|STATE-POLICY'</command> <children> <node name="bridge"> <properties> diff --git a/op-mode-definitions/system-image.xml.in b/op-mode-definitions/system-image.xml.in index 44b055be6..847029dcd 100644 --- a/op-mode-definitions/system-image.xml.in +++ b/op-mode-definitions/system-image.xml.in @@ -193,7 +193,7 @@ <properties> <help>Show installed VyOS images</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_info.py show_images_summary</command> + <command>${vyos_op_scripts_dir}/image_info.py show_images_summary</command> <children> <node name="details"> <properties> diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py index 94215531d..81d986658 100644 --- a/python/vyos/component_version.py +++ b/python/vyos/component_version.py @@ -49,7 +49,9 @@ DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot') REGEX_WARN_VYOS = r'(// Warning: Do not remove the following line.)' REGEX_WARN_VYATTA = r'(/\* Warning: Do not remove the following line. \*/)' REGEX_COMPONENT_VERSION_VYOS = r'// vyos-config-version:\s+"([\w@:-]+)"\s*' -REGEX_COMPONENT_VERSION_VYATTA = r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/' +REGEX_COMPONENT_VERSION_VYATTA = ( + r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/' +) REGEX_RELEASE_VERSION_VYOS = r'// Release version:\s+(\S*)\s*' REGEX_RELEASE_VERSION_VYATTA = r'/\* Release version:\s+(\S*)\s*\*/' @@ -62,16 +64,31 @@ CONFIG_FILE_VERSION = """\ warn_filter_vyos = re.compile(REGEX_WARN_VYOS) warn_filter_vyatta = re.compile(REGEX_WARN_VYATTA) -regex_filter = { 'vyos': dict(zip(['component', 'release'], - [re.compile(REGEX_COMPONENT_VERSION_VYOS), - re.compile(REGEX_RELEASE_VERSION_VYOS)])), - 'vyatta': dict(zip(['component', 'release'], - [re.compile(REGEX_COMPONENT_VERSION_VYATTA), - re.compile(REGEX_RELEASE_VERSION_VYATTA)])) } +regex_filter = { + 'vyos': dict( + zip( + ['component', 'release'], + [ + re.compile(REGEX_COMPONENT_VERSION_VYOS), + re.compile(REGEX_RELEASE_VERSION_VYOS), + ], + ) + ), + 'vyatta': dict( + zip( + ['component', 'release'], + [ + re.compile(REGEX_COMPONENT_VERSION_VYATTA), + re.compile(REGEX_RELEASE_VERSION_VYATTA), + ], + ) + ), +} + @dataclass class VersionInfo: - component: Optional[dict[str,int]] = None + component: Optional[dict[str, int]] = None release: str = get_version() vintage: str = 'vyos' config_body: Optional[str] = None @@ -84,8 +101,9 @@ class VersionInfo: return bool(self.config_body is None) def update_footer(self): - f = CONFIG_FILE_VERSION.format(component_to_string(self.component), - self.release) + f = CONFIG_FILE_VERSION.format( + component_to_string(self.component), self.release + ) self.footer_lines = f.splitlines() def update_syntax(self): @@ -121,13 +139,16 @@ class VersionInfo: except Exception as e: raise ValueError(e) from e + def component_to_string(component: dict) -> str: - l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] + l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] # noqa: E741 return ':'.join(l) + def component_from_string(string: str) -> dict: return {k: int(v) for k, v in re.findall(r'([\w,-]+)@(\d+)', string)} + def version_info_from_file(config_file) -> VersionInfo: """Return config file component and release version info.""" version_info = VersionInfo() @@ -166,27 +187,27 @@ def version_info_from_file(config_file) -> VersionInfo: return version_info + def version_info_from_system() -> VersionInfo: """Return system component and release version info.""" d = component_version() sort_d = dict(sorted(d.items(), key=lambda x: x[0])) - version_info = VersionInfo( - component = sort_d, - release = get_version(), - vintage = 'vyos' - ) + version_info = VersionInfo(component=sort_d, release=get_version(), vintage='vyos') return version_info + def version_info_copy(v: VersionInfo) -> VersionInfo: """Make a copy of dataclass.""" return replace(v) + def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo: """In place pruning of component keys of x not in y.""" if x.component is None or y.component is None: return - x.component = { k: v for k,v in x.component.items() if k in y.component } + x.component = {k: v for k, v in x.component.items() if k in y.component} + def add_system_version(config_str: str = None, out_file: str = None): """Wrap config string with system version and write to out_file. @@ -202,3 +223,11 @@ def add_system_version(config_str: str = None, out_file: str = None): version_info.write(out_file) else: sys.stdout.write(version_info.write_string()) + + +def append_system_version(file: str): + """Append system version data to existing file""" + version_info = version_info_from_system() + version_info.update_footer() + with open(file, 'a') as f: + f.write(version_info.write_string()) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 90b96b88c..a3be29881 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -21,6 +21,10 @@ import subprocess from vyos.defaults import directories from vyos.utils.process import is_systemd_service_running from vyos.utils.dict import dict_to_paths +from vyos.utils.boot import boot_configuration_complete +from vyos.vyconf_session import VyconfSession + +vyconf_backend = False CLI_SHELL_API = '/bin/cli-shell-api' SET = '/opt/vyatta/sbin/my_set' @@ -165,6 +169,11 @@ class ConfigSession(object): self.__run_command([CLI_SHELL_API, 'setupSession']) + if vyconf_backend and boot_configuration_complete(): + self._vyconf_session = VyconfSession(on_error=ConfigSessionError) + else: + self._vyconf_session = None + def __del__(self): try: output = ( @@ -209,7 +218,10 @@ class ConfigSession(object): value = [] else: value = [value] - self.__run_command([SET] + path + value) + if self._vyconf_session is None: + self.__run_command([SET] + path + value) + else: + self._vyconf_session.set(path + value) def set_section(self, path: list, d: dict): try: @@ -223,7 +235,10 @@ class ConfigSession(object): value = [] else: value = [value] - self.__run_command([DELETE] + path + value) + if self._vyconf_session is None: + self.__run_command([DELETE] + path + value) + else: + self._vyconf_session.delete(path + value) def load_section(self, path: list, d: dict): try: @@ -261,20 +276,34 @@ class ConfigSession(object): self.__run_command([COMMENT] + path + value) def commit(self): - out = self.__run_command([COMMIT]) + if self._vyconf_session is None: + out = self.__run_command([COMMIT]) + else: + out, _ = self._vyconf_session.commit() + return out def discard(self): - self.__run_command([DISCARD]) + if self._vyconf_session is None: + self.__run_command([DISCARD]) + else: + out, _ = self._vyconf_session.discard() def show_config(self, path, format='raw'): - config_data = self.__run_command(SHOW_CONFIG + path) + if self._vyconf_session is None: + config_data = self.__run_command(SHOW_CONFIG + path) + else: + config_data, _ = self._vyconf_session.show_config() if format == 'raw': return config_data def load_config(self, file_path): - out = self.__run_command(LOAD_CONFIG + [file_path]) + if self._vyconf_session is None: + out = self.__run_command(LOAD_CONFIG + [file_path]) + else: + out, _ = self._vyconf_session.load_config(file=file_path) + return out def load_explicit(self, file_path): @@ -287,11 +316,21 @@ class ConfigSession(object): raise ConfigSessionError(e) from e def migrate_and_load_config(self, file_path): - out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) + if self._vyconf_session is None: + out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) + else: + out, _ = self._vyconf_session.load_config(file=file_path, migrate=True) + return out def save_config(self, file_path): - out = self.__run_command(SAVE_CONFIG + [file_path]) + if self._vyconf_session is None: + out = self.__run_command(SAVE_CONFIG + [file_path]) + else: + out, _ = self._vyconf_session.save_config( + file=file_path, append_version=True + ) + return out def install_image(self, url): diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index 8d469e3e2..524167d8b 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -92,7 +92,7 @@ def get_frrender_dict(conf, argv=None) -> dict: if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None: del default_values['area'][area_num]['area_type']['nssa'] - for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']: + for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static']: if dict_search(f'redistribute.{protocol}', ospf) is None: del default_values['redistribute'][protocol] if not bool(default_values['redistribute']): diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 9fc5dde3d..5eecbbaad 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -20,8 +20,8 @@ import socket from datetime import datetime from datetime import timezone +from vyos import ConfigError from vyos.template import is_ipv6 -from vyos.template import isc_static_route from vyos.template import netmask_from_cidr from vyos.utils.dict import dict_search_args from vyos.utils.file import file_permissions @@ -44,6 +44,7 @@ kea4_options = { 'wpad_url': 'wpad-url', 'ipv6_only_preferred': 'v6-only-preferred', 'captive_portal': 'v4-captive-portal', + 'capwap_controller': 'capwap-ac-v4', } kea6_options = { @@ -56,6 +57,7 @@ kea6_options = { 'nisplus_server': 'nisp-servers', 'sntp_server': 'sntp-servers', 'captive_portal': 'v6-captive-portal', + 'capwap_controller': 'capwap-ac-v6', } kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket' @@ -111,22 +113,21 @@ def kea_parse_options(config): default_route = '' if 'default_router' in config: - default_route = isc_static_route('0.0.0.0/0', config['default_router']) + default_route = f'0.0.0.0/0 - {config["default_router"]}' routes = [ - isc_static_route(route, route_options['next_hop']) + f'{route} - {route_options["next_hop"]}' for route, route_options in config['static_route'].items() ] options.append( { - 'name': 'rfc3442-static-route', + 'name': 'classless-static-route', 'data': ', '.join( routes if not default_route else routes + [default_route] ), } ) - options.append({'name': 'windows-static-route', 'data': ', '.join(routes)}) if 'time_zone' in config: with open('/usr/share/zoneinfo/' + config['time_zone'], 'rb') as f: @@ -147,7 +148,7 @@ def kea_parse_options(config): def kea_parse_subnet(subnet, config): - out = {'subnet': subnet, 'id': int(config['subnet_id'])} + out = {'subnet': subnet, 'id': int(config['subnet_id']), 'user-context': {}} if 'option' in config: out['option-data'] = kea_parse_options(config['option']) @@ -165,6 +166,9 @@ def kea_parse_subnet(subnet, config): out['valid-lifetime'] = int(config['lease']) out['max-valid-lifetime'] = int(config['lease']) + if 'ping_check' in config: + out['user-context']['enable-ping-check'] = True + if 'range' in config: pools = [] for num, range_config in config['range'].items(): @@ -218,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 @@ -347,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/proto/vyconf_client.py b/python/vyos/proto/vyconf_client.py index f34549309..b385f0951 100644 --- a/python/vyos/proto/vyconf_client.py +++ b/python/vyos/proto/vyconf_client.py @@ -52,7 +52,9 @@ def request_to_msg(req: vyconf_proto.RequestEnvelope) -> vyconf_pb2.RequestEnvel def msg_to_response(msg: vyconf_pb2.Response) -> vyconf_proto.Response: # pylint: disable=no-member - d = MessageToDict(msg, preserving_proto_field_name=True) + d = MessageToDict( + msg, preserving_proto_field_name=True, use_integers_for_enums=True + ) response = vyconf_proto.Response(**d) return response diff --git a/python/vyos/system/grub_util.py b/python/vyos/system/grub_util.py index 4a3d8795e..ad95bb4f9 100644 --- a/python/vyos/system/grub_util.py +++ b/python/vyos/system/grub_util.py @@ -56,13 +56,12 @@ def set_kernel_cmdline_options(cmdline_options: str, version: str = '', @image.if_not_live_boot def update_kernel_cmdline_options(cmdline_options: str, - root_dir: str = '') -> None: + root_dir: str = '', + version = image.get_running_image()) -> None: """Update Kernel custom cmdline options""" if not root_dir: root_dir = disk.find_persistence() - version = image.get_running_image() - boot_opts_current = grub.get_boot_opts(version, root_dir) boot_opts_proposed = grub.BOOT_OPTS_STEM + f'{version} {cmdline_options}' diff --git a/python/vyos/template.py b/python/vyos/template.py index e75db1a8d..d79e1183f 100755 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -390,28 +390,6 @@ def compare_netmask(netmask1, netmask2): except: return False -@register_filter('isc_static_route') -def isc_static_route(subnet, router): - # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server - # Option format is: - # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> - # where bytes with the value 0 are omitted. - from ipaddress import ip_network - net = ip_network(subnet) - # add netmask - string = str(net.prefixlen) + ',' - # add network bytes - if net.prefixlen: - width = net.prefixlen // 8 - if net.prefixlen % 8: - width += 1 - string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ',' - - # add router bytes - string += ','.join(router.split('.')) - - return string - @register_filter('is_file') def is_file(filename): if os.path.exists(filename): @@ -881,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 = [] @@ -895,9 +940,13 @@ def kea_shared_network_json(shared_networks): network = { 'name': name, 'authoritative': ('authoritative' in config), - 'subnet4': [] + 'subnet4': [], + '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']) @@ -907,6 +956,9 @@ def kea_shared_network_json(shared_networks): if 'bootfile_server' in config['option']: network['next-server'] = config['option']['bootfile_server'] + if 'ping_check' in config: + network['user-context']['enable-ping-check'] = True + if 'subnet' in config: for subnet, subnet_config in config['subnet'].items(): if 'disable' in subnet_config: diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py new file mode 100644 index 000000000..506095625 --- /dev/null +++ b/python/vyos/vyconf_session.py @@ -0,0 +1,123 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# + +import tempfile +import shutil +from functools import wraps +from typing import Type + +from vyos.proto import vyconf_client +from vyos.migrate import ConfigMigrate +from vyos.migrate import ConfigMigrateError +from vyos.component_version import append_system_version + + +def output(o): + out = '' + for res in (o.output, o.error, o.warning): + if res is not None: + out = out + res + return out + + +class VyconfSession: + def __init__(self, token: str = None, on_error: Type[Exception] = None): + if token is None: + out = vyconf_client.send_request('setup_session') + self.__token = out.output + else: + self.__token = token + + self.on_error = on_error + + @staticmethod + def raise_exception(f): + @wraps(f) + def wrapped(self, *args, **kwargs): + if self.on_error is None: + return f(self, *args, **kwargs) + o, e = f(self, *args, **kwargs) + if e: + raise self.on_error(o) + return o, e + + return wrapped + + @raise_exception + def set(self, path: list[str]) -> tuple[str, int]: + out = vyconf_client.send_request('set', token=self.__token, path=path) + return output(out), out.status + + @raise_exception + def delete(self, path: list[str]) -> tuple[str, int]: + out = vyconf_client.send_request('delete', token=self.__token, path=path) + return output(out), out.status + + @raise_exception + def commit(self) -> tuple[str, int]: + out = vyconf_client.send_request('commit', token=self.__token) + return output(out), out.status + + @raise_exception + def discard(self) -> tuple[str, int]: + out = vyconf_client.send_request('discard', token=self.__token) + return output(out), out.status + + def session_changed(self) -> bool: + out = vyconf_client.send_request('session_changed', token=self.__token) + return not bool(out.status) + + @raise_exception + def load_config(self, file: str, migrate: bool = False) -> tuple[str, int]: + # pylint: disable=consider-using-with + if migrate: + tmp = tempfile.NamedTemporaryFile() + shutil.copy2(file, tmp.name) + config_migrate = ConfigMigrate(tmp.name) + try: + config_migrate.run() + except ConfigMigrateError as e: + tmp.close() + return repr(e), 1 + file = tmp.name + else: + tmp = '' + + out = vyconf_client.send_request('load', token=self.__token, location=file) + if tmp: + tmp.close() + + return output(out), out.status + + @raise_exception + def save_config(self, file: str, append_version: bool = False) -> tuple[str, int]: + out = vyconf_client.send_request('save', token=self.__token, location=file) + if append_version: + append_system_version(file) + return output(out), out.status + + @raise_exception + def show_config(self, path: list[str] = None) -> tuple[str, int]: + if path is None: + path = [] + out = vyconf_client.send_request('show_config', token=self.__token, path=path) + return output(out), out.status + + def __del__(self): + out = vyconf_client.send_request('teardown', token=self.__token) + if out.status: + print(f'Could not tear down session {self.__token}: {output(out)}') 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_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 05900a4ba..694c24e4d 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2025 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 @@ -25,7 +25,6 @@ from vyos.utils.network import interface_exists from vyos.utils.network import get_vxlan_vlan_tunnels from vyos.utils.network import get_vxlan_vni_filter from vyos.template import is_ipv6 -from vyos import ConfigError from base_interfaces_test import BasicInterfaceTest def convert_to_list(ranges_to_convert): diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 7c2ebff89..e421f04d2 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -32,8 +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'] @@ -96,6 +98,10 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertTrue(key in base_obj) self.assertEqual(base_obj[key], value) + def verify_service_running(self): + tmp = cmd('tail -n 100 /var/log/messages | grep kea') + self.assertTrue(process_named_running(PROCESS_NAME), msg=f'Service not running, log: {tmp}') + def test_dhcp_single_pool_range(self): shared_net_name = 'SMOKE-1' @@ -106,9 +112,12 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['listen-interface', interface]) + self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check']) + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['ignore-client-id']) + self.cli_set(pool + ['ping-check']) # we use the first subnet IP address as default gateway self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['option', 'name-server', dns_1]) @@ -151,6 +160,21 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 ) + # Verify ping-check + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'user-context'], + 'enable-ping-check', + True + ) + + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'user-context'], + 'enable-ping-check', + True + ) + # Verify options self.verify_config_object( obj, @@ -181,7 +205,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_options(self): shared_net_name = 'SMOKE-0815' @@ -197,6 +221,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): wpad = 'http://wpad.vyos.io/foo/bar' server_identifier = bootfile_server ipv6_only_preferred = '300' + capwap_access_controller = '192.168.2.125' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -216,9 +241,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['option', 'bootfile-server', bootfile_server]) self.cli_set(pool + ['option', 'wpad-url', wpad]) self.cli_set(pool + ['option', 'server-identifier', server_identifier]) + self.cli_set( + pool + ['option', 'capwap-controller', capwap_access_controller] + ) + + static_route = '10.0.0.0/24' + static_route_nexthop = '192.0.2.1' self.cli_set( - pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'] + pool + ['option', 'static-route', static_route, 'next-hop', static_route_nexthop] ) self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred]) self.cli_set(pool + ['option', 'time-zone', 'Europe/London']) @@ -301,25 +332,25 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'tftp-server-name', 'data': tftp_server}, + {'name': 'capwap-ac-v4', 'data': capwap_access_controller}, ) self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'wpad-url', 'data': wpad}, + {'name': 'tftp-server-name', 'data': tftp_server}, ) self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - { - 'name': 'rfc3442-static-route', - 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1', - }, + {'name': 'wpad-url', 'data': wpad}, ) self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'}, + { + 'name': 'classless-static-route', + 'data': f'{static_route} - {static_route_nexthop}, 0.0.0.0/0 - {router}', + }, ) self.verify_config_object( obj, @@ -352,7 +383,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_options_scoped(self): shared_net_name = 'SMOKE-2' @@ -438,7 +469,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_static_mapping(self): shared_net_name = 'SMOKE-2' @@ -584,7 +615,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): client_base += 1 # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_multiple_pools(self): lease_time = '14400' @@ -726,7 +757,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): client_base += 1 # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_exclude_not_in_range(self): # T3180: verify else path when slicing DHCP ranges and exclude address @@ -773,7 +804,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_exclude_in_range(self): # T3180: verify else path when slicing DHCP ranges and exclude address @@ -836,7 +867,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_relay_server(self): # Listen on specific address and return DHCP leases from a non @@ -884,7 +915,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_high_availability(self): shared_net_name = 'FAILOVER' @@ -987,8 +1018,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - self.assertTrue(process_named_running(CTRL_PROCESS_NAME)) + self.verify_service_running() def test_dhcp_high_availability_standby(self): shared_net_name = 'FAILOVER' @@ -1087,8 +1117,134 @@ 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(CTRL_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']) @@ -1250,7 +1406,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() # All up and running, now test vyos-hostsd store diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 6ecf6c1cf..6535ca72d 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -108,6 +108,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['lease-time', 'default', lease_time]) self.cli_set(pool + ['lease-time', 'maximum', max_lease_time]) self.cli_set(pool + ['lease-time', 'minimum', min_lease_time]) + self.cli_set(pool + ['option', 'capwap-controller', dns_1]) self.cli_set(pool + ['option', 'name-server', dns_1]) self.cli_set(pool + ['option', 'name-server', dns_2]) self.cli_set(pool + ['option', 'name-server', dns_2]) @@ -157,6 +158,10 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.verify_config_object( obj, ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'capwap-ac-v6', 'data': dns_1}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'}) self.verify_config_object( obj, diff --git a/smoketest/scripts/cli/test_service_ids_ddos-protection.py b/smoketest/scripts/cli/test_service_ids_ddos-protection.py deleted file mode 100755 index 91b056eea..000000000 --- a/smoketest/scripts/cli/test_service_ids_ddos-protection.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2022 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/>. - -import os -import unittest - -from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.configsession import ConfigSessionError -from vyos.utils.process import process_named_running -from vyos.utils.file import read_file - -PROCESS_NAME = 'fastnetmon' -FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf' -NETWORKS_CONF = '/run/fastnetmon/networks_list' -EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list' -base_path = ['service', 'ids', 'ddos-protection'] - -class TestServiceIDS(VyOSUnitTestSHIM.TestCase): - @classmethod - def setUpClass(cls): - super(TestServiceIDS, cls).setUpClass() - - # ensure we can also run this test on a live system - so lets clean - # out the current configuration :) - cls.cli_delete(cls, base_path) - - def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - # delete test config - self.cli_delete(base_path) - self.cli_commit() - - self.assertFalse(os.path.exists(FASTNETMON_CONF)) - self.assertFalse(process_named_running(PROCESS_NAME)) - - def test_fastnetmon(self): - networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64'] - excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128'] - interfaces = ['eth0', 'eth1'] - fps = '3500' - mbps = '300' - pps = '60000' - - self.cli_set(base_path + ['mode', 'mirror']) - # Required network! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in networks: - self.cli_set(base_path + ['network', tmp]) - - # optional excluded-network! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in excluded_networks: - self.cli_set(base_path + ['excluded-network', tmp]) - - # Required interface(s)! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in interfaces: - self.cli_set(base_path + ['listen-interface', tmp]) - - self.cli_set(base_path + ['direction', 'in']) - self.cli_set(base_path + ['threshold', 'general', 'fps', fps]) - self.cli_set(base_path + ['threshold', 'general', 'pps', pps]) - self.cli_set(base_path + ['threshold', 'general', 'mbps', mbps]) - - # commit changes - self.cli_commit() - - # Check configured port - config = read_file(FASTNETMON_CONF) - self.assertIn(f'mirror_afpacket = on', config) - self.assertIn(f'process_incoming_traffic = on', config) - self.assertIn(f'process_outgoing_traffic = off', config) - self.assertIn(f'ban_for_flows = on', config) - self.assertIn(f'threshold_flows = {fps}', config) - self.assertIn(f'ban_for_bandwidth = on', config) - self.assertIn(f'threshold_mbps = {mbps}', config) - self.assertIn(f'ban_for_pps = on', config) - self.assertIn(f'threshold_pps = {pps}', config) - # default - self.assertIn(f'enable_ban = on', config) - self.assertIn(f'enable_ban_ipv6 = on', config) - self.assertIn(f'ban_time = 1900', config) - - tmp = ','.join(interfaces) - self.assertIn(f'interfaces = {tmp}', config) - - - network_config = read_file(NETWORKS_CONF) - for tmp in networks: - self.assertIn(f'{tmp}', network_config) - - excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF) - for tmp in excluded_networks: - self.assertIn(f'{tmp}', excluded_network_config) - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index ed72f378e..71dec68d8 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -25,9 +25,7 @@ import shutil from base_vyostest_shim import VyOSUnitTestSHIM -from contextlib import redirect_stdout from gzip import GzipFile -from io import StringIO, TextIOWrapper from subprocess import Popen from subprocess import PIPE from pwd import getpwall diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 91a76e6f6..c1d943bde 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -352,6 +352,94 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() + def test_site_to_site_vti_ts_afi(self): + local_address = '192.0.2.10' + vti = 'vti10' + # IKE + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', ike_group, 'disable-mobike']) + # ESP + self.cli_set(base_path + ['esp-group', esp_group, 'compression']) + # VTI interface + self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24']) + + # vpn ipsec auth psk <tag> id <x.x.x.x> + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + + # Site to site + peer_base_path = base_path + ['site-to-site', 'peer', connection_name] + self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) + self.cli_set(peer_base_path + ['connection-type', 'none']) + self.cli_set(peer_base_path + ['force-udp-encapsulation']) + self.cli_set(peer_base_path + ['ike-group', ike_group]) + self.cli_set(peer_base_path + ['default-esp-group', esp_group]) + self.cli_set(peer_base_path + ['local-address', local_address]) + self.cli_set(peer_base_path + ['remote-address', peer_ip]) + self.cli_set(peer_base_path + ['vti', 'bind', vti]) + self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group]) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '0.0.0.0/0']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.1/32']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.3/32']) + + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + if_id = vti.lstrip('vti') + # The key defaults to 0 and will match any policies which similarly do + # not have a lookup key configuration - thus we shift the key by one + # to also support a vti0 interface + if_id = str(int(if_id) +1) + swanctl_conf_lines = [ + f'version = 2', + f'auth = psk', + f'proposals = aes128-sha1-modp1024', + f'esp_proposals = aes128-sha1-modp1024', + f'local_addrs = {local_address} # dhcp:no', + f'mobike = no', + f'remote_addrs = {peer_ip}', + f'mode = tunnel', + f'local_ts = 0.0.0.0/0', + f'remote_ts = 192.0.2.1/32,192.0.2.3/32', + f'ipcomp = yes', + f'start_action = none', + f'replay_window = 32', + f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one + f'if_id_out = {if_id}', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + # Check IPv6 TS + self.cli_delete(peer_base_path + ['vti', 'traffic-selector']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '::/0']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '::/0']) + self.cli_commit() + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'local_ts = ::/0', + f'remote_ts = ::/0', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + # Check both TS (IPv4 + IPv6) + self.cli_delete(peer_base_path + ['vti', 'traffic-selector']) + self.cli_commit() + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'local_ts = 0.0.0.0/0,::/0', + f'remote_ts = 0.0.0.0/0,::/0', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + def test_dmvpn(self): ike_lifetime = '3600' esp_lifetime = '1800' diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index 5a729af74..99c7e6a1f 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -41,9 +41,9 @@ from vyos import airbag airbag.enable() -ctrl_config_file = '/run/kea/kea-ctrl-agent.conf' 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' @@ -171,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 @@ -423,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 @@ -480,25 +505,26 @@ def generate(dhcp): dhcp['high_availability']['ca_cert_file'] = ca_cert_file render( - ctrl_config_file, - 'dhcp-server/kea-ctrl-agent.conf.j2', - dhcp, - user=user_group, - group=user_group, - ) - render( config_file, 'dhcp-server/kea-dhcp4.conf.j2', 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 def apply(dhcp): - services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server'] + services = ['kea-dhcp4-server', 'kea-dhcp-ddns-server'] if not dhcp or 'disable' in dhcp: for service in services: @@ -515,9 +541,6 @@ def apply(dhcp): if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp: action = 'stop' - if service == 'kea-ctrl-agent' and 'high_availability' not in dhcp: - action = 'stop' - call(f'systemctl {action} {service}.service') return None diff --git a/src/conf_mode/service_ids_ddos-protection.py b/src/conf_mode/service_ids_ddos-protection.py deleted file mode 100755 index 276a71fcb..000000000 --- a/src/conf_mode/service_ids_ddos-protection.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2023 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/>. - -import os - -from sys import exit - -from vyos.config import Config -from vyos.template import render -from vyos.utils.process import call -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -config_file = r'/run/fastnetmon/fastnetmon.conf' -networks_list = r'/run/fastnetmon/networks_list' -excluded_networks_list = r'/run/fastnetmon/excluded_networks_list' -attack_dir = '/var/log/fastnetmon_attacks' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['service', 'ids', 'ddos-protection'] - if not conf.exists(base): - return None - - fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - - return fastnetmon - -def verify(fastnetmon): - if not fastnetmon: - return None - - if 'mode' not in fastnetmon: - raise ConfigError('Specify operating mode!') - - if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon: - raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring") - - if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}): - raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'") - - if 'alert_script' in fastnetmon: - if os.path.isfile(fastnetmon['alert_script']): - # Check script permissions - if not os.access(fastnetmon['alert_script'], os.X_OK): - raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script'])) - else: - raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon)) - -def generate(fastnetmon): - if not fastnetmon: - for file in [config_file, networks_list]: - if os.path.isfile(file): - os.unlink(file) - - return None - - # Create dir for log attack details - if not os.path.exists(attack_dir): - os.mkdir(attack_dir) - - render(config_file, 'ids/fastnetmon.j2', fastnetmon) - render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon) - render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon) - return None - -def apply(fastnetmon): - systemd_service = 'fastnetmon.service' - if not fastnetmon: - # Stop fastnetmon service if removed - call(f'systemctl stop {systemd_service}') - else: - call(f'systemctl reload-or-restart {systemd_service}') - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index 3fed6d273..4febb6494 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import warnings from passlib.hosts import linux_context from psutil import users @@ -30,12 +29,9 @@ from vyos.config import Config from vyos.configverify import verify_vrf from vyos.template import render from vyos.template import is_ipv4 -from vyos.utils.auth import ( - DEFAULT_PASSWORD, - EPasswdStrength, - evaluate_strength, - get_current_user -) +from vyos.utils.auth import EPasswdStrength +from vyos.utils.auth import evaluate_strength +from vyos.utils.auth import get_current_user from vyos.utils.configfs import delete_cli_node from vyos.utils.configfs import add_cli_node from vyos.utils.dict import dict_search diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index 064a1aa91..b45a9d8a6 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -122,6 +122,10 @@ def generate(options): render(ssh_config, 'system/ssh_config.j2', options) render(usb_autosuspend, 'system/40_usb_autosuspend.j2', options) + # XXX: This code path and if statements must be kept in sync with the Kernel + # option handling in image_installer.py:get_cli_kernel_options(). This + # occurance is used for having the appropriate options passed to GRUB + # when re-configuring options on the CLI. cmdline_options = [] if 'kernel' in options: if 'disable_mitigations' in options['kernel']: @@ -131,8 +135,7 @@ def generate(options): if 'amd_pstate_driver' in options['kernel']: mode = options['kernel']['amd_pstate_driver'] cmdline_options.append( - f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}' - ) + f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}') grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) return None diff --git a/src/etc/netplug/vyos-netplug-dhcp-client b/src/etc/netplug/vyos-netplug-dhcp-client index 4cc824afd..a230fe900 100755 --- a/src/etc/netplug/vyos-netplug-dhcp-client +++ b/src/etc/netplug/vyos-netplug-dhcp-client @@ -20,10 +20,10 @@ import sys from time import sleep from vyos.config import Config -from vyos.configdict import get_interface_dict -from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.boot import boot_configuration_complete +from vyos.utils.process import cmd +from vyos.utils.process import is_systemd_service_active from vyos.utils.commit import commit_in_progress from vyos import airbag @@ -38,20 +38,34 @@ if not boot_configuration_complete(): sys.exit(1) interface = sys.argv[1] -# helper scripts should only work on physical interfaces not on individual -# sub-interfaces. Moving e.g. a VLAN interface in/out a VRF will also trigger -# this script which should be prohibited - bail out early -if '.' in interface: - sys.exit(0) while commit_in_progress(): - sleep(1) + sleep(0.250) in_out = sys.argv[2] config = Config() interface_path = ['interfaces'] + Section.get_config_path(interface).split() -_, interface_config = get_interface_dict( - config, interface_path[:-1], ifname=interface, with_pki=True -) -Interface(interface).update(interface_config) + +systemdV4_service = f'dhclient@{interface}.service' +systemdV6_service = f'dhcp6c@{interface}.service' +if in_out == 'out': + # Interface moved state to down + if is_systemd_service_active(systemdV4_service): + cmd(f'systemctl stop {systemdV4_service}') + if is_systemd_service_active(systemdV6_service): + cmd(f'systemctl stop {systemdV6_service}') +elif in_out == 'in': + if config.exists_effective(interface_path + ['address']): + tmp = config.return_effective_values(interface_path + ['address']) + # Always (re-)start the DHCP(v6) client service. If the DHCP(v6) client + # is already running - which could happen if the interface is re- + # configured in operational down state, it will have a backoff + # time increasing while not receiving a DHCP(v6) reply. + # + # To make the interface instantly available, and as for a DHCP(v6) lease + # we will re-start the service and thus cancel the backoff time. + if 'dhcp' in tmp: + cmd(f'systemctl restart {systemdV4_service}') + if 'dhcpv6' in tmp: + cmd(f'systemctl restart {systemdV6_service}') diff --git a/src/etc/systemd/system/fastnetmon.service.d/override.conf b/src/etc/systemd/system/fastnetmon.service.d/override.conf deleted file mode 100644 index 841666070..000000000 --- a/src/etc/systemd/system/fastnetmon.service.d/override.conf +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -RequiresMountsFor=/run -ConditionPathExists=/run/fastnetmon/fastnetmon.conf -After= -After=vyos-router.service - -[Service] -Type=simple -WorkingDirectory=/run/fastnetmon -PIDFile=/run/fastnetmon.pid -ExecStart= -ExecStart=/usr/sbin/fastnetmon --configuration_file /run/fastnetmon/fastnetmon.conf diff --git a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf deleted file mode 100644 index c74fafb42..000000000 --- a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -After= -After=vyos-router.service -ConditionFileNotEmpty= - -[Service] -ExecStart= -ExecStart=/usr/sbin/kea-ctrl-agent -c /run/kea/kea-ctrl-agent.conf -AmbientCapabilities=CAP_NET_BIND_SERVICE -CapabilityBoundingSet=CAP_NET_BIND_SERVICE 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 diff --git a/src/init/vyos-router b/src/init/vyos-router index ab3cc42cb..081adf214 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -417,6 +417,7 @@ gen_duid () start () { + echo -e "Initializing VyOS router\033[0m" # reset and clean config files security_reset || log_failure_msg "security reset failed" @@ -517,7 +518,6 @@ start () cleanup_post_commit_hooks - log_daemon_msg "Starting VyOS router" disabled migrate || migrate_bootfile restore_if_missing_preconfig_script @@ -557,6 +557,9 @@ start () if [[ ! -z "$tmp" ]]; then vtysh -c "rpki start" fi + + # Start netplug daemon + systemctl start netplug.service } stop() @@ -574,8 +577,8 @@ stop() umount ${vyatta_configdir} log_action_end_msg $? + systemctl stop netplug.service systemctl stop vyconfd.service - systemctl stop frr.service unmount_encrypted_config diff --git a/src/migration-scripts/dhcp-server/7-to-8 b/src/migration-scripts/dhcp-server/7-to-8 index 7fcb62e86..d0f9455bb 100644 --- a/src/migration-scripts/dhcp-server/7-to-8 +++ b/src/migration-scripts/dhcp-server/7-to-8 @@ -41,9 +41,6 @@ def migrate(config: ConfigTree) -> None: for network in config.list_nodes(base + ['shared-network-name']): base_network = base + ['shared-network-name', network] - if config.exists(base_network + ['ping-check']): - config.delete(base_network + ['ping-check']) - if config.exists(base_network + ['shared-network-parameters']): config.delete(base_network +['shared-network-parameters']) @@ -57,9 +54,6 @@ def migrate(config: ConfigTree) -> None: if config.exists(base_subnet + ['enable-failover']): config.delete(base_subnet + ['enable-failover']) - if config.exists(base_subnet + ['ping-check']): - config.delete(base_subnet + ['ping-check']) - if config.exists(base_subnet + ['subnet-parameters']): config.delete(base_subnet + ['subnet-parameters']) diff --git a/src/migration-scripts/ids/1-to-2 b/src/migration-scripts/ids/1-to-2 new file mode 100644 index 000000000..4c0333c88 --- /dev/null +++ b/src/migration-scripts/ids/1-to-2 @@ -0,0 +1,30 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# T: Migrate threshold and add new threshold types + +from vyos.configtree import ConfigTree + +# The old 'service ids' path was only used for FastNetMon +# Suricata is in 'service suricata', +# so this isn't an overreach +base = ['service', 'ids'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + else: + config.delete(base) diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 9c17d0229..2660309a5 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -24,7 +24,9 @@ from glob import glob from sys import exit from os import environ from os import readlink -from os import getpid, getppid +from os import getpid +from os import getppid +from json import loads from typing import Union from urllib.parse import urlparse from passlib.hosts import linux_context @@ -35,15 +37,23 @@ from psutil import disk_partitions from vyos.base import Warning from vyos.configtree import ConfigTree from vyos.remote import download -from vyos.system import disk, grub, image, compat, raid, SYSTEM_CFG_VER +from vyos.system import disk +from vyos.system import grub +from vyos.system import image +from vyos.system import compat +from vyos.system import raid +from vyos.system import SYSTEM_CFG_VER +from vyos.system import grub_util from vyos.template import render from vyos.utils.auth import ( DEFAULT_PASSWORD, EPasswdStrength, evaluate_strength ) +from vyos.utils.dict import dict_search from vyos.utils.io import ask_input, ask_yes_no, select_entry from vyos.utils.file import chmod_2775 +from vyos.utils.file import read_file from vyos.utils.process import cmd, run, rc_cmd from vyos.version import get_version_data @@ -74,6 +84,7 @@ MSG_INPUT_CONFIG_FOUND: str = 'An active configuration was found. Would you like MSG_INPUT_CONFIG_CHOICE: str = 'The following config files are available for boot:' MSG_INPUT_CONFIG_CHOOSE: str = 'Which file would you like as boot config?' MSG_INPUT_IMAGE_NAME: str = 'What would you like to name this image?' +MSG_INPUT_IMAGE_NAME_TAKEN: str = 'There is already an installed image by that name; please choose again' MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set the new image as the default one for boot?' MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:' MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:' @@ -476,6 +487,25 @@ def setup_grub(root_dir: str) -> None: render(grub_cfg_menu, grub.TMPL_GRUB_MENU, {}) render(grub_cfg_options, grub.TMPL_GRUB_OPTS, {}) +def get_cli_kernel_options(config_file: str) -> list: + config = ConfigTree(read_file(config_file)) + config_dict = loads(config.to_json()) + kernel_options = dict_search('system.option.kernel', config_dict) + cmdline_options = [] + + # XXX: This code path and if statements must be kept in sync with the Kernel + # option handling in system_options.py:generate(). This occurance is used + # for having the appropriate options passed to GRUB after an image upgrade! + if 'disable-mitigations' in kernel_options: + cmdline_options.append('mitigations=off') + if 'disable-power-saving' in kernel_options: + cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1') + if 'amd-pstate-driver' in kernel_options: + mode = kernel_options['amd-pstate-driver'] + cmdline_options.append( + f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}') + + return cmdline_options def configure_authentication(config_file: str, password: str) -> None: """Write encrypted password to config file @@ -490,10 +520,7 @@ def configure_authentication(config_file: str, password: str) -> None: plaintext exposed """ encrypted_password = linux_context.hash(password) - - with open(config_file) as f: - config_string = f.read() - + config_string = read_file(config_file) config = ConfigTree(config_string) config.set([ 'system', 'login', 'user', 'vyos', 'authentication', @@ -984,8 +1011,12 @@ def add_image(image_path: str, vrf: str = None, username: str = '', f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed') if not no_prompt: + versions = grub.version_list() while True: image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name) + if image_name in versions: + print(MSG_INPUT_IMAGE_NAME_TAKEN) + continue if image.validate_name(image_name): break print(MSG_WARN_IMAGE_NAME_WRONG) @@ -1040,6 +1071,12 @@ def add_image(image_path: str, vrf: str = None, username: str = '', if set_as_default: grub.set_default(image_name, root_dir) + cmdline_options = get_cli_kernel_options( + f'{target_config_dir}/config.boot') + grub_util.update_kernel_cmdline_options(' '.join(cmdline_options), + root_dir=root_dir, + version=image_name) + except OSError as e: # if no space error, remove image dir and cleanup if e.errno == ENOSPC: diff --git a/src/services/vyos-conntrack-logger b/src/services/vyos-conntrack-logger index 9c31b465f..ec0e1f717 100755 --- a/src/services/vyos-conntrack-logger +++ b/src/services/vyos-conntrack-logger @@ -15,10 +15,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import argparse -import grp import logging import multiprocessing -import os import queue import signal import socket diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver index aba5ba9db..4419fc4a7 100755 --- a/src/services/vyos-domain-resolver +++ b/src/services/vyos-domain-resolver @@ -92,12 +92,14 @@ def resolve(domains, ipv6=False): for domain in domains: resolved = fqdn_resolve(domain, ipv6=ipv6) + cache_key = f'{domain}_ipv6' if ipv6 else domain + if resolved and cache: - domain_state[domain] = resolved + domain_state[cache_key] = resolved elif not resolved: - if domain not in domain_state: + if cache_key not in domain_state: continue - resolved = domain_state[domain] + resolved = domain_state[cache_key] ip_list = ip_list | resolved return ip_list diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 1ba90471e..44f03586c 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -233,10 +233,7 @@ # } import os -import sys -import time import json -import signal import traceback import re import logging @@ -245,7 +242,6 @@ import zmq from voluptuous import Schema, MultipleInvalid, Required, Any from collections import OrderedDict from vyos.utils.file import makedir -from vyos.utils.permission import chown from vyos.utils.permission import chmod_755 from vyos.utils.process import popen from vyos.utils.process import process_named_running diff --git a/src/systemd/netplug.service b/src/systemd/netplug.service new file mode 100644 index 000000000..928c553e8 --- /dev/null +++ b/src/systemd/netplug.service @@ -0,0 +1,9 @@ +[Unit] +Description=Network cable hotplug management daemon +Documentation=man:netplugd(8) +After=vyos-router.service + +[Service] +Type=forking +PIDFile=/run/netplugd.pid +ExecStart=/sbin/netplugd -c /etc/netplug/netplugd.conf -p /run/netplugd.pid diff --git a/src/systemd/vyos.target b/src/systemd/vyos.target index 47c91c1cc..c5d04891d 100644 --- a/src/systemd/vyos.target +++ b/src/systemd/vyos.target @@ -1,3 +1,3 @@ [Unit] Description=VyOS target -After=multi-user.target +After=multi-user.target vyos-grub-update.service |