summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/op-mode-standardized.json1
-rw-r--r--data/templates/dhcp-server/10-override.conf.j22
-rw-r--r--data/templates/frr/daemons.frr.tmpl2
-rw-r--r--data/templates/frr/nhrpd.frr.j262
-rw-r--r--data/templates/frr/nhrpd_nftables.conf.j246
-rw-r--r--data/templates/ipsec/swanctl/profile.j28
-rw-r--r--data/templates/login/pam_radius_auth.conf.j24
-rw-r--r--data/templates/nhrp/nftables.conf.j217
-rw-r--r--data/templates/nhrp/opennhrp.conf.j242
-rw-r--r--debian/control3
-rwxr-xr-xdebian/vyos-1x-smoketest.postinst6
-rw-r--r--interface-definitions/include/version/nhrp-version.xml.i3
-rw-r--r--interface-definitions/protocols_nhrp.xml.in186
-rw-r--r--op-mode-definitions/nhrp.xml.in73
-rw-r--r--python/vyos/frrender.py62
-rw-r--r--python/vyos/remote.py1
-rw-r--r--smoketest/config-tests/bgp-dmvpn-hub8
-rw-r--r--smoketest/config-tests/bgp-dmvpn-spoke14
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_nhrp.py78
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py210
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py46
-rwxr-xr-xsrc/conf_mode/interfaces_tunnel.py12
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py118
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py270
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py27
-rw-r--r--src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf1
-rw-r--r--src/migration-scripts/nhrp/0-to-1129
-rwxr-xr-xsrc/op_mode/dhcp.py2
-rwxr-xr-xsrc/op_mode/ipsec.py23
-rwxr-xr-xsrc/op_mode/nhrp.py101
-rwxr-xr-xsrc/op_mode/vtysh_wrapper.sh2
31 files changed, 972 insertions, 587 deletions
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json
index 35587b63c..170f0d259 100644
--- a/data/op-mode-standardized.json
+++ b/data/op-mode-standardized.json
@@ -19,7 +19,6 @@
"multicast.py",
"nat.py",
"neighbor.py",
-"nhrp.py",
"openconnect.py",
"openvpn.py",
"otp.py",
diff --git a/data/templates/dhcp-server/10-override.conf.j2 b/data/templates/dhcp-server/10-override.conf.j2
deleted file mode 100644
index 6cf9e0a11..000000000
--- a/data/templates/dhcp-server/10-override.conf.j2
+++ /dev/null
@@ -1,2 +0,0 @@
-[Unit]
-ConditionFileNotEmpty=
diff --git a/data/templates/frr/daemons.frr.tmpl b/data/templates/frr/daemons.frr.tmpl
index 3506528d2..835dc382b 100644
--- a/data/templates/frr/daemons.frr.tmpl
+++ b/data/templates/frr/daemons.frr.tmpl
@@ -30,7 +30,7 @@ isisd=yes
pimd=no
pim6d=yes
ldpd=yes
-nhrpd=no
+nhrpd=yes
eigrpd=no
babeld=yes
sharpd=no
diff --git a/data/templates/frr/nhrpd.frr.j2 b/data/templates/frr/nhrpd.frr.j2
new file mode 100644
index 000000000..2b2aba256
--- /dev/null
+++ b/data/templates/frr/nhrpd.frr.j2
@@ -0,0 +1,62 @@
+!
+{% if redirect is vyos_defined %}
+nhrp nflog-group {{ redirect }}
+{% endif %}
+{% if multicast is vyos_defined %}
+nhrp multicast-nflog-group {{ multicast }}
+{% endif %}
+{% if tunnel is vyos_defined %}
+{% for iface, iface_config in tunnel.items() %}
+interface {{ iface }}
+{% if iface_config.authentication is vyos_defined %}
+ ip nhrp authentication {{ iface_config.authentication }}
+{% endif %}
+{% if iface_config.holdtime is vyos_defined %}
+ ip nhrp holdtime {{ iface_config.holdtime }}
+{% endif %}
+{% if iface_config.map.tunnel_ip is vyos_defined %}
+{% for tunip, tunip_config in iface_config.map.tunnel_ip.items() %}
+{% if tunip_config.nbma is vyos_defined %}
+ ip nhrp map {{ tunip }} {{ tunip_config.nbma }}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% if iface_config.mtu is vyos_defined %}
+ ip nhrp mtu {{ iface_config.mtu }}
+{% endif %}
+{% if iface_config.multicast is vyos_defined %}
+{% for multicast_ip in iface_config.multicast %}
+ ip nhrp map multicast {{ multicast_ip }}
+{% endfor %}
+{% endif %}
+{% if iface_config.nhs.tunnel_ip is vyos_defined %}
+{% for tunip, tunip_config in iface_config.nhs.tunnel_ip.items() %}
+{% if tunip_config.nbma is vyos_defined %}
+{% for nbmaip in tunip_config.nbma %}
+ ip nhrp nhs {{ tunip }} nbma {{ nbmaip }}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% if iface_config.network_id is vyos_defined %}
+ ip nhrp network-id {{ iface_config.network_id }}
+{% endif %}
+{% if iface_config.redirect is vyos_defined %}
+ ip nhrp redirect
+{% endif %}
+{% if iface_config.registration_no_unique is vyos_defined %}
+ ip nhrp registration no-unique
+{% endif %}
+{% if iface_config.shortcut is vyos_defined %}
+ ip nhrp shortcut
+{% endif %}
+{% if iface_config.security_profile is vyos_defined %}
+ tunnel protection vici profile dmvpn-{{ iface_config.security_profile }}-{{ iface }}-child
+{% endif %}
+exit
+!
+{% endfor %}
+{% endif %}
+!
+exit
+!
diff --git a/data/templates/frr/nhrpd_nftables.conf.j2 b/data/templates/frr/nhrpd_nftables.conf.j2
new file mode 100644
index 000000000..6ae35ef52
--- /dev/null
+++ b/data/templates/frr/nhrpd_nftables.conf.j2
@@ -0,0 +1,46 @@
+#!/usr/sbin/nft -f
+
+table ip vyos_nhrp_multicast
+table ip vyos_nhrp_redirect
+delete table ip vyos_nhrp_multicast
+delete table ip vyos_nhrp_redirect
+{% if multicast is vyos_defined %}
+table ip vyos_nhrp_multicast {
+ chain VYOS_NHRP_MULTICAST_OUTPUT {
+ type filter hook output priority filter+10; policy accept;
+{% if tunnel is vyos_defined %}
+{% for tun, tunnel_conf in tunnel.items() %}
+{% if tunnel_conf.multicast is vyos_defined %}
+ oifname "{{ tun }}" ip daddr 224.0.0.0/24 counter log group {{ multicast }}
+ oifname "{{ tun }}" ip daddr 224.0.0.0/24 counter drop
+{% endif %}
+{% endfor %}
+{% endif %}
+ }
+ chain VYOS_NHRP_MULTICAST_FORWARD {
+ type filter hook forward priority filter+10; policy accept;
+{% if tunnel is vyos_defined %}
+{% for tun, tunnel_conf in tunnel.items() %}
+{% if tunnel_conf.multicast is vyos_defined %}
+ oifname "{{ tun }}" ip daddr 224.0.0.0/4 counter log group {{ multicast }}
+ oifname "{{ tun }}" ip daddr 224.0.0.0/4 counter drop
+{% endif %}
+{% endfor %}
+{% endif %}
+ }
+}
+{% endif %}
+{% if redirect is vyos_defined %}
+table ip vyos_nhrp_redirect {
+ chain VYOS_NHRP_REDIRECT_FORWARD {
+ type filter hook forward priority filter+10; policy accept;
+{% if tunnel is vyos_defined %}
+{% for tun, tunnel_conf in tunnel.items() %}
+{% if tunnel_conf.redirect is vyos_defined %}
+ iifname "{{ tun }}" oifname "{{ tun }}" meter loglimit-0 size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets } counter log group {{ redirect }}
+{% endif %}
+{% endfor %}
+{% endif %}
+ }
+}
+{% endif %}
diff --git a/data/templates/ipsec/swanctl/profile.j2 b/data/templates/ipsec/swanctl/profile.j2
index 8519a84f8..6a04b038a 100644
--- a/data/templates/ipsec/swanctl/profile.j2
+++ b/data/templates/ipsec/swanctl/profile.j2
@@ -22,16 +22,16 @@
}
{% endif %}
children {
- dmvpn {
+ dmvpn-{{ name }}-{{ interface }}-child {
esp_proposals = {{ esp | get_esp_ike_cipher(ike) | join(',') }}
rekey_time = {{ esp.lifetime }}s
rand_time = 540s
local_ts = dynamic[gre]
remote_ts = dynamic[gre]
mode = {{ esp.mode }}
-{% if ike.dead_peer_detection.action is vyos_defined %}
- dpd_action = {{ ike.dead_peer_detection.action }}
-{% endif %}
+ dpd_action = clear
+ close_action = none
+ start_action = none
{% if esp.compression is vyos_defined('enable') %}
ipcomp = yes
{% endif %}
diff --git a/data/templates/login/pam_radius_auth.conf.j2 b/data/templates/login/pam_radius_auth.conf.j2
index 75437ca71..f9b8d5e87 100644
--- a/data/templates/login/pam_radius_auth.conf.j2
+++ b/data/templates/login/pam_radius_auth.conf.j2
@@ -9,7 +9,7 @@
{% if address | is_ipv4 %}
{% set source_address.ipv4 = address %}
{% elif address | is_ipv6 %}
-{% set source_address.ipv6 = "[" + address + "]" %}
+{% set source_address.ipv6 = address %}
{% endif %}
{% endfor %}
{% endif %}
@@ -21,7 +21,7 @@
{% if server | is_ipv4 %}
{{ server }}:{{ options.port }} {{ "%-25s" | format(options.key) }} {{ "%-10s" | format(options.timeout) }} {{ source_address.ipv4 if source_address.ipv4 is vyos_defined }}
{% else %}
-[{{ server }}]:{{ options.port }} {{ "%-25s" | format(options.key) }} {{ "%-10s" | format(options.timeout) }} {{ source_address.ipv6 if source_address.ipv6 is vyos_defined }}
+{{ server | bracketize_ipv6 }}:{{ options.port }} {{ "%-25s" | format(options.key) }} {{ "%-10s" | format(options.timeout) }} {{ source_address.ipv6 if source_address.ipv6 is vyos_defined }}
{% endif %}
{% endfor %}
{% endif %}
diff --git a/data/templates/nhrp/nftables.conf.j2 b/data/templates/nhrp/nftables.conf.j2
deleted file mode 100644
index a0d1f6d4c..000000000
--- a/data/templates/nhrp/nftables.conf.j2
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/sbin/nft -f
-
-{% if first_install is not vyos_defined %}
-delete table ip vyos_nhrp_filter
-{% endif %}
-table ip vyos_nhrp_filter {
- chain VYOS_NHRP_OUTPUT {
- type filter hook output priority 10; policy accept;
-{% if tunnel is vyos_defined %}
-{% for tun, tunnel_conf in tunnel.items() %}
-{% if if_tunnel[tun].source_address is vyos_defined %}
- ip protocol gre ip saddr {{ if_tunnel[tun].source_address }} ip daddr 224.0.0.0/4 counter drop comment "VYOS_NHRP_{{ tun }}"
-{% endif %}
-{% endfor %}
-{% endif %}
- }
-}
diff --git a/data/templates/nhrp/opennhrp.conf.j2 b/data/templates/nhrp/opennhrp.conf.j2
deleted file mode 100644
index c040a8f14..000000000
--- a/data/templates/nhrp/opennhrp.conf.j2
+++ /dev/null
@@ -1,42 +0,0 @@
-{# j2lint: disable=jinja-variable-format #}
-# Created by VyOS - manual changes will be overwritten
-
-{% if tunnel is vyos_defined %}
-{% for name, tunnel_conf in tunnel.items() %}
-{% set type = 'spoke' if tunnel_conf.map is vyos_defined or tunnel_conf.dynamic_map is vyos_defined else 'hub' %}
-{% set profile_name = profile_map[name] if profile_map is vyos_defined and name in profile_map else '' %}
-interface {{ name }} #{{ type }} {{ profile_name }}
-{% if tunnel_conf.map is vyos_defined %}
-{% for map, map_conf in tunnel_conf.map.items() %}
-{% set cisco = ' cisco' if map_conf.cisco is vyos_defined else '' %}
-{% set register = ' register' if map_conf.register is vyos_defined else '' %}
- map {{ map }} {{ map_conf.nbma_address }}{{ register }}{{ cisco }}
-{% endfor %}
-{% endif %}
-{% if tunnel_conf.dynamic_map is vyos_defined %}
-{% for map, map_conf in tunnel_conf.dynamic_map.items() %}
- dynamic-map {{ map }} {{ map_conf.nbma_domain_name }}
-{% endfor %}
-{% endif %}
-{% if tunnel_conf.cisco_authentication is vyos_defined %}
- cisco-authentication {{ tunnel_conf.cisco_authentication }}
-{% endif %}
-{% if tunnel_conf.holding_time is vyos_defined %}
- holding-time {{ tunnel_conf.holding_time }}
-{% endif %}
-{% if tunnel_conf.multicast is vyos_defined %}
- multicast {{ tunnel_conf.multicast }}
-{% endif %}
-{% for key in ['non_caching', 'redirect', 'shortcut', 'shortcut_destination'] %}
-{% if key in tunnel_conf %}
- {{ key | replace("_", "-") }}
-{% endif %}
-{% endfor %}
-{% if tunnel_conf.shortcut_target is vyos_defined %}
-{% for target, shortcut_conf in tunnel_conf.shortcut_target.items() %}
- shortcut-target {{ target }}{{ ' holding-time ' + shortcut_conf.holding_time if shortcut_conf.holding_time is vyos_defined }}
-{% endfor %}
-{% endif %}
-
-{% endfor %}
-{% endif %}
diff --git a/debian/control b/debian/control
index a0d475d56..76fe5c331 100644
--- a/debian/control
+++ b/debian/control
@@ -172,9 +172,6 @@ Depends:
frr-rpki-rtrlib,
frr-snmp,
# End "protocols *"
-# For "protocols nhrp" (part of DMVPN)
- opennhrp,
-# End "protocols nhrp"
# For "protocols igmp-proxy"
igmpproxy,
# End "protocols igmp-proxy"
diff --git a/debian/vyos-1x-smoketest.postinst b/debian/vyos-1x-smoketest.postinst
index 08d6d7d4f..bff73796c 100755
--- a/debian/vyos-1x-smoketest.postinst
+++ b/debian/vyos-1x-smoketest.postinst
@@ -11,3 +11,9 @@ TACPLUS_PATH="/usr/share/vyos/tacplus-alpine.tar"
if [[ ! -f $TACPLUS_PATH ]]; then
skopeo copy --additional-tag "$TACPLUS_TAG" "docker://$TACPLUS_TAG" "docker-archive:/$TACPLUS_PATH"
fi
+
+RADIUS_TAG="docker.io/dchidell/radius-web:latest"
+RADIUS_PATH="/usr/share/vyos/radius-latest.tar"
+if [[ ! -f $RADIUS_PATH ]]; then
+ skopeo copy --additional-tag "$RADIUS_TAG" "docker://$RADIUS_TAG" "docker-archive:/$RADIUS_PATH"
+fi
diff --git a/interface-definitions/include/version/nhrp-version.xml.i b/interface-definitions/include/version/nhrp-version.xml.i
new file mode 100644
index 000000000..7f6f3c4f7
--- /dev/null
+++ b/interface-definitions/include/version/nhrp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/nhrp-version.xml.i -->
+<syntaxVersion component='nhrp' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/protocols_nhrp.xml.in b/interface-definitions/protocols_nhrp.xml.in
index d7663c095..5304fbd78 100644
--- a/interface-definitions/protocols_nhrp.xml.in
+++ b/interface-definitions/protocols_nhrp.xml.in
@@ -20,115 +20,163 @@
</valueHelp>
</properties>
<children>
- <leafNode name="cisco-authentication">
+ <node name="map">
<properties>
- <help>Pass phrase for cisco authentication</help>
- <valueHelp>
- <format>txt</format>
- <description>Pass phrase for cisco authentication</description>
- </valueHelp>
- <constraint>
- <regex>[^[:space:]]{1,8}</regex>
- </constraint>
- <constraintErrorMessage>Password should contain up to eight non-whitespace characters</constraintErrorMessage>
- </properties>
- </leafNode>
- <tagNode name="dynamic-map">
- <properties>
- <help>Set an HUB tunnel address</help>
- <valueHelp>
- <format>ipv4net</format>
- <description>Set the IP address and prefix length</description>
- </valueHelp>
+ <help>Map tunnel IP to NBMA </help>
</properties>
<children>
- <leafNode name="nbma-domain-name">
+ <tagNode name ="tunnel-ip">
<properties>
- <help>Set HUB fqdn (nbma-address - fqdn)</help>
+ <help>Set a NHRP tunnel address</help>
<valueHelp>
- <format>&lt;fqdn&gt;</format>
- <description>Set the external HUB fqdn</description>
+ <format>ipv4</format>
+ <description>Set the IP address to map</description>
</valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
</properties>
- </leafNode>
+ <children>
+ <leafNode name="nbma">
+ <properties>
+ <help>Set NHRP NBMA address to map</help>
+ <completionHelp>
+ <list>local</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Set the IP address to map</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local</format>
+ <description>Set the local address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ <regex>(local)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
</children>
- </tagNode>
- <leafNode name="holding-time">
+ </node>
+ <node name="nhs">
<properties>
- <help>Holding time in seconds</help>
- </properties>
- </leafNode>
- <tagNode name="map">
- <properties>
- <help>Set an HUB tunnel address</help>
+ <help>Map tunnel IP to NBMA of Next Hop Server</help>
</properties>
<children>
- <leafNode name="cisco">
- <properties>
- <help>If the statically mapped peer is running Cisco IOS, specify this</help>
- <valueless/>
- </properties>
- </leafNode>
- <leafNode name="nbma-address">
+ <tagNode name ="tunnel-ip">
<properties>
- <help>Set HUB address (nbma-address - external hub address or fqdn)</help>
- </properties>
- </leafNode>
- <leafNode name="register">
- <properties>
- <help>Specifies that Registration Request should be sent to this peer on startup</help>
- <valueless/>
+ <help>Set a NHRP NHS tunnel address</help>
+ <completionHelp>
+ <list>dynamic</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Set the IP address to map</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dynamic</format>
+ <description> Set Next Hop Server to have a dynamic address </description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ <regex>(dynamic)</regex>
+ </constraint>
</properties>
- </leafNode>
+ <children>
+ <leafNode name="nbma">
+ <properties>
+ <help>Set NHRP NBMA address of NHS</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Set the IP address to map</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
</children>
- </tagNode>
+ </node>
<leafNode name="multicast">
<properties>
- <help>Set multicast for NHRP</help>
+ <help>Map multicast to NBMA</help>
<completionHelp>
- <list>dynamic nhs</list>
+ <list>dynamic</list>
</completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Set the IP address to map(IP|FQDN)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dynamic</format>
+ <description>NBMA address is learnt dynamically</description>
+ </valueHelp>
<constraint>
- <regex>(dynamic|nhs)</regex>
+ <validator name="ip-address"/>
+ <regex>(dynamic)</regex>
</constraint>
+ <multi/>
</properties>
</leafNode>
- <leafNode name="non-caching">
+ <leafNode name="registration-no-unique">
<properties>
- <help>This can be used to reduce memory consumption on big NBMA subnets</help>
+ <help>Don't set unique flag</help>
<valueless/>
</properties>
</leafNode>
- <leafNode name="redirect">
+ <leafNode name="authentication">
<properties>
- <help>Enable sending of Cisco style NHRP Traffic Indication packets</help>
- <valueless/>
+ <help>NHRP authentication</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Pass phrase for NHRP authentication</description>
+ </valueHelp>
+ <constraint>
+ <regex>[^[:space:]]{1,8}</regex>
+ </constraint>
+ <constraintErrorMessage>Password should contain up to eight non-whitespace characters</constraintErrorMessage>
</properties>
</leafNode>
- <leafNode name="shortcut-destination">
+ <leafNode name="holdtime">
<properties>
- <help>This instructs opennhrp to reply with authorative answers on NHRP Resolution Requests destined to addresses in this interface</help>
- <valueless/>
+ <help>Holding time in seconds</help>
+ <valueHelp>
+ <format>u32:1-65000</format>
+ <description>ring buffer size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65000"/>
+ </constraint>
</properties>
</leafNode>
- <tagNode name="shortcut-target">
+ <leafNode name="redirect">
<properties>
- <help>Defines an off-NBMA network prefix for which the GRE interface will act as a gateway</help>
+ <help>Enable sending of Cisco style NHRP Traffic Indication packets</help>
+ <valueless/>
</properties>
- <children>
- <leafNode name="holding-time">
- <properties>
- <help>Holding time in seconds</help>
- </properties>
- </leafNode>
- </children>
- </tagNode>
+ </leafNode>
<leafNode name="shortcut">
<properties>
<help>Enable creation of shortcut routes. A received NHRP Traffic Indication will trigger the resolution and establishment of a shortcut route</help>
<valueless/>
</properties>
</leafNode>
+ #include <include/interface/mtu-68-16000.xml.i>
+ <leafNode name="network-id">
+ <properties>
+ <help>NHRP network id</help>
+ <valueHelp>
+ <format>&lt;1-4294967295&gt;</format>
+ <description>NHRP network id</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
</children>
</tagNode>
</children>
diff --git a/op-mode-definitions/nhrp.xml.in b/op-mode-definitions/nhrp.xml.in
index 11a4b8814..4ae1972c6 100644
--- a/op-mode-definitions/nhrp.xml.in
+++ b/op-mode-definitions/nhrp.xml.in
@@ -2,38 +2,26 @@
<interfaceDefinition>
<node name="reset">
<children>
- <node name="nhrp">
- <properties>
- <help>Clear/Purge NHRP entries</help>
- </properties>
+ <node name="ip">
<children>
- <node name="flush">
+ <node name="nhrp">
<properties>
- <help>Clear all non-permanent entries</help>
+ <help>Clear/Purge NHRP entries</help>
</properties>
<children>
- <tagNode name="tunnel">
+ <leafNode name="cache">
<properties>
- <help>Clear all non-permanent entries</help>
+ <help>Clear Dynamic cache entries</help>
</properties>
- <command>sudo opennhrpctl flush dev $5 || echo OpenNHRP is not running.</command>
- </tagNode>
- </children>
- <command>sudo opennhrpctl flush || echo OpenNHRP is not running.</command>
- </node>
- <node name="purge">
- <properties>
- <help>Purge entries from NHRP cache</help>
- </properties>
- <children>
- <tagNode name="tunnel">
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ <leafNode name="shortcut">
<properties>
- <help>Purge all entries from NHRP cache</help>
+ <help>Clear Shortcut entries</help>
</properties>
- <command>sudo opennhrpctl purge dev $5 || echo OpenNHRP is not running.</command>
- </tagNode>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
</children>
- <command>sudo opennhrpctl purge || echo OpenNHRP is not running.</command>
</node>
</children>
</node>
@@ -41,25 +29,38 @@
</node>
<node name="show">
<children>
- <node name="nhrp">
+ <node name="ip">
<properties>
- <help>Show NHRP (Next Hop Resolution Protocol) information</help>
+ <help>Show IPv4 routing information</help>
</properties>
<children>
- <leafNode name="interface">
+ <node name="nhrp">
<properties>
- <help>Show NHRP interface connection information</help>
+ <help>Show NHRP (Next Hop Resolution Protocol) information</help>
</properties>
- <command>${vyos_op_scripts_dir}/nhrp.py show_interface</command>
- </leafNode>
- <leafNode name="tunnel">
- <properties>
- <help>Show NHRP tunnel connection information</help>
- </properties>
- <command>${vyos_op_scripts_dir}/nhrp.py show_tunnel</command>
- </leafNode>
+ <children>
+ <leafNode name="cache">
+ <properties>
+ <help>Forwarding cache information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ <leafNode name="nhs">
+ <properties>
+ <help>Next hop server information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ <leafNode name="shortcut">
+ <properties>
+ <help>Shortcut information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ </children>
+ </node>
</children>
</node>
- </children>
+ </children>
</node>
</interfaceDefinition>
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index 544983b2c..ba44978d1 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2024-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
@@ -52,6 +52,7 @@ pim6_daemon = 'pim6d'
rip_daemon = 'ripd'
ripng_daemon = 'ripngd'
zebra_daemon = 'zebra'
+nhrp_daemon = 'nhrpd'
def get_frrender_dict(conf, argv=None) -> dict:
from copy import deepcopy
@@ -147,6 +148,50 @@ def get_frrender_dict(conf, argv=None) -> dict:
pim = config_dict_merge(default_values, pim)
return pim
+ def dict_helper_nhrp_defaults(nhrp):
+ # NFLOG group numbers which are used in netfilter firewall rules and
+ # in the global config in FRR.
+ # https://docs.frrouting.org/en/latest/nhrpd.html#hub-functionality
+ # https://docs.frrouting.org/en/latest/nhrpd.html#multicast-functionality
+ # Use nflog group number for NHRP redirects = 1
+ # Use nflog group number from MULTICAST traffic = 2
+ nflog_redirect = 1
+ nflog_multicast = 2
+
+ nhrp = conf.merge_defaults(nhrp, recursive=True)
+
+ nhrp_tunnel = conf.get_config_dict(['interfaces', 'tunnel'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ if nhrp_tunnel: nhrp.update({'if_tunnel': nhrp_tunnel})
+
+ for intf, intf_config in nhrp['tunnel'].items():
+ if 'multicast' in intf_config:
+ nhrp['multicast'] = nflog_multicast
+ if 'redirect' in intf_config:
+ nhrp['redirect'] = nflog_redirect
+
+ ##Add ipsec profile config to nhrp configuration to apply encryption
+ profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ for name, profile_conf in profile.items():
+ if 'disable' in profile_conf:
+ continue
+ if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']:
+ interfaces = profile_conf['bind']['tunnel']
+ if isinstance(interfaces, str):
+ interfaces = [interfaces]
+ for interface in interfaces:
+ if dict_search(f'tunnel.{interface}', nhrp):
+ nhrp['tunnel'][interface][
+ 'security_profile'] = name
+ return nhrp
+
# Ethernet and bonding interfaces can participate in EVPN which is configured via FRR
tmp = {}
for if_type in ['ethernet', 'bonding']:
@@ -364,6 +409,18 @@ def get_frrender_dict(conf, argv=None) -> dict:
elif conf.exists_effective(static_cli_path):
dict.update({'static' : {'deleted' : ''}})
+ # We need to check the CLI if the NHRP node is present and thus load in all the default
+ # values present on the CLI - that's why we have if conf.exists()
+ nhrp_cli_path = ['protocols', 'nhrp']
+ if conf.exists(nhrp_cli_path):
+ nhrp = conf.get_config_dict(nhrp_cli_path, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ nhrp = dict_helper_nhrp_defaults(nhrp)
+ dict.update({'nhrp' : nhrp})
+ elif conf.exists_effective(nhrp_cli_path):
+ dict.update({'nhrp' : {'deleted' : ''}})
+
# T3680 - get a list of all interfaces currently configured to use DHCP
tmp = get_dhcp_interfaces(conf)
if tmp:
@@ -626,6 +683,9 @@ class FRRender:
if 'ipv6' in config_dict and 'deleted' not in config_dict['ipv6']:
output += render_to_string('frr/zebra.route-map.frr.j2', config_dict['ipv6'])
output += '\n'
+ if 'nhrp' in config_dict and 'deleted' not in config_dict['nhrp']:
+ output += render_to_string('frr/nhrpd.frr.j2', config_dict['nhrp'])
+ output += '\n'
return output
debug('FRR: START CONFIGURATION RENDERING')
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index d87fd24f6..c54fb6031 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -363,6 +363,7 @@ class GitC:
# environment vars for our git commands
env = {
+ **os.environ,
"GIT_TERMINAL_PROMPT": "0",
"GIT_AUTHOR_NAME": name,
"GIT_AUTHOR_EMAIL": email,
diff --git a/smoketest/config-tests/bgp-dmvpn-hub b/smoketest/config-tests/bgp-dmvpn-hub
index 30521520a..99f3799a4 100644
--- a/smoketest/config-tests/bgp-dmvpn-hub
+++ b/smoketest/config-tests/bgp-dmvpn-hub
@@ -4,7 +4,7 @@ set interfaces ethernet eth0 duplex 'auto'
set interfaces ethernet eth1 speed 'auto'
set interfaces ethernet eth1 duplex 'auto'
set interfaces loopback lo
-set interfaces tunnel tun0 address '192.168.254.62/26'
+set interfaces tunnel tun0 address '192.168.254.62/32'
set interfaces tunnel tun0 enable-multicast
set interfaces tunnel tun0 encapsulation 'gre'
set interfaces tunnel tun0 parameters ip key '1'
@@ -21,10 +21,12 @@ set protocols bgp peer-group DMVPN address-family ipv4-unicast
set protocols bgp system-as '65000'
set protocols bgp timers holdtime '30'
set protocols bgp timers keepalive '10'
-set protocols nhrp tunnel tun0 cisco-authentication 'secret'
-set protocols nhrp tunnel tun0 holding-time '300'
+set protocols nhrp tunnel tun0 authentication 'secret'
+set protocols nhrp tunnel tun0 holdtime '300'
set protocols nhrp tunnel tun0 multicast 'dynamic'
+set protocols nhrp tunnel tun0 network-id '1'
set protocols nhrp tunnel tun0 redirect
+set protocols nhrp tunnel tun0 registration-no-unique
set protocols nhrp tunnel tun0 shortcut
set protocols static route 0.0.0.0/0 next-hop 100.64.10.0
set protocols static route 172.20.0.0/16 blackhole distance '200'
diff --git a/smoketest/config-tests/bgp-dmvpn-spoke b/smoketest/config-tests/bgp-dmvpn-spoke
index d1c7bc7c0..e4fb82a0e 100644
--- a/smoketest/config-tests/bgp-dmvpn-spoke
+++ b/smoketest/config-tests/bgp-dmvpn-spoke
@@ -5,7 +5,7 @@ set interfaces pppoe pppoe1 authentication password 'cpe-1'
set interfaces pppoe pppoe1 authentication username 'cpe-1'
set interfaces pppoe pppoe1 no-peer-dns
set interfaces pppoe pppoe1 source-interface 'eth0.7'
-set interfaces tunnel tun0 address '192.168.254.1/26'
+set interfaces tunnel tun0 address '192.168.254.1/32'
set interfaces tunnel tun0 enable-multicast
set interfaces tunnel tun0 encapsulation 'gre'
set interfaces tunnel tun0 parameters ip key '1'
@@ -21,14 +21,16 @@ set protocols bgp parameters log-neighbor-changes
set protocols bgp system-as '65001'
set protocols bgp timers holdtime '30'
set protocols bgp timers keepalive '10'
-set protocols nhrp tunnel tun0 cisco-authentication 'secret'
-set protocols nhrp tunnel tun0 holding-time '300'
-set protocols nhrp tunnel tun0 map 192.168.254.62/26 nbma-address '100.64.10.1'
-set protocols nhrp tunnel tun0 map 192.168.254.62/26 register
-set protocols nhrp tunnel tun0 multicast 'nhs'
+set protocols nhrp tunnel tun0 authentication 'secret'
+set protocols nhrp tunnel tun0 holdtime '300'
+set protocols nhrp tunnel tun0 multicast '100.64.10.1'
+set protocols nhrp tunnel tun0 network-id '1'
+set protocols nhrp tunnel tun0 nhs tunnel-ip 192.168.254.62 nbma '100.64.10.1'
set protocols nhrp tunnel tun0 redirect
+set protocols nhrp tunnel tun0 registration-no-unique
set protocols nhrp tunnel tun0 shortcut
set protocols static route 172.17.0.0/16 blackhole distance '200'
+set protocols static route 192.168.254.0/26 next-hop 192.168.254.62 distance '250'
set service dhcp-server shared-network-name LAN-3 subnet 172.17.1.0/24 option default-router '172.17.1.1'
set service dhcp-server shared-network-name LAN-3 subnet 172.17.1.0/24 option name-server '172.17.1.1'
set service dhcp-server shared-network-name LAN-3 subnet 172.17.1.0/24 range 0 start '172.17.1.100'
diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py
index 43ae4abf2..f6d1f1da5 100755
--- a/smoketest/scripts/cli/test_protocols_nhrp.py
+++ b/smoketest/scripts/cli/test_protocols_nhrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-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,6 +25,7 @@ from vyos.utils.file import read_file
tunnel_path = ['interfaces', 'tunnel']
nhrp_path = ['protocols', 'nhrp']
vpn_path = ['vpn', 'ipsec']
+PROCESS_NAME = 'nhrpd'
class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -41,29 +42,41 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
self.cli_delete(tunnel_path)
self.cli_commit()
- def test_config(self):
+ def test_01_nhrp_config(self):
tunnel_if = "tun100"
- tunnel_source = "192.0.2.1"
+ tunnel_ip = '172.16.253.134/32'
+ tunnel_source = "192.0.2.134"
tunnel_encapsulation = "gre"
esp_group = "ESP-HUB"
ike_group = "IKE-HUB"
nhrp_secret = "vyos123"
nhrp_profile = "NHRPVPN"
+ nhrp_holdtime = '300'
+ nhs_tunnelip = '172.16.253.1'
+ nhs_nbmaip = '192.0.2.1'
+ map_tunnelip = '172.16.253.135'
+ map_nbmaip = "192.0.2.135"
+ nhrp_networkid = '1'
ipsec_secret = "secret"
-
+ multicat_log_group = '2'
+ redirect_log_group = '1'
# Tunnel
- self.cli_set(tunnel_path + [tunnel_if, "address", "172.16.253.134/29"])
+ self.cli_set(tunnel_path + [tunnel_if, "address", tunnel_ip])
self.cli_set(tunnel_path + [tunnel_if, "encapsulation", tunnel_encapsulation])
self.cli_set(tunnel_path + [tunnel_if, "source-address", tunnel_source])
self.cli_set(tunnel_path + [tunnel_if, "enable-multicast"])
self.cli_set(tunnel_path + [tunnel_if, "parameters", "ip", "key", "1"])
# NHRP
- self.cli_set(nhrp_path + ["tunnel", tunnel_if, "cisco-authentication", nhrp_secret])
- self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holding-time", "300"])
- self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", "dynamic"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "authentication", nhrp_secret])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holdtime", nhrp_holdtime])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", nhs_tunnelip])
self.cli_set(nhrp_path + ["tunnel", tunnel_if, "redirect"])
self.cli_set(nhrp_path + ["tunnel", tunnel_if, "shortcut"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "registration-no-unique"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "network-id", nhrp_networkid])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "nhs", "tunnel-ip", nhs_tunnelip, "nbma", nhs_nbmaip])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "map", "tunnel-ip", map_tunnelip, "nbma", map_nbmaip])
# IKE/ESP Groups
self.cli_set(vpn_path + ["esp-group", esp_group, "lifetime", "1800"])
@@ -93,29 +106,40 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- opennhrp_lines = [
- f'interface {tunnel_if} #hub {nhrp_profile}',
- f'cisco-authentication {nhrp_secret}',
- f'holding-time 300',
- f'shortcut',
- f'multicast dynamic',
- f'redirect'
+ frrconfig = self.getFRRconfig(f'interface {tunnel_if}', endsection='^exit')
+ self.assertIn(f'interface {tunnel_if}', frrconfig)
+ self.assertIn(f' ip nhrp authentication {nhrp_secret}', frrconfig)
+ self.assertIn(f' ip nhrp holdtime {nhrp_holdtime}', frrconfig)
+ self.assertIn(f' ip nhrp map multicast {nhs_tunnelip}', frrconfig)
+ self.assertIn(f' ip nhrp redirect', frrconfig)
+ self.assertIn(f' ip nhrp registration no-unique', frrconfig)
+ self.assertIn(f' ip nhrp shortcut', frrconfig)
+ self.assertIn(f' ip nhrp network-id {nhrp_networkid}', frrconfig)
+ self.assertIn(f' ip nhrp nhs {nhs_tunnelip} nbma {nhs_nbmaip}', frrconfig)
+ self.assertIn(f' ip nhrp map {map_tunnelip} {map_nbmaip}', frrconfig)
+ self.assertIn(f' tunnel protection vici profile dmvpn-{nhrp_profile}-{tunnel_if}-child',
+ frrconfig)
+
+ nftables_search_multicast = [
+ ['chain VYOS_NHRP_MULTICAST_OUTPUT'],
+ ['type filter hook output priority filter + 10; policy accept;'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/24', 'counter', f'log group {multicat_log_group}'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/24', 'counter', 'drop'],
+ ['chain VYOS_NHRP_MULTICAST_FORWARD'],
+ ['type filter hook output priority filter + 10; policy accept;'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/4', 'counter', f'log group {multicat_log_group}'],
+ [f'oifname "{tunnel_if}"', 'ip daddr 224.0.0.0/4', 'counter', 'drop']
]
- tmp_opennhrp_conf = read_file('/run/opennhrp/opennhrp.conf')
-
- for line in opennhrp_lines:
- self.assertIn(line, tmp_opennhrp_conf)
-
- firewall_matches = [
- f'ip protocol {tunnel_encapsulation}',
- f'ip saddr {tunnel_source}',
- f'ip daddr 224.0.0.0/4',
- f'comment "VYOS_NHRP_{tunnel_if}"'
+ nftables_search_redirect = [
+ ['chain VYOS_NHRP_REDIRECT_FORWARD'],
+ ['type filter hook forward priority filter + 10; policy accept;'],
+ [f'iifname "{tunnel_if}" oifname "{tunnel_if}"', 'meter loglimit-0 size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets }', 'counter', f'log group {redirect_log_group}']
]
+ self.verify_nftables(nftables_search_multicast, 'ip vyos_nhrp_multicast')
+ self.verify_nftables(nftables_search_redirect, 'ip vyos_nhrp_redirect')
- self.assertTrue(find_nftables_rule('ip vyos_nhrp_filter', 'VYOS_NHRP_OUTPUT', firewall_matches) is not None)
- self.assertTrue(process_named_running('opennhrp'))
+ self.assertTrue(process_named_running(PROCESS_NAME))
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 f6a2c3cb3..d79f5521c 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -31,17 +31,19 @@ from subprocess import PIPE
from pwd import getpwall
from vyos.configsession import ConfigSessionError
+from vyos.configquery import ConfigTreeQuery
from vyos.utils.auth import get_current_user
from vyos.utils.process import cmd
-from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
from vyos.utils.file import write_file
from vyos.template import inc_ip
+from vyos.template import is_ipv6
+from vyos.xml_ref import default_value
base_path = ['system', 'login']
users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice']
-SSH_PROCESS_NAME = 'sshd'
+ssh_test_command = '/opt/vyatta/bin/vyatta-op-cmd-wrapper show version'
ssh_pubkey = """
AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF
@@ -57,7 +59,6 @@ TTSb0X1zPGxPIRFy5GoGtO9Mm5h4OZk=
tac_image = 'docker.io/lfkeitel/tacacs_plus:alpine'
tac_image_path = '/usr/share/vyos/tacplus-alpine.tar'
-
TAC_PLUS_TMPL_SRC = """
id = spawnd {
debug redirect = /dev/stdout
@@ -100,6 +101,25 @@ id = tac_plus {
member = admin
}
}
+
+"""
+
+radius_image = 'docker.io/dchidell/radius-web:latest'
+radius_image_path = '/usr/share/vyos/radius-latest.tar'
+RADIUS_CLIENTS_TMPL_SRC = """
+client SMOKETEST {
+ secret = {{ radius_key }}
+ nastype = other
+ ipaddr = {{ source_address }}
+}
+
+"""
+RADIUS_USERS_TMPL_SRC = """
+# User configuration
+{{ username }} Cleartext-Password := "{{ password }}"
+ Service-Type = NAS-Prompt-User,
+ Cisco-AVPair = "shell:priv-lvl=15"
+
"""
class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
@@ -112,16 +132,36 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
cls.cli_delete(cls, base_path + ['radius'])
cls.cli_delete(cls, base_path + ['tacacs'])
- # Load image for smoketest provided in vyos-1x-smoketest
+ # Load images for smoketest provided in vyos-1x-smoketest
if not os.path.exists(tac_image_path):
cls.fail(cls, f'{tac_image} image not available')
cmd(f'sudo podman load -i {tac_image_path}')
+ if not os.path.exists(radius_image_path):
+ cls.fail(cls, f'{radius_image} image not available')
+ cmd(f'sudo podman load -i {radius_image_path}')
+
+ cls.ssh_test_command_result = cls.op_mode(cls, ['show', 'version'])
+
+ # Dynamically start SSH service if it's not running
+ config = ConfigTreeQuery()
+ cls.is_sshd_pre_test = config.exists(['service', 'sshd'])
+ if not cls.is_sshd_pre_test:
+ # Start SSH service
+ cls.cli_set(cls, ['service', 'ssh'])
+
@classmethod
def tearDownClass(cls):
+ # Stop SSH service - if it was not running before starting the test
+ if not cls.is_sshd_pre_test:
+ cls.cli_set(cls, ['service', 'ssh'])
+ cls.cli_commit(cls)
+
super(TestSystemLogin, cls).tearDownClass()
- # Cleanup podman image
+
+ # Cleanup container images
cmd(f'sudo podman image rm -f {tac_image}')
+ cmd(f'sudo podman image rm -f {radius_image}')
def tearDown(self):
# Delete individual users from configuration
@@ -152,9 +192,6 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path + ['user', system_user])
def test_system_login_user(self):
- # Check if user can be created and we can SSH to localhost
- self.cli_set(['service', 'ssh', 'port', '22'])
-
for user in users:
name = f'VyOS Roxx {user}'
home_dir = f'/tmp/smoketest/{user}'
@@ -240,71 +277,71 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'{option}=y', kernel_config)
def test_system_login_radius_ipv4(self):
- # Verify generated RADIUS configuration files
-
- radius_key = 'VyOSsecretVyOS'
- radius_server = '172.16.100.10'
- radius_source = '127.0.0.1'
- radius_port = '2000'
- radius_timeout = '1'
-
- self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
- self.cli_set(base_path + ['radius', 'source-address', radius_source])
- self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+ radius_servers = ['100.64.0.4', '100.64.0.5']
+ radius_source = '100.64.0.1'
+ self._system_login_radius_test_helper(radius_servers, radius_source)
- # check validate() - Only one IPv4 source-address supported
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
-
- self.cli_commit()
+ def test_system_login_radius_ipv6(self):
+ radius_servers = ['2001:db8::4', '2001:db8::5']
+ radius_source = '2001:db8::1'
+ self._system_login_radius_test_helper(radius_servers, radius_source)
- # this file must be read with higher permissions
- pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
- tmp = re.findall(r'\n?{}:{}\s+{}\s+{}\s+{}'.format(radius_server,
- radius_port, radius_key, radius_timeout,
- radius_source), pam_radius_auth_conf)
- self.assertTrue(tmp)
+ def _system_login_radius_test_helper(self, radius_servers: list, radius_source: str):
+ # Verify generated RADIUS configuration files
+ radius_key = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))
- # required, static options
- self.assertIn('priv-lvl 15', pam_radius_auth_conf)
- self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf)
+ default_port = default_value(base_path + ['radius', 'server', radius_servers[0], 'port'])
+ default_timeout = default_value(base_path + ['radius', 'server', radius_servers[0], 'timeout'])
- # PAM
- pam_common_account = read_file('/etc/pam.d/common-account')
- self.assertIn('pam_radius_auth.so', pam_common_account)
+ dummy_if = 'dum12760'
- pam_common_auth = read_file('/etc/pam.d/common-auth')
- self.assertIn('pam_radius_auth.so', pam_common_auth)
+ # Load container image for FreeRADIUS server
+ radius_config = '/tmp/smoketest-radius-server'
+ radius_container_path = ['container', 'name', 'radius-1']
- pam_common_session = read_file('/etc/pam.d/common-session')
- self.assertIn('pam_radius_auth.so', pam_common_session)
-
- pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive')
- self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive)
+ # Generate random string with 10 digits
+ username = 'radius-admin'
+ password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))
+ radius_source_mask = '32'
+ if is_ipv6(radius_source):
+ radius_source_mask = '128'
+ radius_test_user = {
+ 'username' : username,
+ 'password' : password,
+ 'radius_key' : radius_key,
+ 'source_address' : f'{radius_source}/{radius_source_mask}'
+ }
- # NSS
- nsswitch_conf = read_file('/etc/nsswitch.conf')
- tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf)
- self.assertTrue(tmp)
+ tmpl = jinja2.Template(RADIUS_CLIENTS_TMPL_SRC)
+ write_file(f'{radius_config}/clients.cfg', tmpl.render(radius_test_user))
- tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
- self.assertTrue(tmp)
+ tmpl = jinja2.Template(RADIUS_USERS_TMPL_SRC)
+ write_file(f'{radius_config}/users', tmpl.render(radius_test_user))
- def test_system_login_radius_ipv6(self):
- # Verify generated RADIUS configuration files
+ # Start tac_plus container
+ self.cli_set(radius_container_path + ['allow-host-networks'])
+ self.cli_set(radius_container_path + ['image', radius_image])
+ self.cli_set(radius_container_path + ['volume', 'clients', 'destination', '/etc/raddb/clients.conf'])
+ self.cli_set(radius_container_path + ['volume', 'clients', 'mode', 'ro'])
+ self.cli_set(radius_container_path + ['volume', 'clients', 'source', f'{radius_config}/clients.cfg'])
+ self.cli_set(radius_container_path + ['volume', 'users', 'destination', '/etc/raddb/users'])
+ self.cli_set(radius_container_path + ['volume', 'users', 'mode', 'ro'])
+ self.cli_set(radius_container_path + ['volume', 'users', 'source', f'{radius_config}/users'])
- radius_key = 'VyOS-VyOS'
- radius_server = '2001:db8::1'
- radius_source = '::1'
- radius_port = '4000'
- radius_timeout = '4'
+ # Start container
+ self.cli_commit()
- self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
- self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
+ # Deinfine RADIUS servers
+ for radius_server in radius_servers:
+ # Use this system as "remote" RADIUS server
+ dummy_address_mask = '32'
+ if is_ipv6(radius_server):
+ dummy_address_mask = '128'
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_server}/{dummy_address_mask}'])
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
+
+ # Define RADIUS traffic source address
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_source}/{radius_source_mask}'])
self.cli_set(base_path + ['radius', 'source-address', radius_source])
self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
@@ -317,10 +354,13 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
# this file must be read with higher permissions
pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
- tmp = re.findall(r'\n?\[{}\]:{}\s+{}\s+{}\s+\[{}\]'.format(radius_server,
- radius_port, radius_key, radius_timeout,
- radius_source), pam_radius_auth_conf)
- self.assertTrue(tmp)
+
+ for radius_server in radius_servers:
+ if is_ipv6(radius_server):
+ # it is essential to escape the [] brackets when searching with a regex
+ radius_server = rf'\[{radius_server}\]'
+ tmp = re.findall(rf'\n?{radius_server}:{default_port}\s+{radius_key}\s+{default_timeout}\s+{radius_source}', pam_radius_auth_conf)
+ self.assertTrue(tmp)
# required, static options
self.assertIn('priv-lvl 15', pam_radius_auth_conf)
@@ -347,6 +387,27 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
self.assertTrue(tmp)
+ # Login with proper credentials
+ out, err = self.ssh_send_cmd(ssh_test_command, username, password)
+ # verify login
+ self.assertFalse(err)
+ self.assertEqual(out, self.ssh_test_command_result)
+
+ # Login with invalid credentials
+ with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
+ _, _ = self.ssh_send_cmd(ssh_test_command, username, f'{password}1')
+
+ # Remove RADIUS configuration
+ self.cli_delete(base_path + ['radius'])
+ # Remove RADIUS container
+ self.cli_delete(radius_container_path)
+ # Remove dummy interface
+ self.cli_delete(['interfaces', 'dummy', dummy_if])
+ self.cli_commit()
+
+ # Remove rendered tac_plus daemon configuration
+ shutil.rmtree(radius_config)
+
def test_system_login_max_login_session(self):
max_logins = '2'
timeout = '600'
@@ -390,12 +451,6 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
tmpl = jinja2.Template(TAC_PLUS_TMPL_SRC)
write_file(f'{tac_plus_config}/tac_plus.cfg', tmpl.render(tac_test_user))
- # Check if SSH service is running
- ssh_running = process_named_running(SSH_PROCESS_NAME)
- if not ssh_running:
- # Start SSH service
- self.cli_set(['service', 'ssh'])
-
# Start tac_plus container
self.cli_set(tac_container_path + ['allow-host-networks'])
self.cli_set(tac_container_path + ['image', tac_image])
@@ -450,15 +505,14 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'server={server}', nss_tacacs_conf)
# Login with proper credentials
- test_command = 'uname -a'
- out, err = self.ssh_send_cmd(test_command, username, password)
+ out, err = self.ssh_send_cmd(ssh_test_command, username, password)
# verify login
self.assertFalse(err)
- self.assertEqual(out, cmd(test_command))
+ self.assertEqual(out, self.ssh_test_command_result)
# Login with invalid credentials
with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
- _, _ = self.ssh_send_cmd(test_command, username, f'{password}1')
+ _, _ = self.ssh_send_cmd(ssh_test_command, username, f'{password}1')
# Remove TACACS configuration
self.cli_delete(base_path + ['tacacs'])
@@ -471,10 +525,6 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
# Remove rendered tac_plus daemon configuration
shutil.rmtree(tac_plus_config)
- # Stop SSH service if it was not running before
- if not ssh_running:
- self.cli_delete(['service', 'ssh'])
-
def test_delete_current_user(self):
current_user = get_current_user()
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index f2bea58d1..91a76e6f6 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-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
@@ -353,24 +353,40 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
def test_dmvpn(self):
- tunnel_if = 'tun100'
- nhrp_secret = 'secret'
ike_lifetime = '3600'
esp_lifetime = '1800'
+ tunnel_if = "tun100"
+ tunnel_ip = '172.16.253.134/32'
+ tunnel_source = "192.0.2.134"
+ tunnel_encapsulation = "gre"
+ esp_group = "ESP-HUB"
+ ike_group = "IKE-HUB"
+ nhrp_secret = "vyos123"
+ nhrp_holdtime = '300'
+ nhs_tunnelip = '172.16.253.1'
+ nhs_nbmaip = '192.0.2.1'
+ map_tunnelip = '172.16.253.135'
+ map_nbmaip = "192.0.2.135"
+ nhrp_networkid = '1'
+
# Tunnel
- self.cli_set(tunnel_path + [tunnel_if, 'address', '172.16.253.134/29'])
- self.cli_set(tunnel_path + [tunnel_if, 'encapsulation', 'gre'])
- self.cli_set(tunnel_path + [tunnel_if, 'source-address', '192.0.2.1'])
- self.cli_set(tunnel_path + [tunnel_if, 'enable-multicast'])
- self.cli_set(tunnel_path + [tunnel_if, 'parameters', 'ip', 'key', '1'])
+ self.cli_set(tunnel_path + [tunnel_if, "address", tunnel_ip])
+ self.cli_set(tunnel_path + [tunnel_if, "encapsulation", tunnel_encapsulation])
+ self.cli_set(tunnel_path + [tunnel_if, "source-address", tunnel_source])
+ self.cli_set(tunnel_path + [tunnel_if, "enable-multicast"])
+ self.cli_set(tunnel_path + [tunnel_if, "parameters", "ip", "key", "1"])
# NHRP
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'cisco-authentication', nhrp_secret])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'holding-time', '300'])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'multicast', 'dynamic'])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'redirect'])
- self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'shortcut'])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "authentication", nhrp_secret])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holdtime", nhrp_holdtime])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", nhs_tunnelip])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "redirect"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "shortcut"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "registration-no-unique"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "network-id", nhrp_networkid])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "nhs", "tunnel-ip", nhs_tunnelip, "nbma", nhs_nbmaip])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "map", "tunnel-ip", map_tunnelip, "nbma", map_nbmaip])
# IKE/ESP Groups
self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', esp_lifetime])
@@ -399,11 +415,11 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
swanctl_conf = read_file(swanctl_file)
swanctl_lines = [
- f'proposals = aes128-sha1-modp1024,aes256-sha1-prfsha1-modp1024',
+ f'proposals = aes256-sha1-prfsha1-modp1024',
f'version = 1',
f'rekey_time = {ike_lifetime}s',
f'rekey_time = {esp_lifetime}s',
- f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024',
+ f'esp_proposals = aes256-sha1-modp1024,3des-md5-modp1024',
f'local_ts = dynamic[gre]',
f'remote_ts = dynamic[gre]',
f'mode = transport',
diff --git a/src/conf_mode/interfaces_tunnel.py b/src/conf_mode/interfaces_tunnel.py
index 98ef98d12..ee1436e49 100755
--- a/src/conf_mode/interfaces_tunnel.py
+++ b/src/conf_mode/interfaces_tunnel.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 yOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -13,9 +13,8 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
from sys import exit
-
+import ipaddress
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
@@ -89,6 +88,13 @@ def verify(tunnel):
raise ConfigError('Tunnel used for NHRP, it can not be deleted!')
return None
+ if 'nhrp' in tunnel:
+ if 'address' in tunnel:
+ address_list = dict_search('address', tunnel)
+ for tunip in address_list:
+ if ipaddress.ip_network(tunip, strict=False).prefixlen != 32:
+ raise ConfigError(
+ 'Tunnel is used for NHRP, Netmask should be /32!')
verify_tunnel(tunnel)
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index 0bd68b7d8..ac92c9d99 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-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
@@ -14,95 +14,112 @@
# 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 sys import argv
+import ipaddress
from vyos.config import Config
-from vyos.configdict import node_changed
from vyos.template import render
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.utils.process import run
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos.utils.process import is_systemd_service_running
+
airbag.enable()
-opennhrp_conf = '/run/opennhrp/opennhrp.conf'
+nflog_redirect = 1
+nflog_multicast = 2
nhrp_nftables_conf = '/run/nftables_nhrp.conf'
+
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'nhrp']
-
- nhrp = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- nhrp['del_tunnels'] = node_changed(conf, base + ['tunnel'])
-
- if not conf.exists(base):
- return nhrp
- nhrp['if_tunnel'] = conf.get_config_dict(['interfaces', 'tunnel'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ return get_frrender_dict(conf, argv)
- nhrp['profile_map'] = {}
- profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- for name, profile_conf in profile.items():
- if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']:
- interfaces = profile_conf['bind']['tunnel']
- if isinstance(interfaces, str):
- interfaces = [interfaces]
- for interface in interfaces:
- nhrp['profile_map'][interface] = name
-
- return nhrp
-
-def verify(nhrp):
- if 'tunnel' in nhrp:
- for name, nhrp_conf in nhrp['tunnel'].items():
- if not nhrp['if_tunnel'] or name not in nhrp['if_tunnel']:
+def verify(config_dict):
+ if not config_dict or 'deleted' in config_dict:
+ return None
+ if 'tunnel' in config_dict:
+ for name, nhrp_conf in config_dict['tunnel'].items():
+ if not config_dict['if_tunnel'] or name not in config_dict['if_tunnel']:
raise ConfigError(f'Tunnel interface "{name}" does not exist')
- tunnel_conf = nhrp['if_tunnel'][name]
+ tunnel_conf = config_dict['if_tunnel'][name]
+ if 'address' in tunnel_conf:
+ address_list = dict_search('address', tunnel_conf)
+ for tunip in address_list:
+ if ipaddress.ip_network(tunip,
+ strict=False).prefixlen != 32:
+ raise ConfigError(
+ f'Tunnel {name} is used for NHRP, Netmask should be /32!')
if 'encapsulation' not in tunnel_conf or tunnel_conf['encapsulation'] != 'gre':
raise ConfigError(f'Tunnel "{name}" is not an mGRE tunnel')
+ if 'network_id' not in nhrp_conf:
+ raise ConfigError(f'network-id is not specified in tunnel "{name}"')
+
if 'remote' in tunnel_conf:
raise ConfigError(f'Tunnel "{name}" cannot have a remote address defined')
- if 'map' in nhrp_conf:
- for map_name, map_conf in nhrp_conf['map'].items():
- if 'nbma_address' not in map_conf:
+ map_tunnelip = dict_search('map.tunnel_ip', nhrp_conf)
+ if map_tunnelip:
+ for map_name, map_conf in map_tunnelip.items():
+ if 'nbma' not in map_conf:
raise ConfigError(f'nbma-address missing on map {map_name} on tunnel {name}')
- if 'dynamic_map' in nhrp_conf:
- for map_name, map_conf in nhrp_conf['dynamic_map'].items():
- if 'nbma_domain_name' not in map_conf:
- raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}')
+ nhs_tunnelip = dict_search('nhs.tunnel_ip', nhrp_conf)
+ nbma_list = []
+ if nhs_tunnelip:
+ for nhs_name, nhs_conf in nhs_tunnelip.items():
+ if 'nbma' not in nhs_conf:
+ raise ConfigError(f'nbma-address missing on map nhs {nhs_name} on tunnel {name}')
+ if nhs_name != 'dynamic':
+ if len(list(dict_search('nbma', nhs_conf))) > 1:
+ raise ConfigError(
+ f'Static nhs tunnel-ip {nhs_name} cannot contain multiple nbma-addresses')
+ for nbma_ip in dict_search('nbma', nhs_conf):
+ if nbma_ip not in nbma_list:
+ nbma_list.append(nbma_ip)
+ else:
+ raise ConfigError(
+ f'Nbma address {nbma_ip} cannot be maped to several tunnel-ip')
return None
-def generate(nhrp):
- if not os.path.exists(nhrp_nftables_conf):
- nhrp['first_install'] = True
- render(opennhrp_conf, 'nhrp/opennhrp.conf.j2', nhrp)
- render(nhrp_nftables_conf, 'nhrp/nftables.conf.j2', nhrp)
+def generate(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'nhrp'):
+ return None
+
+ if 'deleted' in config_dict['nhrp']:
+ return None
+ render(nhrp_nftables_conf, 'frr/nhrpd_nftables.conf.j2', config_dict['nhrp'])
+
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(nhrp):
+
+def apply(config_dict):
+
nft_rc = run(f'nft --file {nhrp_nftables_conf}')
if nft_rc != 0:
raise ConfigError('Failed to apply NHRP tunnel firewall rules')
- action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
- service_rc = run(f'systemctl {action} opennhrp.service')
- if service_rc != 0:
- raise ConfigError(f'Failed to {action} the NHRP service')
-
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
+
if __name__ == '__main__':
try:
c = get_config()
@@ -112,3 +129,4 @@ if __name__ == '__main__':
except ConfigError as e:
print(e)
exit(1)
+
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 9c59aa63d..5a729af74 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -38,6 +38,7 @@ from vyos.utils.network import is_subnet_connected
from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
+
airbag.enable()
ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
@@ -45,13 +46,13 @@ ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
config_file = '/run/kea/kea-dhcp4.conf'
lease_file = '/config/dhcp/dhcp4-leases.csv'
lease_file_glob = '/config/dhcp/dhcp4-leases*'
-systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf'
user_group = '_kea'
ca_cert_file = '/run/kea/kea-failover-ca.pem'
cert_file = '/run/kea/kea-failover.pem'
cert_key_file = '/run/kea/kea-failover-key.pem'
+
def dhcp_slice_range(exclude_list, range_dict):
"""
This function is intended to slice a DHCP range. What does it mean?
@@ -74,19 +75,17 @@ def dhcp_slice_range(exclude_list, range_dict):
range_last_exclude = ''
for e in exclude_list:
- if (ip_address(e) >= ip_address(range_start)) and \
- (ip_address(e) <= ip_address(range_stop)):
+ if (ip_address(e) >= ip_address(range_start)) and (
+ ip_address(e) <= ip_address(range_stop)
+ ):
range_last_exclude = e
for e in exclude_list:
- if (ip_address(e) >= ip_address(range_start)) and \
- (ip_address(e) <= ip_address(range_stop)):
-
+ if (ip_address(e) >= ip_address(range_start)) and (
+ ip_address(e) <= ip_address(range_stop)
+ ):
# Build new address range ending one address before exclude address
- r = {
- 'start' : range_start,
- 'stop' : str(ip_address(e) -1)
- }
+ r = {'start': range_start, 'stop': str(ip_address(e) - 1)}
if 'option' in range_dict:
r['option'] = range_dict['option']
@@ -104,10 +103,7 @@ def dhcp_slice_range(exclude_list, range_dict):
# Take care of last IP address range spanning from the last exclude
# address (+1) to the end of the initial configured range
if ip_address(e) == ip_address(range_last_exclude):
- r = {
- 'start': str(ip_address(e) + 1),
- 'stop': str(range_stop)
- }
+ r = {'start': str(ip_address(e) + 1), 'stop': str(range_stop)}
if 'option' in range_dict:
r['option'] = range_dict['option']
@@ -115,14 +111,15 @@ def dhcp_slice_range(exclude_list, range_dict):
if not (ip_address(r['start']) > ip_address(r['stop'])):
output.append(r)
else:
- # if the excluded address was not part of the range, we simply return
- # the entire ranga again
- if not range_last_exclude:
- if range_dict not in output:
- output.append(range_dict)
+ # if the excluded address was not part of the range, we simply return
+ # the entire ranga again
+ if not range_last_exclude:
+ if range_dict not in output:
+ output.append(range_dict)
return output
+
def get_config(config=None):
if config:
conf = config
@@ -132,10 +129,13 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcp = conf.get_config_dict(base, key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True,
- with_recursive_defaults=True)
+ dhcp = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True,
+ )
if 'shared_network_name' in dhcp:
for network, network_config in dhcp['shared_network_name'].items():
@@ -147,22 +147,31 @@ def get_config(config=None):
new_range_id = 0
new_range_dict = {}
for r, r_config in subnet_config['range'].items():
- for slice in dhcp_slice_range(subnet_config['exclude'], r_config):
- new_range_dict.update({new_range_id : slice})
- new_range_id +=1
+ for slice in dhcp_slice_range(
+ subnet_config['exclude'], r_config
+ ):
+ new_range_dict.update({new_range_id: slice})
+ new_range_id += 1
dhcp['shared_network_name'][network]['subnet'][subnet].update(
- {'range' : new_range_dict})
+ {'range': new_range_dict}
+ )
if len(dhcp['high_availability']) == 1:
## only default value for mode is set, need to remove ha node
del dhcp['high_availability']
else:
if dict_search('high_availability.certificate', dhcp):
- dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
+ dhcp['pki'] = conf.get_config_dict(
+ ['pki'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
return dhcp
+
def verify(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
@@ -170,13 +179,15 @@ def verify(dhcp):
# If DHCP is enabled we need one share-network
if 'shared_network_name' not in dhcp:
- raise ConfigError('No DHCP shared networks configured.\n' \
- 'At least one DHCP shared network must be configured.')
+ raise ConfigError(
+ 'No DHCP shared networks configured.\n'
+ 'At least one DHCP shared network must be configured.'
+ )
# Inspect shared-network/subnet
listen_ok = False
subnets = []
- shared_networks = len(dhcp['shared_network_name'])
+ shared_networks = len(dhcp['shared_network_name'])
disabled_shared_networks = 0
subnet_ids = []
@@ -187,12 +198,16 @@ def verify(dhcp):
disabled_shared_networks += 1
if 'subnet' not in network_config:
- raise ConfigError(f'No subnets defined for {network}. At least one\n' \
- 'lease subnet must be configured.')
+ raise ConfigError(
+ f'No subnets defined for {network}. At least one\n'
+ 'lease subnet must be configured.'
+ )
for subnet, subnet_config in network_config['subnet'].items():
if 'subnet_id' not in subnet_config:
- raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"')
+ raise ConfigError(
+ f'Unique subnet ID not specified for subnet "{subnet}"'
+ )
if subnet_config['subnet_id'] in subnet_ids:
raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique')
@@ -203,32 +218,46 @@ def verify(dhcp):
if 'static_route' in subnet_config:
for route, route_option in subnet_config['static_route'].items():
if 'next_hop' not in route_option:
- raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
+ raise ConfigError(
+ f'DHCP static-route "{route}" requires router to be defined!'
+ )
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
networks = []
for range, range_config in subnet_config['range'].items():
if not {'start', 'stop'} <= set(range_config):
- raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!')
+ raise ConfigError(
+ f'DHCP range "{range}" start and stop address must be defined!'
+ )
# Start/Stop address must be inside network
for key in ['start', 'stop']:
if ip_address(range_config[key]) not in ip_network(subnet):
- raise ConfigError(f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!'
+ )
# Stop address must be greater or equal to start address
- if ip_address(range_config['stop']) < ip_address(range_config['start']):
- raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \
- 'to the ranges start address!')
+ if ip_address(range_config['stop']) < ip_address(
+ range_config['start']
+ ):
+ raise ConfigError(
+ f'DHCP range "{range}" stop address must be greater or equal\n'
+ 'to the ranges start address!'
+ )
for network in networks:
start = range_config['start']
stop = range_config['stop']
if start in network:
- raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!')
+ raise ConfigError(
+ f'Range "{range}" start address "{start}" already part of another range!'
+ )
if stop in network:
- raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!')
+ raise ConfigError(
+ f'Range "{range}" stop address "{stop}" already part of another range!'
+ )
tmp = IPRange(range_config['start'], range_config['stop'])
networks.append(tmp)
@@ -237,12 +266,16 @@ def verify(dhcp):
if 'exclude' in subnet_config:
for exclude in subnet_config['exclude']:
if ip_address(exclude) not in ip_network(subnet):
- raise ConfigError(f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!'
+ )
# At least one DHCP address range or static-mapping required
if 'range' not in subnet_config and 'static_mapping' not in subnet_config:
- raise ConfigError(f'No DHCP address range or active static-mapping configured\n' \
- f'within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'No DHCP address range or active static-mapping configured\n'
+ f'within shared-network "{network}, {subnet}"!'
+ )
if 'static_mapping' in subnet_config:
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
@@ -251,29 +284,42 @@ def verify(dhcp):
used_duid = []
for mapping, mapping_config in subnet_config['static_mapping'].items():
if 'ip_address' in mapping_config:
- if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
- raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \
- f'not within shared-network "{network}, {subnet}"!')
-
- if ('mac' not in mapping_config and 'duid' not in mapping_config) or \
- ('mac' in mapping_config and 'duid' in mapping_config):
- raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
- f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+ if ip_address(mapping_config['ip_address']) not in ip_network(
+ subnet
+ ):
+ raise ConfigError(
+ f'Configured static lease address for mapping "{mapping}" is\n'
+ f'not within shared-network "{network}, {subnet}"!'
+ )
+
+ if (
+ 'mac' not in mapping_config and 'duid' not in mapping_config
+ ) or ('mac' in mapping_config and 'duid' in mapping_config):
+ raise ConfigError(
+ f'Either MAC address or Client identifier (DUID) is required for '
+ f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!'
+ )
if 'disable' not in mapping_config:
if mapping_config['ip_address'] in used_ips:
- raise ConfigError(f'Configured IP address for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured IP address for static mapping "{mapping}" already exists on another static mapping'
+ )
used_ips.append(mapping_config['ip_address'])
if 'disable' not in mapping_config:
if 'mac' in mapping_config:
if mapping_config['mac'] in used_mac:
- raise ConfigError(f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping'
+ )
used_mac.append(mapping_config['mac'])
if 'duid' in mapping_config:
if mapping_config['duid'] in used_duid:
- raise ConfigError(f'Configured DUID for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured DUID for static mapping "{mapping}" already exists on another static mapping'
+ )
used_duid.append(mapping_config['duid'])
# There must be one subnet connected to a listen interface.
@@ -284,73 +330,102 @@ def verify(dhcp):
# Subnets must be non overlapping
if subnet in subnets:
- raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n'
- 'defined multiple times!')
+ raise ConfigError(
+ f'Configured subnets must be unique! Subnet "{subnet}"\n'
+ 'defined multiple times!'
+ )
subnets.append(subnet)
# Check for overlapping subnets
net = ip_network(subnet)
for n in subnets:
net2 = ip_network(n)
- if (net != net2):
+ if net != net2:
if net.overlaps(net2):
- raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ raise ConfigError(
+ f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!'
+ )
# Prevent 'disable' for shared-network if only one network is configured
if (shared_networks - disabled_shared_networks) < 1:
- raise ConfigError(f'At least one shared network must be active!')
+ raise ConfigError('At least one shared network must be active!')
if 'high_availability' in dhcp:
for key in ['name', 'remote', 'source_address', 'status']:
if key not in dhcp['high_availability']:
tmp = key.replace('_', '-')
- raise ConfigError(f'DHCP high-availability requires "{tmp}" to be specified!')
+ raise ConfigError(
+ f'DHCP high-availability requires "{tmp}" to be specified!'
+ )
if len({'certificate', 'ca_certificate'} & set(dhcp['high_availability'])) == 1:
- raise ConfigError(f'DHCP secured high-availability requires both certificate and CA certificate')
+ raise ConfigError(
+ 'DHCP secured high-availability requires both certificate and CA certificate'
+ )
if 'certificate' in dhcp['high_availability']:
cert_name = dhcp['high_availability']['certificate']
if cert_name not in dhcp['pki']['certificate']:
- raise ConfigError(f'Invalid certificate specified for DHCP high-availability')
-
- if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'):
- raise ConfigError(f'Invalid certificate specified for DHCP high-availability')
-
- if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'):
- raise ConfigError(f'Missing private key on certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid certificate specified for DHCP high-availability'
+ )
+
+ if not dict_search_args(
+ dhcp['pki']['certificate'], cert_name, 'certificate'
+ ):
+ raise ConfigError(
+ 'Invalid certificate specified for DHCP high-availability'
+ )
+
+ if not dict_search_args(
+ dhcp['pki']['certificate'], cert_name, 'private', 'key'
+ ):
+ raise ConfigError(
+ 'Missing private key on certificate specified for DHCP high-availability'
+ )
if 'ca_certificate' in dhcp['high_availability']:
ca_cert_name = dhcp['high_availability']['ca_certificate']
if ca_cert_name not in dhcp['pki']['ca']:
- raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid CA certificate specified for DHCP high-availability'
+ )
if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'):
- raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid CA certificate specified for DHCP high-availability'
+ )
- for address in (dict_search('listen_address', dhcp) or []):
+ for address in dict_search('listen_address', dhcp) or []:
if is_addr_assigned(address, include_vrf=True):
listen_ok = True
# no need to probe further networks, we have one that is valid
continue
else:
- raise ConfigError(f'listen-address "{address}" not configured on any interface')
+ raise ConfigError(
+ f'listen-address "{address}" not configured on any interface'
+ )
if not listen_ok:
- raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
- 'broadcast interface configured, nor was there an explicit listen-address\n'
- 'configured for serving DHCP relay packets!')
+ raise ConfigError(
+ 'None of the configured subnets have an appropriate primary IP address on any\n'
+ 'broadcast interface configured, nor was there an explicit listen-address\n'
+ 'configured for serving DHCP relay packets!'
+ )
if 'listen_address' in dhcp and 'listen_interface' in dhcp:
- raise ConfigError(f'Cannot define listen-address and listen-interface at the same time')
+ raise ConfigError(
+ 'Cannot define listen-address and listen-interface at the same time'
+ )
- for interface in (dict_search('listen_interface', dhcp) or []):
+ for interface in dict_search('listen_interface', dhcp) or []:
if not interface_exists(interface):
raise ConfigError(f'listen-interface "{interface}" does not exist')
return None
+
def generate(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
@@ -382,8 +457,12 @@ def generate(dhcp):
cert_name = dhcp['high_availability']['certificate']
cert_data = dhcp['pki']['certificate'][cert_name]['certificate']
key_data = dhcp['pki']['certificate'][cert_name]['private']['key']
- write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600)
- write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600)
+ write_file(
+ cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600
+ )
+ write_file(
+ cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600
+ )
dhcp['high_availability']['cert_file'] = cert_file
dhcp['high_availability']['cert_key_file'] = cert_key_file
@@ -391,17 +470,33 @@ def generate(dhcp):
if 'ca_certificate' in dhcp['high_availability']:
ca_cert_name = dhcp['high_availability']['ca_certificate']
ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate']
- write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600)
+ write_file(
+ ca_cert_file,
+ wrap_certificate(ca_cert_data),
+ user=user_group,
+ mode=0o600,
+ )
dhcp['high_availability']['ca_cert_file'] = ca_cert_file
- render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
-
- 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)
+ 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,
+ )
return None
+
def apply(dhcp):
services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
@@ -427,6 +522,7 @@ def apply(dhcp):
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index e22b7550c..25604d2a2 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-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
@@ -86,8 +86,6 @@ def get_config(config=None):
conf = Config()
base = ['vpn', 'ipsec']
l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
- if not conf.exists(base):
- return None
# retrieve common dictionary keys
ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
@@ -95,6 +93,14 @@ def get_config(config=None):
get_first_key=True,
with_pki=True)
+ ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
+ if ipsec['nhrp_exists']:
+ set_dependents('nhrp', conf)
+
+ if not conf.exists(base):
+ ipsec.update({'deleted' : ''})
+ return ipsec
+
# We have to cleanup the default dict, as default values could
# enable features which are not explicitly enabled on the
# CLI. E.g. dead-peer-detection defaults should not be injected
@@ -115,7 +121,6 @@ def get_config(config=None):
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
- ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
if ipsec['nhrp_exists']:
set_dependents('nhrp', conf)
@@ -196,8 +201,8 @@ def verify_pki_rsa(pki, rsa_conf):
return True
def verify(ipsec):
- if not ipsec:
- return None
+ if not ipsec or 'deleted' in ipsec:
+ return
if 'authentication' in ipsec:
if 'psk' in ipsec['authentication']:
@@ -624,7 +629,7 @@ def generate_pki_files_rsa(pki, rsa_conf):
def generate(ipsec):
cleanup_pki_files()
- if not ipsec:
+ if not ipsec or 'deleted' in ipsec:
for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
@@ -721,15 +726,12 @@ def generate(ipsec):
def apply(ipsec):
systemd_service = 'strongswan.service'
- if not ipsec:
+ if not ipsec or 'deleted' in ipsec:
call(f'systemctl stop {systemd_service}')
-
if vti_updown_db_exists():
remove_vti_updown_db()
-
else:
call(f'systemctl reload-or-restart {systemd_service}')
-
if ipsec['enabled_vti_interfaces']:
with open_vti_updown_db_for_create_or_update() as db:
db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces'])
@@ -737,7 +739,7 @@ def apply(ipsec):
db.commit(lambda interface: ipsec['vti_interface_dicts'][interface])
elif vti_updown_db_exists():
remove_vti_updown_db()
-
+ if ipsec:
if ipsec.get('nhrp_exists', False):
try:
call_dependents()
@@ -746,7 +748,6 @@ def apply(ipsec):
# ConfigError("ConfigError('Interface ethN requires an IP address!')")
pass
-
if __name__ == '__main__':
try:
ipsec = get_config()
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
index 0f5bf801e..c74fafb42 100644
--- a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf
+++ b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf
@@ -1,6 +1,7 @@
[Unit]
After=
After=vyos-router.service
+ConditionFileNotEmpty=
[Service]
ExecStart=
diff --git a/src/migration-scripts/nhrp/0-to-1 b/src/migration-scripts/nhrp/0-to-1
new file mode 100644
index 000000000..badd88e04
--- /dev/null
+++ b/src/migration-scripts/nhrp/0-to-1
@@ -0,0 +1,129 @@
+# 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/>.
+
+# Migration from Opennhrp to FRR NHRP
+import ipaddress
+
+from vyos.configtree import ConfigTree
+
+base = ['protocols', 'nhrp', 'tunnel']
+interface_base = ['interfaces', 'tunnel']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base):
+ return
+ networkid = 1
+ for tunnel_name in config.list_nodes(base):
+ ## Cisco Authentication migration
+ if config.exists(base + [tunnel_name,'cisco-authentication']):
+ auth = config.return_value(base + [tunnel_name,'cisco-authentication'])
+ config.delete(base + [tunnel_name,'cisco-authentication'])
+ config.set(base + [tunnel_name,'authentication'], value=auth)
+ ## Delete Dynamic-map to fqdn
+ if config.exists(base + [tunnel_name,'dynamic-map']):
+ config.delete(base + [tunnel_name,'dynamic-map'])
+ ## Holdtime migration
+ if config.exists(base + [tunnel_name,'holding-time']):
+ holdtime = config.return_value(base + [tunnel_name,'holding-time'])
+ config.delete(base + [tunnel_name,'holding-time'])
+ config.set(base + [tunnel_name,'holdtime'], value=holdtime)
+ ## Add network-id
+ config.set(base + [tunnel_name, 'network-id'], value=networkid)
+ networkid+=1
+ ## Map and nhs migration
+ nhs_tunnelip_list = []
+ nhs_nbmaip_list = []
+ is_nhs = False
+ if config.exists(base + [tunnel_name,'map']):
+ is_map = False
+ for tunnel_ip in config.list_nodes(base + [tunnel_name, 'map']):
+ tunnel_ip_path = base + [tunnel_name, 'map', tunnel_ip]
+ tunnel_ip = tunnel_ip.split('/')[0]
+ if config.exists(tunnel_ip_path + ['cisco']):
+ config.delete(tunnel_ip_path + ['cisco'])
+ if config.exists(tunnel_ip_path + ['nbma-address']):
+ nbma = config.return_value(tunnel_ip_path + ['nbma-address'])
+ if config.exists (tunnel_ip_path + ['register']):
+ config.delete(tunnel_ip_path + ['register'])
+ config.delete(tunnel_ip_path + ['nbma-address'])
+ config.set(base + [tunnel_name, 'nhs', 'tunnel-ip', tunnel_ip, 'nbma'], value=nbma)
+ is_nhs = True
+ if tunnel_ip not in nhs_tunnelip_list:
+ nhs_tunnelip_list.append(tunnel_ip)
+ if nbma not in nhs_nbmaip_list:
+ nhs_nbmaip_list.append(nbma)
+ else:
+ config.delete(tunnel_ip_path + ['nbma-address'])
+ config.set(base + [tunnel_name, 'map_test', 'tunnel-ip', tunnel_ip, 'nbma'], value=nbma)
+ is_map = True
+ config.delete(base + [tunnel_name,'map'])
+
+ if is_nhs:
+ config.set_tag(base + [tunnel_name, 'nhs', 'tunnel-ip'])
+
+ if is_map:
+ config.copy(base + [tunnel_name, 'map_test'], base + [tunnel_name, 'map'])
+ config.delete(base + [tunnel_name, 'map_test'])
+ config.set_tag(base + [tunnel_name, 'map', 'tunnel-ip'])
+
+ #
+ # Change netmask to /32 on tunnel interface
+ # If nhs is alone, add static route tunnel network to nhs
+ #
+ if config.exists(interface_base + [tunnel_name, 'address']):
+ tunnel_ip_list = []
+ for tunnel_ip in config.return_values(
+ interface_base + [tunnel_name, 'address']):
+ tunnel_ip_ch = tunnel_ip.split('/')[0]+'/32'
+ if tunnel_ip_ch not in tunnel_ip_list:
+ tunnel_ip_list.append(tunnel_ip_ch)
+ for nhs in nhs_tunnelip_list:
+ config.set(['protocols', 'static', 'route', str(ipaddress.ip_network(tunnel_ip, strict=False)), 'next-hop', nhs, 'distance'], value='250')
+ if nhs_tunnelip_list:
+ if not config.is_tag(['protocols', 'static', 'route']):
+ config.set_tag(['protocols', 'static', 'route'])
+ if not config.is_tag(['protocols', 'static', 'route', str(ipaddress.ip_network(tunnel_ip, strict=False)), 'next-hop']):
+ config.set_tag(['protocols', 'static', 'route', str(ipaddress.ip_network(tunnel_ip, strict=False)), 'next-hop'])
+
+ config.delete(interface_base + [tunnel_name, 'address'])
+ for tunnel_ip in tunnel_ip_list:
+ config.set(
+ interface_base + [tunnel_name, 'address'], value=tunnel_ip, replace=False)
+
+ ## Map multicast migration
+ if config.exists(base + [tunnel_name, 'multicast']):
+ multicast_map = config.return_value(
+ base + [tunnel_name, 'multicast'])
+ if multicast_map == 'nhs':
+ config.delete(base + [tunnel_name, 'multicast'])
+ for nbma in nhs_nbmaip_list:
+ config.set(base + [tunnel_name, 'multicast'], value=nbma,
+ replace=False)
+
+ ## Delete non-cahching
+ if config.exists(base + [tunnel_name, 'non-caching']):
+ config.delete(base + [tunnel_name, 'non-caching'])
+ ## Delete shortcut-destination
+ if config.exists(base + [tunnel_name, 'shortcut-destination']):
+ if not config.exists(base + [tunnel_name, 'shortcut']):
+ config.set(base + [tunnel_name, 'shortcut'])
+ config.delete(base + [tunnel_name, 'shortcut-destination'])
+ ## Delete shortcut-target
+ if config.exists(base + [tunnel_name, 'shortcut-target']):
+ if not config.exists(base + [tunnel_name, 'shortcut']):
+ config.set(base + [tunnel_name, 'shortcut'])
+ config.delete(base + [tunnel_name, 'shortcut-target'])
+ ## Set registration-no-unique
+ config.set(base + [tunnel_name, 'registration-no-unique']) \ No newline at end of file
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 20f54df25..45de86cab 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -113,7 +113,7 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig
data_lease['origin'] = 'local' # TODO: Determine remote in HA
data_lease['hostname'] = lease.get('hostname', '-')
# remove trailing dot to ensure consistency for `vyos-hostsd-client`
- if data_lease['hostname'][-1] == '.':
+ if data_lease['hostname'] and data_lease['hostname'][-1] == '.':
data_lease['hostname'] = data_lease['hostname'][:-1]
if family == 'inet':
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index 02ba126b4..1ab50b105 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright (C) 2022-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
@@ -700,15 +700,6 @@ def reset_profile_dst(profile: str, tunnel: str, nbma_dst: str):
]
)
)
- # initiate IKE SAs
- for ike in sa_nbma_list:
- if ike_sa_name in ike:
- vyos.ipsec.vici_initiate(
- ike_sa_name,
- 'dmvpn',
- ike[ike_sa_name]['local-host'],
- ike[ike_sa_name]['remote-host'],
- )
print(
f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success'
)
@@ -732,18 +723,6 @@ def reset_profile_all(profile: str, tunnel: str):
)
# terminate IKE SAs
vyos.ipsec.terminate_vici_by_name(ike_sa_name, None)
- # initiate IKE SAs
- for ike in sa_list:
- if ike_sa_name in ike:
- vyos.ipsec.vici_initiate(
- ike_sa_name,
- 'dmvpn',
- ike[ike_sa_name]['local-host'],
- ike[ike_sa_name]['remote-host'],
- )
- print(
- f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success'
- )
print(f'Profile {profile} tunnel {tunnel} reset result: success')
except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
diff --git a/src/op_mode/nhrp.py b/src/op_mode/nhrp.py
deleted file mode 100755
index e66f33079..000000000
--- a/src/op_mode/nhrp.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 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 sys
-import tabulate
-import vyos.opmode
-
-from vyos.utils.process import cmd
-from vyos.utils.process import process_named_running
-from vyos.utils.dict import colon_separated_to_dict
-
-
-def _get_formatted_output(output_dict: dict) -> str:
- """
- Create formatted table for CLI output
- :param output_dict: dictionary for API
- :type output_dict: dict
- :return: tabulate string
- :rtype: str
- """
- print(f"Status: {output_dict['Status']}")
- output: str = tabulate.tabulate(output_dict['routes'], headers='keys',
- numalign="left")
- return output
-
-
-def _get_formatted_dict(output_string: str) -> dict:
- """
- Format string returned from CMD to API list
- :param output_string: String received by CMD
- :type output_string: str
- :return: dictionary for API
- :rtype: dict
- """
- formatted_dict: dict = {
- 'Status': '',
- 'routes': []
- }
- output_list: list = output_string.split('\n\n')
- for list_a in output_list:
- output_dict = colon_separated_to_dict(list_a, True)
- if 'Status' in output_dict:
- formatted_dict['Status'] = output_dict['Status']
- else:
- formatted_dict['routes'].append(output_dict)
- return formatted_dict
-
-
-def show_interface(raw: bool):
- """
- Command 'show nhrp interface'
- :param raw: if API
- :type raw: bool
- """
- if not process_named_running('opennhrp'):
- raise vyos.opmode.UnconfiguredSubsystem('OpenNHRP is not running.')
- interface_string: str = cmd('sudo opennhrpctl interface show')
- interface_dict: dict = _get_formatted_dict(interface_string)
- if raw:
- return interface_dict
- else:
- return _get_formatted_output(interface_dict)
-
-
-def show_tunnel(raw: bool):
- """
- Command 'show nhrp tunnel'
- :param raw: if API
- :type raw: bool
- """
- if not process_named_running('opennhrp'):
- raise vyos.opmode.UnconfiguredSubsystem('OpenNHRP is not running.')
- tunnel_string: str = cmd('sudo opennhrpctl show')
- tunnel_dict: list = _get_formatted_dict(tunnel_string)
- if raw:
- return tunnel_dict
- else:
- return _get_formatted_output(tunnel_dict)
-
-
-if __name__ == '__main__':
- try:
- res = vyos.opmode.run(sys.modules[__name__])
- if res:
- print(res)
- except (ValueError, vyos.opmode.Error) as e:
- print(e)
- sys.exit(1)
diff --git a/src/op_mode/vtysh_wrapper.sh b/src/op_mode/vtysh_wrapper.sh
index 25d09ce77..bc472f7bb 100755
--- a/src/op_mode/vtysh_wrapper.sh
+++ b/src/op_mode/vtysh_wrapper.sh
@@ -2,5 +2,5 @@
declare -a tmp
# FRR uses ospf6 where we use ospfv3, and we use reset over clear for BGP,
# thus alter the commands
-tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/" | sed -e "s/^reset bgp/clear bgp/" | sed -e "s/^reset ip bgp/clear ip bgp/")
+tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/" | sed -e "s/^reset bgp/clear bgp/" | sed -e "s/^reset ip bgp/clear ip bgp/"| sed -e "s/^reset ip nhrp/clear ip nhrp/")
vtysh -c "$tmp"