summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/cleanup-mirror-pr-branch.yml1
-rw-r--r--.github/workflows/mirror-pr-and-sync.yml26
-rw-r--r--.github/workflows/trigger-pr-mirror-repo-sync.yml1
-rw-r--r--data/op-mode-standardized.json1
-rw-r--r--data/templates/dhcp-server/10-override.conf.j22
-rw-r--r--data/templates/dns-dynamic/ddclient.conf.j23
-rw-r--r--data/templates/dns-dynamic/override.conf.j23
-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--data/templates/pmacct/uacctd.conf.j223
-rw-r--r--data/templates/sflow/hsflowd.conf.j23
-rw-r--r--data/templates/telegraf/syslog_telegraf.j24
-rw-r--r--debian/control3
-rwxr-xr-xdebian/rules6
-rwxr-xr-xdebian/vyos-1x-smoketest.postinst16
-rw-r--r--debian/vyos-1x.postinst4
-rw-r--r--interface-definitions/container.xml.in2
-rw-r--r--interface-definitions/include/haproxy/rule-backend.xml.i2
-rw-r--r--interface-definitions/include/haproxy/rule-frontend.xml.i5
-rw-r--r--interface-definitions/include/netlink/log-level.xml.i21
-rw-r--r--interface-definitions/include/netlink/queue-size.xml.i15
-rw-r--r--interface-definitions/include/version/flow-accounting-version.xml.i2
-rw-r--r--interface-definitions/include/version/nhrp-version.xml.i3
-rw-r--r--interface-definitions/interfaces_wireguard.xml.in25
-rw-r--r--interface-definitions/protocols_nhrp.xml.in186
-rw-r--r--interface-definitions/service_monitoring_network_event.xml.in61
-rw-r--r--interface-definitions/system_conntrack.xml.in34
-rw-r--r--interface-definitions/system_flow-accounting.xml.in67
-rw-r--r--interface-definitions/system_sflow.xml.in6
-rw-r--r--interface-definitions/xml-component-version.xml.in50
-rw-r--r--op-mode-definitions/dhcp.xml.in8
-rw-r--r--op-mode-definitions/include/log/network-event-type-interface.xml.i11
-rw-r--r--op-mode-definitions/include/show-interface-type-event-log.xml.i40
-rw-r--r--op-mode-definitions/nhrp.xml.in73
-rw-r--r--op-mode-definitions/reset-wireguard.xml.in34
-rw-r--r--op-mode-definitions/show-interfaces-bonding.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-bridge.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-dummy.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-ethernet.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-geneve.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-input.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-l2tpv3.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-loopback.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-macsec.xml.in3
-rw-r--r--op-mode-definitions/show-interfaces-pppoe.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-sstpc.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-tunnel.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-virtual-ethernet.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-vti.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-vxlan.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-wireguard.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-wireless.xml.in1
-rw-r--r--op-mode-definitions/show-interfaces-wwan.xml.in1
-rwxr-xr-xop-mode-definitions/show-log.xml.in62
-rw-r--r--python/vyos/configquery.py9
-rw-r--r--python/vyos/configtree.py172
-rw-r--r--python/vyos/frrender.py62
-rw-r--r--python/vyos/ifconfig/control.py4
-rw-r--r--python/vyos/ifconfig/wireguard.py231
-rw-r--r--python/vyos/include/__init__.py15
-rw-r--r--python/vyos/include/uapi/__init__.py15
-rw-r--r--python/vyos/include/uapi/linux/__init__.py15
-rw-r--r--python/vyos/include/uapi/linux/fib_rules.py20
-rw-r--r--python/vyos/include/uapi/linux/icmpv6.py18
-rw-r--r--python/vyos/include/uapi/linux/if_arp.py176
-rw-r--r--python/vyos/include/uapi/linux/lwtunnel.py38
-rw-r--r--python/vyos/include/uapi/linux/neighbour.py34
-rw-r--r--python/vyos/include/uapi/linux/rtnetlink.py63
-rw-r--r--python/vyos/kea.py255
-rw-r--r--python/vyos/qos/base.py3
-rw-r--r--python/vyos/remote.py1
-rw-r--r--python/vyos/utils/kernel.py4
-rwxr-xr-xscripts/transclude-template5
-rw-r--r--smoketest/config-tests/basic-vyos-no-ntp53
-rw-r--r--smoketest/config-tests/bgp-big-as-cloud4
-rw-r--r--smoketest/config-tests/bgp-dmvpn-hub8
-rw-r--r--smoketest/config-tests/bgp-dmvpn-spoke14
-rw-r--r--smoketest/configs/basic-vyos-no-ntp132
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireguard.py108
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py2
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_nhrp.py78
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_dynamic.py19
-rw-r--r--smoketest/scripts/cli/test_service_monitoring_network_event.py65
-rwxr-xr-xsmoketest/scripts/cli/test_system_flow-accounting.py107
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py210
-rwxr-xr-xsmoketest/scripts/cli/test_system_sflow.py33
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py46
-rwxr-xr-xsmoketest/scripts/system/test_kernel_options.py6
-rwxr-xr-xsrc/conf_mode/interfaces_tunnel.py12
-rwxr-xr-xsrc/conf_mode/interfaces_wireguard.py53
-rwxr-xr-xsrc/conf_mode/nat.py8
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py118
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py270
-rw-r--r--src/conf_mode/service_monitoring_network_event.py93
-rwxr-xr-xsrc/conf_mode/system_flow-accounting.py53
-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/flow-accounting/1-to-263
-rw-r--r--src/migration-scripts/nhrp/0-to-1129
-rw-r--r--src/migration-scripts/ntp/1-to-27
-rwxr-xr-xsrc/op_mode/dhcp.py482
-rwxr-xr-xsrc/op_mode/ipsec.py23
-rwxr-xr-xsrc/op_mode/nhrp.py101
-rwxr-xr-xsrc/op_mode/reset_wireguard.py55
-rwxr-xr-xsrc/op_mode/vtysh_wrapper.sh2
-rwxr-xr-xsrc/services/vyos-domain-resolver44
-rw-r--r--src/services/vyos-network-event-logger1218
-rw-r--r--src/systemd/vyos-network-event-logger.service21
-rw-r--r--src/tests/test_configd_inspect.py210
115 files changed, 4524 insertions, 1496 deletions
diff --git a/.github/workflows/cleanup-mirror-pr-branch.yml b/.github/workflows/cleanup-mirror-pr-branch.yml
index bbe6aa2f2..a62e44b24 100644
--- a/.github/workflows/cleanup-mirror-pr-branch.yml
+++ b/.github/workflows/cleanup-mirror-pr-branch.yml
@@ -11,5 +11,6 @@ permissions:
jobs:
call-delete-branch:
+ if: github.repository_owner != 'vyos'
uses: vyos/.github/.github/workflows/cleanup-mirror-pr-branch.yml@current
secrets: inherit
diff --git a/.github/workflows/mirror-pr-and-sync.yml b/.github/workflows/mirror-pr-and-sync.yml
new file mode 100644
index 000000000..48a67a43f
--- /dev/null
+++ b/.github/workflows/mirror-pr-and-sync.yml
@@ -0,0 +1,26 @@
+name: Create Mirror PR and Repo Sync
+on:
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: 'Source repo PR Number'
+ required: true
+ type: string
+ sync_branch:
+ description: 'branch to sync'
+ required: true
+ type: string
+
+permissions:
+ pull-requests: write
+ contents: write
+
+jobs:
+ call-mirror-pr-and-sync:
+ if: github.repository_owner != 'vyos'
+ uses: VyOS-Networks/vyos-reusable-workflows/.github/workflows/mirror-pr-and-sync.yml@main
+ with:
+ pr_number: ${{ inputs.pr_number }}
+ sync_branch: ${{ inputs.sync_branch }}
+ secrets:
+ PAT: ${{ secrets.PAT }}
diff --git a/.github/workflows/trigger-pr-mirror-repo-sync.yml b/.github/workflows/trigger-pr-mirror-repo-sync.yml
index d5e8ce3b4..f74895987 100644
--- a/.github/workflows/trigger-pr-mirror-repo-sync.yml
+++ b/.github/workflows/trigger-pr-mirror-repo-sync.yml
@@ -8,5 +8,6 @@ on:
jobs:
call-trigger-mirror-pr-repo-sync:
+ if: github.repository_owner == 'vyos' && github.event.pull_request.merged == true
uses: vyos/.github/.github/workflows/trigger-pr-mirror-repo-sync.yml@current
secrets: inherit
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/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2
index 23aad4cb8..b209c8c81 100644
--- a/data/templates/dns-dynamic/ddclient.conf.j2
+++ b/data/templates/dns-dynamic/ddclient.conf.j2
@@ -21,10 +21,7 @@ if{{ ipv }}={{ address }}, \
{{ host }}
{% endmacro %}
### Autogenerated by service_dns_dynamic.py ###
-daemon={{ interval }}
-syslog=yes
ssl=yes
-cache={{ config_file | replace('.conf', '.cache') }}
{# ddclient default (web=dyndns) doesn't support ssl and results in process lockup #}
web=googledomains
{# ddclient default (use=ip) results in confusing warning message in log #}
diff --git a/data/templates/dns-dynamic/override.conf.j2 b/data/templates/dns-dynamic/override.conf.j2
index c0edd8f05..aaed4ff35 100644
--- a/data/templates/dns-dynamic/override.conf.j2
+++ b/data/templates/dns-dynamic/override.conf.j2
@@ -1,4 +1,5 @@
{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %}
+{% set cache_file = config_file.replace('.conf', '.cache') %}
[Unit]
ConditionPathExists={{ config_file }}
Wants=
@@ -7,5 +8,5 @@ After=vyos-router.service
[Service]
EnvironmentFile=
ExecStart=
-ExecStart={{ vrf_command }}/usr/bin/ddclient --file {{ config_file }} --foreground
+ExecStart={{ vrf_command }}/usr/bin/ddclient --file {{ config_file }} --cache {{ cache_file }} --foreground --daemon {{ interval }}
Restart=always
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/data/templates/pmacct/uacctd.conf.j2 b/data/templates/pmacct/uacctd.conf.j2
index aae0a0619..d2de80df4 100644
--- a/data/templates/pmacct/uacctd.conf.j2
+++ b/data/templates/pmacct/uacctd.conf.j2
@@ -25,12 +25,6 @@ imt_mem_pools_number: 169
{% set _ = plugin.append('nfprobe['~ nf_server_key ~ ']') %}
{% endfor %}
{% endif %}
-{% if sflow.server is vyos_defined %}
-{% for server in sflow.server %}
-{% set sf_server_key = 'sf_' ~ server | dot_colon_to_dash %}
-{% set _ = plugin.append('sfprobe[' ~ sf_server_key ~ ']') %}
-{% endfor %}
-{% endif %}
{% if disable_imt is not defined %}
{% set _ = plugin.append('memory') %}
{% endif %}
@@ -61,20 +55,3 @@ nfprobe_timeouts[{{ nf_server_key }}]: expint={{ netflow.timeout.expiry_interval
{% endfor %}
{% endif %}
-
-{% if sflow.server is vyos_defined %}
-# sFlow servers
-{% for server, server_config in sflow.server.items() %}
-{# # prevent pmacct syntax error when using IPv6 flow collectors #}
-{% set sf_server_key = 'sf_' ~ server | dot_colon_to_dash %}
-sfprobe_receiver[{{ sf_server_key }}]: {{ server | bracketize_ipv6 }}:{{ server_config.port }}
-sfprobe_agentip[{{ sf_server_key }}]: {{ sflow.agent_address }}
-{% if sflow.sampling_rate is vyos_defined %}
-sampling_rate[{{ sf_server_key }}]: {{ sflow.sampling_rate }}
-{% endif %}
-{% if sflow.source_address is vyos_defined %}
-sfprobe_source_ip[{{ sf_server_key }}]: {{ sflow.source_address | bracketize_ipv6 }}
-{% endif %}
-
-{% endfor %}
-{% endif %}
diff --git a/data/templates/sflow/hsflowd.conf.j2 b/data/templates/sflow/hsflowd.conf.j2
index 5000956bd..6a1ba2956 100644
--- a/data/templates/sflow/hsflowd.conf.j2
+++ b/data/templates/sflow/hsflowd.conf.j2
@@ -25,6 +25,9 @@ sflow {
pcap { dev={{ iface }} }
{% endfor %}
{% endif %}
+{% if enable_egress is vyos_defined %}
+ psample { group=1 egress=on }
+{% endif %}
{% if drop_monitor_limit is vyos_defined %}
dropmon { limit={{ drop_monitor_limit }} start=on sw=on hw=off }
{% endif %}
diff --git a/data/templates/telegraf/syslog_telegraf.j2 b/data/templates/telegraf/syslog_telegraf.j2
index cdcbd92a4..4fe6382ab 100644
--- a/data/templates/telegraf/syslog_telegraf.j2
+++ b/data/templates/telegraf/syslog_telegraf.j2
@@ -2,4 +2,8 @@
$ModLoad omuxsock
$OMUxSockSocket /run/telegraf/telegraf_syslog.sock
+{% if telegraf.loki is vyos_defined or telegraf.splunk is vyos_defined %}
+*.info;*.notice :omuxsock:
+{% else %}
*.notice :omuxsock:
+{% 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/rules b/debian/rules
index d7c427b0d..f579ffec9 100755
--- a/debian/rules
+++ b/debian/rules
@@ -9,7 +9,7 @@ VYOS_CFG_TMPL_DIR := opt/vyatta/share/vyatta-cfg/templates
VYOS_OP_TMPL_DIR := opt/vyatta/share/vyatta-op/templates
VYOS_MIBS_DIR := usr/share/snmp/mibs
VYOS_LOCALUI_DIR := srv/localui
-VYCONF_CONFIG_DIR := $(VYOS_LIBEXEC_DIR)/vyconf/config
+VYCONF_REFTREE_DIR := $(VYOS_LIBEXEC_DIR)/vyconf/reftree
MIGRATION_SCRIPTS_DIR := opt/vyatta/etc/config-migrate/migrate
ACTIVATION_SCRIPTS_DIR := usr/libexec/vyos/activate
@@ -90,8 +90,8 @@ override_dh_auto_install:
cp -r templates-op/* $(DIR)/$(VYOS_OP_TMPL_DIR)
# Install data files
- mkdir -p $(DIR)/$(VYCONF_CONFIG_DIR)
- cp -r data/reftree.cache $(DIR)/$(VYCONF_CONFIG_DIR)
+ mkdir -p $(DIR)/$(VYCONF_REFTREE_DIR)
+ cp -r data/reftree.cache $(DIR)/$(VYCONF_REFTREE_DIR)
mkdir -p $(DIR)/$(VYOS_DATA_DIR)
cp -r data/* $(DIR)/$(VYOS_DATA_DIR)
# Remove j2lint comments / linter configuration which would insert additional new-lines
diff --git a/debian/vyos-1x-smoketest.postinst b/debian/vyos-1x-smoketest.postinst
index 57149af82..bff73796c 100755
--- a/debian/vyos-1x-smoketest.postinst
+++ b/debian/vyos-1x-smoketest.postinst
@@ -2,14 +2,18 @@
BUSYBOX_TAG="docker.io/library/busybox:stable"
BUSYBOX_PATH="/usr/share/vyos/busybox-stable.tar"
-if [[ -f $BUSYBOX_PATH ]]; then
- rm -f $BUSYBOX_PATH
+if [[ ! -f $BUSYBOX_PATH ]]; then
+ skopeo copy --additional-tag "$BUSYBOX_TAG" "docker://$BUSYBOX_TAG" "docker-archive:/$BUSYBOX_PATH"
fi
-skopeo copy --additional-tag "$BUSYBOX_TAG" "docker://$BUSYBOX_TAG" "docker-archive:/$BUSYBOX_PATH"
TACPLUS_TAG="docker.io/lfkeitel/tacacs_plus:alpine"
TACPLUS_PATH="/usr/share/vyos/tacplus-alpine.tar"
-if [[ -f $TACPLUS_PATH ]]; then
- rm -f $TACPLUS_PATH
+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
-skopeo copy --additional-tag "$TACPLUS_TAG" "docker://$TACPLUS_TAG" "docker-archive:/$TACPLUS_PATH"
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index ff5a91e09..fde58651a 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -272,3 +272,7 @@ update-alternatives --set regulatory.db /lib/firmware/regulatory.db-upstream
if systemctl is-active --quiet vyos-configd; then
systemctl restart vyos-configd
fi
+# Restart vyos-domain-resolver if running
+if systemctl is-active --quiet vyos-domain-resolver; then
+ systemctl restart vyos-domain-resolver
+fi
diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in
index ad1815604..04318a7c9 100644
--- a/interface-definitions/container.xml.in
+++ b/interface-definitions/container.xml.in
@@ -131,7 +131,7 @@
<properties>
<help>Add custom environment variables</help>
<constraint>
- <regex>[-_a-zA-Z0-9]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
</constraint>
<constraintErrorMessage>Environment variable name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
</properties>
diff --git a/interface-definitions/include/haproxy/rule-backend.xml.i b/interface-definitions/include/haproxy/rule-backend.xml.i
index b2be4fde4..1df9d5dcf 100644
--- a/interface-definitions/include/haproxy/rule-backend.xml.i
+++ b/interface-definitions/include/haproxy/rule-backend.xml.i
@@ -47,7 +47,7 @@
<properties>
<help>Server name</help>
<constraint>
- <regex>[-_a-zA-Z0-9]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
</constraint>
<constraintErrorMessage>Server name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
</properties>
diff --git a/interface-definitions/include/haproxy/rule-frontend.xml.i b/interface-definitions/include/haproxy/rule-frontend.xml.i
index 001ae2d80..eabdd8632 100644
--- a/interface-definitions/include/haproxy/rule-frontend.xml.i
+++ b/interface-definitions/include/haproxy/rule-frontend.xml.i
@@ -47,9 +47,12 @@
<properties>
<help>Backend name</help>
<constraint>
- <regex>[-_a-zA-Z0-9]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
</constraint>
<constraintErrorMessage>Server name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
+ <completionHelp>
+ <path>load-balancing haproxy backend</path>
+ </completionHelp>
</properties>
</leafNode>
</children>
diff --git a/interface-definitions/include/netlink/log-level.xml.i b/interface-definitions/include/netlink/log-level.xml.i
new file mode 100644
index 000000000..bbaf9412c
--- /dev/null
+++ b/interface-definitions/include/netlink/log-level.xml.i
@@ -0,0 +1,21 @@
+<!-- include start from netlink/log-level.xml.i -->
+<leafNode name="log-level">
+ <properties>
+ <help>Set log-level</help>
+ <completionHelp>
+ <list>info debug</list>
+ </completionHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Info log level</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug log level</description>
+ </valueHelp>
+ <constraint>
+ <regex>(info|debug)</regex>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/netlink/queue-size.xml.i b/interface-definitions/include/netlink/queue-size.xml.i
new file mode 100644
index 000000000..d284838cf
--- /dev/null
+++ b/interface-definitions/include/netlink/queue-size.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from netlink/queue-size.xml.i -->
+<leafNode name="queue-size">
+ <properties>
+ <help>Internal message queue size</help>
+ <valueHelp>
+ <format>u32:100-2147483647</format>
+ <description>Queue size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-2147483647"/>
+ </constraint>
+ <constraintErrorMessage>Queue size must be between 100 and 2147483647</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/version/flow-accounting-version.xml.i b/interface-definitions/include/version/flow-accounting-version.xml.i
index 5b01fe4b5..95d1e20db 100644
--- a/interface-definitions/include/version/flow-accounting-version.xml.i
+++ b/interface-definitions/include/version/flow-accounting-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/flow-accounting-version.xml.i -->
-<syntaxVersion component='flow-accounting' version='1'></syntaxVersion>
+<syntaxVersion component='flow-accounting' version='2'></syntaxVersion>
<!-- include end -->
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/interfaces_wireguard.xml.in b/interface-definitions/interfaces_wireguard.xml.in
index ce49de038..4f8b6c751 100644
--- a/interface-definitions/interfaces_wireguard.xml.in
+++ b/interface-definitions/interfaces_wireguard.xml.in
@@ -40,6 +40,19 @@
</properties>
<defaultValue>0</defaultValue>
</leafNode>
+ <leafNode name="max-dns-retry">
+ <properties>
+ <help>DNS retries when resolve fails</help>
+ <valueHelp>
+ <format>u32:1-15</format>
+ <description>Maximum number of retries</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-15"/>
+ </constraint>
+ </properties>
+ <defaultValue>3</defaultValue>
+ </leafNode>
<leafNode name="private-key">
<properties>
<help>Base64 encoded private key</help>
@@ -104,6 +117,18 @@
</constraint>
</properties>
</leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>Hostname of tunnel endpoint</help>
+ <valueHelp>
+ <format>hostname</format>
+ <description>FQDN of WireGuard endpoint</description>
+ </valueHelp>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ </properties>
+ </leafNode>
#include <include/port-number.xml.i>
<leafNode name="persistent-keepalive">
<properties>
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/interface-definitions/service_monitoring_network_event.xml.in b/interface-definitions/service_monitoring_network_event.xml.in
new file mode 100644
index 000000000..edf23a06a
--- /dev/null
+++ b/interface-definitions/service_monitoring_network_event.xml.in
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="monitoring">
+ <properties>
+ <help>Monitoring services</help>
+ </properties>
+ <children>
+ <node name="network-event" owner="${vyos_conf_scripts_dir}/service_monitoring_network_event.py">
+ <properties>
+ <help>Network event logger</help>
+ <priority>1280</priority>
+ </properties>
+ <children>
+ <node name="event">
+ <properties>
+ <help>Network event type</help>
+ </properties>
+ <children>
+ <leafNode name="route">
+ <properties>
+ <help>Log routing table update events</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="link">
+ <properties>
+ <help>Log link status change events</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="addr">
+ <properties>
+ <help>Log address assignment and removal events</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="neigh">
+ <properties>
+ <help>Log neighbor (ARP/ND) table update events</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="rule">
+ <properties>
+ <help>Log policy routing rule change events</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/netlink/queue-size.xml.i>
+ #include <include/netlink/log-level.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system_conntrack.xml.in b/interface-definitions/system_conntrack.xml.in
index cd59d1308..54610b625 100644
--- a/interface-definitions/system_conntrack.xml.in
+++ b/interface-definitions/system_conntrack.xml.in
@@ -263,38 +263,8 @@
<valueless/>
</properties>
</leafNode>
- <leafNode name="queue-size">
- <properties>
- <help>Internal message queue size</help>
- <valueHelp>
- <format>u32:100-999999</format>
- <description>Queue size</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-999999"/>
- </constraint>
- <constraintErrorMessage>Queue size must be between 100 and 999999</constraintErrorMessage>
- </properties>
- </leafNode>
- <leafNode name="log-level">
- <properties>
- <help>Set log-level. Log must be enable.</help>
- <completionHelp>
- <list>info debug</list>
- </completionHelp>
- <valueHelp>
- <format>info</format>
- <description>Info log level</description>
- </valueHelp>
- <valueHelp>
- <format>debug</format>
- <description>Debug log level</description>
- </valueHelp>
- <constraint>
- <regex>(info|debug)</regex>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/netlink/queue-size.xml.i>
+ #include <include/netlink/log-level.xml.i>
</children>
</node>
<node name="modules">
diff --git a/interface-definitions/system_flow-accounting.xml.in b/interface-definitions/system_flow-accounting.xml.in
index 83a2480a3..4799205ad 100644
--- a/interface-definitions/system_flow-accounting.xml.in
+++ b/interface-definitions/system_flow-accounting.xml.in
@@ -362,73 +362,6 @@
</node>
</children>
</node>
- <node name="sflow">
- <properties>
- <help>sFlow settings</help>
- </properties>
- <children>
- <leafNode name="agent-address">
- <properties>
- <help>sFlow agent IPv4 address</help>
- <completionHelp>
- <list>auto</list>
- <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script>
- </completionHelp>
- <valueHelp>
- <format>ipv4</format>
- <description>sFlow IPv4 agent address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="sampling-rate">
- <properties>
- <help>sFlow sampling-rate</help>
- <valueHelp>
- <format>u32</format>
- <description>Sampling rate (1 in N packets)</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
- </constraint>
- </properties>
- </leafNode>
- <tagNode name="server">
- <properties>
- <help>sFlow destination server</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 server to export sFlow</description>
- </valueHelp>
- <valueHelp>
- <format>ipv6</format>
- <description>IPv6 server to export sFlow</description>
- </valueHelp>
- <constraint>
- <validator name="ip-address"/>
- </constraint>
- </properties>
- <children>
- <leafNode name="port">
- <properties>
- <help>sFlow port number</help>
- <valueHelp>
- <format>u32:1025-65535</format>
- <description>sFlow port number</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1025-65535"/>
- </constraint>
- </properties>
- <defaultValue>6343</defaultValue>
- </leafNode>
- </children>
- </tagNode>
- #include <include/source-address-ipv4-ipv6.xml.i>
- </children>
- </node>
#include <include/interface/vrf.xml.i>
</children>
</node>
diff --git a/interface-definitions/system_sflow.xml.in b/interface-definitions/system_sflow.xml.in
index aaf4033d8..2cd7a5d12 100644
--- a/interface-definitions/system_sflow.xml.in
+++ b/interface-definitions/system_sflow.xml.in
@@ -106,6 +106,12 @@
</leafNode>
</children>
</tagNode>
+ <leafNode name="enable-egress">
+ <properties>
+ <help>Enable egress sampling</help>
+ <valueless/>
+ </properties>
+ </leafNode>
#include <include/interface/vrf.xml.i>
</children>
</node>
diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in
index 67d86a1d0..70957c5fa 100644
--- a/interface-definitions/xml-component-version.xml.in
+++ b/interface-definitions/xml-component-version.xml.in
@@ -1,52 +1,4 @@
<?xml version="1.0"?>
<interfaceDefinition>
- #include <include/version/bgp-version.xml.i>
- #include <include/version/broadcast-relay-version.xml.i>
- #include <include/version/cluster-version.xml.i>
- #include <include/version/config-management-version.xml.i>
- #include <include/version/conntrack-sync-version.xml.i>
- #include <include/version/conntrack-version.xml.i>
- #include <include/version/container-version.xml.i>
- #include <include/version/dhcp-relay-version.xml.i>
- #include <include/version/dhcp-server-version.xml.i>
- #include <include/version/dhcpv6-server-version.xml.i>
- #include <include/version/dns-dynamic-version.xml.i>
- #include <include/version/dns-forwarding-version.xml.i>
- #include <include/version/firewall-version.xml.i>
- #include <include/version/flow-accounting-version.xml.i>
- #include <include/version/https-version.xml.i>
- #include <include/version/interfaces-version.xml.i>
- #include <include/version/ids-version.xml.i>
- #include <include/version/ipoe-server-version.xml.i>
- #include <include/version/ipsec-version.xml.i>
- #include <include/version/openvpn-version.xml.i>
- #include <include/version/isis-version.xml.i>
- #include <include/version/l2tp-version.xml.i>
- #include <include/version/lldp-version.xml.i>
- #include <include/version/mdns-version.xml.i>
- #include <include/version/monitoring-version.xml.i>
- #include <include/version/nat66-version.xml.i>
- #include <include/version/nat-version.xml.i>
- #include <include/version/ntp-version.xml.i>
- #include <include/version/openconnect-version.xml.i>
- #include <include/version/ospf-version.xml.i>
- #include <include/version/pim-version.xml.i>
- #include <include/version/policy-version.xml.i>
- #include <include/version/pppoe-server-version.xml.i>
- #include <include/version/pptp-version.xml.i>
- #include <include/version/qos-version.xml.i>
- #include <include/version/quagga-version.xml.i>
- #include <include/version/rip-version.xml.i>
- #include <include/version/rpki-version.xml.i>
- #include <include/version/salt-version.xml.i>
- #include <include/version/snmp-version.xml.i>
- #include <include/version/ssh-version.xml.i>
- #include <include/version/sstp-version.xml.i>
- #include <include/version/system-version.xml.i>
- #include <include/version/vrf-version.xml.i>
- #include <include/version/vrrp-version.xml.i>
- #include <include/version/vyos-accel-ppp-version.xml.i>
- #include <include/version/wanloadbalance-version.xml.i>
- #include <include/version/webproxy-version.xml.i>
- #include <include/version/reverseproxy-version.xml.i>
+ #include <include/version/*>
</interfaceDefinition>
diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in
index 63b1f62bb..4ee66a90c 100644
--- a/op-mode-definitions/dhcp.xml.in
+++ b/op-mode-definitions/dhcp.xml.in
@@ -140,7 +140,7 @@
<properties>
<help>Show DHCP server statistics</help>
</properties>
- <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet</command>
+ <command>${vyos_op_scripts_dir}/dhcp.py show_server_pool_statistics --family inet</command>
<children>
<tagNode name="pool">
<properties>
@@ -149,7 +149,7 @@
<path>service dhcp-server shared-network-name</path>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet --pool $6</command>
+ <command>${vyos_op_scripts_dir}/dhcp.py show_server_pool_statistics --family inet --pool $6</command>
</tagNode>
</children>
</node>
@@ -232,7 +232,7 @@
<properties>
<help>Show DHCPv6 server statistics</help>
</properties>
- <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet6</command>
+ <command>${vyos_op_scripts_dir}/dhcp.py show_server_pool_statistics --family inet6</command>
<children>
<tagNode name="pool">
<properties>
@@ -241,7 +241,7 @@
<path>service dhcpv6-server shared-network-name</path>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet6 --pool $6</command>
+ <command>${vyos_op_scripts_dir}/dhcp.py show_server_pool_statistics --family inet6 --pool $6</command>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/include/log/network-event-type-interface.xml.i b/op-mode-definitions/include/log/network-event-type-interface.xml.i
new file mode 100644
index 000000000..2d781223c
--- /dev/null
+++ b/op-mode-definitions/include/log/network-event-type-interface.xml.i
@@ -0,0 +1,11 @@
+<!-- included start from network-event-type-interface.xml.i -->
+<tagNode name="interface">
+ <properties>
+ <help>Show log for specific interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service | grep "$(echo "\[$4\]" | tr '[:lower:]' '[:upper:]')" | grep "\b$6\b"</command>
+</tagNode>
+<!-- included end -->
diff --git a/op-mode-definitions/include/show-interface-type-event-log.xml.i b/op-mode-definitions/include/show-interface-type-event-log.xml.i
new file mode 100644
index 000000000..c69073fda
--- /dev/null
+++ b/op-mode-definitions/include/show-interface-type-event-log.xml.i
@@ -0,0 +1,40 @@
+<!-- included start from show-interface-type-event-log.xml.i -->
+<node name="event-log">
+ <properties>
+ <help>Show network interface change event log</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b"</command>
+ <children>
+ <leafNode name="route">
+ <properties>
+ <help>Show log for route events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"</command>
+ </leafNode>
+ <leafNode name="link">
+ <properties>
+ <help>Show log for network link events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"</command>
+ </leafNode>
+ <leafNode name="addr">
+ <properties>
+ <help>Show log for network address events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"</command>
+ </leafNode>
+ <leafNode name="neigh">
+ <properties>
+ <help>Show log for neighbor table events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"</command>
+ </leafNode>
+ <leafNode name="rule">
+ <properties>
+ <help>Show log for PBR rule change events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"</command>
+ </leafNode>
+ </children>
+</node>
+<!-- included end -->
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/op-mode-definitions/reset-wireguard.xml.in b/op-mode-definitions/reset-wireguard.xml.in
new file mode 100644
index 000000000..c2243f519
--- /dev/null
+++ b/op-mode-definitions/reset-wireguard.xml.in
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reset">
+ <children>
+ <node name="wireguard">
+ <properties>
+ <help>Reset WireGuard Peers</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>WireGuard interface name</help>
+ <completionHelp>
+ <path>interfaces wireguard</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4"</command>
+ <children>
+ <tagNode name="peer">
+ <properties>
+ <help>WireGuard peer name</help>
+ <completionHelp>
+ <path>interfaces wireguard ${COMP_WORDS[3]} peer</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4" --peer="$6"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-bonding.xml.in b/op-mode-definitions/show-interfaces-bonding.xml.in
index e2950331b..0abb7cd5a 100644
--- a/op-mode-definitions/show-interfaces-bonding.xml.in
+++ b/op-mode-definitions/show-interfaces-bonding.xml.in
@@ -67,6 +67,7 @@
</leafNode>
</children>
</tagNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="bonding">
diff --git a/op-mode-definitions/show-interfaces-bridge.xml.in b/op-mode-definitions/show-interfaces-bridge.xml.in
index dc813682d..998dacd38 100644
--- a/op-mode-definitions/show-interfaces-bridge.xml.in
+++ b/op-mode-definitions/show-interfaces-bridge.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=bridge</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="bridge">
diff --git a/op-mode-definitions/show-interfaces-dummy.xml.in b/op-mode-definitions/show-interfaces-dummy.xml.in
index b8ec7da91..18f21e97e 100644
--- a/op-mode-definitions/show-interfaces-dummy.xml.in
+++ b/op-mode-definitions/show-interfaces-dummy.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=dummy</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="dummy">
diff --git a/op-mode-definitions/show-interfaces-ethernet.xml.in b/op-mode-definitions/show-interfaces-ethernet.xml.in
index 09f0b3933..8a23455bf 100644
--- a/op-mode-definitions/show-interfaces-ethernet.xml.in
+++ b/op-mode-definitions/show-interfaces-ethernet.xml.in
@@ -68,6 +68,7 @@
</leafNode>
</children>
</tagNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="ethernet">
diff --git a/op-mode-definitions/show-interfaces-geneve.xml.in b/op-mode-definitions/show-interfaces-geneve.xml.in
index d3d188031..b5fe84ca7 100644
--- a/op-mode-definitions/show-interfaces-geneve.xml.in
+++ b/op-mode-definitions/show-interfaces-geneve.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=geneve</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="geneve">
diff --git a/op-mode-definitions/show-interfaces-input.xml.in b/op-mode-definitions/show-interfaces-input.xml.in
index e5d420056..c9856f77f 100644
--- a/op-mode-definitions/show-interfaces-input.xml.in
+++ b/op-mode-definitions/show-interfaces-input.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=input</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="input">
diff --git a/op-mode-definitions/show-interfaces-l2tpv3.xml.in b/op-mode-definitions/show-interfaces-l2tpv3.xml.in
index 2d165171c..88b73d7d7 100644
--- a/op-mode-definitions/show-interfaces-l2tpv3.xml.in
+++ b/op-mode-definitions/show-interfaces-l2tpv3.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=l2tpv3</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="l2tpv3">
diff --git a/op-mode-definitions/show-interfaces-loopback.xml.in b/op-mode-definitions/show-interfaces-loopback.xml.in
index d341a6359..467e1a13d 100644
--- a/op-mode-definitions/show-interfaces-loopback.xml.in
+++ b/op-mode-definitions/show-interfaces-loopback.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=loopback</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="loopback">
diff --git a/op-mode-definitions/show-interfaces-macsec.xml.in b/op-mode-definitions/show-interfaces-macsec.xml.in
index 28264d252..640031b77 100644
--- a/op-mode-definitions/show-interfaces-macsec.xml.in
+++ b/op-mode-definitions/show-interfaces-macsec.xml.in
@@ -29,6 +29,9 @@
</completionHelp>
</properties>
<command>ip macsec show $4</command>
+ <children>
+ #include <include/show-interface-type-event-log.xml.i>
+ </children>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/show-interfaces-pppoe.xml.in b/op-mode-definitions/show-interfaces-pppoe.xml.in
index 1c6e0b83e..c1f502cb3 100644
--- a/op-mode-definitions/show-interfaces-pppoe.xml.in
+++ b/op-mode-definitions/show-interfaces-pppoe.xml.in
@@ -28,6 +28,7 @@
</properties>
<command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="pppoe">
diff --git a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in
index 4ab2a5fbb..a9e4257ce 100644
--- a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in
+++ b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=pseudo-ethernet</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="pseudo-ethernet">
diff --git a/op-mode-definitions/show-interfaces-sstpc.xml.in b/op-mode-definitions/show-interfaces-sstpc.xml.in
index 307276f72..3bd7a8247 100644
--- a/op-mode-definitions/show-interfaces-sstpc.xml.in
+++ b/op-mode-definitions/show-interfaces-sstpc.xml.in
@@ -28,6 +28,7 @@
</properties>
<command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="sstpc">
diff --git a/op-mode-definitions/show-interfaces-tunnel.xml.in b/op-mode-definitions/show-interfaces-tunnel.xml.in
index b99b0cbb2..579b173cb 100644
--- a/op-mode-definitions/show-interfaces-tunnel.xml.in
+++ b/op-mode-definitions/show-interfaces-tunnel.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=tunnel</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="tunnel">
diff --git a/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in
index 18ae806b7..4112a17af 100644
--- a/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in
+++ b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=virtual-ethernet</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="virtual-ethernet">
diff --git a/op-mode-definitions/show-interfaces-vti.xml.in b/op-mode-definitions/show-interfaces-vti.xml.in
index ae5cfeb9c..d13b3e7cc 100644
--- a/op-mode-definitions/show-interfaces-vti.xml.in
+++ b/op-mode-definitions/show-interfaces-vti.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=vti</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="vti">
diff --git a/op-mode-definitions/show-interfaces-vxlan.xml.in b/op-mode-definitions/show-interfaces-vxlan.xml.in
index fd729b986..89c8d075b 100644
--- a/op-mode-definitions/show-interfaces-vxlan.xml.in
+++ b/op-mode-definitions/show-interfaces-vxlan.xml.in
@@ -19,6 +19,7 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=vxlan</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="vxlan">
diff --git a/op-mode-definitions/show-interfaces-wireguard.xml.in b/op-mode-definitions/show-interfaces-wireguard.xml.in
index 0e61ccd74..d86152a21 100644
--- a/op-mode-definitions/show-interfaces-wireguard.xml.in
+++ b/op-mode-definitions/show-interfaces-wireguard.xml.in
@@ -43,6 +43,7 @@
</properties>
<command>sudo ${vyos_op_scripts_dir}/interfaces_wireguard.py show_summary --intf-name="$4"</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="wireguard">
diff --git a/op-mode-definitions/show-interfaces-wireless.xml.in b/op-mode-definitions/show-interfaces-wireless.xml.in
index 09c9a7895..b0a1502de 100644
--- a/op-mode-definitions/show-interfaces-wireless.xml.in
+++ b/op-mode-definitions/show-interfaces-wireless.xml.in
@@ -73,6 +73,7 @@
</leafNode>
</children>
</tagNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
</children>
diff --git a/op-mode-definitions/show-interfaces-wwan.xml.in b/op-mode-definitions/show-interfaces-wwan.xml.in
index 3682282a3..2301b32d0 100644
--- a/op-mode-definitions/show-interfaces-wwan.xml.in
+++ b/op-mode-definitions/show-interfaces-wwan.xml.in
@@ -80,6 +80,7 @@
</properties>
<command>echo not implemented</command>
</leafNode>
+ #include <include/show-interface-type-event-log.xml.i>
</children>
</tagNode>
<node name="wwan">
diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in
index 7ace50cc9..5ee7c973f 100755
--- a/op-mode-definitions/show-log.xml.in
+++ b/op-mode-definitions/show-log.xml.in
@@ -958,6 +958,68 @@
</properties>
<command>journalctl --no-hostname --boot --unit squid.service</command>
</leafNode>
+ <node name="network-event">
+ <properties>
+ <help>Show log for network events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service</command>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Show log for specific interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep $5</command>
+ </tagNode>
+ <node name="route">
+ <properties>
+ <help>Show log for route events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"</command>
+ <children>
+ #include <include/log/network-event-type-interface.xml.i>
+ </children>
+ </node>
+ <node name="link">
+ <properties>
+ <help>Show log for network link events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"</command>
+ <children>
+ #include <include/log/network-event-type-interface.xml.i>
+ </children>
+ </node>
+ <node name="addr">
+ <properties>
+ <help>Show log for network address events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"</command>
+ <children>
+ #include <include/log/network-event-type-interface.xml.i>
+ </children>
+ </node>
+ <node name="neigh">
+ <properties>
+ <help>Show log for neighbor table events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"</command>
+ <children>
+ #include <include/log/network-event-type-interface.xml.i>
+ </children>
+ </node>
+ <node name="rule">
+ <properties>
+ <help>Show log for PBR rule change events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"</command>
+ <children>
+ #include <include/log/network-event-type-interface.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
</children>
</node>
</children>
diff --git a/python/vyos/configquery.py b/python/vyos/configquery.py
index 5d6ca9be9..4c4ead0a3 100644
--- a/python/vyos/configquery.py
+++ b/python/vyos/configquery.py
@@ -1,4 +1,4 @@
-# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-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
@@ -120,11 +120,14 @@ class ConfigTreeQuery(GenericConfigQuery):
def get_config_dict(self, path=[], effective=False, key_mangling=None,
get_first_key=False, no_multi_convert=False,
- no_tag_node_value_mangle=False):
+ no_tag_node_value_mangle=False, with_defaults=False,
+ with_recursive_defaults=False):
return self.config.get_config_dict(path, effective=effective,
key_mangling=key_mangling, get_first_key=get_first_key,
no_multi_convert=no_multi_convert,
- no_tag_node_value_mangle=no_tag_node_value_mangle)
+ no_tag_node_value_mangle=no_tag_node_value_mangle,
+ with_defaults=with_defaults,
+ with_recursive_defaults=with_recursive_defaults)
class VbashOpRun(GenericOpRun):
def __init__(self):
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index fb79e8459..8d27a7e46 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -1,5 +1,5 @@
# configtree -- a standalone VyOS config file manipulation library (Python bindings)
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 VyOS maintainers and contributors
#
# 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;
@@ -21,33 +21,40 @@ from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool
LIBPATH = '/usr/lib/libvyosconfig.so.0'
+
def replace_backslash(s, search, replace):
"""Modify quoted strings containing backslashes not of escape sequences"""
+
def replace_method(match):
result = match.group().replace(search, replace)
return result
+
p = re.compile(r'("[^"]*[\\][^"]*"\n|\'[^\']*[\\][^\']*\'\n)')
return p.sub(replace_method, s)
+
def escape_backslash(string: str) -> str:
"""Escape single backslashes in quoted strings"""
result = replace_backslash(string, '\\', '\\\\')
return result
+
def unescape_backslash(string: str) -> str:
"""Unescape backslashes in quoted strings"""
result = replace_backslash(string, '\\\\', '\\')
return result
+
def extract_version(s):
- """ Extract the version string from the config string """
+ """Extract the version string from the config string"""
t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE)
return (s, ''.join(t[1:]))
+
def check_path(path):
# Necessary type checking
if not isinstance(path, list):
- raise TypeError("Expected a list, got a {}".format(type(path)))
+ raise TypeError('Expected a list, got a {}'.format(type(path)))
else:
pass
@@ -165,7 +172,7 @@ class ConfigTree(object):
config = self.__from_string(config_section.encode())
if config is None:
msg = self.__get_error().decode()
- raise ValueError("Failed to parse config: {0}".format(msg))
+ raise ValueError('Failed to parse config: {0}'.format(msg))
else:
self.__config = config
self.__version = version_section
@@ -195,10 +202,10 @@ class ConfigTree(object):
config_string = unescape_backslash(config_string)
if no_version:
return config_string
- config_string = "{0}\n{1}".format(config_string, self.__version)
+ config_string = '{0}\n{1}'.format(config_string, self.__version)
return config_string
- def to_commands(self, op="set"):
+ def to_commands(self, op='set'):
commands = self.__to_commands(self.__config, op.encode()).decode()
commands = unescape_backslash(commands)
return commands
@@ -211,11 +218,11 @@ class ConfigTree(object):
def create_node(self, path):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__create_node(self.__config, path_str)
- if (res != 0):
- raise ConfigTreeError(f"Path already exists: {path}")
+ if res != 0:
+ raise ConfigTreeError(f'Path already exists: {path}')
def set(self, path, value=None, replace=True):
"""Set new entry in VyOS configuration.
@@ -227,7 +234,7 @@ class ConfigTree(object):
"""
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
if value is None:
self.__set_valueless(self.__config, path_str)
@@ -238,25 +245,27 @@ class ConfigTree(object):
self.__set_add_value(self.__config, path_str, str(value).encode())
if self.__migration:
- self.migration_log.info(f"- op: set path: {path} value: {value} replace: {replace}")
+ self.migration_log.info(
+ f'- op: set path: {path} value: {value} replace: {replace}'
+ )
def delete(self, path):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__delete(self.__config, path_str)
- if (res != 0):
+ if res != 0:
raise ConfigTreeError(f"Path doesn't exist: {path}")
if self.__migration:
- self.migration_log.info(f"- op: delete path: {path}")
+ self.migration_log.info(f'- op: delete path: {path}')
def delete_value(self, path, value):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__delete_value(self.__config, path_str, value.encode())
- if (res != 0):
+ if res != 0:
if res == 1:
raise ConfigTreeError(f"Path doesn't exist: {path}")
elif res == 2:
@@ -265,11 +274,11 @@ class ConfigTree(object):
raise ConfigTreeError()
if self.__migration:
- self.migration_log.info(f"- op: delete_value path: {path} value: {value}")
+ self.migration_log.info(f'- op: delete_value path: {path} value: {value}')
def rename(self, path, new_name):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
newname_str = new_name.encode()
# Check if a node with intended new name already exists
@@ -277,42 +286,46 @@ class ConfigTree(object):
if self.exists(new_path):
raise ConfigTreeError()
res = self.__rename(self.__config, path_str, newname_str)
- if (res != 0):
+ if res != 0:
raise ConfigTreeError("Path [{}] doesn't exist".format(path))
if self.__migration:
- self.migration_log.info(f"- op: rename old_path: {path} new_path: {new_path}")
+ self.migration_log.info(
+ f'- op: rename old_path: {path} new_path: {new_path}'
+ )
def copy(self, old_path, new_path):
check_path(old_path)
check_path(new_path)
- oldpath_str = " ".join(map(str, old_path)).encode()
- newpath_str = " ".join(map(str, new_path)).encode()
+ oldpath_str = ' '.join(map(str, old_path)).encode()
+ newpath_str = ' '.join(map(str, new_path)).encode()
# Check if a node with intended new name already exists
if self.exists(new_path):
raise ConfigTreeError()
res = self.__copy(self.__config, oldpath_str, newpath_str)
- if (res != 0):
+ if res != 0:
msg = self.__get_error().decode()
raise ConfigTreeError(msg)
if self.__migration:
- self.migration_log.info(f"- op: copy old_path: {old_path} new_path: {new_path}")
+ self.migration_log.info(
+ f'- op: copy old_path: {old_path} new_path: {new_path}'
+ )
def exists(self, path):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__exists(self.__config, path_str)
- if (res == 0):
+ if res == 0:
return False
else:
return True
def list_nodes(self, path, path_must_exist=True):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res_json = self.__list_nodes(self.__config, path_str).decode()
res = json.loads(res_json)
@@ -327,7 +340,7 @@ class ConfigTree(object):
def return_value(self, path):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res_json = self.__return_value(self.__config, path_str).decode()
res = json.loads(res_json)
@@ -339,7 +352,7 @@ class ConfigTree(object):
def return_values(self, path):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res_json = self.__return_values(self.__config, path_str).decode()
res = json.loads(res_json)
@@ -351,61 +364,62 @@ class ConfigTree(object):
def is_tag(self, path):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__is_tag(self.__config, path_str)
- if (res >= 1):
+ if res >= 1:
return True
else:
return False
def set_tag(self, path, value=True):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__set_tag(self.__config, path_str, value)
- if (res == 0):
+ if res == 0:
return True
else:
raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
def is_leaf(self, path):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
return self.__is_leaf(self.__config, path_str)
def set_leaf(self, path, value):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__set_leaf(self.__config, path_str, value)
- if (res == 0):
+ if res == 0:
return True
else:
raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
def get_subtree(self, path, with_node=False):
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__get_subtree(self.__config, path_str, with_node)
subt = ConfigTree(address=res)
return subt
+
def show_diff(left, right, path=[], commands=False, libpath=LIBPATH):
if left is None:
left = ConfigTree(config_string='\n')
if right is None:
right = ConfigTree(config_string='\n')
if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
- raise TypeError("Arguments must be instances of ConfigTree")
+ raise TypeError('Arguments must be instances of ConfigTree')
if path:
if (not left.exists(path)) and (not right.exists(path)):
raise ConfigTreeError(f"Path {path} doesn't exist")
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
__lib = cdll.LoadLibrary(libpath)
__show_diff = __lib.show_diff
@@ -417,20 +431,21 @@ def show_diff(left, right, path=[], commands=False, libpath=LIBPATH):
res = __show_diff(commands, path_str, left._get_config(), right._get_config())
res = res.decode()
- if res == "#1@":
+ if res == '#1@':
msg = __get_error().decode()
raise ConfigTreeError(msg)
res = unescape_backslash(res)
return res
+
def union(left, right, libpath=LIBPATH):
if left is None:
left = ConfigTree(config_string='\n')
if right is None:
right = ConfigTree(config_string='\n')
if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
- raise TypeError("Arguments must be instances of ConfigTree")
+ raise TypeError('Arguments must be instances of ConfigTree')
__lib = cdll.LoadLibrary(libpath)
__tree_union = __lib.tree_union
@@ -440,14 +455,15 @@ def union(left, right, libpath=LIBPATH):
__get_error.argtypes = []
__get_error.restype = c_char_p
- res = __tree_union( left._get_config(), right._get_config())
+ res = __tree_union(left._get_config(), right._get_config())
tree = ConfigTree(address=res)
return tree
+
def mask_inclusive(left, right, libpath=LIBPATH):
if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
- raise TypeError("Arguments must be instances of ConfigTree")
+ raise TypeError('Arguments must be instances of ConfigTree')
try:
__lib = cdll.LoadLibrary(libpath)
@@ -469,7 +485,8 @@ def mask_inclusive(left, right, libpath=LIBPATH):
return tree
-def reference_tree_to_json(from_dir, to_file, internal_cache="", libpath=LIBPATH):
+
+def reference_tree_to_json(from_dir, to_file, internal_cache='', libpath=LIBPATH):
try:
__lib = cdll.LoadLibrary(libpath)
__reference_tree_to_json = __lib.reference_tree_to_json
@@ -477,13 +494,66 @@ def reference_tree_to_json(from_dir, to_file, internal_cache="", libpath=LIBPATH
__get_error = __lib.get_error
__get_error.argtypes = []
__get_error.restype = c_char_p
- res = __reference_tree_to_json(internal_cache.encode(), from_dir.encode(), to_file.encode())
+ res = __reference_tree_to_json(
+ internal_cache.encode(), from_dir.encode(), to_file.encode()
+ )
except Exception as e:
raise ConfigTreeError(e)
if res == 1:
msg = __get_error().decode()
raise ConfigTreeError(msg)
+
+def merge_reference_tree_cache(cache_dir, primary_name, result_name, libpath=LIBPATH):
+ try:
+ __lib = cdll.LoadLibrary(libpath)
+ __merge_reference_tree_cache = __lib.merge_reference_tree_cache
+ __merge_reference_tree_cache.argtypes = [c_char_p, c_char_p, c_char_p]
+ __get_error = __lib.get_error
+ __get_error.argtypes = []
+ __get_error.restype = c_char_p
+ res = __merge_reference_tree_cache(
+ cache_dir.encode(), primary_name.encode(), result_name.encode()
+ )
+ except Exception as e:
+ raise ConfigTreeError(e)
+ if res == 1:
+ msg = __get_error().decode()
+ raise ConfigTreeError(msg)
+
+
+def interface_definitions_to_cache(from_dir, cache_path, libpath=LIBPATH):
+ try:
+ __lib = cdll.LoadLibrary(libpath)
+ __interface_definitions_to_cache = __lib.interface_definitions_to_cache
+ __interface_definitions_to_cache.argtypes = [c_char_p, c_char_p]
+ __get_error = __lib.get_error
+ __get_error.argtypes = []
+ __get_error.restype = c_char_p
+ res = __interface_definitions_to_cache(from_dir.encode(), cache_path.encode())
+ except Exception as e:
+ raise ConfigTreeError(e)
+ if res == 1:
+ msg = __get_error().decode()
+ raise ConfigTreeError(msg)
+
+
+def reference_tree_cache_to_json(cache_path, render_file, libpath=LIBPATH):
+ try:
+ __lib = cdll.LoadLibrary(libpath)
+ __reference_tree_cache_to_json = __lib.reference_tree_cache_to_json
+ __reference_tree_cache_to_json.argtypes = [c_char_p, c_char_p]
+ __get_error = __lib.get_error
+ __get_error.argtypes = []
+ __get_error.restype = c_char_p
+ res = __reference_tree_cache_to_json(cache_path.encode(), render_file.encode())
+ except Exception as e:
+ raise ConfigTreeError(e)
+ if res == 1:
+ msg = __get_error().decode()
+ raise ConfigTreeError(msg)
+
+
class DiffTree:
def __init__(self, left, right, path=[], libpath=LIBPATH):
if left is None:
@@ -491,7 +561,7 @@ class DiffTree:
if right is None:
right = ConfigTree(config_string='\n')
if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
- raise TypeError("Arguments must be instances of ConfigTree")
+ raise TypeError('Arguments must be instances of ConfigTree')
if path:
if not left.exists(path):
raise ConfigTreeError(f"Path {path} doesn't exist in lhs tree")
@@ -508,7 +578,7 @@ class DiffTree:
self.__diff_tree.restype = c_void_p
check_path(path)
- path_str = " ".join(map(str, path)).encode()
+ path_str = ' '.join(map(str, path)).encode()
res = self.__diff_tree(path_str, left._get_config(), right._get_config())
@@ -524,11 +594,11 @@ class DiffTree:
def to_commands(self):
add = self.add.to_commands()
- delete = self.delete.to_commands(op="delete")
- return delete + "\n" + add
+ delete = self.delete.to_commands(op='delete')
+ return delete + '\n' + add
+
def deep_copy(config_tree: ConfigTree) -> ConfigTree:
- """An inelegant, but reasonably fast, copy; replace with backend copy
- """
+ """An inelegant, but reasonably fast, copy; replace with backend copy"""
D = DiffTree(None, config_tree)
return D.add
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/ifconfig/control.py b/python/vyos/ifconfig/control.py
index 7402da55a..a886c1b9e 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -48,7 +48,7 @@ class Control(Section):
def _popen(self, command):
return popen(command, self.debug)
- def _cmd(self, command):
+ def _cmd(self, command, env=None):
import re
if 'netns' in self.config:
# This command must be executed from default netns 'ip link set dev X netns X'
@@ -61,7 +61,7 @@ class Control(Section):
command = command
else:
command = f'ip netns exec {self.config["netns"]} {command}'
- return cmd(command, self.debug)
+ return cmd(command, self.debug, env=env)
def _get_command(self, config, name):
"""
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 519012625..341fd32ff 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-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
@@ -22,9 +22,11 @@ from tempfile import NamedTemporaryFile
from hurry.filesize import size
from hurry.filesize import alternative
+from vyos.configquery import ConfigTreeQuery
from vyos.ifconfig import Interface
from vyos.ifconfig import Operational
from vyos.template import is_ipv6
+from vyos.template import is_ipv4
class WireGuardOperational(Operational):
def _dump(self):
@@ -80,80 +82,76 @@ class WireGuardOperational(Operational):
}
return output
- def show_interface(self):
- from vyos.config import Config
+ def get_latest_handshakes(self):
+ """Get latest handshake time for each peer"""
+ output = {}
- c = Config()
+ # Dump wireguard last handshake
+ tmp = self._cmd(f'wg show {self.ifname} latest-handshakes')
+ # Output:
+ # PUBLIC-KEY= 1732812147
+ for line in tmp.split('\n'):
+ if not line:
+ # Skip empty lines and last line
+ continue
+ items = line.split('\t')
- wgdump = self._dump().get(self.config['ifname'], None)
+ if len(items) != 2:
+ continue
- c.set_level(['interfaces', 'wireguard', self.config['ifname']])
- description = c.return_effective_value(['description'])
- ips = c.return_effective_values(['address'])
+ output[items[0]] = int(items[1])
- answer = 'interface: {}\n'.format(self.config['ifname'])
- if description:
- answer += ' description: {}\n'.format(description)
- if ips:
- answer += ' address: {}\n'.format(', '.join(ips))
+ return output
+
+ def reset_peer(self, peer_name=None, public_key=None):
+ c = ConfigTreeQuery()
+ tmp = c.get_config_dict(['interfaces', 'wireguard', self.ifname],
+ effective=True, get_first_key=True,
+ key_mangling=('-', '_'), with_defaults=True)
+
+ current_peers = self._dump().get(self.ifname, {}).get('peers', {})
+
+ for peer, peer_config in tmp['peer'].items():
+ peer_public_key = peer_config['public_key']
+ if peer_name is None or peer == peer_name or public_key == peer_public_key:
+ if ('address' not in peer_config and 'host_name' not in peer_config) or 'port' not in peer_config:
+ if peer_name is not None:
+ print(f'WireGuard interface "{self.ifname}" peer "{peer_name}" address/host-name unset!')
+ continue
- answer += ' public key: {}\n'.format(wgdump['public_key'])
- answer += ' private key: (hidden)\n'
- answer += ' listening port: {}\n'.format(wgdump['listen_port'])
- answer += '\n'
+ # As we work with an effective config, a port CLI node is always
+ # available when an address/host-name is defined on the CLI
+ port = peer_config['port']
- for peer in c.list_effective_nodes(['peer']):
- if wgdump['peers']:
- pubkey = c.return_effective_value(['peer', peer, 'public-key'])
- if pubkey in wgdump['peers']:
- wgpeer = wgdump['peers'][pubkey]
+ # address has higher priority than host-name
+ if 'address' in peer_config:
+ address = peer_config['address']
+ new_endpoint = f'{address}:{port}'
+ else:
+ host_name = peer_config['host_name']
+ new_endpoint = f'{host_name}:{port}'
- answer += ' peer: {}\n'.format(peer)
- answer += ' public key: {}\n'.format(pubkey)
+ if 'disable' in peer_config:
+ print(f'WireGuard interface "{self.ifname}" peer "{peer_name}" disabled!')
+ continue
- """ figure out if the tunnel is recently active or not """
- status = 'inactive'
- if wgpeer['latest_handshake'] is None:
- """ no handshake ever """
- status = 'inactive'
+ cmd = f'wg set {self.ifname} peer {peer_public_key} endpoint {new_endpoint}'
+ try:
+ if (peer_public_key in current_peers
+ and 'endpoint' in current_peers[peer_public_key]
+ and current_peers[peer_public_key]['endpoint'] is not None
+ ):
+ current_endpoint = current_peers[peer_public_key]['endpoint']
+ message = f'Resetting {self.ifname} peer {peer_public_key} from {current_endpoint} endpoint to {new_endpoint} ... '
else:
- if int(wgpeer['latest_handshake']) > 0:
- delta = timedelta(
- seconds=int(time.time() - wgpeer['latest_handshake'])
- )
- answer += ' latest handshake: {}\n'.format(delta)
- if time.time() - int(wgpeer['latest_handshake']) < (60 * 5):
- """ Five minutes and the tunnel is still active """
- status = 'active'
- else:
- """ it's been longer than 5 minutes """
- status = 'inactive'
- elif int(wgpeer['latest_handshake']) == 0:
- """ no handshake ever """
- status = 'inactive'
- answer += ' status: {}\n'.format(status)
-
- if wgpeer['endpoint'] is not None:
- answer += ' endpoint: {}\n'.format(wgpeer['endpoint'])
-
- if wgpeer['allowed_ips'] is not None:
- answer += ' allowed ips: {}\n'.format(
- ','.join(wgpeer['allowed_ips']).replace(',', ', ')
- )
-
- if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0:
- rx_size = size(wgpeer['transfer_rx'], system=alternative)
- tx_size = size(wgpeer['transfer_tx'], system=alternative)
- answer += ' transfer: {} received, {} sent\n'.format(
- rx_size, tx_size
- )
-
- if wgpeer['persistent_keepalive'] is not None:
- answer += ' persistent keepalive: every {} seconds\n'.format(
- wgpeer['persistent_keepalive']
- )
- answer += '\n'
- return answer
+ message = f'Resetting {self.ifname} peer {peer_public_key} endpoint to {new_endpoint} ... '
+ print(message, end='')
+
+ self._cmd(cmd, env={'WG_ENDPOINT_RESOLUTION_RETRIES':
+ tmp['max_dns_retry']})
+ print('done')
+ except:
+ print(f'Error\nPlease try to run command manually:\n{cmd}\n')
@Interface.register
@@ -180,22 +178,26 @@ class WireGuardIf(Interface):
get_config_dict(). It's main intention is to consolidate the scattered
interface setup code and provide a single point of entry when workin
on any interface."""
-
tmp_file = NamedTemporaryFile('w')
tmp_file.write(config['private_key'])
tmp_file.flush()
# Wireguard base command is identical for every peer
- base_cmd = 'wg set {ifname}'
+ base_cmd = f'wg set {self.ifname}'
+ interface_cmd = base_cmd
if 'port' in config:
- base_cmd += ' listen-port {port}'
+ interface_cmd += ' listen-port {port}'
if 'fwmark' in config:
- base_cmd += ' fwmark {fwmark}'
+ interface_cmd += ' fwmark {fwmark}'
- base_cmd += f' private-key {tmp_file.name}'
- base_cmd = base_cmd.format(**config)
+ interface_cmd += f' private-key {tmp_file.name}'
+ interface_cmd = interface_cmd.format(**config)
# T6490: execute command to ensure interface configured
- self._cmd(base_cmd)
+ self._cmd(interface_cmd)
+
+ # If no PSK is given remove it by using /dev/null - passing keys via
+ # the shell (usually bash) is considered insecure, thus we use a file
+ no_psk_file = '/dev/null'
if 'peer' in config:
for peer, peer_config in config['peer'].items():
@@ -203,43 +205,60 @@ class WireGuardIf(Interface):
# marked as disabled - also active sessions are terminated as
# the public key was already removed when entering this method!
if 'disable' in peer_config:
+ # remove peer if disabled, no error report even if peer not exists
+ cmd = base_cmd + ' peer {public_key} remove'
+ self._cmd(cmd.format(**peer_config))
continue
- # start of with a fresh 'wg' command
- cmd = base_cmd + ' peer {public_key}'
-
- # If no PSK is given remove it by using /dev/null - passing keys via
- # the shell (usually bash) is considered insecure, thus we use a file
- no_psk_file = '/dev/null'
psk_file = no_psk_file
- if 'preshared_key' in peer_config:
- psk_file = '/tmp/tmp.wireguard.psk'
- with open(psk_file, 'w') as f:
- f.write(peer_config['preshared_key'])
- cmd += f' preshared-key {psk_file}'
-
- # Persistent keepalive is optional
- if 'persistent_keepalive' in peer_config:
- cmd += ' persistent-keepalive {persistent_keepalive}'
-
- # Multiple allowed-ip ranges can be defined - ensure we are always
- # dealing with a list
- if isinstance(peer_config['allowed_ips'], str):
- peer_config['allowed_ips'] = [peer_config['allowed_ips']]
- cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
-
- # Endpoint configuration is optional
- if {'address', 'port'} <= set(peer_config):
- if is_ipv6(peer_config['address']):
- cmd += ' endpoint [{address}]:{port}'
- else:
- cmd += ' endpoint {address}:{port}'
- self._cmd(cmd.format(**peer_config))
-
- # PSK key file is not required to be stored persistently as its backed by CLI
- if psk_file != no_psk_file and os.path.exists(psk_file):
- os.remove(psk_file)
+ # start of with a fresh 'wg' command
+ peer_cmd = base_cmd + ' peer {public_key}'
+
+ try:
+ cmd = peer_cmd
+
+ if 'preshared_key' in peer_config:
+ psk_file = '/tmp/tmp.wireguard.psk'
+ with open(psk_file, 'w') as f:
+ f.write(peer_config['preshared_key'])
+ cmd += f' preshared-key {psk_file}'
+
+ # Persistent keepalive is optional
+ if 'persistent_keepalive' in peer_config:
+ cmd += ' persistent-keepalive {persistent_keepalive}'
+
+ # Multiple allowed-ip ranges can be defined - ensure we are always
+ # dealing with a list
+ if isinstance(peer_config['allowed_ips'], str):
+ peer_config['allowed_ips'] = [peer_config['allowed_ips']]
+ cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
+
+ self._cmd(cmd.format(**peer_config))
+
+ cmd = peer_cmd
+
+ # Ensure peer is created even if dns not working
+ if {'address', 'port'} <= set(peer_config):
+ if is_ipv6(peer_config['address']):
+ cmd += ' endpoint [{address}]:{port}'
+ elif is_ipv4(peer_config['address']):
+ cmd += ' endpoint {address}:{port}'
+ else:
+ # don't set endpoint if address uses domain name
+ continue
+ elif {'host_name', 'port'} <= set(peer_config):
+ cmd += ' endpoint {host_name}:{port}'
+
+ self._cmd(cmd.format(**peer_config), env={
+ 'WG_ENDPOINT_RESOLUTION_RETRIES': config['max_dns_retry']})
+ except:
+ # todo: logging
+ pass
+ finally:
+ # PSK key file is not required to be stored persistently as its backed by CLI
+ if psk_file != no_psk_file and os.path.exists(psk_file):
+ os.remove(psk_file)
# call base class
super().update(config)
diff --git a/python/vyos/include/__init__.py b/python/vyos/include/__init__.py
new file mode 100644
index 000000000..22e836531
--- /dev/null
+++ b/python/vyos/include/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
+
diff --git a/python/vyos/include/uapi/__init__.py b/python/vyos/include/uapi/__init__.py
new file mode 100644
index 000000000..22e836531
--- /dev/null
+++ b/python/vyos/include/uapi/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
+
diff --git a/python/vyos/include/uapi/linux/__init__.py b/python/vyos/include/uapi/linux/__init__.py
new file mode 100644
index 000000000..22e836531
--- /dev/null
+++ b/python/vyos/include/uapi/linux/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
+
diff --git a/python/vyos/include/uapi/linux/fib_rules.py b/python/vyos/include/uapi/linux/fib_rules.py
new file mode 100644
index 000000000..72f0b18cb
--- /dev/null
+++ b/python/vyos/include/uapi/linux/fib_rules.py
@@ -0,0 +1,20 @@
+# Copyright (C) 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
+# 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/>.
+
+FIB_RULE_PERMANENT = 0x00000001
+FIB_RULE_INVERT = 0x00000002
+FIB_RULE_UNRESOLVED = 0x00000004
+FIB_RULE_IIF_DETACHED = 0x00000008
+FIB_RULE_DEV_DETACHED = FIB_RULE_IIF_DETACHED
+FIB_RULE_OIF_DETACHED = 0x00000010
diff --git a/python/vyos/include/uapi/linux/icmpv6.py b/python/vyos/include/uapi/linux/icmpv6.py
new file mode 100644
index 000000000..47e0c723c
--- /dev/null
+++ b/python/vyos/include/uapi/linux/icmpv6.py
@@ -0,0 +1,18 @@
+# Copyright (C) 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
+# 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/>.
+
+ICMPV6_ROUTER_PREF_LOW = 3
+ICMPV6_ROUTER_PREF_MEDIUM = 0
+ICMPV6_ROUTER_PREF_HIGH = 1
+ICMPV6_ROUTER_PREF_INVALID = 2
diff --git a/python/vyos/include/uapi/linux/if_arp.py b/python/vyos/include/uapi/linux/if_arp.py
new file mode 100644
index 000000000..90cb66ebd
--- /dev/null
+++ b/python/vyos/include/uapi/linux/if_arp.py
@@ -0,0 +1,176 @@
+# Copyright (C) 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
+# 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/>.
+
+# ARP protocol HARDWARE identifiers
+ARPHRD_NETROM = 0 # from KA9Q: NET/ROM pseudo
+ARPHRD_ETHER = 1 # Ethernet 10Mbps
+ARPHRD_EETHER = 2 # Experimental Ethernet
+ARPHRD_AX25 = 3 # AX.25 Level 2
+ARPHRD_PRONET = 4 # PROnet token ring
+ARPHRD_CHAOS = 5 # Chaosnet
+ARPHRD_IEEE802 = 6 # IEEE 802.2 Ethernet/TR/TB
+ARPHRD_ARCNET = 7 # ARCnet
+ARPHRD_APPLETLK = 8 # APPLEtalk
+ARPHRD_DLCI = 15 # Frame Relay DLCI
+ARPHRD_ATM = 19 # ATM
+ARPHRD_METRICOM = 23 # Metricom STRIP (new IANA id)
+ARPHRD_IEEE1394 = 24 # IEEE 1394 IPv4 - RFC 2734
+ARPHRD_EUI64 = 27 # EUI-64
+ARPHRD_INFINIBAND = 32 # InfiniBand
+
+# Dummy types for non-ARP hardware
+ARPHRD_SLIP = 256
+ARPHRD_CSLIP = 257
+ARPHRD_SLIP6 = 258
+ARPHRD_CSLIP6 = 259
+ARPHRD_RSRVD = 260 # Notional KISS type
+ARPHRD_ADAPT = 264
+ARPHRD_ROSE = 270
+ARPHRD_X25 = 271 # CCITT X.25
+ARPHRD_HWX25 = 272 # Boards with X.25 in firmware
+ARPHRD_CAN = 280 # Controller Area Network
+ARPHRD_MCTP = 290
+ARPHRD_PPP = 512
+ARPHRD_CISCO = 513 # Cisco HDLC
+ARPHRD_HDLC = ARPHRD_CISCO # Alias for CISCO
+ARPHRD_LAPB = 516 # LAPB
+ARPHRD_DDCMP = 517 # Digital's DDCMP protocol
+ARPHRD_RAWHDLC = 518 # Raw HDLC
+ARPHRD_RAWIP = 519 # Raw IP
+
+ARPHRD_TUNNEL = 768 # IPIP tunnel
+ARPHRD_TUNNEL6 = 769 # IP6IP6 tunnel
+ARPHRD_FRAD = 770 # Frame Relay Access Device
+ARPHRD_SKIP = 771 # SKIP vif
+ARPHRD_LOOPBACK = 772 # Loopback device
+ARPHRD_LOCALTLK = 773 # Localtalk device
+ARPHRD_FDDI = 774 # Fiber Distributed Data Interface
+ARPHRD_BIF = 775 # AP1000 BIF
+ARPHRD_SIT = 776 # sit0 device - IPv6-in-IPv4
+ARPHRD_IPDDP = 777 # IP over DDP tunneller
+ARPHRD_IPGRE = 778 # GRE over IP
+ARPHRD_PIMREG = 779 # PIMSM register interface
+ARPHRD_HIPPI = 780 # High Performance Parallel Interface
+ARPHRD_ASH = 781 # Nexus 64Mbps Ash
+ARPHRD_ECONET = 782 # Acorn Econet
+ARPHRD_IRDA = 783 # Linux-IrDA
+ARPHRD_FCPP = 784 # Point to point fibrechannel
+ARPHRD_FCAL = 785 # Fibrechannel arbitrated loop
+ARPHRD_FCPL = 786 # Fibrechannel public loop
+ARPHRD_FCFABRIC = 787 # Fibrechannel fabric
+
+ARPHRD_IEEE802_TR = 800 # Magic type ident for TR
+ARPHRD_IEEE80211 = 801 # IEEE 802.11
+ARPHRD_IEEE80211_PRISM = 802 # IEEE 802.11 + Prism2 header
+ARPHRD_IEEE80211_RADIOTAP = 803 # IEEE 802.11 + radiotap header
+ARPHRD_IEEE802154 = 804
+ARPHRD_IEEE802154_MONITOR = 805 # IEEE 802.15.4 network monitor
+
+ARPHRD_PHONET = 820 # PhoNet media type
+ARPHRD_PHONET_PIPE = 821 # PhoNet pipe header
+ARPHRD_CAIF = 822 # CAIF media type
+ARPHRD_IP6GRE = 823 # GRE over IPv6
+ARPHRD_NETLINK = 824 # Netlink header
+ARPHRD_6LOWPAN = 825 # IPv6 over LoWPAN
+ARPHRD_VSOCKMON = 826 # Vsock monitor header
+
+ARPHRD_VOID = 0xFFFF # Void type, nothing is known
+ARPHRD_NONE = 0xFFFE # Zero header length
+
+# ARP protocol opcodes
+ARPOP_REQUEST = 1 # ARP request
+ARPOP_REPLY = 2 # ARP reply
+ARPOP_RREQUEST = 3 # RARP request
+ARPOP_RREPLY = 4 # RARP reply
+ARPOP_InREQUEST = 8 # InARP request
+ARPOP_InREPLY = 9 # InARP reply
+ARPOP_NAK = 10 # (ATM)ARP NAK
+
+ARPHRD_TO_NAME = {
+ ARPHRD_NETROM: "netrom",
+ ARPHRD_ETHER: "ether",
+ ARPHRD_EETHER: "eether",
+ ARPHRD_AX25: "ax25",
+ ARPHRD_PRONET: "pronet",
+ ARPHRD_CHAOS: "chaos",
+ ARPHRD_IEEE802: "ieee802",
+ ARPHRD_ARCNET: "arcnet",
+ ARPHRD_APPLETLK: "atalk",
+ ARPHRD_DLCI: "dlci",
+ ARPHRD_ATM: "atm",
+ ARPHRD_METRICOM: "metricom",
+ ARPHRD_IEEE1394: "ieee1394",
+ ARPHRD_INFINIBAND: "infiniband",
+ ARPHRD_SLIP: "slip",
+ ARPHRD_CSLIP: "cslip",
+ ARPHRD_SLIP6: "slip6",
+ ARPHRD_CSLIP6: "cslip6",
+ ARPHRD_RSRVD: "rsrvd",
+ ARPHRD_ADAPT: "adapt",
+ ARPHRD_ROSE: "rose",
+ ARPHRD_X25: "x25",
+ ARPHRD_HWX25: "hwx25",
+ ARPHRD_CAN: "can",
+ ARPHRD_PPP: "ppp",
+ ARPHRD_HDLC: "hdlc",
+ ARPHRD_LAPB: "lapb",
+ ARPHRD_DDCMP: "ddcmp",
+ ARPHRD_RAWHDLC: "rawhdlc",
+ ARPHRD_TUNNEL: "ipip",
+ ARPHRD_TUNNEL6: "tunnel6",
+ ARPHRD_FRAD: "frad",
+ ARPHRD_SKIP: "skip",
+ ARPHRD_LOOPBACK: "loopback",
+ ARPHRD_LOCALTLK: "ltalk",
+ ARPHRD_FDDI: "fddi",
+ ARPHRD_BIF: "bif",
+ ARPHRD_SIT: "sit",
+ ARPHRD_IPDDP: "ip/ddp",
+ ARPHRD_IPGRE: "gre",
+ ARPHRD_PIMREG: "pimreg",
+ ARPHRD_HIPPI: "hippi",
+ ARPHRD_ASH: "ash",
+ ARPHRD_ECONET: "econet",
+ ARPHRD_IRDA: "irda",
+ ARPHRD_FCPP: "fcpp",
+ ARPHRD_FCAL: "fcal",
+ ARPHRD_FCPL: "fcpl",
+ ARPHRD_FCFABRIC: "fcfb0",
+ ARPHRD_FCFABRIC+1: "fcfb1",
+ ARPHRD_FCFABRIC+2: "fcfb2",
+ ARPHRD_FCFABRIC+3: "fcfb3",
+ ARPHRD_FCFABRIC+4: "fcfb4",
+ ARPHRD_FCFABRIC+5: "fcfb5",
+ ARPHRD_FCFABRIC+6: "fcfb6",
+ ARPHRD_FCFABRIC+7: "fcfb7",
+ ARPHRD_FCFABRIC+8: "fcfb8",
+ ARPHRD_FCFABRIC+9: "fcfb9",
+ ARPHRD_FCFABRIC+10: "fcfb10",
+ ARPHRD_FCFABRIC+11: "fcfb11",
+ ARPHRD_FCFABRIC+12: "fcfb12",
+ ARPHRD_IEEE802_TR: "tr",
+ ARPHRD_IEEE80211: "ieee802.11",
+ ARPHRD_IEEE80211_PRISM: "ieee802.11/prism",
+ ARPHRD_IEEE80211_RADIOTAP: "ieee802.11/radiotap",
+ ARPHRD_IEEE802154: "ieee802.15.4",
+ ARPHRD_IEEE802154_MONITOR: "ieee802.15.4/monitor",
+ ARPHRD_PHONET: "phonet",
+ ARPHRD_PHONET_PIPE: "phonet_pipe",
+ ARPHRD_CAIF: "caif",
+ ARPHRD_IP6GRE: "gre6",
+ ARPHRD_NETLINK: "netlink",
+ ARPHRD_6LOWPAN: "6lowpan",
+ ARPHRD_NONE: "none",
+ ARPHRD_VOID: "void",
+} \ No newline at end of file
diff --git a/python/vyos/include/uapi/linux/lwtunnel.py b/python/vyos/include/uapi/linux/lwtunnel.py
new file mode 100644
index 000000000..6797a762b
--- /dev/null
+++ b/python/vyos/include/uapi/linux/lwtunnel.py
@@ -0,0 +1,38 @@
+# Copyright (C) 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
+# 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/>.
+
+LWTUNNEL_ENCAP_NONE = 0
+LWTUNNEL_ENCAP_MPLS = 1
+LWTUNNEL_ENCAP_IP = 2
+LWTUNNEL_ENCAP_ILA = 3
+LWTUNNEL_ENCAP_IP6 = 4
+LWTUNNEL_ENCAP_SEG6 = 5
+LWTUNNEL_ENCAP_BPF = 6
+LWTUNNEL_ENCAP_SEG6_LOCAL = 7
+LWTUNNEL_ENCAP_RPL = 8
+LWTUNNEL_ENCAP_IOAM6 = 9
+LWTUNNEL_ENCAP_XFRM = 10
+
+ENCAP_TO_NAME = {
+ LWTUNNEL_ENCAP_MPLS: 'mpls',
+ LWTUNNEL_ENCAP_IP: 'ip',
+ LWTUNNEL_ENCAP_IP6: 'ip6',
+ LWTUNNEL_ENCAP_ILA: 'ila',
+ LWTUNNEL_ENCAP_BPF: 'bpf',
+ LWTUNNEL_ENCAP_SEG6: 'seg6',
+ LWTUNNEL_ENCAP_SEG6_LOCAL: 'seg6local',
+ LWTUNNEL_ENCAP_RPL: 'rpl',
+ LWTUNNEL_ENCAP_IOAM6: 'ioam6',
+ LWTUNNEL_ENCAP_XFRM: 'xfrm',
+}
diff --git a/python/vyos/include/uapi/linux/neighbour.py b/python/vyos/include/uapi/linux/neighbour.py
new file mode 100644
index 000000000..d5caf44b9
--- /dev/null
+++ b/python/vyos/include/uapi/linux/neighbour.py
@@ -0,0 +1,34 @@
+# Copyright (C) 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
+# 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/>.
+
+NTF_USE = (1 << 0)
+NTF_SELF = (1 << 1)
+NTF_MASTER = (1 << 2)
+NTF_PROXY = (1 << 3)
+NTF_EXT_LEARNED = (1 << 4)
+NTF_OFFLOADED = (1 << 5)
+NTF_STICKY = (1 << 6)
+NTF_ROUTER = (1 << 7)
+NTF_EXT_MANAGED = (1 << 0)
+NTF_EXT_LOCKED = (1 << 1)
+
+NTF_FlAGS = {
+ 'self': NTF_SELF,
+ 'router': NTF_ROUTER,
+ 'extern_learn': NTF_EXT_LEARNED,
+ 'offload': NTF_OFFLOADED,
+ 'master': NTF_MASTER,
+ 'sticky': NTF_STICKY,
+ 'locked': NTF_EXT_LOCKED,
+}
diff --git a/python/vyos/include/uapi/linux/rtnetlink.py b/python/vyos/include/uapi/linux/rtnetlink.py
new file mode 100644
index 000000000..e31272460
--- /dev/null
+++ b/python/vyos/include/uapi/linux/rtnetlink.py
@@ -0,0 +1,63 @@
+# Copyright (C) 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
+# 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/>.
+
+RTM_F_NOTIFY = 0x100
+RTM_F_CLONED = 0x200
+RTM_F_EQUALIZE = 0x400
+RTM_F_PREFIX = 0x800
+RTM_F_LOOKUP_TABLE = 0x1000
+RTM_F_FIB_MATCH = 0x2000
+RTM_F_OFFLOAD = 0x4000
+RTM_F_TRAP = 0x8000
+RTM_F_OFFLOAD_FAILED = 0x20000000
+
+RTNH_F_DEAD = 1
+RTNH_F_PERVASIVE = 2
+RTNH_F_ONLINK = 4
+RTNH_F_OFFLOAD = 8
+RTNH_F_LINKDOWN = 16
+RTNH_F_UNRESOLVED = 32
+RTNH_F_TRAP = 64
+
+RT_TABLE_COMPAT = 252
+RT_TABLE_DEFAULT = 253
+RT_TABLE_MAIN = 254
+RT_TABLE_LOCAL = 255
+
+RTAX_FEATURE_ECN = (1 << 0)
+RTAX_FEATURE_SACK = (1 << 1)
+RTAX_FEATURE_TIMESTAMP = (1 << 2)
+RTAX_FEATURE_ALLFRAG = (1 << 3)
+RTAX_FEATURE_TCP_USEC_TS = (1 << 4)
+
+RT_FlAGS = {
+ 'dead': RTNH_F_DEAD,
+ 'onlink': RTNH_F_ONLINK,
+ 'pervasive': RTNH_F_PERVASIVE,
+ 'offload': RTNH_F_OFFLOAD,
+ 'trap': RTNH_F_TRAP,
+ 'notify': RTM_F_NOTIFY,
+ 'linkdown': RTNH_F_LINKDOWN,
+ 'unresolved': RTNH_F_UNRESOLVED,
+ 'rt_offload': RTM_F_OFFLOAD,
+ 'rt_trap': RTM_F_TRAP,
+ 'rt_offload_failed': RTM_F_OFFLOAD_FAILED,
+}
+
+RT_TABLE_TO_NAME = {
+ RT_TABLE_COMPAT: 'compat',
+ RT_TABLE_DEFAULT: 'default',
+ RT_TABLE_MAIN: 'main',
+ RT_TABLE_LOCAL: 'local',
+}
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index addfdba49..951c83693 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-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
@@ -17,6 +17,9 @@ import json
import os
import socket
+from datetime import datetime
+from datetime import timezone
+
from vyos.template import is_ipv6
from vyos.template import isc_static_route
from vyos.template import netmask_from_cidr
@@ -40,7 +43,7 @@ kea4_options = {
'time_offset': 'time-offset',
'wpad_url': 'wpad-url',
'ipv6_only_preferred': 'v6-only-preferred',
- 'captive_portal': 'v4-captive-portal'
+ 'captive_portal': 'v4-captive-portal',
}
kea6_options = {
@@ -52,11 +55,35 @@ kea6_options = {
'nisplus_domain': 'nisp-domain-name',
'nisplus_server': 'nisp-servers',
'sntp_server': 'sntp-servers',
- 'captive_portal': 'v6-captive-portal'
+ 'captive_portal': 'v6-captive-portal',
}
kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket'
+
+def _format_hex_string(in_str):
+ out_str = ''
+ # if input is divisible by 2, add : every 2 chars
+ if len(in_str) > 0 and len(in_str) % 2 == 0:
+ out_str = ':'.join(a + b for a, b in zip(in_str[::2], in_str[1::2]))
+ else:
+ out_str = in_str
+
+ return out_str
+
+
+def _find_list_of_dict_index(lst, key='ip', value=''):
+ """
+ Find the index entry of list of dict matching the dict value
+ Exampe:
+ % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}]
+ % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2')
+ % 1
+ """
+ idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None)
+ return idx
+
+
def kea_parse_options(config):
options = []
@@ -64,14 +91,21 @@ def kea_parse_options(config):
if node not in config:
continue
- value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
+ value = (
+ ', '.join(config[node]) if isinstance(config[node], list) else config[node]
+ )
options.append({'name': option_name, 'data': value})
if 'client_prefix_length' in config:
- options.append({'name': 'subnet-mask', 'data': netmask_from_cidr('0.0.0.0/' + config['client_prefix_length'])})
+ options.append(
+ {
+ 'name': 'subnet-mask',
+ 'data': netmask_from_cidr('0.0.0.0/' + config['client_prefix_length']),
+ }
+ )
if 'ip_forwarding' in config:
- options.append({'name': 'ip-forwarding', 'data': "true"})
+ options.append({'name': 'ip-forwarding', 'data': 'true'})
if 'static_route' in config:
default_route = ''
@@ -79,31 +113,41 @@ def kea_parse_options(config):
if 'default_router' in config:
default_route = isc_static_route('0.0.0.0/0', config['default_router'])
- routes = [isc_static_route(route, route_options['next_hop']) for route, route_options in config['static_route'].items()]
-
- options.append({'name': 'rfc3442-static-route', 'data': ", ".join(routes if not default_route else routes + [default_route])})
- options.append({'name': 'windows-static-route', 'data': ", ".join(routes)})
+ routes = [
+ isc_static_route(route, route_options['next_hop'])
+ for route, route_options in config['static_route'].items()
+ ]
+
+ options.append(
+ {
+ 'name': 'rfc3442-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:
- tz_string = f.read().split(b"\n")[-2].decode("utf-8")
+ with open('/usr/share/zoneinfo/' + config['time_zone'], 'rb') as f:
+ tz_string = f.read().split(b'\n')[-2].decode('utf-8')
options.append({'name': 'pcode', 'data': tz_string})
options.append({'name': 'tcode', 'data': config['time_zone']})
- unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
+ unifi_controller = dict_search_args(
+ config, 'vendor_option', 'ubiquiti', 'unifi_controller'
+ )
if unifi_controller:
- options.append({
- 'name': 'unifi-controller',
- 'data': unifi_controller,
- 'space': 'ubnt'
- })
+ options.append(
+ {'name': 'unifi-controller', 'data': unifi_controller, 'space': 'ubnt'}
+ )
return options
+
def kea_parse_subnet(subnet, config):
out = {'subnet': subnet, 'id': int(config['subnet_id'])}
- options = []
if 'option' in config:
out['option-data'] = kea_parse_options(config['option'])
@@ -125,9 +169,7 @@ def kea_parse_subnet(subnet, config):
pools = []
for num, range_config in config['range'].items():
start, stop = range_config['start'], range_config['stop']
- pool = {
- 'pool': f'{start} - {stop}'
- }
+ pool = {'pool': f'{start} - {stop}'}
if 'option' in range_config:
pool['option-data'] = kea_parse_options(range_config['option'])
@@ -164,16 +206,21 @@ def kea_parse_subnet(subnet, config):
reservation['option-data'] = kea_parse_options(host_config['option'])
if 'bootfile_name' in host_config['option']:
- reservation['boot-file-name'] = host_config['option']['bootfile_name']
+ reservation['boot-file-name'] = host_config['option'][
+ 'bootfile_name'
+ ]
if 'bootfile_server' in host_config['option']:
- reservation['next-server'] = host_config['option']['bootfile_server']
+ reservation['next-server'] = host_config['option'][
+ 'bootfile_server'
+ ]
reservations.append(reservation)
out['reservations'] = reservations
return out
+
def kea6_parse_options(config):
options = []
@@ -181,7 +228,9 @@ def kea6_parse_options(config):
if node not in config:
continue
- value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
+ value = (
+ ', '.join(config[node]) if isinstance(config[node], list) else config[node]
+ )
options.append({'name': option_name, 'data': value})
if 'sip_server' in config:
@@ -197,17 +246,20 @@ def kea6_parse_options(config):
hosts.append(server)
if addrs:
- options.append({'name': 'sip-server-addr', 'data': ", ".join(addrs)})
+ options.append({'name': 'sip-server-addr', 'data': ', '.join(addrs)})
if hosts:
- options.append({'name': 'sip-server-dns', 'data': ", ".join(hosts)})
+ options.append({'name': 'sip-server-dns', 'data': ', '.join(hosts)})
cisco_tftp = dict_search_args(config, 'vendor_option', 'cisco', 'tftp-server')
if cisco_tftp:
- options.append({'name': 'tftp-servers', 'code': 2, 'space': 'cisco', 'data': cisco_tftp})
+ options.append(
+ {'name': 'tftp-servers', 'code': 2, 'space': 'cisco', 'data': cisco_tftp}
+ )
return options
+
def kea6_parse_subnet(subnet, config):
out = {'subnet': subnet, 'id': int(config['subnet_id'])}
@@ -245,12 +297,14 @@ def kea6_parse_subnet(subnet, config):
pd_pool = {
'prefix': prefix,
'prefix-len': int(pd_conf['prefix_length']),
- 'delegated-len': int(pd_conf['delegated_length'])
+ 'delegated-len': int(pd_conf['delegated_length']),
}
if 'excluded_prefix' in pd_conf:
pd_pool['excluded-prefix'] = pd_conf['excluded_prefix']
- pd_pool['excluded-prefix-len'] = int(pd_conf['excluded_prefix_length'])
+ pd_pool['excluded-prefix-len'] = int(
+ pd_conf['excluded_prefix_length']
+ )
pd_pools.append(pd_pool)
@@ -270,9 +324,7 @@ def kea6_parse_subnet(subnet, config):
if 'disable' in host_config:
continue
- reservation = {
- 'hostname': host
- }
+ reservation = {'hostname': host}
if 'mac' in host_config:
reservation['hw-address'] = host_config['mac']
@@ -281,10 +333,10 @@ def kea6_parse_subnet(subnet, config):
reservation['duid'] = host_config['duid']
if 'ipv6_address' in host_config:
- reservation['ip-addresses'] = [ host_config['ipv6_address'] ]
+ reservation['ip-addresses'] = [host_config['ipv6_address']]
if 'ipv6_prefix' in host_config:
- reservation['prefixes'] = [ host_config['ipv6_prefix'] ]
+ reservation['prefixes'] = [host_config['ipv6_prefix']]
if 'option' in host_config:
reservation['option-data'] = kea6_parse_options(host_config['option'])
@@ -295,6 +347,7 @@ def kea6_parse_subnet(subnet, config):
return out
+
def _ctrl_socket_command(inet, command, args=None):
path = kea_ctrl_socket.format(inet=inet)
@@ -321,6 +374,7 @@ def _ctrl_socket_command(inet, command, args=None):
return json.loads(result.decode('utf-8'))
+
def kea_get_leases(inet):
leases = _ctrl_socket_command(inet, f'lease{inet}-get-all')
@@ -329,6 +383,7 @@ def kea_get_leases(inet):
return leases['arguments']['leases']
+
def kea_delete_lease(inet, ip_address):
args = {'ip-address': ip_address}
@@ -339,6 +394,7 @@ def kea_delete_lease(inet, ip_address):
return False
+
def kea_get_active_config(inet):
config = _ctrl_socket_command(inet, 'config-get')
@@ -347,8 +403,18 @@ def kea_get_active_config(inet):
return config
+
+def kea_get_dhcp_pools(config, inet):
+ shared_networks = dict_search_args(
+ config, 'arguments', f'Dhcp{inet}', 'shared-networks'
+ )
+ return [network['name'] for network in shared_networks] if shared_networks else []
+
+
def kea_get_pool_from_subnet_id(config, inet, subnet_id):
- shared_networks = dict_search_args(config, 'arguments', f'Dhcp{inet}', 'shared-networks')
+ shared_networks = dict_search_args(
+ config, 'arguments', f'Dhcp{inet}', 'shared-networks'
+ )
if not shared_networks:
return None
@@ -362,3 +428,120 @@ def kea_get_pool_from_subnet_id(config, inet, subnet_id):
return network['name']
return None
+
+
+def kea_get_static_mappings(config, inet, pools=[]) -> list:
+ """
+ Get DHCP static mapping from active Kea DHCPv4 or DHCPv6 configuration
+ :return list
+ """
+ shared_networks = dict_search_args(
+ config, 'arguments', f'Dhcp{inet}', 'shared-networks'
+ )
+
+ mappings = []
+
+ if shared_networks:
+ for network in shared_networks:
+ if f'subnet{inet}' not in network:
+ continue
+
+ for p in pools:
+ if network['name'] == p:
+ for subnet in network[f'subnet{inet}']:
+ if 'reservations' in subnet:
+ for reservation in subnet['reservations']:
+ mapping = {'pool': p, 'subnet': subnet['subnet']}
+ mapping.update(reservation)
+ # rename 'ip(v6)-address' to 'ip', inet6 has 'ipv6-address' and inet has 'ip-address'
+ mapping['ip'] = mapping.pop(
+ 'ipv6-address', mapping.pop('ip-address', None)
+ )
+ # rename 'hw-address' to 'mac'
+ mapping['mac'] = mapping.pop('hw-address', None)
+ mappings.append(mapping)
+
+ return mappings
+
+
+def kea_get_server_leases(config, inet, pools=[], state=[], origin=None) -> list:
+ """
+ Get DHCP server leases from active Kea DHCPv4 or DHCPv6 configuration
+ :return list
+ """
+ leases = kea_get_leases(inet)
+
+ data = []
+ for lease in leases:
+ lifetime = lease['valid-lft']
+ expiry = lease['cltt'] + lifetime
+
+ lease['start_timestamp'] = datetime.fromtimestamp(
+ expiry - lifetime, timezone.utc
+ )
+ lease['expire_timestamp'] = (
+ datetime.fromtimestamp(expiry, timezone.utc) if expiry else None
+ )
+
+ data_lease = {}
+ data_lease['ip'] = lease['ip-address']
+ lease_state_long = {0: 'active', 1: 'rejected', 2: 'expired'}
+ data_lease['state'] = lease_state_long[lease['state']]
+ data_lease['pool'] = (
+ kea_get_pool_from_subnet_id(config, inet, lease['subnet-id'])
+ if config
+ else '-'
+ )
+ data_lease['end'] = (
+ lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None
+ )
+ data_lease['origin'] = 'local' # TODO: Determine remote in HA
+ # remove trailing dot in 'hostname' to ensure consistency for `vyos-hostsd-client`
+ data_lease['hostname'] = lease.get('hostname', '-').rstrip('.')
+
+ if inet == '4':
+ data_lease['mac'] = lease['hw-address']
+ data_lease['start'] = lease['start_timestamp'].timestamp()
+
+ if inet == '6':
+ data_lease['last_communication'] = lease['start_timestamp'].timestamp()
+ data_lease['duid'] = _format_hex_string(lease['duid'])
+ data_lease['type'] = lease['type']
+
+ if lease['type'] == 'IA_PD':
+ prefix_len = lease['prefix-len']
+ data_lease['ip'] += f'/{prefix_len}'
+
+ data_lease['remaining'] = '-'
+
+ if lease['valid-lft'] > 0:
+ data_lease['remaining'] = lease['expire_timestamp'] - datetime.now(
+ timezone.utc
+ )
+
+ if data_lease['remaining'].days >= 0:
+ # substraction gives us a timedelta object which can't be formatted with strftime
+ # so we use str(), split gets rid of the microseconds
+ data_lease['remaining'] = str(data_lease['remaining']).split('.')[0]
+
+ # Do not add old leases
+ if (
+ data_lease['remaining']
+ and data_lease['pool'] in pools
+ and data_lease['state'] != 'free'
+ and (not state or state == 'all' or data_lease['state'] in state)
+ ):
+ data.append(data_lease)
+
+ # deduplicate
+ checked = []
+ for entry in data:
+ addr = entry.get('ip')
+ if addr not in checked:
+ checked.append(addr)
+ else:
+ idx = _find_list_of_dict_index(data, key='ip', value=addr)
+ if idx is not None:
+ data.pop(idx)
+
+ return data
diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py
index 66df5d107..b477b5b5e 100644
--- a/python/vyos/qos/base.py
+++ b/python/vyos/qos/base.py
@@ -89,7 +89,8 @@ class QoSBase:
if value in self._dsfields:
return self._dsfields[value]
else:
- return value
+ # left shift operation aligns the DSCP/TOS value with its bit position in the IP header.
+ return int(value) << 2
def _calc_random_detect_queue_params(self, avg_pkt, max_thr, limit=None, min_thr=None,
mark_probability=None, precedence=0):
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/python/vyos/utils/kernel.py b/python/vyos/utils/kernel.py
index 847f80108..05eac8a6a 100644
--- a/python/vyos/utils/kernel.py
+++ b/python/vyos/utils/kernel.py
@@ -15,6 +15,10 @@
import os
+# A list of used Kernel constants
+# https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/net/wireguard/messages.h?h=linux-6.6.y#n45
+WIREGUARD_REKEY_AFTER_TIME = 120
+
def check_kmod(k_mod):
""" Common utility function to load required kernel modules on demand """
from vyos import ConfigError
diff --git a/scripts/transclude-template b/scripts/transclude-template
index 5c6668a84..767583acd 100755
--- a/scripts/transclude-template
+++ b/scripts/transclude-template
@@ -23,6 +23,7 @@
import os
import re
import sys
+import glob
regexp = re.compile(r'^ *#include <(.+)>$')
@@ -34,7 +35,9 @@ def parse_file(filename):
if line:
result = regexp.match(line)
if result:
- lines += parse_file(os.path.join(directory, result.group(1)))
+ res = os.path.join(directory, result.group(1))
+ for g in sorted(glob.glob(res)):
+ lines += parse_file(g)
else:
lines += line
else:
diff --git a/smoketest/config-tests/basic-vyos-no-ntp b/smoketest/config-tests/basic-vyos-no-ntp
new file mode 100644
index 000000000..a18260108
--- /dev/null
+++ b/smoketest/config-tests/basic-vyos-no-ntp
@@ -0,0 +1,53 @@
+set interfaces dummy dum0 address '172.18.254.203/32'
+set interfaces ethernet eth0 duplex 'auto'
+set interfaces ethernet eth0 offload gro
+set interfaces ethernet eth0 offload gso
+set interfaces ethernet eth0 offload sg
+set interfaces ethernet eth0 offload tso
+set interfaces ethernet eth0 speed 'auto'
+set interfaces ethernet eth0 vif 203 address '172.18.203.10/24'
+set interfaces ethernet eth1 duplex 'auto'
+set interfaces ethernet eth1 offload gro
+set interfaces ethernet eth1 offload gso
+set interfaces ethernet eth1 offload sg
+set interfaces ethernet eth1 offload tso
+set interfaces ethernet eth1 speed 'auto'
+set interfaces ethernet eth2 offload gro
+set interfaces ethernet eth2 offload gso
+set interfaces ethernet eth2 offload sg
+set interfaces ethernet eth2 offload tso
+set interfaces ethernet eth3 offload gro
+set interfaces ethernet eth3 offload gso
+set interfaces ethernet eth3 offload sg
+set interfaces ethernet eth3 offload tso
+set protocols ospf area 0 network '172.18.203.0/24'
+set protocols ospf area 0 network '172.18.254.203/32'
+set protocols ospf interface eth0.203 authentication md5 key-id 10 md5-key 'vyos'
+set protocols ospf interface eth0.203 dead-interval '40'
+set protocols ospf interface eth0.203 hello-interval '10'
+set protocols ospf interface eth0.203 passive disable
+set protocols ospf interface eth0.203 priority '1'
+set protocols ospf interface eth0.203 retransmit-interval '5'
+set protocols ospf interface eth0.203 transmit-delay '1'
+set protocols ospf log-adjacency-changes detail
+set protocols ospf parameters abr-type 'cisco'
+set protocols ospf parameters router-id '172.18.254.203'
+set protocols ospf passive-interface 'default'
+set protocols ospf redistribute connected metric-type '2'
+set system config-management commit-revisions '50'
+set system conntrack modules ftp
+set system conntrack modules h323
+set system conntrack modules nfs
+set system conntrack modules pptp
+set system conntrack modules sip
+set system conntrack modules sqlnet
+set system conntrack modules tftp
+set system console device ttyS0 speed '115200'
+set system domain-name 'vyos.ci.net'
+set system host-name 'no-ntp'
+set system login user vyos authentication encrypted-password '$6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0'
+set system login user vyos authentication plaintext-password ''
+set system name-server '172.16.254.30'
+set system syslog global facility all level 'debug'
+set system syslog global facility local7 level 'debug'
+set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/bgp-big-as-cloud b/smoketest/config-tests/bgp-big-as-cloud
index 03efef868..d6c17b3d2 100644
--- a/smoketest/config-tests/bgp-big-as-cloud
+++ b/smoketest/config-tests/bgp-big-as-cloud
@@ -836,7 +836,6 @@ set system flow-accounting interface 'eth0.4089'
set system flow-accounting netflow engine-id '1'
set system flow-accounting netflow server 192.0.2.55 port '2055'
set system flow-accounting netflow version '9'
-set system flow-accounting sflow server 1.2.3.4 port '1234'
set system flow-accounting syslog-facility 'daemon'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
@@ -845,6 +844,9 @@ set system name-server '2001:db8::1'
set system name-server '2001:db8::2'
set system name-server '192.0.2.1'
set system name-server '192.0.2.2'
+set system sflow interface 'eth0.4088'
+set system sflow interface 'eth0.4089'
+set system sflow server 1.2.3.4 port '1234'
set system syslog global facility all level 'all'
set system syslog global preserve-fqdn
set system time-zone 'Europe/Zurich'
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/configs/basic-vyos-no-ntp b/smoketest/configs/basic-vyos-no-ntp
new file mode 100644
index 000000000..6fb8f384f
--- /dev/null
+++ b/smoketest/configs/basic-vyos-no-ntp
@@ -0,0 +1,132 @@
+interfaces {
+ dummy dum0 {
+ address 172.18.254.203/32
+ }
+ ethernet eth0 {
+ duplex auto
+ offload {
+ gro
+ gso
+ sg
+ tso
+ }
+ speed auto
+ vif 203 {
+ address 172.18.203.10/24
+ ip {
+ ospf {
+ authentication {
+ md5 {
+ key-id 10 {
+ md5-key vyos
+ }
+ }
+ }
+ dead-interval 40
+ hello-interval 10
+ priority 1
+ retransmit-interval 5
+ transmit-delay 1
+ }
+ }
+ }
+ }
+ ethernet eth1 {
+ duplex auto
+ offload {
+ gro
+ gso
+ sg
+ tso
+ }
+ speed auto
+ }
+ ethernet eth2 {
+ offload {
+ gro
+ gso
+ sg
+ tso
+ }
+ }
+ ethernet eth3 {
+ offload {
+ gro
+ gso
+ sg
+ tso
+ }
+ }
+}
+protocols {
+ ospf {
+ area 0 {
+ network 172.18.203.0/24
+ network 172.18.254.203/32
+ }
+ log-adjacency-changes {
+ detail
+ }
+ parameters {
+ abr-type cisco
+ router-id 172.18.254.203
+ }
+ passive-interface default
+ passive-interface-exclude eth0.203
+ redistribute {
+ connected {
+ metric-type 2
+ }
+ }
+ }
+}
+system {
+ config-management {
+ commit-revisions 50
+ }
+ conntrack {
+ modules {
+ ftp
+ h323
+ nfs
+ pptp
+ sip
+ sqlnet
+ tftp
+ }
+ }
+ domain-name vyos.ci.net
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ host-name no-ntp
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0
+ plaintext-password ""
+ }
+ }
+ }
+ name-server 172.16.254.30
+ ntp {
+ }
+ syslog {
+ global {
+ facility all {
+ level debug
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+ time-zone Europe/Berlin
+}
+
+
+// Warning: Do not remove the following line.
+// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:container@1:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@23:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
+// Release version: 1.3.8
diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py
index 4b994a659..f8cd18cf2 100755
--- a/smoketest/scripts/cli/test_interfaces_wireguard.py
+++ b/smoketest/scripts/cli/test_interfaces_wireguard.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
@@ -17,27 +17,33 @@
import os
import unittest
-from base_vyostest_shim import VyOSUnitTestSHIM
+from base_interfaces_test import BasicInterfaceTest
from vyos.configsession import ConfigSessionError
from vyos.utils.file import read_file
from vyos.utils.process import cmd
+from vyos.utils.process import is_systemd_service_running
base_path = ['interfaces', 'wireguard']
-
-class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
+domain_resolver = 'vyos-domain-resolver.service'
+class WireGuardInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
- super(WireGuardInterfaceTest, cls).setUpClass()
-
- cls._test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32',
- '2001:db8:1::ffff/64', '2001:db8:101::1/112']
- cls._interfaces = ['wg0', 'wg1']
+ cls._base_path = ['interfaces', 'wireguard']
+ cls._options = {
+ 'wg0': ['private-key wBbGJJXYllwDcw63AFjiIR6ZlsvqvAf3eDwog64Dp0Q=',
+ 'peer RED public-key 6hkkfxN4VUQLu36NLZr47I7ST/FkQl2clPWr+9a6ZH8=',
+ 'peer RED allowed-ips 169.254.0.0/16',
+ 'port 5678'],
+ 'wg1': ['private-key QFwnBHlHYspehvpklBKb7cikM+QMkEy2p6gfsg06S08=',
+ 'peer BLUE public-key hRJLmP8SVU9/MLmPmYmpOa+RTB4F/zhDqA+/QDuW1Hg=',
+ 'peer BLUE allowed-ips 169.254.0.0/16',
+ 'port 4567'],
+ }
+ cls._interfaces = list(cls._options)
- def tearDown(self):
- self.cli_delete(base_path)
- self.cli_commit()
+ super(WireGuardInterfaceTest, cls).setUpClass()
- def test_01_wireguard_peer(self):
+ def test_wireguard_peer(self):
# Create WireGuard interfaces with associated peers
for intf in self._interfaces:
peer = 'foo-' + intf
@@ -64,7 +70,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}'))
- def test_02_wireguard_add_remove_peer(self):
+ def test_wireguard_add_remove_peer(self):
# T2939: Create WireGuard interfaces with associated peers.
# Remove one of the configured peers.
# T4774: Test prevention of duplicate peer public keys
@@ -102,7 +108,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path + [interface, 'peer', 'PEER01'])
self.cli_commit()
- def test_03_wireguard_same_public_key(self):
+ def test_wireguard_same_public_key(self):
# T5413: Test prevention of equality interface public key and peer's
# public key
interface = 'wg0'
@@ -115,45 +121,40 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + [interface, 'private-key', privkey])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_fail])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
# The same pubkey as the interface wg0
with self.assertRaises(ConfigSessionError):
self.cli_commit()
-
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_ok])
+ # If address is defined for a peer, so must be the peer port
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
+
# Commit peers
self.cli_commit()
self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}'))
- def test_04_wireguard_threaded(self):
+ def test_wireguard_threaded(self):
# T5409: Test adding threaded option on interface.
- # Test prevention for adding threaded
- # if no enabled peer is configured.
- interface = 'wg0'
- port = '12345'
- privkey = 'OOjcXGfgQlAuM6q8Z9aAYduCua7pxf7UKYvIqoUPoGQ='
- pubkey = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I='
+ for intf in self._interfaces:
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
- self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
- self.cli_set(base_path + [interface, 'private-key', privkey])
-
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
- self.cli_set(base_path + [interface, 'per-client-thread'])
+ self.cli_set(base_path + [intf, 'per-client-thread'])
# Commit peers
self.cli_commit()
- tmp = read_file(f'/sys/class/net/{interface}/threaded')
- self.assertTrue(tmp, "1")
- def test_05_wireguard_peer_pubkey_change(self):
+ for intf in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{intf}/threaded')
+ self.assertTrue(tmp, "1")
+
+ def test_wireguard_peer_pubkey_change(self):
# T5707 changing WireGuard CLI public key of a peer - it's not removed
def get_peers(interface) -> list:
@@ -171,7 +172,6 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
peers.append(items[0])
return peers
-
interface = 'wg1337'
port = '1337'
privkey = 'iJi4lb2HhkLx2KSAGOjji2alKkYsJjSPkHkrcpxgEVU='
@@ -200,5 +200,41 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(pubkey_1, peers)
self.assertIn(pubkey_2, peers)
+ def test_wireguard_hostname(self):
+ # T4930: Test dynamic endpoint support
+ interface = 'wg1234'
+ port = '54321'
+ privkey = 'UOWIeZKNzijhgu0bPRy2PB3gnuOBLfQax5GiYfkmU3A='
+ pubkey = '4nG5NfhHBQUq/DnwT0RjRoBCqAh3VrRHqdQgzC/xujk='
+
+ base_interface_path = base_path + [interface]
+ self.cli_set(base_interface_path + ['address', '172.16.0.1/24'])
+ self.cli_set(base_interface_path + ['private-key', privkey])
+
+ peer_base_path = base_interface_path + ['peer', 'dynamic01']
+ self.cli_set(peer_base_path + ['port', port])
+ self.cli_set(peer_base_path + ['public-key', pubkey])
+ self.cli_set(peer_base_path + ['allowed-ips', '169.254.0.0/16'])
+ self.cli_set(peer_base_path + ['address', '192.0.2.1'])
+ self.cli_set(peer_base_path + ['host-name', 'wg.vyos.net'])
+
+ # Peer address and host-name are mutually exclusive
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(peer_base_path + ['address'])
+
+ # Commit peers
+ self.cli_commit()
+
+ # Ensure the service is running which checks for DNS changes
+ self.assertTrue(is_systemd_service_running(domain_resolver))
+
+ self.cli_delete(base_interface_path)
+ self.cli_commit()
+
+ # Ensure the service is no longer running after WireGuard interface is deleted
+ self.assertFalse(is_systemd_service_running(domain_resolver))
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py
index 0beafcc6c..b33ef2617 100755
--- a/smoketest/scripts/cli/test_nat.py
+++ b/smoketest/scripts/cli/test_nat.py
@@ -84,7 +84,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase):
address_group = 'smoketest_addr'
address_group_member = '192.0.2.1'
interface_group = 'smoketest_ifaces'
- interface_group_member = 'bond.99'
+ interface_group_member = 'eth0'
self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member])
self.cli_set(['firewall', 'group', 'interface-group', interface_group, 'interface', interface_group_member])
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_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py
index 9fbc931de..522102e67 100755
--- a/smoketest/scripts/cli/test_service_dns_dynamic.py
+++ b/smoketest/scripts/cli/test_service_dns_dynamic.py
@@ -20,8 +20,10 @@ import tempfile
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
from vyos.utils.process import cmd
from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
DDCLIENT_SYSTEMD_UNIT = '/run/systemd/system/ddclient.service.d/override.conf'
DDCLIENT_CONF = '/run/ddclient/ddclient.conf'
@@ -29,6 +31,7 @@ DDCLIENT_PNAME = 'ddclient'
base_path = ['service', 'dns', 'dynamic']
name_path = base_path + ['name']
+default_interval = default_value(base_path + ['interval'])
server = 'ddns.vyos.io'
hostname = 'test.ddns.vyos.io'
zone = 'vyos.io'
@@ -95,12 +98,14 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
# Check the generating config parameters
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
- # default value 300 seconds
- self.assertIn(f'daemon=300', ddclient_conf)
self.assertIn(f'usev4=ifv4', ddclient_conf)
self.assertIn(f'ifv4={interface}', ddclient_conf)
self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ # Check default interval of 300 seconds
+ systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
+ self.assertIn(f'--daemon {default_interval}', systemd_override)
+
for opt in details.keys():
if opt == 'username':
login = details[opt]
@@ -140,7 +145,6 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
# Check the generating config parameters
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
- self.assertIn(f'daemon={interval}', ddclient_conf)
self.assertIn(f'usev6=ifv6', ddclient_conf)
self.assertIn(f'ifv6={interface}', ddclient_conf)
self.assertIn(f'protocol={proto}', ddclient_conf)
@@ -150,6 +154,10 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'min-interval={wait_time}', ddclient_conf)
self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf)
+ # default value 300 seconds
+ systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
+ self.assertIn(f'--daemon {interval}', systemd_override)
+
# IPv4+IPv6 dual DDNS service configuration
def test_03_dyndns_service_dual_stack(self):
services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone},
@@ -339,9 +347,10 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Check for process in VRF
- systemd_override = cmd(f'cat {DDCLIENT_SYSTEMD_UNIT}')
+ systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient ' \
- f'--file {DDCLIENT_CONF} --foreground', systemd_override)
+ f'--file {DDCLIENT_CONF} --cache {DDCLIENT_CONF.replace("conf", "cache")} ' \
+ f'--foreground --daemon {default_interval}', systemd_override)
# Check for process in VRF
proc = cmd(f'ip vrf pids {vrf_name}')
diff --git a/smoketest/scripts/cli/test_service_monitoring_network_event.py b/smoketest/scripts/cli/test_service_monitoring_network_event.py
new file mode 100644
index 000000000..3c9b4bf7f
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_network_event.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 unittest
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.file import read_json
+
+
+base_path = ['service', 'monitoring', 'network-event']
+
+
+def get_logger_config():
+ return read_json('/run/vyos-network-event-logger.conf')
+
+
+class TestMonitoringNetworkEvent(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestMonitoringNetworkEvent, 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):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_network_event_log(self):
+ expected_config = {
+ 'event': {
+ 'route': {},
+ 'link': {},
+ 'addr': {},
+ 'neigh': {},
+ 'rule': {},
+ },
+ 'queue_size': '10000'
+ }
+
+ self.cli_set(base_path + ['event', 'route'])
+ self.cli_set(base_path + ['event', 'link'])
+ self.cli_set(base_path + ['event', 'addr'])
+ self.cli_set(base_path + ['event', 'neigh'])
+ self.cli_set(base_path + ['event', 'rule'])
+ self.cli_set(base_path + ['queue-size', '10000'])
+ self.cli_commit()
+ self.assertEqual(expected_config, get_logger_config())
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py
index 515134220..9d7942789 100755
--- a/smoketest/scripts/cli/test_system_flow-accounting.py
+++ b/smoketest/scripts/cli/test_system_flow-accounting.py
@@ -97,111 +97,6 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'syslog: {syslog}', uacctd)
self.assertIn(f'plugins: memory', uacctd)
- def test_sflow(self):
- sampling_rate = '4000'
- source_address = '192.0.2.1'
- dummy_if = 'dum3841'
- agent_address = '192.0.2.2'
-
- sflow_server = {
- '1.2.3.4' : { },
- '5.6.7.8' : { 'port' : '6000' },
- }
-
- self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32'])
- self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32'])
- self.cli_set(base_path + ['disable-imt'])
-
- # You need to configure at least one interface for flow-accounting
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- for interface in Section.interfaces('ethernet'):
- self.cli_set(base_path + ['interface', interface])
-
-
- # You need to configure at least one sFlow or NetFlow protocol, or not
- # set "disable-imt" for flow-accounting
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
-
- self.cli_set(base_path + ['sflow', 'agent-address', agent_address])
- self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate])
- self.cli_set(base_path + ['sflow', 'source-address', source_address])
- for server, server_config in sflow_server.items():
- self.cli_set(base_path + ['sflow', 'server', server])
- if 'port' in server_config:
- self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']])
-
- # commit changes
- self.cli_commit()
-
- uacctd = read_file(uacctd_conf)
-
- # when 'disable-imt' is not configured on the CLI it must be present
- self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd)
- self.assertNotIn(f'imt_mem_pools_number: 169', uacctd)
- self.assertNotIn(f'plugins: memory', uacctd)
-
- for server, server_config in sflow_server.items():
- plugin_name = server.replace('.', '-')
- if 'port' in server_config:
- self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}', uacctd)
- else:
- self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}:6343', uacctd)
-
- self.assertIn(f'sfprobe_agentip[sf_{plugin_name}]: {agent_address}', uacctd)
- self.assertIn(f'sampling_rate[sf_{plugin_name}]: {sampling_rate}', uacctd)
- self.assertIn(f'sfprobe_source_ip[sf_{plugin_name}]: {source_address}', uacctd)
-
- self.cli_delete(['interfaces', 'dummy', dummy_if])
-
- def test_sflow_ipv6(self):
- sampling_rate = '100'
- sflow_server = {
- '2001:db8::1' : { },
- '2001:db8::2' : { 'port' : '6000' },
- }
-
- self.cli_set(base_path + ['disable-imt'])
-
- # You need to configure at least one interface for flow-accounting
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- for interface in Section.interfaces('ethernet'):
- self.cli_set(base_path + ['interface', interface])
-
-
- # You need to configure at least one sFlow or NetFlow protocol, or not
- # set "disable-imt" for flow-accounting
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
-
- self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate])
- for server, server_config in sflow_server.items():
- self.cli_set(base_path + ['sflow', 'server', server])
- if 'port' in server_config:
- self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']])
-
- # commit changes
- self.cli_commit()
-
- uacctd = read_file(uacctd_conf)
-
- # when 'disable-imt' is not configured on the CLI it must be present
- self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd)
- self.assertNotIn(f'imt_mem_pools_number: 169', uacctd)
- self.assertNotIn(f'plugins: memory', uacctd)
-
- for server, server_config in sflow_server.items():
- tmp_srv = server
- tmp_srv = tmp_srv.replace(':', '-')
-
- if 'port' in server_config:
- self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd)
- else:
- self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}:6343', uacctd)
- self.assertIn(f'sampling_rate[sf_{tmp_srv}]: {sampling_rate}', uacctd)
-
def test_netflow(self):
engine_id = '33'
max_flows = '667'
@@ -288,8 +183,8 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'nfprobe_timeouts[nf_{tmp_srv}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd)
-
self.cli_delete(['interfaces', 'dummy', dummy_if])
+
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_system_sflow.py b/smoketest/scripts/cli/test_system_sflow.py
index 74c065473..700253e2b 100755
--- a/smoketest/scripts/cli/test_system_sflow.py
+++ b/smoketest/scripts/cli/test_system_sflow.py
@@ -96,6 +96,39 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
for interface in Section.interfaces('ethernet'):
self.assertIn(f'pcap {{ dev={interface} }}', hsflowd)
+ def test_sflow_ipv6(self):
+ sampling_rate = '100'
+ default_polling = '30'
+ default_port = '6343'
+ sflow_server = {
+ '2001:db8::1': {},
+ '2001:db8::2': {'port': '8023'},
+ }
+
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+ self.cli_set(base_path + ['sampling-rate', sampling_rate])
+ for server, server_config in sflow_server.items():
+ self.cli_set(base_path + ['server', server])
+ if 'port' in server_config:
+ self.cli_set(base_path + ['server', server, 'port', server_config['port']])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ hsflowd = read_file(hsflowd_conf)
+
+ self.assertIn(f'sampling={sampling_rate}', hsflowd)
+ self.assertIn(f'polling={default_polling}', hsflowd)
+
+ for server, server_config in sflow_server.items():
+ if 'port' in server_config:
+ self.assertIn(f'collector {{ ip = {server} udpport = {server_config["port"]} }}', hsflowd)
+ else:
+ self.assertIn(f'collector {{ ip = {server} udpport = {default_port} }}', hsflowd)
+
def test_vrf(self):
interface = 'eth0'
server = '192.0.2.1'
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/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py
index 700e4cec7..b51b0be1d 100755
--- a/smoketest/scripts/system/test_kernel_options.py
+++ b/smoketest/scripts/system/test_kernel_options.py
@@ -128,5 +128,11 @@ class TestKernelModules(unittest.TestCase):
tmp = re.findall(f'{option}=(y|m)', self._config_data)
self.assertTrue(tmp)
+ def test_psample_enabled(self):
+ # Psample must be enabled in the OS Kernel to enable egress flow for hsflowd
+ for option in ['CONFIG_PSAMPLE']:
+ tmp = re.findall(f'{option}=y', self._config_data)
+ self.assertTrue(tmp)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
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/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index b6fd6b0b2..877d013cf 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -29,11 +29,12 @@ from vyos.ifconfig import WireGuardIf
from vyos.utils.kernel import check_kmod
from vyos.utils.network import check_port_availability
from vyos.utils.network import is_wireguard_key_pair
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
+from pathlib import Path
airbag.enable()
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -54,6 +55,12 @@ def get_config(config=None):
if is_node_changed(conf, base + [ifname, 'peer']):
wireguard.update({'rebuild_required': {}})
+ wireguard['peers_need_resolve'] = []
+ if 'peer' in wireguard:
+ for peer, peer_config in wireguard['peer'].items():
+ if 'disable' not in peer_config and 'host_name' in peer_config:
+ wireguard['peers_need_resolve'].append(peer)
+
return wireguard
def verify(wireguard):
@@ -82,22 +89,33 @@ def verify(wireguard):
for tmp in wireguard['peer']:
peer = wireguard['peer'][tmp]
+ base_error = f'WireGuard peer "{tmp}":'
+
+ if 'host_name' in peer and 'address' in peer:
+ raise ConfigError(f'{base_error} address/host-name are mutually exclusive!')
+
if 'allowed_ips' not in peer:
- raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+ raise ConfigError(f'{base_error} missing mandatory allowed-ips!')
if 'public_key' not in peer:
- raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
-
- if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
- raise ConfigError('Both Wireguard port and address must be defined '
- f'for peer "{tmp}" if either one of them is set!')
+ raise ConfigError(f'{base_error} missing mandatory public-key!')
if peer['public_key'] in public_keys:
- raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
+ raise ConfigError(f'{base_error} duplicate public-key!')
if 'disable' not in peer:
if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']):
- raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"')
+ tmp = wireguard["ifname"]
+ raise ConfigError(f'{base_error} identical public key as interface "{tmp}"!')
+
+ port_addr_error = f'{base_error} both port and address/host-name must '\
+ 'be defined if either one of them is set!'
+ if 'port' not in peer:
+ if 'host_name' in peer or 'address' in peer:
+ raise ConfigError(port_addr_error)
+ else:
+ if 'host_name' not in peer and 'address' not in peer:
+ raise ConfigError(port_addr_error)
public_keys.append(peer['public_key'])
@@ -122,6 +140,23 @@ def apply(wireguard):
wg = WireGuardIf(**wireguard)
wg.update(wireguard)
+ domain_resolver_usage = '/run/use-vyos-domain-resolver-interfaces-wireguard-' + wireguard['ifname']
+
+ ## DOMAIN RESOLVER
+ domain_action = 'restart'
+ if 'peers_need_resolve' in wireguard and len(wireguard['peers_need_resolve']) > 0 and 'disable' not in wireguard:
+ from vyos.utils.file import write_file
+
+ text = f'# Automatically generated by interfaces_wireguard.py\nThis file indicates that vyos-domain-resolver service is used by the interfaces_wireguard.\n'
+ text += "intefaces:\n" + "".join([f" - {peer}\n" for peer in wireguard['peers_need_resolve']])
+ Path(domain_resolver_usage).write_text(text)
+ write_file(domain_resolver_usage, text)
+ else:
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
+ domain_action = 'stop'
+ call(f'systemctl {domain_action} vyos-domain-resolver.service')
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 98b2f3f29..504b3e82a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -17,6 +17,7 @@
import os
from sys import exit
+from pathlib import Path
from vyos.base import Warning
from vyos.config import Config
@@ -43,7 +44,6 @@ k_mod = ['nft_nat', 'nft_chain_nat']
nftables_nat_config = '/run/nftables_nat.conf'
nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
domain_resolver_usage = '/run/use-vyos-domain-resolver-nat'
-domain_resolver_usage_firewall = '/run/use-vyos-domain-resolver-firewall'
valid_groups = [
'address_group',
@@ -265,9 +265,9 @@ def apply(nat):
text = f'# Automatically generated by nat.py\nThis file indicates that vyos-domain-resolver service is used by nat.\n'
write_file(domain_resolver_usage, text)
elif os.path.exists(domain_resolver_usage):
- os.unlink(domain_resolver_usage)
- if not os.path.exists(domain_resolver_usage_firewall):
- # Firewall not using domain resolver
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
domain_action = 'stop'
call(f'systemctl {domain_action} vyos-domain-resolver.service')
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/service_monitoring_network_event.py b/src/conf_mode/service_monitoring_network_event.py
new file mode 100644
index 000000000..104e6ce23
--- /dev/null
+++ b/src/conf_mode/service_monitoring_network_event.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 json
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.utils.file import write_file
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+vyos_network_event_logger_config = r'/run/vyos-network-event-logger.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'monitoring', 'network-event']
+ if not conf.exists(base):
+ return None
+
+ monitoring = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ monitoring = conf.merge_defaults(monitoring, recursive=True)
+
+ return monitoring
+
+
+def verify(monitoring):
+ if not monitoring:
+ return None
+
+ return None
+
+
+def generate(monitoring):
+ if not monitoring:
+ # Delete config
+ if os.path.exists(vyos_network_event_logger_config):
+ os.unlink(vyos_network_event_logger_config)
+
+ return None
+
+ # Create config
+ log_conf_json = json.dumps(monitoring, indent=4)
+ write_file(vyos_network_event_logger_config, log_conf_json)
+
+ return None
+
+
+def apply(monitoring):
+ # Reload systemd manager configuration
+ systemd_service = 'vyos-network-event-logger.service'
+
+ if not monitoring:
+ call(f'systemctl stop {systemd_service}')
+ return
+
+ call(f'systemctl restart {systemd_service}')
+
+
+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_flow-accounting.py b/src/conf_mode/system_flow-accounting.py
index a12ee363d..925c4a562 100755
--- a/src/conf_mode/system_flow-accounting.py
+++ b/src/conf_mode/system_flow-accounting.py
@@ -18,7 +18,6 @@ import os
import re
from sys import exit
-from ipaddress import ip_address
from vyos.config import Config
from vyos.config import config_dict_merge
@@ -159,9 +158,9 @@ def get_config(config=None):
# delete individual flow type defaults - should only be added if user
# sets this feature
- for flow_type in ['sflow', 'netflow']:
- if flow_type not in flow_accounting and flow_type in default_values:
- del default_values[flow_type]
+ flow_type = 'netflow'
+ if flow_type not in flow_accounting and flow_type in default_values:
+ del default_values[flow_type]
flow_accounting = config_dict_merge(default_values, flow_accounting)
@@ -171,9 +170,9 @@ def verify(flow_config):
if not flow_config:
return None
- # check if at least one collector is enabled
- if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config:
- raise ConfigError('You need to configure at least sFlow or NetFlow, ' \
+ # check if collector is enabled
+ if 'netflow' not in flow_config and 'disable_imt' in flow_config:
+ raise ConfigError('You need to configure NetFlow, ' \
'or not set "disable-imt" for flow-accounting!')
# Check if at least one interface is configured
@@ -185,45 +184,7 @@ def verify(flow_config):
for interface in flow_config['interface']:
verify_interface_exists(flow_config, interface, warning_only=True)
- # check sFlow configuration
- if 'sflow' in flow_config:
- # check if at least one sFlow collector is configured
- if 'server' not in flow_config['sflow']:
- raise ConfigError('You need to configure at least one sFlow server!')
-
- # check that all sFlow collectors use the same IP protocol version
- sflow_collector_ipver = None
- for server in flow_config['sflow']['server']:
- if sflow_collector_ipver:
- if sflow_collector_ipver != ip_address(server).version:
- raise ConfigError("All sFlow servers must use the same IP protocol")
- else:
- sflow_collector_ipver = ip_address(server).version
-
- # check if vrf is defined for Sflow
- verify_vrf(flow_config)
- sflow_vrf = None
- if 'vrf' in flow_config:
- sflow_vrf = flow_config['vrf']
-
- # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
- for server in flow_config['sflow']['server']:
- if 'agent_address' in flow_config['sflow']:
- if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version:
- raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\
- 'server". You need to set the same IP version for both "agent-address" and '\
- 'all sFlow servers')
-
- if 'agent_address' in flow_config['sflow']:
- tmp = flow_config['sflow']['agent_address']
- if not is_addr_assigned(tmp, sflow_vrf):
- raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
-
- # Check if configured sflow source-address exist in the system
- if 'source_address' in flow_config['sflow']:
- if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf):
- tmp = flow_config['sflow']['source_address']
- raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
+ verify_vrf(flow_config)
# check NetFlow configuration
if 'netflow' in flow_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/flow-accounting/1-to-2 b/src/migration-scripts/flow-accounting/1-to-2
new file mode 100644
index 000000000..5ffb1eec8
--- /dev/null
+++ b/src/migration-scripts/flow-accounting/1-to-2
@@ -0,0 +1,63 @@
+# Copyright 2021-2024 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/>.
+
+# migrate 'system flow-accounting sflow' to 'system sflow'
+
+from vyos.configtree import ConfigTree
+
+base = ['system', 'flow-accounting']
+base_fa_sflow = base + ['sflow']
+base_sflow = ['system', 'sflow']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base_fa_sflow):
+ # Nothing to do
+ return
+
+ if not config.exists(base_sflow):
+
+ for iface in config.return_values(base + ['interface']):
+ config.set(base_sflow + ['interface'], value=iface, replace=False)
+
+ if config.exists(base + ['vrf']):
+ vrf = config.return_value(base + ['vrf'])
+ config.set(base_sflow + ['vrf'], value=vrf)
+
+ if config.exists(base + ['enable-egress']):
+ config.set(base_sflow + ['enable-egress'])
+
+ if config.exists(base_fa_sflow + ['agent-address']):
+ address = config.return_value(base_fa_sflow + ['agent-address'])
+ config.set(base_sflow + ['agent-address'], value=address)
+
+ if config.exists(base_fa_sflow + ['sampling-rate']):
+ sr = config.return_value(base_fa_sflow + ['sampling-rate'])
+ config.set(base_sflow + ['sampling-rate'], value=sr)
+
+ for server in config.list_nodes(base_fa_sflow + ['server']):
+ config.set(base_sflow + ['server'])
+ config.set_tag(base_sflow + ['server'])
+ config.set(base_sflow + ['server', server])
+ tmp = base_fa_sflow + ['server', server]
+ if config.exists(tmp + ['port']):
+ port = config.return_value(tmp + ['port'])
+ config.set(base_sflow + ['server', server, 'port'], value=port)
+
+ if config.exists(base + ['netflow']):
+ # delete only sflow from flow-accounting if netflow is set
+ config.delete(base_fa_sflow)
+ else:
+ # delete all flow-accounting config otherwise
+ config.delete(base)
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/migration-scripts/ntp/1-to-2 b/src/migration-scripts/ntp/1-to-2
index fd7b08221..d5f800922 100644
--- a/src/migration-scripts/ntp/1-to-2
+++ b/src/migration-scripts/ntp/1-to-2
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-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
@@ -25,6 +25,11 @@ def migrate(config: ConfigTree) -> None:
# Nothing to do
return
+ # T6911: do not migrate NTP configuration if mandatory server is missing
+ if not config.exists(base_path + ['server']):
+ config.delete(base_path)
+ return
+
# config.copy does not recursively create a path, so create ['service'] if
# it doesn't yet exist, such as for config.boot.default
if not config.exists(['service']):
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 20f54df25..b3d7d4dd3 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.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
@@ -19,7 +19,6 @@ import sys
import typing
from datetime import datetime
-from datetime import timezone
from glob import glob
from ipaddress import ip_address
from tabulate import tabulate
@@ -30,137 +29,78 @@ from vyos.base import Warning
from vyos.configquery import ConfigTreeQuery
from vyos.kea import kea_get_active_config
+from vyos.kea import kea_get_dhcp_pools
from vyos.kea import kea_get_leases
-from vyos.kea import kea_get_pool_from_subnet_id
+from vyos.kea import kea_get_server_leases
+from vyos.kea import kea_get_static_mappings
from vyos.kea import kea_delete_lease
-from vyos.utils.process import is_systemd_service_running
from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
-time_string = "%a %b %d %H:%M:%S %Z %Y"
+time_string = '%a %b %d %H:%M:%S %Z %Y'
config = ConfigTreeQuery()
-lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
-sort_valid_inet = ['end', 'mac', 'hostname', 'ip', 'pool', 'remaining', 'start', 'state']
-sort_valid_inet6 = ['end', 'duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type']
+lease_valid_states = [
+ 'all',
+ 'active',
+ 'free',
+ 'expired',
+ 'released',
+ 'abandoned',
+ 'reset',
+ 'backup',
+]
+sort_valid_inet = [
+ 'end',
+ 'mac',
+ 'hostname',
+ 'ip',
+ 'pool',
+ 'remaining',
+ 'start',
+ 'state',
+]
+sort_valid_inet6 = [
+ 'end',
+ 'duid',
+ 'ip',
+ 'last_communication',
+ 'pool',
+ 'remaining',
+ 'state',
+ 'type',
+]
mapping_sort_valid = ['mac', 'ip', 'pool', 'duid']
+stale_warn_msg = 'DHCP server is configured but not started. Data may be stale.'
+
ArgFamily = typing.Literal['inet', 'inet6']
-ArgState = typing.Literal['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
+ArgState = typing.Literal[
+ 'all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'
+]
ArgOrigin = typing.Literal['local', 'remote']
-def _utc_to_local(utc_dt):
- return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds())
-
-def _format_hex_string(in_str):
- out_str = ""
- # if input is divisible by 2, add : every 2 chars
- if len(in_str) > 0 and len(in_str) % 2 == 0:
- out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2]))
- else:
- out_str = in_str
-
- return out_str
-
-
-def _find_list_of_dict_index(lst, key='ip', value=''):
- """
- Find the index entry of list of dict matching the dict value
- Exampe:
- % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}]
- % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2')
- % 1
- """
- idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None)
- return idx
+def _utc_to_local(utc_dt):
+ return datetime.fromtimestamp(
+ (datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds()
+ )
-def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], origin=None) -> list:
- """
- Get DHCP server leases
- :return list
- """
+def _get_raw_server_leases(
+ config, family='inet', pool=None, sorted=None, state=[], origin=None
+) -> list:
inet_suffix = '6' if family == 'inet6' else '4'
- try:
- leases = kea_get_leases(inet_suffix)
- except Exception:
- raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server lease information')
+ pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
- if pool is None:
- pool = _get_dhcp_pools(family=family)
- else:
- pool = [pool]
-
- try:
- active_config = kea_get_active_config(inet_suffix)
- except Exception:
- raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
-
- data = []
- for lease in leases:
- lifetime = lease['valid-lft']
- expiry = (lease['cltt'] + lifetime)
-
- lease['start_timestamp'] = datetime.fromtimestamp(expiry - lifetime, timezone.utc)
- lease['expire_timestamp'] = datetime.fromtimestamp(expiry, timezone.utc) if expiry else None
-
- data_lease = {}
- data_lease['ip'] = lease['ip-address']
- lease_state_long = {0: 'active', 1: 'rejected', 2: 'expired'}
- data_lease['state'] = lease_state_long[lease['state']]
- data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet-id']) if active_config else '-'
- data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None
- 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] == '.':
- data_lease['hostname'] = data_lease['hostname'][:-1]
-
- if family == 'inet':
- data_lease['mac'] = lease['hw-address']
- data_lease['start'] = lease['start_timestamp'].timestamp()
-
- if family == 'inet6':
- data_lease['last_communication'] = lease['start_timestamp'].timestamp()
- data_lease['duid'] = _format_hex_string(lease['duid'])
- data_lease['type'] = lease['type']
-
- if lease['type'] == 'IA_PD':
- prefix_len = lease['prefix-len']
- data_lease['ip'] += f'/{prefix_len}'
-
- data_lease['remaining'] = '-'
-
- if lease['valid-lft'] > 0:
- data_lease['remaining'] = lease['expire_timestamp'] - datetime.now(timezone.utc)
-
- if data_lease['remaining'].days >= 0:
- # substraction gives us a timedelta object which can't be formatted with strftime
- # so we use str(), split gets rid of the microseconds
- data_lease['remaining'] = str(data_lease['remaining']).split('.')[0]
-
- # Do not add old leases
- if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free':
- if not state or state == 'all' or data_lease['state'] in state:
- data.append(data_lease)
-
- # deduplicate
- checked = []
- for entry in data:
- addr = entry.get('ip')
- if addr not in checked:
- checked.append(addr)
- else:
- idx = _find_list_of_dict_index(data, key='ip', value=addr)
- if idx is not None:
- data.pop(idx)
+ mappings = kea_get_server_leases(config, inet_suffix, pools, state, origin)
if sorted:
if sorted == 'ip':
- data.sort(key = lambda x:ip_address(x['ip']))
+ mappings.sort(key=lambda x: ip_address(x['ip']))
else:
- data.sort(key = lambda x:x[sorted])
- return data
+ mappings.sort(key=lambda x: x[sorted])
+ return mappings
def _get_formatted_server_leases(raw_data, family='inet'):
@@ -171,45 +111,60 @@ def _get_formatted_server_leases(raw_data, family='inet'):
hw_addr = lease.get('mac')
state = lease.get('state')
start = lease.get('start')
- start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
+ start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
end = lease.get('end')
- end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-'
+ end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-'
remain = lease.get('remaining')
pool = lease.get('pool')
hostname = lease.get('hostname')
origin = lease.get('origin')
- data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname, origin])
-
- headers = ['IP Address', 'MAC address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool',
- 'Hostname', 'Origin']
+ data_entries.append(
+ [ipaddr, hw_addr, state, start, end, remain, pool, hostname, origin]
+ )
+
+ headers = [
+ 'IP Address',
+ 'MAC address',
+ 'State',
+ 'Lease start',
+ 'Lease expiration',
+ 'Remaining',
+ 'Pool',
+ 'Hostname',
+ 'Origin',
+ ]
if family == 'inet6':
for lease in raw_data:
ipaddr = lease.get('ip')
state = lease.get('state')
start = lease.get('last_communication')
- start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
+ start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
end = lease.get('end')
- end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
+ end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
remain = lease.get('remaining')
lease_type = lease.get('type')
pool = lease.get('pool')
host_identifier = lease.get('duid')
- data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier])
-
- headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool',
- 'DUID']
+ data_entries.append(
+ [ipaddr, state, start, end, remain, lease_type, pool, host_identifier]
+ )
+
+ headers = [
+ 'IPv6 address',
+ 'State',
+ 'Last communication',
+ 'Lease expiration',
+ 'Remaining',
+ 'Type',
+ 'Pool',
+ 'DUID',
+ ]
output = tabulate(data_entries, headers, numalign='left')
return output
-def _get_dhcp_pools(family='inet') -> list:
- v = 'v6' if family == 'inet6' else ''
- pools = config.list_nodes(f'service dhcp{v}-server shared-network-name')
- return pools
-
-
def _get_pool_size(pool, family='inet'):
v = 'v6' if family == 'inet6' else ''
base = f'service dhcp{v}-server shared-network-name {pool}'
@@ -229,26 +184,27 @@ def _get_pool_size(pool, family='inet'):
return size
-def _get_raw_pool_statistics(family='inet', pool=None):
- if pool is None:
- pool = _get_dhcp_pools(family=family)
- else:
- pool = [pool]
+def _get_raw_server_pool_statistics(config, family='inet', pool=None):
+ inet_suffix = '6' if family == 'inet6' else '4'
+ pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
- v = 'v6' if family == 'inet6' else ''
stats = []
- for p in pool:
- subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet')
+ for p in pools:
size = _get_pool_size(family=family, pool=p)
- leases = len(_get_raw_server_leases(family=family, pool=p))
+ leases = len(_get_raw_server_leases(config, family=family, pool=p))
use_percentage = round(leases / size * 100) if size != 0 else 0
- pool_stats = {'pool': p, 'size': size, 'leases': leases,
- 'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet}
+ pool_stats = {
+ 'pool': p,
+ 'size': size,
+ 'leases': leases,
+ 'available': (size - leases),
+ 'use_percentage': use_percentage,
+ }
stats.append(pool_stats)
return stats
-def _get_formatted_pool_statistics(pool_data, family='inet'):
+def _get_formatted_server_pool_statistics(pool_data, family='inet'):
data_entries = []
for entry in pool_data:
pool = entry.get('pool')
@@ -259,67 +215,54 @@ def _get_formatted_pool_statistics(pool_data, family='inet'):
use_percentage = f'{use_percentage}%'
data_entries.append([pool, size, leases, available, use_percentage])
- headers = ['Pool', 'Size','Leases', 'Available', 'Usage']
+ headers = ['Pool', 'Size', 'Leases', 'Available', 'Usage']
output = tabulate(data_entries, headers, numalign='left')
return output
-def _get_raw_server_static_mappings(family='inet', pool=None, sorted=None):
- if pool is None:
- pool = _get_dhcp_pools(family=family)
- else:
- pool = [pool]
- v = 'v6' if family == 'inet6' else ''
- mappings = []
- for p in pool:
- pool_config = config.get_config_dict(['service', f'dhcp{v}-server', 'shared-network-name', p],
- get_first_key=True)
- if 'subnet' in pool_config:
- for subnet, subnet_config in pool_config['subnet'].items():
- if 'static-mapping' in subnet_config:
- for name, mapping_config in subnet_config['static-mapping'].items():
- mapping = {'pool': p, 'subnet': subnet, 'name': name}
- mapping.update(mapping_config)
- mappings.append(mapping)
+def _get_raw_server_static_mappings(config, family='inet', pool=None, sorted=None):
+ inet_suffix = '6' if family == 'inet6' else '4'
+ pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
+
+ mappings = kea_get_static_mappings(config, inet_suffix, pools)
if sorted:
if sorted == 'ip':
- if family == 'inet6':
- mappings.sort(key = lambda x:ip_address(x['ipv6-address']))
- else:
- mappings.sort(key = lambda x:ip_address(x['ip-address']))
+ mappings.sort(key=lambda x: ip_address(x['ip']))
else:
- mappings.sort(key = lambda x:x[sorted])
+ mappings.sort(key=lambda x: x[sorted])
return mappings
+
def _get_formatted_server_static_mappings(raw_data, family='inet'):
data_entries = []
- if family == 'inet':
- for entry in raw_data:
- pool = entry.get('pool')
- subnet = entry.get('subnet')
- name = entry.get('name')
- ip_addr = entry.get('ip-address', 'N/A')
- mac_addr = entry.get('mac', 'N/A')
- duid = entry.get('duid', 'N/A')
- description = entry.get('description', 'N/A')
- data_entries.append([pool, subnet, name, ip_addr, mac_addr, duid, description])
- elif family == 'inet6':
- for entry in raw_data:
- pool = entry.get('pool')
- subnet = entry.get('subnet')
- name = entry.get('name')
- ip_addr = entry.get('ipv6-address', 'N/A')
- mac_addr = entry.get('mac', 'N/A')
- duid = entry.get('duid', 'N/A')
- description = entry.get('description', 'N/A')
- data_entries.append([pool, subnet, name, ip_addr, mac_addr, duid, description])
-
- headers = ['Pool', 'Subnet', 'Name', 'IP Address', 'MAC Address', 'DUID', 'Description']
+
+ for entry in raw_data:
+ pool = entry.get('pool')
+ subnet = entry.get('subnet')
+ hostname = entry.get('hostname')
+ ip_addr = entry.get('ip', 'N/A')
+ mac_addr = entry.get('mac', 'N/A')
+ duid = entry.get('duid', 'N/A')
+ description = entry.get('description', 'N/A')
+ data_entries.append(
+ [pool, subnet, hostname, ip_addr, mac_addr, duid, description]
+ )
+
+ headers = [
+ 'Pool',
+ 'Subnet',
+ 'Hostname',
+ 'IP Address',
+ 'MAC Address',
+ 'DUID',
+ 'Description',
+ ]
output = tabulate(data_entries, headers, numalign='left')
return output
-def _verify(func):
+
+def _verify_server(func):
"""Decorator checks if DHCP(v6) config exists"""
from functools import wraps
@@ -333,8 +276,10 @@ def _verify(func):
if not config.exists(f'service dhcp{v}-server'):
raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
return func(*args, **kwargs)
+
return _wrapper
+
def _verify_client(func):
"""Decorator checks if interface is configured as DHCP client"""
from functools import wraps
@@ -353,67 +298,124 @@ def _verify_client(func):
if not config.exists(f'interfaces {interface_path} address dhcp{v}'):
raise vyos.opmode.UnconfiguredObject(unconf_message)
return func(*args, **kwargs)
+
return _wrapper
-@_verify
-def show_pool_statistics(raw: bool, family: ArgFamily, pool: typing.Optional[str]):
- pool_data = _get_raw_pool_statistics(family=family, pool=pool)
+
+@_verify_server
+def show_server_pool_statistics(
+ raw: bool, family: ArgFamily, pool: typing.Optional[str]
+):
+ v = 'v6' if family == 'inet6' else ''
+ inet_suffix = '6' if family == 'inet6' else '4'
+
+ if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
+ Warning(stale_warn_msg)
+
+ try:
+ active_config = kea_get_active_config(inet_suffix)
+ except Exception:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
+
+ active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
+
+ if pool and active_pools and pool not in active_pools:
+ raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
+
+ pool_data = _get_raw_server_pool_statistics(active_config, family=family, pool=pool)
if raw:
return pool_data
else:
- return _get_formatted_pool_statistics(pool_data, family=family)
+ return _get_formatted_server_pool_statistics(pool_data, family=family)
+
+
+@_verify_server
+def show_server_leases(
+ raw: bool,
+ family: ArgFamily,
+ pool: typing.Optional[str],
+ sorted: typing.Optional[str],
+ state: typing.Optional[ArgState],
+ origin: typing.Optional[ArgOrigin],
+):
+ v = 'v6' if family == 'inet6' else ''
+ inet_suffix = '6' if family == 'inet6' else '4'
+ if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
+ Warning(stale_warn_msg)
-@_verify
-def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str],
- sorted: typing.Optional[str], state: typing.Optional[ArgState],
- origin: typing.Optional[ArgOrigin] ):
- # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
- v = '6' if family == 'inet6' else '4'
- if not is_systemd_service_running(f'kea-dhcp{v}-server.service'):
- Warning('DHCP server is configured but not started. Data may be stale.')
+ try:
+ active_config = kea_get_active_config(inet_suffix)
+ except Exception:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
- v = 'v6' if family == 'inet6' else ''
- if pool and pool not in _get_dhcp_pools(family=family):
- raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
+ active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
- if state and state not in lease_valid_states:
- raise vyos.opmode.IncorrectValue(f'DHCP{v} state "{state}" is invalid!')
+ if pool and active_pools and pool not in active_pools:
+ raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
sort_valid = sort_valid_inet6 if family == 'inet6' else sort_valid_inet
if sorted and sorted not in sort_valid:
raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!')
- lease_data = _get_raw_server_leases(family=family, pool=pool, sorted=sorted, state=state, origin=origin)
+ if state and state not in lease_valid_states:
+ raise vyos.opmode.IncorrectValue(f'DHCP{v} state "{state}" is invalid!')
+
+ lease_data = _get_raw_server_leases(
+ config=active_config,
+ family=family,
+ pool=pool,
+ sorted=sorted,
+ state=state,
+ origin=origin,
+ )
if raw:
return lease_data
else:
return _get_formatted_server_leases(lease_data, family=family)
-@_verify
-def show_server_static_mappings(raw: bool, family: ArgFamily, pool: typing.Optional[str],
- sorted: typing.Optional[str]):
+
+@_verify_server
+def show_server_static_mappings(
+ raw: bool,
+ family: ArgFamily,
+ pool: typing.Optional[str],
+ sorted: typing.Optional[str],
+):
v = 'v6' if family == 'inet6' else ''
- if pool and pool not in _get_dhcp_pools(family=family):
+ inet_suffix = '6' if family == 'inet6' else '4'
+
+ if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
+ Warning(stale_warn_msg)
+
+ try:
+ active_config = kea_get_active_config(inet_suffix)
+ except Exception:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
+
+ active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
+
+ if pool and active_pools and pool not in active_pools:
raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
if sorted and sorted not in mapping_sort_valid:
raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!')
- static_mappings = _get_raw_server_static_mappings(family=family, pool=pool, sorted=sorted)
+ static_mappings = _get_raw_server_static_mappings(
+ config=active_config, family=family, pool=pool, sorted=sorted
+ )
if raw:
return static_mappings
else:
return _get_formatted_server_static_mappings(static_mappings, family=family)
+
def _lease_valid(inet, address):
leases = kea_get_leases(inet)
- for lease in leases:
- if address == lease['ip-address']:
- return True
- return False
+ return any(lease['ip-address'] == address for lease in leases)
+
-@_verify
+@_verify_server
def clear_dhcp_server_lease(family: ArgFamily, address: str):
v = 'v6' if family == 'inet6' else ''
inet = '6' if family == 'inet6' else '4'
@@ -428,6 +430,7 @@ def clear_dhcp_server_lease(family: ArgFamily, address: str):
print(f'Lease "{address}" has been cleared')
+
def _get_raw_client_leases(family='inet', interface=None):
from time import mktime
from datetime import datetime
@@ -456,21 +459,28 @@ def _get_raw_client_leases(family='inet', interface=None):
# format this makes less sense for an API and also the expiry
# timestamp is provided in UNIX time. Convert string (e.g. Sun Jul
# 30 18:13:44 CEST 2023) to UNIX time (1690733624)
- tmp.update({'last_update' : int(mktime(datetime.strptime(line, time_string).timetuple()))})
+ tmp.update(
+ {
+ 'last_update': int(
+ mktime(datetime.strptime(line, time_string).timetuple())
+ )
+ }
+ )
continue
k, v = line.split('=')
- tmp.update({k : v.replace("'", "")})
+ tmp.update({k: v.replace("'", '')})
if 'interface' in tmp:
vrf = get_interface_vrf(tmp['interface'])
if vrf:
- tmp.update({'vrf' : vrf})
+ tmp.update({'vrf': vrf})
lease_data.append(tmp)
return lease_data
+
def _get_formatted_client_leases(lease_data, family):
from time import localtime
from time import strftime
@@ -481,30 +491,34 @@ def _get_formatted_client_leases(lease_data, family):
for lease in lease_data:
if not lease.get('new_ip_address'):
continue
- data_entries.append(["Interface", lease['interface']])
+ data_entries.append(['Interface', lease['interface']])
if 'new_ip_address' in lease:
- tmp = '[Active]' if is_intf_addr_assigned(lease['interface'], lease['new_ip_address']) else '[Inactive]'
- data_entries.append(["IP address", lease['new_ip_address'], tmp])
+ tmp = (
+ '[Active]'
+ if is_intf_addr_assigned(lease['interface'], lease['new_ip_address'])
+ else '[Inactive]'
+ )
+ data_entries.append(['IP address', lease['new_ip_address'], tmp])
if 'new_subnet_mask' in lease:
- data_entries.append(["Subnet Mask", lease['new_subnet_mask']])
+ data_entries.append(['Subnet Mask', lease['new_subnet_mask']])
if 'new_domain_name' in lease:
- data_entries.append(["Domain Name", lease['new_domain_name']])
+ data_entries.append(['Domain Name', lease['new_domain_name']])
if 'new_routers' in lease:
- data_entries.append(["Router", lease['new_routers']])
+ data_entries.append(['Router', lease['new_routers']])
if 'new_domain_name_servers' in lease:
- data_entries.append(["Name Server", lease['new_domain_name_servers']])
+ data_entries.append(['Name Server', lease['new_domain_name_servers']])
if 'new_dhcp_server_identifier' in lease:
- data_entries.append(["DHCP Server", lease['new_dhcp_server_identifier']])
+ data_entries.append(['DHCP Server', lease['new_dhcp_server_identifier']])
if 'new_dhcp_lease_time' in lease:
- data_entries.append(["DHCP Server", lease['new_dhcp_lease_time']])
+ data_entries.append(['DHCP Server', lease['new_dhcp_lease_time']])
if 'vrf' in lease:
- data_entries.append(["VRF", lease['vrf']])
+ data_entries.append(['VRF', lease['vrf']])
if 'last_update' in lease:
tmp = strftime(time_string, localtime(int(lease['last_update'])))
- data_entries.append(["Last Update", tmp])
+ data_entries.append(['Last Update', tmp])
if 'new_expiry' in lease:
tmp = strftime(time_string, localtime(int(lease['new_expiry'])))
- data_entries.append(["Expiry", tmp])
+ data_entries.append(['Expiry', tmp])
# Add empty marker
data_entries.append([''])
@@ -513,6 +527,7 @@ def _get_formatted_client_leases(lease_data, family):
return output
+
def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[str]):
lease_data = _get_raw_client_leases(family=family, interface=interface)
if raw:
@@ -520,6 +535,7 @@ def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[
else:
return _get_formatted_client_leases(lease_data, family=family)
+
@_verify_client
def renew_client_lease(raw: bool, family: ArgFamily, interface: str):
if not raw:
@@ -530,6 +546,7 @@ def renew_client_lease(raw: bool, family: ArgFamily, interface: str):
else:
call(f'systemctl restart dhclient@{interface}.service')
+
@_verify_client
def release_client_lease(raw: bool, family: ArgFamily, interface: str):
if not raw:
@@ -540,6 +557,7 @@ def release_client_lease(raw: bool, family: ArgFamily, interface: str):
else:
call(f'systemctl stop dhclient@{interface}.service')
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
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/reset_wireguard.py b/src/op_mode/reset_wireguard.py
new file mode 100755
index 000000000..1fcfb31b5
--- /dev/null
+++ b/src/op_mode/reset_wireguard.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+# 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 typing
+
+import vyos.opmode
+
+from vyos.ifconfig import WireGuardIf
+from vyos.configquery import ConfigTreeQuery
+
+
+def _verify(func):
+ """Decorator checks if WireGuard interface config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ interface = kwargs.get('interface')
+ if not config.exists(['interfaces', 'wireguard', interface]):
+ unconf_message = f'WireGuard interface {interface} is not configured'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def reset_peer(interface: str, peer: typing.Optional[str] = None):
+ intf = WireGuardIf(interface, create=False, debug=False)
+ return intf.operational.reset_peer(peer)
+
+
+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"
diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver
index bc74a05d1..fe0f40a07 100755
--- a/src/services/vyos-domain-resolver
+++ b/src/services/vyos-domain-resolver
@@ -22,8 +22,10 @@ from vyos.configdict import dict_merge
from vyos.configquery import ConfigTreeQuery
from vyos.firewall import fqdn_config_parse
from vyos.firewall import fqdn_resolve
+from vyos.ifconfig import WireGuardIf
from vyos.utils.commit import commit_in_progress
from vyos.utils.dict import dict_search_args
+from vyos.utils.kernel import WIREGUARD_REKEY_AFTER_TIME
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.xml_ref import get_defaults
@@ -33,6 +35,7 @@ timeout = 300
cache = False
base_firewall = ['firewall']
base_nat = ['nat']
+base_interfaces = ['interfaces']
domain_state = {}
@@ -171,8 +174,45 @@ def update_fqdn(config, node):
logger.info(f'Updated {count} sets in {node} - result: {code}')
+def update_interfaces(config, node):
+ if node == 'interfaces':
+ wg_interfaces = dict_search_args(config, 'wireguard')
+
+ peer_public_keys = {}
+ # for each wireguard interfaces
+ for interface, wireguard in wg_interfaces.items():
+ peer_public_keys[interface] = []
+ for peer, peer_config in wireguard['peer'].items():
+ # check peer if peer host-name or address is set
+ if 'host_name' in peer_config or 'address' in peer_config:
+ # check latest handshake
+ peer_public_keys[interface].append(
+ peer_config['public_key']
+ )
+
+ now_time = time.time()
+ for (interface, check_peer_public_keys) in peer_public_keys.items():
+ if len(check_peer_public_keys) == 0:
+ continue
+
+ intf = WireGuardIf(interface, create=False, debug=False)
+ handshakes = intf.operational.get_latest_handshakes()
+
+ # WireGuard performs a handshake every WIREGUARD_REKEY_AFTER_TIME
+ # if data is being transmitted between the peers. If no data is
+ # transmitted, the handshake will not be initiated unless new
+ # data begins to flow. Each handshake generates a new session
+ # key, and the key is rotated at least every 120 seconds or
+ # upon data transmission after a prolonged silence.
+ for public_key, handshake_time in handshakes.items():
+ if public_key in check_peer_public_keys and (
+ handshake_time == 0
+ or (now_time - handshake_time > 3*WIREGUARD_REKEY_AFTER_TIME)
+ ):
+ intf.operational.reset_peer(public_key=public_key)
+
if __name__ == '__main__':
- logger.info(f'VyOS domain resolver')
+ logger.info('VyOS domain resolver')
count = 1
while commit_in_progress():
@@ -184,10 +224,12 @@ if __name__ == '__main__':
conf = ConfigTreeQuery()
firewall = get_config(conf, base_firewall)
nat = get_config(conf, base_nat)
+ interfaces = get_config(conf, base_interfaces)
logger.info(f'interval: {timeout}s - cache: {cache}')
while True:
update_fqdn(firewall, 'firewall')
update_fqdn(nat, 'nat')
+ update_interfaces(interfaces, 'interfaces')
time.sleep(timeout)
diff --git a/src/services/vyos-network-event-logger b/src/services/vyos-network-event-logger
new file mode 100644
index 000000000..840ff3cda
--- /dev/null
+++ b/src/services/vyos-network-event-logger
@@ -0,0 +1,1218 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+# 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 argparse
+import logging
+import multiprocessing
+import queue
+import signal
+import socket
+import threading
+from pathlib import Path
+from time import sleep
+from typing import Dict, AnyStr, List, Union
+
+from pyroute2.common import AF_MPLS
+from pyroute2.iproute import IPRoute
+from pyroute2.netlink import rtnl, nlmsg
+from pyroute2.netlink.nfnetlink.nfctsocket import nfct_msg
+from pyroute2.netlink.rtnl import (rt_proto as RT_PROTO, rt_type as RT_TYPES,
+ rtypes as RTYPES
+ )
+from pyroute2.netlink.rtnl.fibmsg import FR_ACT_GOTO, FR_ACT_NOP, FR_ACT_TO_TBL, \
+ fibmsg
+from pyroute2.netlink.rtnl import ifaddrmsg
+from pyroute2.netlink.rtnl import ifinfmsg
+from pyroute2.netlink.rtnl import ndmsg
+from pyroute2.netlink.rtnl import rtmsg
+from pyroute2.netlink.rtnl.rtmsg import nh, rtmsg_base
+
+from vyos.include.uapi.linux.fib_rules import *
+from vyos.include.uapi.linux.icmpv6 import *
+from vyos.include.uapi.linux.if_arp import *
+from vyos.include.uapi.linux.lwtunnel import *
+from vyos.include.uapi.linux.neighbour import *
+from vyos.include.uapi.linux.rtnetlink import *
+
+from vyos.utils.file import read_json
+
+
+manager = multiprocessing.Manager()
+cache = manager.dict()
+
+
+class UnsupportedMessageType(Exception):
+ pass
+
+shutdown_event = multiprocessing.Event()
+
+logging.basicConfig(level=logging.INFO, format='%(message)s')
+logger = logging.getLogger(__name__)
+
+
+class DebugFormatter(logging.Formatter):
+ def format(self, record):
+ self._style._fmt = '[%(asctime)s] %(levelname)s: %(message)s'
+ return super().format(record)
+
+
+def set_log_level(level: str) -> None:
+ if level == 'debug':
+ logger.setLevel(logging.DEBUG)
+ logger.parent.handlers[0].setFormatter(DebugFormatter())
+ else:
+ logger.setLevel(logging.INFO)
+
+IFF_FLAGS = {
+ 'RUNNING': ifinfmsg.IFF_RUNNING,
+ 'LOOPBACK': ifinfmsg.IFF_LOOPBACK,
+ 'BROADCAST': ifinfmsg.IFF_BROADCAST,
+ 'POINTOPOINT': ifinfmsg.IFF_POINTOPOINT,
+ 'MULTICAST': ifinfmsg.IFF_MULTICAST,
+ 'NOARP': ifinfmsg.IFF_NOARP,
+ 'ALLMULTI': ifinfmsg.IFF_ALLMULTI,
+ 'PROMISC': ifinfmsg.IFF_PROMISC,
+ 'MASTER': ifinfmsg.IFF_MASTER,
+ 'SLAVE': ifinfmsg.IFF_SLAVE,
+ 'DEBUG': ifinfmsg.IFF_DEBUG,
+ 'DYNAMIC': ifinfmsg.IFF_DYNAMIC,
+ 'AUTOMEDIA': ifinfmsg.IFF_AUTOMEDIA,
+ 'PORTSEL': ifinfmsg.IFF_PORTSEL,
+ 'NOTRAILERS': ifinfmsg.IFF_NOTRAILERS,
+ 'UP': ifinfmsg.IFF_UP,
+ 'LOWER_UP': ifinfmsg.IFF_LOWER_UP,
+ 'DORMANT': ifinfmsg.IFF_DORMANT,
+ 'ECHO': ifinfmsg.IFF_ECHO,
+}
+
+NEIGH_STATE_FLAGS = {
+ 'INCOMPLETE': ndmsg.NUD_INCOMPLETE,
+ 'REACHABLE': ndmsg.NUD_REACHABLE,
+ 'STALE': ndmsg.NUD_STALE,
+ 'DELAY': ndmsg.NUD_DELAY,
+ 'PROBE': ndmsg.NUD_PROBE,
+ 'FAILED': ndmsg.NUD_FAILED,
+ 'NOARP': ndmsg.NUD_NOARP,
+ 'PERMANENT': ndmsg.NUD_PERMANENT,
+}
+
+IFA_FLAGS = {
+ 'secondary': ifaddrmsg.IFA_F_SECONDARY,
+ 'temporary': ifaddrmsg.IFA_F_SECONDARY,
+ 'nodad': ifaddrmsg.IFA_F_NODAD,
+ 'optimistic': ifaddrmsg.IFA_F_OPTIMISTIC,
+ 'dadfailed': ifaddrmsg.IFA_F_DADFAILED,
+ 'home': ifaddrmsg.IFA_F_HOMEADDRESS,
+ 'deprecated': ifaddrmsg.IFA_F_DEPRECATED,
+ 'tentative': ifaddrmsg.IFA_F_TENTATIVE,
+ 'permanent': ifaddrmsg.IFA_F_PERMANENT,
+ 'mngtmpaddr': ifaddrmsg.IFA_F_MANAGETEMPADDR,
+ 'noprefixroute': ifaddrmsg.IFA_F_NOPREFIXROUTE,
+ 'autojoin': ifaddrmsg.IFA_F_MCAUTOJOIN,
+ 'stable-privacy': ifaddrmsg.IFA_F_STABLE_PRIVACY,
+}
+
+RT_SCOPE_TO_NAME = {
+ rtmsg.RT_SCOPE_UNIVERSE: 'global',
+ rtmsg.RT_SCOPE_SITE: 'site',
+ rtmsg.RT_SCOPE_LINK: 'link',
+ rtmsg.RT_SCOPE_HOST: 'host',
+ rtmsg.RT_SCOPE_NOWHERE: 'nowhere',
+}
+
+FAMILY_TO_NAME = {
+ socket.AF_INET: 'inet',
+ socket.AF_INET6: 'inet6',
+ socket.AF_PACKET: 'link',
+ AF_MPLS: 'mpls',
+ socket.AF_BRIDGE: 'bridge',
+}
+
+_INFINITY = 4294967295
+
+
+def _get_iif_name(idx: int) -> str:
+ """
+ Retrieves the interface name associated with a given index.
+ """
+ try:
+ if_info = IPRoute().link("get", index=idx)
+ if if_info:
+ return if_info[0].get_attr('IFLA_IFNAME')
+ except Exception as e:
+ pass
+
+ return ''
+
+
+def remember_if_index(idx: int, event_type: int) -> None:
+ """
+ Manages the caching of network interface names based on their index and event type.
+
+ - For RTM_DELLINK event, the interface name is removed from the cache if exists.
+ - For RTM_NEWLINK event, the interface name is retrieved and updated in the cache.
+ """
+ name = cache.get(idx)
+ if name:
+ if event_type == rtnl.RTM_DELLINK:
+ del cache[idx]
+ else:
+ name = _get_iif_name(idx)
+ if name:
+ cache[idx] = name
+ else:
+ cache[idx] = _get_iif_name(idx)
+
+
+class BaseFormatter:
+ """
+ A base class providing utility methods for formatting network message data.
+ """
+ def _get_if_name_by_index(self, idx: int) -> str:
+ """
+ Retrieves the name of a network interface based on its index.
+
+ Uses a cached lookup for efficiency. If the name is not found in the cache,
+ it queries the system and updates the cache.
+ """
+ if_name = cache.get(idx)
+ if not if_name:
+ if_name = _get_iif_name(idx)
+ cache[idx] = if_name
+
+ return if_name
+
+ def _format_rttable(self, idx: int) -> str:
+ """
+ Formats a route table identifier into a readable name.
+ """
+ return f'{RT_TABLE_TO_NAME.get(idx, idx)}'
+
+ def _parse_flag(self, data: int, flags: dict) -> list:
+ """
+ Extracts and returns flag names equal the bits set in a numeric value.
+ """
+ result = list()
+ if data:
+ for key, val in flags.items():
+ if data & val:
+ result.append(key)
+ data &= ~val
+
+ if data:
+ result.append(f"{data:#x}")
+
+ return result
+
+ def af_bit_len(self, af: int) -> int:
+ """
+ Gets the bit length of a given address family.
+ Supports common address families like IPv4, IPv6, and MPLS.
+ """
+ _map = {
+ socket.AF_INET6: 128,
+ socket.AF_INET: 32,
+ AF_MPLS: 20,
+ }
+
+ return _map.get(af)
+
+ def _format_simple_field(self, data: str, prefix: str='') -> str:
+ """
+ Formats a simple field with an optional prefix.
+
+ A simple field represents a value that does not require additional
+ parsing and is used as is.
+ """
+ return self._output(f'{prefix} {data}') if data is not None else ''
+
+ def _output(self, data: str) -> str:
+ """
+ Standardizes the output format.
+
+ Ensures that the output is enclosed with single spaces and has no leading
+ or trailing whitespace.
+ """
+ return f' {data.strip()} ' if data else ''
+
+
+class BaseMSGFormatter(BaseFormatter):
+ """
+ A base formatter class for network messages.
+ This class provides common methods for formatting network-related messages,
+ """
+
+ def _prepare_start_message(self, event: str) -> str:
+ """
+ Prepares a starting message string based on the event type.
+ """
+ if event in ['RTM_DELROUTE', 'RTM_DELLINK', 'RTM_DELNEIGH',
+ 'RTM_DELADDR', 'RTM_DELADDRLABEL', 'RTM_DELRULE',
+ 'RTM_DELNETCONF']:
+ return 'Deleted '
+ if event == 'RTM_GETNEIGH':
+ return 'Miss '
+ return ''
+
+ def _format_flow_field(self, data: int) -> str:
+ """
+ Formats a flow field to represent traffic realms.
+ """
+ to = data & 0xFFFF
+ from_ = data >> 16
+ result = f"realm{'s' if from_ else ''} "
+ if from_:
+ result += f'{from_}/'
+ result += f'{to}'
+
+ return self._output(result)
+
+ def format(self, msg: nlmsg) -> str:
+ """
+ Abstract method to format a complete message.
+
+ This method must be implemented by subclasses to provide specific formatting
+ logic for different types of messages.
+ """
+ raise NotImplementedError(f'{msg.get("event")}: {msg}')
+
+
+class LinkFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling link-related network messages
+ `RTM_NEWLINK` and `RTM_DELLINK`.
+ """
+ def _format_iff_flags(self, flags: int) -> str:
+ """
+ Formats interface flags into a human-readable string.
+ """
+ result = list()
+ if flags:
+ if flags & IFF_FLAGS['UP'] and not flags & IFF_FLAGS['RUNNING']:
+ result.append('NO-CARRIER')
+
+ flags &= ~IFF_FLAGS['RUNNING']
+
+ result.extend(self._parse_flag(flags, IFF_FLAGS))
+
+ return self._output(f'<{(",").join(result)}>')
+
+ def _format_if_props(self, data: ifinfmsg.ifinfbase.proplist) -> str:
+ """
+ Formats interface alternative name properties.
+ """
+ result = ''
+ for rec in data.altnames():
+ result += f'[altname {rec}] '
+ return self._output(result)
+
+ def _format_link(self, msg: ifinfmsg.ifinfmsg) -> str:
+ """
+ Formats the link attribute of a network interface message.
+ """
+ if msg.get_attr("IFLA_LINK") is not None:
+ iflink = msg.get_attr("IFLA_LINK")
+ if iflink:
+ if msg.get_attr("IFLA_LINK_NETNSID"):
+ return f'if{iflink}'
+ else:
+ return self._get_if_name_by_index(iflink)
+ return 'NONE'
+
+ def _format_link_info(self, msg: ifinfmsg.ifinfmsg) -> str:
+ """
+ Formats detailed information about the link, including type, address,
+ broadcast address, and permanent address.
+ """
+ result = f'link/{ARPHRD_TO_NAME.get(msg.get("ifi_type"), msg.get("ifi_type"))}'
+ result += self._format_simple_field(msg.get_attr('IFLA_ADDRESS'))
+
+ if msg.get_attr("IFLA_BROADCAST"):
+ if msg.get('flags') & ifinfmsg.IFF_POINTOPOINT:
+ result += f' peer'
+ else:
+ result += f' brd'
+ result += f' {msg.get_attr("IFLA_BROADCAST")}'
+
+ if msg.get_attr("IFLA_PERM_ADDRESS"):
+ if not msg.get_attr("IFLA_ADDRESS") or \
+ msg.get_attr("IFLA_ADDRESS") != msg.get_attr("IFLA_PERM_ADDRESS"):
+ result += f' permaddr {msg.get_attr("IFLA_PERM_ADDRESS")}'
+
+ return self._output(result)
+
+ def format(self, msg: ifinfmsg.ifinfmsg):
+ """
+ Formats a network link message into a structured output string.
+ """
+ if msg.get("family") not in [socket.AF_UNSPEC, socket.AF_BRIDGE]:
+ return None
+
+ message = self._prepare_start_message(msg.get('event'))
+
+ link = self._format_link(msg)
+
+ message += f'{msg.get("index")}: {msg.get_attr("IFLA_IFNAME")}'
+ message += f'@{link}' if link else ''
+ message += f': {self._format_iff_flags(msg.get("flags"))}'
+
+ message += self._format_simple_field(msg.get_attr('IFLA_MTU'), prefix='mtu')
+ message += self._format_simple_field(msg.get_attr('IFLA_QDISC'), prefix='qdisc')
+ message += self._format_simple_field(msg.get_attr('IFLA_OPERSTATE'), prefix='state')
+ message += self._format_simple_field(msg.get_attr('IFLA_GROUP'), prefix='group')
+ message += self._format_simple_field(msg.get_attr('IFLA_MASTER'), prefix='master')
+
+ message += self._format_link_info(msg)
+
+ if msg.get_attr('IFLA_PROP_LIST'):
+ message += self._format_if_props(msg.get_attr('IFLA_PROP_LIST'))
+
+ return self._output(message)
+
+
+class EncapFormatter(BaseFormatter):
+ """
+ A formatter class for handling encapsulation attributes in routing messages.
+ """
+ # TODO: implement other lwtunnel decoder in pyroute2
+ # https://github.com/svinota/pyroute2/blob/78cfe838bec8d96324811a3962bda15fb028e0ce/pyroute2/netlink/rtnl/rtmsg.py#L657
+ def __init__(self):
+ """
+ Initializes the EncapFormatter with supported encapsulation types.
+ """
+ self.formatters = {
+ rtmsg.LWTUNNEL_ENCAP_MPLS: self.mpls_format,
+ rtmsg.LWTUNNEL_ENCAP_SEG6: self.seg6_format,
+ rtmsg.LWTUNNEL_ENCAP_BPF: self.bpf_format,
+ rtmsg.LWTUNNEL_ENCAP_SEG6_LOCAL: self.seg6local_format,
+ }
+
+ def _format_srh(self, data: rtmsg_base.seg6_encap_info.ipv6_sr_hdr):
+ """
+ Formats Segment Routing Header (SRH) attributes.
+ """
+ result = ''
+ # pyroute2 decode mode only as inline or encap (encap, l2encap, encap.red, l2encap.red")
+ # https://github.com/svinota/pyroute2/blob/78cfe838bec8d96324811a3962bda15fb028e0ce/pyroute2/netlink/rtnl/rtmsg.py#L220
+ for key in ['mode', 'segs']:
+
+ val = data.get(key)
+
+ if val:
+ if key == 'segs':
+ result += f'{key} {len(val)} {val} '
+ else:
+ result += f'{key} {val} '
+
+ return self._output(result)
+
+ def _format_bpf_object(self, data: rtmsg_base.bpf_encap_info, attr_name: str, attr_key: str):
+ """
+ Formats eBPF program attributes.
+ """
+ attr = data.get_attr(attr_name)
+ if not attr:
+ return ''
+ result = ''
+ if attr.get_attr("LWT_BPF_PROG_NAME"):
+ result += f'{attr.get_attr("LWT_BPF_PROG_NAME")} '
+ if attr.get_attr("LWT_BPF_PROG_FD"):
+ result += f'{attr.get_attr("LWT_BPF_PROG_FD")} '
+
+ return self._output(f'{attr_key} {result.strip()}')
+
+ def mpls_format(self, data: rtmsg_base.mpls_encap_info):
+ """
+ Formats MPLS encapsulation attributes.
+ """
+ result = ''
+ if data.get_attr("MPLS_IPTUNNEL_DST"):
+ for rec in data.get_attr("MPLS_IPTUNNEL_DST"):
+ for key, val in rec.items():
+ if val:
+ result += f'{key} {val} '
+
+ if data.get_attr("MPLS_IPTUNNEL_TTL"):
+ result += f' ttl {data.get_attr("MPLS_IPTUNNEL_TTL")}'
+
+ return self._output(result)
+
+ def bpf_format(self, data: rtmsg_base.bpf_encap_info):
+ """
+ Formats eBPF encapsulation attributes.
+ """
+ result = ''
+ result += self._format_bpf_object(data, 'LWT_BPF_IN', 'in')
+ result += self._format_bpf_object(data, 'LWT_BPF_OUT', 'out')
+ result += self._format_bpf_object(data, 'LWT_BPF_XMIT', 'xmit')
+
+ if data.get_attr('LWT_BPF_XMIT_HEADROOM'):
+ result += f'headroom {data.get_attr("LWT_BPF_XMIT_HEADROOM")} '
+
+ return self._output(result)
+
+ def seg6_format(self, data: rtmsg_base.seg6_encap_info):
+ """
+ Formats Segment Routing (SEG6) encapsulation attributes.
+ """
+ result = ''
+ if data.get_attr("SEG6_IPTUNNEL_SRH"):
+ result += self._format_srh(data.get_attr("SEG6_IPTUNNEL_SRH"))
+
+ return self._output(result)
+
+ def seg6local_format(self, data: rtmsg_base.seg6local_encap_info):
+ """
+ Formats SEG6 local encapsulation attributes.
+ """
+ result = ''
+ formatters = {
+ 'SEG6_LOCAL_ACTION': lambda val: f' action {next((k for k, v in data.action.actions.items() if v == val), "unknown")}',
+ 'SEG6_LOCAL_SRH': lambda val: f' {self._format_srh(val)}',
+ 'SEG6_LOCAL_TABLE': lambda val: f' table {self._format_rttable(val)}',
+ 'SEG6_LOCAL_NH4': lambda val: f' nh4 {val}',
+ 'SEG6_LOCAL_NH6': lambda val: f' nh6 {val}',
+ 'SEG6_LOCAL_IIF': lambda val: f' iif {self._get_if_name_by_index(val)}',
+ 'SEG6_LOCAL_OIF': lambda val: f' oif {self._get_if_name_by_index(val)}',
+ 'SEG6_LOCAL_BPF': lambda val: f' endpoint {val.get("LWT_BPF_PROG_NAME")}',
+ 'SEG6_LOCAL_VRFTABLE': lambda val: f' vrftable {self._format_rttable(val)}',
+ }
+
+ for rec in data.get('attrs'):
+ if rec[0] in formatters:
+ result += formatters[rec[0]](rec[1])
+
+ return self._output(result)
+
+ def format(self, type: int, data: Union[rtmsg_base.mpls_encap_info,
+ rtmsg_base.bpf_encap_info,
+ rtmsg_base.seg6_encap_info,
+ rtmsg_base.seg6local_encap_info]):
+ """
+ Formats encapsulation attributes based on their type.
+ """
+ result = ''
+ formatter = self.formatters.get(type)
+
+ result += f'encap {ENCAP_TO_NAME.get(type, "unknown")}'
+
+ if formatter:
+ result += f' {formatter(data)}'
+
+ return self._output(result)
+
+
+class RouteFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling network routing messages
+ `RTM_NEWROUTE` and `RTM_DELROUTE`.
+ """
+
+ def _format_rt_flags(self, flags: int) -> str:
+ """
+ Formats route flags into a comma-separated string.
+ """
+ result = list()
+ result.extend(self._parse_flag(flags, RT_FlAGS))
+
+ return self._output(",".join(result))
+
+ def _format_rta_encap(self, type: int, data: Union[rtmsg_base.mpls_encap_info,
+ rtmsg_base.bpf_encap_info,
+ rtmsg_base.seg6_encap_info,
+ rtmsg_base.seg6local_encap_info]) -> str:
+ """
+ Formats encapsulation attributes.
+ """
+ return EncapFormatter().format(type, data)
+
+ def _format_rta_newdest(self, data: str) -> str:
+ """
+ Formats a new destination attribute.
+ """
+ return self._output(f'as to {data}')
+
+ def _format_rta_gateway(self, data: str) -> str:
+ """
+ Formats a gateway attribute.
+ """
+ return self._output(f'via {data}')
+
+ def _format_rta_via(self, data: str) -> str:
+ """
+ Formats a 'via' route attribute.
+ """
+ return self._output(f'{data}')
+
+ def _format_rta_metrics(self, data: rtmsg_base.metrics):
+ """
+ Formats routing metrics.
+ """
+ result = ''
+
+ def __format_metric_time(_val: int) -> str:
+ """Formats metric time values into seconds or milliseconds."""
+ return f"{_val / 1000}s" if _val >= 1000 else f"{_val}ms"
+
+ def __format_reatures(_val: int) -> str:
+ """Parse and formats routing feature flags."""
+ result = self._parse_flag(_val, {'ecn': RTAX_FEATURE_ECN,
+ 'tcp_usec_ts': RTAX_FEATURE_TCP_USEC_TS})
+ return ",".join(result)
+
+ formatters = {
+ 'RTAX_MTU': lambda val: f' mtu {val}',
+ 'RTAX_WINDOW': lambda val: f' window {val}',
+ 'RTAX_RTT': lambda val: f' rtt {__format_metric_time(val / 8)}',
+ 'RTAX_RTTVAR': lambda val: f' rttvar {__format_metric_time(val / 4)}',
+ 'RTAX_SSTHRESH': lambda val: f' ssthresh {val}',
+ 'RTAX_CWND': lambda val: f' cwnd {val}',
+ 'RTAX_ADVMSS': lambda val: f' advmss {val}',
+ 'RTAX_REORDERING': lambda val: f' reordering {val}',
+ 'RTAX_HOPLIMIT': lambda val: f' hoplimit {val}',
+ 'RTAX_INITCWND': lambda val: f' initcwnd {val}',
+ 'RTAX_FEATURES': lambda val: f' features {__format_reatures(val)}',
+ 'RTAX_RTO_MIN': lambda val: f' rto_min {__format_metric_time(val)}',
+ 'RTAX_INITRWND': lambda val: f' initrwnd {val}',
+ 'RTAX_QUICKACK': lambda val: f' quickack {val}',
+ }
+
+ for rec in data.get('attrs'):
+ if rec[0] in formatters:
+ result += formatters[rec[0]](rec[1])
+
+ return self._output(result)
+
+ def _format_rta_pref(self, data: int) -> str:
+ """
+ Formats a pref attribute.
+ """
+ pref = {
+ ICMPV6_ROUTER_PREF_LOW: "low",
+ ICMPV6_ROUTER_PREF_MEDIUM: "medium",
+ ICMPV6_ROUTER_PREF_HIGH: "high",
+ }
+
+ return self._output(f' pref {pref.get(data, data)}')
+
+ def _format_rta_multipath(self, mcast_cloned: bool, family: int, data: List[nh]) -> str:
+ """
+ Formats multipath route attributes.
+ """
+ result = ''
+ first = True
+ for rec in data:
+ if mcast_cloned:
+ if first:
+ result += ' Oifs: '
+ first = False
+ else:
+ result += ' '
+ else:
+ result += ' nexthop '
+
+ if rec.get_attr('RTA_ENCAP'):
+ result += self._format_rta_encap(rec.get_attr('RTA_ENCAP_TYPE'),
+ rec.get_attr('RTA_ENCAP'))
+
+ if rec.get_attr('RTA_NEWDST'):
+ result += self._format_rta_newdest(rec.get_attr('RTA_NEWDST'))
+
+ if rec.get_attr('RTA_GATEWAY'):
+ result += self._format_rta_gateway(rec.get_attr('RTA_GATEWAY'))
+
+ if rec.get_attr('RTA_VIA'):
+ result += self._format_rta_via(rec.get_attr('RTA_VIA'))
+
+ if rec.get_attr('RTA_FLOW'):
+ result += self._format_flow_field(rec.get_attr('RTA_FLOW'))
+
+ result += f' dev {self._get_if_name_by_index(rec.get("oif"))}'
+ if mcast_cloned:
+ if rec.get("hops") != 1:
+ result += f' (ttl>{rec.get("hops")})'
+ else:
+ if family != AF_MPLS:
+ result += f' weight {rec.get("hops") + 1}'
+
+ result += self._format_rt_flags(rec.get("flags"))
+
+ return self._output(result)
+
+ def format(self, msg: rtmsg.rtmsg) -> str:
+ """
+ Formats a network route message into a human-readable string representation.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+
+ message += RT_TYPES.get(msg.get('type'))
+
+ if msg.get_attr('RTA_DST'):
+ host_len = self.af_bit_len(msg.get('family'))
+ if msg.get('dst_len') != host_len:
+ message += f' {msg.get_attr("RTA_DST")}/{msg.get("dst_len")}'
+ else:
+ message += f' {msg.get_attr("RTA_DST")}'
+ elif msg.get('dst_len'):
+ message += f' 0/{msg.get("dst_len")}'
+ else:
+ message += ' default'
+
+ if msg.get_attr('RTA_SRC'):
+ message += f' from {msg.get_attr("RTA_SRC")}'
+ elif msg.get('src_len'):
+ message += f' from 0/{msg.get("src_len")}'
+
+ message += self._format_simple_field(msg.get_attr('RTA_NH_ID'), prefix='nhid')
+
+ if msg.get_attr('RTA_NEWDST'):
+ message += self._format_rta_newdest(msg.get_attr('RTA_NEWDST'))
+
+ if msg.get_attr('RTA_ENCAP'):
+ message += self._format_rta_encap(msg.get_attr('RTA_ENCAP_TYPE'),
+ msg.get_attr('RTA_ENCAP'))
+
+ message += self._format_simple_field(msg.get('tos'), prefix='tos')
+
+ if msg.get_attr('RTA_GATEWAY'):
+ message += self._format_rta_gateway(msg.get_attr('RTA_GATEWAY'))
+
+ if msg.get_attr('RTA_VIA'):
+ message += self._format_rta_via(msg.get_attr('RTA_VIA'))
+
+ if msg.get_attr('RTA_OIF') is not None:
+ message += f' dev {self._get_if_name_by_index(msg.get_attr("RTA_OIF"))}'
+
+ if msg.get_attr("RTA_TABLE"):
+ message += f' table {self._format_rttable(msg.get_attr("RTA_TABLE"))}'
+
+ if not msg.get('flags') & RTM_F_CLONED:
+ message += f' proto {RT_PROTO.get(msg.get("proto"))}'
+
+ if not msg.get('scope') == rtmsg.RT_SCOPE_UNIVERSE:
+ message += f' scope {RT_SCOPE_TO_NAME.get(msg.get("scope"))}'
+
+ message += self._format_simple_field(msg.get_attr('RTA_PREFSRC'), prefix='src')
+ message += self._format_simple_field(msg.get_attr('RTA_PRIORITY'), prefix='metric')
+
+ message += self._format_rt_flags(msg.get("flags"))
+
+ if msg.get_attr('RTA_MARK'):
+ mark = msg.get_attr("RTA_MARK")
+ if mark >= 16:
+ message += f' mark 0x{mark:x}'
+ else:
+ message += f' mark {mark}'
+
+ if msg.get_attr('RTA_FLOW'):
+ message += self._format_flow_field(msg.get_attr('RTA_FLOW'))
+
+ message += self._format_simple_field(msg.get_attr('RTA_UID'), prefix='uid')
+
+ if msg.get_attr('RTA_METRICS'):
+ message += self._format_rta_metrics(msg.get_attr("RTA_METRICS"))
+
+ if msg.get_attr('RTA_IIF') is not None:
+ message += f' iif {self._get_if_name_by_index(msg.get_attr("RTA_IIF"))}'
+
+ if msg.get_attr('RTA_PREF') is not None:
+ message += self._format_rta_pref(msg.get_attr("RTA_PREF"))
+
+ if msg.get_attr('RTA_TTL_PROPAGATE') is not None:
+ message += f' ttl-propogate {"enabled" if msg.get_attr("RTA_TTL_PROPAGATE") else "disabled"}'
+
+ if msg.get_attr('RTA_MULTIPATH') is not None:
+ _tmp = self._format_rta_multipath(
+ mcast_cloned=msg.get('flags') & RTM_F_CLONED and msg.get('type') == RTYPES['RTN_MULTICAST'],
+ family=msg.get('family'),
+ data=msg.get_attr("RTA_MULTIPATH"))
+ message += f' {_tmp}'
+
+ return self._output(message)
+
+
+class AddrFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling address-related network messages
+ `RTM_NEWADDR` and `RTM_DELADDR`.
+ """
+ INFINITY_LIFE_TIME = _INFINITY
+
+ def _format_ifa_flags(self, flags: int, family: int) -> str:
+ """
+ Formats address flags into a human-readable string.
+ """
+ result = list()
+ if flags:
+ if not flags & IFA_FLAGS['permanent']:
+ result.append('dynamic')
+ flags &= ~IFA_FLAGS['permanent']
+
+ if flags & IFA_FLAGS['temporary'] and family == socket.AF_INET6:
+ result.append('temporary')
+ flags &= ~IFA_FLAGS['temporary']
+
+ result.extend(self._parse_flag(flags, IFA_FLAGS))
+
+ return self._output(",".join(result))
+
+ def _format_ifa_addr(self, local: str, addr: str, preflen: int, priority: int) -> str:
+ """
+ Formats address information into a shuman-readable string.
+ """
+ result = ''
+ local = local or addr
+ addr = addr or local
+
+ if local:
+ result += f'{local}'
+ if addr and addr != local:
+ result += f' peer {addr}'
+ result += f'/{preflen}'
+
+ if priority:
+ result += f' {priority}'
+
+ return self._output(result)
+
+ def _format_ifa_cacheinfo(self, data: ifaddrmsg.ifaddrmsg.cacheinfo) -> str:
+ """
+ Formats cache information for an address.
+ """
+ result = ''
+ _map = {
+ 'ifa_valid': 'valid_lft',
+ 'ifa_preferred': 'preferred_lft',
+ }
+
+ for key in ['ifa_valid', 'ifa_preferred']:
+ val = data.get(key)
+ if val == self.INFINITY_LIFE_TIME:
+ result += f'{_map.get(key)} forever '
+ else:
+ result += f'{_map.get(key)} {val}sec '
+
+ return self._output(result)
+
+ def format(self, msg: ifaddrmsg.ifaddrmsg) -> str:
+ """
+ Formats a full network address message.
+ Combine attributes such as index, family, address, flags, and cache
+ information into a structured output string.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+
+ message += f'{msg.get("index")}: {self._get_if_name_by_index(msg.get("index"))} '
+ message += f'{FAMILY_TO_NAME.get(msg.get("family"), msg.get("family"))} '
+
+ message += self._format_ifa_addr(
+ msg.get_attr('IFA_LOCAL'),
+ msg.get_attr('IFA_ADDRESS'),
+ msg.get('prefixlen'),
+ msg.get_attr('IFA_RT_PRIORITY')
+ )
+ message += self._format_simple_field(msg.get_attr('IFA_BROADCAST'), prefix='brd')
+ message += self._format_simple_field(msg.get_attr('IFA_ANYCAST'), prefix='any')
+
+ if msg.get('scope') is not None:
+ message += f' scope {RT_SCOPE_TO_NAME.get(msg.get("scope"))}'
+
+ message += self._format_ifa_flags(msg.get_attr("IFA_FLAGS"), msg.get("family"))
+ message += self._format_simple_field(msg.get_attr('IFA_LABEL'), prefix='label:')
+
+ if msg.get_attr('IFA_CACHEINFO'):
+ message += self._format_ifa_cacheinfo(msg.get_attr('IFA_CACHEINFO'))
+
+ return self._output(message)
+
+
+class NeighFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling neighbor-related network messages
+ `RTM_NEWNEIGH`, `RTM_DELNEIGH` and `RTM_GETNEIGH`
+ """
+ def _format_ntf_flags(self, flags: int) -> str:
+ """
+ Formats neighbor table entry flags into a human-readable string.
+ """
+ result = list()
+ result.extend(self._parse_flag(flags, NTF_FlAGS))
+
+ return self._output(",".join(result))
+
+ def _format_neigh_state(self, data: int) -> str:
+ """
+ Formats the state of a neighbor entry.
+ """
+ result = list()
+ result.extend(self._parse_flag(data, NEIGH_STATE_FLAGS))
+
+ return self._output(",".join(result))
+
+ def format(self, msg: ndmsg.ndmsg) -> str:
+ """
+ Formats a full neighbor-related network message.
+ Combine attributes such as destination, device, link-layer address,
+ flags, state, and protocol into a structured output string.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+ message += self._format_simple_field(msg.get_attr('NDA_DST'), prefix='')
+
+ if msg.get("ifindex") is not None:
+ message += f' dev {self._get_if_name_by_index(msg.get("ifindex"))}'
+
+ message += self._format_simple_field(msg.get_attr('NDA_LLADDR'), prefix='lladdr')
+ message += f' {self._format_ntf_flags(msg.get("flags"))}'
+ message += f' {self._format_neigh_state(msg.get("state"))}'
+
+ if msg.get_attr('NDA_PROTOCOL'):
+ message += f' proto {RT_PROTO.get(msg.get_attr("NDA_PROTOCOL"), msg.get_attr("NDA_PROTOCOL"))}'
+
+ return self._output(message)
+
+
+class RuleFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling ruting tule network messages
+ `RTM_NEWRULE` and `RTM_DELRULE`
+ """
+ def _format_direction(self, data: str, length: int, host_len: int):
+ """
+ Formats the direction of traffic based on source or destination and prefix length.
+ """
+ result = ''
+ if data:
+ result += f' {data}'
+ if length != host_len:
+ result += f'/{length}'
+ elif length:
+ result += f' 0/{length}'
+
+ return self._output(result)
+
+ def _format_fra_interface(self, data: str, flags: int, prefix: str):
+ """
+ Formats interface-related attributes.
+ """
+ result = f'{prefix} {data}'
+ if flags & FIB_RULE_IIF_DETACHED:
+ result += '[detached]'
+
+ return self._output(result)
+
+ def _format_fra_range(self, data: [str, dict], prefix: str):
+ """
+ Formats a range of values (e.g., UID, sport, or dport).
+ """
+ result = ''
+ if data:
+ if isinstance(data, str):
+ result += f' {prefix} {data}'
+ else:
+ result += f' {prefix} {data.get("start")}:{data.get("end")}'
+ return self._output(result)
+
+ def _format_fra_table(self, msg: fibmsg):
+ """
+ Formats the lookup table and associated attributes in the message.
+ """
+ def __format_field(data: int, prefix: str):
+ if data and data not in [-1, _INFINITY]:
+ return f' {prefix} {data}'
+ return ''
+
+ result = ''
+ table = msg.get_attr('FRA_TABLE') or msg.get('table')
+ if table:
+ result += f' lookup {self._format_rttable(table)}'
+ result += __format_field(msg.get_attr('FRA_SUPPRESS_PREFIXLEN'), 'suppress_prefixlength')
+ result += __format_field(msg.get_attr('FRA_SUPPRESS_IFGROUP'), 'suppress_ifgroup')
+
+ return self._output(result)
+
+ def _format_fra_action(self, msg: fibmsg):
+ """
+ Formats the action associated with the rule.
+ """
+ result = ''
+ if msg.get('action') == RTYPES.get('RTN_NAT'):
+ if msg.get_attr('RTA_GATEWAY'): # looks like deprecated but still use in iproute2
+ result += f' map-to {msg.get_attr("RTA_GATEWAY")}'
+ else:
+ result += ' masquerade'
+
+ elif msg.get('action') == FR_ACT_GOTO:
+ result += f' goto {msg.get_attr("FRA_GOTO") or "none"}'
+ if msg.get('flags') & FIB_RULE_UNRESOLVED:
+ result += ' [unresolved]'
+
+ elif msg.get('action') == FR_ACT_NOP:
+ result += ' nop'
+
+ elif msg.get('action') != FR_ACT_TO_TBL:
+ result += f' {RTYPES.get(msg.get("action"))}'
+
+ return self._output(result)
+
+ def format(self, msg: fibmsg):
+ """
+ Formats a complete routing rule message.
+ Combines information about source, destination, interfaces, actions,
+ and other attributes into a single formatted string.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+ host_len = self.af_bit_len(msg.get('family'))
+ message += self._format_simple_field(msg.get_attr('FRA_PRIORITY'), prefix='')
+
+ if msg.get('flags') & FIB_RULE_INVERT:
+ message += ' not'
+
+ tmp = self._format_direction(msg.get_attr('FRA_SRC'), msg.get('src_len'), host_len)
+ message += ' from' + (tmp if tmp else ' all ')
+
+ if msg.get_attr('FRA_DST'):
+ tmp = self._format_direction(msg.get_attr('FRA_DST'), msg.get('dst_len'), host_len)
+ message += ' to' + tmp
+
+ if msg.get('tos'):
+ message += f' tos {hex(msg.get("tos"))}'
+
+ if msg.get_attr('FRA_FWMARK') or msg.get_attr('FRA_FWMASK'):
+ mark = msg.get_attr('FRA_FWMARK') or 0
+ mask = msg.get_attr('FRA_FWMASK') or 0
+ if mask != 0xFFFFFFFF:
+ message += f' fwmark {mark}/{mask}'
+ else:
+ message += f' fwmark {mark}'
+
+ if msg.get_attr('FRA_IIFNAME'):
+ message += self._format_fra_interface(
+ msg.get_attr('FRA_IIFNAME'),
+ msg.get('flags'),
+ 'iif'
+ )
+
+ if msg.get_attr('FRA_OIFNAME'):
+ message += self._format_fra_interface(
+ msg.get_attr('FRA_OIFNAME'),
+ msg.get('flags'),
+ 'oif'
+ )
+
+ if msg.get_attr('FRA_L3MDEV'):
+ message += f' lookup [l3mdev-table]'
+
+ if msg.get_attr('FRA_UID_RANGE'):
+ message += self._format_fra_range(msg.get_attr('FRA_UID_RANGE'), 'uidrange')
+
+ message += self._format_simple_field(msg.get_attr('FRA_IP_PROTO'), prefix='ipproto')
+
+ if msg.get_attr('FRA_SPORT_RANGE'):
+ message += self._format_fra_range(msg.get_attr('FRA_SPORT_RANGE'), 'sport')
+
+ if msg.get_attr('FRA_DPORT_RANGE'):
+ message += self._format_fra_range(msg.get_attr('FRA_DPORT_RANGE'), 'dport')
+
+ message += self._format_simple_field(msg.get_attr('FRA_TUN_ID'), prefix='tun_id')
+
+ message += self._format_fra_table(msg)
+
+ if msg.get_attr('FRA_FLOW'):
+ message += self._format_flow_field(msg.get_attr('FRA_FLOW'))
+
+ message += self._format_fra_action(msg)
+
+ if msg.get_attr('FRA_PROTOCOL'):
+ message += f' proto {RT_PROTO.get(msg.get_attr("FRA_PROTOCOL"), msg.get_attr("FRA_PROTOCOL"))}'
+
+ return self._output(message)
+
+
+class AddrlabelFormatter(BaseMSGFormatter):
+ # Not implemented decoder on pytroute2 but ip monitor use it message
+ pass
+
+
+class PrefixFormatter(BaseMSGFormatter):
+ # Not implemented decoder on pytroute2 but ip monitor use it message
+ pass
+
+
+class NetconfFormatter(BaseMSGFormatter):
+ # Not implemented decoder on pytroute2 but ip monitor use it message
+ pass
+
+
+EVENT_MAP = {
+ rtnl.RTM_NEWROUTE: {'parser': RouteFormatter, 'event': 'route'},
+ rtnl.RTM_DELROUTE: {'parser': RouteFormatter, 'event': 'route'},
+ rtnl.RTM_NEWLINK: {'parser': LinkFormatter, 'event': 'link'},
+ rtnl.RTM_DELLINK: {'parser': LinkFormatter, 'event': 'link'},
+ rtnl.RTM_NEWADDR: {'parser': AddrFormatter, 'event': 'addr'},
+ rtnl.RTM_DELADDR: {'parser': AddrFormatter, 'event': 'addr'},
+ # rtnl.RTM_NEWADDRLABEL: {'parser': AddrlabelFormatter, 'event': 'addrlabel'},
+ # rtnl.RTM_DELADDRLABEL: {'parser': AddrlabelFormatter, 'event': 'addrlabel'},
+ rtnl.RTM_NEWNEIGH: {'parser': NeighFormatter, 'event': 'neigh'},
+ rtnl.RTM_DELNEIGH: {'parser': NeighFormatter, 'event': 'neigh'},
+ rtnl.RTM_GETNEIGH: {'parser': NeighFormatter, 'event': 'neigh'},
+ # rtnl.RTM_NEWPREFIX: {'parser': PrefixFormatter, 'event': 'prefix'},
+ rtnl.RTM_NEWRULE: {'parser': RuleFormatter, 'event': 'rule'},
+ rtnl.RTM_DELRULE: {'parser': RuleFormatter, 'event': 'rule'},
+ # rtnl.RTM_NEWNETCONF: {'parser': NetconfFormatter, 'event': 'netconf'},
+ # rtnl.RTM_DELNETCONF: {'parser': NetconfFormatter, 'event': 'netconf'},
+}
+
+
+def sig_handler(signum, frame):
+ process_name = multiprocessing.current_process().name
+ logger.debug(
+ f'[{process_name}]: {"Shutdown" if signum == signal.SIGTERM else "Reload"} signal received...'
+ )
+ shutdown_event.set()
+
+
+def parse_event_type(header: Dict) -> tuple:
+ """
+ Extract event type and parser.
+ """
+ event_type = EVENT_MAP.get(header['type'], {}).get('event', 'unknown')
+ _parser = EVENT_MAP.get(header['type'], {}).get('parser')
+
+ if _parser is None:
+ raise UnsupportedMessageType(f'Unsupported message type: {header["type"]}')
+
+ return event_type, _parser
+
+
+def is_need_to_log(event_type: AnyStr, conf_event: Dict):
+ """
+ Filter message by event type and protocols
+ """
+ conf = conf_event.get(event_type)
+ if conf == {}:
+ return True
+ return False
+
+
+def parse_event(msg: nfct_msg, conf_event: Dict) -> str:
+ """
+ Convert nfct_msg to internal data dict.
+ """
+ data = ''
+ event_type, parser = parse_event_type(msg['header'])
+ if event_type == 'link':
+ remember_if_index(idx=msg.get('index'), event_type=msg['header'].get('type'))
+
+ if not is_need_to_log(event_type, conf_event):
+ return data
+
+ message = parser().format(msg)
+ if message:
+ data = f'{f"[{event_type}]".upper():<{7}} {message}'
+
+ return data
+
+
+def worker(ct: IPRoute, shutdown_event: multiprocessing.Event, conf_event: Dict) -> None:
+ """
+ Main function of parser worker process
+ """
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}] started')
+ timeout = 0.1
+ while not shutdown_event.is_set():
+ if not ct.buffer_queue.empty():
+ msg = None
+ try:
+ for msg in ct.get():
+ message = parse_event(msg, conf_event)
+ if message:
+ if logger.level == logging.DEBUG:
+ logger.debug(f'[{process_name}]: {message} raw: {msg}')
+ else:
+ logger.info(message)
+ except queue.Full:
+ logger.error('IPRoute message queue if full.')
+ except UnsupportedMessageType as e:
+ logger.debug(f'{e} =====> raw msg: {msg}')
+ except Exception as e:
+ logger.error(f'Unexpected error: {e.__class__} {e} [{msg}]')
+ else:
+ sleep(timeout)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '-c',
+ '--config',
+ action='store',
+ help='Path to vyos-network-event-logger configuration',
+ required=True,
+ type=Path,
+ )
+
+ args = parser.parse_args()
+ try:
+ config = read_json(args.config)
+ except Exception as err:
+ logger.error(f'Configuration file "{args.config}" does not exist or malformed: {err}')
+ exit(1)
+
+ set_log_level(config.get('log_level', 'info'))
+
+ signal.signal(signal.SIGHUP, sig_handler)
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ if 'event' in config:
+ event_groups = list(config.get('event').keys())
+ else:
+ logger.error(f'Configuration is wrong. Event filter is empty.')
+ exit(1)
+
+ conf_event = config['event']
+ qsize = config.get('queue_size')
+ ct = IPRoute(async_qsize=int(qsize) if qsize else None)
+ ct.buffer_queue = multiprocessing.Queue(ct.async_qsize)
+ ct.bind(async_cache=True)
+
+ processes = list()
+ try:
+ for _ in range(multiprocessing.cpu_count()):
+ p = multiprocessing.Process(target=worker, args=(ct, shutdown_event, conf_event))
+ processes.append(p)
+ p.start()
+ logger.info('IPRoute socket bound and listening for messages.')
+
+ while not shutdown_event.is_set():
+ if not ct.pthread.is_alive():
+ if ct.buffer_queue.qsize() / ct.async_qsize < 0.9:
+ if not shutdown_event.is_set():
+ logger.debug('Restart listener thread')
+ # restart listener thread after queue overloaded when queue size low than 90%
+ ct.pthread = threading.Thread(name='Netlink async cache', target=ct.async_recv)
+ ct.pthread.daemon = True
+ ct.pthread.start()
+ else:
+ sleep(0.1)
+ finally:
+ for p in processes:
+ p.join()
+ if not p.is_alive():
+ logger.debug(f'[{p.name}]: finished')
+ ct.close()
+ logging.info('IPRoute socket closed.')
+ exit()
diff --git a/src/systemd/vyos-network-event-logger.service b/src/systemd/vyos-network-event-logger.service
new file mode 100644
index 000000000..990dc43ba
--- /dev/null
+++ b/src/systemd/vyos-network-event-logger.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=VyOS network-event logger daemon
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-configd needs is read/write mounted root
+After=vyos.target
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-network-event-logger -c /run/vyos-network-event-logger.conf
+Type=idle
+
+SyslogIdentifier=vyos-network-event-logger
+SyslogFacility=daemon
+
+Restart=on-failure
+
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/tests/test_configd_inspect.py b/src/tests/test_configd_inspect.py
index ccd631893..a0470221d 100644
--- a/src/tests/test_configd_inspect.py
+++ b/src/tests/test_configd_inspect.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2020-2024 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
@@ -12,93 +12,151 @@
# 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 re
+import ast
import json
-import warnings
-import importlib.util
-from inspect import signature
-from inspect import getsource
-from functools import wraps
from unittest import TestCase
INC_FILE = 'data/configd-include.json'
CONF_DIR = 'src/conf_mode'
-f_list = ['get_config', 'verify', 'generate', 'apply']
-
-def import_script(s):
- path = os.path.join(CONF_DIR, s)
- name = os.path.splitext(s)[0].replace('-', '_')
- spec = importlib.util.spec_from_file_location(name, path)
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module
-
-# importing conf_mode scripts imports jinja2 with deprecation warning
-def ignore_deprecation_warning(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- f(*args, **kwargs)
- return decorated_function
+funcs = ['get_config', 'verify', 'generate', 'apply']
+
+
+class FunctionSig(ast.NodeVisitor):
+ def __init__(self):
+ self.func_sig_len = dict.fromkeys(funcs, None)
+ self.get_config_default_values = []
+
+ def visit_FunctionDef(self, node):
+ func_name = node.name
+ if func_name in funcs:
+ self.func_sig_len[func_name] = len(node.args.args)
+
+ if func_name == 'get_config':
+ for default in node.args.defaults:
+ if isinstance(default, ast.Constant):
+ self.get_config_default_values.append(default.value)
+
+ self.generic_visit(node)
+
+ def get_sig_lengths(self):
+ return self.func_sig_len
+
+ def get_config_default(self):
+ return self.get_config_default_values[0]
+
+
+class LegacyCall(ast.NodeVisitor):
+ def __init__(self):
+ self.legacy_func_count = 0
+
+ def visit_Constant(self, node):
+ value = node.value
+ if isinstance(value, str):
+ if 'my_set' in value or 'my_delete' in value:
+ self.legacy_func_count += 1
+
+ self.generic_visit(node)
+
+ def get_legacy_func_count(self):
+ return self.legacy_func_count
+
+
+class ConfigInstance(ast.NodeVisitor):
+ def __init__(self):
+ self.count = 0
+
+ def visit_Call(self, node):
+ if isinstance(node.func, ast.Name):
+ name = node.func.id
+ if name == 'Config':
+ self.count += 1
+ self.generic_visit(node)
+
+ def get_count(self):
+ return self.count
+
+
+class FunctionConfigInstance(ast.NodeVisitor):
+ def __init__(self):
+ self.func_config_instance = dict.fromkeys(funcs, 0)
+
+ def visit_FunctionDef(self, node):
+ func_name = node.name
+ if func_name in funcs:
+ config_instance = ConfigInstance()
+ config_instance.visit(node)
+ self.func_config_instance[func_name] = config_instance.get_count()
+ self.generic_visit(node)
+
+ def get_func_config_instance(self):
+ return self.func_config_instance
+
class TestConfigdInspect(TestCase):
def setUp(self):
+ self.ast_list = []
+
with open(INC_FILE) as f:
self.inc_list = json.load(f)
- @ignore_deprecation_warning
- def test_signatures(self):
for s in self.inc_list:
- m = import_script(s)
- for i in f_list:
- f = getattr(m, i, None)
- self.assertIsNotNone(f, f"'{s}': missing function '{i}'")
- sig = signature(f)
- par = sig.parameters
- l = len(par)
- self.assertEqual(l, 1,
- f"'{s}': '{i}' incorrect signature")
- if i == 'get_config':
- for p in par.values():
- self.assertTrue(p.default is None,
- f"'{s}': '{i}' incorrect signature")
-
- @ignore_deprecation_warning
- def test_function_instance(self):
- for s in self.inc_list:
- m = import_script(s)
- for i in f_list:
- f = getattr(m, i, None)
- if not f:
- continue
- str_f = getsource(f)
- # Regex not XXXConfig() T3108
- n = len(re.findall(r'[^a-zA-Z]Config\(\)', str_f))
- if i == 'get_config':
- self.assertEqual(n, 1,
- f"'{s}': '{i}' no instance of Config")
- if i != 'get_config':
- self.assertEqual(n, 0,
- f"'{s}': '{i}' instance of Config")
-
- @ignore_deprecation_warning
- def test_file_instance(self):
- for s in self.inc_list:
- m = import_script(s)
- str_m = getsource(m)
- # Regex not XXXConfig T3108
- n = len(re.findall(r'[^a-zA-Z]Config\(\)', str_m))
- self.assertEqual(n, 1,
- f"'{s}' more than one instance of Config")
-
- @ignore_deprecation_warning
+ s_path = f'{CONF_DIR}/{s}'
+ with open(s_path) as f:
+ s_str = f.read()
+ s_tree = ast.parse(s_str)
+ self.ast_list.append((s, s_tree))
+
+ def test_signatures(self):
+ for s, t in self.ast_list:
+ visitor = FunctionSig()
+ visitor.visit(t)
+ sig_lens = visitor.get_sig_lengths()
+
+ for f in funcs:
+ self.assertIsNotNone(sig_lens[f], f"'{s}': '{f}' missing")
+ self.assertEqual(sig_lens[f], 1, f"'{s}': '{f}' incorrect signature")
+
+ self.assertEqual(
+ visitor.get_config_default(),
+ None,
+ f"'{s}': 'get_config' incorrect signature",
+ )
+
+ def test_file_config_instance(self):
+ for s, t in self.ast_list:
+ visitor = ConfigInstance()
+ visitor.visit(t)
+ count = visitor.get_count()
+
+ self.assertEqual(count, 1, f"'{s}' more than one instance of Config")
+
+ def test_function_config_instance(self):
+ for s, t in self.ast_list:
+ visitor = FunctionConfigInstance()
+ visitor.visit(t)
+ func_config_instance = visitor.get_func_config_instance()
+
+ for f in funcs:
+ if f == 'get_config':
+ self.assertTrue(
+ func_config_instance[f] > 0,
+ f"'{s}': '{f}' no instance of Config",
+ )
+ self.assertTrue(
+ func_config_instance[f] < 2,
+ f"'{s}': '{f}' more than one instance of Config",
+ )
+ else:
+ self.assertEqual(
+ func_config_instance[f], 0, f"'{s}': '{f}' instance of Config"
+ )
+
def test_config_modification(self):
- for s in self.inc_list:
- m = import_script(s)
- str_m = getsource(m)
- n = str_m.count('my_set')
- self.assertEqual(n, 0, f"'{s}' modifies config")
+ for s, t in self.ast_list:
+ visitor = LegacyCall()
+ visitor.visit(t)
+ legacy_func_count = visitor.get_legacy_func_count()
+
+ self.assertEqual(legacy_func_count, 0, f"'{s}' modifies config")