diff options
483 files changed, 16040 insertions, 4671 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cd348ead7..caabab3d9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ <!-- All PR should follow this template to allow a clean and transparent review --> <!-- Text placed between these delimiters is considered a comment and is not rendered --> -## Change Summary +## Change summary <!--- Provide a general summary of your changes in the Title above --> ## Types of changes @@ -24,13 +24,7 @@ the box, please use [x] ## Related PR(s) <!-- Link here any PRs in other repositories that are required by this PR --> -## Component(s) name -<!-- A rather incomplete list of components: ethernet, wireguard, bgp, mpls, ldp, l2tp, dhcp ... --> - -## Proposed changes -<!--- Describe your changes in detail --> - -## How to test +## How to test / Smoketest result <!--- Please describe in detail how you tested your changes. Include details of your testing environment, and the tests you ran. When pasting configs, logs, shell output, backtraces, @@ -38,10 +32,9 @@ and other large chunks of text, surround this text with triple backtics ``` like this ``` ---> -## Smoketest result -<!-- Provide the output of the smoketest +Or provide the output of the smoketest + ``` $ /usr/libexec/vyos/tests/smoke/cli/test_xxx_feature.py test_01_simple_options (__main__.TestFeature.test_01_simple_options) ... ok 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..120e116d4 --- /dev/null +++ b/.github/workflows/mirror-pr-and-sync.yml @@ -0,0 +1,21 @@ +name: Create Mirror PR and Repo Sync +on: + workflow_dispatch: + inputs: + 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: + sync_branch: ${{ inputs.sync_branch }} + secrets: + PAT: ${{ secrets.PAT }} diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml index d352bd3cb..5ed764217 100644 --- a/.github/workflows/package-smoketest.yml +++ b/.github/workflows/package-smoketest.yml @@ -24,6 +24,7 @@ jobs: build_iso: runs-on: ubuntu-24.04 timeout-minutes: 45 + if: github.repository == 'vyos/vyos-1x' container: image: vyos/vyos-build:current options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged @@ -41,6 +42,7 @@ jobs: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} + submodules: true - name: Build vyos-1x package run: | cd packages/vyos-1x; dpkg-buildpackage -uc -us -tc -b @@ -63,8 +65,11 @@ jobs: generic - uses: actions/upload-artifact@v4 with: + retention-days: 2 name: vyos-${{ steps.version.outputs.build_version }} - path: build/live-image-amd64.hybrid.iso + path: | + build/live-image-amd64.hybrid.iso + build/manifest.json test_smoketest_cli: needs: build_iso diff --git a/.github/workflows/trigger-pr-mirror-repo-sync.yml b/.github/workflows/trigger-pr-mirror-repo-sync.yml index d5e8ce3b4..978be0582 100644 --- a/.github/workflows/trigger-pr-mirror-repo-sync.yml +++ b/.github/workflows/trigger-pr-mirror-repo-sync.yml @@ -6,7 +6,13 @@ on: branches: - current +permissions: + pull-requests: write + contents: write + issues: write + jobs: call-trigger-mirror-pr-repo-sync: + if: github.repository_owner == 'vyos' && github.event.pull_request.merged == true uses: vyos/.github/.github/workflows/trigger-pr-mirror-repo-sync.yml@current secrets: inherit diff --git a/.gitignore b/.gitignore index d1bfc91d7..839d2afff 100644 --- a/.gitignore +++ b/.gitignore @@ -151,6 +151,11 @@ data/reftree.cache # autogenerated vyos-configd JSON definition data/configd-include.json +# autogenerated vyos-commitd protobuf files +python/vyos/proto/*pb2.py +python/vyos/proto/*.desc +python/vyos/proto/vyconf_proto.py + # We do not use pip Pipfile Pipfile.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..05eaf619f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "libvyosconfig"] + path = libvyosconfig + url = ../../vyos/libvyosconfig + branch = current diff --git a/CODEOWNERS b/CODEOWNERS index 4891a0325..0bf2e6d79 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # Users from reviewers github team -* @dmbaturin @sarthurdev @jestabro @sever-sever @c-po @fett0 @nicolas-fort @zdc +# * @vyos/reviewers @@ -7,7 +7,9 @@ LIBS := -lzmq CFLAGS := BUILD_ARCH := $(shell dpkg-architecture -q DEB_BUILD_ARCH) J2LINT := $(shell command -v j2lint 2> /dev/null) -PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts) +PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts src/services) +LIBVYOSCONFIG_BUILD_PATH := /tmp/libvyosconfig/_build/libvyosconfig.so +LIBVYOSCONFIG_STATUS := $(shell git submodule status) config_xml_src = $(wildcard interface-definitions/*.xml.in) config_xml_obj = $(config_xml_src:.xml.in=.xml) @@ -19,9 +21,21 @@ op_xml_obj = $(op_xml_src:.xml.in=.xml) mkdir -p $(BUILD_DIR)/$(dir $@) $(CURDIR)/scripts/transclude-template $< > $(BUILD_DIR)/$@ +.PHONY: libvyosconfig +.ONESHELL: +libvyosconfig: + if test ! -f $(LIBVYOSCONFIG_BUILD_PATH); then + if ! echo $(firstword $(LIBVYOSCONFIG_STATUS))|grep -Eq '^[a-z0-9]'; then + git submodule sync; git submodule update --init --remote + fi + rm -rf /tmp/libvyosconfig && mkdir /tmp/libvyosconfig + cp -r libvyosconfig /tmp && cd /tmp/libvyosconfig && \ + eval $$(opam env --root=/opt/opam --set-root) && ./build.sh || exit 1 + fi + .PHONY: interface_definitions .ONESHELL: -interface_definitions: $(config_xml_obj) +interface_definitions: $(config_xml_obj) libvyosconfig mkdir -p $(TMPL_DIR) $(CURDIR)/scripts/override-default $(BUILD_DIR)/interface-definitions @@ -75,7 +89,7 @@ vyshim: $(MAKE) -C $(SHIM_DIR) .PHONY: all -all: clean interface_definitions op_mode_definitions test j2lint vyshim generate-configd-include-json +all: clean libvyosconfig interface_definitions op_mode_definitions test j2lint vyshim generate-configd-include-json .PHONY: clean clean: diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index cbd14f7c6..7506a0908 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -14,6 +14,9 @@ "vxlan": ["interfaces_vxlan"], "wlan": ["interfaces_wireless"] }, + "interfaces_wireguard": { + "vxlan": ["interfaces_vxlan"] + }, "load_balancing_wan": { "conntrack": ["system_conntrack"] }, diff --git a/data/config.boot.default b/data/config.boot.default index 93369d9b7..db5d11ea1 100644 --- a/data/config.boot.default +++ b/data/config.boot.default @@ -41,7 +41,7 @@ system { } } syslog { - global { + local { facility all { level "info" } diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 35587b63c..5d3f4a249 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -13,13 +13,13 @@ "evpn.py", "interfaces.py", "ipsec.py", +"load-balancing_wan.py", "lldp.py", "log.py", "memory.py", "multicast.py", "nat.py", "neighbor.py", -"nhrp.py", "openconnect.py", "openvpn.py", "otp.py", @@ -28,6 +28,7 @@ "load-balancing_haproxy.py", "route.py", "storage.py", +"stp.py", "system.py", "uptime.py", "version.py", diff --git a/data/templates/accel-ppp/chap-secrets.ipoe.j2 b/data/templates/accel-ppp/chap-secrets.ipoe.j2 index dd85160c0..59b9dfc8d 100644 --- a/data/templates/accel-ppp/chap-secrets.ipoe.j2 +++ b/data/templates/accel-ppp/chap-secrets.ipoe.j2 @@ -6,7 +6,7 @@ {% if mac_config.vlan is vyos_defined %} {% set iface = iface ~ '.' ~ mac_config.vlan %} {% endif %} -{{ "%-11s" | format(iface) }} * {{ mac | lower }} {{ mac_config.static_ip if mac_config.static_ip is vyos_defined else '*' }} {{ mac_config.rate_limit.download ~ '/' ~ mac_config.rate_limit.upload if mac_config.rate_limit.download is vyos_defined and mac_config.rate_limit.upload is vyos_defined }} +{{ "%-11s" | format(iface) }} * {{ mac | lower }} {{ mac_config.ip_address if mac_config.ip_address is vyos_defined else '*' }} {{ mac_config.rate_limit.download ~ '/' ~ mac_config.rate_limit.upload if mac_config.rate_limit.download is vyos_defined and mac_config.rate_limit.upload is vyos_defined }} {% endfor %} {% endif %} {% endfor %} diff --git a/data/templates/conserver/dropbear@.service.j2 b/data/templates/conserver/dropbear@.service.j2 index e355dab43..c6c31f98f 100644 --- a/data/templates/conserver/dropbear@.service.j2 +++ b/data/templates/conserver/dropbear@.service.j2 @@ -1,4 +1,4 @@ [Service] ExecStart= -ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I +ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -r /etc/dropbear/dropbear_ecdsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I PIDFile=/run/conserver/dropbear.%I.pid diff --git a/data/templates/container/containers.conf.j2 b/data/templates/container/containers.conf.j2 index c8b54dfbb..65436801e 100644 --- a/data/templates/container/containers.conf.j2 +++ b/data/templates/container/containers.conf.j2 @@ -172,7 +172,11 @@ default_sysctls = [ # Logging driver for the container. Available options: k8s-file and journald. # +{% if log_driver is vyos_defined %} +log_driver = "{{ log_driver }}" +{% else %} #log_driver = "k8s-file" +{% endif %} # Maximum size allowed for the container log file. Negative numbers indicate # that no size limit is imposed. If positive, it must be >= 8192 to match or diff --git a/data/templates/container/registries.conf.j2 b/data/templates/container/registries.conf.j2 index eb7ff8775..b5c7eed9b 100644 --- a/data/templates/container/registries.conf.j2 +++ b/data/templates/container/registries.conf.j2 @@ -28,4 +28,14 @@ {% set _ = registry_list.append(r) %} {% endfor %} unqualified-search-registries = {{ registry_list }} +{% for r, r_options in registry.items() if r_options.disable is not vyos_defined %} +[[registry]] +{% if r_options.mirror is vyos_defined %} +location = "{{ r_options.mirror.host_name if r_options.mirror.host_name is vyos_defined else r_options.mirror.address }}{{ ":" + r_options.mirror.port if r_options.mirror.port is vyos_defined }}{{ r_options.mirror.path if r_options.mirror.path is vyos_defined }}" +{% else %} +location = "{{ r }}" +{% endif %} +insecure = {{ 'true' if r_options.insecure is vyos_defined else 'false' }} +prefix = "{{ r }}" +{% endfor %} {% endif %} diff --git a/data/templates/dhcp-client/ipv6.override.conf.j2 b/data/templates/dhcp-client/ipv6.override.conf.j2 index b0c0e0544..d270a55fc 100644 --- a/data/templates/dhcp-client/ipv6.override.conf.j2 +++ b/data/templates/dhcp-client/ipv6.override.conf.j2 @@ -4,6 +4,9 @@ [Unit] ConditionPathExists={{ dhcp6_client_dir }}/dhcp6c.%i.conf +{% if ifname.startswith('pppoe') %} +After=ppp@{{ ifname }}.service +{% endif %} [Service] ExecStart= diff --git a/data/templates/dhcp-server/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/dhcp-server/kea-ctrl-agent.conf.j2 b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 deleted file mode 100644 index b37cf4798..000000000 --- a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Control-agent": { -{% if high_availability is vyos_defined %} - "http-host": "{{ high_availability.source_address }}", - "http-port": 647, - "control-sockets": { - "dhcp4": { - "socket-type": "unix", - "socket-name": "/run/kea/dhcp4-ctrl-socket" - } - } -{% endif %} - } -} diff --git a/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2 b/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2 new file mode 100644 index 000000000..7b0394a88 --- /dev/null +++ b/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2 @@ -0,0 +1,30 @@ +{ + "DhcpDdns": { + "ip-address": "127.0.0.1", + "port": 53001, + "control-socket": { + "socket-type": "unix", + "socket-name": "/run/kea/kea-ddns-ctrl-socket" + }, + "tsig-keys": {{ dynamic_dns_update | kea_dynamic_dns_update_tsig_key_json }}, + "forward-ddns" : { + "ddns-domains": {{ dynamic_dns_update | kea_dynamic_dns_update_domains('forward_domain') }} + }, + "reverse-ddns" : { + "ddns-domains": {{ dynamic_dns_update | kea_dynamic_dns_update_domains('reverse_domain') }} + }, + "loggers": [ + { + "name": "kea-dhcp-ddns", + "output_options": [ + { + "output": "stdout", + "pattern": "%-5p %m\n" + } + ], + "severity": "INFO", + "debuglevel": 0 + } + ] + } +} diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2 index 2e10d58e0..d08ca0eaa 100644 --- a/data/templates/dhcp-server/kea-dhcp4.conf.j2 +++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2 @@ -25,20 +25,6 @@ }, "option-def": [ { - "name": "rfc3442-static-route", - "code": 121, - "type": "record", - "array": true, - "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8" - }, - { - "name": "windows-static-route", - "code": 249, - "type": "record", - "array": true, - "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8" - }, - { "name": "wpad-url", "code": 252, "type": "string" @@ -50,6 +36,19 @@ "space": "ubnt" } ], +{% if dynamic_dns_update is vyos_defined %} + "dhcp-ddns": { + "enable-updates": true, + "server-ip": "127.0.0.1", + "server-port": 53001, + "sender-ip": "", + "sender-port": 0, + "max-queue-size": 1024, + "ncr-protocol": "UDP", + "ncr-format": "JSON" + }, + {{ dynamic_dns_update | kea_dynamic_dns_update_main_json }} +{% endif %} "hooks-libraries": [ {% if high_availability is vyos_defined %} { @@ -69,6 +68,16 @@ }, {% endif %} { + "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_ping_check.so", + "parameters": { + "enable-ping-check" : false, + "min-ping-requests" : 1, + "reply-timeout" : 100, + "ping-cltt-secs" : 60, + "ping-channel-threads" : 0 + } + }, + { "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so", "parameters": {} } diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 5538ea56c..b209c8c81 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -21,11 +21,7 @@ if{{ ipv }}={{ address }}, \ {{ host }} {% endmacro %} ### Autogenerated by service_dns_dynamic.py ### -daemon={{ interval }} -syslog=yes ssl=yes -pid={{ config_file | replace('.conf', '.pid') }} -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 4a6851cef..aaed4ff35 100644 --- a/data/templates/dns-dynamic/override.conf.j2 +++ b/data/templates/dns-dynamic/override.conf.j2 @@ -1,10 +1,12 @@ {% 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= After=vyos-router.service [Service] -PIDFile={{ config_file | replace('.conf', '.pid') }} EnvironmentFile= ExecStart= -ExecStart={{ vrf_command }}/usr/bin/ddclient -file {{ config_file }} +ExecStart={{ vrf_command }}/usr/bin/ddclient --file {{ config_file }} --cache {{ cache_file }} --foreground --daemon {{ interval }} +Restart=always diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2 index fa6cd74c0..a1d1fa4f6 100644 --- a/data/templates/firewall/nftables-defines.j2 +++ b/data/templates/firewall/nftables-defines.j2 @@ -35,6 +35,24 @@ } {% endfor %} {% endif %} +{% if group.remote_group is vyos_defined and is_l3 and not is_ipv6 %} +{% for name, name_config in group.remote_group.items() %} + set R_{{ name }} { + type {{ ip_type }} + flags interval + auto-merge + } +{% endfor %} +{% endif %} +{% if group.remote_group is vyos_defined and is_l3 and is_ipv6 %} +{% for name, name_config in group.remote_group.items() %} + set R6_{{ name }} { + type {{ ip_type }} + flags interval + auto-merge + } +{% endfor %} +{% endif %} {% if group.mac_group is vyos_defined %} {% for group_name, group_conf in group.mac_group.items() %} {% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} diff --git a/data/templates/firewall/nftables-geoip-update.j2 b/data/templates/firewall/nftables-geoip-update.j2 index 832ccc3e9..d8f80d1f5 100644 --- a/data/templates/firewall/nftables-geoip-update.j2 +++ b/data/templates/firewall/nftables-geoip-update.j2 @@ -31,3 +31,36 @@ table ip6 vyos_filter { {% endfor %} } {% endif %} + + +{% if ipv4_sets_policy is vyos_defined %} +{% for setname, ip_list in ipv4_sets_policy.items() %} +flush set ip vyos_mangle {{ setname }} +{% endfor %} + +table ip vyos_mangle { +{% for setname, ip_list in ipv4_sets_policy.items() %} + set {{ setname }} { + type ipv4_addr + flags interval + elements = { {{ ','.join(ip_list) }} } + } +{% endfor %} +} +{% endif %} + +{% if ipv6_sets_policy is vyos_defined %} +{% for setname, ip_list in ipv6_sets_policy.items() %} +flush set ip6 vyos_mangle {{ setname }} +{% endfor %} + +table ip6 vyos_mangle { +{% for setname, ip_list in ipv6_sets_policy.items() %} + set {{ setname }} { + type ipv6_addr + flags interval + elements = { {{ ','.join(ip_list) }} } + } +{% endfor %} +} +{% endif %} diff --git a/data/templates/firewall/nftables-policy.j2 b/data/templates/firewall/nftables-policy.j2 index 9e28899b0..00d0e8a62 100644 --- a/data/templates/firewall/nftables-policy.j2 +++ b/data/templates/firewall/nftables-policy.j2 @@ -33,6 +33,15 @@ table ip vyos_mangle { {% endif %} } {% endfor %} + +{% if geoip_updated.name is vyos_defined %} +{% for setname in geoip_updated.name %} + set {{ setname }} { + type ipv4_addr + flags interval + } +{% endfor %} +{% endif %} {% endif %} {{ group_tmpl.groups(firewall_group, False, True) }} @@ -65,6 +74,14 @@ table ip6 vyos_mangle { {% endif %} } {% endfor %} +{% if geoip_updated.ipv6_name is vyos_defined %} +{% for setname in geoip_updated.ipv6_name %} + set {{ setname }} { + type ipv6_addr + flags interval + } +{% endfor %} +{% endif %} {% endif %} {{ group_tmpl.groups(firewall_group, True, True) }} diff --git a/data/templates/firewall/nftables-zone.j2 b/data/templates/firewall/nftables-zone.j2 index e78725079..645a38706 100644 --- a/data/templates/firewall/nftables-zone.j2 +++ b/data/templates/firewall/nftables-zone.j2 @@ -8,7 +8,14 @@ {% endif %} {% for zone_name, zone_conf in zone.items() %} {% if 'local_zone' not in zone_conf %} - oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }} +{% if 'interface' in zone_conf.member %} + oifname { {{ zone_conf.member.interface | join(',') }} } counter jump VZONE_{{ zone_name }} +{% endif %} +{% if 'vrf' in zone_conf.member %} +{% for vrf_name in zone_conf.member.vrf %} + oifname { {{ zone_conf['vrf_interfaces'][vrf_name] }} } counter jump VZONE_{{ zone_name }} +{% endfor %} +{% endif %} {% endif %} {% endfor %} } @@ -40,8 +47,15 @@ iifname lo counter return {% if zone_conf.from is vyos_defined %} {% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %} - iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} - iifname { {{ zone[from_zone].interface | join(",") }} } counter return + +{% if 'interface' in zone[from_zone].member %} + iifname { {{ zone[from_zone].member.interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} + iifname { {{ zone[from_zone].member.interface | join(",") }} } counter return +{% endif %} +{% if 'vrf' in zone[from_zone].member %} + iifname { {{ zone[from_zone].member.vrf | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} + iifname { {{ zone[from_zone].member.vrf | join(",") }} } counter return +{% endif %} {% endfor %} {% endif %} {{ zone_conf | nft_default_rule('zone_' + zone_name, family) }} @@ -50,23 +64,47 @@ oifname lo counter return {% if zone_conf.from_local is vyos_defined %} {% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall[fw_name] is vyos_defined %} - oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} - oifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% if 'interface' in zone[from_zone].member %} + oifname { {{ zone[from_zone].member.interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} + oifname { {{ zone[from_zone].member.interface | join(",") }} } counter return +{% endif %} +{% if 'vrf' in zone[from_zone].member %} +{% for vrf_name in zone[from_zone].member.vrf %} + oifname { {{ zone[from_zone]['vrf_interfaces'][vrf_name] }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} + oifname { {{ zone[from_zone]['vrf_interfaces'][vrf_name] }} } counter return +{% endfor %} +{% endif %} {% endfor %} {% endif %} {{ zone_conf | nft_default_rule('zone_' + zone_name, family) }} } {% else %} chain VZONE_{{ zone_name }} { - iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6) }} +{% if 'interface' in zone_conf.member %} + iifname { {{ zone_conf.member.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6) }} +{% endif %} +{% if 'vrf' in zone_conf.member %} + iifname { {{ zone_conf.member.vrf | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6) }} +{% endif %} {% if zone_conf.intra_zone_filtering is vyos_defined %} - iifname { {{ zone_conf.interface | join(",") }} } counter return +{% if 'interface' in zone_conf.member %} + iifname { {{ zone_conf.member.interface | join(",") }} } counter return +{% endif %} +{% if 'vrf' in zone_conf.member %} + iifname { {{ zone_conf.member.vrf | join(",") }} } counter return +{% endif %} {% endif %} {% if zone_conf.from is vyos_defined %} {% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %} {% if zone[from_zone].local_zone is not defined %} - iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} - iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% if 'interface' in zone[from_zone].member %} + iifname { {{ zone[from_zone].member.interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} + iifname { {{ zone[from_zone].member.interface | join(",") }} } counter return +{% endif %} +{% if 'vrf' in zone[from_zone].member %} + iifname { {{ zone[from_zone].member.vrf | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }} + iifname { {{ zone[from_zone].member.vrf | join(",") }} } counter return +{% endif %} {% endif %} {% endfor %} {% endif %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index a35143870..a78119a80 100755 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -47,7 +47,7 @@ table ip vyos_filter { chain VYOS_FORWARD_{{ prior }} { type filter hook forward priority {{ prior }}; policy accept; {% if global_options.state_policy is vyos_defined %} - jump VYOS_STATE_POLICY + jump VYOS_STATE_POLICY_FORWARD {% endif %} {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} @@ -180,6 +180,22 @@ table ip vyos_filter { {% endif %} return } + + chain VYOS_STATE_POLICY_FORWARD { +{% if global_options.state_policy.offload is vyos_defined %} + counter flow add @VYOS_FLOWTABLE_{{ global_options.state_policy.offload.offload_target }} +{% endif %} +{% if global_options.state_policy.established is vyos_defined %} + {{ global_options.state_policy.established | nft_state_policy('established') }} +{% endif %} +{% if global_options.state_policy.invalid is vyos_defined %} + {{ global_options.state_policy.invalid | nft_state_policy('invalid') }} +{% endif %} +{% if global_options.state_policy.related is vyos_defined %} + {{ global_options.state_policy.related | nft_state_policy('related') }} +{% endif %} + return + } {% endif %} } @@ -200,7 +216,7 @@ table ip6 vyos_filter { chain VYOS_IPV6_FORWARD_{{ prior }} { type filter hook forward priority {{ prior }}; policy accept; {% if global_options.state_policy is vyos_defined %} - jump VYOS_STATE_POLICY6 + jump VYOS_STATE_POLICY6_FORWARD {% endif %} {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} @@ -331,6 +347,22 @@ table ip6 vyos_filter { {% endif %} return } + + chain VYOS_STATE_POLICY6_FORWARD { +{% if global_options.state_policy.offload is vyos_defined %} + counter flow add @VYOS_FLOWTABLE_{{ global_options.state_policy.offload.offload_target }} +{% endif %} +{% if global_options.state_policy.established is vyos_defined %} + {{ global_options.state_policy.established | nft_state_policy('established') }} +{% endif %} +{% if global_options.state_policy.invalid is vyos_defined %} + {{ global_options.state_policy.invalid | nft_state_policy('invalid') }} +{% endif %} +{% if global_options.state_policy.related is vyos_defined %} + {{ global_options.state_policy.related | nft_state_policy('related') }} +{% endif %} + return + } {% endif %} } @@ -435,13 +467,13 @@ table bridge vyos_filter { {% if global_options.state_policy is vyos_defined %} chain VYOS_STATE_POLICY { {% if global_options.state_policy.established is vyos_defined %} - {{ global_options.state_policy.established | nft_state_policy('established') }} + {{ global_options.state_policy.established | nft_state_policy('established', bridge=True) }} {% endif %} {% if global_options.state_policy.invalid is vyos_defined %} - {{ global_options.state_policy.invalid | nft_state_policy('invalid') }} + {{ global_options.state_policy.invalid | nft_state_policy('invalid', bridge=True) }} {% endif %} {% if global_options.state_policy.related is vyos_defined %} - {{ global_options.state_policy.related | nft_state_policy('related') }} + {{ global_options.state_policy.related | nft_state_policy('related', bridge=True) }} {% endif %} return } diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index 51a3f2564..e153dd4e8 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -98,6 +98,8 @@ {% endif %} {% if config.enforce_first_as is vyos_defined %} neighbor {{ neighbor }} enforce-first-as +{% else %} + no neighbor {{ neighbor }} enforce-first-as {% endif %} {% if config.strict_capability_match is vyos_defined %} neighbor {{ neighbor }} strict-capability-match @@ -310,7 +312,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if afi_config.redistribute is vyos_defined %} {% for protocol, protocol_config in afi_config.redistribute.items() %} {% if protocol == 'table' %} - redistribute table {{ protocol_config.table }} +{% for table, table_config in protocol_config.items() %} + redistribute table-direct {{ table }} {{ 'metric ' ~ table_config.metric if table_config.metric is vyos_defined }} {{ 'route-map ' ~ table_config.route_map if table_config.route_map is vyos_defined }} +{% endfor %} {% else %} {% set redistribution_protocol = protocol %} {% if protocol == 'ospfv3' %} @@ -355,6 +359,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} import vrf {{ vrf }} {% endfor %} {% endif %} +{% if afi_config.route_map.vrf.import is vyos_defined %} + import vrf route-map {{ afi_config.route_map.vrf.import }} +{% endif %} {% if afi_config.label.vpn.export is vyos_defined %} label vpn export {{ afi_config.label.vpn.export }} {% endif %} 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/ldpd.frr.j2 b/data/templates/frr/ldpd.frr.j2 index 9a893cc55..b8fb0cfc7 100644 --- a/data/templates/frr/ldpd.frr.j2 +++ b/data/templates/frr/ldpd.frr.j2 @@ -82,8 +82,11 @@ mpls ldp {% endfor %} {% endif %} {% if ldp.interface is vyos_defined %} -{% for interface in ldp.interface %} +{% for interface, iface_config in ldp.interface.items() %} interface {{ interface }} +{% if iface_config.disable_establish_hello is vyos_defined %} + disable-establish-hello +{% endif %} exit {% endfor %} {% endif %} @@ -135,8 +138,11 @@ mpls ldp {% endfor %} {% endif %} {% if ldp.interface is vyos_defined %} -{% for interface in ldp.interface %} +{% for interface, iface_config in ldp.interface.items() %} interface {{ interface }} +{% if iface_config.disable_establish_hello is vyos_defined %} + disable-establish-hello +{% endif %} {% endfor %} {% endif %} exit-address-family diff --git a/data/templates/frr/nhrpd.frr.j2 b/data/templates/frr/nhrpd.frr.j2 new file mode 100644 index 000000000..813a9384b --- /dev/null +++ b/data/templates/frr/nhrpd.frr.j2 @@ -0,0 +1,60 @@ +! +{% 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 %} +! 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/frr/policy.frr.j2 b/data/templates/frr/policy.frr.j2 index ed5876ae9..c28633f6f 100644 --- a/data/templates/frr/policy.frr.j2 +++ b/data/templates/frr/policy.frr.j2 @@ -252,6 +252,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} {% if rule_config.match.rpki is vyos_defined %} match rpki {{ rule_config.match.rpki }} {% endif %} +{% if rule_config.match.source_vrf is vyos_defined %} + match source-vrf {{ rule_config.match.source_vrf }} +{% endif %} {% if rule_config.match.tag is vyos_defined %} match tag {{ rule_config.match.tag }} {% endif %} diff --git a/data/templates/frr/rpki.frr.j2 b/data/templates/frr/rpki.frr.j2 index 59d5bf0ac..edf0ccaa2 100644 --- a/data/templates/frr/rpki.frr.j2 +++ b/data/templates/frr/rpki.frr.j2 @@ -5,9 +5,9 @@ rpki {% for peer, peer_config in cache.items() %} {# port is mandatory and preference uses a default value #} {% if peer_config.ssh.username is vyos_defined %} - rpki cache ssh {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }} preference {{ peer_config.preference }} + rpki cache ssh {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }}{{ ' source ' ~ peer_config.source_address if peer_config.source_address is vyos_defined }} preference {{ peer_config.preference }} {% else %} - rpki cache tcp {{ peer | replace('_', '-') }} {{ peer_config.port }} preference {{ peer_config.preference }} + rpki cache tcp {{ peer | replace('_', '-') }} {{ peer_config.port }}{{ ' source ' ~ peer_config.source_address if peer_config.source_address is vyos_defined }} preference {{ peer_config.preference }} {% endif %} {% endfor %} {% endif %} diff --git a/data/templates/frr/staticd.frr.j2 b/data/templates/frr/staticd.frr.j2 index 90d17ec14..18d300dae 100644 --- a/data/templates/frr/staticd.frr.j2 +++ b/data/templates/frr/staticd.frr.j2 @@ -94,14 +94,14 @@ vrf {{ vrf }} {% if pppoe is vyos_defined %} {% for interface, interface_config in pppoe.items() if interface_config.no_default_route is not vyos_defined %} {{ ip_prefix }} route 0.0.0.0/0 {{ interface }} tag 210 {{ interface_config.default_route_distance if interface_config.default_route_distance is vyos_defined }} -{%- endfor %} +{% endfor %} {% endif %} {# IPv6 routing #} {% if route6 is vyos_defined %} {% for prefix, prefix_config in route6.items() %} {{ static_routes(ipv6_prefix, prefix, prefix_config) }} {# j2lint: disable=jinja-statements-delimeter #} -{%- endfor %} +{% endfor %} {% endif %} {% if vrf is vyos_defined %} exit-vrf diff --git a/data/templates/frr/zebra.segment_routing.frr.j2 b/data/templates/frr/zebra.segment_routing.frr.j2 index 7b12fcdd0..718d47d8f 100644 --- a/data/templates/frr/zebra.segment_routing.frr.j2 +++ b/data/templates/frr/zebra.segment_routing.frr.j2 @@ -11,6 +11,9 @@ segment-routing {% if locator_config.behavior_usid is vyos_defined %} behavior usid {% endif %} +{% if locator_config.format is vyos_defined %} + format {{ locator_config.format }} +{% endif %} exit ! {% endfor %} diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2 index 51da46946..692ccbff7 100644 --- a/data/templates/https/nginx.default.j2 +++ b/data/templates/https/nginx.default.j2 @@ -48,7 +48,7 @@ server { ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; # proxy settings for HTTP API, if enabled; 503, if not - location ~ ^/(retrieve|configure|config-file|image|import-pki|container-image|generate|show|reboot|reset|poweroff|traceroute|docs|openapi.json|redoc|graphql) { + location ~ ^/(retrieve|configure|config-file|image|import-pki|container-image|generate|show|reboot|reset|poweroff|traceroute|info|docs|openapi.json|redoc|graphql) { {% if api is vyos_defined %} proxy_pass http://unix:/run/api.sock; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/data/templates/ids/fastnetmon.j2 b/data/templates/ids/fastnetmon.j2 deleted file mode 100644 index f6f03d0db..000000000 --- a/data/templates/ids/fastnetmon.j2 +++ /dev/null @@ -1,121 +0,0 @@ -# enable this option if you want to send logs to local syslog facility -logging:logging_level = debug -logging:local_syslog_logging = on - -# list of all your networks in CIDR format -networks_list_path = /run/fastnetmon/networks_list - -# list networks in CIDR format which will be not monitored for attacks -white_list_path = /run/fastnetmon/excluded_networks_list - -# Enable/Disable any actions in case of attack -enable_ban = on -enable_ban_ipv6 = on - -## How many packets will be collected from attack traffic -ban_details_records_count = 500 - -## How long (in seconds) we should keep an IP in blocked state -## If you set 0 here it completely disables unban capability -{% if ban_time is vyos_defined %} -ban_time = {{ ban_time }} -{% endif %} - -# Check if the attack is still active, before triggering an unban callback with this option -# If the attack is still active, check each run of the unban watchdog -unban_only_if_attack_finished = on - -# enable per subnet speed meters -# For each subnet, list track speed in bps and pps for both directions -enable_subnet_counters = off - -{% if mode is vyos_defined('mirror') %} -mirror_afpacket = on -{% elif mode is vyos_defined('sflow') %} -sflow = on -{% if sflow.port is vyos_defined %} -sflow_port = {{ sflow.port }} -{% endif %} -{% if sflow.listen_address is vyos_defined %} -sflow_host = {{ sflow.listen_address }} -{% endif %} -{% endif %} - - -process_incoming_traffic = {{ 'on' if direction is vyos_defined and 'in' in direction else 'off' }} -process_outgoing_traffic = {{ 'on' if direction is vyos_defined and 'out' in direction else 'off' }} - -{% if threshold is vyos_defined %} -{% if threshold.general is vyos_defined %} -# General threshold -{% for thr, thr_value in threshold.general.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_flows = on -threshold_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_bandwidth = on -threshold_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_pps = on -threshold_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% if threshold.tcp is vyos_defined %} -# TCP threshold -{% for thr, thr_value in threshold.tcp.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_tcp_flows = on -threshold_tcp_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_tcp_bandwidth = on -threshold_tcp_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_tcp_pps = on -threshold_tcp_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% if threshold.udp is vyos_defined %} -# UDP threshold -{% for thr, thr_value in threshold.udp.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_udp_flows = on -threshold_udp_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_udp_bandwidth = on -threshold_udp_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_udp_pps = on -threshold_udp_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% if threshold.icmp is vyos_defined %} -# ICMP threshold -{% for thr, thr_value in threshold.icmp.items() %} -{% if thr is vyos_defined('fps') %} -ban_for_icmp_flows = on -threshold_icmp_flows = {{ thr_value }} -{% elif thr is vyos_defined('mbps') %} -ban_for_icmp_bandwidth = on -threshold_icmp_mbps = {{ thr_value }} -{% elif thr is vyos_defined('pps') %} -ban_for_icmp_pps = on -threshold_icmp_pps = {{ thr_value }} -{% endif %} -{% endfor %} -{% endif %} - -{% endif %} - -{% if listen_interface is vyos_defined %} -interfaces = {{ listen_interface | join(',') }} -{% endif %} - -{% if alert_script is vyos_defined %} -notify_script_path = {{ alert_script }} -{% endif %} diff --git a/data/templates/ids/fastnetmon_excluded_networks_list.j2 b/data/templates/ids/fastnetmon_excluded_networks_list.j2 deleted file mode 100644 index c88a1c527..000000000 --- a/data/templates/ids/fastnetmon_excluded_networks_list.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% if excluded_network is vyos_defined %} -{% for net in excluded_network %} -{{ net }} -{% endfor %} -{% endif %} diff --git a/data/templates/ids/fastnetmon_networks_list.j2 b/data/templates/ids/fastnetmon_networks_list.j2 deleted file mode 100644 index 0a0576d2a..000000000 --- a/data/templates/ids/fastnetmon_networks_list.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% if network is vyos_defined %} -{% for net in network %} -{{ net }} -{% endfor %} -{% endif %} diff --git a/data/templates/ipsec/charon_systemd.conf.j2 b/data/templates/ipsec/charon_systemd.conf.j2 new file mode 100644 index 000000000..368aa1ae3 --- /dev/null +++ b/data/templates/ipsec/charon_systemd.conf.j2 @@ -0,0 +1,18 @@ +# Generated by ${vyos_conf_scripts_dir}/vpn_ipsec.py + +charon-systemd { + + # Section to configure native systemd journal logger, very similar to the + # syslog logger as described in LOGGER CONFIGURATION in strongswan.conf(5). + journal { + + # Loglevel for a specific subsystem. + # <subsystem> = <default> + +{% if log.level is vyos_defined %} + # Default loglevel. + default = {{ log.level }} +{% endif %} + } + +} diff --git a/data/templates/ipsec/ios_profile.j2 b/data/templates/ipsec/ios_profile.j2 index 966fad433..6993f82bf 100644 --- a/data/templates/ipsec/ios_profile.j2 +++ b/data/templates/ipsec/ios_profile.j2 @@ -55,11 +55,9 @@ <!-- The server is authenticated using a certificate --> <key>AuthenticationMethod</key> <string>Certificate</string> -{% if authentication.client_mode.startswith("eap") %} <!-- The client uses EAP to authenticate --> <key>ExtendedAuthEnabled</key> <integer>1</integer> -{% endif %} <!-- The next two dictionaries are optional (as are the keys in them), but it is recommended to specify them as the default is to use 3DES. IMPORTANT: Because only one proposal is sent (even if nothing is configured here) it must match the server configuration --> <key>IKESecurityAssociationParameters</key> @@ -80,9 +78,9 @@ <string>{{ esp_encryption.encryption }}</string> <key>IntegrityAlgorithm</key> <string>{{ esp_encryption.hash }}</string> -{% if esp_encryption.pfs is vyos_defined %} +{% if ike_encryption.dh_group is vyos_defined %} <key>DiffieHellmanGroup</key> - <integer>{{ esp_encryption.pfs }}</integer> + <integer>{{ ike_encryption.dh_group }}</integer> {% endif %} </dict> <!-- Controls whether the client offers Perfect Forward Secrecy (PFS). This should be set to match the server. --> diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index 3a9af2c94..cf0865c88 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -68,8 +68,19 @@ rekey_packets = 0 rekey_time = 0s {% endif %} - local_ts = 0.0.0.0/0,::/0 - remote_ts = 0.0.0.0/0,::/0 +{# set default traffic-selectors #} +{% set local_ts = '0.0.0.0/0,::/0' %} +{% set remote_ts = '0.0.0.0/0,::/0' %} +{% if peer_conf.vti.traffic_selector is vyos_defined %} +{% if peer_conf.vti.traffic_selector.local is vyos_defined and peer_conf.vti.traffic_selector.local.prefix is vyos_defined %} +{% set local_ts = peer_conf.vti.traffic_selector.local.prefix | join(',') %} +{% endif %} +{% if peer_conf.vti.traffic_selector.remote is vyos_defined and peer_conf.vti.traffic_selector.remote.prefix is vyos_defined %} +{% set remote_ts = peer_conf.vti.traffic_selector.remote.prefix | join(',') %} +{% endif %} +{% endif %} + local_ts = {{ local_ts }} + remote_ts = {{ remote_ts }} updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}" {# The key defaults to 0 and will match any policies which similarly do not have a lookup key configuration. #} {# Thus we simply shift the key by one to also support a vti0 interface #} diff --git a/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/lldp/vyos.conf.j2 b/data/templates/lldp/vyos.conf.j2 index 4b4228cea..432a7a8e6 100644 --- a/data/templates/lldp/vyos.conf.j2 +++ b/data/templates/lldp/vyos.conf.j2 @@ -4,7 +4,7 @@ configure system platform VyOS configure system description "VyOS {{ version }}" {% if interface is vyos_defined %} {% set tmp = [] %} -{% for iface, iface_options in interface.items() if iface_options.disable is not vyos_defined %} +{% for iface, iface_options in interface.items() %} {% if iface == 'all' %} {% set iface = '*' %} {% endif %} @@ -17,6 +17,15 @@ configure ports {{ iface }} med location elin "{{ iface_options.location.elin }} configure ports {{ iface }} med location coordinate latitude "{{ iface_options.location.coordinate_based.latitude }}" longitude "{{ iface_options.location.coordinate_based.longitude }}" altitude "{{ iface_options.location.coordinate_based.altitude }}m" datum "{{ iface_options.location.coordinate_based.datum }}" {% endif %} {% endif %} +{% set mode = iface_options.mode %} +{% if mode == 'tx' %} +{% set mode = 'tx-only' %} +{% elif mode == 'rx' %} +{% set mode = 'rx-only' %} +{% elif mode == 'rx-tx' %} +{% set mode = 'rx-and-tx' %} +{% endif %} +configure ports {{ iface }} lldp status {{ mode }} {% endfor %} configure system interface pattern "{{ tmp | join(",") }}" {% endif %} diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2 index 786ebfb21..62934c612 100644 --- a/data/templates/load-balancing/haproxy.cfg.j2 +++ b/data/templates/load-balancing/haproxy.cfg.j2 @@ -38,9 +38,10 @@ defaults log global mode http option dontlognull - timeout connect 10s - timeout client 50s - timeout server 50s + timeout check {{ timeout.check }}s + timeout connect {{ timeout.connect }}s + timeout client {{ timeout.client }}s + timeout server {{ timeout.server }}s errorfile 400 /etc/haproxy/errors/400.http errorfile 403 /etc/haproxy/errors/403.http errorfile 408 /etc/haproxy/errors/408.http @@ -49,9 +50,29 @@ defaults errorfile 503 /etc/haproxy/errors/503.http errorfile 504 /etc/haproxy/errors/504.http +# Default ACME backend +backend buildin_acme_certbot + server localhost 127.0.0.1:{{ get_default_port('certbot_haproxy') }} + # Frontend {% if service is vyos_defined %} {% for front, front_config in service.items() %} +{% if front_config.redirect_http_to_https is vyos_defined %} +{% set certbot_backend_name = 'certbot_' ~ front ~ '_backend' %} +frontend {{ front }}-http + mode http +{% if front_config.listen_address is vyos_defined %} +{% for address in front_config.listen_address %} + bind {{ address | bracketize_ipv6 }}:80 +{% endfor %} +{% else %} + bind [::]:80 v4v6 +{% endif %} + acl acme_acl path_beg /.well-known/acme-challenge/ + use_backend buildin_acme_certbot if acme_acl + redirect scheme https code 301 if !acme_acl +{% endif %} + frontend {{ front }} {% set ssl_front = [] %} {% if front_config.ssl.certificate is vyos_defined and front_config.ssl.certificate is iterable %} @@ -67,9 +88,6 @@ frontend {{ front }} {% else %} bind [::]:{{ front_config.port }} v4v6 {{ ssl_directive }} {{ ssl_front | join(' ') }} {% endif %} -{% if front_config.redirect_http_to_https is vyos_defined %} - http-request redirect scheme https unless { ssl_fc } -{% endif %} {% if front_config.logging is vyos_defined %} {% for facility, facility_config in front_config.logging.facility.items() %} log /dev/log {{ facility }} {{ facility_config.level }} @@ -93,6 +111,11 @@ frontend {{ front }} http-response set-header {{ header }} '{{ header_config['value'] }}' {% endfor %} {% endif %} +{% if front_config.http_compression is vyos_defined %} + filter compression + compression algo {{ front_config.http_compression.algorithm }} + compression type {{ front_config.http_compression.mime_type | join(' ') }} +{% endif %} {% if front_config.rule is vyos_defined %} {% for rule, rule_config in front_config.rule.items() %} # rule {{ rule }} @@ -129,6 +152,11 @@ frontend {{ front }} default_backend {{ backend }} {% endfor %} {% endif %} +{% if front_config.timeout is vyos_defined %} +{% if front_config.timeout.client is vyos_defined %} + timeout client {{ front_config.timeout.client }}s +{% endif %} +{% endif %} {% endfor %} {% endif %} @@ -226,6 +254,5 @@ backend {{ back }} {% if back_config.timeout.server is vyos_defined %} timeout server {{ back_config.timeout.server }}s {% endif %} - {% endfor %} {% endif %} diff --git a/data/templates/load-balancing/nftables-wlb.j2 b/data/templates/load-balancing/nftables-wlb.j2 new file mode 100644 index 000000000..b3d7c3376 --- /dev/null +++ b/data/templates/load-balancing/nftables-wlb.j2 @@ -0,0 +1,64 @@ +#!/usr/sbin/nft -f + +{% if first_install is not vyos_defined %} +delete table ip vyos_wanloadbalance +{% endif %} +table ip vyos_wanloadbalance { + chain wlb_nat_postrouting { + type nat hook postrouting priority srcnat - 1; policy accept; +{% for ifname, health_conf in interface_health.items() if health_state[ifname].if_addr %} +{% if disable_source_nat is not vyos_defined %} +{% set state = health_state[ifname] %} + ct mark {{ state.mark }} counter snat to {{ state.if_addr }} +{% endif %} +{% endfor %} + } + + chain wlb_mangle_prerouting { + type filter hook prerouting priority mangle; policy accept; +{% for ifname, health_conf in interface_health.items() %} +{% set state = health_state[ifname] %} +{% if sticky_connections is vyos_defined %} + iifname "{{ ifname }}" ct state new ct mark set {{ state.mark }} +{% endif %} +{% endfor %} +{% if rule is vyos_defined %} +{% for rule_id, rule_conf in rule.items() %} +{% if rule_conf.exclude is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, exclude=True, action='return') }} +{% else %} +{% set limit = rule_conf.limit is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, limit=limit, weight=True, health_state=health_state) }} + {{ rule_conf | wlb_nft_rule(rule_id, restore_mark=True) }} +{% endif %} +{% endfor %} +{% endif %} + } + + chain wlb_mangle_output { + type filter hook output priority -150; policy accept; +{% if enable_local_traffic is vyos_defined %} + meta mark != 0x0 counter return + meta l4proto icmp counter return + ip saddr 127.0.0.0/8 ip daddr 127.0.0.0/8 counter return +{% if rule is vyos_defined %} +{% for rule_id, rule_conf in rule.items() %} +{% if rule_conf.exclude is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, local=True, exclude=True, action='return') }} +{% else %} +{% set limit = rule_conf.limit is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, local=True, limit=limit, weight=True, health_state=health_state) }} + {{ rule_conf | wlb_nft_rule(rule_id, local=True, restore_mark=True) }} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} + } + +{% for ifname, health_conf in interface_health.items() %} +{% set state = health_state[ifname] %} + chain wlb_mangle_isp_{{ ifname }} { + meta mark set {{ state.mark }} ct mark set {{ state.mark }} counter accept + } +{% endfor %} +} diff --git a/data/templates/load-balancing/wlb.conf.j2 b/data/templates/load-balancing/wlb.conf.j2 deleted file mode 100644 index 7f04d797e..000000000 --- a/data/templates/load-balancing/wlb.conf.j2 +++ /dev/null @@ -1,134 +0,0 @@ -### Autogenerated by load-balancing_wan.py ### - -{% if disable_source_nat is vyos_defined %} -disable-source-nat -{% endif %} -{% if enable_local_traffic is vyos_defined %} -enable-local-traffic -{% endif %} -{% if sticky_connections is vyos_defined %} -sticky-connections inbound -{% endif %} -{% if flush_connections is vyos_defined %} -flush-conntrack -{% endif %} -{% if hook is vyos_defined %} -hook "{{ hook }}" -{% endif %} -{% if interface_health is vyos_defined %} -health { -{% for interface, interface_config in interface_health.items() %} - interface {{ interface }} { -{% if interface_config.failure_count is vyos_defined %} - failure-ct {{ interface_config.failure_count }} -{% endif %} -{% if interface_config.success_count is vyos_defined %} - success-ct {{ interface_config.success_count }} -{% endif %} -{% if interface_config.nexthop is vyos_defined %} - nexthop {{ interface_config.nexthop }} -{% endif %} -{% if interface_config.test is vyos_defined %} -{% for test_rule, test_config in interface_config.test.items() %} - rule {{ test_rule }} { -{% if test_config.type is vyos_defined %} -{% set type_translate = {'ping': 'ping', 'ttl': 'udp', 'user-defined': 'user-defined'} %} - type {{ type_translate[test_config.type] }} { -{% if test_config.ttl_limit is vyos_defined and test_config.type == 'ttl' %} - ttl {{ test_config.ttl_limit }} -{% endif %} -{% if test_config.test_script is vyos_defined and test_config.type == 'user-defined' %} - test-script {{ test_config.test_script }} -{% endif %} -{% if test_config.target is vyos_defined %} - target {{ test_config.target }} -{% endif %} - resp-time {{ test_config.resp_time | int * 1000 }} - } -{% endif %} - } -{% endfor %} -{% endif %} - } -{% endfor %} -} -{% endif %} - -{% if rule is vyos_defined %} -{% for rule, rule_config in rule.items() %} -rule {{ rule }} { -{% if rule_config.exclude is vyos_defined %} - exclude -{% endif %} -{% if rule_config.failover is vyos_defined %} - failover -{% endif %} -{% if rule_config.limit is vyos_defined %} - limit { -{% if rule_config.limit.burst is vyos_defined %} - burst {{ rule_config.limit.burst }} -{% endif %} -{% if rule_config.limit.rate is vyos_defined %} - rate {{ rule_config.limit.rate }} -{% endif %} -{% if rule_config.limit.period is vyos_defined %} - period {{ rule_config.limit.period }} -{% endif %} -{% if rule_config.limit.threshold is vyos_defined %} - thresh {{ rule_config.limit.threshold }} -{% endif %} - } -{% endif %} -{% if rule_config.per_packet_balancing is vyos_defined %} - per-packet-balancing -{% endif %} -{% if rule_config.protocol is vyos_defined %} - protocol {{ rule_config.protocol }} -{% endif %} -{% if rule_config.destination is vyos_defined %} - destination { -{% if rule_config.destination.address is vyos_defined %} - address "{{ rule_config.destination.address }}" -{% endif %} -{% if rule_config.destination.port is vyos_defined %} -{% if '-' in rule_config.destination.port %} - port-ipt "-m multiport --dports {{ rule_config.destination.port | replace('-', ':') }}" -{% elif ',' in rule_config.destination.port %} - port-ipt "-m multiport --dports {{ rule_config.destination.port }}" -{% else %} - port-ipt " --dport {{ rule_config.destination.port }}" -{% endif %} -{% endif %} - } -{% endif %} -{% if rule_config.source is vyos_defined %} - source { -{% if rule_config.source.address is vyos_defined %} - address "{{ rule_config.source.address }}" -{% endif %} -{% if rule_config.source.port is vyos_defined %} -{% if '-' in rule_config.source.port %} - port-ipt "-m multiport --sports {{ rule_config.source.port | replace('-', ':') }}" -{% elif ',' in rule_config.destination.port %} - port-ipt "-m multiport --sports {{ rule_config.source.port }}" -{% else %} - port.ipt " --sport {{ rule_config.source.port }}" -{% endif %} -{% endif %} - } -{% endif %} -{% if rule_config.inbound_interface is vyos_defined %} - inbound-interface {{ rule_config.inbound_interface }} -{% endif %} -{% if rule_config.interface is vyos_defined %} -{% for interface, interface_config in rule_config.interface.items() %} - interface {{ interface }} { -{% if interface_config.weight is vyos_defined %} - weight {{ interface_config.weight }} -{% endif %} - } -{% endfor %} -{% endif %} -} -{% endfor %} -{% endif %} diff --git a/data/templates/login/motd_vyos_nonproduction.j2 b/data/templates/login/motd_vyos_nonproduction.j2 index 3f10423ff..4b81acc5b 100644 --- a/data/templates/login/motd_vyos_nonproduction.j2 +++ b/data/templates/login/motd_vyos_nonproduction.j2 @@ -2,3 +2,4 @@ --- WARNING: This VyOS system is not a stable long-term support version and is not intended for production use. + 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/prometheus/blackbox_exporter.service.j2 b/data/templates/prometheus/blackbox_exporter.service.j2 new file mode 100644 index 000000000..e93030246 --- /dev/null +++ b/data/templates/prometheus/blackbox_exporter.service.j2 @@ -0,0 +1,21 @@ +{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' runuser -u node_exporter -- ' if vrf is vyos_defined else '' %} +[Unit] +Description=Blackbox Exporter +Documentation=https://github.com/prometheus/blackbox_exporter +After=network.target + +[Service] +{% if vrf is not vyos_defined %} +User=node_exporter +{% endif %} +ExecStart={{ vrf_command }}/usr/sbin/blackbox_exporter \ +{% if listen_address is vyos_defined %} +{% for address in listen_address %} + --web.listen-address={{ address }}:{{ port }} \ +{% endfor %} +{% else %} + --web.listen-address=:{{ port }} \ +{% endif %} + --config.file=/run/blackbox_exporter/config.yml +[Install] +WantedBy=multi-user.target diff --git a/data/templates/prometheus/blackbox_exporter.yml.j2 b/data/templates/prometheus/blackbox_exporter.yml.j2 new file mode 100644 index 000000000..ba2eecd77 --- /dev/null +++ b/data/templates/prometheus/blackbox_exporter.yml.j2 @@ -0,0 +1,23 @@ +modules: +{% if modules is defined and modules.dns is defined and modules.dns.name is defined %} +{% for module_name, module_config in modules.dns.name.items() %} + {{ module_name }}: + prober: dns + timeout: {{ module_config.timeout }}s + dns: + query_name: "{{ module_config.query_name }}" + query_type: "{{ module_config.query_type }}" + preferred_ip_protocol: "{{ module_config.preferred_ip_protocol | replace('v', '') }}" + ip_protocol_fallback: {{ 'true' if module_config.ip_protocol_fallback is vyos_defined else 'false' }} +{% endfor %} +{% endif %} +{% if modules is defined and modules.icmp is vyos_defined and modules.icmp.name is vyos_defined %} +{% for module_name, module_config in modules.icmp.name.items() %} + {{ module_name }}: + prober: icmp + timeout: {{ module_config.timeout }}s + icmp: + preferred_ip_protocol: "{{ module_config.preferred_ip_protocol | replace('v', '') }}" + ip_protocol_fallback: {{ 'true' if module_config.ip_protocol_fallback is vyos_defined else 'false' }} +{% endfor %} +{% endif %}
\ No newline at end of file diff --git a/data/templates/prometheus/node_exporter.service.j2 b/data/templates/prometheus/node_exporter.service.j2 index 62e7e6774..9a943cd75 100644 --- a/data/templates/prometheus/node_exporter.service.j2 +++ b/data/templates/prometheus/node_exporter.service.j2 @@ -9,6 +9,11 @@ After=network.target User=node_exporter {% endif %} ExecStart={{ vrf_command }}/usr/sbin/node_exporter \ +{% if collectors is vyos_defined %} +{% if collectors.textfile is vyos_defined %} + --collector.textfile.directory=/run/node_exporter/collector \ +{% endif %} +{% endif %} {% if listen_address is vyos_defined %} {% for address in listen_address %} --web.listen-address={{ address }}:{{ port }} @@ -16,5 +21,6 @@ ExecStart={{ vrf_command }}/usr/sbin/node_exporter \ {% else %} --web.listen-address=:{{ port }} {% endif %} + [Install] WantedBy=multi-user.target diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2 index a83bd03ac..34f8e1f6d 100644 --- a/data/templates/router-advert/radvd.conf.j2 +++ b/data/templates/router-advert/radvd.conf.j2 @@ -57,6 +57,21 @@ interface {{ iface }} { }; {% endfor %} {% endif %} +{% if iface_config.prefix is vyos_defined and "::/64" in iface_config.prefix %} +{% if iface_config.auto_ignore is vyos_defined or iface_config.prefix | count > 1 %} + autoignoreprefixes { +{% if iface_config.auto_ignore is vyos_defined %} +{% for auto_ignore_prefix in (iface_config.auto_ignore + iface_config.prefix | list) | reject("eq", "::/64") | unique %} + {{ auto_ignore_prefix }}; +{% endfor %} +{% else %} +{% for auto_ignore_prefix in iface_config.prefix | reject("eq", "::/64") %} + {{ auto_ignore_prefix }}; +{% endfor %} +{% endif %} + }; +{% endif %} +{% endif %} {% if iface_config.prefix is vyos_defined %} {% for prefix, prefix_options in iface_config.prefix.items() %} prefix {{ prefix }} { diff --git a/data/templates/rsyslog/override.conf.j2 b/data/templates/rsyslog/override.conf.j2 deleted file mode 100644 index 5f6a87edf..000000000 --- a/data/templates/rsyslog/override.conf.j2 +++ /dev/null @@ -1,11 +0,0 @@ -{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} -[Unit] -StartLimitIntervalSec=0 - -[Service] -ExecStart= -ExecStart={{ vrf_command }}/usr/sbin/rsyslogd -n -iNONE -Restart=always -RestartPreventExitStatus= -RestartSec=10 -RuntimeDirectoryPreserve=yes diff --git a/data/templates/rsyslog/rsyslog.conf.j2 b/data/templates/rsyslog/rsyslog.conf.j2 index 253a4bee2..6ef2afcaf 100644 --- a/data/templates/rsyslog/rsyslog.conf.j2 +++ b/data/templates/rsyslog/rsyslog.conf.j2 @@ -1,78 +1,121 @@ ### Autogenerated by system_syslog.py ### -{% if global.marker is vyos_defined %} -$ModLoad immark -{% if global.marker.interval is vyos_defined %} -$MarkMessagePeriod {{ global.marker.interval }} -{% endif %} -{% endif %} -{% if global.preserve_fqdn is vyos_defined %} -$PreserveFQDN on -{% endif %} +#### MODULES #### +# Load input modules for local logging and journald -{% if global.local_host_name is vyos_defined %} -$LocalHostName {{ global.local_host_name }} -{% endif %} +# Old-style log file format with low-precision timestamps +# A modern-style logfile format with high-precision timestamps and timezone info +# RSYSLOG_FileFormat +module(load="builtin:omfile" Template="RSYSLOG_TraditionalFileFormat") +module(load="imuxsock") # provides support for local system logging (collection from /dev/log unix socket) -# We always log to /var/log/messages -$outchannel global,/var/log/messages,262144,/usr/sbin/logrotate {{ logrotate }} -{% if global.facility is vyos_defined %} -{% set tmp = [] %} -{% for facility, facility_options in global.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} -{% endfor %} -{{ tmp | join(';') }} :omfile:$global +# Import logs from journald, which includes kernel log messages +module( + load="imjournal" + StateFile="/var/spool/rsyslog/imjournal.state" # Persistent state file to track the journal cursor + Ratelimit.Interval="0" # Disable rate limiting (set to "0" for no limit) + RateLimit.Burst="0" +) + +########################### +#### GLOBAL DIRECTIVES #### +########################### +# Log specific programs to auth.log, then stop further processing +if ( + $programname == "CRON" or + $programname == "sudo" or + $programname == "su" +) then { + action(type="omfile" file="/var/log/auth.log") + stop +} + +global(workDirectory="/var/spool/rsyslog") + +############### +#### RULES #### +############### + +# Send emergency messages to all logged-in users +*.emerg action(type="omusrmsg" users="*") + +{% if marker is vyos_defined and marker.disable is not vyos_defined %} +# Load the immark module for periodic --MARK-- message capability +module(load="immark" interval="{{ marker.interval }}") +{% endif %} +{% if preserve_fqdn is vyos_defined %} +# Preserve the fully qualified domain name (FQDN) in log messages +global(preserveFQDN="on") +{% if preserve_fqdn.host_name is vyos_defined and preserve_fqdn.domain_name is vyos_defined %} +# Set the local hostname for log messages +global(localHostname="{{ preserve_fqdn.host_name }}.{{ preserve_fqdn.domain_name }}") +{% endif %} {% endif %} -{% if file is vyos_defined %} -# File based configuration section -{% for file_name, file_options in file.items() %} -{% set tmp = [] %} -$outchannel {{ file_name }},/var/log/user/{{ file_name }},{{ file_options.archive.size }},/usr/sbin/logrotate {{ logrotate }} -{% if file_options.facility is vyos_defined %} -{% for facility, facility_options in file_options.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} -{% endfor %} -{% endif %} -{{ tmp | join(';') }} :omfile:${{ file }} -{% endfor %} +#### GLOBAL LOGGING #### +{% if local.facility is vyos_defined %} +{% set tmp = [] %} +{% if local.facility is vyos_defined %} +{% for facility, facility_options in local.facility.items() %} +{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %} +{% endfor %} +if prifilt("{{ tmp | join(',') }}") then { + action( + type="omfile" + file="/var/log/messages" + rotation.sizeLimit="524288" # 512Kib - maximum filesize before rotation + rotation.sizeLimitCommand="/usr/sbin/logrotate {{ logrotate }}" + ) +} +{% endif %} {% endif %} +#### CONSOLE LOGGING #### {% if console.facility is vyos_defined %} -# Console logging {% set tmp = [] %} -{% for facility, facility_options in console.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} -{% endfor %} -{{ tmp | join(';') }} /dev/console +{% if console.facility is vyos_defined %} +{% for facility, facility_options in console.facility.items() %} +{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %} +{% endfor %} +if prifilt("{{ tmp | join(',') }}") then { + action(type="omfile" file="/dev/console") +} +{% endif %} {% endif %} -{% if host is vyos_defined %} -# Remote logging -{% for host_name, host_options in host.items() %} +#### REMOTE LOGGING #### +{% if remote is vyos_defined %} +{% for remote_name, remote_options in remote.items() %} {% set tmp = [] %} -{% if host_options.facility is vyos_defined %} -{% for facility, facility_options in host_options.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} +{% if remote_options.facility is vyos_defined %} +{% for facility, facility_options in remote_options.facility.items() %} +{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %} {% endfor %} -{% endif %} -{% if host_options.protocol is vyos_defined('tcp') %} -{{ tmp | join(';') }} @@{{ '(o)' if host_options.format.octet_counted is vyos_defined }}{{ host_name | bracketize_ipv6 }}:{{ host_options.port }}{{ ';RSYSLOG_SyslogProtocol23Format' if host_options.format.include_timezone is vyos_defined }} -{% else %} -{{ tmp | join(';') }} @{{ host_name | bracketize_ipv6 }}:{{ host_options.port }}{{ ';RSYSLOG_SyslogProtocol23Format' if host_options.format.include_timezone is vyos_defined }} +{% set _ = tmp.sort() %} +# Remote syslog to {{ remote_name }} +if prifilt("{{ tmp | join(',') }}") then { + action( + type="omfwd" + # Remote syslog server where we send our logs to + target="{{ remote_name }}" + # Port on the remote syslog server + port="{{ remote_options.port }}" + protocol="{{ remote_options.protocol }}" +{% if remote_options.format.include_timezone is vyos_defined %} + template="RSYSLOG_SyslogProtocol23Format" +{% endif %} + TCP_Framing="{{ 'octet-counted' if remote_options.format.octet_counted is vyos_defined else 'traditional' }}" +{% if remote_options.source_address is vyos_defined %} + Address="{{ remote_options.source_address }}" +{% endif %} +{% if remote_options.vrf is vyos_defined %} + Device="{{ remote_options.vrf }}" +{% endif %} + ) +} {% endif %} {% endfor %} {% endif %} -{% if user is defined and user is not none %} -# Log to user terminal -{% for username, user_options in user.items() %} -{% set tmp = [] %} -{% if user_options.facility is vyos_defined %} -{% for facility, facility_options in user_options.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} -{% endfor %} -{% endif %} -{{ tmp | join(';') }} :omusrmsg:{{ username }} -{% endfor %} -{% endif %} +# Include all configuration files in /etc/rsyslog.d/ +include(file="/etc/rsyslog.d/*.conf") 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/squid/squid.conf.j2 b/data/templates/squid/squid.conf.j2 index b953c8b18..4e3d702a8 100644 --- a/data/templates/squid/squid.conf.j2 +++ b/data/templates/squid/squid.conf.j2 @@ -30,6 +30,14 @@ acl BLOCKDOMAIN dstdomain {{ domain }} {% endfor %} http_access deny BLOCKDOMAIN {% endif %} + +{% if domain_noncache is vyos_defined %} +{% for domain in domain_noncache %} +acl NOCACHE dstdomain {{ domain }} +{% endfor %} +no_cache deny NOCACHE +{% endif %} + {% if authentication is vyos_defined %} {% if authentication.children is vyos_defined %} auth_param basic children {{ authentication.children }} 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/data/templates/telegraf/telegraf.j2 b/data/templates/telegraf/telegraf.j2 index 535e3a347..043fc6878 100644 --- a/data/templates/telegraf/telegraf.j2 +++ b/data/templates/telegraf/telegraf.j2 @@ -52,7 +52,7 @@ password = "{{ loki.authentication.password }}" {% endif %} {% if loki.metric_name_label is vyos_defined %} -metric_name_label = "{{ loki.metric_name_label }}" + metric_name_label = "{{ loki.metric_name_label }}" {% endif %} ### End Loki ### {% endif %} diff --git a/debian/control b/debian/control index 08b86356a..9b798eb97 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,6 @@ Build-Depends: fakeroot, gcc, iproute2, - libvyosconfig0 (>= 0.0.7), libzmq3-dev, python3 (>= 3.10), # For QA @@ -16,6 +15,8 @@ Build-Depends: # For generating command definitions python3-lxml, python3-xmltodict, +# For generating serialization functions + protobuf-compiler, # For running tests python3-coverage, python3-hurry.filesize, @@ -40,7 +41,9 @@ Pre-Depends: libpam-runtime [amd64], libnss-tacplus [amd64], libpam-tacplus [amd64], - libpam-radius-auth [amd64] + vyos-libpam-radius-auth, + vyos-libnss-mapuser, + tzdata (>= 2025b) Depends: ## Fundamentals ${python3:Depends} (>= 3.10), @@ -70,6 +73,7 @@ Depends: python3-netifaces, python3-paramiko, python3-passlib, + python3-protobuf, python3-pyroute2, python3-psutil, python3-pyhumps, @@ -77,6 +81,7 @@ Depends: python3-pyudev, python3-six, python3-tabulate, + python3-tomli, python3-voluptuous, python3-xmltodict, python3-zmq, @@ -115,7 +120,7 @@ Depends: dosfstools, grub-efi-amd64-signed [amd64], grub-efi-arm64-bin [arm64], - mokutil [amd64], + mokutil, shim-signed [amd64], sbsigntool [amd64], # Image signature verification tool @@ -123,6 +128,8 @@ Depends: # Live filesystem tools squashfs-tools, fuse-overlayfs, +# Tools for checking password strength + python3-cracklib, ## End installer auditd, iputils-arping, @@ -171,9 +178,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" @@ -192,7 +196,6 @@ Depends: ddclient (>= 3.11.1), # End "service dns dynamic" # # For "service ids" - fastnetmon [amd64], suricata, suricata-update, # End "service ids" @@ -200,14 +203,11 @@ Depends: ndppd, # End "service ndp-proxy" # For "service router-advert" - radvd, + radvd (>= 2.20), # End "service route-advert" # For "load-balancing haproxy" haproxy, # End "load-balancing haproxy" -# For "load-balancing wan" - vyatta-wanloadbalance, -# End "load-balancing wan" # For "service dhcp-relay" isc-dhcp-relay, # For "service dhcp-server" @@ -241,6 +241,9 @@ Depends: # For "service monitoring prometheus frr-exporter" frr-exporter, # End "service monitoring prometheus frr-exporter" +# For "service monitoring prometheus blackbox-exporter" + blackbox-exporter, +# End "service monitoring prometheus blackbox-exporter" # For "service monitoring telegraf" telegraf (>= 1.20), # End "service monitoring telegraf" @@ -324,6 +327,14 @@ Depends: # iptables is only used for containers now, not the the firewall CLI iptables, # End container +# For "vpp" + libvppinfra, + python3-vpp-api, + vpp, + vpp-dev, + vpp-plugin-core, + vpp-plugin-dpdk, +# End "vpp" ## End Configuration mode ## Operational mode # Used for hypervisor model in "run show version" 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 18612804c..bff73796c 100755 --- a/debian/vyos-1x-smoketest.postinst +++ b/debian/vyos-1x-smoketest.postinst @@ -1,10 +1,19 @@ #!/bin/sh -e BUSYBOX_TAG="docker.io/library/busybox:stable" -OUTPUT_PATH="/usr/share/vyos/busybox-stable.tar" +BUSYBOX_PATH="/usr/share/vyos/busybox-stable.tar" +if [[ ! -f $BUSYBOX_PATH ]]; then + skopeo copy --additional-tag "$BUSYBOX_TAG" "docker://$BUSYBOX_TAG" "docker-archive:/$BUSYBOX_PATH" +fi -if [[ -f $OUTPUT_PATH ]]; then - rm -f $OUTPUT_PATH +TACPLUS_TAG="docker.io/lfkeitel/tacacs_plus:alpine" +TACPLUS_PATH="/usr/share/vyos/tacplus-alpine.tar" +if [[ ! -f $TACPLUS_PATH ]]; then + skopeo copy --additional-tag "$TACPLUS_TAG" "docker://$TACPLUS_TAG" "docker-archive:/$TACPLUS_PATH" fi -skopeo copy --additional-tag "$BUSYBOX_TAG" "docker://$BUSYBOX_TAG" "docker-archive:/$OUTPUT_PATH" +RADIUS_TAG="docker.io/dchidell/radius-web:latest" +RADIUS_PATH="/usr/share/vyos/radius-latest.tar" +if [[ ! -f $RADIUS_PATH ]]; then + skopeo copy --additional-tag "$RADIUS_TAG" "docker://$RADIUS_TAG" "docker-archive:/$RADIUS_PATH" +fi diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index d5dd3bcec..4e312a648 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,5 +1,6 @@ etc/bash_completion.d etc/commit +etc/cron.d etc/default etc/dhcp etc/ipsec.d @@ -8,7 +9,6 @@ etc/netplug etc/opennhrp etc/modprobe.d etc/ppp -etc/rsyslog.conf etc/securetty etc/security etc/skel diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index ff5a91e09..798ecaa1b 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -221,11 +221,9 @@ fi # Remove unwanted daemon files from /etc # conntackd # pmacct -# fastnetmon # ntp DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd /etc/default/pmacctd /etc/pmacct - /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf /etc/ntp.conf /etc/default/ssh /etc/avahi/avahi-daemon.conf /etc/avahi/hosts /etc/powerdns /etc/default/pdns-recursor /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns" @@ -272,3 +270,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/debian/vyos-1x.preinst b/debian/vyos-1x.preinst index fbfc85566..08f48cac2 100644 --- a/debian/vyos-1x.preinst +++ b/debian/vyos-1x.preinst @@ -5,6 +5,7 @@ dpkg-divert --package vyos-1x --add --no-rename /etc/logrotate.d/conntrackd dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.conf dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.bashrc dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.profile +dpkg-divert --package vyos-1x --add --no-rename /etc/sysctl.d/80-vpp.conf dpkg-divert --package vyos-1x --add --no-rename /etc/netplug/netplugd.conf dpkg-divert --package vyos-1x --add --no-rename /etc/netplug/netplug dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.d/45-frr.conf diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index ad1815604..434bf7528 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -31,7 +31,7 @@ <properties> <help>Grant individual Linux capability to container instance</help> <completionHelp> - <list>net-admin net-bind-service net-raw setpcap sys-admin sys-module sys-nice sys-time</list> + <list>net-admin net-bind-service net-raw mknod setpcap sys-admin sys-module sys-nice sys-time</list> </completionHelp> <valueHelp> <format>net-admin</format> @@ -46,6 +46,10 @@ <description>Permission to create raw network sockets</description> </valueHelp> <valueHelp> + <format>mknod</format> + <description>Permission to create special files</description> + </valueHelp> + <valueHelp> <format>setpcap</format> <description>Capability sets (from bounded or inherited set)</description> </valueHelp> @@ -66,11 +70,17 @@ <description>Permission to set system clock</description> </valueHelp> <constraint> - <regex>(net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-module|sys-nice|sys-time)</regex> + <regex>(net-admin|net-bind-service|net-raw|mknod|setpcap|sys-admin|sys-module|sys-nice|sys-time)</regex> </constraint> <multi/> </properties> </leafNode> + <leafNode name="privileged"> + <properties> + <help>Grant root capabilities to the container</help> + <valueless/> + </properties> + </leafNode> <node name="sysctl"> <properties> <help>Configure namespaced kernel parameters of the container</help> @@ -131,7 +141,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> @@ -412,6 +422,35 @@ </constraint> </properties> </leafNode> + <tagNode name="tmpfs"> + <properties> + <help>Mount a tmpfs filesystem into the container</help> + </properties> + <children> + <leafNode name="destination"> + <properties> + <help>Destination container directory</help> + <valueHelp> + <format>txt</format> + <description>Destination container directory</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>tmpfs filesystem size in MB</help> + <valueHelp> + <format>u32:1-65536</format> + <description>tmpfs filesystem size in MB</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Container tmpfs size must be between 1 and 65535 MB</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> <tagNode name="volume"> <properties> <help>Mount a volume into the container</help> @@ -538,8 +577,75 @@ <children> #include <include/interface/authentication.xml.i> #include <include/generic-disable-node.xml.i> + <leafNode name="insecure"> + <properties> + <help>Allow registry access over unencrypted HTTP or TLS connections with untrusted certificates</help> + <valueless/> + </properties> + </leafNode> + <node name="mirror"> + <properties> + <help>Registry mirror, use host-name|address[:port][/path]</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address of container registry mirror</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of container registry mirror</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of container registry mirror</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + <validator name="ipv6-link-local"/> + </constraint> + </properties> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>Hostname of container registry mirror</help> + <valueHelp> + <format>hostname</format> + <description>FQDN of container registry mirror</description> + </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> + </properties> + </leafNode> + #include <include/port-number.xml.i> + <leafNode name="path"> + <properties> + <help>Path of container registry mirror, optional, must be start with '/' if not empty</help> + </properties> + </leafNode> + </children> + </node> </children> </tagNode> + <leafNode name="log-driver"> + <properties> + <help>Configure container log driver</help> + <completionHelp> + <list>k8s-file journald</list> + </completionHelp> + <valueHelp> + <format>k8s-file</format> + <description>Logs to plain-text json file</description> + </valueHelp> + <valueHelp> + <format>journald</format> + <description>Logs to systemd's journal</description> + </valueHelp> + <constraint> + <regex>(k8s-file|journald)</regex> + </constraint> + </properties> + </leafNode> </children> </node> </interfaceDefinition> diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 07c88f799..7538c3cc5 100755..100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -16,15 +16,7 @@ </properties> <children> #include <include/generic-description.xml.i> - <leafNode name="interface"> - <properties> - <help>Interfaces to use this flowtable</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi.xml.i> <leafNode name="offload"> <properties> <help>Offloading method</help> @@ -146,6 +138,19 @@ </tagNode> </children> </node> + <tagNode name="remote-group"> + <properties> + <help>Firewall remote-group</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> + <constraintErrorMessage>Name of firewall group can only contain alphanumeric letters, hyphen, underscores and dot</constraintErrorMessage> + </properties> + <children> + #include <include/url-http-https.xml.i> + #include <include/generic-description.xml.i> + </children> + </tagNode> <tagNode name="interface-group"> <properties> <help>Firewall interface-group</help> @@ -155,15 +160,7 @@ <constraintErrorMessage>Name of firewall group can only contain alphanumeric letters, hyphen, underscores and dot</constraintErrorMessage> </properties> <children> - <leafNode name="interface"> - <properties> - <help>Interface-group member</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi.xml.i> <leafNode name="include"> <properties> <help>Include another interface-group</help> @@ -464,24 +461,27 @@ </node> </children> </tagNode> - <leafNode name="interface"> + <node name="member"> <properties> <help>Interface associated with zone</help> - <valueHelp> - <format>txt</format> - <description>Interface associated with zone</description> - </valueHelp> - <valueHelp> - <format>vrf</format> - <description>VRF associated with zone</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - <path>vrf name</path> - </completionHelp> - <multi/> </properties> - </leafNode> + <children> + #include <include/generic-interface-multi.xml.i> + <leafNode name="vrf"> + <properties> + <help>VRF associated with zone</help> + <valueHelp> + <format>vrf</format> + <description>VRF associated with zone</description> + </valueHelp> + <completionHelp> + <path>vrf name</path> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> <node name="intra-zone-filtering"> <properties> <help>Intra-zone filtering</help> diff --git a/interface-definitions/include/babel/redistribute-common.xml.i b/interface-definitions/include/babel/redistribute-common.xml.i index 93efe68dd..e988cc0d0 100644 --- a/interface-definitions/include/babel/redistribute-common.xml.i +++ b/interface-definitions/include/babel/redistribute-common.xml.i @@ -23,6 +23,12 @@ <valueless/> </properties> </leafNode> + <leafNode name="nhrp"> + <properties> + <help>Redistribute NHRP routes</help> + <valueless/> + </properties> + </leafNode> <leafNode name="openfabric"> <properties> <help>OpenFabric Routing Protocol</help> diff --git a/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i b/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i new file mode 100644 index 000000000..3f6517d03 --- /dev/null +++ b/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i @@ -0,0 +1,62 @@ +<!-- include start from bgp/afi-redistribute-common-protocols.xml.i --> +<node name="babel"> + <properties> + <help>Redistribute Babel routes into BGP</help> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</node> +<node name="connected"> + <properties> + <help>Redistribute connected routes into BGP</help> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</node> +<node name="isis"> + <properties> + <help>Redistribute IS-IS routes into BGP</help> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</node> +<node name="kernel"> + <properties> + <help>Redistribute kernel routes into BGP</help> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</node> +<node name="nhrp"> + <properties> + <help>Redistribute NHRP routes into BGP</help> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</node> +<node name="static"> + <properties> + <help>Redistribute static routes into BGP</help> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</node> +<tagNode name="table"> + <properties> + <help>Redistribute non-main Kernel Routing Table</help> + <completionHelp> + <path>protocols static table</path> + </completionHelp> + #include <include/constraint/protocols-static-table.xml.i> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i b/interface-definitions/include/bgp/afi-route-map-export-import.xml.i deleted file mode 100644 index 388991241..000000000 --- a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i +++ /dev/null @@ -1,34 +0,0 @@ -<!-- include start from bgp/afi-route-map.xml.i --> -<leafNode name="export"> - <properties> - <help>Route-map to filter outgoing route updates</help> - <completionHelp> - <path>policy route-map</path> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>Route map name</description> - </valueHelp> - <constraint> - #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> - </constraint> - <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage> - </properties> -</leafNode> -<leafNode name="import"> - <properties> - <help>Route-map to filter incoming route updates</help> - <completionHelp> - <path>policy route-map</path> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>Route map name</description> - </valueHelp> - <constraint> - #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> - </constraint> - <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/bgp/afi-route-map-export.xml.i b/interface-definitions/include/bgp/afi-route-map-export.xml.i new file mode 100644 index 000000000..94d77caf2 --- /dev/null +++ b/interface-definitions/include/bgp/afi-route-map-export.xml.i @@ -0,0 +1,18 @@ +<!-- include start from bgp/afi-route-map-export.xml.i --> +<leafNode name="export"> + <properties> + <help>Route-map to filter outgoing route updates</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Route map name</description> + </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> + <constraintErrorMessage>Route map names can only contain alphanumeric characters, hyphens, and underscores</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/bgp/afi-route-map-import.xml.i b/interface-definitions/include/bgp/afi-route-map-import.xml.i new file mode 100644 index 000000000..a1b154fcd --- /dev/null +++ b/interface-definitions/include/bgp/afi-route-map-import.xml.i @@ -0,0 +1,18 @@ +<!-- include start from bgp/afi-route-map-import.xml.i --> +<leafNode name="import"> + <properties> + <help>Route-map to filter incoming route updates</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Route map name</description> + </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> + <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/bgp/afi-route-map-vpn.xml.i b/interface-definitions/include/bgp/afi-route-map-vpn.xml.i index e6be113c5..ac7b55af6 100644 --- a/interface-definitions/include/bgp/afi-route-map-vpn.xml.i +++ b/interface-definitions/include/bgp/afi-route-map-vpn.xml.i @@ -9,7 +9,8 @@ <help>Between current address-family and VPN</help> </properties> <children> - #include <include/bgp/afi-route-map-export-import.xml.i> + #include <include/bgp/afi-route-map-export.xml.i> + #include <include/bgp/afi-route-map-import.xml.i> </children> </node> </children> diff --git a/interface-definitions/include/bgp/afi-route-map-vrf.xml.i b/interface-definitions/include/bgp/afi-route-map-vrf.xml.i new file mode 100644 index 000000000..5c1783bda --- /dev/null +++ b/interface-definitions/include/bgp/afi-route-map-vrf.xml.i @@ -0,0 +1,17 @@ +<!-- include start from bgp/afi-route-map-vrf.xml.i --> +<node name="route-map"> + <properties> + <help>Route-map to filter route updates to/from this peer</help> + </properties> + <children> + <node name="vrf"> + <properties> + <help>Between current address-family and VRF</help> + </properties> + <children> + #include <include/bgp/afi-route-map-import.xml.i> + </children> + </node> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/bgp/afi-route-map.xml.i b/interface-definitions/include/bgp/afi-route-map.xml.i index 0b6178176..f8e1d7033 100644 --- a/interface-definitions/include/bgp/afi-route-map.xml.i +++ b/interface-definitions/include/bgp/afi-route-map.xml.i @@ -4,7 +4,8 @@ <help>Route-map to filter route updates to/from this peer</help> </properties> <children> - #include <include/bgp/afi-route-map-export-import.xml.i> + #include <include/bgp/afi-route-map-export.xml.i> + #include <include/bgp/afi-route-map-import.xml.i> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 4953251c5..31c8cafea 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -119,6 +119,7 @@ </tagNode> #include <include/bgp/afi-rd.xml.i> #include <include/bgp/afi-route-map-vpn.xml.i> + #include <include/bgp/afi-route-map-vrf.xml.i> #include <include/bgp/afi-route-target-vpn.xml.i> #include <include/bgp/afi-nexthop-vpn-export.xml.i> <node name="redistribute"> @@ -126,30 +127,7 @@ <help>Redistribute routes from other protocols into BGP</help> </properties> <children> - <node name="connected"> - <properties> - <help>Redistribute connected routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> - <node name="isis"> - <properties> - <help>Redistribute IS-IS routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> - <node name="kernel"> - <properties> - <help>Redistribute kernel routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> + #include <include/bgp/afi-redistribute-common-protocols.xml.i> <node name="ospf"> <properties> <help>Redistribute OSPF routes into BGP</help> @@ -166,27 +144,6 @@ #include <include/bgp/afi-redistribute-metric-route-map.xml.i> </children> </node> - <node name="babel"> - <properties> - <help>Redistribute Babel routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> - <node name="static"> - <properties> - <help>Redistribute static routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> - <leafNode name="table"> - <properties> - <help>Redistribute non-main Kernel Routing Table</help> - </properties> - </leafNode> </children> </node> #include <include/bgp/afi-sid.xml.i> @@ -503,22 +460,7 @@ <help>Redistribute routes from other protocols into BGP</help> </properties> <children> - <node name="connected"> - <properties> - <help>Redistribute connected routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> - <node name="kernel"> - <properties> - <help>Redistribute kernel routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> + #include <include/bgp/afi-redistribute-common-protocols.xml.i> <node name="ospfv3"> <properties> <help>Redistribute OSPFv3 routes into BGP</help> @@ -535,27 +477,6 @@ #include <include/bgp/afi-redistribute-metric-route-map.xml.i> </children> </node> - <node name="babel"> - <properties> - <help>Redistribute Babel routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> - <node name="static"> - <properties> - <help>Redistribute static routes into BGP</help> - </properties> - <children> - #include <include/bgp/afi-redistribute-metric-route-map.xml.i> - </children> - </node> - <leafNode name="table"> - <properties> - <help>Redistribute non-main Kernel Routing Table</help> - </properties> - </leafNode> </children> </node> #include <include/bgp/afi-sid.xml.i> diff --git a/interface-definitions/include/constraint/interface-name.xml.i b/interface-definitions/include/constraint/interface-name.xml.i index 3e7c4e667..f64ea86f5 100644 --- a/interface-definitions/include/constraint/interface-name.xml.i +++ b/interface-definitions/include/constraint/interface-name.xml.i @@ -1,4 +1,4 @@ <!-- include start from constraint/interface-name.xml.i --> -<regex>(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo</regex> +<regex>(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|veth|vpptap|vpptun|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|pod-[-_a-zA-Z0-9]{1,11}|lo</regex> <validator name="file-path --lookup-path /sys/class/net --directory"/> <!-- include end --> diff --git a/interface-definitions/include/constraint/protocols-static-table.xml.i b/interface-definitions/include/constraint/protocols-static-table.xml.i new file mode 100644 index 000000000..2d8b067a4 --- /dev/null +++ b/interface-definitions/include/constraint/protocols-static-table.xml.i @@ -0,0 +1,9 @@ +<!-- include start from constraint/host-name.xml.i --> +<valueHelp> + <format>u32:1-200</format> + <description>Policy route table number</description> +</valueHelp> +<constraint> + <validator name="numeric" argument="--range 1-200"/> +</constraint> +<!-- include end --> diff --git a/interface-definitions/include/constraint/wireguard-keys.xml.i b/interface-definitions/include/constraint/wireguard-keys.xml.i new file mode 100644 index 000000000..f59c86087 --- /dev/null +++ b/interface-definitions/include/constraint/wireguard-keys.xml.i @@ -0,0 +1,6 @@ +<!-- include start from constraint/wireguard-keys.xml.i --> +<constraint> + <validator name="base64" argument="--decoded-len 32"/> +</constraint> +<constraintErrorMessage>Key must be Base64-encoded with 32 bytes in length</constraintErrorMessage> +<!-- include end --> diff --git a/interface-definitions/include/dhcp/ddns-dns-server.xml.i b/interface-definitions/include/dhcp/ddns-dns-server.xml.i new file mode 100644 index 000000000..ba9f186d0 --- /dev/null +++ b/interface-definitions/include/dhcp/ddns-dns-server.xml.i @@ -0,0 +1,19 @@ +<!-- include start from dhcp/ddns-dns-server.xml.i --> +<tagNode name="dns-server"> + <properties> + <help>DNS server specification</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number for this DNS server</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>DNS server number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + #include <include/address-ipv4-ipv6-single.xml.i> + #include <include/port-number.xml.i> + </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/include/dhcp/ddns-settings.xml.i b/interface-definitions/include/dhcp/ddns-settings.xml.i new file mode 100644 index 000000000..3e202685e --- /dev/null +++ b/interface-definitions/include/dhcp/ddns-settings.xml.i @@ -0,0 +1,172 @@ +<!-- include start from dhcp/ddns-settings.xml.i --> +<leafNode name="send-updates"> + <properties> + <help>Enable or disable updates for this scope</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable updates for this scope</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable updates for this scope</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="override-client-update"> + <properties> + <help>Always update both forward and reverse DNS data, regardless of the client's request</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Force update both forward and reverse DNS records</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Respect client request settings</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="override-no-update"> + <properties> + <help>Perform a DDNS update, even if the client instructs the server not to</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Force DDNS updates regardless of client request</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Respect client request settings</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="replace-client-name"> + <properties> + <help>Replace client name mode</help> + <completionHelp> + <list>never always when-present when-not-present</list> + </completionHelp> + <valueHelp> + <format>never</format> + <description>Use the name the client sent. If the client sent no name, do not generate + one</description> + </valueHelp> + <valueHelp> + <format>always</format> + <description>Replace the name the client sent. If the client sent no name, generate one + for the client</description> + </valueHelp> + <valueHelp> + <format>when-present</format> + <description>Replace the name the client sent. If the client sent no name, do not + generate one</description> + </valueHelp> + <valueHelp> + <format>when-not-present</format> + <description>Use the name the client sent. If the client sent no name, generate one for + the client</description> + </valueHelp> + <constraint> + <regex>(never|always|when-present|when-not-present)</regex> + </constraint> + <constraintErrorMessage>Invalid replace client name mode</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="generated-prefix"> + <properties> + <help>The prefix used in the generation of an FQDN</help> + <constraint> + <validator name="fqdn" /> + </constraint> + <constraintErrorMessage>Invalid generated prefix</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="qualifying-suffix"> + <properties> + <help>The suffix used when generating an FQDN, or when qualifying a partial name</help> + <constraint> + <validator name="fqdn" /> + </constraint> + <constraintErrorMessage>Invalid qualifying suffix</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="update-on-renew"> + <properties> + <help>Update DNS record on lease renew</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Update DNS record on lease renew</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Do not update DNS record on lease renew</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="conflict-resolution"> + <properties> + <help>DNS conflict resolution behavior</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable DNS conflict resolution</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable DNS conflict resolution</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="ttl-percent"> + <properties> + <help>Calculate TTL of the DNS record as a percentage of the lease lifetime</help> + <constraint> + <validator name="numeric" argument="--range 1-100" /> + </constraint> + <constraintErrorMessage>Invalid qualifying suffix</constraintErrorMessage> + </properties> +</leafNode> +<leafNode name="hostname-char-set"> + <properties> + <help>A regular expression describing the invalid character set in the host name</help> + </properties> +</leafNode> +<leafNode name="hostname-char-replacement"> + <properties> + <help>A string of zero or more characters with which to replace each invalid character in + the host name</help> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/dhcp/option-v4.xml.i b/interface-definitions/include/dhcp/option-v4.xml.i index bd6fc6043..08fbcca4a 100644 --- a/interface-definitions/include/dhcp/option-v4.xml.i +++ b/interface-definitions/include/dhcp/option-v4.xml.i @@ -59,6 +59,18 @@ <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> </properties> </leafNode> + <leafNode name="capwap-controller"> + <properties> + <help>IP address of CAPWAP access controller (Option 138)</help> + <valueHelp> + <format>ipv4</format> + <description>CAPWAP AC controller</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> <leafNode name="default-router"> <properties> <help>IP address of default router</help> diff --git a/interface-definitions/include/dhcp/option-v6.xml.i b/interface-definitions/include/dhcp/option-v6.xml.i index e1897f52d..202843ddf 100644 --- a/interface-definitions/include/dhcp/option-v6.xml.i +++ b/interface-definitions/include/dhcp/option-v6.xml.i @@ -7,6 +7,18 @@ #include <include/dhcp/captive-portal.xml.i> #include <include/dhcp/domain-search.xml.i> #include <include/name-server-ipv6.xml.i> + <leafNode name="capwap-controller"> + <properties> + <help>IP address of CAPWAP access controller (Option 52)</help> + <valueHelp> + <format>ipv6</format> + <description>CAPWAP AC controller</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> <leafNode name="nis-domain"> <properties> <help>NIS domain name for client to use</help> diff --git a/interface-definitions/include/dhcp/ping-check.xml.i b/interface-definitions/include/dhcp/ping-check.xml.i new file mode 100644 index 000000000..a506f68e4 --- /dev/null +++ b/interface-definitions/include/dhcp/ping-check.xml.i @@ -0,0 +1,8 @@ +<!-- include start from dhcp/ping-check.xml.i --> +<leafNode name="ping-check"> + <properties> + <help>Sends ICMP Echo request to the address being assigned</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/common-rule-ipv4.xml.i b/interface-definitions/include/firewall/common-rule-ipv4.xml.i index 803b94b06..b67ef25dc 100644 --- a/interface-definitions/include/firewall/common-rule-ipv4.xml.i +++ b/interface-definitions/include/firewall/common-rule-ipv4.xml.i @@ -16,6 +16,7 @@ #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/source-destination-dynamic-group.xml.i> + #include <include/firewall/source-destination-remote-group.xml.i> </children> </node> <leafNode name="jump-target"> @@ -39,6 +40,7 @@ #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/source-destination-dynamic-group.xml.i> + #include <include/firewall/source-destination-remote-group.xml.i> </children> </node> <!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/common-rule-ipv6.xml.i b/interface-definitions/include/firewall/common-rule-ipv6.xml.i index bb176fe71..65ec415fb 100644 --- a/interface-definitions/include/firewall/common-rule-ipv6.xml.i +++ b/interface-definitions/include/firewall/common-rule-ipv6.xml.i @@ -16,6 +16,7 @@ #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/source-destination-dynamic-group-ipv6.xml.i> + #include <include/firewall/source-destination-remote-group.xml.i> </children> </node> <leafNode name="jump-target"> @@ -39,6 +40,7 @@ #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/source-destination-dynamic-group-ipv6.xml.i> + #include <include/firewall/source-destination-remote-group.xml.i> </children> </node> -<!-- include end -->
\ No newline at end of file +<!-- include end --> diff --git a/interface-definitions/include/firewall/global-options.xml.i b/interface-definitions/include/firewall/global-options.xml.i index 355b41fde..7393ff5c9 100644 --- a/interface-definitions/include/firewall/global-options.xml.i +++ b/interface-definitions/include/firewall/global-options.xml.i @@ -217,6 +217,14 @@ <help>Global firewall state-policy</help> </properties> <children> + <node name="offload"> + <properties> + <help>All stateful forward traffic is offloaded to a flowtable</help> + </properties> + <children> + #include <include/firewall/offload-target.xml.i> + </children> + </node> <node name="established"> <properties> <help>Global firewall policy for packets part of an established connection</help> diff --git a/interface-definitions/include/firewall/source-destination-remote-group.xml.i b/interface-definitions/include/firewall/source-destination-remote-group.xml.i new file mode 100644 index 000000000..16463c8eb --- /dev/null +++ b/interface-definitions/include/firewall/source-destination-remote-group.xml.i @@ -0,0 +1,17 @@ +<!-- include start from firewall/source-destination-remote-group.xml.i --> +<node name="group"> + <properties> + <help>Group</help> + </properties> + <children> + <leafNode name="remote-group"> + <properties> + <help>Group of remote addresses</help> + <completionHelp> + <path>firewall group remote-group</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/logging.xml.i b/interface-definitions/include/haproxy/logging.xml.i index e0af54fa4..315c959bf 100644 --- a/interface-definitions/include/haproxy/logging.xml.i +++ b/interface-definitions/include/haproxy/logging.xml.i @@ -4,7 +4,137 @@ <help>Logging parameters</help> </properties> <children> - #include <include/syslog-facility.xml.i> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth cron daemon kern lpr mail news syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7</list> + </completionHelp> + <constraint> + <regex>(auth|cron|daemon|kern|lpr|mail|news|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7)</regex> + </constraint> + <constraintErrorMessage>Invalid facility type</constraintErrorMessage> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <constraint> + <regex>(emerg|alert|crit|err|warning|notice|info|debug)</regex> + </constraint> + <constraintErrorMessage>Invalid loglevel</constraintErrorMessage> + </properties> + <defaultValue>err</defaultValue> + </leafNode> + </children> + </tagNode> </children> </node> <!-- include end --> 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/haproxy/timeout-check.xml.i b/interface-definitions/include/haproxy/timeout-check.xml.i new file mode 100644 index 000000000..d1217fac3 --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-check.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-check.xml.i --> +<leafNode name="check"> + <properties> + <help>Timeout in seconds for established connections</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Check timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout-client.xml.i b/interface-definitions/include/haproxy/timeout-client.xml.i new file mode 100644 index 000000000..2250ccdef --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-client.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-client.xml.i --> +<leafNode name="client"> + <properties> + <help>Maximum inactivity time on the client side</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout-connect.xml.i b/interface-definitions/include/haproxy/timeout-connect.xml.i new file mode 100644 index 000000000..da4f983af --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-connect.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-connect.xml.i --> +<leafNode name="connect"> + <properties> + <help>Set the maximum time to wait for a connection attempt to a server to succeed</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Connect timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout-server.xml.i b/interface-definitions/include/haproxy/timeout-server.xml.i new file mode 100644 index 000000000..f27d415c1 --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-server.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-server.xml.i --> +<leafNode name="server"> + <properties> + <help>Set the maximum inactivity time on the server side</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Server timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout.xml.i b/interface-definitions/include/haproxy/timeout.xml.i index 79e7303b1..a3a5a8a3e 100644 --- a/interface-definitions/include/haproxy/timeout.xml.i +++ b/interface-definitions/include/haproxy/timeout.xml.i @@ -4,42 +4,9 @@ <help>Timeout options</help> </properties> <children> - <leafNode name="check"> - <properties> - <help>Timeout in seconds for established connections</help> - <valueHelp> - <format>u32:1-3600</format> - <description>Check timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-3600"/> - </constraint> - </properties> - </leafNode> - <leafNode name="connect"> - <properties> - <help>Set the maximum time to wait for a connection attempt to a server to succeed</help> - <valueHelp> - <format>u32:1-3600</format> - <description>Connect timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-3600"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server"> - <properties> - <help>Set the maximum inactivity time on the server side</help> - <valueHelp> - <format>u32:1-3600</format> - <description>Server timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-3600"/> - </constraint> - </properties> - </leafNode> + #include <include/haproxy/timeout-check.xml.i> + #include <include/haproxy/timeout-connect.xml.i> + #include <include/haproxy/timeout-server.xml.i> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/interface/ipv6-address-interface-identifier.xml.i b/interface-definitions/include/interface/ipv6-address-interface-identifier.xml.i new file mode 100644 index 000000000..d173dfdb8 --- /dev/null +++ b/interface-definitions/include/interface/ipv6-address-interface-identifier.xml.i @@ -0,0 +1,15 @@ +<!-- include start from interface/ipv6-address-interface-identifier.xml.i --> +<leafNode name="interface-identifier"> + <properties> + <help>SLAAC interface identifier</help> + <valueHelp> + <format>::h:h:h:h</format> + <description>Interface identifier</description> + </valueHelp> + <constraint> + <regex>::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,3})</regex> + </constraint> + <constraintErrorMessage>Interface identifier format must start with :: and may contain up four hextets (::h:h:h:h)</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/ipv6-address.xml.i b/interface-definitions/include/interface/ipv6-address.xml.i deleted file mode 100644 index e1bdf02fd..000000000 --- a/interface-definitions/include/interface/ipv6-address.xml.i +++ /dev/null @@ -1,12 +0,0 @@ -<!-- include start from interface/ipv6-address.xml.i --> -<node name="address"> - <properties> - <help>IPv6 address configuration modes</help> - </properties> - <children> - #include <include/interface/ipv6-address-autoconf.xml.i> - #include <include/interface/ipv6-address-eui64.xml.i> - #include <include/interface/ipv6-address-no-default-link-local.xml.i> - </children> -</node> -<!-- include end --> diff --git a/interface-definitions/include/interface/ipv6-options-with-nd.xml.i b/interface-definitions/include/interface/ipv6-options-with-nd.xml.i new file mode 100644 index 000000000..5894104b3 --- /dev/null +++ b/interface-definitions/include/interface/ipv6-options-with-nd.xml.i @@ -0,0 +1,9 @@ + <node name="ipv6"> + <children> + <node name="address"> + <children> + #include <include/interface/ipv6-address-interface-identifier.xml.i> + </children> + </node> + </children> + </node> diff --git a/interface-definitions/include/interface/ipv6-options.xml.i b/interface-definitions/include/interface/ipv6-options.xml.i index ec6ec64ee..f84a9f2cd 100644 --- a/interface-definitions/include/interface/ipv6-options.xml.i +++ b/interface-definitions/include/interface/ipv6-options.xml.i @@ -8,9 +8,18 @@ #include <include/interface/base-reachable-time.xml.i> #include <include/interface/disable-forwarding.xml.i> #include <include/interface/ipv6-accept-dad.xml.i> - #include <include/interface/ipv6-address.xml.i> #include <include/interface/ipv6-dup-addr-detect-transmits.xml.i> #include <include/interface/source-validation.xml.i> + <node name="address"> + <properties> + <help>IPv6 address configuration modes</help> + </properties> + <children> + #include <include/interface/ipv6-address-autoconf.xml.i> + #include <include/interface/ipv6-address-eui64.xml.i> + #include <include/interface/ipv6-address-no-default-link-local.xml.i> + </children> + </node> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i index 02e7ab057..65ca10207 100644 --- a/interface-definitions/include/interface/vif-s.xml.i +++ b/interface-definitions/include/interface/vif-s.xml.i @@ -21,6 +21,7 @@ #include <include/interface/vlan-protocol.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> #include <include/interface/mtu-68-16000.xml.i> @@ -41,6 +42,7 @@ #include <include/interface/disable.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> #include <include/interface/mtu-68-16000.xml.i> diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i index ec3921bf6..87f91c5ce 100644 --- a/interface-definitions/include/interface/vif.xml.i +++ b/interface-definitions/include/interface/vif.xml.i @@ -46,6 +46,7 @@ </leafNode> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> #include <include/interface/mtu-68-16000.xml.i> diff --git a/interface-definitions/include/ip-address.xml.i b/interface-definitions/include/ip-address.xml.i new file mode 100644 index 000000000..6027e97ee --- /dev/null +++ b/interface-definitions/include/ip-address.xml.i @@ -0,0 +1,14 @@ +<!-- include start from ip-address.xml.i --> +<leafNode name="ip-address"> + <properties> + <help>Fixed IP address of static mapping</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address used in static mapping</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i index 35ce80be9..e0a7e62b6 100644 --- a/interface-definitions/include/isis/protocol-common-config.xml.i +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -418,6 +418,14 @@ #include <include/isis/redistribute-level-1-2.xml.i> </children> </node> + <node name="nhrp"> + <properties> + <help>Redistribute NHRP routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-level-1-2.xml.i> + </children> + </node> <node name="ospf"> <properties> <help>Redistribute OSPF routes into IS-IS</help> diff --git a/interface-definitions/include/monitoring/blackbox-exporter-module-commons.xml.i b/interface-definitions/include/monitoring/blackbox-exporter-module-commons.xml.i new file mode 100644 index 000000000..a97eb5232 --- /dev/null +++ b/interface-definitions/include/monitoring/blackbox-exporter-module-commons.xml.i @@ -0,0 +1,39 @@ +<!-- include start from monitoring/blackbox-module-commons.xml.i --> +<leafNode name="timeout"> + <properties> + <help>Timeout in seconds for the probe request</help> + <valueHelp> + <format>u32:1-60</format> + <description>Timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-60"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 60 seconds</constraintErrorMessage> + </properties> + <defaultValue>5</defaultValue> +</leafNode> +<leafNode name="preferred-ip-protocol"> + <properties> + <help>Preferred IP protocol for this module</help> + <valueHelp> + <format>ipv4</format> + <description>Prefer IPv4</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Prefer IPv6</description> + </valueHelp> + <constraint> + <regex>(ipv4|ipv6)</regex> + </constraint> + </properties> + <defaultValue>ip6</defaultValue> +</leafNode> +<leafNode name="ip-protocol-fallback"> + <properties> + <help>Allow fallback to other IP protocol if necessary</help> + <valueless/> + </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file 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/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i index cef832381..f597be64e 100644 --- a/interface-definitions/include/ospf/protocol-common-config.xml.i +++ b/interface-definitions/include/ospf/protocol-common-config.xml.i @@ -798,6 +798,16 @@ #include <include/route-map.xml.i> </children> </node> + <node name="nhrp"> + <properties> + <help>Redistribute NHRP routes</help> + </properties> + <children> + #include <include/ospf/metric.xml.i> + #include <include/ospf/metric-type.xml.i> + #include <include/route-map.xml.i> + </children> + </node> <node name="rip"> <properties> <help>Redistribute RIP routes</help> diff --git a/interface-definitions/include/policy/community-value-list.xml.i b/interface-definitions/include/policy/community-value-list.xml.i index 8c665c5f0..b1499440a 100644 --- a/interface-definitions/include/policy/community-value-list.xml.i +++ b/interface-definitions/include/policy/community-value-list.xml.i @@ -4,7 +4,6 @@ local-as no-advertise no-export - internet graceful-shutdown accept-own route-filter-translated-v4 @@ -35,10 +34,6 @@ <description>Well-known communities value NO_EXPORT 0xFFFFFF01</description> </valueHelp> <valueHelp> - <format>internet</format> - <description>Well-known communities value 0</description> -</valueHelp> -<valueHelp> <format>graceful-shutdown</format> <description>Well-known communities value GRACEFUL_SHUTDOWN 0xFFFF0000</description> </valueHelp> @@ -84,7 +79,7 @@ </valueHelp> <multi/> <constraint> - <regex>local-as|no-advertise|no-export|internet|graceful-shutdown|accept-own|route-filter-translated-v4|route-filter-v4|route-filter-translated-v6|route-filter-v6|llgr-stale|no-llgr|accept-own-nexthop|blackhole|no-peer</regex> + <regex>local-as|no-advertise|no-export|graceful-shutdown|accept-own|route-filter-translated-v4|route-filter-v4|route-filter-translated-v6|route-filter-v6|llgr-stale|no-llgr|accept-own-nexthop|blackhole|no-peer</regex> <validator name="bgp-regular-community"/> </constraint> <!-- include end --> diff --git a/interface-definitions/include/version/bgp-version.xml.i b/interface-definitions/include/version/bgp-version.xml.i index 6bed7189f..c90276151 100644 --- a/interface-definitions/include/version/bgp-version.xml.i +++ b/interface-definitions/include/version/bgp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/bgp-version.xml.i --> -<syntaxVersion component='bgp' version='5'></syntaxVersion> +<syntaxVersion component='bgp' version='6'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/firewall-version.xml.i b/interface-definitions/include/version/firewall-version.xml.i index a15cf0eec..1a8098297 100644 --- a/interface-definitions/include/version/firewall-version.xml.i +++ b/interface-definitions/include/version/firewall-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/firewall-version.xml.i --> -<syntaxVersion component='firewall' version='17'></syntaxVersion> +<syntaxVersion component='firewall' version='18'></syntaxVersion> <!-- 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/ids-version.xml.i b/interface-definitions/include/version/ids-version.xml.i index 9133be02b..6d4e92c21 100644 --- a/interface-definitions/include/version/ids-version.xml.i +++ b/interface-definitions/include/version/ids-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ids-version.xml.i --> -<syntaxVersion component='ids' version='1'></syntaxVersion> +<syntaxVersion component='ids' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/lldp-version.xml.i b/interface-definitions/include/version/lldp-version.xml.i index b41d80451..a7110691a 100644 --- a/interface-definitions/include/version/lldp-version.xml.i +++ b/interface-definitions/include/version/lldp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/lldp-version.xml.i --> -<syntaxVersion component='lldp' version='2'></syntaxVersion> +<syntaxVersion component='lldp' version='3'></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/include/version/policy-version.xml.i b/interface-definitions/include/version/policy-version.xml.i index db727fea9..5c53a4032 100644 --- a/interface-definitions/include/version/policy-version.xml.i +++ b/interface-definitions/include/version/policy-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/policy-version.xml.i --> -<syntaxVersion component='policy' version='8'></syntaxVersion> +<syntaxVersion component='policy' version='9'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/reverseproxy-version.xml.i b/interface-definitions/include/version/reverseproxy-version.xml.i index 4f09f2848..71f7def1a 100644 --- a/interface-definitions/include/version/reverseproxy-version.xml.i +++ b/interface-definitions/include/version/reverseproxy-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/reverseproxy-version.xml.i --> -<syntaxVersion component='reverse-proxy' version='2'></syntaxVersion> +<syntaxVersion component='reverse-proxy' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/system-version.xml.i b/interface-definitions/include/version/system-version.xml.i index 3ecf124c7..5cdece74a 100644 --- a/interface-definitions/include/version/system-version.xml.i +++ b/interface-definitions/include/version/system-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/system-version.xml.i --> -<syntaxVersion component='system' version='28'></syntaxVersion> +<syntaxVersion component='system' version='29'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/wanloadbalance-version.xml.i b/interface-definitions/include/version/wanloadbalance-version.xml.i index 59f8729cc..34c3c76ff 100644 --- a/interface-definitions/include/version/wanloadbalance-version.xml.i +++ b/interface-definitions/include/version/wanloadbalance-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/wanloadbalance-version.xml.i --> -<syntaxVersion component='wanloadbalance' version='3'></syntaxVersion> +<syntaxVersion component='wanloadbalance' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_bonding.xml.in b/interface-definitions/interfaces_bonding.xml.in index b17cad478..cdacae2d0 100644 --- a/interface-definitions/interfaces_bonding.xml.in +++ b/interface-definitions/interfaces_bonding.xml.in @@ -141,6 +141,7 @@ </leafNode> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> <leafNode name="mii-mon-interval"> <properties> diff --git a/interface-definitions/interfaces_bridge.xml.in b/interface-definitions/interfaces_bridge.xml.in index 29dd61df5..667ae3b19 100644 --- a/interface-definitions/interfaces_bridge.xml.in +++ b/interface-definitions/interfaces_bridge.xml.in @@ -93,6 +93,7 @@ </node> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> <leafNode name="enable-vlan"> diff --git a/interface-definitions/interfaces_ethernet.xml.in b/interface-definitions/interfaces_ethernet.xml.in index b3559a626..819ceb2cb 100644 --- a/interface-definitions/interfaces_ethernet.xml.in +++ b/interface-definitions/interfaces_ethernet.xml.in @@ -74,6 +74,7 @@ #include <include/interface/hw-id.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mtu-68-16000.xml.i> #include <include/interface/mirror.xml.i> diff --git a/interface-definitions/interfaces_geneve.xml.in b/interface-definitions/interfaces_geneve.xml.in index 990c5bd91..b85bd3b9e 100644 --- a/interface-definitions/interfaces_geneve.xml.in +++ b/interface-definitions/interfaces_geneve.xml.in @@ -21,8 +21,13 @@ #include <include/interface/disable.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mtu-1200-16000.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>6081</defaultValue> + </leafNode> <node name="parameters"> <properties> <help>GENEVE tunnel parameters</help> diff --git a/interface-definitions/interfaces_l2tpv3.xml.in b/interface-definitions/interfaces_l2tpv3.xml.in index 5f816c956..381e86bd0 100644 --- a/interface-definitions/interfaces_l2tpv3.xml.in +++ b/interface-definitions/interfaces_l2tpv3.xml.in @@ -55,6 +55,7 @@ </leafNode> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/source-address-ipv4-ipv6.xml.i> #include <include/interface/mirror.xml.i> #include <include/interface/mtu-68-16000.xml.i> diff --git a/interface-definitions/interfaces_macsec.xml.in b/interface-definitions/interfaces_macsec.xml.in index d825f8262..5279a9495 100644 --- a/interface-definitions/interfaces_macsec.xml.in +++ b/interface-definitions/interfaces_macsec.xml.in @@ -21,6 +21,7 @@ #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mirror.xml.i> <node name="security"> <properties> diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index 3c844107e..6510ed733 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -135,6 +135,7 @@ </node> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mirror.xml.i> <leafNode name="hash"> <properties> diff --git a/interface-definitions/interfaces_pppoe.xml.in b/interface-definitions/interfaces_pppoe.xml.in index f24bc41d8..66a774e21 100644 --- a/interface-definitions/interfaces_pppoe.xml.in +++ b/interface-definitions/interfaces_pppoe.xml.in @@ -88,6 +88,7 @@ </properties> <children> #include <include/interface/ipv6-address-autoconf.xml.i> + #include <include/interface/ipv6-address-interface-identifier.xml.i> </children> </node> #include <include/interface/adjust-mss.xml.i> diff --git a/interface-definitions/interfaces_pseudo-ethernet.xml.in b/interface-definitions/interfaces_pseudo-ethernet.xml.in index 031af3563..f13144bed 100644 --- a/interface-definitions/interfaces_pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces_pseudo-ethernet.xml.in @@ -25,6 +25,7 @@ #include <include/interface/vrf.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/source-interface-ethernet.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> diff --git a/interface-definitions/interfaces_virtual-ethernet.xml.in b/interface-definitions/interfaces_virtual-ethernet.xml.in index c4610feec..2dfbd50b8 100644 --- a/interface-definitions/interfaces_virtual-ethernet.xml.in +++ b/interface-definitions/interfaces_virtual-ethernet.xml.in @@ -21,6 +21,10 @@ #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable.xml.i> + #include <include/interface/mtu-68-16000.xml.i> + <leafNode name="mtu"> + <defaultValue>1500</defaultValue> + </leafNode> #include <include/interface/netns.xml.i> #include <include/interface/vif-s.xml.i> #include <include/interface/vif.xml.i> diff --git a/interface-definitions/interfaces_vxlan.xml.in b/interface-definitions/interfaces_vxlan.xml.in index 937acb123..f4cd4fcd2 100644 --- a/interface-definitions/interfaces_vxlan.xml.in +++ b/interface-definitions/interfaces_vxlan.xml.in @@ -45,6 +45,7 @@ </leafNode> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mtu-1200-16000.xml.i> #include <include/interface/mirror.xml.i> diff --git a/interface-definitions/interfaces_wireguard.xml.in b/interface-definitions/interfaces_wireguard.xml.in index ce49de038..33cb5864a 100644 --- a/interface-definitions/interfaces_wireguard.xml.in +++ b/interface-definitions/interfaces_wireguard.xml.in @@ -40,13 +40,23 @@ </properties> <defaultValue>0</defaultValue> </leafNode> - <leafNode name="private-key"> + <leafNode name="max-dns-retry"> <properties> - <help>Base64 encoded private key</help> + <help>DNS retries when resolve fails</help> + <valueHelp> + <format>u32:1-15</format> + <description>Maximum number of retries</description> + </valueHelp> <constraint> - <validator name="base64"/> + <validator name="numeric" argument="--range 1-15"/> </constraint> - <constraintErrorMessage>Key is not base64-encoded</constraintErrorMessage> + </properties> + <defaultValue>3</defaultValue> + </leafNode> + <leafNode name="private-key"> + <properties> + <help>Base64 encoded private key</help> + #include <include/constraint/wireguard-keys.xml.i> </properties> </leafNode> <tagNode name="peer"> @@ -62,20 +72,14 @@ #include <include/generic-description.xml.i> <leafNode name="public-key"> <properties> - <help>base64 encoded public key</help> - <constraint> - <validator name="base64"/> - </constraint> - <constraintErrorMessage>Key is not base64-encoded</constraintErrorMessage> + <help>Base64 encoded public key</help> + #include <include/constraint/wireguard-keys.xml.i> </properties> </leafNode> <leafNode name="preshared-key"> <properties> - <help>base64 encoded preshared key</help> - <constraint> - <validator name="base64"/> - </constraint> - <constraintErrorMessage>Key is not base64-encoded</constraintErrorMessage> + <help>Base64 encoded preshared key</help> + #include <include/constraint/wireguard-keys.xml.i> </properties> </leafNode> <leafNode name="allowed-ips"> @@ -104,6 +108,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/interfaces_wireless.xml.in b/interface-definitions/interfaces_wireless.xml.in index 474953500..1b5356caa 100644 --- a/interface-definitions/interfaces_wireless.xml.in +++ b/interface-definitions/interfaces_wireless.xml.in @@ -626,6 +626,7 @@ </leafNode> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/hw-id.xml.i> <leafNode name="isolate-stations"> <properties> diff --git a/interface-definitions/interfaces_wwan.xml.in b/interface-definitions/interfaces_wwan.xml.in index 1580c3bcb..552806d4e 100644 --- a/interface-definitions/interfaces_wwan.xml.in +++ b/interface-definitions/interfaces_wwan.xml.in @@ -38,6 +38,7 @@ </leafNode> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/ipv6-options-with-nd.xml.i> #include <include/interface/dial-on-demand.xml.i> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> diff --git a/interface-definitions/load-balancing_haproxy.xml.in b/interface-definitions/load-balancing_haproxy.xml.in index 742272436..f0f64e75a 100644 --- a/interface-definitions/load-balancing_haproxy.xml.in +++ b/interface-definitions/load-balancing_haproxy.xml.in @@ -4,7 +4,7 @@ <children> <node name="haproxy" owner="${vyos_conf_scripts_dir}/load-balancing_haproxy.py"> <properties> - <help>Configure haproxy</help> + <help>HAProxy TCP/HTTP Load Balancer</help> <priority>900</priority> </properties> <children> @@ -26,7 +26,7 @@ <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage> <valueHelp> <format>txt</format> - <description>Name of haproxy backend system</description> + <description>HAProxy backend system name</description> </valueHelp> <completionHelp> <path>load-balancing haproxy backend</path> @@ -48,6 +48,46 @@ <valueless/> </properties> </leafNode> + <node name="timeout"> + <properties> + <help>Timeout options</help> + </properties> + <children> + #include <include/haproxy/timeout-client.xml.i> + </children> + </node> + <node name="http-compression"> + <properties> + <help>Compress HTTP responses</help> + </properties> + <children> + <leafNode name="algorithm"> + <properties> + <help>Compression algorithm</help> + <completionHelp> + <list>gzip deflate identity raw-deflate</list> + </completionHelp> + <constraint> + <regex>(gzip|deflate|identity|raw-deflate)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="mime-type"> + <properties> + <help>MIME types to compress</help> + <valueHelp> + <format>txt</format> + <description>MIME type to compress</description> + </valueHelp> + <multi/> + <constraint> + <regex>\w+\/[-+.\w]+</regex> + </constraint> + <constraintErrorMessage>Invalid MIME type specified</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> <node name="ssl"> <properties> <help>SSL Certificate, SSL Key and CA</help> @@ -336,6 +376,29 @@ </leafNode> </children> </node> + <node name="timeout"> + <properties> + <help>Timeout options</help> + </properties> + <children> + #include <include/haproxy/timeout-check.xml.i> + <leafNode name="check"> + <defaultValue>5</defaultValue> + </leafNode> + #include <include/haproxy/timeout-connect.xml.i> + <leafNode name="connect"> + <defaultValue>10</defaultValue> + </leafNode> + #include <include/haproxy/timeout-client.xml.i> + <leafNode name="client"> + <defaultValue>50</defaultValue> + </leafNode> + #include <include/haproxy/timeout-server.xml.i> + <leafNode name="server"> + <defaultValue>50</defaultValue> + </leafNode> + </children> + </node> #include <include/interface/vrf.xml.i> </children> </node> diff --git a/interface-definitions/load-balancing_wan.xml.in b/interface-definitions/load-balancing_wan.xml.in index 310aa0343..f80440411 100644 --- a/interface-definitions/load-balancing_wan.xml.in +++ b/interface-definitions/load-balancing_wan.xml.in @@ -7,7 +7,7 @@ <children> <node name="wan" owner="${vyos_conf_scripts_dir}/load-balancing_wan.py"> <properties> - <help>Configure Wide Area Network (WAN) load-balancing</help> + <help>Wide Area Network (WAN) load-balancing</help> <priority>900</priority> </properties> <children> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index c59725c53..2c1babd5a 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -53,6 +53,7 @@ </properties> </leafNode> #include <include/nat-port.xml.i> + #include <include/firewall/source-destination-group-ipv6.xml.i> </children> </node> <node name="source"> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index 5c0b735ef..161f20b33 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -202,30 +202,6 @@ </node> </children> </tagNode> - <tagNode name="openssh"> - <properties> - <help>OpenSSH public and private keys</help> - </properties> - <children> - <node name="public"> - <properties> - <help>Public key</help> - </properties> - <children> - #include <include/pki/cli-public-key-base64.xml.i> - </children> - </node> - <node name="private"> - <properties> - <help>Private key</help> - </properties> - <children> - #include <include/pki/cli-private-key-base64.xml.i> - #include <include/pki/password-protected.xml.i> - </children> - </node> - </children> - </tagNode> <node name="openvpn"> <properties> <help>OpenVPN keys</help> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index cbab6173f..25dbf5581 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -202,7 +202,7 @@ <properties> <help>Regular expression to match against a community-list</help> <completionHelp> - <list>local-AS no-advertise no-export internet graceful-shutdown accept-own-nexthop accept-own route-filter-translated-v4 route-filter-v4 route-filter-translated-v6 route-filter-v6 llgr-stale no-llgr blackhole no-peer additive</list> + <list>local-AS no-advertise no-export graceful-shutdown accept-own-nexthop accept-own route-filter-translated-v4 route-filter-v4 route-filter-translated-v6 route-filter-v6 llgr-stale no-llgr blackhole no-peer additive</list> </completionHelp> <valueHelp> <format><aa:nn></format> @@ -221,10 +221,6 @@ <description>Well-known communities value NO_EXPORT 0xFFFFFF01</description> </valueHelp> <valueHelp> - <format>internet</format> - <description>Well-known communities value 0</description> - </valueHelp> - <valueHelp> <format>graceful-shutdown</format> <description>Well-known communities value GRACEFUL_SHUTDOWN 0xFFFF0000</description> </valueHelp> @@ -1096,6 +1092,20 @@ </constraint> </properties> </leafNode> + <leafNode name="source-vrf"> + <properties> + <help>Source vrf</help> + #include <include/constraint/vrf.xml.i> + <valueHelp> + <format>txt</format> + <description>VRF instance name</description> + </valueHelp> + <completionHelp> + <path>vrf name</path> + <list>default</list> + </completionHelp> + </properties> + </leafNode> #include <include/policy/tag.xml.i> </children> </node> diff --git a/interface-definitions/policy_route.xml.in b/interface-definitions/policy_route.xml.in index 9cc22540b..48f728923 100644 --- a/interface-definitions/policy_route.xml.in +++ b/interface-definitions/policy_route.xml.in @@ -35,6 +35,7 @@ #include <include/firewall/address-ipv6.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/geoip.xml.i> </children> </node> <node name="source"> @@ -45,6 +46,7 @@ #include <include/firewall/address-ipv6.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/geoip.xml.i> </children> </node> #include <include/policy/route-common.xml.i> @@ -90,6 +92,7 @@ #include <include/firewall/address.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/geoip.xml.i> </children> </node> <node name="source"> @@ -100,6 +103,7 @@ #include <include/firewall/address.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/geoip.xml.i> </children> </node> #include <include/policy/route-common.xml.i> diff --git a/interface-definitions/protocols_mpls.xml.in b/interface-definitions/protocols_mpls.xml.in index 831601fc6..fc1864f38 100644 --- a/interface-definitions/protocols_mpls.xml.in +++ b/interface-definitions/protocols_mpls.xml.in @@ -524,7 +524,29 @@ </node> </children> </node> - #include <include/generic-interface-multi.xml.i> + <tagNode name="interface"> + <properties> + <help>Interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> + </properties> + <children> + <leafNode name="disable-establish-hello"> + <properties> + <help>Disable response to hello packet with an additional hello LDP packet</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> </children> </node> <node name="parameters"> 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><fqdn></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><1-4294967295></format> + <description>NHRP network id</description> + </valueHelp> + </properties> + </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/protocols_rip.xml.in b/interface-definitions/protocols_rip.xml.in index 0edd8f2ce..745280fd7 100644 --- a/interface-definitions/protocols_rip.xml.in +++ b/interface-definitions/protocols_rip.xml.in @@ -209,6 +209,14 @@ #include <include/rip/redistribute.xml.i> </children> </node> + <node name="nhrp"> + <properties> + <help>Redistribute NHRP routes</help> + </properties> + <children> + #include <include/rip/redistribute.xml.i> + </children> + </node> <node name="ospf"> <properties> <help>Redistribute OSPF routes</help> diff --git a/interface-definitions/protocols_rpki.xml.in b/interface-definitions/protocols_rpki.xml.in index 54d69eadb..9e2e84717 100644 --- a/interface-definitions/protocols_rpki.xml.in +++ b/interface-definitions/protocols_rpki.xml.in @@ -42,6 +42,7 @@ </constraint> </properties> </leafNode> + #include <include/source-address-ipv4.xml.i> <node name="ssh"> <properties> <help>RPKI SSH connection settings</help> diff --git a/interface-definitions/protocols_segment-routing.xml.in b/interface-definitions/protocols_segment-routing.xml.in index c299f624e..688b253b6 100644 --- a/interface-definitions/protocols_segment-routing.xml.in +++ b/interface-definitions/protocols_segment-routing.xml.in @@ -126,6 +126,25 @@ </properties> <defaultValue>24</defaultValue> </leafNode> + <leafNode name="format"> + <properties> + <help>SRv6 SID format</help> + <completionHelp> + <list>uncompressed-f4024 usid-f3216</list> + </completionHelp> + <valueHelp> + <format>uncompressed-f4024</format> + <description>Uncompressed f4024 format</description> + </valueHelp> + <valueHelp> + <format>usid-f3216</format> + <description>usid-f3216 format</description> + </valueHelp> + <constraint> + <regex>(uncompressed-f4024|usid-f3216)</regex> + </constraint> + </properties> + </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/protocols_static.xml.in b/interface-definitions/protocols_static.xml.in index d8e0ee56b..c721bb3fc 100644 --- a/interface-definitions/protocols_static.xml.in +++ b/interface-definitions/protocols_static.xml.in @@ -65,14 +65,8 @@ #include <include/static/static-route6.xml.i> <tagNode name="table"> <properties> - <help>Policy route table number</help> - <valueHelp> - <format>u32:1-200</format> - <description>Policy route table number</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-200"/> - </constraint> + <help>Non-main Kernel Routing Table</help> + #include <include/constraint/protocols-static-table.xml.i> </properties> <children> <!-- diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in index cb5f9a804..78f1cea4e 100644 --- a/interface-definitions/service_dhcp-server.xml.in +++ b/interface-definitions/service_dhcp-server.xml.in @@ -10,12 +10,111 @@ </properties> <children> #include <include/generic-disable-node.xml.i> - <leafNode name="dynamic-dns-update"> + <node name="dynamic-dns-update"> <properties> <help>Dynamically update Domain Name System (RFC4702)</help> - <valueless/> </properties> - </leafNode> + <children> + #include <include/dhcp/ddns-settings.xml.i> + <tagNode name="tsig-key"> + <properties> + <help>TSIG key definition for DNS updates</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, hyphen and underscore</constraintErrorMessage> + </properties> + <children> + <leafNode name="algorithm"> + <properties> + <help>TSIG key algorithm</help> + <completionHelp> + <list>md5 sha1 sha224 sha256 sha384 sha512</list> + </completionHelp> + <valueHelp> + <format>md5</format> + <description>MD5 HMAC algorithm</description> + </valueHelp> + <valueHelp> + <format>sha1</format> + <description>SHA1 HMAC algorithm</description> + </valueHelp> + <valueHelp> + <format>sha224</format> + <description>SHA224 HMAC algorithm</description> + </valueHelp> + <valueHelp> + <format>sha256</format> + <description>SHA256 HMAC algorithm</description> + </valueHelp> + <valueHelp> + <format>sha384</format> + <description>SHA384 HMAC algorithm</description> + </valueHelp> + <valueHelp> + <format>sha512</format> + <description>SHA512 HMAC algorithm</description> + </valueHelp> + <constraint> + <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex> + </constraint> + <constraintErrorMessage>Invalid TSIG key algorithm</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="secret"> + <properties> + <help>TSIG key secret (base64-encoded)</help> + <constraint> + <validator name="base64"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="forward-domain"> + <properties> + <help>Forward DNS domain name</help> + <constraint> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid forward DNS domain name</constraintErrorMessage> + </properties> + <children> + <leafNode name="key-name"> + <properties> + <help>TSIG key name for forward DNS updates</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, numbers, hyphen and underscore</constraintErrorMessage> + </properties> + </leafNode> + #include <include/dhcp/ddns-dns-server.xml.i> + </children> + </tagNode> + <tagNode name="reverse-domain"> + <properties> + <help>Reverse DNS domain name</help> + <constraint> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid reverse DNS domain name</constraintErrorMessage> + </properties> + <children> + <leafNode name="key-name"> + <properties> + <help>TSIG key name for reverse DNS updates</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, numbers, hyphen and underscore</constraintErrorMessage> + </properties> + </leafNode> + #include <include/dhcp/ddns-dns-server.xml.i> + </children> + </tagNode> + </children> + </node> <node name="high-availability"> <properties> <help>DHCP high availability configuration</help> @@ -105,6 +204,14 @@ <constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage> </properties> <children> + <node name="dynamic-dns-update"> + <properties> + <help>Dynamically update Domain Name System (RFC4702)</help> + </properties> + <children> + #include <include/dhcp/ddns-settings.xml.i> + </children> + </node> <leafNode name="authoritative"> <properties> <help>Option to make DHCP server authoritative for this physical network</help> @@ -112,6 +219,7 @@ </properties> </leafNode> #include <include/dhcp/option-v4.xml.i> + #include <include/dhcp/ping-check.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> <tagNode name="subnet"> @@ -128,8 +236,17 @@ </properties> <children> #include <include/dhcp/option-v4.xml.i> + #include <include/dhcp/ping-check.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> + <node name="dynamic-dns-update"> + <properties> + <help>Dynamically update Domain Name System (RFC4702)</help> + </properties> + <children> + #include <include/dhcp/ddns-settings.xml.i> + </children> + </node> <leafNode name="exclude"> <properties> <help>IP address to exclude from DHCP lease range</help> @@ -211,18 +328,7 @@ #include <include/dhcp/option-v4.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> - <leafNode name="ip-address"> - <properties> - <help>Fixed IP address of static mapping</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address used in static mapping</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> + #include <include/ip-address.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/duid.xml.i> </children> diff --git a/interface-definitions/service_ids_ddos-protection.xml.in b/interface-definitions/service_ids_ddos-protection.xml.in deleted file mode 100644 index 3ef2640b3..000000000 --- a/interface-definitions/service_ids_ddos-protection.xml.in +++ /dev/null @@ -1,167 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="service"> - <children> - <node name="ids"> - <properties> - <help>Intrusion Detection System</help> - </properties> - <children> - <node name="ddos-protection" owner="${vyos_conf_scripts_dir}/service_ids_ddos-protection.py"> - <properties> - <help>FastNetMon detection and protection parameters</help> - <priority>731</priority> - </properties> - <children> - <leafNode name="alert-script"> - <properties> - <help>Path to fastnetmon alert script</help> - </properties> - </leafNode> - <leafNode name="ban-time"> - <properties> - <help>How long we should keep an IP in blocked state</help> - <valueHelp> - <format>u32:1-4294967294</format> - <description>Time in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967294"/> - </constraint> - </properties> - <defaultValue>1900</defaultValue> - </leafNode> - <leafNode name="direction"> - <properties> - <help>Direction for processing traffic</help> - <completionHelp> - <list>in out</list> - </completionHelp> - <constraint> - <regex>(in|out)</regex> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="excluded-network"> - <properties> - <help>Specify IPv4 and IPv6 networks which are going to be excluded from protection</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 prefix(es) to exclude</description> - </valueHelp> - <valueHelp> - <format>ipv6net</format> - <description>IPv6 prefix(es) to exclude</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - <validator name="ipv6-prefix"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="listen-interface"> - <properties> - <help>Listen interface for mirroring traffic</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - <multi/> - </properties> - </leafNode> - <leafNode name="mode"> - <properties> - <help>Traffic capture mode</help> - <completionHelp> - <list>mirror sflow</list> - </completionHelp> - <valueHelp> - <format>mirror</format> - <description>Listen to mirrored traffic</description> - </valueHelp> - <valueHelp> - <format>sflow</format> - <description>Capture sFlow flows</description> - </valueHelp> - <constraint> - <regex>(mirror|sflow)</regex> - </constraint> - </properties> - </leafNode> - <node name="sflow"> - <properties> - <help>Sflow settings</help> - </properties> - <children> - #include <include/listen-address-ipv4-single.xml.i> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>6343</defaultValue> - </leafNode> - </children> - </node> - <leafNode name="network"> - <properties> - <help>Specify IPv4 and IPv6 networks which belong to you</help> - <valueHelp> - <format>ipv4net</format> - <description>Your IPv4 prefix(es)</description> - </valueHelp> - <valueHelp> - <format>ipv6net</format> - <description>Your IPv6 prefix(es)</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - <validator name="ipv6-prefix"/> - </constraint> - <multi/> - </properties> - </leafNode> - <node name="threshold"> - <properties> - <help>Attack limits thresholds</help> - </properties> - <children> - <node name="general"> - <properties> - <help>General threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - <node name="tcp"> - <properties> - <help>TCP threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - <node name="udp"> - <properties> - <help>UDP threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - <node name="icmp"> - <properties> - <help>ICMP threshold</help> - </properties> - <children> - #include <include/ids/threshold.xml.i> - </children> - </node> - </children> - </node> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in index 6cc4471af..fe9d32bbd 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -70,18 +70,7 @@ <constraintErrorMessage>VLAN IDs need to be in range 1-4094</constraintErrorMessage> </properties> </leafNode> - <leafNode name="static-ip"> - <properties> - <help>Static client IP address</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> + #include <include/ip-address.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/service_lldp.xml.in b/interface-definitions/service_lldp.xml.in index 51a9f9cce..a189cc13b 100644 --- a/interface-definitions/service_lldp.xml.in +++ b/interface-definitions/service_lldp.xml.in @@ -29,7 +29,34 @@ </constraint> </properties> <children> - #include <include/generic-disable-node.xml.i> + <leafNode name="mode"> + <properties> + <help>Set LLDP receive/transmit operation mode of this interface</help> + <completionHelp> + <list>disable rx-tx tx rx</list> + </completionHelp> + <valueHelp> + <format>disable</format> + <description>Do not process or send LLDP messages</description> + </valueHelp> + <valueHelp> + <format>rx-tx</format> + <description>Send and process LLDP messages</description> + </valueHelp> + <valueHelp> + <format>rx</format> + <description>Process incoming LLDP messages</description> + </valueHelp> + <valueHelp> + <format>tx</format> + <description>Send LLDP messages</description> + </valueHelp> + <constraint> + <regex>(disable|rx-tx|tx|rx)</regex> + </constraint> + </properties> + <defaultValue>rx-tx</defaultValue> + </leafNode> <node name="location"> <properties> <help>LLDP-MED location data</help> 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/service_monitoring_prometheus.xml.in b/interface-definitions/service_monitoring_prometheus.xml.in index 24f31e15c..8bcebf5f3 100644 --- a/interface-definitions/service_monitoring_prometheus.xml.in +++ b/interface-definitions/service_monitoring_prometheus.xml.in @@ -21,6 +21,19 @@ <defaultValue>9100</defaultValue> </leafNode> #include <include/interface/vrf.xml.i> + <node name="collectors"> + <properties> + <help>Collectors specific configuration</help> + </properties> + <children> + <leafNode name="textfile"> + <properties> + <help>Enables textfile collector to read from /run/node_exporter/collector</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> </children> </node> <node name="frr-exporter"> @@ -36,6 +49,82 @@ #include <include/interface/vrf.xml.i> </children> </node> + <node name="blackbox-exporter"> + <properties> + <help>Prometheus exporter for probing endpoints</help> + </properties> + <children> + #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>9115</defaultValue> + </leafNode> + #include <include/interface/vrf.xml.i> + <node name="modules"> + <properties> + <help>Configure blackbox exporter modules</help> + </properties> + <children> + <node name="dns"> + <properties> + <help>Configure dns module</help> + </properties> + <children> + <tagNode name="name"> + <properties> + <help>Name of the dns module</help> + </properties> + <children> + <leafNode name="query-name"> + <properties> + <help>Name to be queried</help> + <constraint> + <validator name="fqdn"/> + </constraint> + </properties> + </leafNode> + <leafNode name="query-type"> + <properties> + <help>DNS query type</help> + <valueHelp> + <format>ANY</format> + <description>Query any DNS record</description> + </valueHelp> + <valueHelp> + <format>A</format> + <description>Query IPv4 address record</description> + </valueHelp> + <valueHelp> + <format>AAAA</format> + <description>Query IPv6 address record</description> + </valueHelp> + </properties> + <defaultValue>ANY</defaultValue> + </leafNode> + #include <include/monitoring/blackbox-exporter-module-commons.xml.i> + </children> + </tagNode> + </children> + </node> + <node name="icmp"> + <properties> + <help>Configure icmp module</help> + </properties> + <children> + <tagNode name="name"> + <properties> + <help>Name of the icmp module</help> + </properties> + <children> + #include <include/monitoring/blackbox-exporter-module-commons.xml.i> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> </children> </node> </children> diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in index 3fd33540a..7f96cdb19 100644 --- a/interface-definitions/service_router-advert.xml.in +++ b/interface-definitions/service_router-advert.xml.in @@ -255,6 +255,19 @@ </leafNode> </children> </tagNode> + <leafNode name="auto-ignore"> + <properties> + <help>IPv6 prefix to be excluded in Router Advertisements (RAs) - use in conjunction with the ::/64 wildcard prefix</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix to be excluded</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> <tagNode name="prefix"> <properties> <help>IPv6 prefix to be advertised in Router Advertisements (RAs)</help> diff --git a/interface-definitions/service_snmp.xml.in b/interface-definitions/service_snmp.xml.in index f23151ef9..cc21f5b8b 100644 --- a/interface-definitions/service_snmp.xml.in +++ b/interface-definitions/service_snmp.xml.in @@ -304,7 +304,6 @@ </constraint> <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage> </properties> - <defaultValue></defaultValue> </leafNode> <tagNode name="group"> <properties> 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_option.xml.in b/interface-definitions/system_option.xml.in index 638ac1a3d..c0ea958a2 100644 --- a/interface-definitions/system_option.xml.in +++ b/interface-definitions/system_option.xml.in @@ -37,7 +37,145 @@ <help>Kernel boot parameters</help> </properties> <children> - <leafNode name="disable-mitigations"> + <node name="cpu"> + <properties> + <help>CPU settings</help> + </properties> + <children> + <leafNode name="disable-nmi-watchdog"> + <properties> + <help>Disable the NMI watchdog for detecting hard CPU lockups</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="isolate-cpus"> + <properties> + <help>Isolate specified CPUs from the scheduler</help> + <valueHelp> + <format>u32:0-511</format> + <description>CPU core</description> + </valueHelp> + <valueHelp> + <format><start-end></format> + <description>CPU core range (examples: "1", "4-7", "1,2-5,7")</description> + </valueHelp> + <constraint> + <validator name="cpu"/> + </constraint> + </properties> + </leafNode> + <leafNode name="nohz-full"> + <properties> + <help>Enable full tickless mode for specified CPUs</help> + <valueHelp> + <format>u32:0-511</format> + <description>CPU core</description> + </valueHelp> + <valueHelp> + <format><start-end></format> + <description>CPU core range (examples: "1", "4-7", "1,2-5,7")</description> + </valueHelp> + <constraint> + <validator name="cpu"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rcu-no-cbs"> + <properties> + <help>Offload Read-Copy-Update (RCU) callback processing to specified CPUs</help> + <valueHelp> + <format>u32:0-511</format> + <description>CPU core</description> + </valueHelp> + <valueHelp> + <format><start-end></format> + <description>CPU core range (examples: "1", "4-7", "1,2-5,7")</description> + </valueHelp> + <constraint> + <validator name="cpu"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="memory"> + <properties> + <help>Memory settings</help> + </properties> + <children> + <leafNode name="disable-numa-balancing"> + <properties> + <help>Disable automatic NUMA memory balancing</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="default-hugepage-size"> + <properties> + <help>Set default hugepage size (e.g., 2M, 1G)</help> + <completionHelp> + <list>2M 1G</list> + </completionHelp> + <valueHelp> + <format>2M</format> + <description>2 megabytes</description> + </valueHelp> + <valueHelp> + <format>1G</format> + <description>1 gigabyte</description> + </valueHelp> + <constraint> + <regex>(2M|1G)</regex> + </constraint> + </properties> + </leafNode> + <tagNode name="hugepage-size"> + <properties> + <help>Set hugepage size for allocation (e.g., 2M, 1G)</help> + <completionHelp> + <list>2M 1G</list> + </completionHelp> + <valueHelp> + <format>2M</format> + <description>2 megabytes</description> + </valueHelp> + <valueHelp> + <format>1G</format> + <description>1 gigabyte</description> + </valueHelp> + <constraint> + <regex>(2M|1G)</regex> + </constraint> + </properties> + <children> + <leafNode name="hugepage-count"> + <properties> + <help>Allocate number of hugepages for system use</help> + <valueHelp> + <format>u32</format> + <description>Number of hugepages</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-100000"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="disable-hpet"> + <properties> + <help>Disable High Precision Event Timer (HPET)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-mce"> + <properties> + <help>Disable Machine Check Exceptions (MCE) reporting and handling</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-mitigations"> <properties> <help>Disable all optional CPU mitigations</help> <valueless/> @@ -69,6 +207,18 @@ </valueHelp> </properties> </leafNode> + <leafNode name="disable-softlockup"> + <properties> + <help>Disable soft lockup detector for kernel threads</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="quiet"> + <properties> + <help>Disable most log messages</help> + <valueless/> + </properties> + </leafNode> <node name="debug"> <properties> <help>Dynamic debugging for kernel module</help> 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/system_syslog.xml.in b/interface-definitions/system_syslog.xml.in index 0a9a00572..116cbde73 100644 --- a/interface-definitions/system_syslog.xml.in +++ b/interface-definitions/system_syslog.xml.in @@ -8,28 +8,17 @@ <priority>400</priority> </properties> <children> - <tagNode name="user"> + <node name="console"> <properties> - <help>Logging to specific terminal of given user</help> - <completionHelp> - <path>system login user</path> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>Local user account</description> - </valueHelp> - <constraint> - #include <include/constraint/login-username.xml.i> - </constraint> - <constraintErrorMessage>illegal characters in user</constraintErrorMessage> + <help>Log to system console (/dev/console)</help> </properties> <children> #include <include/syslog-facility.xml.i> </children> - </tagNode> - <tagNode name="host"> + </node> + <tagNode name="remote"> <properties> - <help>Logging to remote host</help> + <help>Log to remote host</help> <constraint> <validator name="ip-address"/> <validator name="fqdn"/> @@ -49,11 +38,6 @@ </valueHelp> </properties> <children> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>514</defaultValue> - </leafNode> - #include <include/protocol-tcp-udp.xml.i> #include <include/syslog-facility.xml.i> <node name="format"> <properties> @@ -62,98 +46,63 @@ <children> <leafNode name="octet-counted"> <properties> - <help>Allows for the transmission of all characters inside a syslog message</help> + <help>Allows for the transmission of multi-line messages (TCP only)</help> <valueless/> </properties> </leafNode> <leafNode name="include-timezone"> <properties> - <help>Include system timezone in syslog message</help> + <help>Use RFC 5424 format (with RFC 3339 timestamp and timezone)</help> <valueless/> </properties> </leafNode> </children> </node> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>514</defaultValue> + </leafNode> + #include <include/protocol-tcp-udp.xml.i> + #include <include/source-address-ipv4-ipv6.xml.i> + #include <include/interface/vrf.xml.i> </children> </tagNode> - <node name="global"> + <node name="local"> <properties> - <help>Logging to system standard location</help> + <help>Log to standard system location /var/log/messages</help> </properties> <children> #include <include/syslog-facility.xml.i> - <node name="marker"> - <properties> - <help>mark messages sent to syslog</help> - </properties> - <children> - <leafNode name="interval"> - <properties> - <help>time interval how often a mark message is being sent in seconds</help> - <constraint> - <validator name="numeric" argument="--positive"/> - </constraint> - </properties> - <defaultValue>1200</defaultValue> - </leafNode> - </children> - </node> - <leafNode name="preserve-fqdn"> - <properties> - <help>uses FQDN for logging</help> - <valueless/> - </properties> - </leafNode> </children> </node> - <tagNode name="file"> + <node name="marker"> <properties> - <help>Logging to a file</help> - <constraint> - <regex>[a-zA-Z0-9\-_.]{1,255}</regex> - </constraint> - <constraintErrorMessage>illegal characters in filename or filename longer than 255 characters</constraintErrorMessage> + <help>Mark messages sent to syslog</help> </properties> <children> - <node name="archive"> + #include <include/generic-disable-node.xml.i> + <leafNode name="interval"> <properties> - <help>Log file size and rotation characteristics</help> + <help>Mark message interval</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> </properties> - <children> - <leafNode name="file"> - <properties> - <help>Number of saved files</help> - <constraint> - <regex>[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> - </properties> - <defaultValue>5</defaultValue> - </leafNode> - <leafNode name="size"> - <properties> - <help>Size of log files in kbytes</help> - <constraint> - <regex>[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in size</constraintErrorMessage> - </properties> - <defaultValue>256</defaultValue> - </leafNode> - </children> - </node> - #include <include/syslog-facility.xml.i> + <defaultValue>1200</defaultValue> + </leafNode> </children> - </tagNode> - <node name="console"> + </node> + <leafNode name="preserve-fqdn"> <properties> - <help>logging to serial console</help> + <help>Always include domain portion in hostname</help> + <valueless/> </properties> - <children> - #include <include/syslog-facility.xml.i> - </children> - </node> - #include <include/interface/vrf.xml.i> + </leafNode> </children> </node> </children> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 0cf526fad..873a4f882 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -1244,6 +1244,63 @@ <children> #include <include/ipsec/bind.xml.i> #include <include/ipsec/esp-group.xml.i> + <node name="traffic-selector"> + <properties> + <help>Traffic-selectors parameters</help> + </properties> + <children> + <node name="local"> + <properties> + <help>Local parameters for interesting traffic</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>Local IPv4 or IPv6 prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>Local IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>Local IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="remote"> + <properties> + <help>Remote parameters for interesting traffic</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>Remote IPv4 or IPv6 prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>Remote IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>Remote IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> </children> </node> </children> diff --git a/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/libvyosconfig b/libvyosconfig new file mode 160000 +Subproject 1dedc69476d707718031c45b53b626da8badf86 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/firewall.xml.in b/op-mode-definitions/firewall.xml.in index 82e6c8668..21159eb1b 100755 --- a/op-mode-definitions/firewall.xml.in +++ b/op-mode-definitions/firewall.xml.in @@ -14,9 +14,16 @@ <path>firewall group address-group</path> <path>firewall group network-group</path> <path>firewall group port-group</path> + <path>firewall group domain-group</path> + <path>firewall group dynamic-group address-group</path> + <path>firewall group dynamic-group ipv6-address-group</path> <path>firewall group interface-group</path> <path>firewall group ipv6-address-group</path> <path>firewall group ipv6-network-group</path> + <path>firewall group mac-group</path> + <path>firewall group network-group</path> + <path>firewall group port-group</path> + <path>firewall group remote-group</path> </completionHelp> </properties> <children> diff --git a/op-mode-definitions/generate_tech-support_archive.xml.in b/op-mode-definitions/generate_tech-support_archive.xml.in index fc664eb90..65c93541e 100644 --- a/op-mode-definitions/generate_tech-support_archive.xml.in +++ b/op-mode-definitions/generate_tech-support_archive.xml.in @@ -11,12 +11,27 @@ <properties> <help>Generate tech support archive</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/tech_support.py show --raw | gzip> $4.json.gz</command> + <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py</command> </node> <tagNode name="archive"> <properties> <help>Generate tech support archive to defined location</help> <completionHelp> + <list> <file> <scp://user:passwd@host> <ftp://user:passwd@host></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py $4</command> + </tagNode> + <node name="machine-readable-archive"> + <properties> + <help>Generate tech support archive</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/tech_support.py show --raw | gzip> $4.json.gz</command> + </node> + <tagNode name="machine-readable-archive"> + <properties> + <help>Generate tech support archive to defined location</help> + <completionHelp> <list> <file> </list> </completionHelp> </properties> diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i new file mode 100644 index 000000000..bb95ce3f5 --- /dev/null +++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i @@ -0,0 +1,22 @@ +<!-- included start from bgp/afi-ipv4-ipv6-vpn-rd.xml.i --> +<tagNode name="rd"> + <properties> + <help>Display routes matching the route distinguisher</help> + <completionHelp> + <list>ASN:NN IPADDRESS:NN all</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <tagNode name=""> + <properties> + <help>Show IP routes of specified prefix</help> + <completionHelp> + <list><x.x.x.x/x> <x:x:x:x:x:x:x:x/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </tagNode> + </children> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i index f6737c8bd..a9fb6e255 100644 --- a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i +++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i @@ -18,6 +18,7 @@ <children> #include <include/bgp/afi-common.xml.i> #include <include/bgp/afi-ipv4-ipv6-common.xml.i> + #include <include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i> </children> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </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/install-mok.xml.in b/op-mode-definitions/install-mok.xml.in index 18526a354..c7e62349a 100644 --- a/op-mode-definitions/install-mok.xml.in +++ b/op-mode-definitions/install-mok.xml.in @@ -6,7 +6,7 @@ <properties> <help>Install Secure Boot MOK (Machine Owner Key)</help> </properties> - <command>if test -f /var/lib/shim-signed/mok/MOK.der; then sudo mokutil --ignore-keyring --import /var/lib/shim-signed/mok/MOK.der; else echo "Secure Boot Machine Owner Key not found"; fi</command> + <command>if test -f /var/lib/shim-signed/mok/vyos-dev-2025-shim.der; then sudo mokutil --ignore-keyring --import /var/lib/shim-signed/mok/vyos-dev-2025-shim.der; else echo "Secure Boot Machine Owner Key not found"; fi</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/load-balacing_haproxy.in b/op-mode-definitions/load-balancing_haproxy.xml.in index c3d6c799b..8de7ae97f 100644 --- a/op-mode-definitions/load-balacing_haproxy.in +++ b/op-mode-definitions/load-balancing_haproxy.xml.in @@ -16,7 +16,7 @@ <properties> <help>Show load-balancing haproxy</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/load-balacing_haproxy.py show</command> + <command>sudo ${vyos_op_scripts_dir}/load-balancing_haproxy.py show</command> </node> </children> </node> diff --git a/op-mode-definitions/load-balancing_wan.xml.in b/op-mode-definitions/load-balancing_wan.xml.in new file mode 100644 index 000000000..91c57c1f4 --- /dev/null +++ b/op-mode-definitions/load-balancing_wan.xml.in @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="restart"> + <children> + <node name="wan-load-balance"> + <properties> + <help>Restart Wide Area Network (WAN) load-balancing daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name load-balancing_wan</command> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="wan-load-balance"> + <properties> + <help>Show Wide Area Network (WAN) load-balancing information</help> + </properties> + <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_summary</command> + <children> + <node name="connection"> + <properties> + <help>Show Wide Area Network (WAN) load-balancing flow</help> + </properties> + <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_connection</command> + </node> + <node name="status"> + <properties> + <help>Show WAN load-balancing statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_status</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition>
\ No newline at end of file diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index c9dc49b3a..a6aa6f05e 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -17,19 +17,6 @@ </properties> <command>SYSTEMD_COLORS=false grc journalctl --no-hostname --follow --boot</command> </node> - <node name="ids"> - <properties> - <help>Monitor Intrusion Detection System log</help> - </properties> - <children> - <leafNode name="ddos-protection"> - <properties> - <help>Monitor last lines of DDOS protection</help> - </properties> - <command>journalctl --no-hostname --follow --boot --unit fastnetmon.service</command> - </leafNode> - </children> - </node> <leafNode name="certbot"> <properties> <help>Monitor last lines of certbot log</help> @@ -120,6 +107,12 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit frr.service</command> </leafNode> + <leafNode name="haproxy"> + <properties> + <help>Monitor last lines of HAProxy log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit haproxy.service</command> + </leafNode> <leafNode name="ipoe-server"> <properties> <help>Monitor last lines of IP over Ethernet server log</help> @@ -377,6 +370,12 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit vyos-configd.service</command> </leafNode> + <leafNode name="vyos-domain-resolver"> + <properties> + <help>Monitor last lines of VyOS domain resolver daemon log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit vyos-domain-resolver.service</command> + </leafNode> <node name="wireless"> <properties> <help>Monitor last lines of Wireless interface log</help> 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/clear-session.xml.in b/op-mode-definitions/reset-session.xml.in index bfafe6312..1e52e278b 100644 --- a/op-mode-definitions/clear-session.xml.in +++ b/op-mode-definitions/reset-session.xml.in @@ -1,6 +1,6 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="clear"> + <node name="reset"> <children> <tagNode name="session"> <properties> 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-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in index 1212ab1f9..40fadac8b 100644 --- a/op-mode-definitions/show-bridge.xml.in +++ b/op-mode-definitions/show-bridge.xml.in @@ -7,6 +7,20 @@ <help>Show bridging information</help> </properties> <children> + <node name="spanning-tree"> + <properties> + <help>View Spanning Tree info for all bridges</help> + </properties> + <command>${vyos_op_scripts_dir}/stp.py show_stp</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed Spanning Tree info for all bridges</help> + </properties> + <command>${vyos_op_scripts_dir}/stp.py show_stp --detail</command> + </leafNode> + </children> + </node> <node name="vlan"> <properties> <help>View the VLAN filter settings of the bridge</help> @@ -44,6 +58,20 @@ </properties> <command>bridge -c link show | grep "master $3"</command> <children> + <node name="spanning-tree"> + <properties> + <help>View Spanning Tree info for specified bridges</help> + </properties> + <command>${vyos_op_scripts_dir}/stp.py show_stp --ifname=$3</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed Spanning Tree info for specified bridge</help> + </properties> + <command>${vyos_op_scripts_dir}/stp.py show_stp --ifname=$3 --detail</command> + </leafNode> + </children> + </node> <leafNode name="mdb"> <properties> <help>Displays the multicast group database for the bridge</help> 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-interfaces.xml.in b/op-mode-definitions/show-interfaces.xml.in index 09466647d..2d94080c7 100644 --- a/op-mode-definitions/show-interfaces.xml.in +++ b/op-mode-definitions/show-interfaces.xml.in @@ -26,6 +26,48 @@ </properties> <command>${vyos_op_scripts_dir}/interfaces.py show_summary</command> </leafNode> + <tagNode name="kernel"> + <properties> + <completionHelp> + <script>ip -j link show | jq -r '.[].ifname'</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --intf-name=$4</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show system interface in JSON format</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --intf-name=$4 --detail</command> + </leafNode> + <leafNode name="json"> + <properties> + <help>Show system interface in JSON format</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --intf-name=$4 --raw</command> + </leafNode> + </children> + </tagNode> + <node name="kernel"> + <properties> + <help>Show all interfaces on this system</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_kernel</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show system interface in JSON format</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --detail</command> + </leafNode> + <leafNode name="json"> + <properties> + <help>Show all interfaces in JSON format</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --raw</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 9dcebb6af..c2bc03910 100755 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -50,6 +50,39 @@ </properties> <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e heartbeat -e cl_status -e mach_down -e ha_log</command> </leafNode> + <node name="conntrack"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(NEW|UPDATE|DESTROY)\]'</command> + <children> + <node name="event"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <children> + <leafNode name="new"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(NEW)\]'</command> + </leafNode> + <leafNode name="update"> + <properties> + <help>Show log for conntrack events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(UPDATE)\]'</command> + </leafNode> + <leafNode name="destroy"> + <properties> + <help>Show log for Conntrack Events</help> + </properties> + <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(DESTROY)\]'</command> + </leafNode> + </children> + </node> + </children> + </node> <leafNode name="conntrack-sync"> <properties> <help>Show log for Conntrack-sync</help> @@ -62,19 +95,6 @@ </properties> <command>journalctl --no-hostname --boot --unit conserver-server.service</command> </leafNode> - <node name="ids"> - <properties> - <help>Show log for for Intrusion Detection System</help> - </properties> - <children> - <leafNode name="ddos-protection"> - <properties> - <help>Show log for DDOS protection</help> - </properties> - <command>journalctl --no-hostname --boot --unit fastnetmon.service</command> - </leafNode> - </children> - </node> <node name="dhcp"> <properties> <help>Show log for Dynamic Host Control Protocol (DHCP)</help> @@ -139,7 +159,7 @@ <properties> <help>Show log for Firewall</help> </properties> - <command>journalctl --no-hostname --boot -k | egrep "(ipv[46]|bri)-(FWD|INP|OUT|NAM)"</command> + <command>journalctl --no-hostname --boot -k --grep='(ipv[46]|bri)-(FWD|INP|OUT|NAM)|STATE-POLICY'</command> <children> <node name="bridge"> <properties> @@ -539,6 +559,12 @@ </properties> <command>journalctl --no-hostname --boot --unit frr.service</command> </leafNode> + <leafNode name="haproxy"> + <properties> + <help>Show log for HAProxy</help> + </properties> + <command>journalctl --no-hostname --boot --unit haproxy.service</command> + </leafNode> <leafNode name="https"> <properties> <help>Show log for HTTPs</help> @@ -905,6 +931,12 @@ </properties> <command>journalctl --no-hostname --boot --unit vyos-configd.service</command> </leafNode> + <leafNode name="vyos-domain-resolver"> + <properties> + <help>Show log for VyOS domain resolver daemon</help> + </properties> + <command>journalctl --no-hostname --boot --unit vyos-domain-resolver.service</command> + </leafNode> <node name="wireless"> <properties> <help>Show log for Wireless interface</help> @@ -952,6 +984,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/op-mode-definitions/system-image.xml.in b/op-mode-definitions/system-image.xml.in index 44b055be6..847029dcd 100644 --- a/op-mode-definitions/system-image.xml.in +++ b/op-mode-definitions/system-image.xml.in @@ -193,7 +193,7 @@ <properties> <help>Show installed VyOS images</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_info.py show_images_summary</command> + <command>${vyos_op_scripts_dir}/image_info.py show_images_summary</command> <children> <node name="details"> <properties> diff --git a/python/setup.py b/python/setup.py index 2d614e724..571b956ee 100644 --- a/python/setup.py +++ b/python/setup.py @@ -1,5 +1,14 @@ import os +import sys +import subprocess from setuptools import setup +from setuptools.command.build_py import build_py + +sys.path.append('./vyos') +from defaults import directories + +def desc_out(f): + return os.path.splitext(f)[0] + '.desc' def packages(directory): return [ @@ -8,6 +17,43 @@ def packages(directory): if os.path.isfile(os.path.join(_[0], '__init__.py')) ] + +class GenerateProto(build_py): + ver = os.environ.get('OCAML_VERSION') + if ver: + proto_path = f'/opt/opam/{ver}/share/vyconf' + else: + proto_path = directories['proto_path'] + + def run(self): + # find all .proto files in vyconf proto_path + proto_files = [] + for _, _, files in os.walk(self.proto_path): + for file in files: + if file.endswith('.proto'): + proto_files.append(file) + + # compile each .proto file to Python + for proto_file in proto_files: + subprocess.check_call( + [ + 'protoc', + '--python_out=vyos/proto', + f'--proto_path={self.proto_path}/', + f'--descriptor_set_out=vyos/proto/{desc_out(proto_file)}', + proto_file, + ] + ) + subprocess.check_call( + [ + 'vyos/proto/generate_dataclass.py', + 'vyos/proto/vyconf.desc', + '--out-dir=vyos/proto', + ] + ) + + build_py.run(self) + setup( name = "vyos", version = "1.3.0", @@ -29,4 +75,7 @@ setup( "config-mgmt = vyos.config_mgmt:run", ], }, + cmdclass={ + 'build_py': GenerateProto, + }, ) diff --git a/python/vyos/base.py b/python/vyos/base.py index ca96d96ce..3173ddc20 100644 --- a/python/vyos/base.py +++ b/python/vyos/base.py @@ -1,4 +1,4 @@ -# Copyright 2018-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2018-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 @@ -15,8 +15,7 @@ from textwrap import fill - -class BaseWarning: +class UserMessage: def __init__(self, header, message, **kwargs): self.message = message self.kwargs = kwargs @@ -33,7 +32,6 @@ class BaseWarning: messages = self.message.split('\n') isfirstmessage = True initial_indent = self.textinitindent - print('') for mes in messages: mes = fill(mes, initial_indent=initial_indent, subsequent_indent=self.standardindent, **self.kwargs) @@ -44,17 +42,24 @@ class BaseWarning: print('', flush=True) +class Message(): + def __init__(self, message, **kwargs): + self.Message = UserMessage('', message, **kwargs) + self.Message.print() + class Warning(): def __init__(self, message, **kwargs): - self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs) - self.BaseWarn.print() + print('') + self.UserMessage = UserMessage('WARNING: ', message, **kwargs) + self.UserMessage.print() class DeprecationWarning(): def __init__(self, message, **kwargs): # Reformat the message and trim it to 72 characters in length - self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs) - self.BaseWarn.print() + print('') + self.UserMessage = UserMessage('DEPRECATION WARNING: ', message, **kwargs) + self.UserMessage.print() class ConfigError(Exception): diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py index 94215531d..81d986658 100644 --- a/python/vyos/component_version.py +++ b/python/vyos/component_version.py @@ -49,7 +49,9 @@ DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot') REGEX_WARN_VYOS = r'(// Warning: Do not remove the following line.)' REGEX_WARN_VYATTA = r'(/\* Warning: Do not remove the following line. \*/)' REGEX_COMPONENT_VERSION_VYOS = r'// vyos-config-version:\s+"([\w@:-]+)"\s*' -REGEX_COMPONENT_VERSION_VYATTA = r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/' +REGEX_COMPONENT_VERSION_VYATTA = ( + r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/' +) REGEX_RELEASE_VERSION_VYOS = r'// Release version:\s+(\S*)\s*' REGEX_RELEASE_VERSION_VYATTA = r'/\* Release version:\s+(\S*)\s*\*/' @@ -62,16 +64,31 @@ CONFIG_FILE_VERSION = """\ warn_filter_vyos = re.compile(REGEX_WARN_VYOS) warn_filter_vyatta = re.compile(REGEX_WARN_VYATTA) -regex_filter = { 'vyos': dict(zip(['component', 'release'], - [re.compile(REGEX_COMPONENT_VERSION_VYOS), - re.compile(REGEX_RELEASE_VERSION_VYOS)])), - 'vyatta': dict(zip(['component', 'release'], - [re.compile(REGEX_COMPONENT_VERSION_VYATTA), - re.compile(REGEX_RELEASE_VERSION_VYATTA)])) } +regex_filter = { + 'vyos': dict( + zip( + ['component', 'release'], + [ + re.compile(REGEX_COMPONENT_VERSION_VYOS), + re.compile(REGEX_RELEASE_VERSION_VYOS), + ], + ) + ), + 'vyatta': dict( + zip( + ['component', 'release'], + [ + re.compile(REGEX_COMPONENT_VERSION_VYATTA), + re.compile(REGEX_RELEASE_VERSION_VYATTA), + ], + ) + ), +} + @dataclass class VersionInfo: - component: Optional[dict[str,int]] = None + component: Optional[dict[str, int]] = None release: str = get_version() vintage: str = 'vyos' config_body: Optional[str] = None @@ -84,8 +101,9 @@ class VersionInfo: return bool(self.config_body is None) def update_footer(self): - f = CONFIG_FILE_VERSION.format(component_to_string(self.component), - self.release) + f = CONFIG_FILE_VERSION.format( + component_to_string(self.component), self.release + ) self.footer_lines = f.splitlines() def update_syntax(self): @@ -121,13 +139,16 @@ class VersionInfo: except Exception as e: raise ValueError(e) from e + def component_to_string(component: dict) -> str: - l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] + l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] # noqa: E741 return ':'.join(l) + def component_from_string(string: str) -> dict: return {k: int(v) for k, v in re.findall(r'([\w,-]+)@(\d+)', string)} + def version_info_from_file(config_file) -> VersionInfo: """Return config file component and release version info.""" version_info = VersionInfo() @@ -166,27 +187,27 @@ def version_info_from_file(config_file) -> VersionInfo: return version_info + def version_info_from_system() -> VersionInfo: """Return system component and release version info.""" d = component_version() sort_d = dict(sorted(d.items(), key=lambda x: x[0])) - version_info = VersionInfo( - component = sort_d, - release = get_version(), - vintage = 'vyos' - ) + version_info = VersionInfo(component=sort_d, release=get_version(), vintage='vyos') return version_info + def version_info_copy(v: VersionInfo) -> VersionInfo: """Make a copy of dataclass.""" return replace(v) + def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo: """In place pruning of component keys of x not in y.""" if x.component is None or y.component is None: return - x.component = { k: v for k,v in x.component.items() if k in y.component } + x.component = {k: v for k, v in x.component.items() if k in y.component} + def add_system_version(config_str: str = None, out_file: str = None): """Wrap config string with system version and write to out_file. @@ -202,3 +223,11 @@ def add_system_version(config_str: str = None, out_file: str = None): version_info.write(out_file) else: sys.stdout.write(version_info.write_string()) + + +def append_system_version(file: str): + """Append system version data to existing file""" + version_info = version_info_from_system() + version_info.update_footer() + with open(file, 'a') as f: + f.write(version_info.write_string()) diff --git a/python/vyos/config.py b/python/vyos/config.py index 1fab46761..546eeceab 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -149,6 +149,18 @@ class Config(object): return self._running_config return self._session_config + def get_bool_attr(self, attr) -> bool: + if not hasattr(self, attr): + return False + else: + tmp = getattr(self, attr) + if not isinstance(tmp, bool): + return False + return tmp + + def set_bool_attr(self, attr, val): + setattr(self, attr, val) + def _make_path(self, path): # Backwards-compatibility stuff: original implementation used string paths # libvyosconfig paths are lists, but since node names cannot contain whitespace, diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index 1c2b70fdf..dd8910afb 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -287,7 +287,7 @@ Proceed ?""" # commits under commit-confirm are not added to revision list unless # confirmed, hence a soft revert is to revision 0 - revert_ct = self._get_config_tree_revision(0) + revert_ct = self.get_config_tree_revision(0) message = '[commit-confirm] Reverting to previous config now' os.system('wall -n ' + message) @@ -351,7 +351,7 @@ Proceed ?""" ) return msg, 1 - rollback_ct = self._get_config_tree_revision(rev) + rollback_ct = self.get_config_tree_revision(rev) try: load(rollback_ct, switch='explicit') print('Rollback diff has been applied.') @@ -382,7 +382,7 @@ Proceed ?""" if rev1 is not None: if not self._check_revision_number(rev1): return f'Invalid revision number {rev1}', 1 - ct1 = self._get_config_tree_revision(rev1) + ct1 = self.get_config_tree_revision(rev1) ct2 = self.working_config msg = f'No changes between working and revision {rev1} configurations.\n' if rev2 is not None: @@ -390,7 +390,7 @@ Proceed ?""" return f'Invalid revision number {rev2}', 1 # compare older to newer ct2 = ct1 - ct1 = self._get_config_tree_revision(rev2) + ct1 = self.get_config_tree_revision(rev2) msg = f'No changes between revisions {rev2} and {rev1} configurations.\n' out = '' @@ -575,7 +575,7 @@ Proceed ?""" r = f.read().decode() return r - def _get_config_tree_revision(self, rev: int): + def get_config_tree_revision(self, rev: int): c = self._get_file_revision(rev) return ConfigTree(c) diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py index cf7c9d543..747af8dbe 100644 --- a/python/vyos/configdep.py +++ b/python/vyos/configdep.py @@ -102,11 +102,16 @@ def run_config_mode_script(target: str, config: 'Config'): mod = load_as_module(name, path) config.set_level([]) + dry_run = config.get_bool_attr('dry_run') try: c = mod.get_config(config) mod.verify(c) - mod.generate(c) - mod.apply(c) + if not dry_run: + mod.generate(c) + mod.apply(c) + else: + if hasattr(mod, 'call_dependents'): + mod.call_dependents() except (VyOSError, ConfigError) as e: raise ConfigError(str(e)) from e diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 5a353b110..ff0a15933 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -491,10 +491,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk # Check if any DHCP options changed which require a client restat dhcp = is_node_changed(config, base + [ifname, 'dhcp-options']) if dhcp: dict.update({'dhcp_options_changed' : {}}) - - # Changine interface VRF assignemnts require a DHCP restart, too - dhcp = is_node_changed(config, base + [ifname, 'vrf']) - if dhcp: dict.update({'dhcp_options_changed' : {}}) + dhcpv6 = is_node_changed(config, base + [ifname, 'dhcpv6-options']) + if dhcpv6: dict.update({'dhcpv6_options_changed' : {}}) # Some interfaces come with a source_interface which must also not be part # of any other bond or bridge interface as it is exclusivly assigned as the @@ -519,6 +517,14 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk else: dict['ipv6']['address'].update({'eui64_old': eui64}) + interface_identifier = leaf_node_changed(config, base + [ifname, 'ipv6', 'address', 'interface-identifier']) + if interface_identifier: + tmp = dict_search('ipv6.address', dict) + if not tmp: + dict.update({'ipv6': {'address': {'interface_identifier_old': interface_identifier}}}) + else: + dict['ipv6']['address'].update({'interface_identifier_old': interface_identifier}) + for vif, vif_config in dict.get('vif', {}).items(): # Add subinterface name to dictionary dict['vif'][vif].update({'ifname' : f'{ifname}.{vif}'}) @@ -543,6 +549,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk # Check if any DHCP options changed which require a client restat dhcp = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcp-options']) if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : {}}) + dhcpv6 = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcpv6-options']) + if dhcpv6: dict['vif'][vif].update({'dhcpv6_options_changed' : {}}) for vif_s, vif_s_config in dict.get('vif_s', {}).items(): # Add subinterface name to dictionary @@ -569,6 +577,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk # Check if any DHCP options changed which require a client restat dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcp-options']) if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : {}}) + dhcpv6 = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcpv6-options']) + if dhcpv6: dict['vif_s'][vif_s].update({'dhcpv6_options_changed' : {}}) for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): # Add subinterface name to dictionary @@ -597,6 +607,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk # Check if any DHCP options changed which require a client restat dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options']) if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : {}}) + dhcpv6 = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcpv6-options']) + if dhcpv6: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcpv6_options_changed' : {}}) # Check vif, vif-s/vif-c VLAN interfaces for removal dict = get_removed_vlans(config, base + [ifname], dict) @@ -622,6 +634,23 @@ def get_vlan_ids(interface): return vlan_ids +def get_vlans_ids_and_range(interface): + vlan_ids = set() + + vlan_filter_status = json.loads(cmd(f'bridge -j -d vlan show dev {interface}')) + + if vlan_filter_status is not None: + for interface_status in vlan_filter_status: + for vlan_entry in interface_status.get("vlans", []): + start = vlan_entry["vlan"] + end = vlan_entry.get("vlanEnd") + if end: + vlan_ids.add(f"{start}-{end}") + else: + vlan_ids.add(str(start)) + + return vlan_ids + def get_accel_dict(config, base, chap_secrets, with_pki=False): """ Common utility function to retrieve and mangle the Accel-PPP configuration 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/configsession.py b/python/vyos/configsession.py index dd3ad1e3d..a3be29881 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -21,10 +21,14 @@ import subprocess from vyos.defaults import directories from vyos.utils.process import is_systemd_service_running from vyos.utils.dict import dict_to_paths +from vyos.utils.boot import boot_configuration_complete +from vyos.vyconf_session import VyconfSession + +vyconf_backend = False CLI_SHELL_API = '/bin/cli-shell-api' -SET = '/usr/libexec/vyos/vyconf/vy_set' -DELETE = '/usr/libexec/vyos/vyconf/vy_delete' +SET = '/opt/vyatta/sbin/my_set' +DELETE = '/opt/vyatta/sbin/my_delete' COMMENT = '/opt/vyatta/sbin/my_comment' COMMIT = '/opt/vyatta/sbin/my_commit' DISCARD = '/opt/vyatta/sbin/my_discard' @@ -165,6 +169,11 @@ class ConfigSession(object): self.__run_command([CLI_SHELL_API, 'setupSession']) + if vyconf_backend and boot_configuration_complete(): + self._vyconf_session = VyconfSession(on_error=ConfigSessionError) + else: + self._vyconf_session = None + def __del__(self): try: output = ( @@ -209,7 +218,10 @@ class ConfigSession(object): value = [] else: value = [value] - self.__run_command([SET] + path + value) + if self._vyconf_session is None: + self.__run_command([SET] + path + value) + else: + self._vyconf_session.set(path + value) def set_section(self, path: list, d: dict): try: @@ -223,7 +235,10 @@ class ConfigSession(object): value = [] else: value = [value] - self.__run_command([DELETE] + path + value) + if self._vyconf_session is None: + self.__run_command([DELETE] + path + value) + else: + self._vyconf_session.delete(path + value) def load_section(self, path: list, d: dict): try: @@ -261,20 +276,34 @@ class ConfigSession(object): self.__run_command([COMMENT] + path + value) def commit(self): - out = self.__run_command([COMMIT]) + if self._vyconf_session is None: + out = self.__run_command([COMMIT]) + else: + out, _ = self._vyconf_session.commit() + return out def discard(self): - self.__run_command([DISCARD]) + if self._vyconf_session is None: + self.__run_command([DISCARD]) + else: + out, _ = self._vyconf_session.discard() def show_config(self, path, format='raw'): - config_data = self.__run_command(SHOW_CONFIG + path) + if self._vyconf_session is None: + config_data = self.__run_command(SHOW_CONFIG + path) + else: + config_data, _ = self._vyconf_session.show_config() if format == 'raw': return config_data def load_config(self, file_path): - out = self.__run_command(LOAD_CONFIG + [file_path]) + if self._vyconf_session is None: + out = self.__run_command(LOAD_CONFIG + [file_path]) + else: + out, _ = self._vyconf_session.load_config(file=file_path) + return out def load_explicit(self, file_path): @@ -287,11 +316,21 @@ class ConfigSession(object): raise ConfigSessionError(e) from e def migrate_and_load_config(self, file_path): - out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) + if self._vyconf_session is None: + out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) + else: + out, _ = self._vyconf_session.load_config(file=file_path, migrate=True) + return out def save_config(self, file_path): - out = self.__run_command(SAVE_CONFIG + [file_path]) + if self._vyconf_session is None: + out = self.__run_command(SAVE_CONFIG + [file_path]) + else: + out, _ = self._vyconf_session.save_config( + file=file_path, append_version=True + ) + return out def install_image(self, url): diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py index 59e5ac8a1..65cef5333 100644 --- a/python/vyos/configsource.py +++ b/python/vyos/configsource.py @@ -319,3 +319,13 @@ class ConfigSourceString(ConfigSource): self._session_config = ConfigTree(session_config_text) if session_config_text else None except ValueError: raise ConfigSourceError(f"Init error in {type(self)}") + +class ConfigSourceCache(ConfigSource): + def __init__(self, running_config_cache=None, session_config_cache=None): + super().__init__() + + try: + self._running_config = ConfigTree(internal=running_config_cache) if running_config_cache else None + self._session_config = ConfigTree(internal=session_config_cache) if session_config_cache else None + except ValueError: + raise ConfigSourceError(f"Init error in {type(self)}") diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index fb79e8459..ff40fbad0 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; @@ -19,35 +19,44 @@ import logging from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool -LIBPATH = '/usr/lib/libvyosconfig.so.0' +BUILD_PATH = '/tmp/libvyosconfig/_build/libvyosconfig.so' +INSTALL_PATH = '/usr/lib/libvyosconfig.so.0' +LIBPATH = BUILD_PATH if os.path.isfile(BUILD_PATH) else INSTALL_PATH + 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:])) + return (t[0], ''.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 @@ -57,9 +66,14 @@ class ConfigTreeError(Exception): class ConfigTree(object): - def __init__(self, config_string=None, address=None, libpath=LIBPATH): - if config_string is None and address is None: - raise TypeError("ConfigTree() requires one of 'config_string' or 'address'") + def __init__( + self, config_string=None, address=None, internal=None, libpath=LIBPATH + ): + if config_string is None and address is None and internal is None: + raise TypeError( + "ConfigTree() requires one of 'config_string', 'address', or 'internal'" + ) + self.__config = None self.__lib = cdll.LoadLibrary(libpath) @@ -80,6 +94,13 @@ class ConfigTree(object): self.__to_commands.argtypes = [c_void_p, c_char_p] self.__to_commands.restype = c_char_p + self.__read_internal = self.__lib.read_internal + self.__read_internal.argtypes = [c_char_p] + self.__read_internal.restype = c_void_p + + self.__write_internal = self.__lib.write_internal + self.__write_internal.argtypes = [c_void_p, c_char_p] + self.__to_json = self.__lib.to_json self.__to_json.argtypes = [c_void_p] self.__to_json.restype = c_char_p @@ -159,19 +180,34 @@ class ConfigTree(object): self.__destroy = self.__lib.destroy self.__destroy.argtypes = [c_void_p] - if address is None: + self.__equal = self.__lib.equal + self.__equal.argtypes = [c_void_p, c_void_p] + self.__equal.restype = c_bool + + if address is not None: + self.__config = address + self.__version = '' + elif internal is not None: + config = self.__read_internal(internal.encode()) + if config is None: + msg = self.__get_error().decode() + raise ValueError('Failed to read internal rep: {0}'.format(msg)) + else: + self.__config = config + elif config_string is not None: config_section, version_section = extract_version(config_string) config_section = escape_backslash(config_section) 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 else: - self.__config = address - self.__version = '' + raise TypeError( + "ConfigTree() requires one of 'config_string', 'address', or 'internal'" + ) self.__migration = os.environ.get('VYOS_MIGRATION') if self.__migration: @@ -181,6 +217,11 @@ class ConfigTree(object): if self.__config is not None: self.__destroy(self.__config) + def __eq__(self, other): + if isinstance(other, ConfigTree): + return self.__equal(self._get_config(), other._get_config()) + return False + def __str__(self): return self.to_string() @@ -190,15 +231,18 @@ class ConfigTree(object): def get_version_string(self): return self.__version + def write_cache(self, file_name): + self.__write_internal(self._get_config(), file_name) + def to_string(self, ordered_values=False, no_version=False): config_string = self.__to_string(self.__config, ordered_values).decode() 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 +255,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 +271,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 +282,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 +311,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 +323,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 +377,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 +389,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 +401,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 +468,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 +492,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 +522,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 +531,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 +598,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 +615,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 +631,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/configverify.py b/python/vyos/configverify.py index 4084425b1..d5f443f15 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -92,6 +92,9 @@ def verify_mtu_ipv6(config): tmp = dict_search('ipv6.address.eui64', config) if tmp != None: raise ConfigError(error_msg) + tmp = dict_search('ipv6.address.interface_identifier', config) + if tmp != None: raise ConfigError(error_msg) + def verify_vrf(config): """ Common helper function used by interface implementations to perform @@ -356,6 +359,7 @@ def verify_vlan_config(config): verify_vrf(vlan) verify_mirror_redirect(vlan) verify_mtu_parent(vlan, config) + verify_mtu_ipv6(vlan) # 802.1ad (Q-in-Q) VLANs for s_vlan_id in config.get('vif_s', {}): @@ -367,6 +371,7 @@ def verify_vlan_config(config): verify_vrf(s_vlan) verify_mirror_redirect(s_vlan) verify_mtu_parent(s_vlan, config) + verify_mtu_ipv6(s_vlan) for c_vlan_id in s_vlan.get('vif_c', {}): c_vlan = s_vlan['vif_c'][c_vlan_id] @@ -378,6 +383,7 @@ def verify_vlan_config(config): verify_mirror_redirect(c_vlan) verify_mtu_parent(c_vlan, config) verify_mtu_parent(c_vlan, s_vlan) + verify_mtu_ipv6(c_vlan) def verify_diffie_hellman_length(file, min_keysize): diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 9757a34df..c1e5ddc04 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -1,4 +1,4 @@ -# Copyright 2018-2024 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2018-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 @@ -37,7 +37,19 @@ directories = { 'dhcp6_client_dir' : '/run/dhcp6c', 'vyos_configdir' : '/opt/vyatta/config', 'completion_dir' : f'{base_dir}/completion', - 'ca_certificates' : '/usr/local/share/ca-certificates/vyos' + 'ca_certificates' : '/usr/local/share/ca-certificates/vyos', + 'ppp_nexthop_dir' : '/run/ppp_nexthop', + 'proto_path' : '/usr/share/vyos/vyconf' +} + +systemd_services = { + 'haproxy' : 'haproxy.service', + 'syslog' : 'syslog.service', + 'snmpd' : 'snmpd.service', +} + +internal_ports = { + 'certbot_haproxy' : 65080, # Certbot running behing haproxy } config_status = '/tmp/vyos-config-status' @@ -63,3 +75,5 @@ rt_symbolic_names = { rt_global_vrf = rt_symbolic_names['main'] rt_global_table = rt_symbolic_names['main'] + +vyconfd_conf = '/etc/vyos/vyconfd.conf' diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 314e8dfe3..64022db84 100755 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -233,6 +233,9 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): hook_name = 'prerouting' if hook == 'NAM': hook_name = f'name' + # for policy + if hook == 'route' or hook == 'route6': + hook_name = hook output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC{def_suffix}_{hook_name}_{fw_name}_{rule_id}') if 'mac_address' in side_conf: @@ -310,6 +313,16 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): operator = '!=' group_name = group_name[1:] output.append(f'{ip_name} {prefix}addr {operator} @D_{group_name}') + elif 'remote_group' in group: + group_name = group['remote_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + if ip_name == 'ip': + output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}') + elif ip_name == 'ip6': + output.append(f'{ip_name} {prefix}addr {operator} @R6_{group_name}') if 'mac_group' in group: group_name = group['mac_group'] operator = '' @@ -461,14 +474,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output.append('gre version 1') if gre_key: - # The offset of the key within the packet shifts depending on the C-flag. - # nftables cannot handle complex enough expressions to match multiple + # The offset of the key within the packet shifts depending on the C-flag. + # nftables cannot handle complex enough expressions to match multiple # offsets based on bitfields elsewhere. - # We enforce a specific match for the checksum flag in validation, so the - # gre_flags dict will always have a 'checksum' key when gre_key is populated. - if not gre_flags['checksum']: + # We enforce a specific match for the checksum flag in validation, so the + # gre_flags dict will always have a 'checksum' key when gre_key is populated. + if not gre_flags['checksum']: # No "unset" child node means C is set, we offset key lookup +32 bits - output.append(f'@th,64,32 == {gre_key}') + output.append(f'@th,64,32 == {gre_key}') else: output.append(f'@th,32,32 == {gre_key}') @@ -627,7 +640,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): return " ".join(output) def parse_gre_flags(flags, force_keyed=False): - flag_map = { # nft does not have symbolic names for these. + flag_map = { # nft does not have symbolic names for these. 'checksum': 1<<0, 'routing': 1<<1, 'key': 1<<2, @@ -638,7 +651,7 @@ def parse_gre_flags(flags, force_keyed=False): include = 0 exclude = 0 for fl_name, fl_state in flags.items(): - if not fl_state: + if not fl_state: include |= flag_map[fl_name] else: # 'unset' child tag exclude |= flag_map[fl_name] @@ -731,14 +744,14 @@ class GeoIPLock(object): def __exit__(self, exc_type, exc_value, tb): os.unlink(self.file) -def geoip_update(firewall, force=False): +def geoip_update(firewall=None, policy=None, force=False): with GeoIPLock(geoip_lock_file) as lock: if not lock: print("Script is already running") return False - if not firewall: - print("Firewall is not configured") + if not firewall and not policy: + print("Firewall and policy are not configured") return True if not os.path.exists(geoip_database): @@ -753,23 +766,41 @@ def geoip_update(firewall, force=False): ipv4_sets = {} ipv6_sets = {} + ipv4_codes_policy = {} + ipv6_codes_policy = {} + + ipv4_sets_policy = {} + ipv6_sets_policy = {} + # Map country codes to set names - for codes, path in dict_search_recursive(firewall, 'country_code'): - set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' - if ( path[0] == 'ipv4'): - for code in codes: - ipv4_codes.setdefault(code, []).append(set_name) - elif ( path[0] == 'ipv6' ): - set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}' - for code in codes: - ipv6_codes.setdefault(code, []).append(set_name) - - if not ipv4_codes and not ipv6_codes: + if firewall: + for codes, path in dict_search_recursive(firewall, 'country_code'): + set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' + if ( path[0] == 'ipv4'): + for code in codes: + ipv4_codes.setdefault(code, []).append(set_name) + elif ( path[0] == 'ipv6' ): + set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}' + for code in codes: + ipv6_codes.setdefault(code, []).append(set_name) + + if policy: + for codes, path in dict_search_recursive(policy, 'country_code'): + set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}' + if ( path[0] == 'route'): + for code in codes: + ipv4_codes_policy.setdefault(code, []).append(set_name) + elif ( path[0] == 'route6' ): + set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}' + for code in codes: + ipv6_codes_policy.setdefault(code, []).append(set_name) + + if not ipv4_codes and not ipv6_codes and not ipv4_codes_policy and not ipv6_codes_policy: if force: - print("GeoIP not in use by firewall") + print("GeoIP not in use by firewall and policy") return True - geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes]) + geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes, *ipv4_codes_policy, *ipv6_codes_policy]) # Iterate IP blocks to assign to sets for start, end, code in geoip_data: @@ -778,19 +809,29 @@ def geoip_update(firewall, force=False): ip_range = f'{start}-{end}' if start != end else start for setname in ipv4_codes[code]: ipv4_sets.setdefault(setname, []).append(ip_range) + if code in ipv4_codes_policy and ipv4: + ip_range = f'{start}-{end}' if start != end else start + for setname in ipv4_codes_policy[code]: + ipv4_sets_policy.setdefault(setname, []).append(ip_range) if code in ipv6_codes and not ipv4: ip_range = f'{start}-{end}' if start != end else start for setname in ipv6_codes[code]: ipv6_sets.setdefault(setname, []).append(ip_range) + if code in ipv6_codes_policy and not ipv4: + ip_range = f'{start}-{end}' if start != end else start + for setname in ipv6_codes_policy[code]: + ipv6_sets_policy.setdefault(setname, []).append(ip_range) render(nftables_geoip_conf, 'firewall/nftables-geoip-update.j2', { 'ipv4_sets': ipv4_sets, - 'ipv6_sets': ipv6_sets + 'ipv6_sets': ipv6_sets, + 'ipv4_sets_policy': ipv4_sets_policy, + 'ipv6_sets_policy': ipv6_sets_policy, }) result = run(f'nft --file {nftables_geoip_conf}') if result != 0: - print('Error: GeoIP failed to update firewall') + print('Error: GeoIP failed to update firewall/policy') return False return True diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index badc5d59f..73d6dd5f0 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 @@ -59,6 +60,10 @@ def get_frrender_dict(conf, argv=None) -> dict: from vyos.configdict import get_dhcp_interfaces from vyos.configdict import get_pppoe_interfaces + # We need to re-set the CLI path to the root level, as this function uses + # conf.exists() with an absolute path form the CLI root + conf.set_level([]) + # Create an empty dictionary which will be filled down the code path and # returned to the caller dict = {} @@ -87,7 +92,7 @@ def get_frrender_dict(conf, argv=None) -> dict: if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None: del default_values['area'][area_num]['area_type']['nssa'] - for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']: + for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static']: if dict_search(f'redistribute.{protocol}', ospf) is None: del default_values['redistribute'][protocol] if not bool(default_values['redistribute']): @@ -147,6 +152,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']: @@ -218,11 +267,11 @@ def get_frrender_dict(conf, argv=None) -> dict: # values present on the CLI - that's why we have if conf.exists() eigrp_cli_path = ['protocols', 'eigrp'] if conf.exists(eigrp_cli_path): - isis = conf.get_config_dict(eigrp_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) - dict.update({'eigrp' : isis}) + eigrp = conf.get_config_dict(eigrp_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True) + dict.update({'eigrp' : eigrp}) elif conf.exists_effective(eigrp_cli_path): dict.update({'eigrp' : {'deleted' : ''}}) @@ -360,17 +409,36 @@ def get_frrender_dict(conf, argv=None) -> dict: static = conf.get_config_dict(static_cli_path, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - - # T3680 - get a list of all interfaces currently configured to use DHCP - tmp = get_dhcp_interfaces(conf) - if tmp: static.update({'dhcp' : tmp}) - tmp = get_pppoe_interfaces(conf) - if tmp: static.update({'pppoe' : tmp}) - dict.update({'static' : static}) 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: + if 'static' in dict: + dict['static'].update({'dhcp' : tmp}) + else: + dict.update({'static' : {'dhcp' : tmp}}) + tmp = get_pppoe_interfaces(conf) + if tmp: + if 'static' in dict: + dict['static'].update({'pppoe' : tmp}) + else: + dict.update({'static' : {'pppoe' : tmp}}) + # keep a re-usable list of dependent VRFs dependent_vrfs_default = {} if 'bgp' in dict: @@ -534,17 +602,34 @@ def get_frrender_dict(conf, argv=None) -> dict: dict.update({'vrf' : vrf}) + if os.path.exists(frr_debug_enable): + print(f'---- get_frrender_dict({conf}) ----') + import pprint + pprint.pprint(dict) + print('-----------------------------------') + return dict class FRRender: + cached_config_dict = {} def __init__(self): self._frr_conf = '/run/frr/config/vyos.frr.conf' def generate(self, config_dict) -> None: + """ + Generate FRR configuration file + Returns False if no changes to configuration were made, otherwise True + """ if not isinstance(config_dict, dict): tmp = type(config_dict) raise ValueError(f'Config must be of type "dict" and not "{tmp}"!') + + if self.cached_config_dict == config_dict: + debug('FRR: NO CHANGES DETECTED') + return False + self.cached_config_dict = config_dict + def inline_helper(config_dict) -> str: output = '!\n' if 'babel' in config_dict and 'deleted' not in config_dict['babel']: @@ -604,11 +689,17 @@ 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') # we can not reload an empty file, thus we always embed the marker output = '!\n' + # Enable FRR logging + output += 'log syslog\n' + output += 'log facility local7\n' # Enable SNMP agentx support # SNMP AgentX support cannot be disabled once enabled if 'snmp' in config_dict: @@ -639,7 +730,7 @@ class FRRender: debug(output) write_file(self._frr_conf, output) debug('FRR: RENDERING CONFIG COMPLETE') - return None + return True def apply(self, count_max=5): count = 0 @@ -649,7 +740,7 @@ class FRRender: debug(f'FRR: reloading configuration - tries: {count} | Python class ID: {id(self)}') cmdline = '/usr/lib/frr/frr-reload.py --reload' if os.path.exists(frr_debug_enable): - cmdline += ' --debug' + cmdline += ' --debug --stdout' rc, emsg = rc_cmd(f'{cmdline} {self._frr_conf}') if rc != 0: sleep(2) diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 8ba481728..a659b9bd2 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -31,7 +31,6 @@ class BondIf(Interface): monitoring may be performed. """ - iftype = 'bond' definition = { **Interface.definition, ** { @@ -109,6 +108,9 @@ class BondIf(Interface): ] return options + def _create(self): + super()._create('bond') + def remove(self): """ Remove interface from operating system. Removing the interface diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 917f962b7..f81026965 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -19,7 +19,7 @@ from vyos.utils.assertion import assert_list from vyos.utils.assertion import assert_positive from vyos.utils.dict import dict_search from vyos.utils.network import interface_exists -from vyos.configdict import get_vlan_ids +from vyos.configdict import get_vlans_ids_and_range from vyos.configdict import list_diff @Interface.register @@ -32,7 +32,6 @@ class BridgeIf(Interface): The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. """ - iftype = 'bridge' definition = { **Interface.definition, **{ @@ -107,6 +106,9 @@ class BridgeIf(Interface): }, }} + def _create(self): + super()._create('bridge') + def get_vlan_filter(self): """ Get the status of the bridge VLAN filter @@ -378,7 +380,7 @@ class BridgeIf(Interface): add_vlan = [] native_vlan_id = None allowed_vlan_ids= [] - cur_vlan_ids = get_vlan_ids(interface) + cur_vlan_ids = get_vlans_ids_and_range(interface) if 'native_vlan' in interface_config: vlan_id = interface_config['native_vlan'] @@ -387,14 +389,8 @@ class BridgeIf(Interface): if 'allowed_vlan' in interface_config: for vlan in interface_config['allowed_vlan']: - vlan_range = vlan.split('-') - if len(vlan_range) == 2: - for vlan_add in range(int(vlan_range[0]),int(vlan_range[1]) + 1): - add_vlan.append(str(vlan_add)) - allowed_vlan_ids.append(str(vlan_add)) - else: - add_vlan.append(vlan) - allowed_vlan_ids.append(vlan) + add_vlan.append(vlan) + allowed_vlan_ids.append(vlan) # Remove redundant VLANs from the system for vlan in list_diff(cur_vlan_ids, add_vlan): 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/dummy.py b/python/vyos/ifconfig/dummy.py index d45769931..29a1965a3 100644 --- a/python/vyos/ifconfig/dummy.py +++ b/python/vyos/ifconfig/dummy.py @@ -22,8 +22,6 @@ class DummyIf(Interface): interface. The purpose of a dummy interface is to provide a device to route packets through without actually transmitting them. """ - - iftype = 'dummy' definition = { **Interface.definition, **{ @@ -31,3 +29,6 @@ class DummyIf(Interface): 'prefixes': ['dum', ], }, } + + def _create(self): + super()._create('dummy') diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index d0c03dbe0..93727bdf6 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -33,7 +33,6 @@ class EthernetIf(Interface): Abstraction of a Linux Ethernet Interface """ - iftype = 'ethernet' definition = { **Interface.definition, **{ @@ -119,6 +118,9 @@ class EthernetIf(Interface): super().__init__(ifname, **kargs) self.ethtool = Ethtool(ifname) + def _create(self): + pass + def remove(self): """ Remove interface from config. Removing the interface deconfigures all @@ -137,7 +139,7 @@ class EthernetIf(Interface): # Remove all VLAN subinterfaces - filter with the VLAN dot for vlan in [ x - for x in Section.interfaces(self.iftype) + for x in Section.interfaces('ethernet') if x.startswith(f'{self.ifname}.') ]: Interface(vlan).remove() diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index fbb261a35..f53ef4166 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -27,7 +27,6 @@ class GeneveIf(Interface): https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve https://lwn.net/Articles/644938/ """ - iftype = 'geneve' definition = { **Interface.definition, **{ @@ -49,7 +48,7 @@ class GeneveIf(Interface): 'parameters.ipv6.flowlabel' : 'flowlabel', } - cmd = 'ip link add name {ifname} type {type} id {vni} remote {remote}' + cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote} dstport {port}' for vyos_key, iproute2_key in mapping.items(): # dict_search will return an empty dict "{}" for valueless nodes like # "parameters.nolearning" - thus we need to test the nodes existence diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py index 3e5f5790d..201d3cacb 100644 --- a/python/vyos/ifconfig/input.py +++ b/python/vyos/ifconfig/input.py @@ -25,8 +25,6 @@ class InputIf(Interface): a single stack of qdiscs, classes and filters can be shared between multiple interfaces. """ - - iftype = 'ifb' definition = { **Interface.definition, **{ @@ -34,3 +32,6 @@ class InputIf(Interface): 'prefixes': ['ifb', ], }, } + + def _create(self): + super()._create('ifb') diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index cad1685a9..003a273c0 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.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,6 +22,7 @@ from copy import deepcopy from glob import glob from ipaddress import IPv4Network +from ipaddress import IPv6Interface from netifaces import ifaddresses # this is not the same as socket.AF_INET/INET6 from netifaces import AF_INET @@ -29,7 +30,6 @@ from netifaces import AF_INET6 from netaddr import EUI from netaddr import mac_unix_expanded -from vyos.base import ConfigError from vyos.configdict import list_diff from vyos.configdict import dict_merge from vyos.configdict import get_vlan_ids @@ -74,7 +74,6 @@ class Interface(Control): OperationalClass = Operational options = ['debug', 'create'] - required = [] default = { 'debug': True, 'create': True, @@ -336,22 +335,10 @@ class Interface(Control): super().__init__(**kargs) if not self.exists(ifname): - # Any instance of Interface, such as Interface('eth0') can be used - # safely to access the generic function in this class as 'type' is - # unset, the class can not be created - if not hasattr(self, 'iftype'): - raise ConfigError(f'Interface "{ifname}" has no "iftype" attribute defined!') - self.config['type'] = self.iftype - # Should an Instance of a child class (EthernetIf, DummyIf, ..) # be required, then create should be set to False to not accidentally create it. # In case a subclass does not define it, we use get to set the default to True - if self.config.get('create',True): - for k in self.required: - if k not in kargs: - name = self.default['type'] - raise ConfigError(f'missing required option {k} for {name} {ifname} creation') - + if self.config.get('create', True): self._create() # If we can not connect to the interface then let the caller know # as the class could not be correctly initialised @@ -364,13 +351,14 @@ class Interface(Control): self.operational = self.OperationalClass(ifname) self.vrrp = VRRP(ifname) - def _create(self): + def _create(self, type: str=''): # Do not create interface that already exist or exists in netns netns = self.config.get('netns', None) if self.exists(f'{self.ifname}', netns=netns): return - cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) + cmd = f'ip link add dev {self.ifname}' + if type: cmd += f' type {type}' if 'netns' in self.config: cmd = f'ip netns exec {netns} {cmd}' self._cmd(cmd) @@ -608,12 +596,16 @@ class Interface(Control): """ Add/Remove interface from given VRF instance. + Keyword arguments: + vrf: VRF instance name or empty string (default VRF) + + Return True if VRF was changed, False otherwise + Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_vrf('foo') >>> Interface('eth0').set_vrf() """ - # Don't allow for netns yet if 'netns' in self.config: return False @@ -624,21 +616,33 @@ class Interface(Control): # Get current VRF table ID old_vrf_tableid = get_vrf_tableid(self.ifname) - self.set_interface('vrf', vrf) + # Always stop the DHCP client process to clean up routes within the VRF + # where the process was originally started. There is no need to add a + # condition to only call the method if "address dhcp" was defined, as + # this is handled inside set_dhcp(v6) by only stopping if the daemon is + # running. DHCP client process restart will be handled later on once the + # interface is moved to the new VRF. + self.set_dhcp(False) + self.set_dhcpv6(False) + + # Move interface in/out of VRF + self.set_interface('vrf', vrf) if vrf: # Get routing table ID number for VRF vrf_table_id = get_vrf_tableid(vrf) # Add map element with interface and zone ID - if vrf_table_id: + if vrf_table_id and old_vrf_tableid != vrf_table_id: # delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF - if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id): - self._del_interface_from_ct_iface_map() + self._del_interface_from_ct_iface_map() self._add_interface_to_ct_iface_map(vrf_table_id) + return True else: - self._del_interface_from_ct_iface_map() + if old_vrf_tableid != get_vrf_tableid(self.ifname): + self._del_interface_from_ct_iface_map() + return True - return True + return False def set_arp_cache_tmo(self, tmo): """ @@ -906,7 +910,11 @@ class Interface(Control): tmp = self.get_interface('ipv6_autoconf') if tmp == autoconf: return None - return self.set_interface('ipv6_autoconf', autoconf) + rc = self.set_interface('ipv6_autoconf', autoconf) + if autoconf == '0': + flushed = self.flush_ipv6_slaac_addrs() + self.flush_ipv6_slaac_routes(ra_addrs=flushed) + return rc def add_ipv6_eui64_address(self, prefix): """ @@ -934,6 +942,20 @@ class Interface(Control): prefixlen = prefix.split('/')[1] self.del_addr(f'{eui64}/{prefixlen}') + def set_ipv6_interface_identifier(self, identifier): + """ + Set the interface identifier for IPv6 autoconf. + """ + cmd = f'ip token set {identifier} dev {self.ifname}' + self._cmd(cmd) + + def del_ipv6_interface_identifier(self): + """ + Delete the interface identifier for IPv6 autoconf. + """ + cmd = f'ip token delete dev {self.ifname}' + self._cmd(cmd) + def set_ipv6_forwarding(self, forwarding): """ Configure IPv6 interface-specific Host/Router behaviour. @@ -1194,7 +1216,7 @@ class Interface(Control): """ return self.get_addr_v4() + self.get_addr_v6() - def add_addr(self, addr): + def add_addr(self, addr: str, vrf_changed: bool=False) -> bool: """ Add IP(v6) address to interface. Address is only added if it is not already assigned to that interface. Address format must be validated @@ -1227,15 +1249,14 @@ class Interface(Control): # add to interface if addr == 'dhcp': - self.set_dhcp(True) + self.set_dhcp(True, vrf_changed=vrf_changed) elif addr == 'dhcpv6': - self.set_dhcpv6(True) + self.set_dhcpv6(True, vrf_changed=vrf_changed) elif not is_intf_addr_assigned(self.ifname, addr, netns=netns): netns_cmd = f'ip netns exec {netns}' if netns else '' tmp = f'{netns_cmd} ip addr add {addr} dev {self.ifname}' # Add broadcast address for IPv4 if is_ipv4(addr): tmp += ' brd +' - self._cmd(tmp) else: return False @@ -1245,7 +1266,7 @@ class Interface(Control): return True - def del_addr(self, addr): + def del_addr(self, addr: str) -> bool: """ Delete IP(v6) address from interface. Address is only deleted if it is assigned to that interface. Address format must be exactly the same as @@ -1308,6 +1329,71 @@ class Interface(Control): # flush all addresses self._cmd(cmd) + def flush_ipv6_slaac_addrs(self) -> list: + """ + Flush all IPv6 addresses installed in response to router advertisement + messages from this interface. + + Will raise an exception on error. + Will return a list of flushed IPv6 addresses. + """ + netns = get_interface_namespace(self.ifname) + netns_cmd = f'ip netns exec {netns}' if netns else '' + tmp = get_interface_address(self.ifname) + if not tmp or 'addr_info' not in tmp: + return + + # Parse interface IP addresses. Example data: + # {'family': 'inet6', 'local': '2001:db8:1111:0:250:56ff:feb3:38c5', + # 'prefixlen': 64, 'scope': 'global', 'dynamic': True, + # 'mngtmpaddr': True, 'protocol': 'kernel_ra', + # 'valid_life_time': 2591987, 'preferred_life_time': 14387} + flushed = [] + for addr_info in tmp['addr_info']: + if 'protocol' not in addr_info: + continue + if (addr_info['protocol'] == 'kernel_ra' and + addr_info['scope'] == 'global'): + # Flush IPv6 addresses installed by router advertisement + ra_addr = f"{addr_info['local']}/{addr_info['prefixlen']}" + flushed.append(ra_addr) + cmd = f'{netns_cmd} ip -6 addr del dev {self.ifname} {ra_addr}' + self._cmd(cmd) + return flushed + + def flush_ipv6_slaac_routes(self, ra_addrs: list=[]) -> None: + """ + Flush IPv6 default routes installed in response to router advertisement + messages from this interface. + + Will raise an exception on error. + """ + # Find IPv6 connected prefixes for flushed SLAAC addresses + connected = [] + for addr in ra_addrs if isinstance(ra_addrs, list) else []: + connected.append(str(IPv6Interface(addr).network)) + + netns = get_interface_namespace(self.ifname) + netns_cmd = f'ip netns exec {netns}' if netns else '' + + tmp = self._cmd(f'{netns_cmd} ip -j -6 route show dev {self.ifname}') + tmp = json.loads(tmp) + # Parse interface routes. Example data: + # {'dst': 'default', 'gateway': 'fe80::250:56ff:feb3:cdba', + # 'protocol': 'ra', 'metric': 1024, 'flags': [], 'expires': 1398, + # 'metrics': [{'hoplimit': 64}], 'pref': 'medium'} + for route in tmp: + # If it's a default route received from RA, delete it + if (dict_search('dst', route) == 'default' and + dict_search('protocol', route) == 'ra'): + self._cmd(f'{netns_cmd} ip -6 route del default via {route["gateway"]} dev {self.ifname}') + # Remove connected prefixes received from RA + if dict_search('dst', route) in connected: + # If it's a connected prefix, delete it + self._cmd(f'{netns_cmd} ip -6 route del {route["dst"]} dev {self.ifname}') + + return None + def add_to_bridge(self, bridge_dict): """ Adds the interface to the bridge with the passed port config. @@ -1318,8 +1404,6 @@ class Interface(Control): # drop all interface addresses first self.flush_addrs() - ifname = self.ifname - for bridge, bridge_config in bridge_dict.items(): # add interface to bridge - use Section.klass to get BridgeIf class Section.klass(bridge)(bridge, create=True).add_port(self.ifname) @@ -1335,7 +1419,7 @@ class Interface(Control): bridge_vlan_filter = Section.klass(bridge)(bridge, create=True).get_vlan_filter() if int(bridge_vlan_filter): - cur_vlan_ids = get_vlan_ids(ifname) + cur_vlan_ids = get_vlan_ids(self.ifname) add_vlan = [] native_vlan_id = None allowed_vlan_ids= [] @@ -1358,18 +1442,18 @@ class Interface(Control): # Remove redundant VLANs from the system for vlan in list_diff(cur_vlan_ids, add_vlan): - cmd = f'bridge vlan del dev {ifname} vid {vlan} master' + cmd = f'bridge vlan del dev {self.ifname} vid {vlan} master' self._cmd(cmd) for vlan in allowed_vlan_ids: - cmd = f'bridge vlan add dev {ifname} vid {vlan} master' + cmd = f'bridge vlan add dev {self.ifname} vid {vlan} master' self._cmd(cmd) # Setting native VLAN to system if native_vlan_id: - cmd = f'bridge vlan add dev {ifname} vid {native_vlan_id} pvid untagged master' + cmd = f'bridge vlan add dev {self.ifname} vid {native_vlan_id} pvid untagged master' self._cmd(cmd) - def set_dhcp(self, enable): + def set_dhcp(self, enable: bool, vrf_changed: bool=False): """ Enable/Disable DHCP client on a given interface. """ @@ -1409,7 +1493,9 @@ class Interface(Control): # the old lease is released a new one is acquired (T4203). We will # only restart DHCP client if it's option changed, or if it's not # running, but it should be running (e.g. on system startup) - if 'dhcp_options_changed' in self.config or not is_systemd_service_active(systemd_service): + if (vrf_changed or + ('dhcp_options_changed' in self.config) or + (not is_systemd_service_active(systemd_service))): return self._cmd(f'systemctl restart {systemd_service}') else: if is_systemd_service_active(systemd_service): @@ -1424,10 +1510,10 @@ class Interface(Control): if tmp and 'addr_info' in tmp: for address_dict in tmp['addr_info']: # Only remove dynamic assigned addresses - if 'dynamic' not in address_dict: - continue - address = address_dict['local'] - self.del_addr(address) + if address_dict['family'] == 'inet' and 'dynamic' in address_dict: + address = address_dict['local'] + prefixlen = address_dict['prefixlen'] + self.del_addr(f'{address}/{prefixlen}') # cleanup old config files for file in [dhclient_config_file, systemd_override_file, dhclient_lease_file]: @@ -1436,19 +1522,18 @@ class Interface(Control): return None - def set_dhcpv6(self, enable): + def set_dhcpv6(self, enable: bool, vrf_changed: bool=False): """ Enable/Disable DHCPv6 client on a given interface. """ if enable not in [True, False]: raise ValueError() - ifname = self.ifname config_base = directories['dhcp6_client_dir'] - config_file = f'{config_base}/dhcp6c.{ifname}.conf' - script_file = f'/etc/wide-dhcpv6/dhcp6c.{ifname}.script' # can not live under /run b/c of noexec mount option - systemd_override_file = f'/run/systemd/system/dhcp6c@{ifname}.service.d/10-override.conf' - systemd_service = f'dhcp6c@{ifname}.service' + config_file = f'{config_base}/dhcp6c.{self.ifname}.conf' + script_file = f'/etc/wide-dhcpv6/dhcp6c.{self.ifname}.script' # can not live under /run b/c of noexec mount option + systemd_override_file = f'/run/systemd/system/dhcp6c@{self.ifname}.service.d/10-override.conf' + systemd_service = f'dhcp6c@{self.ifname}.service' # Rendered client configuration files require additional settings config = deepcopy(self.config) @@ -1465,7 +1550,10 @@ class Interface(Control): # We must ignore any return codes. This is required to enable # DHCPv6-PD for interfaces which are yet not up and running. - return self._popen(f'systemctl restart {systemd_service}') + if (vrf_changed or + ('dhcpv6_options_changed' in self.config) or + (not is_systemd_service_active(systemd_service))): + return self._popen(f'systemctl restart {systemd_service}') else: if is_systemd_service_active(systemd_service): self._cmd(f'systemctl stop {systemd_service}') @@ -1682,30 +1770,31 @@ class Interface(Control): else: self.del_addr(addr) - # start DHCPv6 client when only PD was configured - if dhcpv6pd: - self.set_dhcpv6(True) - # XXX: Bind interface to given VRF or unbind it if vrf is not set. Unbinding # will call 'ip link set dev eth0 nomaster' which will also drop the # interface out of any bridge or bond - thus this is checked before. + vrf_changed = False if 'is_bond_member' in config: bond_if = next(iter(config['is_bond_member'])) tmp = get_interface_config(config['ifname']) if 'master' in tmp and tmp['master'] != bond_if: - self.set_vrf('') + vrf_changed = self.set_vrf('') elif 'is_bridge_member' in config: bridge_if = next(iter(config['is_bridge_member'])) tmp = get_interface_config(config['ifname']) if 'master' in tmp and tmp['master'] != bridge_if: - self.set_vrf('') + vrf_changed = self.set_vrf('') else: - self.set_vrf(config.get('vrf', '')) + vrf_changed = self.set_vrf(config.get('vrf', '')) + + # start DHCPv6 client when only PD was configured + if dhcpv6pd: + self.set_dhcpv6(True, vrf_changed=vrf_changed) # Add this section after vrf T4331 for addr in new_addr: - self.add_addr(addr) + self.add_addr(addr, vrf_changed=vrf_changed) # Configure MSS value for IPv4 TCP connections tmp = dict_search('ip.adjust_mss', config) @@ -1784,11 +1873,26 @@ class Interface(Control): value = '0' if (tmp != None) else '1' self.set_ipv6_forwarding(value) + # Delete old interface identifier + # This should be before setting the accept_ra value + old = dict_search('ipv6.address.interface_identifier_old', config) + now = dict_search('ipv6.address.interface_identifier', config) + if old and not now: + # accept_ra of ra is required to delete the interface identifier + self.set_ipv6_accept_ra('2') + self.del_ipv6_interface_identifier() + + # Set IPv6 Interface identifier + # This should be before setting the accept_ra value + tmp = dict_search('ipv6.address.interface_identifier', config) + if tmp: + # accept_ra is required to set the interface identifier + self.set_ipv6_accept_ra('2') + self.set_ipv6_interface_identifier(tmp) + # IPv6 router advertisements tmp = dict_search('ipv6.address.autoconf', config) - value = '2' if (tmp != None) else '1' - if 'dhcpv6' in new_addr: - value = '2' + value = '2' if (tmp != None) else '0' self.set_ipv6_accept_ra(value) # IPv6 address autoconfiguration @@ -1952,8 +2056,6 @@ class Interface(Control): class VLANIf(Interface): """ Specific class which abstracts 802.1q and 802.1ad (Q-in-Q) VLAN interfaces """ - iftype = 'vlan' - def _create(self): # bail out early if interface already exists if self.exists(f'{self.ifname}'): diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index c1f2803ee..dfaa006aa 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -45,7 +45,6 @@ class L2TPv3If(Interface): either hot standby or load balancing services. Additionally, link integrity monitoring may be performed. """ - iftype = 'l2tp' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py index e1d041839..13e8a2c50 100644 --- a/python/vyos/ifconfig/loopback.py +++ b/python/vyos/ifconfig/loopback.py @@ -22,16 +22,20 @@ class LoopbackIf(Interface): uses to communicate with itself. """ _persistent_addresses = ['127.0.0.1/8', '::1/128'] - iftype = 'loopback' definition = { **Interface.definition, **{ 'section': 'loopback', 'prefixes': ['lo', ], 'bridgeable': True, + 'eternal': 'lo$', } } + def _create(self): + # we can not create this interface as it is managed by the Kernel + pass + def remove(self): """ Loopback interface can not be deleted from operating system. We can diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index 383905814..3b4dc223f 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -27,7 +27,6 @@ class MACsecIf(Interface): other security solutions such as IPsec (layer 3) or TLS (layer 4), as all those solutions are used for their own specific use cases. """ - iftype = 'macsec' definition = { **Interface.definition, **{ @@ -43,7 +42,7 @@ class MACsecIf(Interface): """ # create tunnel interface - cmd = 'ip link add link {source_interface} {ifname} type {type}'.format(**self.config) + cmd = 'ip link add link {source_interface} {ifname} type macsec'.format(**self.config) cmd += f' cipher {self.config["security"]["cipher"]}' if 'encrypt' in self.config["security"]: diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index fb7f1d298..fe948b920 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -20,7 +20,6 @@ class MACVLANIf(Interface): """ Abstraction of a Linux MACvlan interface """ - iftype = 'macvlan' definition = { **Interface.definition, **{ @@ -35,12 +34,12 @@ class MACVLANIf(Interface): down by default. """ # please do not change the order when assembling the command - cmd = 'ip link add {ifname} link {source_interface} type {type} mode {mode}' + cmd = 'ip link add {ifname} link {source_interface} type macvlan mode {mode}' self._cmd(cmd.format(**self.config)) # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') def set_mode(self, mode): - cmd = f'ip link set dev {self.ifname} type {self.iftype} mode {mode}' + cmd = f'ip link set dev {self.ifname} type macvlan mode {mode}' return self._cmd(cmd) diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py index f80a68d4f..85ca3877e 100644 --- a/python/vyos/ifconfig/pppoe.py +++ b/python/vyos/ifconfig/pppoe.py @@ -19,7 +19,6 @@ from vyos.utils.network import get_interface_config @Interface.register class PPPoEIf(Interface): - iftype = 'pppoe' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/sstpc.py b/python/vyos/ifconfig/sstpc.py index 50fc6ee6b..d92ef23dc 100644 --- a/python/vyos/ifconfig/sstpc.py +++ b/python/vyos/ifconfig/sstpc.py @@ -17,7 +17,6 @@ from vyos.ifconfig.interface import Interface @Interface.register class SSTPCIf(Interface): - iftype = 'sstpc' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 9ba7b31a6..df904f7d5 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -90,9 +90,8 @@ class TunnelIf(Interface): # T3357: we do not have the 'encapsulation' in kargs when calling this # class from op-mode like "show interfaces tunnel" if 'encapsulation' in kargs: - self.iftype = kargs['encapsulation'] # The gretap interface has the possibility to act as L2 bridge - if self.iftype in ['gretap', 'ip6gretap']: + if kargs['encapsulation'] in ['gretap', 'ip6gretap']: # no multicast, ttl or tos for gretap self.definition = { **TunnelIf.definition, @@ -110,10 +109,10 @@ class TunnelIf(Interface): mapping = { **self.mapping, **self.mapping_ipv4 } cmd = 'ip tunnel add {ifname} mode {encapsulation}' - if self.iftype in ['gretap', 'ip6gretap', 'erspan', 'ip6erspan']: + if self.config['encapsulation'] in ['gretap', 'ip6gretap', 'erspan', 'ip6erspan']: cmd = 'ip link add name {ifname} type {encapsulation}' # ERSPAN requires the serialisation of packets - if self.iftype in ['erspan', 'ip6erspan']: + if self.config['encapsulation'] in ['erspan', 'ip6erspan']: cmd += ' seq' for vyos_key, iproute2_key in mapping.items(): @@ -132,7 +131,7 @@ class TunnelIf(Interface): def _change_options(self): # gretap interfaces do not support changing any parameter - if self.iftype in ['gretap', 'ip6gretap', 'erspan', 'ip6erspan']: + if self.config['encapsulation'] in ['gretap', 'ip6gretap', 'erspan', 'ip6erspan']: return if self.config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: diff --git a/python/vyos/ifconfig/veth.py b/python/vyos/ifconfig/veth.py index aafbf226a..2c8709d20 100644 --- a/python/vyos/ifconfig/veth.py +++ b/python/vyos/ifconfig/veth.py @@ -21,7 +21,6 @@ class VethIf(Interface): """ Abstraction of a Linux veth interface """ - iftype = 'veth' definition = { **Interface.definition, **{ @@ -46,7 +45,7 @@ class VethIf(Interface): return # create virtual-ethernet interface - cmd = 'ip link add {ifname} type {type}'.format(**self.config) + cmd = f'ip link add {self.ifname} type veth' cmd += f' peer name {self.config["peer_name"]}' self._cmd(cmd) diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index a3657370f..3ee22706c 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -26,15 +26,12 @@ from vyos.utils.file import read_file from vyos.utils.file import wait_for_file_write_complete from vyos.utils.process import process_running - class VRRPError(Exception): pass - class VRRPNoData(VRRPError): pass - class VRRP(object): _vrrp_prefix = '00:00:5E:00:01:' location = { diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index 251cbeb36..78f5895f8 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -19,7 +19,6 @@ from vyos.utils.vti_updown_db import vti_updown_db_exists, open_vti_updown_db_re @Interface.register class VTIIf(Interface): - iftype = 'vti' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py index 6fb414e56..ee790f275 100644 --- a/python/vyos/ifconfig/vtun.py +++ b/python/vyos/ifconfig/vtun.py @@ -17,7 +17,6 @@ from vyos.ifconfig.interface import Interface @Interface.register class VTunIf(Interface): - iftype = 'vtun' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index 1023c58d1..58844885b 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -42,8 +42,6 @@ class VXLANIf(Interface): For more information please refer to: https://www.kernel.org/doc/Documentation/networking/vxlan.txt """ - - iftype = 'vxlan' definition = { **Interface.definition, **{ @@ -94,7 +92,7 @@ class VXLANIf(Interface): remote_list = self.config['remote'][1:] self.config['remote'] = self.config['remote'][0] - cmd = 'ip link add {ifname} type {type} dstport {port}' + cmd = 'ip link add {ifname} type vxlan dstport {port}' for vyos_key, iproute2_key in mapping.items(): # dict_search will return an empty dict "{}" for valueless nodes like # "parameters.nolearning" - thus we need to test the nodes existence diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index cccac361d..f5217aecb 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,10 +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): @@ -91,12 +92,15 @@ class WireGuardOperational(Operational): c.set_level(['interfaces', 'wireguard', self.config['ifname']]) description = c.return_effective_value(['description']) ips = c.return_effective_values(['address']) + hostnames = c.return_effective_values(['host-name']) answer = 'interface: {}\n'.format(self.config['ifname']) if description: answer += ' description: {}\n'.format(description) if ips: answer += ' address: {}\n'.format(', '.join(ips)) + if hostnames: + answer += ' hostname: {}\n'.format(', '.join(hostnames)) answer += ' public key: {}\n'.format(wgdump['public_key']) answer += ' private key: (hidden)\n' @@ -156,22 +160,93 @@ class WireGuardOperational(Operational): answer += '\n' return answer + def get_latest_handshakes(self): + """Get latest handshake time for each peer""" + output = {} + + # 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') + + if len(items) != 2: + continue + + output[items[0]] = int(items[1]) + + 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 + + # 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'] + + # 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}' + + if 'disable' in peer_config: + print(f'WireGuard interface "{self.ifname}" peer "{peer_name}" disabled!') + continue + + 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: + 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 class WireGuardIf(Interface): OperationalClass = WireGuardOperational - iftype = 'wireguard' definition = { **Interface.definition, **{ 'section': 'wireguard', - 'prefixes': [ - 'wg', - ], + 'prefixes': ['wg', ], 'bridgeable': False, }, } + def _create(self): + super()._create('wireguard') + def get_mac(self): """Get a synthetic MAC address.""" return self.get_mac_synthetic() @@ -181,22 +256,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(): @@ -204,43 +283,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/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index 88eaa772b..121f56bd5 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -20,7 +20,6 @@ class WiFiIf(Interface): """ Handle WIFI/WLAN interfaces. """ - iftype = 'wifi' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/wwan.py b/python/vyos/ifconfig/wwan.py index 845c9bef9..004a64b39 100644 --- a/python/vyos/ifconfig/wwan.py +++ b/python/vyos/ifconfig/wwan.py @@ -17,7 +17,6 @@ from vyos.ifconfig.interface import Interface @Interface.register class WWANIf(Interface): - iftype = 'wwan' definition = { **Interface.definition, **{ 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..5eecbbaad 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,8 +17,11 @@ import json import os import socket +from datetime import datetime +from datetime import timezone + +from vyos import ConfigError from vyos.template import is_ipv6 -from vyos.template import isc_static_route from vyos.template import netmask_from_cidr from vyos.utils.dict import dict_search_args from vyos.utils.file import file_permissions @@ -40,7 +43,8 @@ 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', + 'capwap_controller': 'capwap-ac-v4', } kea6_options = { @@ -52,11 +56,36 @@ 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', + 'capwap_controller': 'capwap-ac-v6', } 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,46 +93,62 @@ 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 = '' 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)}) + default_route = f'0.0.0.0/0 - {config["default_router"]}' + + routes = [ + f'{route} - {route_options["next_hop"]}' + for route, route_options in config['static_route'].items() + ] + + options.append( + { + 'name': 'classless-static-route', + 'data': ', '.join( + routes if not default_route else routes + [default_route] + ), + } + ) 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 = [] + out = {'subnet': subnet, 'id': int(config['subnet_id']), 'user-context': {}} if 'option' in config: out['option-data'] = kea_parse_options(config['option']) @@ -121,13 +166,14 @@ def kea_parse_subnet(subnet, config): out['valid-lifetime'] = int(config['lease']) out['max-valid-lifetime'] = int(config['lease']) + if 'ping_check' in config: + out['user-context']['enable-ping-check'] = True + if 'range' in config: pools = [] for num, range_config in config['range'].items(): 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 +210,24 @@ 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 + if 'dynamic_dns_update' in config: + out.update(kea_parse_ddns_settings(config['dynamic_dns_update'])) + return out + def kea6_parse_options(config): options = [] @@ -181,7 +235,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 +253,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 +304,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 +331,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 +340,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 +354,55 @@ def kea6_parse_subnet(subnet, config): return out +def kea_parse_tsig_algo(algo_spec): + translate = { + 'md5': 'HMAC-MD5', + 'sha1': 'HMAC-SHA1', + 'sha224': 'HMAC-SHA224', + 'sha256': 'HMAC-SHA256', + 'sha384': 'HMAC-SHA384', + 'sha512': 'HMAC-SHA512' + } + if algo_spec not in translate: + raise ConfigError(f'Unsupported TSIG algorithm: {algo_spec}') + return translate[algo_spec] + +def kea_parse_enable_disable(value): + return True if value == 'enable' else False + +def kea_parse_ddns_settings(config): + data = {} + + if send_updates := config.get('send_updates'): + data['ddns-send-updates'] = kea_parse_enable_disable(send_updates) + + if override_client_update := config.get('override_client_update'): + data['ddns-override-client-update'] = kea_parse_enable_disable(override_client_update) + + if override_no_update := config.get('override_no_update'): + data['ddns-override-no-update'] = kea_parse_enable_disable(override_no_update) + + if update_on_renew := config.get('update_on_renew'): + data['ddns-update-on-renew'] = kea_parse_enable_disable(update_on_renew) + + if conflict_resolution := config.get('conflict_resolution'): + data['ddns-use-conflict-resolution'] = kea_parse_enable_disable(conflict_resolution) + + if 'replace_client_name' in config: + data['ddns-replace-client-name'] = config['replace_client_name'] + if 'generated_prefix' in config: + data['ddns-generated-prefix'] = config['generated_prefix'] + if 'qualifying_suffix' in config: + data['ddns-qualifying-suffix'] = config['qualifying_suffix'] + if 'ttl_percent' in config: + data['ddns-ttl-percent'] = int(config['ttl_percent']) / 100 + if 'hostname_char_set' in config: + data['hostname-char-set'] = config['hostname_char_set'] + if 'hostname_char_replacement' in config: + data['hostname-char-replacement'] = config['hostname_char_replacement'] + + return data + def _ctrl_socket_command(inet, command, args=None): path = kea_ctrl_socket.format(inet=inet) @@ -321,6 +429,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 +438,42 @@ def kea_get_leases(inet): return leases['arguments']['leases'] + +def kea_add_lease( + inet, + ip_address, + host_name=None, + mac_address=None, + iaid=None, + duid=None, + subnet_id=None, +): + args = {'ip-address': ip_address} + + if host_name: + args['hostname'] = host_name + + if subnet_id: + args['subnet-id'] = subnet_id + + # IPv4 requires MAC address, IPv6 requires either MAC address or DUID + if mac_address: + args['hw-address'] = mac_address + if duid: + args['duid'] = duid + + # IPv6 requires IAID + if inet == '6' and iaid: + args['iaid'] = iaid + + result = _ctrl_socket_command(inet, f'lease{inet}-add', args) + + if result and 'result' in result: + return result['result'] == 0 + + return False + + def kea_delete_lease(inet, ip_address): args = {'ip-address': ip_address} @@ -339,6 +484,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 +493,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 +518,146 @@ def kea_get_pool_from_subnet_id(config, inet, subnet_id): return network['name'] return None + + +def kea_get_domain_from_subnet_id(config, inet, subnet_id): + shared_networks = dict_search_args( + config, 'arguments', f'Dhcp{inet}', 'shared-networks' + ) + + if not shared_networks: + return None + + for network in shared_networks: + if f'subnet{inet}' not in network: + continue + + for subnet in network[f'subnet{inet}']: + if 'id' in subnet and int(subnet['id']) == int(subnet_id): + for option in subnet['option-data']: + if option['name'] == 'domain-name': + return option['data'] + + # domain-name is not found in subnet, fallback to shared-network pool option + for option in network['option-data']: + if option['name'] == 'domain-name': + return option['data'] + + 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'] + start = lease['cltt'] + expiry = start + lifetime + + lease['start_time'] = datetime.fromtimestamp(start, timezone.utc) + lease['expire_time'] = ( + 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['domain'] = ( + kea_get_domain_from_subnet_id(config, inet, lease['subnet-id']) + if config + else '' + ) + data_lease['end'] = ( + lease['expire_time'].timestamp() if lease['expire_time'] 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('.') or '-' + + if inet == '4': + data_lease['mac'] = lease['hw-address'] + data_lease['start'] = lease['start_time'].timestamp() + + if inet == '6': + data_lease['last_communication'] = lease['start_time'].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'] = '' + + now = datetime.now(timezone.utc) + if lease['valid-lft'] > 0 and lease['expire_time'] > now: + # 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(lease['expire_time'] - now).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/proto/__init__.py b/python/vyos/proto/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python/vyos/proto/__init__.py diff --git a/python/vyos/proto/generate_dataclass.py b/python/vyos/proto/generate_dataclass.py new file mode 100755 index 000000000..c6296c568 --- /dev/null +++ b/python/vyos/proto/generate_dataclass.py @@ -0,0 +1,178 @@ +#!/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 os + +from google.protobuf.descriptor_pb2 import FileDescriptorSet # pylint: disable=no-name-in-module +from google.protobuf.descriptor_pb2 import FieldDescriptorProto # pylint: disable=no-name-in-module +from humps import decamelize + +HEADER = """\ +from enum import IntEnum +from dataclasses import dataclass +from dataclasses import field +""" + + +def normalize(s: str) -> str: + """Decamelize and avoid syntactic collision""" + t = decamelize(s) + return t + '_' if t in ['from'] else t + + +def generate_dataclass(descriptor_proto): + class_name = descriptor_proto.name + fields = [] + for field_p in descriptor_proto.field: + field_name = field_p.name + field_type, field_default = get_type(field_p.type, field_p.type_name) + match field_p.label: + case FieldDescriptorProto.LABEL_REPEATED: + field_type = f'list[{field_type}] = field(default_factory=list)' + case FieldDescriptorProto.LABEL_OPTIONAL: + field_type = f'{field_type} = None' + case _: + field_type = f'{field_type} = {field_default}' + + fields.append(f' {field_name}: {field_type}') + + code = f""" +@dataclass +class {class_name}: +{chr(10).join(fields) if fields else ' pass'} +""" + + return code + + +def generate_request(descriptor_proto): + class_name = descriptor_proto.name + fields = [] + f_vars = [] + for field_p in descriptor_proto.field: + field_name = field_p.name + field_type, field_default = get_type(field_p.type, field_p.type_name) + match field_p.label: + case FieldDescriptorProto.LABEL_REPEATED: + field_type = f'list[{field_type}] = []' + case FieldDescriptorProto.LABEL_OPTIONAL: + field_type = f'{field_type} = None' + case _: + field_type = f'{field_type} = {field_default}' + + fields.append(f'{normalize(field_name)}: {field_type}') + f_vars.append(f'{normalize(field_name)}') + + fields.insert(0, 'token: str = None') + + code = f""" +def set_request_{decamelize(class_name)}({', '.join(fields)}): + reqi = {class_name} ({', '.join(f_vars)}) + req = Request({decamelize(class_name)}=reqi) + req_env = RequestEnvelope(token, req) + return req_env +""" + + return code + + +def generate_nested_dataclass(descriptor_proto): + out = '' + for nested_p in descriptor_proto.nested_type: + out = out + generate_dataclass(nested_p) + + return out + + +def generate_nested_request(descriptor_proto): + out = '' + for nested_p in descriptor_proto.nested_type: + out = out + generate_request(nested_p) + + return out + + +def generate_enum_dataclass(descriptor_proto): + code = '' + for enum_p in descriptor_proto.enum_type: + enums = [] + enum_name = enum_p.name + for enum_val in enum_p.value: + enums.append(f' {enum_val.name} = {enum_val.number}') + + code += f""" +class {enum_name}(IntEnum): +{chr(10).join(enums)} +""" + + return code + + +def get_type(field_type, type_name): + res = 'Any', None + match field_type: + case FieldDescriptorProto.TYPE_STRING: + res = 'str', '""' + case FieldDescriptorProto.TYPE_INT32 | FieldDescriptorProto.TYPE_INT64: + res = 'int', 0 + case FieldDescriptorProto.TYPE_FLOAT | FieldDescriptorProto.TYPE_DOUBLE: + res = 'float', 0.0 + case FieldDescriptorProto.TYPE_BOOL: + res = 'bool', False + case FieldDescriptorProto.TYPE_MESSAGE | FieldDescriptorProto.TYPE_ENUM: + res = type_name.split('.')[-1], None + case _: + pass + + return res + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('descriptor_file', help='protobuf .desc file') + parser.add_argument('--out-dir', help='directory to write generated file') + args = parser.parse_args() + desc_file = args.descriptor_file + out_dir = args.out_dir + + with open(desc_file, 'rb') as f: + descriptor_set_data = f.read() + + descriptor_set = FileDescriptorSet() + descriptor_set.ParseFromString(descriptor_set_data) + + for file_proto in descriptor_set.file: + f = f'{file_proto.name.replace(".", "_")}.py' + f = os.path.join(out_dir, f) + dataclass_code = '' + nested_code = '' + enum_code = '' + request_code = '' + with open(f, 'w') as f: + enum_code += generate_enum_dataclass(file_proto) + for message_proto in file_proto.message_type: + dataclass_code += generate_dataclass(message_proto) + nested_code += generate_nested_dataclass(message_proto) + enum_code += generate_enum_dataclass(message_proto) + request_code += generate_nested_request(message_proto) + + f.write(HEADER) + f.write(enum_code) + f.write(nested_code) + f.write(dataclass_code) + f.write(request_code) diff --git a/python/vyos/proto/vyconf_client.py b/python/vyos/proto/vyconf_client.py new file mode 100644 index 000000000..b385f0951 --- /dev/null +++ b/python/vyos/proto/vyconf_client.py @@ -0,0 +1,89 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import socket +from dataclasses import asdict + +from vyos.proto import vyconf_proto +from vyos.proto import vyconf_pb2 + +from google.protobuf.json_format import MessageToDict +from google.protobuf.json_format import ParseDict + +socket_path = '/var/run/vyconfd.sock' + + +def send_socket(msg: bytearray) -> bytes: + data = bytes() + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client.connect(socket_path) + client.sendall(msg) + + data_length = client.recv(4) + if data_length: + length = int.from_bytes(data_length) + data = client.recv(length) + + client.close() + + return data + + +def request_to_msg(req: vyconf_proto.RequestEnvelope) -> vyconf_pb2.RequestEnvelope: + # pylint: disable=no-member + + msg = vyconf_pb2.RequestEnvelope() + msg = ParseDict(asdict(req), msg, ignore_unknown_fields=True) + return msg + + +def msg_to_response(msg: vyconf_pb2.Response) -> vyconf_proto.Response: + # pylint: disable=no-member + + d = MessageToDict( + msg, preserving_proto_field_name=True, use_integers_for_enums=True + ) + + response = vyconf_proto.Response(**d) + return response + + +def write_request(req: vyconf_proto.RequestEnvelope) -> bytearray: + req_msg = request_to_msg(req) + encoded_data = req_msg.SerializeToString() + byte_size = req_msg.ByteSize() + length_bytes = byte_size.to_bytes(4) + arr = bytearray(length_bytes) + arr.extend(encoded_data) + + return arr + + +def read_response(msg: bytes) -> vyconf_proto.Response: + response_msg = vyconf_pb2.Response() # pylint: disable=no-member + response_msg.ParseFromString(msg) + response = msg_to_response(response_msg) + + return response + + +def send_request(name, *args, **kwargs): + func = getattr(vyconf_proto, f'set_request_{name}') + request_env = func(*args, **kwargs) + msg = write_request(request_env) + response_msg = send_socket(msg) + response = read_response(response_msg) + + return response 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/system/grub_util.py b/python/vyos/system/grub_util.py index 4a3d8795e..ad95bb4f9 100644 --- a/python/vyos/system/grub_util.py +++ b/python/vyos/system/grub_util.py @@ -56,13 +56,12 @@ def set_kernel_cmdline_options(cmdline_options: str, version: str = '', @image.if_not_live_boot def update_kernel_cmdline_options(cmdline_options: str, - root_dir: str = '') -> None: + root_dir: str = '', + version = image.get_running_image()) -> None: """Update Kernel custom cmdline options""" if not root_dir: root_dir = disk.find_persistence() - version = image.get_running_image() - boot_opts_current = grub.get_boot_opts(version, root_dir) boot_opts_proposed = grub.BOOT_OPTS_STEM + f'{version} {cmdline_options}' diff --git a/python/vyos/template.py b/python/vyos/template.py index be9f781a6..11e1cc50f 100755 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -36,6 +36,7 @@ DEFAULT_TEMPLATE_DIR = directories["templates"] # Holds template filters registered via register_filter() _FILTERS = {} _TESTS = {} +_CLEVER_FUNCTIONS = {} # reuse Environments with identical settings to improve performance @functools.lru_cache(maxsize=2) @@ -58,6 +59,7 @@ def _get_environment(location=None): ) env.filters.update(_FILTERS) env.tests.update(_TESTS) + env.globals.update(_CLEVER_FUNCTIONS) return env @@ -77,7 +79,7 @@ def register_filter(name, func=None): "Filters can only be registered before rendering the first template" ) if name in _FILTERS: - raise ValueError(f"A filter with name {name!r} was registered already") + raise ValueError(f"A filter with name {name!r} was already registered") _FILTERS[name] = func return func @@ -97,10 +99,30 @@ def register_test(name, func=None): "Tests can only be registered before rendering the first template" ) if name in _TESTS: - raise ValueError(f"A test with name {name!r} was registered already") + raise ValueError(f"A test with name {name!r} was already registered") _TESTS[name] = func return func +def register_clever_function(name, func=None): + """Register a function to be available as test in templates under given name. + + It can also be used as a decorator, see below in this module for examples. + + :raise RuntimeError: + when trying to register a test after a template has been rendered already + :raise ValueError: when trying to register a name which was taken already + """ + if func is None: + return functools.partial(register_clever_function, name) + if _get_environment.cache_info().currsize: + raise RuntimeError( + "Clever functions can only be registered before rendering the" \ + "first template") + if name in _CLEVER_FUNCTIONS: + raise ValueError(f"A clever function with name {name!r} was already "\ + "registered") + _CLEVER_FUNCTIONS[name] = func + return func def render_to_string(template, content, formater=None, location=None): """Render a template from the template directory, raise on any errors. @@ -150,6 +172,8 @@ def render( # As we are opening the file with 'w', we are performing the rendering before # calling open() to not accidentally erase the file if rendering fails rendered = render_to_string(template, content, formater, location) + # Remove any trailing character and always add a new line at the end + rendered = rendered.rstrip() + "\n" # Write to file with open(destination, "w") as file: @@ -390,28 +414,6 @@ def compare_netmask(netmask1, netmask2): except: return False -@register_filter('isc_static_route') -def isc_static_route(subnet, router): - # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server - # Option format is: - # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> - # where bytes with the value 0 are omitted. - from ipaddress import ip_network - net = ip_network(subnet) - # add netmask - string = str(net.prefixlen) + ',' - # add network bytes - if net.prefixlen: - width = net.prefixlen // 8 - if net.prefixlen % 8: - width += 1 - string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ',' - - # add router bytes - string += ','.join(router.split('.')) - - return string - @register_filter('is_file') def is_file(filename): if os.path.exists(filename): @@ -612,12 +614,17 @@ def nft_default_rule(fw_conf, fw_name, family): return " ".join(output) @register_filter('nft_state_policy') -def nft_state_policy(conf, state): +def nft_state_policy(conf, state, bridge=False): out = [f'ct state {state}'] + action = conf['action'] if 'action' in conf else None + + if bridge and action == 'reject': + action = 'drop' # T7148 - Bridge cannot use reject + if 'log' in conf: log_state = state[:3].upper() - log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper() + log_action = (action if action else 'accept')[:1].upper() out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"') if 'log_level' in conf: @@ -626,8 +633,8 @@ def nft_state_policy(conf, state): out.append('counter') - if 'action' in conf: - out.append(conf['action']) + if action: + out.append(action) return " ".join(out) @@ -779,6 +786,11 @@ def conntrack_ct_policy(protocol_conf): return ", ".join(output) +@register_filter('wlb_nft_rule') +def wlb_nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False): + from vyos.wanloadbalance import nft_rule as wlb_nft_rule + return wlb_nft_rule(rule_conf, rule_id, local, exclude, limit, weight, health_state, action, restore_mark) + @register_filter('range_to_regex') def range_to_regex(num_range): """Convert range of numbers or list of ranges @@ -871,10 +883,77 @@ def kea_high_availability_json(config): return dumps(data) +@register_filter('kea_dynamic_dns_update_main_json') +def kea_dynamic_dns_update_main_json(config): + from vyos.kea import kea_parse_ddns_settings + from json import dumps + + data = kea_parse_ddns_settings(config) + + if len(data) == 0: + return '' + + return dumps(data, indent=8)[1:-1] + ',' + +@register_filter('kea_dynamic_dns_update_tsig_key_json') +def kea_dynamic_dns_update_tsig_key_json(config): + from vyos.kea import kea_parse_tsig_algo + from json import dumps + out = [] + + if 'tsig_key' not in config: + return dumps(out) + + tsig_keys = config['tsig_key'] + + for tsig_key_name, tsig_key_config in tsig_keys.items(): + tsig_key = { + 'name': tsig_key_name, + 'algorithm': kea_parse_tsig_algo(tsig_key_config['algorithm']), + 'secret': tsig_key_config['secret'] + } + out.append(tsig_key) + + return dumps(out, indent=12) + +@register_filter('kea_dynamic_dns_update_domains') +def kea_dynamic_dns_update_domains(config, type_key): + from json import dumps + out = [] + + if type_key not in config: + return dumps(out) + + domains = config[type_key] + + for domain_name, domain_config in domains.items(): + domain = { + 'name': domain_name, + + } + if 'key_name' in domain_config: + domain['key-name'] = domain_config['key_name'] + + if 'dns_server' in domain_config: + dns_servers = [] + for dns_server_config in domain_config['dns_server'].values(): + dns_server = { + 'ip-address': dns_server_config['address'] + } + if 'port' in dns_server_config: + dns_server['port'] = int(dns_server_config['port']) + dns_servers.append(dns_server) + domain['dns-servers'] = dns_servers + + out.append(domain) + + return dumps(out, indent=12) + @register_filter('kea_shared_network_json') def kea_shared_network_json(shared_networks): from vyos.kea import kea_parse_options from vyos.kea import kea_parse_subnet + from vyos.kea import kea_parse_ddns_settings from json import dumps out = [] @@ -885,9 +964,13 @@ def kea_shared_network_json(shared_networks): network = { 'name': name, 'authoritative': ('authoritative' in config), - 'subnet4': [] + 'subnet4': [], + 'user-context': {} } + if 'dynamic_dns_update' in config: + network.update(kea_parse_ddns_settings(config['dynamic_dns_update'])) + if 'option' in config: network['option-data'] = kea_parse_options(config['option']) @@ -897,6 +980,9 @@ def kea_shared_network_json(shared_networks): if 'bootfile_server' in config['option']: network['next-server'] = config['option']['bootfile_server'] + if 'ping_check' in config: + network['user-context']['enable-ping-check'] = True + if 'subnet' in config: for subnet, subnet_config in config['subnet'].items(): if 'disable' in subnet_config: @@ -988,3 +1074,21 @@ def vyos_defined(value, test_value=None, var_type=None): else: # Valid value and is matching optional argument if provided - return true return True + +@register_clever_function('get_default_port') +def get_default_port(service): + """ + Jinja2 plugin to retrieve common service port number from vyos.defaults + class form a Jinja2 template. This removes the need to hardcode, or pass in + the data using the general dictionary. + + Added to remove code complexity and make it easier to read. + + Example: + {{ get_default_port('certbot_haproxy') }} + """ + from vyos.defaults import internal_ports + if service not in internal_ports: + raise RuntimeError(f'Service "{service}" not found in internal ' \ + 'vyos.defaults.internal_ports dict!') + return internal_ports[service] diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py index a0b3e1cae..5d0e3464a 100644 --- a/python/vyos/utils/auth.py +++ b/python/vyos/utils/auth.py @@ -13,10 +13,80 @@ # You should have received a copy of the GNU Lesser General Public License along with this library; # if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import cracklib +import math import re +import string +from enum import StrEnum +from decimal import Decimal from vyos.utils.process import cmd + +DEFAULT_PASSWORD: str = 'vyos' +LOW_ENTROPY_MSG: str = 'should be at least 8 characters long;' +WEAK_PASSWORD_MSG: str = 'The password complexity is too low - @MSG@' +CRACKLIB_ERROR_MSG: str = 'A following error occurred: @MSG@\n' \ + 'Possibly the cracklib database is corrupted or is missing. ' \ + 'Try reinstalling the python3-cracklib package.' + +class EPasswdStrength(StrEnum): + WEAK = 'Weak' + DECENT = 'Decent' + STRONG = 'Strong' + ERROR = 'Cracklib Error' + + +def calculate_entropy(charset: str, passwd: str) -> float: + """ + Calculate the entropy of a password based on the set of characters used + Uses E = log2(R**L) formula, where + - R is the range (length) of the character set + - L is the length of password + """ + return math.log(math.pow(len(charset), len(passwd)), 2) + +def evaluate_strength(passwd: str) -> dict[str, str]: + """ Evaluates password strength and returns a check result dict """ + charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE + + string.punctuation + string.digits) + + result = { + 'strength': '', + 'error': '', + } + + try: + cracklib.FascistCheck(passwd) + except ValueError as e: + # The password is vulnerable to dictionary attack no matter the entropy + if 'is' in str(e): + msg = str(e).replace('is', 'should not be') + else: + msg = f'should not be {e}' + result.update(strength=EPasswdStrength.WEAK) + result.update(error=WEAK_PASSWORD_MSG.replace('@MSG@', msg)) + except Exception as e: + result.update(strength=EPasswdStrength.ERROR) + result.update(error=CRACKLIB_ERROR_MSG.replace('@MSG@', str(e))) + else: + # Now check the password's entropy + # Cast to Decimal for more precise rounding + entropy = Decimal.from_float(calculate_entropy(charset, passwd)) + + match round(entropy): + case e if e in range(0, 59): + result.update(strength=EPasswdStrength.WEAK) + result.update( + error=WEAK_PASSWORD_MSG.replace('@MSG@', LOW_ENTROPY_MSG) + ) + case e if e in range(60, 119): + result.update(strength=EPasswdStrength.DECENT) + case e if e >= 120: + result.update(strength=EPasswdStrength.STRONG) + + return result + def make_password_hash(password): """ Makes a password hash for /etc/shadow using mkpasswd """ diff --git a/python/vyos/utils/cpu.py b/python/vyos/utils/cpu.py index 3bea5ac12..8ace77d15 100644 --- a/python/vyos/utils/cpu.py +++ b/python/vyos/utils/cpu.py @@ -99,3 +99,18 @@ def get_core_count(): core_count += 1 return core_count + + +def get_available_cpus(): + """ List of cpus with ids that are available in the system + Uses 'lscpu' command + + Returns: list[dict[str, str | int | bool]]: cpus details + """ + import json + + from vyos.utils.process import cmd + + out = json.loads(cmd('lscpu --extended -b --json')) + + return out['cpus'] 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/python/vyos/utils/misc.py b/python/vyos/utils/misc.py index ac8011b8d..d82655914 100644 --- a/python/vyos/utils/misc.py +++ b/python/vyos/utils/misc.py @@ -52,7 +52,7 @@ def install_into_config(conf, config_paths, override_prompt=True): continue try: - cmd(f'/usr/libexec/vyos/vyconf/vy_set {path}') + cmd(f'/opt/vyatta/sbin/my_set {path}') count += 1 except: failed.append(path) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 8fce08de0..20b6a3c9e 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -69,7 +69,9 @@ def get_vrf_members(vrf: str) -> list: answer = json.loads(output) for data in answer: if 'ifname' in data: - interfaces.append(data.get('ifname')) + # Skip PIM interfaces which appears in VRF + if 'pim' not in data.get('ifname'): + interfaces.append(data.get('ifname')) except: pass return interfaces @@ -254,40 +256,60 @@ def mac2eui64(mac, prefix=None): except: # pylint: disable=bare-except return -def check_port_availability(ipaddress, port, protocol): +def check_port_availability(address: str=None, port: int=0, protocol: str='tcp') -> bool: """ - Check if port is available and not used by any service - Return False if a port is busy or IP address does not exists + Check if given port is available and not used by any service. + Should be used carefully for services that can start listening dynamically, because IP address may be dynamic too + + Args: + address: IPv4 or IPv6 address - if None, checks on all interfaces + port: TCP/UDP port number. + + + Returns: + False if a port is busy or IP address does not exists + True if a port is free and IP address exists """ - from socketserver import TCPServer, UDPServer + import socket from ipaddress import ip_address + # treat None as "any address" + address = address or '::' + # verify arguments try: - ipaddress = ip_address(ipaddress).compressed - except: - raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address') + address = ip_address(address).compressed + except ValueError: + raise ValueError(f'{address} is not a valid IPv4 or IPv6 address') if port not in range(1, 65536): - raise ValueError(f'The port number {port} is not in the 1-65535 range') + raise ValueError(f'Port {port} is not in range 1-65535') if protocol not in ['tcp', 'udp']: - raise ValueError(f'The protocol {protocol} is not supported. Only tcp and udp are allowed') + raise ValueError(f'{protocol} is not supported - only tcp and udp are allowed') - # check port availability + protocol = socket.SOCK_STREAM if protocol == 'tcp' else socket.SOCK_DGRAM try: - if protocol == 'tcp': - server = TCPServer((ipaddress, port), None, bind_and_activate=True) - if protocol == 'udp': - server = UDPServer((ipaddress, port), None, bind_and_activate=True) - server.server_close() - except Exception as e: - # errno.h: - #define EADDRINUSE 98 /* Address already in use */ - if e.errno == 98: + addr_info = socket.getaddrinfo(address, port, socket.AF_UNSPEC, protocol) + except socket.gaierror as e: + print(f'Invalid address: {address}') + return False + + for family, socktype, proto, canonname, sockaddr in addr_info: + try: + with socket.socket(family, socktype, proto) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(sockaddr) + # port is free to use + return True + except OSError: + # port is already in use return False - return True + # if we reach this point, no socket was tested and we assume the port is + # already in use - better safe then sorry + return False + def is_listen_port_bind_service(port: int, service: str) -> bool: """Check if listen port bound to expected program name @@ -597,3 +619,35 @@ def get_nft_vrf_zone_mapping() -> dict: for (vrf_name, vrf_id) in vrf_list: output.append({'interface' : vrf_name, 'vrf_tableid' : vrf_id}) return output + +def is_valid_ipv4_address_or_range(addr: str) -> bool: + """ + Validates if the provided address is a valid IPv4, CIDR or IPv4 range + :param addr: address to test + :return: bool: True if provided address is valid + """ + from ipaddress import ip_network + try: + if '-' in addr: # If we are checking a range, validate both address's individually + split = addr.split('-') + return is_valid_ipv4_address_or_range(split[0]) and is_valid_ipv4_address_or_range(split[1]) + else: + return ip_network(addr).version == 4 + except: + return False + +def is_valid_ipv6_address_or_range(addr: str) -> bool: + """ + Validates if the provided address is a valid IPv4, CIDR or IPv4 range + :param addr: address to test + :return: bool: True if provided address is valid + """ + from ipaddress import ip_network + try: + if '-' in addr: # If we are checking a range, validate both address's individually + split = addr.split('-') + return is_valid_ipv6_address_or_range(split[0]) and is_valid_ipv6_address_or_range(split[1]) + else: + return ip_network(addr).version == 6 + except: + return False diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index d8aabb822..21335e6b3 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -14,16 +14,27 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import shlex from subprocess import Popen from subprocess import PIPE from subprocess import STDOUT from subprocess import DEVNULL + +def get_wrapper(vrf, netns): + wrapper = None + if vrf: + wrapper = ['ip', 'vrf', 'exec', vrf] + elif netns: + wrapper = ['ip', 'netns', 'exec', netns] + return wrapper + + def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=PIPE, decode='utf-8'): + stdout=PIPE, stderr=PIPE, decode='utf-8', vrf=None, netns=None): """ - popen is a wrapper helper aound subprocess.Popen + popen is a wrapper helper around subprocess.Popen with it default setting it will return a tuple (out, err) out: the output of the program run err: the error code returned by the program @@ -45,6 +56,8 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - DEVNULL, discard the output decode: specify the expected text encoding (utf-8, ascii, ...) the default is explicitely utf-8 which is python's own default + vrf: run command in a VRF context + netns: run command in the named network namespace usage: get both stdout and stderr: popen('command', stdout=PIPE, stderr=STDOUT) @@ -60,9 +73,6 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, if not debug.enabled(flag): flag = 'command' - cmd_msg = f"cmd '{command}'" - debug.message(cmd_msg, flag) - use_shell = shell stdin = None if shell is None: @@ -72,6 +82,24 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, if env: use_shell = True + # Must be run as root to execute command in VRF or network namespace + wrapper = get_wrapper(vrf, netns) + if vrf or netns: + if os.getuid() != 0: + raise OSError( + 'Permission denied: cannot execute commands in VRF and netns contexts as an unprivileged user' + ) + + if use_shell: + command = f'{shlex.join(wrapper)} {command}' + else: + if type(command) is not list: + command = [command] + command = wrapper + command + + cmd_msg = f"cmd '{command}'" if use_shell else f"cmd '{shlex.join(command)}'" + debug.message(cmd_msg, flag) + if input: stdin = PIPE input = input.encode() if type(input) is str else input @@ -111,7 +139,7 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, def run(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=DEVNULL, stderr=PIPE, decode='utf-8'): + stdout=DEVNULL, stderr=PIPE, decode='utf-8', vrf=None, netns=None): """ A wrapper around popen, which discard the stdout and will return the error code of a command @@ -122,13 +150,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None, input=input, timeout=timeout, env=env, shell=shell, decode=decode, + vrf=vrf, + netns=netns, ) return code def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, stdout=PIPE, stderr=PIPE, decode='utf-8', raising=None, message='', - expect=[0], auth=''): + expect=[0], vrf=None, netns=None): """ A wrapper around popen, which returns the stdout and will raise the error code of a command @@ -139,13 +169,17 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, expect: a list of error codes to consider as normal """ decoded, code = popen( - f'{auth} {command}'.strip(), flag, + command, flag, stdout=stdout, stderr=stderr, input=input, timeout=timeout, env=env, shell=shell, decode=decode, + vrf=vrf, + netns=netns, ) if code not in expect: + wrapper = get_wrapper(vrf, netns) + command = f'{wrapper} {command}' feedback = message + '\n' if message else '' feedback += f'failed to run command: {command}\n' feedback += f'returned: {decoded}\n' @@ -159,7 +193,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=STDOUT, decode='utf-8'): + stdout=PIPE, stderr=STDOUT, decode='utf-8', vrf=None, netns=None): """ A wrapper around popen, which returns the return code of a command and stdout @@ -175,11 +209,14 @@ def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None, input=input, timeout=timeout, env=env, shell=shell, decode=decode, + vrf=vrf, + netns=netns, ) return code, out + def call(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=None, stderr=None, decode='utf-8'): + stdout=None, stderr=None, decode='utf-8', vrf=None, netns=None): """ A wrapper around popen, which print the stdout and will return the error code of a command @@ -190,11 +227,14 @@ def call(command, flag='', shell=None, input=None, timeout=None, env=None, input=input, timeout=timeout, env=env, shell=shell, decode=decode, + vrf=vrf, + netns=netns, ) if out: print(out) return code + def process_running(pid_file): """ Checks if a process with PID in pid_file is running """ from psutil import pid_exists diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py new file mode 100644 index 000000000..506095625 --- /dev/null +++ b/python/vyos/vyconf_session.py @@ -0,0 +1,123 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# + +import tempfile +import shutil +from functools import wraps +from typing import Type + +from vyos.proto import vyconf_client +from vyos.migrate import ConfigMigrate +from vyos.migrate import ConfigMigrateError +from vyos.component_version import append_system_version + + +def output(o): + out = '' + for res in (o.output, o.error, o.warning): + if res is not None: + out = out + res + return out + + +class VyconfSession: + def __init__(self, token: str = None, on_error: Type[Exception] = None): + if token is None: + out = vyconf_client.send_request('setup_session') + self.__token = out.output + else: + self.__token = token + + self.on_error = on_error + + @staticmethod + def raise_exception(f): + @wraps(f) + def wrapped(self, *args, **kwargs): + if self.on_error is None: + return f(self, *args, **kwargs) + o, e = f(self, *args, **kwargs) + if e: + raise self.on_error(o) + return o, e + + return wrapped + + @raise_exception + def set(self, path: list[str]) -> tuple[str, int]: + out = vyconf_client.send_request('set', token=self.__token, path=path) + return output(out), out.status + + @raise_exception + def delete(self, path: list[str]) -> tuple[str, int]: + out = vyconf_client.send_request('delete', token=self.__token, path=path) + return output(out), out.status + + @raise_exception + def commit(self) -> tuple[str, int]: + out = vyconf_client.send_request('commit', token=self.__token) + return output(out), out.status + + @raise_exception + def discard(self) -> tuple[str, int]: + out = vyconf_client.send_request('discard', token=self.__token) + return output(out), out.status + + def session_changed(self) -> bool: + out = vyconf_client.send_request('session_changed', token=self.__token) + return not bool(out.status) + + @raise_exception + def load_config(self, file: str, migrate: bool = False) -> tuple[str, int]: + # pylint: disable=consider-using-with + if migrate: + tmp = tempfile.NamedTemporaryFile() + shutil.copy2(file, tmp.name) + config_migrate = ConfigMigrate(tmp.name) + try: + config_migrate.run() + except ConfigMigrateError as e: + tmp.close() + return repr(e), 1 + file = tmp.name + else: + tmp = '' + + out = vyconf_client.send_request('load', token=self.__token, location=file) + if tmp: + tmp.close() + + return output(out), out.status + + @raise_exception + def save_config(self, file: str, append_version: bool = False) -> tuple[str, int]: + out = vyconf_client.send_request('save', token=self.__token, location=file) + if append_version: + append_system_version(file) + return output(out), out.status + + @raise_exception + def show_config(self, path: list[str] = None) -> tuple[str, int]: + if path is None: + path = [] + out = vyconf_client.send_request('show_config', token=self.__token, path=path) + return output(out), out.status + + def __del__(self): + out = vyconf_client.send_request('teardown', token=self.__token) + if out.status: + print(f'Could not tear down session {self.__token}: {output(out)}') diff --git a/python/vyos/wanloadbalance.py b/python/vyos/wanloadbalance.py new file mode 100644 index 000000000..62e109f21 --- /dev/null +++ b/python/vyos/wanloadbalance.py @@ -0,0 +1,153 @@ +#!/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 + +from vyos.defaults import directories +from vyos.utils.process import run + +dhclient_lease = 'dhclient_{0}.lease' + +def nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False): + output = [] + + if 'inbound_interface' in rule_conf: + ifname = rule_conf['inbound_interface'] + if local and not exclude: + output.append(f'oifname != "{ifname}"') + elif not local: + output.append(f'iifname "{ifname}"') + + if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': + protocol = rule_conf['protocol'] + operator = '' + + if protocol[:1] == '!': + operator = '!=' + protocol = protocol[1:] + + if protocol == 'tcp_udp': + protocol = '{ tcp, udp }' + + output.append(f'meta l4proto {operator} {protocol}') + + for direction in ['source', 'destination']: + if direction not in rule_conf: + continue + + direction_conf = rule_conf[direction] + prefix = direction[:1] + + if 'address' in direction_conf: + operator = '' + address = direction_conf['address'] + if address[:1] == '!': + operator = '!=' + address = address[1:] + output.append(f'ip {prefix}addr {operator} {address}') + + if 'port' in direction_conf: + operator = '' + port = direction_conf['port'] + if port[:1] == '!': + operator = '!=' + port = port[1:] + output.append(f'th {prefix}port {operator} {port}') + + if 'source_based_routing' not in rule_conf and not restore_mark: + output.append('ct state new') + + if limit and 'limit' in rule_conf and 'rate' in rule_conf['limit']: + output.append(f'limit rate {rule_conf["limit"]["rate"]}/{rule_conf["limit"]["period"]}') + if 'burst' in rule_conf['limit']: + output.append(f'burst {rule_conf["limit"]["burst"]} packets') + + output.append('counter') + + if restore_mark: + output.append('meta mark set ct mark') + elif weight: + weights, total_weight = wlb_weight_interfaces(rule_conf, health_state) + if len(weights) > 1: # Create weight-based verdict map + vmap_str = ", ".join(f'{weight} : jump wlb_mangle_isp_{ifname}' for ifname, weight in weights) + output.append(f'numgen random mod {total_weight} vmap {{ {vmap_str} }}') + elif len(weights) == 1: # Jump to single ISP + ifname, _ = weights[0] + output.append(f'jump wlb_mangle_isp_{ifname}') + else: # No healthy interfaces + return "" + elif action: + output.append(action) + + return " ".join(output) + +def wlb_weight_interfaces(rule_conf, health_state): + interfaces = [] + + for ifname, if_conf in rule_conf['interface'].items(): + if ifname in health_state and health_state[ifname]['state']: + weight = int(if_conf.get('weight', 1)) + interfaces.append((ifname, weight)) + + if not interfaces: + return [], 0 + + if 'failover' in rule_conf: + for ifpair in sorted(interfaces, key=lambda i: i[1], reverse=True): + return [ifpair], ifpair[1] # Return highest weight interface that is ACTIVE when in failover + + total_weight = sum(weight for _, weight in interfaces) + out = [] + start = 0 + for ifname, weight in sorted(interfaces, key=lambda i: i[1]): # build weight ranges + end = start + weight - 1 + out.append((ifname, f'{start}-{end}' if end > start else start)) + start = weight + + return out, total_weight + +def health_ping_host(host, ifname, count=1, wait_time=0): + cmd_str = f'ping -c {count} -W {wait_time} -I {ifname} {host}' + rc = run(cmd_str) + return rc == 0 + +def health_ping_host_ttl(host, ifname, count=1, ttl_limit=0): + cmd_str = f'ping -c {count} -t {ttl_limit} -I {ifname} {host}' + rc = run(cmd_str) + return rc != 0 + +def parse_dhcp_nexthop(ifname): + lease_file = os.path.join(directories['isc_dhclient_dir'], dhclient_lease.format(ifname)) + + if not os.path.exists(lease_file): + return False + + with open(lease_file, 'r') as f: + for line in f.readlines(): + data = line.replace('\n', '').split('=') + if data[0] == 'new_routers': + return data[1].replace("'", '').split(" ")[0] + + return None + +def parse_ppp_nexthop(ifname): + nexthop_file = os.path.join(directories['ppp_nexthop_dir'], ifname) + + if not os.path.exists(nexthop_file): + return False + + with open(nexthop_file, 'r') as f: + return f.read() diff --git a/python/vyos/xml_ref/generate_op_cache.py b/python/vyos/xml_ref/generate_op_cache.py index cd2ac890e..95779d066 100755 --- a/python/vyos/xml_ref/generate_op_cache.py +++ b/python/vyos/xml_ref/generate_op_cache.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright (C) 2024-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 @@ -33,9 +33,9 @@ _here = dirname(__file__) sys.path.append(join(_here, '..')) from defaults import directories -from op_definition import NodeData from op_definition import PathData + xml_op_cache_json = 'xml_op_cache.json' xml_op_tmp = join('/tmp', xml_op_cache_json) op_ref_cache = abspath(join(_here, 'op_cache.py')) @@ -74,7 +74,7 @@ def translate_op_script(s: str) -> str: return s -def insert_node(n: Element, l: list[PathData], path = None) -> None: +def insert_node(n: Element, l: list[PathData], path=None) -> None: # pylint: disable=too-many-locals,too-many-branches prop: OptElement = n.find('properties') children: OptElement = n.find('children') @@ -95,65 +95,67 @@ def insert_node(n: Element, l: list[PathData], path = None) -> None: if command_text is not None: command_text = translate_command(command_text, path) - comp_help = None + comp_help = {} if prop is not None: - che = prop.findall("completionHelp") + che = prop.findall('completionHelp') + for c in che: - lists = c.findall("list") - paths = c.findall("path") - scripts = c.findall("script") - - comp_help = {} - list_l = [] - for i in lists: - list_l.append(i.text) - path_l = [] - for i in paths: - path_str = re.sub(r'\s+', '/', i.text) - path_l.append(path_str) - script_l = [] - for i in scripts: - script_str = translate_op_script(i.text) - script_l.append(script_str) - - comp_help['list'] = list_l - comp_help['fs_path'] = path_l - comp_help['script'] = script_l - - for d in l: - if name in list(d): - break - else: - d = {} - l.append(d) - - inner_l = d.setdefault(name, []) - - inner_d: PathData = {'node_data': NodeData(node_type=node_type, - help_text=help_text, - comp_help=comp_help, - command=command_text, - path=path)} - inner_l.append(inner_d) + comp_list_els = c.findall('list') + comp_path_els = c.findall('path') + comp_script_els = c.findall('script') + + comp_lists = [] + for i in comp_list_els: + comp_lists.append(i.text) + + comp_paths = [] + for i in comp_path_els: + comp_paths.append(i.text) + + comp_scripts = [] + for i in comp_script_els: + comp_script_str = translate_op_script(i.text) + comp_scripts.append(comp_script_str) + + if comp_lists: + comp_help['list'] = comp_lists + if comp_paths: + comp_help['path'] = comp_paths + if comp_scripts: + comp_help['script'] = comp_scripts + + cur_node_dict = {} + cur_node_dict['name'] = name + cur_node_dict['type'] = node_type + cur_node_dict['comp_help'] = comp_help + cur_node_dict['help'] = help_text + cur_node_dict['command'] = command_text + cur_node_dict['path'] = path + cur_node_dict['children'] = [] + l.append(cur_node_dict) if children is not None: - inner_nodes = children.iterfind("*") + inner_nodes = children.iterfind('*') for inner_n in inner_nodes: inner_path = path[:] - insert_node(inner_n, inner_l, inner_path) + insert_node(inner_n, cur_node_dict['children'], inner_path) def parse_file(file_path, l): tree = ET.parse(file_path) root = tree.getroot() - for n in root.iterfind("*"): + for n in root.iterfind('*'): insert_node(n, l) def main(): parser = ArgumentParser(description='generate dict from xml defintions') - parser.add_argument('--xml-dir', type=str, required=True, - help='transcluded xml op-mode-definition file') + parser.add_argument( + '--xml-dir', + type=str, + required=True, + help='transcluded xml op-mode-definition file', + ) args = vars(parser.parse_args()) @@ -170,5 +172,6 @@ def main(): with open(op_ref_cache, 'w') as f: f.write(f'op_reference = {str(l)}') + if __name__ == '__main__': main() diff --git a/schema/interface_definition.rnc b/schema/interface_definition.rnc index 758d9ce1c..9434f5d18 100644 --- a/schema/interface_definition.rnc +++ b/schema/interface_definition.rnc @@ -97,6 +97,18 @@ properties = element properties valueHelp* & (element constraintErrorMessage { text })? & completionHelp* & + + # "docs" is used to store documentation for a node in a structured format + # It is used to generate documentation for the CLI and Web docs + (element docs { + (element headline { text })? & + (element text { text })? & + (element usageExample { text })? & + (element hints { + attribute type { text }, + text + })* + })? & # These are meaningful only for leaf nodes (element valueless { empty })? & diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng index 94a828c3b..e3d582452 100644 --- a/schema/interface_definition.rng +++ b/schema/interface_definition.rng @@ -2,19 +2,19 @@ <grammar xmlns="http://relaxng.org/ns/structure/1.0"> <!-- interface_definition.rnc: VyConf reference tree XML grammar - + Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net> - + 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 @@ -142,7 +142,7 @@ Nodes may have properties For simplicity, any property is allowed in any node, but whether they are used or not is implementation-defined - + Leaf nodes may differ in number of values that can be associated with them. By default, a leaf node can have only one value. @@ -150,7 +150,7 @@ "valueless" means it can have no values at all. "hidden" means node visibility can be toggled, eg 'dangerous' commands, "secret" allows a node to hide its value from unprivileged users. - + "priority" is used to influence node processing order for nodes with exact same dependencies and in compatibility modes. --> @@ -178,6 +178,39 @@ <ref name="completionHelp"/> </zeroOrMore> <optional> + <!-- + "docs" is used to store documentation for a node in a structured format + It is used to generate documentation for the CLI and Web docs + --> + <group> + <element name="docs"> + <interleave> + <optional> + <element name="headline"> + <text/> + </element> + </optional> + <optional> + <element name="text"> + <text/> + </element> + </optional> + <optional> + <element name="usageExample"> + <text/> + </element> + </optional> + <zeroOrMore> + <element name="hints"> + <attribute name="type"/> + <text/> + </element> + </zeroOrMore> + </interleave> + </element> + </group> + </optional> + <optional> <!-- These are meaningful only for leaf nodes --> <group> <element name="valueless"> diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index d203fdcef..0bb62113e 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -116,7 +116,7 @@ def get_properties(p): if comptype is not None: props["comp_type"] = "imagefiles" comp_exprs.append("echo -n \"<imagefiles>\"") - comp_help = " && ".join(comp_exprs) + comp_help = " ; ".join(comp_exprs) props["comp_help"] = comp_help except: 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-api-service b/smoketest/config-tests/basic-api-service index 3f796f35d..ca10cf4e9 100644 --- a/smoketest/config-tests/basic-api-service +++ b/smoketest/config-tests/basic-api-service @@ -24,5 +24,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/basic-haproxy b/smoketest/config-tests/basic-haproxy new file mode 100644 index 000000000..7755fc4ea --- /dev/null +++ b/smoketest/config-tests/basic-haproxy @@ -0,0 +1,46 @@ +set interfaces dummy dum0 address '172.18.254.203/32' +set interfaces ethernet eth0 duplex 'auto' +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 speed 'auto' +set interfaces ethernet eth2 duplex 'auto' +set interfaces ethernet eth2 speed 'auto' +set load-balancing haproxy backend webserver logging facility daemon +set load-balancing haproxy backend webserver logging facility user level 'info' +set load-balancing haproxy backend webserver server web01 address '192.0.2.1' +set load-balancing haproxy backend webserver server web01 port '443' +set load-balancing haproxy backend webserver ssl no-verify +set load-balancing haproxy global-parameters logging facility daemon +set load-balancing haproxy global-parameters logging facility user level 'info' +set load-balancing haproxy service frontend backend 'webserver' +set load-balancing haproxy service frontend logging facility daemon +set load-balancing haproxy service frontend logging facility user level 'info' +set load-balancing haproxy service frontend port '443' +set load-balancing haproxy service frontend ssl certificate 'dummy' +set pki certificate dummy certificate 'MIIDsTCCApmgAwIBAgIUegVgO1wIN2v44trXZ+Kb1t48uL0wDQYJKoZIhvcNAQELBQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yNTA1MDUxODIzMTdaFw0yNjA1MDUxODIzMTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBSr4OYIWkb8UGuQnEFnjSbmvR+4vDAfBgNVHSMEGDAWgBSr4OYIWkb8UGuQnEFnjSbmvR+4vDANBgkqhkiG9w0BAQsFAAOCAQEAUmRWRPGXsvfuRT+53id3EufH1IJAdowrt6yBZsHobvqCXO2+YhG7oG6/UqUYiv5bHN5xEMQyWd7nyrLOUeFo2bpcMIOlpl6AoUIY65Gm2BqQ7FuPxLLO25RdpZ5WkMGX5kJsKY0/PcpamRKNz1khgFcRyxf9WGhCAIjDCWIWs8lkvPN3m75SFCW7MTuzzQOrzvI6nqqcHO4k8hRBznp26WLUW1rQKpNN09nZGOkeNYK5QbzKN/RUmtEHQZhlgLAIr09jUaA4RDLI1SdD6LR5nvpa9RJBTyS/kISF8BXKMgvUbDHN2nP+VUUrut2ZwoU+pxV4RVT2pS760HuYj4+sYQ==' +set pki certificate dummy private key 'MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAECgf9plqCMg2pKEWRFl183bqWAm7lnLnsUOfABFNPYa3U+uKQUKZpboTBfzDfZqNak3XNQV0mTAR8pFfoMhQjQU9hUxH7ivjw1RUCHjixCF0vLBkTB34gL7FUbiEIFhR1NW3pCJY73OXOkIZG1Obh6Syb8KubDeu4bTmb90/TnDDAs6OYXJ5yo7ZDZLvLu5a3Dli+H4K5Qb5VJ74o/vtodBo3wmKBgy2Ey6JqF+y7/3HeE66rVhYNft5pURgemWnNYqh3oDTJASqpA/8n90o8ceYPVJugQ7029UiyTp0xgBRXFszgiYPkBlsNWB+9+ospopOmYU0owBykH+RtD/bQ0mwECgYEA4gxPdYbHg/GLihHrkv0t5V0pSEhBeBeTBdWG+P/6K90vXofpp7qISdOeYMGkh8mY+PfZNHksdu67d2ks5on5/dNf5YXWCm+LMMRiSsfOo11NISNdNHS6afqs18Wq1aKawv/rwotfxrakM4Gar692+jgz/l/X+FdOQwmE6uEus8ECgYEA3oW4eGtzGq28TiqyhTHduTkas0ckPWX8ulasyPnLxBKDNNohbXXpFIJIcrnl24QFJw3MJbo+R+OxZHgzPK8r64gIGVa7vLCR2fiU/RFoUa1Jo1pPOqXXWqf/Mvdokm2p0atrjRUX9VhjoFsDLcqTgAmfSCsVSqGucX6ER7Gy60UCgYAvmhoNjNFtFquk6rsqHAjTOTgdUaH/0S8T1nBy9SzQmeaEyKhKuvxCV78NbxnfwnNlUoQ6CZ50eTefINXkwn+TlTSnl/SIBA9SuLheOQ9p1ZcNeG4DQuWStcg6NBUSoghnMg+Ky2Di7slLU2qovpGWhcllMve/A1umwFVuRPdZwQKBgHS7mYYyd/Oq6HnpFDWjbzlXp5Yc3/oFooruJT5ZLHfzbjkvpRGTJW7I2dC1jMuXekx+hHXWOg3keI7IL7jJ/DRW7Ei+o0XdKuY57Y7ErwEJ8vNq0N1nWo4IS2wlNgp61PdVAdrFEgh3EexxUj2XY8FrSs/FKio4nxaS1Dn4EnAxAoGBAKqLvuPpmCMVwlIu57WGxL5d9i7EjIGY65l6HTKQYoHCzE51rkowH1La2fuUYz0IpExq2lcrLbOUtSyhXH7Zlktiz//Gu/P90SUfR/ZGcLeZi+EDyK2OctpnWBjs2Dmfg4D6vxk39yV8AB97pYG073GcJ/P54qRUuEitbpJwH+fB' +set service ntp allow-client address '0.0.0.0/0' +set service ntp allow-client address '::/0' +set service ntp server 172.16.100.10 +set service ntp server 172.16.100.20 +set service ntp server 172.16.110.30 +set service ssh disable-host-validation +set service ssh port '22' +set system config-management commit-revisions '200' +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 host-name 'vyos' +set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' +set system login user vyos authentication plaintext-password '' +set system name-server '172.16.254.30' +set system option kernel disable-mitigations +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' +set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/basic-syslog b/smoketest/config-tests/basic-syslog new file mode 100644 index 000000000..349d642fd --- /dev/null +++ b/smoketest/config-tests/basic-syslog @@ -0,0 +1,25 @@ +set interfaces ethernet eth0 duplex 'auto' +set interfaces ethernet eth0 speed 'auto' +set interfaces ethernet eth1 address '172.16.33.154/24' +set interfaces ethernet eth1 duplex 'auto' +set interfaces ethernet eth1 speed 'auto' +set interfaces ethernet eth1 vrf 'red' +set system console device ttyS0 speed '115200' +set system domain-name 'vyos-ci-test.net' +set system host-name 'vyos' +set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' +set system login user vyos authentication plaintext-password '' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' +set system syslog marker interval '999' +set system syslog preserve-fqdn +set system syslog remote syslog01.vyos.net facility local7 level 'notice' +set system syslog remote syslog01.vyos.net port '8000' +set system syslog remote syslog01.vyos.net vrf 'red' +set system syslog remote syslog02.vyos.net facility all level 'debug' +set system syslog remote syslog02.vyos.net format include-timezone +set system syslog remote syslog02.vyos.net format octet-counted +set system syslog remote syslog02.vyos.net port '8001' +set system syslog remote syslog02.vyos.net protocol 'tcp' +set system syslog remote syslog02.vyos.net vrf 'red' +set vrf name red table '12321' diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos index 6ff28ec2e..aaf450e80 100644 --- a/smoketest/config-tests/basic-vyos +++ b/smoketest/config-tests/basic-vyos @@ -28,7 +28,21 @@ set protocols static arp interface eth2.200.201 address 100.64.201.20 mac '00:50 set protocols static arp interface eth2.200.202 address 100.64.202.30 mac '00:50:00:00:00:30' set protocols static arp interface eth2.200.202 address 100.64.202.40 mac '00:50:00:00:00:40' set protocols static route 0.0.0.0/0 next-hop 100.64.0.1 +set service dhcp-server dynamic-dns-update send-updates 'enable' +set service dhcp-server dynamic-dns-update conflict-resolution 'enable' +set service dhcp-server dynamic-dns-update tsig-key domain-lan-updates algorithm 'sha256' +set service dhcp-server dynamic-dns-update tsig-key domain-lan-updates secret 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ==' +set service dhcp-server dynamic-dns-update tsig-key reverse-0-168-192 algorithm 'sha256' +set service dhcp-server dynamic-dns-update tsig-key reverse-0-168-192 secret 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ==' +set service dhcp-server dynamic-dns-update forward-domain domain.lan dns-server 1 address '192.168.0.1' +set service dhcp-server dynamic-dns-update forward-domain domain.lan dns-server 2 address '100.100.0.1' +set service dhcp-server dynamic-dns-update forward-domain domain.lan key-name 'domain-lan-updates' +set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa dns-server 1 address '192.168.0.1' +set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa dns-server 2 address '100.100.0.1' +set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa key-name 'reverse-0-168-192' set service dhcp-server shared-network-name LAN authoritative +set service dhcp-server shared-network-name LAN dynamic-dns-update send-updates 'enable' +set service dhcp-server shared-network-name LAN dynamic-dns-update ttl-percent '75' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-name 'vyos.net' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-search 'vyos.net' @@ -46,6 +60,9 @@ set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-map set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping TEST2-2 ip-address '192.168.0.21' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping TEST2-2 mac '00:01:02:03:04:22' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update send-updates 'enable' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update generated-prefix 'myhost' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update qualifying-suffix 'lan1.domain.lan' set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 interface 'eth0' set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 option domain-search 'vyos.net' set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 option name-server 'fe88::1' @@ -92,12 +109,14 @@ set system login user vyos authentication plaintext-password '' set system name-server '192.168.0.1' set system syslog console facility all level 'emerg' set system syslog console facility mail level 'info' -set system syslog global facility all level 'info' -set system syslog global facility auth level 'info' -set system syslog global facility local7 level 'debug' -set system syslog global preserve-fqdn -set system syslog host syslog.vyos.net facility auth level 'warning' -set system syslog host syslog.vyos.net facility local7 level 'notice' -set system syslog host syslog.vyos.net format octet-counted -set system syslog host syslog.vyos.net port '8000' +set system syslog local facility all level 'info' +set system syslog local facility auth level 'info' +set system syslog local facility local7 level 'debug' +set system syslog marker interval '1000' +set system syslog preserve-fqdn +set system syslog remote syslog.vyos.net facility auth level 'warning' +set system syslog remote syslog.vyos.net facility local7 level 'notice' +set system syslog remote syslog.vyos.net format octet-counted +set system syslog remote syslog.vyos.net port '8000' +set system syslog remote syslog.vyos.net protocol 'tcp' set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/basic-vyos-no-ntp b/smoketest/config-tests/basic-vyos-no-ntp new file mode 100644 index 000000000..f00dea5d4 --- /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 local facility all level 'debug' +set system syslog local facility local7 level 'debug' +set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/bgp-azure-ipsec-gateway b/smoketest/config-tests/bgp-azure-ipsec-gateway index bbd7b961f..0d683c921 100644 --- a/smoketest/config-tests/bgp-azure-ipsec-gateway +++ b/smoketest/config-tests/bgp-azure-ipsec-gateway @@ -135,10 +135,10 @@ set system login user vyos authentication plaintext-password '' set system logs logrotate messages max-size '20' set system logs logrotate messages rotate '10' set system name-server '192.0.2.254' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' -set system syslog host 10.0.9.188 facility all level 'info' -set system syslog host 10.0.9.188 protocol 'udp' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' +set system syslog remote 10.0.9.188 facility all level 'info' +set system syslog remote 10.0.9.188 protocol 'udp' set system time-zone 'Europe/Berlin' set vpn ipsec authentication psk peer_51-105-0-1 id '51.105.0.1' set vpn ipsec authentication psk peer_51-105-0-1 id '192.0.2.189' diff --git a/smoketest/config-tests/bgp-bfd-communities b/smoketest/config-tests/bgp-bfd-communities index 6eee0137e..06e412c55 100644 --- a/smoketest/config-tests/bgp-bfd-communities +++ b/smoketest/config-tests/bgp-bfd-communities @@ -196,6 +196,6 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local 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 8de0cdb02..f71a51be3 100644 --- a/smoketest/config-tests/bgp-big-as-cloud +++ b/smoketest/config-tests/bgp-big-as-cloud @@ -198,44 +198,44 @@ set firewall zone management from peers firewall ipv6-name 'peers-to-management- set firewall zone management from peers firewall name 'peers-to-management-4' set firewall zone management from servers firewall ipv6-name 'servers-to-management-6' set firewall zone management from servers firewall name 'servers-to-management-4' -set firewall zone management interface 'eth0' +set firewall zone management member interface 'eth0' set firewall zone peers default-action 'reject' set firewall zone peers from management firewall ipv6-name 'management-to-peers-6' set firewall zone peers from management firewall name 'management-to-peers-4' set firewall zone peers from servers firewall ipv6-name 'servers-to-peers-6' set firewall zone peers from servers firewall name 'servers-to-peers-4' -set firewall zone peers interface 'eth0.4088' -set firewall zone peers interface 'eth0.4089' -set firewall zone peers interface 'eth0.11' -set firewall zone peers interface 'eth0.838' -set firewall zone peers interface 'eth0.886' +set firewall zone peers member interface 'eth0.4088' +set firewall zone peers member interface 'eth0.4089' +set firewall zone peers member interface 'eth0.11' +set firewall zone peers member interface 'eth0.838' +set firewall zone peers member interface 'eth0.886' set firewall zone servers default-action 'reject' set firewall zone servers from management firewall ipv6-name 'management-to-servers-6' set firewall zone servers from management firewall name 'management-to-servers-4' set firewall zone servers from peers firewall ipv6-name 'peers-to-servers-6' set firewall zone servers from peers firewall name 'peers-to-servers-4' -set firewall zone servers interface 'eth0.1001' -set firewall zone servers interface 'eth0.105' -set firewall zone servers interface 'eth0.102' -set firewall zone servers interface 'eth0.1019' -set firewall zone servers interface 'eth0.1014' -set firewall zone servers interface 'eth0.1020' -set firewall zone servers interface 'eth0.1018' -set firewall zone servers interface 'eth0.1013' -set firewall zone servers interface 'eth0.1012' -set firewall zone servers interface 'eth0.1011' -set firewall zone servers interface 'eth0.1010' -set firewall zone servers interface 'eth0.1009' -set firewall zone servers interface 'eth0.1006' -set firewall zone servers interface 'eth0.1005' -set firewall zone servers interface 'eth0.1017' -set firewall zone servers interface 'eth0.1016' -set firewall zone servers interface 'eth0.1002' -set firewall zone servers interface 'eth0.1015' -set firewall zone servers interface 'eth0.1003' -set firewall zone servers interface 'eth0.1004' -set firewall zone servers interface 'eth0.1007' -set firewall zone servers interface 'eth0.1008' +set firewall zone servers member interface 'eth0.1001' +set firewall zone servers member interface 'eth0.105' +set firewall zone servers member interface 'eth0.102' +set firewall zone servers member interface 'eth0.1019' +set firewall zone servers member interface 'eth0.1014' +set firewall zone servers member interface 'eth0.1020' +set firewall zone servers member interface 'eth0.1018' +set firewall zone servers member interface 'eth0.1013' +set firewall zone servers member interface 'eth0.1012' +set firewall zone servers member interface 'eth0.1011' +set firewall zone servers member interface 'eth0.1010' +set firewall zone servers member interface 'eth0.1009' +set firewall zone servers member interface 'eth0.1006' +set firewall zone servers member interface 'eth0.1005' +set firewall zone servers member interface 'eth0.1017' +set firewall zone servers member interface 'eth0.1016' +set firewall zone servers member interface 'eth0.1002' +set firewall zone servers member interface 'eth0.1015' +set firewall zone servers member interface 'eth0.1003' +set firewall zone servers member interface 'eth0.1004' +set firewall zone servers member interface 'eth0.1007' +set firewall zone servers member interface 'eth0.1008' set high-availability vrrp group 11-4 address 192.0.68.1/27 set high-availability vrrp group 11-4 interface 'eth0.11' set high-availability vrrp group 11-4 priority '200' @@ -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 syslog global facility all level 'all' -set system syslog global preserve-fqdn +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 local facility all level 'all' +set system syslog 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..f9ceba11c 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' @@ -48,8 +50,8 @@ set system login user vyos authentication plaintext-password '' set system name-server '1.1.1.1' set system name-server '8.8.8.8' set system name-server '9.9.9.9' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set vpn ipsec esp-group ESP-DMVPN lifetime '1800' set vpn ipsec esp-group ESP-DMVPN mode 'transport' set vpn ipsec esp-group ESP-DMVPN pfs 'dh-group2' diff --git a/smoketest/config-tests/bgp-dmvpn-spoke b/smoketest/config-tests/bgp-dmvpn-spoke index d1c7bc7c0..a98275ba4 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' @@ -54,8 +56,8 @@ set system login user vyos authentication plaintext-password '' set system name-server '1.1.1.1' set system name-server '8.8.8.8' set system name-server '9.9.9.9' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set vpn ipsec esp-group ESP-DMVPN lifetime '1800' set vpn ipsec esp-group ESP-DMVPN mode 'transport' set vpn ipsec esp-group ESP-DMVPN pfs 'dh-group2' diff --git a/smoketest/config-tests/bgp-evpn-l2vpn-leaf b/smoketest/config-tests/bgp-evpn-l2vpn-leaf index 315cb9e06..5e42a269e 100644 --- a/smoketest/config-tests/bgp-evpn-l2vpn-leaf +++ b/smoketest/config-tests/bgp-evpn-l2vpn-leaf @@ -48,8 +48,8 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set vrf name MGMT protocols static route 0.0.0.0/0 next-hop 192.0.2.62 set vrf name MGMT protocols static route6 ::/0 next-hop 2001:db8::1 set vrf name MGMT table '1000' diff --git a/smoketest/config-tests/bgp-evpn-l2vpn-spine b/smoketest/config-tests/bgp-evpn-l2vpn-spine index dee29e021..e6d876af6 100644 --- a/smoketest/config-tests/bgp-evpn-l2vpn-spine +++ b/smoketest/config-tests/bgp-evpn-l2vpn-spine @@ -41,8 +41,8 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set vrf name MGMT protocols static route 0.0.0.0/0 next-hop 192.0.2.62 set vrf name MGMT protocols static route6 ::/0 next-hop 2001:db8::1 set vrf name MGMT table '1000' diff --git a/smoketest/config-tests/bgp-evpn-l3vpn-pe-router b/smoketest/config-tests/bgp-evpn-l3vpn-pe-router index 7a2ec9f91..f867c221e 100644 --- a/smoketest/config-tests/bgp-evpn-l3vpn-pe-router +++ b/smoketest/config-tests/bgp-evpn-l3vpn-pe-router @@ -101,8 +101,8 @@ set system login user vyos authentication plaintext-password '' set system name-server '192.0.2.251' set system name-server '192.0.2.252' set system name-server '2001:db8::1' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set vrf name blue protocols bgp address-family ipv4-unicast redistribute connected set vrf name blue protocols bgp address-family l2vpn-evpn advertise ipv4 unicast set vrf name blue protocols bgp system-as '100' diff --git a/smoketest/config-tests/bgp-medium-confederation b/smoketest/config-tests/bgp-medium-confederation index 582e28047..71797fe93 100644 --- a/smoketest/config-tests/bgp-medium-confederation +++ b/smoketest/config-tests/bgp-medium-confederation @@ -69,5 +69,5 @@ set system host-name 'vyos' set system ip protocol bgp route-map 'DEFAULT-ZEBRA-IN' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'notice' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'notice' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/bgp-rpki b/smoketest/config-tests/bgp-rpki index 44e95ae98..657d4abcc 100644 --- a/smoketest/config-tests/bgp-rpki +++ b/smoketest/config-tests/bgp-rpki @@ -13,6 +13,7 @@ set policy route-map ebgp-transit-rpki rule 30 set local-preference '100' set policy route-map ebgp-transit-rpki rule 40 action 'permit' set policy route-map ebgp-transit-rpki rule 40 set extcommunity rt '192.0.2.100:100' set policy route-map ebgp-transit-rpki rule 40 set extcommunity soo '64500:100' +set protocols bgp address-family ipv4-unicast redistribute table 100 set protocols bgp neighbor 1.2.3.4 address-family ipv4-unicast nexthop-self set protocols bgp neighbor 1.2.3.4 address-family ipv4-unicast route-map import 'ebgp-transit-rpki' set protocols bgp neighbor 1.2.3.4 remote-as '10' @@ -39,5 +40,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/bgp-small-internet-exchange b/smoketest/config-tests/bgp-small-internet-exchange index a9dce4dd5..2adb3fbb5 100644 --- a/smoketest/config-tests/bgp-small-internet-exchange +++ b/smoketest/config-tests/bgp-small-internet-exchange @@ -205,5 +205,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/bgp-small-ipv4-unicast b/smoketest/config-tests/bgp-small-ipv4-unicast index b8c0e1246..f8820cb3c 100644 --- a/smoketest/config-tests/bgp-small-ipv4-unicast +++ b/smoketest/config-tests/bgp-small-ipv4-unicast @@ -28,5 +28,5 @@ set system domain-name 'vyos.net' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'notice' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'notice' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/cluster-basic b/smoketest/config-tests/cluster-basic index 744c117eb..871b40bbb 100644 --- a/smoketest/config-tests/cluster-basic +++ b/smoketest/config-tests/cluster-basic @@ -16,6 +16,6 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Antarctica/South_Pole' diff --git a/smoketest/config-tests/dialup-router-complex b/smoketest/config-tests/dialup-router-complex index 4416ef82e..12edcfef2 100644 --- a/smoketest/config-tests/dialup-router-complex +++ b/smoketest/config-tests/dialup-router-complex @@ -508,7 +508,7 @@ set firewall zone DMZ from GUEST firewall name 'GUEST-DMZ' set firewall zone DMZ from LAN firewall name 'LAN-DMZ' set firewall zone DMZ from LOCAL firewall name 'LOCAL-DMZ' set firewall zone DMZ from WAN firewall name 'WAN-DMZ' -set firewall zone DMZ interface 'eth0.50' +set firewall zone DMZ member interface 'eth0.50' set firewall zone GUEST default-action 'drop' set firewall zone GUEST from DMZ firewall name 'DMZ-GUEST' set firewall zone GUEST from IOT firewall name 'IOT-GUEST' @@ -517,13 +517,13 @@ set firewall zone GUEST from LOCAL firewall ipv6-name 'ALLOW-ALL-6' set firewall zone GUEST from LOCAL firewall name 'LOCAL-GUEST' set firewall zone GUEST from WAN firewall ipv6-name 'ALLOW-ESTABLISHED-6' set firewall zone GUEST from WAN firewall name 'WAN-GUEST' -set firewall zone GUEST interface 'eth0.20' +set firewall zone GUEST member interface 'eth0.20' set firewall zone IOT default-action 'drop' set firewall zone IOT from GUEST firewall name 'GUEST-IOT' set firewall zone IOT from LAN firewall name 'LAN-IOT' set firewall zone IOT from LOCAL firewall name 'LOCAL-IOT' set firewall zone IOT from WAN firewall name 'WAN-IOT' -set firewall zone IOT interface 'eth0.35' +set firewall zone IOT member interface 'eth0.35' set firewall zone LAN default-action 'drop' set firewall zone LAN from DMZ firewall name 'DMZ-LAN' set firewall zone LAN from GUEST firewall name 'GUEST-LAN' @@ -532,13 +532,13 @@ set firewall zone LAN from LOCAL firewall ipv6-name 'ALLOW-ALL-6' set firewall zone LAN from LOCAL firewall name 'LOCAL-LAN' set firewall zone LAN from WAN firewall ipv6-name 'ALLOW-ESTABLISHED-6' set firewall zone LAN from WAN firewall name 'WAN-LAN' -set firewall zone LAN interface 'eth0.5' -set firewall zone LAN interface 'eth0.10' -set firewall zone LAN interface 'eth0.100' -set firewall zone LAN interface 'eth0.201' -set firewall zone LAN interface 'eth0.202' -set firewall zone LAN interface 'eth0.203' -set firewall zone LAN interface 'eth0.204' +set firewall zone LAN member interface 'eth0.5' +set firewall zone LAN member interface 'eth0.10' +set firewall zone LAN member interface 'eth0.100' +set firewall zone LAN member interface 'eth0.201' +set firewall zone LAN member interface 'eth0.202' +set firewall zone LAN member interface 'eth0.203' +set firewall zone LAN member interface 'eth0.204' set firewall zone LOCAL default-action 'drop' set firewall zone LOCAL from DMZ firewall name 'DMZ-LOCAL' set firewall zone LOCAL from GUEST firewall ipv6-name 'ALLOW-ESTABLISHED-6' @@ -558,7 +558,7 @@ set firewall zone WAN from LAN firewall ipv6-name 'ALLOW-ALL-6' set firewall zone WAN from LAN firewall name 'LAN-WAN' set firewall zone WAN from LOCAL firewall ipv6-name 'ALLOW-ALL-6' set firewall zone WAN from LOCAL firewall name 'LOCAL-WAN' -set firewall zone WAN interface 'pppoe0' +set firewall zone WAN member interface 'pppoe0' set interfaces dummy dum0 address '172.16.254.30/32' set interfaces ethernet eth0 duplex 'auto' set interfaces ethernet eth0 speed 'auto' @@ -695,6 +695,7 @@ set service dns forwarding ignore-hosts-file set service dns forwarding listen-address '172.16.254.30' set service dns forwarding listen-address '172.31.0.254' set service dns forwarding negative-ttl '60' +set service lldp interface pppoe0 mode 'disable' set service lldp legacy-protocols cdp set service lldp snmp set service mdns repeater interface 'eth0.35' @@ -734,7 +735,7 @@ set system name-server '172.16.254.30' set system option ctrl-alt-delete 'ignore' set system option reboot-on-panic set system option startup-beep -set system syslog global facility all level 'debug' -set system syslog global facility local7 level 'debug' -set system syslog host 172.16.100.1 facility all level 'warning' +set system syslog local facility all level 'debug' +set system syslog local facility local7 level 'debug' +set system syslog remote 172.16.100.1 facility all level 'warning' set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn index d6b00c678..ba3ed29f4 100644 --- a/smoketest/config-tests/dialup-router-medium-vpn +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -314,9 +314,9 @@ set system static-host-mapping host-name host107.vyos.net inet '192.168.0.107' set system static-host-mapping host-name host109.vyos.net inet '192.168.0.109' set system sysctl parameter net.core.default_qdisc value 'fq' set system sysctl parameter net.ipv4.tcp_congestion_control value 'bbr' -set system syslog global facility all level 'info' -set system syslog host 192.168.0.252 facility all level 'debug' -set system syslog host 192.168.0.252 protocol 'udp' +set system syslog local facility all level 'info' +set system syslog remote 192.168.0.252 facility all level 'debug' +set system syslog remote 192.168.0.252 protocol 'udp' set system task-scheduler task Update-Blacklists executable path '/config/scripts/vyos-foo-update.script' set system task-scheduler task Update-Blacklists interval '3h' set system time-zone 'Pacific/Auckland' diff --git a/smoketest/config-tests/dialup-router-wireguard-ipv6 b/smoketest/config-tests/dialup-router-wireguard-ipv6 index c2cf2e9d8..269e9d722 100644 --- a/smoketest/config-tests/dialup-router-wireguard-ipv6 +++ b/smoketest/config-tests/dialup-router-wireguard-ipv6 @@ -391,7 +391,7 @@ set firewall zone DMZ from GUEST firewall name 'GUEST-DMZ' set firewall zone DMZ from LAN firewall name 'LAN-DMZ' set firewall zone DMZ from LOCAL firewall name 'LOCAL-DMZ' set firewall zone DMZ from WAN firewall name 'WAN-DMZ' -set firewall zone DMZ interface 'eth0.50' +set firewall zone DMZ member interface 'eth0.50' set firewall zone GUEST default-action 'drop' set firewall zone GUEST from DMZ firewall name 'DMZ-GUEST' set firewall zone GUEST from LAN firewall name 'LAN-GUEST' @@ -399,7 +399,7 @@ set firewall zone GUEST from LOCAL firewall ipv6-name 'ALLOW-ALL-6' set firewall zone GUEST from LOCAL firewall name 'LOCAL-GUEST' set firewall zone GUEST from WAN firewall ipv6-name 'ALLOW-ESTABLISHED-6' set firewall zone GUEST from WAN firewall name 'WAN-GUEST' -set firewall zone GUEST interface 'eth1.20' +set firewall zone GUEST member interface 'eth1.20' set firewall zone LAN default-action 'drop' set firewall zone LAN from DMZ firewall name 'DMZ-LAN' set firewall zone LAN from GUEST firewall name 'GUEST-LAN' @@ -407,10 +407,10 @@ set firewall zone LAN from LOCAL firewall ipv6-name 'ALLOW-ALL-6' set firewall zone LAN from LOCAL firewall name 'LOCAL-LAN' set firewall zone LAN from WAN firewall ipv6-name 'ALLOW-ESTABLISHED-6' set firewall zone LAN from WAN firewall name 'WAN-LAN' -set firewall zone LAN interface 'eth0.5' -set firewall zone LAN interface 'eth0.10' -set firewall zone LAN interface 'wg100' -set firewall zone LAN interface 'wg200' +set firewall zone LAN member interface 'eth0.5' +set firewall zone LAN member interface 'eth0.10' +set firewall zone LAN member interface 'wg100' +set firewall zone LAN member interface 'wg200' set firewall zone LOCAL default-action 'drop' set firewall zone LOCAL from DMZ firewall name 'DMZ-LOCAL' set firewall zone LOCAL from GUEST firewall ipv6-name 'ALLOW-ESTABLISHED-6' @@ -428,8 +428,8 @@ set firewall zone WAN from LAN firewall ipv6-name 'ALLOW-ALL-6' set firewall zone WAN from LAN firewall name 'LAN-WAN' set firewall zone WAN from LOCAL firewall ipv6-name 'ALLOW-ALL-6' set firewall zone WAN from LOCAL firewall name 'LOCAL-WAN' -set firewall zone WAN interface 'pppoe0' -set firewall zone WAN interface 'wg666' +set firewall zone WAN member interface 'pppoe0' +set firewall zone WAN member interface 'wg666' set interfaces dummy dum0 address '172.16.254.30/32' set interfaces ethernet eth0 duplex 'auto' set interfaces ethernet eth0 offload gro @@ -691,7 +691,7 @@ set system option ctrl-alt-delete 'ignore' set system option performance 'network-latency' set system option reboot-on-panic set system option startup-beep -set system syslog global facility all level 'debug' -set system syslog global facility local7 level 'debug' -set system syslog host 172.16.100.1 facility all level 'warning' +set system syslog local facility all level 'debug' +set system syslog local facility local7 level 'debug' +set system syslog remote 172.16.100.1 facility all level 'warning' set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/egp-igp-route-maps b/smoketest/config-tests/egp-igp-route-maps index fc46d25ff..222325cd7 100644 --- a/smoketest/config-tests/egp-igp-route-maps +++ b/smoketest/config-tests/egp-igp-route-maps @@ -42,5 +42,5 @@ set system login user vyos authentication plaintext-password '' set system logs logrotate messages max-size '1' set system logs logrotate messages rotate '5' set system name-server '192.168.0.1' -set system syslog global facility all level 'info' +set system syslog local facility all level 'info' set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/igmp-pim-small b/smoketest/config-tests/igmp-pim-small index 909c3d67b..06051af41 100644 --- a/smoketest/config-tests/igmp-pim-small +++ b/smoketest/config-tests/igmp-pim-small @@ -32,6 +32,6 @@ set system domain-name 'vyos.io' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/ipoe-server b/smoketest/config-tests/ipoe-server index f4a12f502..c21495ab2 100644 --- a/smoketest/config-tests/ipoe-server +++ b/smoketest/config-tests/ipoe-server @@ -44,5 +44,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/ipv6-disable b/smoketest/config-tests/ipv6-disable index 40e34fa0c..5f906b5f7 100644 --- a/smoketest/config-tests/ipv6-disable +++ b/smoketest/config-tests/ipv6-disable @@ -27,5 +27,5 @@ set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX set system login user vyos authentication plaintext-password '' set system name-server '172.16.254.20' set system name-server '172.16.254.30' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/isis-small b/smoketest/config-tests/isis-small index b322f4e29..e61d0362e 100644 --- a/smoketest/config-tests/isis-small +++ b/smoketest/config-tests/isis-small @@ -39,6 +39,6 @@ set system login user vyos authentication plaintext-password '' set service ntp server time1.vyos.net set service ntp server time2.vyos.net set service ntp server time3.vyos.net -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/nat-basic b/smoketest/config-tests/nat-basic index 471add3b3..f1cc0121d 100644 --- a/smoketest/config-tests/nat-basic +++ b/smoketest/config-tests/nat-basic @@ -60,7 +60,7 @@ set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0 set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0 stop '192.168.189.254' set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 subnet-id '1' set service lldp interface all -set service lldp interface eth1 disable +set service lldp interface eth1 mode 'disable' set service ntp allow-client address '192.168.189.0/24' set service ntp listen-address '192.168.189.1' set service ntp server time1.vyos.net @@ -84,5 +84,5 @@ set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX set system login user vyos authentication plaintext-password '' set system name-server '1.1.1.1' set system name-server '9.9.9.9' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/ospf-simple b/smoketest/config-tests/ospf-simple index 355709448..4273e4b8f 100644 --- a/smoketest/config-tests/ospf-simple +++ b/smoketest/config-tests/ospf-simple @@ -20,5 +20,5 @@ set system console device ttyS0 speed '115200' set system host-name 'lab-vyos-r1' set system login user vyos authentication encrypted-password '$6$R.OnGzfXSfl6J$Iba/hl9bmjBs0VPtZ2zdW.Snh/nHuvxUwi0R6ruypgW63iKEbicJH.uUst8xZCyByURblxRtjAC1lAnYfIt.b0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/ospf-small b/smoketest/config-tests/ospf-small index a7f8b682c..af69e5702 100644 --- a/smoketest/config-tests/ospf-small +++ b/smoketest/config-tests/ospf-small @@ -77,6 +77,6 @@ set system sysctl parameter net.ipv4.igmp_max_memberships value '5' set system sysctl parameter net.ipv4.ipfrag_time value '4' set system sysctl parameter net.mpls.default_ttl value '10' set system sysctl parameter net.mpls.ip_ttl_propagate value '0' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/pppoe-server b/smoketest/config-tests/pppoe-server index 34fbea215..e488fc746 100644 --- a/smoketest/config-tests/pppoe-server +++ b/smoketest/config-tests/pppoe-server @@ -43,5 +43,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/qos-basic b/smoketest/config-tests/qos-basic index 0e198b80c..655a5794e 100644 --- a/smoketest/config-tests/qos-basic +++ b/smoketest/config-tests/qos-basic @@ -71,5 +71,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' 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 syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/rip-router b/smoketest/config-tests/rip-router index 829aafbd5..d22f424a5 100644 --- a/smoketest/config-tests/rip-router +++ b/smoketest/config-tests/rip-router @@ -79,5 +79,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/rpki-only b/smoketest/config-tests/rpki-only index dcbc7673d..f3e2a74b9 100644 --- a/smoketest/config-tests/rpki-only +++ b/smoketest/config-tests/rpki-only @@ -38,5 +38,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' 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 syslog global facility all level 'debug' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'debug' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/static-route-basic b/smoketest/config-tests/static-route-basic index d2d33d043..a6135d2c4 100644 --- a/smoketest/config-tests/static-route-basic +++ b/smoketest/config-tests/static-route-basic @@ -32,6 +32,6 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Asia/Macau' diff --git a/smoketest/config-tests/tunnel-broker b/smoketest/config-tests/tunnel-broker index ee6301c85..5518c303b 100644 --- a/smoketest/config-tests/tunnel-broker +++ b/smoketest/config-tests/tunnel-broker @@ -71,5 +71,5 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' diff --git a/smoketest/config-tests/vpn-openconnect-sstp b/smoketest/config-tests/vpn-openconnect-sstp index 28d7d5daa..e7969f633 100644 --- a/smoketest/config-tests/vpn-openconnect-sstp +++ b/smoketest/config-tests/vpn-openconnect-sstp @@ -16,8 +16,8 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set vpn openconnect authentication local-users username test password 'test' set vpn openconnect authentication mode local 'password' set vpn openconnect network-settings client-ip-settings subnet '192.168.160.0/24' diff --git a/smoketest/config-tests/vrf-basic b/smoketest/config-tests/vrf-basic index 1d2874a60..0c4e49c52 100644 --- a/smoketest/config-tests/vrf-basic +++ b/smoketest/config-tests/vrf-basic @@ -35,8 +35,8 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Europe/Berlin' set vrf name green protocols static route 20.0.0.0/8 next-hop 1.1.1.1 interface 'eth1' set vrf name green protocols static route 20.0.0.0/8 next-hop 1.1.1.1 vrf 'default' diff --git a/smoketest/config-tests/vrf-bgp-pppoe-underlay b/smoketest/config-tests/vrf-bgp-pppoe-underlay index bd64c914a..e3c765a9a 100644 --- a/smoketest/config-tests/vrf-bgp-pppoe-underlay +++ b/smoketest/config-tests/vrf-bgp-pppoe-underlay @@ -143,8 +143,8 @@ set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' set system name-server '192.168.0.1' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Europe/Berlin' set vrf bind-to-all set vrf name vyos-test-01 protocols bgp address-family ipv4-unicast network 100.64.50.0/23 diff --git a/smoketest/config-tests/vrf-ospf b/smoketest/config-tests/vrf-ospf index fd14615e0..53207d565 100644 --- a/smoketest/config-tests/vrf-ospf +++ b/smoketest/config-tests/vrf-ospf @@ -28,8 +28,8 @@ set system console device ttyS0 speed '115200' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' set system login user vyos authentication plaintext-password '' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system time-zone 'Europe/Berlin' set vrf name blue protocols ospf area 0 network '172.18.201.0/24' set vrf name blue protocols ospf interface eth2 authentication md5 key-id 30 md5-key 'vyoskey456' diff --git a/smoketest/config-tests/wireless-basic b/smoketest/config-tests/wireless-basic index d9e6c8fac..e424b2b0f 100644 --- a/smoketest/config-tests/wireless-basic +++ b/smoketest/config-tests/wireless-basic @@ -20,6 +20,6 @@ set system console device ttyS0 speed '115200' set system domain-name 'dev.vyos.net' set system host-name 'WR1' set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' -set system syslog global facility all level 'info' -set system syslog global facility local7 level 'debug' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' set system wireless country-code 'es' diff --git a/smoketest/configs/basic-haproxy b/smoketest/configs/basic-haproxy new file mode 100644 index 000000000..83fffbac6 --- /dev/null +++ b/smoketest/configs/basic-haproxy @@ -0,0 +1,153 @@ +interfaces { + dummy dum0 { + address "172.18.254.203/32" + } + ethernet eth0 { + duplex "auto" + speed "auto" + vif 203 { + address "172.18.203.10/24" + } + } + ethernet eth1 { + duplex "auto" + speed "auto" + } + ethernet eth2 { + duplex "auto" + speed "auto" + } +} +load-balancing { + reverse-proxy { + backend webserver { + logging { + facility all { + level "all" + } + facility daemon { + level "all" + } + facility user { + level "info" + } + } + server web01 { + address "192.0.2.1" + port "443" + } + ssl { + no-verify + } + } + global-parameters { + logging { + facility all { + level "all" + } + facility daemon { + level "all" + } + facility user { + level "info" + } + } + } + service frontend { + backend "webserver" + logging { + facility all { + level "all" + } + facility daemon { + level "all" + } + facility user { + level "info" + } + } + port "443" + ssl { + certificate "dummy" + } + } + } +} +pki { + certificate dummy { + certificate "MIIDsTCCApmgAwIBAgIUegVgO1wIN2v44trXZ+Kb1t48uL0wDQYJKoZIhvcNAQELBQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yNTA1MDUxODIzMTdaFw0yNjA1MDUxODIzMTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBSr4OYIWkb8UGuQnEFnjSbmvR+4vDAfBgNVHSMEGDAWgBSr4OYIWkb8UGuQnEFnjSbmvR+4vDANBgkqhkiG9w0BAQsFAAOCAQEAUmRWRPGXsvfuRT+53id3EufH1IJAdowrt6yBZsHobvqCXO2+YhG7oG6/UqUYiv5bHN5xEMQyWd7nyrLOUeFo2bpcMIOlpl6AoUIY65Gm2BqQ7FuPxLLO25RdpZ5WkMGX5kJsKY0/PcpamRKNz1khgFcRyxf9WGhCAIjDCWIWs8lkvPN3m75SFCW7MTuzzQOrzvI6nqqcHO4k8hRBznp26WLUW1rQKpNN09nZGOkeNYK5QbzKN/RUmtEHQZhlgLAIr09jUaA4RDLI1SdD6LR5nvpa9RJBTyS/kISF8BXKMgvUbDHN2nP+VUUrut2ZwoU+pxV4RVT2pS760HuYj4+sYQ==" + private { + key "MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAECgf9plqCMg2pKEWRFl183bqWAm7lnLnsUOfABFNPYa3U+uKQUKZpboTBfzDfZqNak3XNQV0mTAR8pFfoMhQjQU9hUxH7ivjw1RUCHjixCF0vLBkTB34gL7FUbiEIFhR1NW3pCJY73OXOkIZG1Obh6Syb8KubDeu4bTmb90/TnDDAs6OYXJ5yo7ZDZLvLu5a3Dli+H4K5Qb5VJ74o/vtodBo3wmKBgy2Ey6JqF+y7/3HeE66rVhYNft5pURgemWnNYqh3oDTJASqpA/8n90o8ceYPVJugQ7029UiyTp0xgBRXFszgiYPkBlsNWB+9+ospopOmYU0owBykH+RtD/bQ0mwECgYEA4gxPdYbHg/GLihHrkv0t5V0pSEhBeBeTBdWG+P/6K90vXofpp7qISdOeYMGkh8mY+PfZNHksdu67d2ks5on5/dNf5YXWCm+LMMRiSsfOo11NISNdNHS6afqs18Wq1aKawv/rwotfxrakM4Gar692+jgz/l/X+FdOQwmE6uEus8ECgYEA3oW4eGtzGq28TiqyhTHduTkas0ckPWX8ulasyPnLxBKDNNohbXXpFIJIcrnl24QFJw3MJbo+R+OxZHgzPK8r64gIGVa7vLCR2fiU/RFoUa1Jo1pPOqXXWqf/Mvdokm2p0atrjRUX9VhjoFsDLcqTgAmfSCsVSqGucX6ER7Gy60UCgYAvmhoNjNFtFquk6rsqHAjTOTgdUaH/0S8T1nBy9SzQmeaEyKhKuvxCV78NbxnfwnNlUoQ6CZ50eTefINXkwn+TlTSnl/SIBA9SuLheOQ9p1ZcNeG4DQuWStcg6NBUSoghnMg+Ky2Di7slLU2qovpGWhcllMve/A1umwFVuRPdZwQKBgHS7mYYyd/Oq6HnpFDWjbzlXp5Yc3/oFooruJT5ZLHfzbjkvpRGTJW7I2dC1jMuXekx+hHXWOg3keI7IL7jJ/DRW7Ei+o0XdKuY57Y7ErwEJ8vNq0N1nWo4IS2wlNgp61PdVAdrFEgh3EexxUj2XY8FrSs/FKio4nxaS1Dn4EnAxAoGBAKqLvuPpmCMVwlIu57WGxL5d9i7EjIGY65l6HTKQYoHCzE51rkowH1La2fuUYz0IpExq2lcrLbOUtSyhXH7Zlktiz//Gu/P90SUfR/ZGcLeZi+EDyK2OctpnWBjs2Dmfg4D6vxk39yV8AB97pYG073GcJ/P54qRUuEitbpJwH+fB" + } + } +} +service { + ntp { + allow-client { + address "0.0.0.0/0" + address "::/0" + } + server 172.16.100.10 { + } + server 172.16.100.20 { + } + server 172.16.110.30 { + } + } + ssh { + disable-host-validation + port "22" + } +} +system { + config-management { + commit-revisions "200" + } + console { + device ttyS0 { + speed 115200 + } + } + conntrack { + modules { + ftp + h323 + nfs + pptp + sip + sqlnet + tftp + } + } + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + name-server "172.16.254.30" + option { + kernel { + disable-mitigations + } + } + syslog { + global { + facility all { + level "info" + } + facility local7 { + level "debug" + } + } + } + time-zone "Europe/Berlin" +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@5:broadcast-relay@1:cluster@2:config-management@1:conntrack@5:conntrack-sync@2:container@2:dhcp-relay@2:dhcp-server@8:dhcpv6-server@1:dns-dynamic@4:dns-forwarding@4:firewall@15:flow-accounting@1:https@6:ids@1:interfaces@32:ipoe-server@3:ipsec@13:isis@3:l2tp@9:lldp@2:mdns@1:monitoring@1:nat@8:nat66@3:ntp@3:openconnect@3:ospf@2:pim@1:policy@8:pppoe-server@10:pptp@5:qos@2:quagga@11:reverse-proxy@1:rip@1:rpki@2:salt@1:snmp@3:ssh@2:sstp@6:system@27:vrf@3:vrrp@4:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2" +// Release version: 1.4.1 diff --git a/smoketest/configs/basic-syslog b/smoketest/configs/basic-syslog new file mode 100644 index 000000000..9336b73bc --- /dev/null +++ b/smoketest/configs/basic-syslog @@ -0,0 +1,70 @@ +interfaces { + ethernet eth0 { + duplex "auto" + speed "auto" + } + ethernet eth1 { + address 172.16.33.154/24 + duplex auto + speed auto + vrf red + } +} +system { + console { + device ttyS0 { + speed 115200 + } + } + domain-name vyos-ci-test.net + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + syslog { + global { + facility all { + level info + } + facility local7 { + level debug + } + marker { + interval 999 + } + preserve-fqdn + } + host syslog01.vyos.net { + facility local7 { + level notice + } + port 8000 + } + host syslog02.vyos.net { + facility all { + level debug + } + format { + include-timezone + octet-counted + } + protocol tcp + port 8001 + } + vrf red + } +} +vrf { + name red { + table 12321 + } +} + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@5:broadcast-relay@1:cluster@2:config-management@1:conntrack@5:conntrack-sync@2:container@2:dhcp-relay@2:dhcp-server@8:dhcpv6-server@1:dns-dynamic@4:dns-forwarding@4:firewall@15:flow-accounting@1:https@6:ids@1:interfaces@32:ipoe-server@3:ipsec@13:isis@3:l2tp@9:lldp@2:mdns@1:monitoring@1:nat@8:nat66@3:ntp@3:openconnect@3:ospf@2:pim@1:policy@8:pppoe-server@10:pptp@5:qos@2:quagga@11:reverse-proxy@1:rip@1:rpki@2:salt@1:snmp@3:ssh@2:sstp@6:system@27:vrf@3:vrrp@4:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2" +// Release version: 1.4.0 diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index 242f3d1de..5f7a71237 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -99,33 +99,77 @@ protocols { } service { dhcp-server { + dynamic-dns-update { + send-updates enable + forward-domain domain.lan { + dns-server 1 { + address 192.168.0.1 + } + dns-server 2 { + address 100.100.0.1 + } + key-name domain-lan-updates + } + reverse-domain 0.168.192.in-addr.arpa { + dns-server 1 { + address 192.168.0.1 + } + dns-server 2 { + address 100.100.0.1 + } + key-name reverse-0-168-192 + } + tsig-key domain-lan-updates { + algorithm sha256 + secret SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ== + } + tsig-key reverse-0-168-192 { + algorithm sha256 + secret VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ== + } + conflict-resolution enable + } shared-network-name LAN { authoritative + dynamic-dns-update { + send-updates enable + ttl-percent 75 + } subnet 192.168.0.0/24 { - default-router 192.168.0.1 - dns-server 192.168.0.1 - domain-name vyos.net - domain-search vyos.net + dynamic-dns-update { + send-updates enable + generated-prefix myhost + qualifying-suffix lan1.domain.lan + } + option { + default-router 192.168.0.1 + domain-name vyos.net + domain-search vyos.net + name-server 192.168.0.1 + } range LANDynamic { start 192.168.0.30 stop 192.168.0.240 } static-mapping TEST1-1 { ip-address 192.168.0.11 - mac-address 00:01:02:03:04:05 + mac 00:01:02:03:04:05 } static-mapping TEST1-2 { + disable ip-address 192.168.0.12 - mac-address 00:01:02:03:04:05 + mac 00:01:02:03:04:05 } static-mapping TEST2-1 { ip-address 192.168.0.21 - mac-address 00:01:02:03:04:21 + mac 00:01:02:03:04:21 } static-mapping TEST2-2 { + disable ip-address 192.168.0.21 - mac-address 00:01:02:03:04:22 + mac 00:01:02:03:04:22 } + subnet-id 1 } } } @@ -236,6 +280,9 @@ system { facility security { level info } + marker { + interval 1000 + } preserve-fqdn } host syslog.vyos.net { @@ -251,6 +298,7 @@ system { format { octet-counted } + protocol tcp port 8000 } } 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/configs/bgp-rpki b/smoketest/configs/bgp-rpki index 5588f15c9..2d136d545 100644 --- a/smoketest/configs/bgp-rpki +++ b/smoketest/configs/bgp-rpki @@ -46,6 +46,13 @@ policy { } protocols { bgp 64500 { + address-family { + ipv4-unicast { + redistribute { + table 100 + } + } + } neighbor 1.2.3.4 { address-family { ipv4-unicast { diff --git a/smoketest/configs/dialup-router-complex b/smoketest/configs/dialup-router-complex index aa9837fe9..018379bcd 100644 --- a/smoketest/configs/dialup-router-complex +++ b/smoketest/configs/dialup-router-complex @@ -1392,6 +1392,9 @@ service { } } lldp { + interface pppoe0 { + disable + } legacy-protocols { cdp } diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 593b4b415..5348b0cc3 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-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,11 +14,14 @@ import re +from json import loads from netifaces import AF_INET from netifaces import AF_INET6 from netifaces import ifaddresses +from systemd import journal from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.defaults import directories @@ -37,12 +40,15 @@ from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local from vyos.utils.network import get_nft_vrf_zone_mapping from vyos.xml_ref import cli_defined +from vyos.xml_ref import default_value dhclient_base_dir = directories['isc_dhclient_dir'] dhclient_process_name = 'dhclient' dhcp6c_base_dir = directories['dhcp6_client_dir'] dhcp6c_process_name = 'dhcp6c' +MSG_TESTCASE_UNSUPPORTED = 'unsupported on interface family' + server_ca_root_cert_data = """ MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa @@ -133,6 +139,7 @@ def is_mirrored_to(interface, mirror_if, qdisc): if mirror_if in tmp: ret_val = True return ret_val + class BasicInterfaceTest: class TestCase(VyOSUnitTestSHIM.TestCase): _test_dhcp = False @@ -181,6 +188,9 @@ class BasicInterfaceTest: section = Section.section(span) cls.cli_set(cls, ['interfaces', section, span]) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + @classmethod def tearDownClass(cls): # Tear down mirror interfaces for SPAN (Switch Port Analyzer) @@ -213,7 +223,7 @@ class BasicInterfaceTest: def test_dhcp_disable_interface(self): if not self._test_dhcp: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) # When interface is configured as admin down, it must be admin down # even when dhcpc starts on the given interface @@ -236,7 +246,7 @@ class BasicInterfaceTest: def test_dhcp_client_options(self): if not self._test_dhcp or not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) client_id = 'VyOS-router' distance = '100' @@ -276,7 +286,10 @@ class BasicInterfaceTest: def test_dhcp_vrf(self): if not self._test_dhcp or not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) + + cli_default_metric = default_value(self._base_path + [self._interfaces[0], + 'dhcp-options', 'default-route-distance']) vrf_name = 'purple4' self.cli_set(['vrf', 'name', vrf_name, 'table', '65000']) @@ -303,13 +316,34 @@ class BasicInterfaceTest: self.assertIn(str(dhclient_pid), vrf_pids) # and the commandline has the appropriate options cmdline = read_file(f'/proc/{dhclient_pid}/cmdline') - self.assertIn('-e\x00IF_METRIC=210', cmdline) # 210 is the default value + self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline) + + # T5103: remove interface from VRF instance and move DHCP client + # back to default VRF. This must restart the DHCP client process + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'vrf']) + + self.cli_commit() + + # Validate interface state + for interface in self._interfaces: + tmp = get_interface_vrf(interface) + self.assertEqual(tmp, 'default') + # Check if dhclient process runs + dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface, timeout=10) + self.assertTrue(dhclient_pid) + # .. inside the appropriate VRF instance + vrf_pids = cmd(f'ip vrf pids {vrf_name}') + self.assertNotIn(str(dhclient_pid), vrf_pids) + # and the commandline has the appropriate options + cmdline = read_file(f'/proc/{dhclient_pid}/cmdline') + self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline) self.cli_delete(['vrf', 'name', vrf_name]) def test_dhcpv6_vrf(self): if not self._test_ipv6_dhcpc6 or not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) vrf_name = 'purple6' self.cli_set(['vrf', 'name', vrf_name, 'table', '65001']) @@ -337,11 +371,31 @@ class BasicInterfaceTest: vrf_pids = cmd(f'ip vrf pids {vrf_name}') self.assertIn(str(tmp), vrf_pids) + # T7135: remove interface from VRF instance and move DHCP client + # back to default VRF. This must restart the DHCP client process + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'vrf']) + + self.cli_commit() + + # Validate interface state + for interface in self._interfaces: + tmp = get_interface_vrf(interface) + self.assertEqual(tmp, 'default') + + # Check if dhclient process runs + tmp = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10) + self.assertTrue(tmp) + # .. inside the appropriate VRF instance + vrf_pids = cmd(f'ip vrf pids {vrf_name}') + self.assertNotIn(str(tmp), vrf_pids) + + self.cli_delete(['vrf', 'name', vrf_name]) def test_move_interface_between_vrf_instances(self): if not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) vrf1_name = 'smoketest_mgmt1' vrf1_table = '5424' @@ -386,7 +440,7 @@ class BasicInterfaceTest: def test_add_to_invalid_vrf(self): if not self._test_vrf: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) # move interface into first VRF for interface in self._interfaces: @@ -404,7 +458,7 @@ class BasicInterfaceTest: def test_span_mirror(self): if not self._mirror_interfaces: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) # Check the two-way mirror rules of ingress and egress for mirror in self._mirror_interfaces: @@ -513,7 +567,7 @@ class BasicInterfaceTest: def test_ipv6_link_local_address(self): # Common function for IPv6 link-local address assignemnts if not self._test_ipv6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -544,7 +598,7 @@ class BasicInterfaceTest: def test_interface_mtu(self): if not self._test_mtu: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for intf in self._interfaces: base = self._base_path + [intf] @@ -563,8 +617,8 @@ class BasicInterfaceTest: def test_mtu_1200_no_ipv6_interface(self): # Testcase if MTU can be changed to 1200 on non IPv6 # enabled interfaces - if not self._test_mtu: - self.skipTest('not supported') + if not self._test_mtu or not self._test_ipv6: + self.skipTest(MSG_TESTCASE_UNSUPPORTED) old_mtu = self._mtu self._mtu = '1200' @@ -600,7 +654,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -645,7 +699,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan or not self._test_mtu: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) mtu_1500 = '1500' mtu_9000 = '9000' @@ -691,7 +745,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -761,7 +815,7 @@ class BasicInterfaceTest: def test_vif_8021q_lower_up_down(self): # Testcase for https://vyos.dev/T3349 if not self._test_vlan: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -801,7 +855,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_qinq: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -868,7 +922,7 @@ class BasicInterfaceTest: # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_qinq: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) for interface in self._interfaces: base = self._base_path + [interface] @@ -906,7 +960,7 @@ class BasicInterfaceTest: def test_interface_ip_options(self): if not self._test_ip: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) arp_tmo = '300' mss = '1420' @@ -1008,12 +1062,13 @@ class BasicInterfaceTest: def test_interface_ipv6_options(self): if not self._test_ipv6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) mss = '1400' dad_transmits = '10' accept_dad = '0' source_validation = 'strict' + interface_identifier = '::fffe' for interface in self._interfaces: path = self._base_path + [interface] @@ -1036,6 +1091,9 @@ class BasicInterfaceTest: if cli_defined(self._base_path + ['ipv6'], 'source-validation'): self.cli_set(path + ['ipv6', 'source-validation', source_validation]) + if cli_defined(self._base_path + ['ipv6', 'address'], 'interface-identifier'): + self.cli_set(path + ['ipv6', 'address', 'interface-identifier', interface_identifier]) + self.cli_commit() for interface in self._interfaces: @@ -1067,13 +1125,20 @@ class BasicInterfaceTest: self.assertIn('fib saddr . iif oif 0', line) self.assertIn('drop', line) + if cli_defined(self._base_path + ['ipv6', 'address'], 'interface-identifier'): + tmp = cmd(f'ip -j token show dev {interface}') + tmp = loads(tmp)[0] + self.assertEqual(tmp['token'], interface_identifier) + self.assertEqual(tmp['ifname'], interface) + + def test_dhcpv6_client_options(self): if not self._test_ipv6_dhcpc6: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) duid_base = 10 for interface in self._interfaces: - duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base) + duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}' path = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(path + option.split()) @@ -1090,7 +1155,7 @@ class BasicInterfaceTest: duid_base = 10 for interface in self._interfaces: - duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base) + duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}' dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf') self.assertIn(f'interface {interface} ' + '{', dhcpc6_config) self.assertIn(f' request domain-name-servers;', dhcpc6_config) @@ -1102,16 +1167,25 @@ class BasicInterfaceTest: self.assertIn('};', dhcpc6_config) duid_base += 1 + # T7058: verify daemon has no problems understanding the custom DUID option + j = journal.Reader() + j.this_boot() + j.add_match(_SYSTEMD_UNIT=f'dhcp6c@{interface}.service') + for entry in j: + self.assertNotIn('yyerror0', entry.get('MESSAGE', '')) + self.assertNotIn('syntax error', entry.get('MESSAGE', '')) + # Better ask the process about it's commandline in the future pid = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10) self.assertTrue(pid) + # DHCPv6 option "no-release" requires "-n" daemon startup option dhcp6c_options = read_file(f'/proc/{pid}/cmdline') self.assertIn('-n', dhcp6c_options) def test_dhcpv6pd_auto_sla_id(self): if not self._test_ipv6_pd: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) prefix_len = '56' sla_len = str(64 - int(prefix_len)) @@ -1172,7 +1246,7 @@ class BasicInterfaceTest: def test_dhcpv6pd_manual_sla_id(self): if not self._test_ipv6_pd: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) prefix_len = '56' sla_len = str(64 - int(prefix_len)) @@ -1238,7 +1312,7 @@ class BasicInterfaceTest: def test_eapol(self): if not self._test_eapol: - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) cfg_dir = '/run/wpa_supplicant' diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index 9cf6a653a..f0674f187 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -18,7 +18,6 @@ import paramiko import pprint from time import sleep -from time import time from typing import Type from vyos.configsession import ConfigSession @@ -27,10 +26,17 @@ from vyos import ConfigError from vyos.defaults import commit_lock from vyos.utils.process import cmd from vyos.utils.process import run -from vyos.utils.process import process_named_running save_config = '/tmp/vyos-smoketest-save' +# The commit process is not finished until all pending files from +# VYATTA_CHANGES_ONLY_DIR are copied to VYATTA_ACTIVE_CONFIGURATION_DIR. This +# is done inside libvyatta-cfg1 and the FUSE UnionFS part. On large non- +# interactive commits FUSE UnionFS might not replicate the real state in time, +# leading to errors when querying the working and effective configuration. +# TO BE DELETED AFTER SWITCH TO IN MEMORY CONFIG +CSTORE_GUARD_TIME = 4 + # This class acts as shim between individual Smoketests developed for VyOS and # the Python UnitTest framework. Before every test is loaded, we dump the current # system configuration and reload it after the test - despite the test results. @@ -45,7 +51,9 @@ class VyOSUnitTestSHIM: # trigger the certain failure condition. # Use "self.debug = True" in derived classes setUp() method debug = False - commit_guard = time() + # Time to wait after a commit to ensure the CStore is up to date + # only required for testcases using FRR + _commit_guard_time = 0 @classmethod def setUpClass(cls): cls._session = ConfigSession(os.getpid()) @@ -67,10 +75,11 @@ class VyOSUnitTestSHIM: cls._session.discard() cls.fail(cls) - def cli_set(self, config): + def cli_set(self, path, value=None): if self.debug: - print('set ' + ' '.join(config)) - self._session.set(config) + str = f'set {" ".join(path)} {value}' if value else f'set {" ".join(path)}' + print(str) + self._session.set(path, value) def cli_delete(self, config): if self.debug: @@ -85,15 +94,17 @@ class VyOSUnitTestSHIM: def cli_commit(self): if self.debug: print('commit') - self._session.commit() - # during a commit there is a process opening commit_lock, and run() returns 0 + # During a commit there is a process opening commit_lock, and run() + # returns 0 while run(f'sudo lsof -nP {commit_lock}') == 0: sleep(0.250) - # wait for FRR reload to be complete - while process_named_running('frr-reload.py'): - sleep(0.250) - # reset getFRRconfig() guard timer - self.commit_guard = time() + # Return the output of commit + # Necessary for testing Warning cases + out = self._session.commit() + # Wait for CStore completion for fast non-interactive commits + sleep(self._commit_guard_time) + + return out def op_mode(self, path : list) -> None: """ @@ -108,20 +119,27 @@ class VyOSUnitTestSHIM: pprint.pprint(out) return out - def getFRRconfig(self, string=None, end='$', endsection='^!', daemon='', guard_time=10, empty_retry=0): - """ Retrieve current "running configuration" from FRR """ - # Sometimes FRR needs some time after reloading the configuration to - # appear in vtysh. This is a workaround addiung a 10 second guard timer - # between the last cli_commit() and the first read of FRR config via vtysh - while (time() - self.commit_guard) < guard_time: - sleep(0.250) # wait 250 milliseconds - command = f'vtysh -c "show run {daemon} no-header"' - if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"' + def getFRRconfig(self, string=None, end='$', endsection='^!', + substring=None, endsubsection=None, empty_retry=0): + """ + Retrieve current "running configuration" from FRR + + string: search for a specific start string in the configuration + end: end of the section to search for (line ending) + endsection: end of the configuration + substring: search section under the result found by string + endsubsection: end of the subsection (usually something with "exit") + """ + command = f'vtysh -c "show run no-header"' + if string: + command += f' | sed -n "/^{string}{end}/,/{endsection}/p"' + if substring and endsubsection: + command += f' | sed -n "/^{substring}/,/{endsubsection}/p"' out = cmd(command) if self.debug: print(f'\n\ncommand "{command}" returned:\n') pprint.pprint(out) - if empty_retry: + if empty_retry > 0: retry_count = 0 while not out and retry_count < empty_retry: if self.debug and retry_count % 10 == 0: @@ -169,6 +187,15 @@ class VyOSUnitTestSHIM: break self.assertTrue(not matched if inverse else matched, msg=search) + def verify_nftables_chain_exists(self, table, chain, inverse=False): + try: + cmd(f'sudo nft list chain {table} {chain}') + if inverse: + self.fail(f'Chain exists: {table} {chain}') + except OSError: + if not inverse: + self.fail(f'Chain does not exist: {table} {chain}') + # Verify ip rule output def verify_rules(self, rules_search, inverse=False, addr_family='inet'): rule_output = cmd(f'ip -family {addr_family} rule show') diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index 51559a7c6..daad3a909 100755 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.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,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest import glob import json @@ -26,27 +27,28 @@ from vyos.utils.process import cmd from vyos.utils.process import process_named_running base_path = ['container'] -cont_image = 'busybox:stable' # busybox is included in vyos-build PROCESS_NAME = 'conmon' PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid' +busybox_image = 'busybox:stable' busybox_image_path = '/usr/share/vyos/busybox-stable.tar' + def cmd_to_json(command): c = cmd(command + ' --format=json') data = json.loads(c)[0] return data + class TestContainer(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestContainer, cls).setUpClass() - # Load image for smoketest provided in vyos-build - try: - cmd(f'cat {busybox_image_path} | sudo podman load') - except: - cls.skipTest(cls, reason='busybox image not available') + # Load image for smoketest provided in vyos-1x-smoketest + if not os.path.exists(busybox_image_path): + cls.fail(cls, f'{busybox_image} image not available') + cmd(f'sudo podman load -i {busybox_image_path}') # ensure we can also run this test on a live system - so lets clean # out the current configuration :) @@ -55,9 +57,8 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): super(TestContainer, cls).tearDownClass() - # Cleanup podman image - cmd(f'sudo podman image rm -f {cont_image}') + cmd(f'sudo podman image rm -f {busybox_image}') def tearDown(self): self.cli_delete(base_path) @@ -74,13 +75,26 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): cont_name = 'c1' self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '10.0.2.15/24']) - self.cli_set(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2']) + self.cli_set( + ['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2'] + ) self.cli_set(['system', 'name-server', '1.1.1.1']) self.cli_set(['system', 'name-server', '8.8.8.8']) - self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) - self.cli_set(base_path + ['name', cont_name, 'sysctl', 'parameter', 'kernel.msgmax', 'value', '4096']) + self.cli_set( + base_path + + [ + 'name', + cont_name, + 'sysctl', + 'parameter', + 'kernel.msgmax', + 'value', + '4096', + ] + ) # commit changes self.cli_commit() @@ -96,6 +110,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax') self.assertEqual(tmp, 'kernel.msgmax = 4096') + def test_log_driver(self): + self.cli_set(base_path + ['log-driver', 'journald']) + + self.cli_commit() + + tmp = cmd('podman info --format "{{ .Host.LogDriver }}"') + self.assertEqual(tmp, 'journald') + def test_name_server(self): cont_name = 'dns-test' net_name = 'net-test' @@ -104,9 +126,19 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) - self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'name-server', name_server]) - self.cli_set(base_path + ['name', cont_name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) + self.cli_set( + base_path + + [ + 'name', + cont_name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + 2), + ] + ) # verify() - name server has no effect when container network has dns enabled with self.assertRaises(ConfigSessionError): @@ -125,7 +157,7 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): cont_name = 'c2' self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) - self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'cpu-quota', '1.25']) self.cli_commit() @@ -146,8 +178,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + ii), + ] + ) # verify() - first IP address of a prefix can not be used by a container with self.assertRaises(ConfigSessionError): @@ -164,8 +206,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(f'sudo podman container inspect {name}') - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'], str(ip_interface(prefix).ip + ii)) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['Gateway'], + str(ip_interface(prefix).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPAddress'], + str(ip_interface(prefix).ip + ii), + ) def test_ipv6_network(self): prefix = '2001:db8::/64' @@ -176,8 +224,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + ii), + ] + ) # verify() - first IP address of a prefix can not be used by a container with self.assertRaises(ConfigSessionError): @@ -194,8 +252,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(f'sudo podman container inspect {name}') - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix).ip + ii)) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'], + str(ip_interface(prefix).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], + str(ip_interface(prefix).ip + ii), + ) def test_dual_stack_network(self): prefix4 = '192.0.2.0/24' @@ -208,9 +272,29 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(1, 6): name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix4).ip + ii)]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix6).ip + ii)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix4).ip + ii), + ] + ) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix6).ip + ii), + ] + ) # verify() - first IP address of a prefix can not be used by a container with self.assertRaises(ConfigSessionError): @@ -228,10 +312,22 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in range(2, 6): name = f'{base_name}-{ii}' c = cmd_to_json(f'sudo podman container inspect {name}') - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix6).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix6).ip + ii)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix4).ip + 1)) - self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'] , str(ip_interface(prefix4).ip + ii)) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'], + str(ip_interface(prefix6).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], + str(ip_interface(prefix6).ip + ii), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['Gateway'], + str(ip_interface(prefix4).ip + 1), + ) + self.assertEqual( + c['NetworkSettings']['Networks'][net_name]['IPAddress'], + str(ip_interface(prefix4).ip + ii), + ) def test_no_name_server(self): prefix = '192.0.2.0/24' @@ -242,8 +338,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['network', net_name, 'no-name-server']) name = f'{base_name}-2' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + 2), + ] + ) self.cli_commit() n = cmd_to_json(f'sudo podman network inspect {net_name}') @@ -258,8 +364,18 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['network', net_name, 'mtu', '1280']) name = f'{base_name}-2' - self.cli_set(base_path + ['name', name, 'image', cont_image]) - self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) + self.cli_set( + base_path + + [ + 'name', + name, + 'network', + net_name, + 'address', + str(ip_interface(prefix).ip + 2), + ] + ) self.cli_commit() n = cmd_to_json(f'sudo podman network inspect {net_name}') @@ -271,7 +387,7 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): uid = '1001' self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) - self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) + self.cli_set(base_path + ['name', cont_name, 'image', busybox_image]) self.cli_set(base_path + ['name', cont_name, 'gid', gid]) # verify() - GID can only be set if UID is set @@ -293,17 +409,20 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): for ii in container_list: name = f'{base_name}-{ii}' - self.cli_set(base_path + ['name', name, 'image', cont_image]) + self.cli_set(base_path + ['name', name, 'image', busybox_image]) self.cli_set(base_path + ['name', name, 'allow-host-networks']) self.cli_commit() # Query API about running containers - tmp = cmd("sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json") + tmp = cmd( + "sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json" + ) tmp = json.loads(tmp) # We expect the same amount of containers from the API that we started above self.assertEqual(len(container_list), len(tmp)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 6420afa38..851a15f16 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -119,6 +119,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0']) + self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'pod-smoketest']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept']) @@ -133,6 +134,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface']) + # Create container network so test won't fail + self.cli_set(['container', 'network', 'smoketest', 'prefix', '10.0.0.0/24']) + self.cli_commit() self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5') @@ -638,6 +642,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv4_global_state(self): + self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0']) + self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'software']) + + self.cli_set(['firewall', 'global-options', 'state-policy', 'offload', 'offload-target', 'smoketest']) self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop']) @@ -647,6 +655,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['jump VYOS_STATE_POLICY'], ['chain VYOS_STATE_POLICY'], + ['jump VYOS_STATE_POLICY_FORWARD'], + ['chain VYOS_STATE_POLICY_FORWARD'], + ['flow add @VYOS_FLOWTABLE_smoketest'], ['ct state established', 'accept'], ['ct state invalid', 'drop'], ['ct state related', 'accept'] @@ -654,6 +665,13 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') + # T7148 - Ensure bridge rule reject -> drop + self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'reject']) + self.cli_commit() + + self.verify_nftables([['ct state invalid', 'reject']], 'ip vyos_filter') + self.verify_nftables([['ct state invalid', 'drop']], 'bridge vyos_filter') + # Check conntrack is enabled from state-policy self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK') self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK') @@ -906,7 +924,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): def test_zone_basic(self): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', 'smoketestv6', 'default-action', 'drop']) - self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'interface', 'eth0']) + self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'member', 'interface', 'eth0']) self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'intra-zone-filtering', 'firewall', 'ipv6-name', 'smoketestv6']) self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone']) @@ -964,6 +982,98 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter') + def test_zone_with_vrf(self): + self.cli_set(['firewall', 'ipv4', 'name', 'ZONE1-to-LOCAL', 'default-action', 'accept']) + self.cli_set(['firewall', 'ipv4', 'name', 'ZONE2_to_ZONE1', 'default-action', 'continue']) + self.cli_set(['firewall', 'ipv6', 'name', 'LOCAL_to_ZONE2_v6', 'default-action', 'drop']) + self.cli_set(['firewall', 'zone', 'LOCAL', 'from', 'ZONE1', 'firewall', 'name', 'ZONE1-to-LOCAL']) + self.cli_set(['firewall', 'zone', 'LOCAL', 'local-zone']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'from', 'ZONE2', 'firewall', 'name', 'ZONE2_to_ZONE1']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth1']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'interface', 'eth2']) + self.cli_set(['firewall', 'zone', 'ZONE1', 'member', 'vrf', 'VRF-1']) + self.cli_set(['firewall', 'zone', 'ZONE2', 'from', 'LOCAL', 'firewall', 'ipv6-name', 'LOCAL_to_ZONE2_v6']) + self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'interface', 'vtun66']) + self.cli_set(['firewall', 'zone', 'ZONE2', 'member', 'vrf', 'VRF-2']) + + self.cli_set(['vrf', 'name', 'VRF-1', 'table', '101']) + self.cli_set(['vrf', 'name', 'VRF-2', 'table', '102']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'vrf', 'VRF-1']) + self.cli_set(['interfaces', 'vti', 'vti1', 'vrf', 'VRF-2']) + + self.cli_commit() + + nftables_search = [ + ['chain NAME_ZONE1-to-LOCAL'], + ['counter', 'accept', 'comment "NAM-ZONE1-to-LOCAL default-action accept"'], + ['chain NAME_ZONE2_to_ZONE1'], + ['counter', 'continue', 'comment "NAM-ZONE2_to_ZONE1 default-action continue"'], + ['chain VYOS_ZONE_FORWARD'], + ['type filter hook forward priority filter + 1'], + ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "eth0"', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'], + ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'], + ['chain VYOS_ZONE_LOCAL'], + ['type filter hook input priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_IN'], + ['chain VYOS_ZONE_OUTPUT'], + ['type filter hook output priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_OUT'], + ['chain VZONE_LOCAL_IN'], + ['iifname { "eth1", "eth2" }', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'], + ['iifname "VRF-1"', 'counter packets', 'jump NAME_ZONE1-to-LOCAL'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_LOCAL_OUT'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_ZONE1'], + ['iifname { "eth1", "eth2" }', 'counter packets', 'return'], + ['iifname "VRF-1"', 'counter packets', 'return'], + ['iifname "vtun66"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'], + ['iifname "vtun66"', 'counter packets', 'return'], + ['iifname "VRF-2"', 'counter packets', 'jump NAME_ZONE2_to_ZONE1'], + ['iifname "VRF-2"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'], + ['chain VZONE_ZONE2'], + ['iifname "vtun66"', 'counter packets', 'return'], + ['iifname "VRF-2"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"'] + ] + + nftables_search_v6 = [ + ['chain NAME6_LOCAL_to_ZONE2_v6'], + ['counter', 'drop', 'comment "NAM-LOCAL_to_ZONE2_v6 default-action drop"'], + ['chain VYOS_ZONE_FORWARD'], + ['type filter hook forward priority filter + 1'], + ['oifname { "eth1", "eth2" }', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "eth0"', 'counter packets', 'jump VZONE_ZONE1'], + ['oifname "vtun66"', 'counter packets', 'jump VZONE_ZONE2'], + ['oifname "vti1"', 'counter packets', 'jump VZONE_ZONE2'], + ['chain VYOS_ZONE_LOCAL'], + ['type filter hook input priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_IN'], + ['chain VYOS_ZONE_OUTPUT'], + ['type filter hook output priority filter + 1'], + ['counter packets', 'jump VZONE_LOCAL_OUT'], + ['chain VZONE_LOCAL_IN'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_LOCAL_OUT'], + ['oifname "vtun66"', 'counter packets', 'jump NAME6_LOCAL_to_ZONE2_v6'], + ['oifname "vti1"', 'counter packets', 'jump NAME6_LOCAL_to_ZONE2_v6'], + ['counter packets', 'drop', 'comment "zone_LOCAL default-action drop"'], + ['chain VZONE_ZONE1'], + ['iifname { "eth1", "eth2" }', 'counter packets', 'return'], + ['iifname "VRF-1"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE1 default-action drop"'], + ['chain VZONE_ZONE2'], + ['iifname "vtun66"', 'counter packets', 'return'], + ['iifname "VRF-2"', 'counter packets', 'return'], + ['counter packets', 'drop', 'comment "zone_ZONE2 default-action drop"'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter') + def test_flow_offload(self): self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '10']) self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0.10']) @@ -1075,7 +1185,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) - + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) @@ -1110,8 +1220,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1']) - # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not. - # It should be caught by conf validation. + # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not. + # It should be caught by conf validation. with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -1170,5 +1280,113 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() + def test_ipv4_remote_group(self): + # Setup base config for test + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt']) + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + + self.cli_commit() + + # Test remote-group had been loaded correctly in nft + nftables_search = [ + ['R_group01'], + ['type ipv4_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R_group01', 'ipv4-INP-filter-10'] + ] + self.verify_nftables(nftables_search, 'ip vyos_filter') + + # Test remote-group cannot be configured without a URL + self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + # Test remote-group cannot be set alongside address in rules + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'address', '127.0.0.1']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + + def test_ipv6_remote_group(self): + # Setup base config for test + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt']) + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + + self.cli_commit() + + # Test remote-group had been loaded correctly in nft + nftables_search = [ + ['R6_group01'], + ['type ipv6_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R6_group01', 'ipv6-INP-filter-10'] + ] + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + + # Test remote-group cannot be configured without a URL + self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + # Test remote-group cannot be set alongside address in rules + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'address', '2001:db8::1']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_discard() + + + def test_remote_group(self): + # Setup base config for test adding remote group to both ipv4 and ipv6 rules + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt']) + self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'source', 'group', 'remote-group', 'group01']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'source', 'group', 'remote-group', 'group01']) + + self.cli_commit() + + # Test remote-group had been loaded correctly in nft ip table + nftables_v4_search = [ + ['R_group01'], + ['type ipv4_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R_group01', 'ipv4-OUT-filter-10'], + ['meta l4proto', 'saddr @R_group01', 'ipv4-INP-filter-10'], + ] + self.verify_nftables(nftables_v4_search, 'ip vyos_filter') + + # Test remote-group had been loaded correctly in nft ip6 table + nftables_v6_search = [ + ['R6_group01'], + ['type ipv6_addr'], + ['flags interval'], + ['meta l4proto', 'daddr @R6_group01', 'ipv6-OUT-filter-10'], + ['meta l4proto', 'saddr @R6_group01', 'ipv6-INP-filter-10'], + ] + self.verify_nftables(nftables_v6_search, 'ip6 vyos_filter') + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 735e4f3c5..f99fd0363 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -24,7 +24,6 @@ from vyos.ifconfig.interface import Interface from vyos.configsession import ConfigSessionError from vyos.utils.network import get_interface_config from vyos.utils.file import read_file -from vyos.frrender import mgmt_daemon class BondingInterfaceTest(BasicInterfaceTest.TestCase): @classmethod @@ -168,18 +167,25 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): def test_bonding_multi_use_member(self): # Define available bonding hash policies - for interface in ['bond10', 'bond20']: + bonds = ['bond10', 'bond20', 'bond30'] + for interface in bonds: for member in self._members: self.cli_set(self._base_path + [interface, 'member', 'interface', member]) # check validate() - can not use the same member interfaces multiple times with self.assertRaises(ConfigSessionError): self.cli_commit() - - self.cli_delete(self._base_path + ['bond20']) + # only keep the first bond interface configuration + for interface in bonds[1:]: + self.cli_delete(self._base_path + [interface]) self.cli_commit() + bond = bonds[0] + member_ifaces = read_file(f'/sys/class/net/{bond}/bonding/slaves').split() + for member in self._members: + self.assertIn(member, member_ifaces) + def test_bonding_source_interface(self): # Re-use member interface that is already a source-interface bond = 'bond99' @@ -287,7 +293,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): id = '5' for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' evpn mh es-id {id}', frrconfig) self.assertIn(f' evpn mh es-df-pref {id}', frrconfig) @@ -304,7 +310,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): id = '5' for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' evpn mh es-sys-mac 00:12:34:56:78:0{id}', frrconfig) self.assertIn(f' evpn mh uplink', frrconfig) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 54c981adc..4041b3ef3 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -158,6 +158,21 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # verify member is assigned to the bridge self.assertEqual(interface, tmp['master']) + def test_bridge_multi_use_member(self): + # Define available bonding hash policies + bridges = ['br10', 'br20', 'br30'] + for interface in bridges: + for member in self._members: + self.cli_set(self._base_path + [interface, 'member', 'interface', member]) + + # check validate() - can not use the same member interfaces multiple times + with self.assertRaises(ConfigSessionError): + self.cli_commit() + # only keep the first bond interface configuration + for interface in bridges[1:]: + self.cli_delete(self._base_path + [interface]) + + self.cli_commit() def test_add_remove_bridge_member(self): # Add member interfaces to bridge and set STP cost/priority diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 183c10250..2b421e942 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -27,14 +27,12 @@ from netifaces import ifaddresses from base_interfaces_test import BasicInterfaceTest from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section -from vyos.frrender import mgmt_daemon from vyos.utils.file import read_file from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local from vyos.utils.process import cmd from vyos.utils.process import popen - class EthernetInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -222,14 +220,14 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): out = loads(out) self.assertFalse(out[0]['autonegotiate']) - def test_ethtool_evpn_uplink_tarcking(self): + def test_ethtool_evpn_uplink_tracking(self): for interface in self._interfaces: self.cli_set(self._base_path + [interface, 'evpn', 'uplink']) self.cli_commit() for interface in self._interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(' evpn mh uplink', frrconfig) def test_switchdev(self): @@ -241,6 +239,5 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(self._base_path + [interface, 'switchdev']) - if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py index 0454dc658..f4b6038c5 100755 --- a/smoketest/scripts/cli/test_interfaces_loopback.py +++ b/smoketest/scripts/cli/test_interfaces_loopback.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,6 +17,7 @@ import unittest from base_interfaces_test import BasicInterfaceTest +from base_interfaces_test import MSG_TESTCASE_UNSUPPORTED from netifaces import interfaces from vyos.utils.network import is_intf_addr_assigned @@ -53,7 +54,7 @@ class LoopbackInterfaceTest(BasicInterfaceTest.TestCase): self.assertTrue(is_intf_addr_assigned('lo', addr)) def test_interface_disable(self): - self.skipTest('not supported') + self.skipTest(MSG_TESTCASE_UNSUPPORTED) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py index c6a4613a7..b2af86139 100755 --- a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright (C) 2023-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 @@ -34,9 +34,6 @@ class VEthInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(VEthInterfaceTest, cls).setUpClass() - def test_vif_8021q_mtu_limits(self): - self.skipTest('not supported') - # As we always need a pair of veth interfaces, we can not rely on the base # class check to determine if there is a dhcp6c or dhclient instance running. # This test will always fail as there is an instance running on the peer diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index b2076b43b..132496124 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -114,6 +114,30 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(Interface(interface).get_admin_state(), 'up') ttl += 10 + + def test_vxlan_group_remote_error(self): + intf = 'vxlan60' + options = [ + 'group 239.4.4.5', + 'mtu 1420', + 'remote 192.168.0.254', + 'source-address 192.168.0.1', + 'source-interface eth0', + 'vni 60' + ] + for option in options: + opts = option.split() + self.cli_set(self._base_path + [intf] + opts) + + # verify() - Both group and remote cannot be specified + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # Remove blocking CLI option + self.cli_delete(self._base_path + [intf, 'group']) + self.cli_commit() + + def test_vxlan_external(self): interface = 'vxlan0' source_address = '192.0.2.1' 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_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index b8b18f30f..1c69c1be5 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# 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 @@ -64,13 +64,23 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(WirelessInterfaceTest, cls).setUpClass() - # T5245 - currently testcases are disabled - cls._test_ipv6 = False - cls._test_vlan = False + # If any wireless interface is based on mac80211_hwsim, disable all + # VLAN related testcases. See T5245, T7325 + tmp = read_file('/proc/modules') + if 'mac80211_hwsim' in tmp: + cls._test_ipv6 = False + cls._test_vlan = False + cls._test_qinq = False + + # Loading mac80211_hwsim module created two WIFI Interfaces in the + # background (wlan0 and wlan1), remove them to have a clean test start. + # This must happen AFTER the above check for unsupported drivers + for interface in cls._interfaces: + if interface_exists(interface): + call(f'sudo iw dev {interface} del') cls.cli_set(cls, wifi_cc_path + [country]) - def test_wireless_add_single_ip_address(self): # derived method to check if member interfaces are enslaved properly super().test_add_single_ip_address() @@ -627,9 +637,4 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): if __name__ == '__main__': check_kmod('mac80211_hwsim') - # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1) - # remove them to have a clean test start - for interface in ['wlan0', 'wlan1']: - if interface_exists(interface): - call(f'sudo iw dev {interface} del') unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_load-balancing_haproxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py index 967eb3869..833e0a92b 100755 --- a/smoketest/scripts/cli/test_load-balancing_haproxy.py +++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py @@ -14,11 +14,14 @@ # 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 re +import textwrap import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.template import get_default_port from vyos.utils.process import process_named_running from vyos.utils.file import read_file @@ -131,7 +134,25 @@ ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx 7i7xFz2WEiQeSCPaKYOiqM3t """ +haproxy_service_name = 'https_front' +haproxy_backend_name = 'bk-01' +def parse_haproxy_config() -> dict: + config_str = read_file(HAPROXY_CONF) + section_pattern = re.compile(r'^(global|defaults|frontend\s+\S+|backend\s+\S+)', re.MULTILINE) + sections = {} + + matches = list(section_pattern.finditer(config_str)) + + for i, match in enumerate(matches): + section_name = match.group(1).strip() + start = match.end() + end = matches[i + 1].start() if i + 1 < len(matches) else len(config_str) + section_body = config_str[start:end] + dedented_body = textwrap.dedent(section_body).strip() + sections[section_name] = dedented_body + + return sections class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process @@ -146,14 +167,14 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(PROCESS_NAME)) def base_config(self): - self.cli_set(base_path + ['service', 'https_front', 'mode', 'http']) - self.cli_set(base_path + ['service', 'https_front', 'port', '4433']) - self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01']) + self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'http']) + self.cli_set(base_path + ['service', haproxy_service_name, 'port', '4433']) + self.cli_set(base_path + ['service', haproxy_service_name, 'backend', haproxy_backend_name]) - self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090']) - self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'mode', 'http']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'address', '192.0.2.11']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'port', '9090']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'send-proxy']) self.cli_set(base_path + ['global-parameters', 'max-connections', '1000']) @@ -167,15 +188,15 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')]) self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')]) - def test_01_lb_reverse_proxy_domain(self): + def test_reverse_proxy_domain(self): domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com'] domain_bk_second = 'n5.example.com' - frontend = 'https_front' + frontend = 'vyos_smoketest' front_port = '4433' bk_server_first = '192.0.2.11' bk_server_second = '192.0.2.12' - bk_first_name = 'bk-01' - bk_second_name = 'bk-02' + bk_first_name = 'vyosbk-01' + bk_second_name = 'vyosbk-02' bk_server_port = '9090' mode = 'http' rule_ten = '10' @@ -241,9 +262,9 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config) self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config) - def test_02_lb_reverse_proxy_cert_not_exists(self): + def test_reverse_proxy_cert_not_exists(self): self.base_config() - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) + self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() @@ -253,19 +274,19 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.configure_pki() self.base_config() - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) + self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() # self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception)) - self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert']) - self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest']) + self.cli_delete(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert']) + self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'smoketest']) self.cli_commit() - def test_03_lb_reverse_proxy_ca_not_exists(self): + def test_reverse_proxy_ca_not_exists(self): self.base_config() - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() @@ -275,40 +296,40 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.configure_pki() self.base_config() - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() # self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception)) - self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test']) - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) + self.cli_delete(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest']) self.cli_commit() - def test_04_lb_reverse_proxy_backend_ssl_no_verify(self): + def test_reverse_proxy_backend_ssl_no_verify(self): # Setup base self.configure_pki() self.base_config() # Set no-verify option - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'no-verify']) self.cli_commit() # Test no-verify option config = read_file(HAPROXY_CONF) - self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config) + self.assertIn(f'server {haproxy_backend_name} 192.0.2.11:9090 send-proxy ssl verify none', config) # Test setting ca-certificate alongside no-verify option fails, to test config validation - self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() - def test_05_lb_reverse_proxy_backend_http_check(self): + def test_reverse_proxy_backend_http_check(self): # Setup base self.base_config() # Set http-check - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'method', 'get']) self.cli_commit() # Test http-check @@ -317,8 +338,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('http-check send meth GET', config) # Set http-check with uri and status - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'uri', '/health']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200']) self.cli_commit() # Test http-check with uri and status @@ -328,8 +349,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('http-check expect status 200', config) # Set http-check with string - self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success']) + self.cli_delete(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'string', 'success']) self.cli_commit() # Test http-check with string @@ -339,11 +360,11 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('http-check expect string success', config) # Test configuring both http-check & health-check fails validation script - self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'health-check', 'ldap']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() - def test_06_lb_reverse_proxy_tcp_mode(self): + def test_reverse_proxy_tcp_mode(self): frontend = 'tcp_8443' mode = 'tcp' front_port = '8433' @@ -390,27 +411,27 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'mode {mode}', config) self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config) - def test_07_lb_reverse_proxy_http_response_headers(self): + def test_reverse_proxy_http_response_headers(self): # Setup base self.configure_pki() self.base_config() # Set example headers in both frontend and backend - self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800']) - self.cli_set(base_path + ['backend', 'bk-01', 'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01']) + self.cli_set(base_path + ['service', haproxy_service_name, 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800']) + self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-response-headers', 'Proxy-Backend-ID', 'value', haproxy_backend_name]) self.cli_commit() # Test headers are present in generated configuration file config = read_file(HAPROXY_CONF) self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config) - self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config) + self.assertIn(f'http-response set-header Proxy-Backend-ID \'{haproxy_backend_name}\'', config) # Test setting alongside modes other than http is blocked by validation conditions - self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp']) + self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'tcp']) with self.assertRaises(ConfigSessionError) as e: self.cli_commit() - def test_08_lb_reverse_proxy_tcp_health_checks(self): + def test_reverse_proxy_tcp_health_checks(self): # Setup PKI self.configure_pki() @@ -458,7 +479,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): config = read_file(HAPROXY_CONF) self.assertIn(f'option smtpchk', config) - def test_09_lb_reverse_proxy_logging(self): + def test_reverse_proxy_logging(self): # Setup base self.base_config() self.cli_commit() @@ -477,7 +498,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('log /dev/log local2 warning', config) # Test backend logging options - backend_path = base_path + ['backend', 'bk-01'] + backend_path = base_path + ['backend', haproxy_backend_name] self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug']) self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info']) self.cli_commit() @@ -488,7 +509,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('log /dev/log local4 info', config) # Test service logging options - service_path = base_path + ['service', 'https_front'] + service_path = base_path + ['service', haproxy_service_name] self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice']) self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit']) self.cli_commit() @@ -498,5 +519,97 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): self.assertIn('log /dev/log local5 notice', config) self.assertIn('log /dev/log local6 crit', config) + def test_reverse_proxy_http_compression(self): + # Setup base + self.configure_pki() + self.base_config() + + # Configure compression in frontend + http_comp_path = base_path + ['service', haproxy_service_name, 'http-compression'] + self.cli_set(http_comp_path + ['algorithm', 'gzip']) + self.cli_set(http_comp_path + ['mime-type', 'text/html']) + self.cli_set(http_comp_path + ['mime-type', 'text/javascript']) + self.cli_set(http_comp_path + ['mime-type', 'text/plain']) + self.cli_commit() + + # Test compression is present in generated configuration file + config = read_file(HAPROXY_CONF) + self.assertIn('filter compression', config) + self.assertIn('compression algo gzip', config) + self.assertIn('compression type text/html text/javascript text/plain', config) + + # Test setting compression without specifying any mime-types fails verification + self.cli_delete(base_path + ['service', haproxy_service_name, 'http-compression', 'mime-type']) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + + def test_reverse_proxy_timeout(self): + t_default_check = '5' + t_default_client = '50' + t_default_connect = '10' + t_default_server ='50' + t_check = '4' + t_client = '300' + t_connect = '12' + t_server ='120' + t_front_client = '600' + + self.base_config() + self.cli_commit() + # Check default timeout options + config_entries = ( + f'timeout check {t_default_check}s', + f'timeout connect {t_default_connect}s', + f'timeout client {t_default_client}s', + f'timeout server {t_default_server}s', + ) + # Check default timeout options + config = read_file(HAPROXY_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + # Set custom timeout options + self.cli_set(base_path + ['timeout', 'check', t_check]) + self.cli_set(base_path + ['timeout', 'client', t_client]) + self.cli_set(base_path + ['timeout', 'connect', t_connect]) + self.cli_set(base_path + ['timeout', 'server', t_server]) + self.cli_set(base_path + ['service', haproxy_service_name, 'timeout', 'client', t_front_client]) + + self.cli_commit() + + # Check custom timeout options + config_entries = ( + f'timeout check {t_check}s', + f'timeout connect {t_connect}s', + f'timeout client {t_client}s', + f'timeout server {t_server}s', + f'timeout client {t_front_client}s', + ) + + # Check configured options + config = read_file(HAPROXY_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + def test_reverse_proxy_http_redirect(self): + self.base_config() + self.cli_set(base_path + ['service', haproxy_service_name, 'redirect-http-to-https']) + + self.cli_commit() + + config = parse_haproxy_config() + frontend_name = f'frontend {haproxy_service_name}-http' + self.assertIn(frontend_name, config.keys()) + self.assertIn('mode http', config[frontend_name]) + self.assertIn('bind [::]:80 v4v6', config[frontend_name]) + self.assertIn('acl acme_acl path_beg /.well-known/acme-challenge/', config[frontend_name]) + self.assertIn('use_backend buildin_acme_certbot if acme_acl', config[frontend_name]) + self.assertIn('redirect scheme https code 301 if !acme_acl', config[frontend_name]) + + backend_name = 'backend buildin_acme_certbot' + self.assertIn(backend_name, config.keys()) + port = get_default_port('certbot_haproxy') + self.assertIn(f'server localhost 127.0.0.1:{port}', config[backend_name]) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_load-balancing_wan.py b/smoketest/scripts/cli/test_load-balancing_wan.py index 92b4000b8..32e5f6915 100755 --- a/smoketest/scripts/cli/test_load-balancing_wan.py +++ b/smoketest/scripts/cli/test_load-balancing_wan.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 @@ -14,10 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest import time from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.utils.file import chmod_755 +from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import cmd @@ -54,6 +57,16 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + removed_chains = [ + 'wlb_mangle_isp_veth1', + 'wlb_mangle_isp_veth2', + 'wlb_mangle_isp_eth201', + 'wlb_mangle_isp_eth202' + ] + + for chain in removed_chains: + self.verify_nftables_chain_exists('ip vyos_wanloadbalance', chain, inverse=True) + def test_table_routes(self): ns1 = 'ns201' ns2 = 'ns202' @@ -93,6 +106,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): cmd_in_netns(ns3, 'ip link set dev eth0 up') # Set load-balancing configuration + self.cli_set(base_path + ['wan', 'hook', '/bin/true']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1']) @@ -102,7 +116,8 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) - + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2]) # commit changes self.cli_commit() @@ -127,7 +142,6 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): delete_netns(ns3) def test_check_chains(self): - ns1 = 'nsA' ns2 = 'nsB' ns3 = 'nsC' @@ -137,43 +151,28 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): container_iface1 = 'ceth0' container_iface2 = 'ceth1' container_iface3 = 'ceth2' - mangle_isp1 = """table ip mangle { - chain ISP_veth1 { - counter ct mark set 0xc9 - counter meta mark set 0xc9 - counter accept + mangle_isp1 = """table ip vyos_wanloadbalance { + chain wlb_mangle_isp_veth1 { + meta mark set 0x000000c9 ct mark set 0x000000c9 counter accept } }""" - mangle_isp2 = """table ip mangle { - chain ISP_veth2 { - counter ct mark set 0xca - counter meta mark set 0xca - counter accept + mangle_isp2 = """table ip vyos_wanloadbalance { + chain wlb_mangle_isp_veth2 { + meta mark set 0x000000ca ct mark set 0x000000ca counter accept } }""" - mangle_prerouting = """table ip mangle { - chain PREROUTING { + mangle_prerouting = """table ip vyos_wanloadbalance { + chain wlb_mangle_prerouting { type filter hook prerouting priority mangle; policy accept; - counter jump WANLOADBALANCE_PRE - } -}""" - mangle_wanloadbalance_pre = """table ip mangle { - chain WANLOADBALANCE_PRE { - iifname "veth3" ip saddr 198.51.100.0/24 ct state new meta random & 2147483647 < 1073741824 counter jump ISP_veth1 - iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth2 + iifname "veth3" ip saddr 198.51.100.0/24 ct state new limit rate 5/second burst 5 packets counter numgen random mod 11 vmap { 0 : jump wlb_mangle_isp_veth1, 1-10 : jump wlb_mangle_isp_veth2 } iifname "veth3" ip saddr 198.51.100.0/24 counter meta mark set ct mark } }""" - nat_wanloadbalance = """table ip nat { - chain WANLOADBALANCE { - ct mark 0xc9 counter snat to 203.0.113.10 - ct mark 0xca counter snat to 192.0.2.10 - } -}""" - nat_vyos_pre_snat_hook = """table ip nat { - chain VYOS_PRE_SNAT_HOOK { + nat_wanloadbalance = """table ip vyos_wanloadbalance { + chain wlb_nat_postrouting { type nat hook postrouting priority srcnat - 1; policy accept; - counter jump WANLOADBALANCE + ct mark 0x000000c9 counter snat to 203.0.113.10 + ct mark 0x000000ca counter snat to 192.0.2.10 } }""" @@ -214,7 +213,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1]) - self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2, 'weight', '10']) # commit changes self.cli_commit() @@ -222,25 +221,19 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): time.sleep(5) # Check mangle chains - tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}') + tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface1}') self.assertEqual(tmp, mangle_isp1) - tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}') + tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface2}') self.assertEqual(tmp, mangle_isp2) - tmp = cmd(f'sudo nft -s list chain mangle PREROUTING') + tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_prerouting') self.assertEqual(tmp, mangle_prerouting) - tmp = cmd(f'sudo nft -s list chain mangle WANLOADBALANCE_PRE') - self.assertEqual(tmp, mangle_wanloadbalance_pre) - # Check nat chains - tmp = cmd(f'sudo nft -s list chain nat WANLOADBALANCE') + tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_nat_postrouting') self.assertEqual(tmp, nat_wanloadbalance) - tmp = cmd(f'sudo nft -s list chain nat VYOS_PRE_SNAT_HOOK') - self.assertEqual(tmp, nat_vyos_pre_snat_hook) - # Delete veth interfaces and netns for iface in [iface1, iface2, iface3]: call(f'sudo ip link del dev {iface}') @@ -249,6 +242,85 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): delete_netns(ns2) delete_netns(ns3) + def test_criteria_failover_hook(self): + isp1_iface = 'eth0' + isp2_iface = 'eth1' + lan_iface = 'eth2' + + hook_path = '/tmp/wlb_hook.sh' + hook_output_path = '/tmp/wlb_hook_output' + hook_script = f""" +#!/bin/sh + +ifname=$WLB_INTERFACE_NAME +state=$WLB_INTERFACE_STATE + +echo "$ifname - $state" > {hook_output_path} +""" + + write_file(hook_path, hook_script) + chmod_755(hook_path) + + self.cli_set(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30']) + self.cli_set(['interfaces', 'ethernet', isp2_iface, 'address', '192.0.2.2/30']) + self.cli_set(['interfaces', 'ethernet', lan_iface, 'address', '198.51.100.2/30']) + + self.cli_set(base_path + ['wan', 'hook', hook_path]) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'failure-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'nexthop', '203.0.113.2']) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'failure-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'nexthop', '192.0.2.2']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'rule', '5', 'exclude']) + self.cli_set(base_path + ['wan', 'rule', '5', 'inbound-interface', 'eth*']) + self.cli_set(base_path + ['wan', 'rule', '5', 'destination', 'address', '10.0.0.0/8']) + self.cli_set(base_path + ['wan', 'rule', '10', 'failover']) + self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', lan_iface]) + self.cli_set(base_path + ['wan', 'rule', '10', 'protocol', 'udp']) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'port', '53']) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'address', '192.0.2.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'port', '53']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface, 'weight', '10']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp2_iface]) + + # commit changes + self.cli_commit() + + time.sleep(5) + + # Verify isp1 + criteria + + nftables_search = [ + [f'iifname "eth*"', 'ip daddr 10.0.0.0/8', 'return'], + [f'iifname "{lan_iface}"', 'ip saddr 198.51.100.0/24', 'udp sport 53', 'ip daddr 192.0.2.0/24', 'udp dport 53', f'jump wlb_mangle_isp_{isp1_iface}'] + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + + # Trigger failure on isp1 health check + + self.cli_delete(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30']) + self.cli_commit() + + time.sleep(10) + + # Verify failover to isp2 + + nftables_search = [ + [f'iifname "{lan_iface}"', f'jump wlb_mangle_isp_{isp2_iface}'] + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + + # Verify hook output + + self.assertTrue(os.path.exists(hook_output_path)) + + with open(hook_output_path, 'r') as f: + self.assertIn('eth0 - FAILED', f.read()) 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_nat66.py b/smoketest/scripts/cli/test_nat66.py index 52ad8e3ef..d4b5d6aa4 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -227,6 +227,35 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_nat') + def test_source_nat66_network_group(self): + address_group = 'smoketest_addr' + address_group_member = 'fc00::1' + network_group = 'smoketest_net' + network_group_member = 'fc00::/64' + translation_prefix = 'fc01::/64' + + self.cli_set(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member]) + self.cli_set(['firewall', 'group', 'ipv6-network-group', network_group, 'network', network_group_member]) + + self.cli_set(src_path + ['rule', '1', 'destination', 'group', 'address-group', address_group]) + self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_prefix]) + + self.cli_set(src_path + ['rule', '2', 'destination', 'group', 'network-group', network_group]) + self.cli_set(src_path + ['rule', '2', 'translation', 'address', translation_prefix]) + + self.cli_commit() + + nftables_search = [ + [f'set A6_{address_group}'], + [f'elements = {{ {address_group_member} }}'], + [f'set N6_{network_group}'], + [f'elements = {{ {network_group_member} }}'], + ['ip6 daddr', f'@A6_{address_group}', 'snat prefix to fc01::/64'], + ['ip6 daddr', f'@N6_{network_group}', 'snat prefix to fc01::/64'] + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_nat') + def test_nat66_no_rules(self): # T3206: deleting all rules but keep the direction 'destination' or # 'source' resulteds in KeyError: 'rule'. diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index 7ea1b610e..985097726 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 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 @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd @@ -24,6 +25,17 @@ from vyos.utils.process import cmd base_path = ['policy'] class TestPolicy(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestPolicy, 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) + cls.cli_delete(cls, ['vrf']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + def tearDown(self): self.cli_delete(base_path) self.cli_commit() @@ -1137,6 +1149,16 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): }, }, }, + 'vrf-match': { + 'rule': { + '10': { + 'action': 'permit', + 'match': { + 'source-vrf': 'TEST', + }, + }, + }, + }, } self.cli_set(['policy', 'access-list', access_list, 'rule', '10', 'action', 'permit']) @@ -1248,6 +1270,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'match', 'rpki', 'valid']) if 'protocol' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'protocol', rule_config['match']['protocol']]) + if 'source-vrf' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'source-vrf', rule_config['match']['source-vrf']]) if 'tag' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'tag', rule_config['match']['tag']]) @@ -1426,6 +1450,9 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): if 'rpki-valid' in rule_config['match']: tmp = f'match rpki valid' self.assertIn(tmp, config) + if 'source-vrf' in rule_config['match']: + tmp = f'match source-vrf {rule_config["match"]["source-vrf"]}' + self.assertIn(tmp, config) if 'tag' in rule_config['match']: tmp = f'match tag {rule_config["match"]["tag"]}' self.assertIn(tmp, config) diff --git a/smoketest/scripts/cli/test_policy_local-route.py b/smoketest/scripts/cli/test_policy_local-route.py index 8d6ba40dc..a4239b8a1 100644 --- a/smoketest/scripts/cli/test_policy_local-route.py +++ b/smoketest/scripts/cli/test_policy_local-route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright (C) 2024-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,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME interface = 'eth0' mark = '100' @@ -32,6 +33,8 @@ class TestPolicyLocalRoute(VyOSUnitTestSHIM.TestCase): # Clear out current configuration to allow running this test on a live system cls.cli_delete(cls, ['policy', 'local-route']) cls.cli_delete(cls, ['policy', 'local-route6']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME cls.cli_set(cls, ['vrf', 'name', vrf_name, 'table', vrf_rt_id]) diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index 672865eb0..15ddd857e 100755 --- a/smoketest/scripts/cli/test_policy_route.py +++ b/smoketest/scripts/cli/test_policy_route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 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 @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME mark = '100' conn_mark = '555' @@ -36,6 +37,8 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): # Clear out current configuration to allow running this test on a live system cls.cli_delete(cls, ['policy', 'route']) cls.cli_delete(cls, ['policy', 'route6']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip]) cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface]) @@ -304,5 +307,39 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables6_search, 'ip6 vyos_mangle') + def test_geoip(self): + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'action', 'drop']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'action', 'accept']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match']) + + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'action', 'drop']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'action', 'accept']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'inverse-match']) + + self.cli_commit() + + nftables_search = [ + ['ip saddr @GEOIP_CC_route_smoketest_1', 'drop'], + ['ip saddr != @GEOIP_CC_route_smoketest_2', 'accept'], + ] + + # -t prevents 1000+ GeoIP elements being returned + self.verify_nftables(nftables_search, 'ip vyos_mangle', args='-t') + + nftables_search = [ + ['ip6 saddr @GEOIP_CC6_route6_smoketest6_1', 'drop'], + ['ip6 saddr != @GEOIP_CC6_route6_smoketest6_2', 'accept'], + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_mangle', args='-t') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_babel.py b/smoketest/scripts/cli/test_protocols_babel.py index 12f6ecf38..3a9ee2d62 100755 --- a/smoketest/scripts/cli/test_protocols_babel.py +++ b/smoketest/scripts/cli/test_protocols_babel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright (C) 2024-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,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.ifconfig import Section from vyos.frrender import babel_daemon @@ -38,6 +39,8 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['policy', 'prefix-list']) cls.cli_delete(cls, ['policy', 'prefix-list6']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): # always destroy the entire babel configuration to make the processes @@ -62,14 +65,14 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit', daemon=babel_daemon) + frrconfig = self.getFRRconfig('router babel', endsection='^exit') self.assertIn(f' babel diversity', frrconfig) self.assertIn(f' babel diversity-factor {diversity_factor}', frrconfig) self.assertIn(f' babel resend-delay {resend_delay}', frrconfig) self.assertIn(f' babel smoothing-half-life {smoothing_half_life}', frrconfig) def test_02_redistribute(self): - ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'rip', 'static'] + ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'rip', 'static'] ipv6_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static'] self.cli_set(base_path + ['interface', self._interfaces[0], 'enable-timestamps']) @@ -81,7 +84,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit', daemon=babel_daemon, empty_retry=5) + frrconfig = self.getFRRconfig('router babel', endsection='^exit', empty_retry=5) for protocol in ipv4_protos: self.assertIn(f' redistribute ipv4 {protocol}', frrconfig) for protocol in ipv6_protos: @@ -150,7 +153,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit', daemon=babel_daemon) + frrconfig = self.getFRRconfig('router babel', endsection='^exit') self.assertIn(f' distribute-list {access_list_in4} in', frrconfig) self.assertIn(f' distribute-list {access_list_out4} out', frrconfig) self.assertIn(f' ipv6 distribute-list {access_list_in6} in', frrconfig) @@ -198,11 +201,11 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig('router babel', endsection='^exit', daemon=babel_daemon) + frrconfig = self.getFRRconfig('router babel', endsection='^exit') for interface in self._interfaces: self.assertIn(f' network {interface}', frrconfig) - iface_config = self.getFRRconfig(f'interface {interface}', endsection='^exit', daemon=babel_daemon) + iface_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' babel channel {channel}', iface_config) self.assertIn(f' babel enable-timestamps', iface_config) self.assertIn(f' babel update-interval {def_update_interval}', iface_config) diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index 32e39e8f7..2205cd9de 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -17,12 +17,13 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.frrender import bfd_daemon from vyos.utils.process import process_named_running base_path = ['protocols', 'bfd'] -frr_endsection = '^ exit' dum_if = 'dum1001' vrf_name = 'red' @@ -87,6 +88,9 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same cls.daemon_pid = process_named_running(bfd_daemon) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) @@ -131,7 +135,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('bfd', endsection='^exit', daemon=bfd_daemon) + frrconfig = self.getFRRconfig('bfd', endsection='^exit') for peer, peer_config in peers.items(): tmp = f'peer {peer}' if 'multihop' in peer_config: @@ -144,8 +148,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): tmp += f' vrf {peer_config["vrf"]}' self.assertIn(tmp, frrconfig) - peerconfig = self.getFRRconfig(f' peer {peer}', end='', endsection=frr_endsection, - daemon=bfd_daemon) + peerconfig = self.getFRRconfig('bfd', endsection='^exit', substring=f' peer {peer}', + endsubsection='^ exit') if 'echo_mode' in peer_config: self.assertIn(f'echo-mode', peerconfig) if 'intv_echo' in peer_config: @@ -207,7 +211,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): # Verify FRR bgpd configuration for profile, profile_config in profiles.items(): - config = self.getFRRconfig(f' profile {profile}', endsection=frr_endsection) + config = self.getFRRconfig('bfd', endsection='^exit', + substring=f' profile {profile}', endsubsection='^ exit',) if 'echo_mode' in profile_config: self.assertIn(f' echo-mode', config) if 'intv_echo' in profile_config: @@ -229,8 +234,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f'shutdown', config) for peer, peer_config in peers.items(): - peerconfig = self.getFRRconfig(f' peer {peer}', end='', - endsection=frr_endsection, daemon=bfd_daemon) + peerconfig = self.getFRRconfig('bfd', endsection='^exit', + substring=f' peer {peer}', endsubsection='^ exit') if 'profile' in peer_config: self.assertIn(f' profile {peer_config["profile"]}', peerconfig) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index cdf18a051..8403dcc37 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.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 @@ -19,6 +19,7 @@ import unittest from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.ifconfig import Section from vyos.configsession import ConfigSessionError @@ -200,6 +201,9 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'action', 'deny']) cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'prefix', '2001:db8:2000::/64']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['policy', 'route-map']) @@ -217,7 +221,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router bgp', endsection='^exit', daemon=bgp_daemon) + frrconfig = self.getFRRconfig('router bgp', endsection='^exit') self.assertNotIn(f'router bgp', frrconfig) # check process health and continuity @@ -372,7 +376,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', daemon=bgp_daemon) + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' bgp router-id {router_id}', frrconfig) self.assertIn(f' bgp allow-martian-nexthop', frrconfig) @@ -398,18 +402,21 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f'bgp ebgp-requires-policy', frrconfig) self.assertIn(f' no bgp suppress-duplicates', frrconfig) - afiv4_config = self.getFRRconfig(' address-family ipv4 unicast', - endsection='^ exit-address-family', daemon=bgp_daemon) + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config) self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config) - afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast', - endsection='^ exit-address-family', daemon=bgp_daemon) + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 labeled-unicast', + endsubsection='^ exit-address-family') self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config) self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config) - afiv6_config = self.getFRRconfig(' address-family ipv6 unicast', - endsection='^ exit-address-family', daemon=bgp_daemon) + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv6 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' maximum-paths {max_path_v6}', afiv6_config) self.assertIn(f' maximum-paths ibgp {max_path_v6ibgp}', afiv6_config) @@ -516,7 +523,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', daemon=bgp_daemon) + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for peer, peer_config in neighbor_config.items(): @@ -621,7 +628,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', daemon=bgp_daemon) + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for peer, peer_config in peer_group_config.items(): @@ -648,10 +655,71 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): } # We want to redistribute ... - redistributes = ['connected', 'isis', 'kernel', 'ospf', 'rip', 'static'] - for redistribute in redistributes: - self.cli_set(base_path + ['address-family', 'ipv4-unicast', - 'redistribute', redistribute]) + redistributes = { + 'babel' : { + 'metric' : '100', + 'route_map' : 'redistr-ipv4-babel', + }, + 'connected' : { + 'metric' : '200', + 'route_map' : 'redistr-ipv4-connected', + }, + 'isis' : { + 'metric' : '300', + 'route_map' : 'redistr-ipv4-isis', + }, + 'kernel' : { + 'metric' : '400', + 'route_map' : 'redistr-ipv4-kernel', + }, + 'nhrp': { + 'metric': '400', + 'route_map': 'redistr-ipv4-nhrp', + }, + 'ospf' : { + 'metric' : '500', + 'route_map' : 'redistr-ipv4-ospf', + }, + 'rip' : { + 'metric' : '600', + 'route_map' : 'redistr-ipv4-rip', + }, + 'static' : { + 'metric' : '700', + 'route_map' : 'redistr-ipv4-static', + }, + 'table' : { + '10' : { + 'metric' : '810', + 'route_map' : 'redistr-ipv4-table-10', + }, + '20' : { + 'metric' : '820', + 'route_map' : 'redistr-ipv4-table-20', + }, + '30' : { + 'metric' : '830', + 'route_map' : 'redistr-ipv4-table-30', + }, + }, + } + for proto, proto_config in redistributes.items(): + proto_path = base_path + ['address-family', 'ipv4-unicast', 'redistribute', proto] + if proto == 'table': + for table, table_config in proto_config.items(): + self.cli_set(proto_path + [table]) + if 'metric' in table_config: + self.cli_set(proto_path + [table, 'metric'], value=table_config['metric']) + if 'route_map' in table_config: + self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map']) + else: + self.cli_set(proto_path) + if 'metric' in proto_config: + self.cli_set(proto_path + ['metric', proto_config['metric']]) + if 'route_map' in proto_config: + self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '10', 'action', 'permit']) + self.cli_set(proto_path + ['route-map', proto_config['route_map']]) for network, network_config in networks.items(): self.cli_set(base_path + ['address-family', 'ipv4-unicast', @@ -670,12 +738,31 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', daemon=bgp_daemon) + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) - self.assertIn(f' address-family ipv4 unicast', frrconfig) - - for redistribute in redistributes: - self.assertIn(f' redistribute {redistribute}', frrconfig) + self.assertIn(' address-family ipv4 unicast', frrconfig) + + for proto, proto_config in redistributes.items(): + if proto == 'table': + for table, table_config in proto_config.items(): + tmp = f' redistribute table-direct {table}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + self.assertIn(tmp, frrconfig) + else: + tmp = f' redistribute {proto}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + + self.assertIn(tmp, frrconfig) for network, network_config in networks.items(): self.assertIn(f' network {network}', frrconfig) @@ -688,6 +775,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): command = f'{command} route-map {network_config["route_map"]}' self.assertIn(command, frrconfig) + for proto, proto_config in redistributes.items(): + if 'route_map' in proto_config: + self.cli_delete(['policy', 'route-map', proto_config['route_map']]) + def test_bgp_05_afi_ipv6(self): networks = { '2001:db8:100::/48' : { @@ -700,10 +791,67 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): } # We want to redistribute ... - redistributes = ['connected', 'kernel', 'ospfv3', 'ripng', 'static'] - for redistribute in redistributes: - self.cli_set(base_path + ['address-family', 'ipv6-unicast', - 'redistribute', redistribute]) + redistributes = { + 'babel' : { + 'metric' : '100', + 'route_map' : 'redistr-ipv6-babel', + }, + 'connected' : { + 'metric' : '200', + 'route_map' : 'redistr-ipv6-connected', + }, + 'isis' : { + 'metric' : '300', + 'route_map' : 'redistr-ipv6-isis', + }, + 'kernel' : { + 'metric' : '400', + 'route_map' : 'redistr-ipv6-kernel', + }, + 'ospfv3' : { + 'metric' : '500', + 'route_map' : 'redistr-ipv6-ospfv3', + }, + 'ripng' : { + 'metric' : '600', + 'route_map' : 'redistr-ipv6-ripng', + }, + 'static' : { + 'metric' : '700', + 'route_map' : 'redistr-ipv6-static', + }, + 'table' : { + '110' : { + 'metric' : '811', + 'route_map' : 'redistr-ipv6-table-110', + }, + '120' : { + 'metric' : '821', + 'route_map' : 'redistr-ipv6-table-120', + }, + '130' : { + 'metric' : '831', + 'route_map' : 'redistr-ipv6-table-130', + }, + }, + } + for proto, proto_config in redistributes.items(): + proto_path = base_path + ['address-family', 'ipv6-unicast', 'redistribute', proto] + if proto == 'table': + for table, table_config in proto_config.items(): + self.cli_set(proto_path + [table]) + if 'metric' in table_config: + self.cli_set(proto_path + [table, 'metric'], value=table_config['metric']) + if 'route_map' in table_config: + self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map']) + else: + self.cli_set(proto_path) + if 'metric' in proto_config: + self.cli_set(proto_path + ['metric', proto_config['metric']]) + if 'route_map' in proto_config: + self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '20', 'action', 'permit']) + self.cli_set(proto_path + ['route-map', proto_config['route_map']]) for network, network_config in networks.items(): self.cli_set(base_path + ['address-family', 'ipv6-unicast', @@ -716,24 +864,47 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', daemon=bgp_daemon) + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) - self.assertIn(f' address-family ipv6 unicast', frrconfig) + self.assertIn(' address-family ipv6 unicast', frrconfig) # T2100: By default ebgp-requires-policy is disabled to keep VyOS # 1.3 and 1.2 backwards compatibility - self.assertIn(f' no bgp ebgp-requires-policy', frrconfig) - - for redistribute in redistributes: - # FRR calls this OSPF6 - if redistribute == 'ospfv3': - redistribute = 'ospf6' - self.assertIn(f' redistribute {redistribute}', frrconfig) + self.assertIn(' no bgp ebgp-requires-policy', frrconfig) + + for proto, proto_config in redistributes.items(): + if proto == 'table': + for table, table_config in proto_config.items(): + tmp = f' redistribute table-direct {table}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + self.assertIn(tmp, frrconfig) + else: + # FRR calls this OSPF6 + if proto == 'ospfv3': + proto = 'ospf6' + tmp = f' redistribute {proto}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + + self.assertIn(tmp, frrconfig) for network, network_config in networks.items(): self.assertIn(f' network {network}', frrconfig) if 'as_set' in network_config: self.assertIn(f' aggregate-address {network} summary-only', frrconfig) + for proto, proto_config in redistributes.items(): + if 'route_map' in proto_config: + self.cli_delete(['policy', 'route-map', proto_config['route_map']]) + def test_bgp_06_listen_range(self): # Implemented via T1875 limit = '64' @@ -758,7 +929,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', daemon=bgp_daemon) + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {peer_group} peer-group', frrconfig) self.assertIn(f' neighbor {peer_group} remote-as {ASN}', frrconfig) @@ -793,7 +964,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', daemon=bgp_daemon) + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' address-family l2vpn evpn', frrconfig) self.assertIn(f' advertise-all-vni', frrconfig) @@ -806,7 +977,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' flooding disable', frrconfig) self.assertIn(f' mac-vrf soo {soo}', frrconfig) for vni in vnis: - vniconfig = self.getFRRconfig(f' vni {vni}', endsection='^exit-vni') + vniconfig = self.getFRRconfig(f' vni {vni}', endsection='^ exit-vni') self.assertIn(f'vni {vni}', vniconfig) self.assertIn(f' advertise-default-gw', vniconfig) self.assertIn(f' advertise-svi-ip', vniconfig) @@ -849,7 +1020,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR distances configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for family in verify_families: self.assertIn(f'address-family {family}', frrconfig) @@ -887,7 +1058,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' address-family ipv6 unicast', frrconfig) @@ -895,7 +1066,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' import vrf {vrf}', frrconfig) # Verify FRR bgpd configuration - frr_vrf_config = self.getFRRconfig(f'router bgp {ASN} vrf {vrf}') + frr_vrf_config = self.getFRRconfig(f'router bgp {ASN} vrf {vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config) self.assertIn(f' bgp router-id {router_id}', frr_vrf_config) @@ -913,7 +1084,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' bgp router-id {router_id}', frrconfig) self.assertIn(f' bgp confederation identifier {confed_id}', frrconfig) @@ -930,7 +1101,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {interface} interface v6only remote-as {remote_asn}', frrconfig) self.assertIn(f' address-family ipv6 unicast', frrconfig) @@ -962,11 +1133,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) for afi in ['ipv4', 'ipv6']: - afi_config = self.getFRRconfig(f' address-family {afi} unicast', endsection='exit-address-family', daemon=bgp_daemon) + afi_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=f' address-family {afi} unicast', + endsubsection='^ exit-address-family') self.assertIn(f'address-family {afi} unicast', afi_config) self.assertIn(f' export vpn', afi_config) self.assertIn(f' import vpn', afi_config) @@ -1011,7 +1184,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {neighbor} peer-group {peer_group}', frrconfig) self.assertIn(f' neighbor {peer_group} peer-group', frrconfig) @@ -1036,7 +1209,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {neighbor} remote-as {remote_asn}', frrconfig) self.assertIn(f' neighbor {neighbor} local-as {local_asn}', frrconfig) @@ -1061,8 +1234,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): base_path + ['address-family', import_afi, 'import', 'vrf', import_vrf]) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'address-family ipv4 unicast', frrconfig) @@ -1084,8 +1257,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): base_path + ['address-family', import_afi, 'import', 'vrf', import_vrf]) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'address-family ipv4 unicast', frrconfig) self.assertIn(f' import vrf {import_vrf}', frrconfig) @@ -1098,8 +1271,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # Verify deleting existent vrf default if other vrfs were created self.create_bgp_instances_for_import_test() self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) self.cli_delete(base_path) @@ -1115,8 +1288,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 'vpn', 'export', import_rd]) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') - frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) self.assertIn(f'address-family ipv4 unicast', frrconfig_vrf) @@ -1145,7 +1318,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' mpls bgp forwarding', frrconfig) @@ -1159,7 +1332,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' mpls bgp forwarding', frrconfig) self.cli_delete(['interfaces', 'ethernet', interface, 'vrf']) @@ -1179,7 +1352,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path + ['address-family', 'ipv4-unicast', 'sid']) self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' segment-routing srv6', frrconfig) self.assertIn(f' locator {locator_name}', frrconfig) @@ -1194,17 +1367,22 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' segment-routing srv6', frrconfig) self.assertIn(f' locator {locator_name}', frrconfig) - afiv4_config = self.getFRRconfig(' address-family ipv4 unicast') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' sid vpn export {sid}', afiv4_config) self.assertIn(f' nexthop vpn export {nexthop_ipv4}', afiv4_config) - afiv6_config = self.getFRRconfig(' address-family ipv6 unicast') + + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv6 unicast', + endsubsection='^ exit-address-family') self.assertIn(f' sid vpn export {sid}', afiv6_config) - self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv4_config) + self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv6_config) def test_bgp_25_ipv4_labeled_unicast_peer_group(self): pg_ipv4 = 'foo4' @@ -1218,14 +1396,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {pg_ipv4} peer-group', frrconfig) self.assertIn(f' neighbor {pg_ipv4} remote-as external', frrconfig) self.assertIn(f' bgp listen range {ipv4_prefix} peer-group {pg_ipv4}', frrconfig) self.assertIn(f' bgp labeled-unicast ipv4-explicit-null', frrconfig) - afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast') + afiv4_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv4 labeled-unicast', + endsubsection='^ exit-address-family') self.assertIn(f' neighbor {pg_ipv4} activate', afiv4_config) self.assertIn(f' neighbor {pg_ipv4} maximum-prefix {ipv4_max_prefix}', afiv4_config) @@ -1242,14 +1422,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {pg_ipv6} peer-group', frrconfig) self.assertIn(f' neighbor {pg_ipv6} remote-as external', frrconfig) self.assertIn(f' bgp listen range {ipv6_prefix} peer-group {pg_ipv6}', frrconfig) self.assertIn(f' bgp labeled-unicast ipv6-explicit-null', frrconfig) - afiv6_config = self.getFRRconfig(' address-family ipv6 labeled-unicast') + afiv6_config = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family ipv6 labeled-unicast', + endsubsection='^ exit-address-family') self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config) self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config) @@ -1261,7 +1443,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['peer-group', 'peer1', 'remote-as', 'internal']) self.cli_commit() - conf = self.getFRRconfig(' address-family l2vpn evpn') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit', + substring=' address-family l2vpn evpn', endsubsection='^ exit-address-family') self.assertIn('neighbor peer1 route-reflector-client', conf) @@ -1300,7 +1483,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', int_neighbors[1], 'remote-as', ASN]) self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') _common_config_check(conf) # test add internal remote-as to external group @@ -1315,7 +1498,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'remote-as', f'{int(ASN) + 2}']) self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') _common_config_check(conf) self.assertIn(f'neighbor {ext_neighbors[1]} remote-as {int(ASN) + 2}', conf) self.assertIn(f'neighbor {ext_neighbors[1]} peer-group {ext_pg_name}', conf) @@ -1327,7 +1510,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'remote-as', 'external']) self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') _common_config_check(conf, include_ras=False) self.assertIn(f'neighbor {int_neighbors[0]} remote-as internal', conf) @@ -1352,11 +1535,47 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() - conf = self.getFRRconfig(f'router bgp {ASN}') + conf = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'neighbor OVERLAY remote-as {int(ASN) + 1}', conf) self.assertIn(f'neighbor OVERLAY local-as {int(ASN) + 1}', conf) + def test_bgp_30_import_vrf_routemap(self): + router_id = '127.0.0.3' + table = '1000' + vrf = 'red' + vrf_base = ['vrf', 'name', vrf] + self.cli_set(vrf_base + ['table', table]) + self.cli_set(vrf_base + ['protocols', 'bgp', 'system-as', ASN]) + self.cli_set( + vrf_base + ['protocols', 'bgp', 'parameters', 'router-id', + router_id]) + + self.cli_set( + base_path + ['address-family', 'ipv4-unicast', 'import', + 'vrf', vrf]) + self.cli_set( + base_path + ['address-family', 'ipv4-unicast', 'route-map', + 'vrf', 'import', route_map_in]) + + self.cli_commit() + + # Verify FRR bgpd configuration + frrconfig = self.getFRRconfig(f'router bgp {ASN}', + endsection='^exit') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' address-family ipv4 unicast', frrconfig) + + self.assertIn(f' import vrf {vrf}', frrconfig) + self.assertIn(f' import vrf route-map {route_map_in}', frrconfig) + + # Verify FRR bgpd configuration + frr_vrf_config = self.getFRRconfig( + f'router bgp {ASN} vrf {vrf}', endsection='^exit') + self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config) + self.assertIn(f' bgp router-id {router_id}', frr_vrf_config) + + def test_bgp_99_bmp(self): target_name = 'instance-bmp' target_address = '127.0.0.1' @@ -1405,7 +1624,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify bgpd bmp configuration - frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit') self.assertIn(f'bmp mirror buffer-limit {mirror_buffer}', frrconfig) self.assertIn(f'bmp targets {target_name}', frrconfig) self.assertIn(f'bmp mirror', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index bde32a1ca..14e833fd9 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.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 @@ -17,13 +17,14 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.utils.process import process_named_running from vyos.frrender import isis_daemon base_path = ['protocols', 'isis'] - domain = 'VyOS' net = '49.0001.1921.6800.1002.00' @@ -39,6 +40,8 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): # out the current configuration :) cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): # cleanup any possible VRF mess @@ -51,17 +54,12 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(isis_daemon)) - def isis_base_config(self): - self.cli_set(base_path + ['net', net]) - for interface in self._interfaces: - self.cli_set(base_path + ['interface', interface]) - def test_isis_01_redistribute(self): prefix_list = 'EXPORT-ISIS' route_map = 'EXPORT-ISIS' rule = '10' metric_style = 'transition' - + redistribute = ['babel', 'bgp', 'connected', 'kernel', 'nhrp', 'ospf', 'rip', 'static'] self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'action', 'permit']) self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'prefix', '203.0.113.0/24']) self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'action', 'permit']) @@ -73,14 +71,18 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() - self.isis_base_config() + self.cli_set(base_path + ['net', net]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) self.cli_set(base_path + ['redistribute', 'ipv4', 'connected']) # verify() - Redistribute level-1 or level-2 should be specified with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map]) + for proto in redistribute: + self.cli_set(base_path + ['redistribute', 'ipv4', proto, 'level-2', 'route-map', route_map]) + self.cli_set(base_path + ['metric-style', metric_style]) self.cli_set(base_path + ['log-adjacency-changes']) @@ -88,14 +90,15 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' metric-style {metric_style}', tmp) self.assertIn(f' log-adjacency-changes', tmp) - self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp) + for proto in redistribute: + self.assertIn(f' redistribute ipv4 {proto} level-2 route-map {route_map}', tmp) for interface in self._interfaces: - tmp = self.getFRRconfig(f'interface {interface}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -124,11 +127,11 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR isisd configuration - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f'router isis {domain}', tmp) self.assertIn(f' net {net}', tmp) - tmp = self.getFRRconfig(f'router isis {domain} vrf {vrf}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain} vrf {vrf}', endsection='^exit') self.assertIn(f'router isis {domain} vrf {vrf}', tmp) self.assertIn(f' net {net}', tmp) self.assertIn(f' advertise-high-metrics', tmp) @@ -141,7 +144,10 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): metric = '50' route_map = 'default-foo-' - self.isis_base_config() + self.cli_set(base_path + ['net', net]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + for afi in ['ipv4', 'ipv6']: for level in ['level-1', 'level-2']: self.cli_set(base_path + ['default-information', 'originate', afi, level, 'always']) @@ -152,7 +158,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) for afi in ['ipv4', 'ipv6']: @@ -160,11 +166,10 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): route_map_name = route_map + level + afi self.assertIn(f' default-information originate {afi} {level} always route-map {route_map_name} metric {metric}', tmp) - def test_isis_05_password(self): password = 'foo' - self.isis_base_config() + self.cli_set(base_path + ['net', net]) for interface in self._interfaces: self.cli_set(base_path + ['interface', interface, 'password', 'plaintext-password', f'{password}-{interface}']) @@ -187,13 +192,13 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' domain-password clear {password}', tmp) self.assertIn(f' area-password clear {password}', tmp) for interface in self._interfaces: - tmp = self.getFRRconfig(f'interface {interface}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' isis password clear {password}-{interface}', tmp) def test_isis_06_spf_delay_bfd(self): @@ -235,12 +240,12 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' spf-delay-ietf init-delay {init_delay} short-delay {short_delay} long-delay {long_delay} holddown {holddown} time-to-learn {time_to_learn}', tmp) for interface in self._interfaces: - tmp = self.getFRRconfig(f'interface {interface}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) self.assertIn(f' isis network {network}', tmp) @@ -252,7 +257,6 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): global_block_high = "399" local_block_low = "400" local_block_high = "499" - interface = 'lo' maximum_stack_size = '5' prefix_one = '192.168.0.1/32' prefix_two = '192.168.0.2/32' @@ -264,7 +268,9 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): prefix_four_value = '65000' self.cli_set(base_path + ['net', net]) - self.cli_set(base_path + ['interface', interface]) + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size]) self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low]) self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high]) @@ -283,7 +289,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' segment-routing on', tmp) self.assertIn(f' segment-routing global-block {global_block_low} {global_block_high} local-block {local_block_low} {local_block_high}', tmp) @@ -305,7 +311,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify main ISIS changes - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' mpls ldp-sync', tmp) self.assertIn(f' mpls ldp-sync holddown {holddown}', tmp) @@ -318,7 +324,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for interface in self._interfaces: # Verify interface changes for holddown - tmp = self.getFRRconfig(f'interface {interface}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', tmp) self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -332,7 +338,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for interface in self._interfaces: # Verify interface changes for disable - tmp = self.getFRRconfig(f'interface {interface}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', tmp) self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) @@ -355,7 +361,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'remote', 'prefix-list', prefix_list, level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute remote-lfa prefix-list {prefix_list} {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -365,7 +371,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'load-sharing', 'disable', level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute load-sharing disable {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -376,7 +382,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'priority-limit', priority, level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute priority-limit {priority} {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -388,7 +394,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for level in ['level-1', 'level-2']: self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'tiebreaker', tiebreaker, 'index', index, level]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' fast-reroute lfa tiebreaker {tiebreaker} index {index} {level}', tmp) self.cli_delete(base_path + ['fast-reroute']) @@ -408,7 +414,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): for topology in topologies: self.cli_set(base_path + ['topology', topology]) self.cli_commit() - tmp = self.getFRRconfig(f'router isis {domain}', daemon=isis_daemon) + tmp = self.getFRRconfig(f'router isis {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' topology {topology}', tmp) diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py index 88528973d..3840c24f4 100755 --- a/smoketest/scripts/cli/test_protocols_mpls.py +++ b/smoketest/scripts/cli/test_protocols_mpls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 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 @@ -17,6 +17,8 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.frrender import ldpd_daemon @@ -72,10 +74,11 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same cls.daemon_pid = process_named_running(ldpd_daemon) - # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) @@ -106,15 +109,86 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Validate configuration - frrconfig = self.getFRRconfig('mpls ldp', daemon=ldpd_daemon) + frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit') + self.assertIn(f'mpls ldp', frrconfig) + self.assertIn(f' router-id {router_id}', frrconfig) + + # Validate AFI IPv4 + afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv4', + endsubsection='^ exit-address-family') + self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config) + for interface in interfaces: + self.assertIn(f' interface {interface}', afiv4_config) + + def test_02_mpls_disable_establish_hello(self): + router_id = '1.2.3.4' + transport_ipv4_addr = '5.6.7.8' + transport_ipv6_addr = '2001:db8:1111::1111' + interfaces = Section.interfaces('ethernet') + + self.cli_set(base_path + ['router-id', router_id]) + + # At least one LDP interface must be configured + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'disable-establish-hello']) + + # LDP transport address missing + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr]) + self.cli_set(base_path + ['discovery', 'transport-ipv6-address', transport_ipv6_addr]) + + # Commit changes + self.cli_commit() + + # Validate configuration + frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit') self.assertIn(f'mpls ldp', frrconfig) self.assertIn(f' router-id {router_id}', frrconfig) # Validate AFI IPv4 - afiv4_config = self.getFRRconfig(' address-family ipv4', daemon=ldpd_daemon) + afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv4', + endsubsection='^ exit-address-family') self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config) for interface in interfaces: self.assertIn(f' interface {interface}', afiv4_config) + self.assertIn(f' disable-establish-hello', afiv4_config) + + # Validate AFI IPv6 + afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv6', + endsubsection='^ exit-address-family') + self.assertIn(f' discovery transport-address {transport_ipv6_addr}', afiv6_config) + for interface in interfaces: + self.assertIn(f' interface {interface}', afiv6_config) + self.assertIn(f' disable-establish-hello', afiv6_config) + + # Delete disable-establish-hello + for interface in interfaces: + self.cli_delete(base_path + ['interface', interface, 'disable-establish-hello']) + + # Commit changes + self.cli_commit() + + # Validate AFI IPv4 + afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv4', + endsubsection='^ exit-address-family') + # Validate AFI IPv6 + afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit', + substring=' address-family ipv6', + endsubsection='^ exit-address-family') + # Check deleted 'disable-establish-hello' option per interface + for interface in interfaces: + self.assertIn(f' interface {interface}', afiv4_config) + self.assertNotIn(f' disable-establish-hello', afiv4_config) + self.assertIn(f' interface {interface}', afiv6_config) + self.assertNotIn(f' disable-establish-hello', afiv6_config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py index 43ae4abf2..73a760945 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 @@ -17,14 +17,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.firewall import find_nftables_rule from vyos.utils.process import process_named_running -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 +39,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 +103,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_protocols_openfabric.py b/smoketest/scripts/cli/test_protocols_openfabric.py index f1372d7ab..323b6cd74 100644 --- a/smoketest/scripts/cli/test_protocols_openfabric.py +++ b/smoketest/scripts/cli/test_protocols_openfabric.py @@ -17,6 +17,8 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.utils.process import process_named_running from vyos.frrender import openfabric_daemon @@ -40,6 +42,8 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) @@ -75,14 +79,14 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' log-adjacency-changes', tmp) self.assertIn(f' set-overload-bit', tmp) self.assertIn(f' fabric-tier {fabric_tier}', tmp) self.assertIn(f' lsp-gen-interval {lsp_gen_interval}', tmp) - tmp = self.getFRRconfig(f'interface {dummy_if}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'interface {dummy_if}', endsection='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) self.assertIn(f' ipv6 router openfabric {domain}', tmp) @@ -101,12 +105,12 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR openfabric configuration - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f'router openfabric {domain}', tmp) self.assertIn(f' net {net}', tmp) # Verify interface configuration - tmp = self.getFRRconfig(f'interface {interface}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) # for lo interface 'openfabric passive' is implied self.assertIn(f' openfabric passive', tmp) @@ -137,11 +141,11 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f' net {net}', tmp) self.assertIn(f' domain-password clear {password}', tmp) - tmp = self.getFRRconfig(f'interface {dummy_if}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'interface {dummy_if}', endsection='^exit') self.assertIn(f' openfabric password clear {password}-{dummy_if}', tmp) def test_openfabric_multiple_domains(self): @@ -165,22 +169,21 @@ class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR openfabric configuration - tmp = self.getFRRconfig(f'router openfabric {domain}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'router openfabric {domain}', endsection='^exit') self.assertIn(f'router openfabric {domain}', tmp) self.assertIn(f' net {net}', tmp) - tmp = self.getFRRconfig(f'router openfabric {domain_2}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'router openfabric {domain_2}', endsection='^exit') self.assertIn(f'router openfabric {domain_2}', tmp) self.assertIn(f' net {net}', tmp) # Verify interface configuration - tmp = self.getFRRconfig(f'interface {dummy_if}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'interface {dummy_if}', endsection='^exit') self.assertIn(f' ip router openfabric {domain}', tmp) self.assertIn(f' ipv6 router openfabric {domain}', tmp) - tmp = self.getFRRconfig(f'interface {interface}', daemon=openfabric_daemon) + tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip router openfabric {domain_2}', tmp) - if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 599bf3930..ea55fa031 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.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 @@ -18,6 +18,7 @@ import unittest from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section @@ -45,6 +46,8 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME @classmethod def tearDownClass(cls): @@ -56,7 +59,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertNotIn(f'router ospf', frrconfig) # check process health and continuity @@ -68,7 +71,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults @@ -96,7 +99,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' compatible rfc1583', frrconfig) self.assertIn(f' auto-cost reference-bandwidth {bandwidth}', frrconfig) @@ -112,7 +115,6 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f' area 10 range 10.0.1.0/24 not-advertise', frrconfig) self.assertIn(f' area 10 range 10.0.2.0/24 not-advertise', frrconfig) - def test_ospf_03_access_list(self): acl = '100' seq = '10' @@ -128,14 +130,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults for ptotocol in protocols: self.assertIn(f' distribute-list {acl} out {ptotocol}', frrconfig) # defaults self.cli_delete(['policy', 'access-list', acl]) - def test_ospf_04_default_originate(self): seq = '100' metric = '50' @@ -149,7 +150,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -159,10 +160,9 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) - def test_ospf_05_options(self): global_distance = '128' intra_area = '100' @@ -201,7 +201,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' mpls-te on', frrconfig) self.assertIn(f' mpls-te router-address 0.0.0.0', frrconfig) # default @@ -224,7 +224,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['distance', 'ospf', 'inter-area', inter_area]) self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f' distance ospf intra-area {intra_area} inter-area {inter_area} external {external}', frrconfig) # https://github.com/FRRouting/frr/issues/17011 @@ -247,7 +247,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) for neighbor in neighbors: self.assertIn(f' neighbor {neighbor} priority {priority} poll-interval {poll_interval}', frrconfig) # default @@ -255,7 +255,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def test_ospf_07_redistribute(self): metric = '15' metric_type = '1' - redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static'] + redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static'] for protocol in redistribute: self.cli_set(base_path + ['redistribute', protocol, 'metric', metric]) @@ -266,7 +266,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) for protocol in redistribute: self.assertIn(f' redistribute {protocol} metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -294,7 +294,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' area {area} shortcut {shortcut}', frrconfig) self.assertIn(f' area {area} virtual-link {virtual_link} hello-interval {hello} retransmit-interval {retransmit} retransmit-window {window_default} transmit-delay {transmit} dead-interval {dead}', frrconfig) @@ -326,13 +326,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' passive-interface default', frrconfig) for interface in interfaces: # Can not use daemon for getFRRconfig() as bandwidth parameter belongs to zebra process - config = self.getFRRconfig(f'interface {interface}') + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf authentication-key {password}', config) self.assertIn(f' ip ospf bfd', config) @@ -350,7 +350,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=ospf_daemon) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertNotIn(f'interface {interface}', frrconfig) # There should be no OSPF related command at all under the interface self.assertNotIn(f' ip ospf', frrconfig) @@ -371,11 +371,11 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=ospf_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf area {area}', config) @@ -398,17 +398,17 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults - frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', daemon=ospf_daemon) + frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', endsection='^exit') self.assertIn(f'router ospf vrf {vrf}', frrconfig) self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=ospf_daemon) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertIn(f'interface {vrf_iface}', frrconfig) self.assertIn(f' ip ospf area {area}', frrconfig) @@ -418,7 +418,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=ospf_daemon) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertNotIn(f'interface {vrf_iface}', frrconfig) # There should be no OSPF related command at all under the interface self.assertNotIn(f' ip ospf', frrconfig) @@ -444,7 +444,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # default self.assertIn(f' network {network} area {area}', frrconfig) @@ -477,7 +477,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify all changes - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f' segment-routing on', frrconfig) self.assertIn(f' segment-routing global-block {global_block_low} {global_block_high} local-block {local_block_low} {local_block_high}', frrconfig) self.assertIn(f' segment-routing node-msd {maximum_stack_size}', frrconfig) @@ -495,7 +495,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify main OSPF changes - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) self.assertIn(f' mpls ldp-sync holddown {holddown}', frrconfig) @@ -508,7 +508,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # Verify interface changes for holddown - config = self.getFRRconfig(f'interface {interface}', daemon=ospf_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf dead-interval 40', config) self.assertIn(f' ip ospf mpls ldp-sync', config) @@ -522,7 +522,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: # Verify interface changes for disable - config = self.getFRRconfig(f'interface {interface}', daemon=ospf_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf dead-interval 40', config) self.assertNotIn(f' ip ospf mpls ldp-sync', config) @@ -544,7 +544,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit') self.assertIn(f'router ospf', frrconfig) self.assertIn(f' capability opaque', frrconfig) self.assertIn(f' graceful-restart grace-period {period}', frrconfig) @@ -570,7 +570,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf', endsection='^exit', daemon=ospf_daemon, empty_retry=60) + frrconfig = self.getFRRconfig('router ospf', endsection='^exit', empty_retry=60) self.assertIn(f'router ospf', frrconfig) self.assertIn(f' network {network} area {area1}', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py index d961a4fdc..5da4c7c98 100755 --- a/smoketest/scripts/cli/test_protocols_ospfv3.py +++ b/smoketest/scripts/cli/test_protocols_ospfv3.py @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section @@ -44,6 +45,8 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME @classmethod def tearDownClass(cls): @@ -54,7 +57,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertNotIn(f'router ospf6', frrconfig) # check process health and continuity @@ -81,7 +84,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' area {default_area} range {prefix}', frrconfig) self.assertIn(f' ospf6 router-id {router_id}', frrconfig) @@ -89,7 +92,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.assertIn(f' area {default_area} export-list {acl_name}', frrconfig) for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', daemon=ospf6_daemon) + if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'ipv6 ospf6 area {default_area}', if_config) self.cli_delete(['policy', 'access-list6', acl_name]) @@ -110,7 +113,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' distance {dist_global}', frrconfig) self.assertIn(f' distance ospf6 intra-area {dist_intra_area} inter-area {dist_inter_area} external {dist_external}', frrconfig) @@ -134,7 +137,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) for protocol in redistribute: self.assertIn(f' redistribute {protocol} metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -165,13 +168,13 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) cost = '100' priority = '10' for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', daemon=ospf6_daemon) + if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', if_config) self.assertIn(f' ipv6 ospf6 bfd', if_config) self.assertIn(f' ipv6 ospf6 bfd profile {bfd_profile}', if_config) @@ -188,7 +191,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - if_config = self.getFRRconfig(f'interface {interface}', daemon=ospf6_daemon) + if_config = self.getFRRconfig(f'interface {interface}', endsection='^exit') # There should be no OSPF6 configuration at all after interface removal self.assertNotIn(f' ipv6 ospf6', if_config) @@ -204,7 +207,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' area {area_stub} stub', frrconfig) self.assertIn(f' area {area_stub_nosum} stub no-summary', frrconfig) @@ -230,7 +233,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' area {area_nssa} nssa', frrconfig) self.assertIn(f' area {area_nssa_nosum} nssa default-information-originate no-summary', frrconfig) @@ -250,7 +253,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -259,7 +262,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) @@ -285,15 +288,15 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' ospf6 router-id {router_id}', frrconfig) - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertIn(f'interface {vrf_iface}', frrconfig) self.assertIn(f' ipv6 ospf6 bfd', frrconfig) - frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}', endsection='^exit') self.assertIn(f'router ospf6 vrf {vrf}', frrconfig) self.assertIn(f' ospf6 router-id {router_id_vrf}', frrconfig) @@ -303,7 +306,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # T5467: It must also be removed from FRR config - frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig(f'interface {vrf_iface}', endsection='^exit') self.assertNotIn(f'interface {vrf_iface}', frrconfig) # There should be no OSPF related command at all under the interface self.assertNotIn(f' ipv6 ospf6', frrconfig) @@ -329,7 +332,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf6', endsection='^exit', daemon=ospf6_daemon) + frrconfig = self.getFRRconfig('router ospf6', endsection='^exit') self.assertIn(f'router ospf6', frrconfig) self.assertIn(f' graceful-restart grace-period {period}', frrconfig) self.assertIn(f' graceful-restart helper planned-only', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_pim.py b/smoketest/scripts/cli/test_protocols_pim.py index 98b9d57aa..cc62769b3 100755 --- a/smoketest/scripts/cli/test_protocols_pim.py +++ b/smoketest/scripts/cli/test_protocols_pim.py @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.frrender import pim_daemon @@ -26,6 +27,16 @@ from vyos.utils.process import process_named_running base_path = ['protocols', 'pim'] class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsPIM, 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) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + def tearDown(self): # pimd process must be running self.assertTrue(process_named_running(pim_daemon)) @@ -57,11 +68,11 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR pimd configuration - frrconfig = self.getFRRconfig('router pim', endsection='^exit', daemon=pim_daemon) + frrconfig = self.getFRRconfig('router pim', endsection='^exit') self.assertIn(f' rp {rp} {group}', frrconfig) for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=pim_daemon) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' ip pim', frrconfig) self.assertIn(f' ip pim bfd', frrconfig) @@ -108,7 +119,7 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR pimd configuration - frrconfig = self.getFRRconfig('router pim', endsection='^exit', daemon=pim_daemon) + frrconfig = self.getFRRconfig('router pim', endsection='^exit') self.assertIn(f' no send-v6-secondary', frrconfig) self.assertIn(f' rp {rp} {group}', frrconfig) self.assertIn(f' register-suppress-time {register_suppress_time}', frrconfig) @@ -170,11 +181,11 @@ class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): self.cli_commit() - frrconfig = self.getFRRconfig(daemon=pim_daemon) + frrconfig = self.getFRRconfig() self.assertIn(f'ip igmp watermark-warn {watermark_warning}', frrconfig) for interface in interfaces: - frrconfig = self.getFRRconfig(f'interface {interface}', daemon=pim_daemon) + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', frrconfig) self.assertIn(f' ip igmp', frrconfig) self.assertIn(f' ip igmp version {version}', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py index f69eb4ae1..4ed8fcf7a 100755 --- a/smoketest/scripts/cli/test_protocols_pim6.py +++ b/smoketest/scripts/cli/test_protocols_pim6.py @@ -17,6 +17,8 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.frrender import pim6_daemon @@ -34,6 +36,8 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) @@ -52,7 +56,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=pim6_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertNotIn(f' ipv6 mld version 1', config) @@ -65,7 +69,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=pim6_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertIn(f' ipv6 mld version 1', config) @@ -88,7 +92,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=pim6_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld join-group ff18::1234', config) @@ -100,7 +104,7 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=pim6_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld join-group ff38::5678 2001:db8::5678', config) @@ -128,14 +132,14 @@ class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR pim6d configuration - config = self.getFRRconfig('router pim6', endsection='^exit', daemon=pim6_daemon) + config = self.getFRRconfig('router pim6', endsection='^exit') self.assertIn(f' join-prune-interval {join_prune_interval}', config) self.assertIn(f' keep-alive-timer {keep_alive_timer}', config) self.assertIn(f' packets {packets}', config) self.assertIn(f' register-suppress-time {register_suppress_time}', config) for interface in interfaces: - config = self.getFRRconfig(f'interface {interface}', daemon=pim6_daemon) + config = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ipv6 pim drpriority {dr_priority}', config) self.assertIn(f' ipv6 pim hello {hello}', config) self.assertIn(f' no ipv6 pim bsm', config) diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index 33706a9c9..27b543803 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 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 @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.ifconfig import Section from vyos.frrender import rip_daemon @@ -39,6 +40,8 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'source', 'any']) @@ -66,7 +69,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router rip') + frrconfig = self.getFRRconfig('router rip', endsection='^exit') self.assertNotIn(f'router rip', frrconfig) # check process health and continuity @@ -79,7 +82,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): interfaces = Section.interfaces('ethernet') neighbors = ['1.2.3.4', '1.2.3.5', '1.2.3.6'] networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] - redistribute = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'static'] + redistribute = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'static'] timer_garbage = '888' timer_timeout = '1000' timer_update = '90' @@ -116,7 +119,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ripd configuration - frrconfig = self.getFRRconfig('router rip') + frrconfig = self.getFRRconfig('router rip', endsection='^exit') self.assertIn(f'router rip', frrconfig) self.assertIn(f' distance {distance}', frrconfig) self.assertIn(f' default-information originate', frrconfig) @@ -175,10 +178,10 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('router rip') + frrconfig = self.getFRRconfig('router rip', endsection='^exit') self.assertIn(f'version {tx_version}', frrconfig) - frrconfig = self.getFRRconfig(f'interface {interface}') + frrconfig = self.getFRRconfig(f'interface {interface}', endsection='^exit') self.assertIn(f' ip rip receive version {rx_version}', frrconfig) self.assertIn(f' ip rip send version {tx_version}', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py index b10df9679..d2066b825 100755 --- a/smoketest/scripts/cli/test_protocols_ripng.py +++ b/smoketest/scripts/cli/test_protocols_ripng.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 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 @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.ifconfig import Section from vyos.frrender import ripng_daemon @@ -40,6 +41,8 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'source', 'any']) @@ -66,7 +69,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('router ripng') + frrconfig = self.getFRRconfig('router ripng', endsection='^exit') self.assertNotIn(f'router ripng', frrconfig) # check process health and continuity @@ -113,7 +116,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ripng') + frrconfig = self.getFRRconfig('router ripng', endsection='^exit') self.assertIn(f'router ripng', frrconfig) self.assertIn(f' default-information originate', frrconfig) self.assertIn(f' default-metric {metric}', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index 23738717a..0addf7fee 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.frrender import bgp_daemon @@ -111,12 +112,14 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) self.cli_commit() - frrconfig = self.getFRRconfig('rpki') + frrconfig = self.getFRRconfig('rpki', endsection='^exit') self.assertNotIn(f'rpki', frrconfig) # check process health and continuity @@ -153,7 +156,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('rpki') + frrconfig = self.getFRRconfig('rpki', endsection='^exit') self.assertIn(f'rpki expire_interval {expire_interval}', frrconfig) self.assertIn(f'rpki polling_period {polling_period}', frrconfig) self.assertIn(f'rpki retry_interval {retry_interval}', frrconfig) @@ -192,7 +195,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR configuration - frrconfig = self.getFRRconfig('rpki') + frrconfig = self.getFRRconfig('rpki', endsection='^exit') for cache_name, cache_config in cache.items(): port = cache_config['port'] preference = cache_config['preference'] @@ -245,5 +248,41 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() + def test_rpki_source_address(self): + peer = '192.0.2.1' + port = '8080' + preference = '1' + username = 'foo' + source_address = '100.10.10.1' + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', f'{source_address}/24']) + + # Configure a TCP cache server + self.cli_set(base_path + ['cache', peer, 'port', port]) + self.cli_set(base_path + ['cache', peer, 'preference', preference]) + self.cli_set(base_path + ['cache', peer, 'source-address', source_address]) + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('rpki') + self.assertIn(f'rpki cache tcp {peer} {port} source {source_address} preference {preference}', frrconfig) + + self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key.replace('\n', '')]) + self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub.replace('\n', '')]) + self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'type', rpki_key_type]) + + # Configure a SSH cache server + self.cli_set(base_path + ['cache', peer, 'ssh', 'username', username]) + self.cli_set(base_path + ['cache', peer, 'ssh', 'key', rpki_key_name]) + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('rpki') + self.assertIn( + f'rpki cache ssh {peer} {port} {username} /run/frr/id_rpki_{peer} /run/frr/id_rpki_{peer}.pub source {source_address} preference {preference}', + frrconfig, + ) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_segment-routing.py b/smoketest/scripts/cli/test_protocols_segment-routing.py index 624985476..94c808733 100755 --- a/smoketest/scripts/cli/test_protocols_segment-routing.py +++ b/smoketest/scripts/cli/test_protocols_segment-routing.py @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section @@ -26,6 +27,7 @@ from vyos.utils.system import sysctl_read base_path = ['protocols', 'segment-routing'] + class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -36,6 +38,8 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME def tearDown(self): self.cli_delete(base_path) @@ -47,14 +51,64 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): def test_srv6(self): interfaces = Section.interfaces('ethernet', vlan=False) locators = { - 'foo' : { 'prefix' : '2001:a::/64' }, - 'foo' : { 'prefix' : '2001:b::/64', 'usid' : {} }, + 'foo1': {'prefix': '2001:a::/64'}, + 'foo2': {'prefix': '2001:b::/64', 'usid': {}}, + 'foo3': {'prefix': '2001:c::/64', 'format': 'uncompressed-f4024'}, + 'foo4': { + 'prefix': '2001:d::/48', + 'block-len': '32', + 'node-len': '16', + 'func-bits': '16', + 'usid': {}, + 'format': 'usid-f3216', + }, } for locator, locator_config in locators.items(): - self.cli_set(base_path + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']]) + self.cli_set( + base_path + + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']] + ) + if 'block-len' in locator_config: + self.cli_set( + base_path + + [ + 'srv6', + 'locator', + locator, + 'block-len', + locator_config['block-len'], + ] + ) + if 'node-len' in locator_config: + self.cli_set( + base_path + + [ + 'srv6', + 'locator', + locator, + 'node-len', + locator_config['node-len'], + ] + ) + if 'func-bits' in locator_config: + self.cli_set( + base_path + + [ + 'srv6', + 'locator', + locator, + 'func-bits', + locator_config['func-bits'], + ] + ) if 'usid' in locator_config: self.cli_set(base_path + ['srv6', 'locator', locator, 'behavior-usid']) + if 'format' in locator_config: + self.cli_set( + base_path + + ['srv6', 'locator', locator, 'format', locator_config['format']] + ) # verify() - SRv6 should be enabled on at least one interface! with self.assertRaises(ConfigSessionError): @@ -65,16 +119,33 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '0') # default - - frrconfig = self.getFRRconfig(f'segment-routing', daemon=zebra_daemon) - self.assertIn(f'segment-routing', frrconfig) - self.assertIn(f' srv6', frrconfig) - self.assertIn(f' locators', frrconfig) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + ) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '0' + ) # default + + frrconfig = self.getFRRconfig('segment-routing', endsection='^exit') + self.assertIn('segment-routing', frrconfig) + self.assertIn(' srv6', frrconfig) + self.assertIn(' locators', frrconfig) for locator, locator_config in locators.items(): + prefix = locator_config['prefix'] + block_len = locator_config.get('block-len', '40') + node_len = locator_config.get('node-len', '24') + func_bits = locator_config.get('func-bits', '16') + self.assertIn(f' locator {locator}', frrconfig) - self.assertIn(f' prefix {locator_config["prefix"]} block-len 40 node-len 24 func-bits 16', frrconfig) + self.assertIn( + f' prefix {prefix} block-len {block_len} node-len {node_len} func-bits {func_bits}', + frrconfig, + ) + + if 'format' in locator_config: + self.assertIn(f' format {locator_config["format"]}', frrconfig) + if 'usid' in locator_config: + self.assertIn(' behavior usid', frrconfig) def test_srv6_sysctl(self): interfaces = Section.interfaces('ethernet', vlan=False) @@ -86,8 +157,12 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '-1') # ignore + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + ) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '-1' + ) # ignore # HMAC drop for interface in interfaces: @@ -96,8 +171,12 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.cli_commit() for interface in interfaces: - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1') - self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '1') # drop + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1' + ) + self.assertEqual( + sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '1' + ) # drop # Disable SRv6 on first interface first_if = interfaces[-1] @@ -106,5 +185,6 @@ class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): self.assertEqual(sysctl_read(f'net.ipv6.conf.{first_if}.seg6_enabled'), '0') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index a2cde0237..79d6b3af4 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.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,14 +14,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest +from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME from vyos.configsession import ConfigSessionError from vyos.template import is_ipv6 +from vyos.template import get_dhcp_router from vyos.utils.network import get_interface_config from vyos.utils.network import get_vrf_tableid +from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value base_path = ['protocols', 'static'] vrf_path = ['protocols', 'vrf'] @@ -163,16 +169,20 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestProtocolsStatic, cls).setUpClass() + cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) - cls.cli_set(cls, ['vrf', 'name', 'black', 'table', '43210']) + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME @classmethod def tearDownClass(cls): + cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) super(TestProtocolsStatic, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) + self.cli_delete(['vrf']) self.cli_commit() v4route = self.getFRRconfig('ip route', end='') @@ -181,6 +191,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertFalse(v6route) def test_01_static(self): + self.cli_set(['vrf', 'name', 'black', 'table', '43210']) for route, route_config in routes.items(): route_type = 'route' if is_ipv6(route): @@ -315,6 +326,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, frrconfig) def test_02_static_table(self): + self.cli_set(['vrf', 'name', 'black', 'table', '43210']) for table in tables: for route, route_config in routes.items(): route_type = 'route' @@ -409,6 +421,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): def test_03_static_vrf(self): + self.cli_set(['vrf', 'name', 'black', 'table', '43210']) # Create VRF instances and apply the static routes from above to FRR. # Re-read the configured routes and match them if they are programmed # properly. This also includes VRF leaking @@ -567,5 +580,44 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): else: self.assertIn(tmp, frrconfig) + def test_05_dhcp_default_route(self): + # When running via vyos-build under the QEmu environment a local DHCP + # server is available. This test verifies that the default route is set. + # When not running under the VyOS QEMU environment, this test is skipped. + if not os.path.exists('/tmp/vyos.smoketests.hint'): + self.skipTest('Not running under VyOS CI/CD QEMU environment!') + + interface = 'eth0' + interface_path = ['interfaces', 'ethernet', interface] + default_distance = default_value(interface_path + ['dhcp-options', 'default-route-distance']) + self.cli_set(interface_path + ['address', 'dhcp']) + self.cli_commit() + + # Wait for dhclient to receive IP address and default gateway + sleep(5) + + router = get_dhcp_router(interface) + frrconfig = self.getFRRconfig('') + self.assertIn(rf'ip route 0.0.0.0/0 {router} {interface} tag 210 {default_distance}', frrconfig) + + # T6991: Default route is missing when there is no "protocols static" + # CLI node entry + self.cli_delete(base_path) + # We can trigger a FRR reconfiguration and config re-rendering when + # we simply disable IPv6 forwarding + self.cli_set(['system', 'ipv6', 'disable-forwarding']) + self.cli_commit() + + # Re-check FRR configuration that default route is still present + frrconfig = self.getFRRconfig('') + self.assertIn(rf'ip route 0.0.0.0/0 {router} {interface} tag 210 {default_distance}', frrconfig) + + self.cli_delete(interface_path + ['address']) + self.cli_commit() + + # Wait for dhclient to stop + while process_named_running('dhclient', cmdline=interface, timeout=10): + sleep(0.250) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index f891bf295..e421f04d2 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# 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 @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import re import unittest from json import loads @@ -22,15 +23,21 @@ from json import loads from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.kea import kea_add_lease +from vyos.kea import kea_delete_lease +from vyos.utils.process import cmd from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.template import inc_ip from vyos.template import dec_ip PROCESS_NAME = 'kea-dhcp4' +D2_PROCESS_NAME = 'kea-dhcp-ddns' CTRL_PROCESS_NAME = 'kea-ctrl-agent' KEA4_CONF = '/run/kea/kea-dhcp4.conf' +KEA4_D2_CONF = '/run/kea/kea-dhcp-ddns.conf' KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket' +HOSTSD_CLIENT = '/usr/bin/vyos-hostsd-client' base_path = ['service', 'dhcp-server'] interface = 'dum8765' subnet = '192.0.2.0/25' @@ -39,15 +46,18 @@ dns_1 = inc_ip(subnet, 2) dns_2 = inc_ip(subnet, 3) domain_name = 'vyos.net' + class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServiceDHCPServer, cls).setUpClass() - # Clear out current configuration to allow running this test on a live system + # Clear out current configuration to allow running this test on a live system cls.cli_delete(cls, base_path) cidr_mask = subnet.split('/')[-1] - cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}']) + cls.cli_set( + cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'] + ) @classmethod def tearDownClass(cls): @@ -69,7 +79,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}') self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}') else: - assert False, "Invalid type" + assert False, 'Invalid type' current = current[key] @@ -88,19 +98,26 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertTrue(key in base_obj) self.assertEqual(base_obj[key], value) + def verify_service_running(self): + tmp = cmd('tail -n 100 /var/log/messages | grep kea') + self.assertTrue(process_named_running(PROCESS_NAME), msg=f'Service not running, log: {tmp}') + def test_dhcp_single_pool_range(self): shared_net_name = 'SMOKE-1' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_1_start = inc_ip(subnet, 40) - range_1_stop = inc_ip(subnet, 50) + range_1_stop = inc_ip(subnet, 50) self.cli_set(base_path + ['listen-interface', interface]) + self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check']) + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['ignore-client-id']) + self.cli_set(pool + ['ping-check']) # we use the first subnet IP address as default gateway self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['option', 'name-server', dns_1]) @@ -121,55 +138,90 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface]) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'match-client-id', False) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface] + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'match-client-id', False + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) + + # Verify ping-check + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'user-context'], + 'enable-ping-check', + True + ) + + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'user-context'], + 'enable-ping-check', + True + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_1_start} - {range_1_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_1_start} - {range_1_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_options(self): shared_net_name = 'SMOKE-0815' - range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) - smtp_server = '1.2.3.4' - time_server = '4.3.2.1' - tftp_server = 'tftp.vyos.io' - search_domains = ['foo.vyos.net', 'bar.vyos.net'] - bootfile_name = 'vyos' - bootfile_server = '192.0.2.1' - wpad = 'http://wpad.vyos.io/foo/bar' - server_identifier = bootfile_server + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + smtp_server = '1.2.3.4' + time_server = '4.3.2.1' + tftp_server = 'tftp.vyos.io' + search_domains = ['foo.vyos.net', 'bar.vyos.net'] + bootfile_name = 'vyos' + bootfile_server = '192.0.2.1' + wpad = 'http://wpad.vyos.io/foo/bar' + server_identifier = bootfile_server ipv6_only_preferred = '300' + capwap_access_controller = '192.168.2.125' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -189,8 +241,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['option', 'bootfile-server', bootfile_server]) self.cli_set(pool + ['option', 'wpad-url', wpad]) self.cli_set(pool + ['option', 'server-identifier', server_identifier]) + self.cli_set( + pool + ['option', 'capwap-controller', capwap_access_controller] + ) + + static_route = '10.0.0.0/24' + static_route_nexthop = '192.0.2.1' - self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) + self.cli_set( + pool + ['option', 'static-route', static_route, 'next-hop', static_route_nexthop] + ) self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred]) self.cli_set(pool + ['option', 'time-zone', 'Europe/London']) @@ -203,95 +263,133 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'boot-file-name', bootfile_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4'], + 'boot-file-name', + bootfile_name, + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4'], + 'next-server', + bootfile_server, + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-search', 'data': ', '.join(search_domains)}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-search', 'data': ', '.join(search_domains)}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'pop-server', 'data': smtp_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'pop-server', 'data': smtp_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'smtp-server', 'data': smtp_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'smtp-server', 'data': smtp_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'time-servers', 'data': time_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'time-servers', 'data': time_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'dhcp-server-identifier', 'data': server_identifier}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'dhcp-server-identifier', 'data': server_identifier}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'tftp-server-name', 'data': tftp_server}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'capwap-ac-v4', 'data': capwap_access_controller}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'wpad-url', 'data': wpad}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'tftp-server-name', 'data': tftp_server}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'wpad-url', 'data': wpad}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + { + 'name': 'classless-static-route', + 'data': f'{static_route} - {static_route_nexthop}, 0.0.0.0/0 - {router}', + }, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'v6-only-preferred', 'data': ipv6_only_preferred}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'v6-only-preferred', 'data': ipv6_only_preferred}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'ip-forwarding', 'data': "true"}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'ip-forwarding', 'data': 'true'}, + ) # Time zone self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'tcode', 'data': 'Europe/London'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'tcode', 'data': 'Europe/London'}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_options_scoped(self): shared_net_name = 'SMOKE-2' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_router = inc_ip(subnet, 5) range_dns_1 = inc_ip(subnet, 6) @@ -320,40 +418,58 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) # Verify shared-network options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify range options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], - {'name': 'routers', 'data': range_router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'routers', 'data': range_router}, + ) # Verify pool - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}') + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + 'pool', + f'{range_0_start} - {range_0_stop}', + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_single_pool_static_mapping(self): shared_net_name = 'SMOKE-2' @@ -375,18 +491,31 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): for client in ['client1', 'client2', 'client3']: mac = '00:50:00:00:00:{}'.format(client_base) self.cli_set(pool + ['static-mapping', client, 'mac', mac]) - self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]) + self.cli_set( + pool + + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)] + ) client_base += 1 # cannot have both mac-address and duid set with self.assertRaises(ConfigSessionError): - self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11']) + self.cli_set( + pool + + [ + 'static-mapping', + 'client1', + 'duid', + '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11', + ] + ) self.cli_commit() self.cli_delete(pool + ['static-mapping', 'client1', 'duid']) # cannot have mappings with duplicate IP addresses self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:fe:ff']) - self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)]) + self.cli_set( + pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)] + ) with self.assertRaises(ConfigSessionError): self.cli_commit() # Should allow disabled duplicate @@ -396,17 +525,38 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # cannot have mappings with duplicate MAC addresses self.cli_set(pool + ['static-mapping', 'dupe2', 'mac', '00:50:00:00:00:10']) - self.cli_set(pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)]) + self.cli_set( + pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)] + ) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(pool + ['static-mapping', 'dupe2']) - # cannot have mappings with duplicate MAC addresses - self.cli_set(pool + ['static-mapping', 'dupe3', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01']) - self.cli_set(pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)]) - self.cli_set(pool + ['static-mapping', 'dupe4', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01']) - self.cli_set(pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)]) + self.cli_set( + pool + + [ + 'static-mapping', + 'dupe3', + 'duid', + '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01', + ] + ) + self.cli_set( + pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)] + ) + self.cli_set( + pool + + [ + 'static-mapping', + 'dupe4', + 'duid', + '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01', + ] + ) + self.cli_set( + pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)] + ) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(pool + ['static-mapping', 'dupe3']) @@ -418,25 +568,38 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400 + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400 + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) client_base = 10 for client in ['client1', 'client2', 'client3']: @@ -444,14 +607,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ip = inc_ip(subnet, client_base) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'], - {'hostname': client, 'hw-address': mac, 'ip-address': ip}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'], + {'hostname': client, 'hw-address': mac, 'ip-address': ip}, + ) client_base += 1 # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_multiple_pools(self): lease_time = '14400' @@ -463,11 +627,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): dns_1 = inc_ip(subnet, 2) range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_1_start = inc_ip(subnet, 30) - range_1_stop = inc_ip(subnet, 40) - - pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + range_1_stop = inc_ip(subnet, 40) + + pool = base_path + [ + 'shared-network-name', + shared_net_name, + 'subnet', + subnet, + ] self.cli_set(pool + ['subnet-id', str(int(network) + 1)]) # we use the first subnet IP address as default gateway self.cli_set(pool + ['option', 'default-router', router]) @@ -484,7 +653,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): for client in ['client1', 'client2', 'client3', 'client4']: mac = '02:50:00:00:00:{}'.format(client_base) self.cli_set(pool + ['static-mapping', client, 'mac', mac]) - self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]) + self.cli_set( + pool + + [ + 'static-mapping', + client, + 'ip-address', + inc_ip(subnet, client_base), + ] + ) client_base += 1 # commit changes @@ -500,37 +677,64 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): dns_1 = inc_ip(subnet, 2) range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) range_1_start = inc_ip(subnet, 30) - range_1_stop = inc_ip(subnet, 40) + range_1_stop = inc_ip(subnet, 40) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'id', int(network) + 1) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time)) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time)) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'subnet', + subnet, + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'id', + int(network) + 1, + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'valid-lifetime', + int(lease_time), + ) + self.verify_config_value( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4'], + 'max-valid-lifetime', + int(lease_time), + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], - {'name': 'domain-name', 'data': domain_name}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], - {'name': 'domain-name-servers', 'data': dns_1}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': dns_1}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], - {'pool': f'{range_1_start} - {range_1_stop}'}) + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], + {'pool': f'{range_1_start} - {range_1_stop}'}, + ) client_base = 60 for client in ['client1', 'client2', 'client3', 'client4']: @@ -538,20 +742,28 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ip = inc_ip(subnet, client_base) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'], - {'hostname': client, 'hw-address': mac, 'ip-address': ip}) + obj, + [ + 'Dhcp4', + 'shared-networks', + int(network), + 'subnet4', + 0, + 'reservations', + ], + {'hostname': client, 'hw-address': mac, 'ip-address': ip}, + ) client_base += 1 # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_exclude_not_in_range(self): # T3180: verify else path when slicing DHCP ranges and exclude address # is not part of the DHCP range range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -567,38 +779,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST') - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST' + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) pool_obj = { 'pool': f'{range_0_start} - {range_0_stop}', - 'option-data': [{'name': 'routers', 'data': router}] + 'option-data': [{'name': 'routers', 'data': router}], } # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - pool_obj) + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_exclude_in_range(self): # T3180: verify else path when slicing DHCP ranges and exclude address # is not part of the DHCP range range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 100) + range_0_stop = inc_ip(subnet, 100) # the DHCP exclude addresse is blanked out of the range which is done # by slicing one range into two ranges - exclude_addr = inc_ip(range_0_start, 20) + exclude_addr = inc_ip(range_0_start, 20) range_0_stop_excl = dec_ip(exclude_addr, 1) range_0_start_excl = inc_ip(exclude_addr, 1) @@ -616,37 +832,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2') - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2' + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) pool_obj = { 'pool': f'{range_0_start} - {range_0_stop_excl}', - 'option-data': [{'name': 'routers', 'data': router}] + 'option-data': [{'name': 'routers', 'data': router}], } pool_exclude_obj = { 'pool': f'{range_0_start_excl} - {range_0_stop}', - 'option-data': [{'name': 'routers', 'data': router}] + 'option-data': [{'name': 'routers', 'data': router}], } # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - pool_obj) + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj + ) self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - pool_exclude_obj) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + pool_exclude_obj, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_relay_server(self): # Listen on specific address and return DHCP leases from a non @@ -657,7 +878,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): relay_router = inc_ip(relay_subnet, 1) range_0_start = '10.0.1.0' - range_0_stop = '10.0.250.255' + range_0_stop = '10.0.250.255' pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -671,31 +892,37 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}']) + self.verify_config_value( + obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}'] + ) self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY') - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': relay_router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': relay_router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.verify_service_running() def test_dhcp_high_availability(self): shared_net_name = 'FAILOVER' failover_name = 'VyOS-Failover' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -712,7 +939,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): failover_local = router failover_remote = inc_ip(router, 1) - self.cli_set(base_path + ['high-availability', 'source-address', failover_local]) + self.cli_set( + base_path + ['high-availability', 'source-address', failover_local] + ) self.cli_set(base_path + ['high-availability', 'name', failover_name]) self.cli_set(base_path + ['high-availability', 'remote', failover_remote]) self.cli_set(base_path + ['high-availability', 'status', 'primary']) @@ -725,43 +954,78 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): obj = loads(config) # Verify failover - self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL) + self.verify_config_value( + obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True}) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': os.uname()[1], + 'url': f'http://{failover_local}:647/', + 'role': 'primary', + 'auto-failover': True, + }, + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'secondary', 'auto-failover': True}) - - self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) - self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': failover_name, + 'url': f'http://{failover_remote}:647/', + 'role': 'secondary', + 'auto-failover': True, + }, + ) + + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) # Verify options self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) # Verify pools self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - self.assertTrue(process_named_running(CTRL_PROCESS_NAME)) + self.verify_service_running() def test_dhcp_high_availability_standby(self): shared_net_name = 'FAILOVER' failover_name = 'VyOS-Failover' range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['subnet-id', '1']) @@ -774,7 +1038,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): failover_local = router failover_remote = inc_ip(router, 1) - self.cli_set(base_path + ['high-availability', 'source-address', failover_local]) + self.cli_set( + base_path + ['high-availability', 'source-address', failover_local] + ) self.cli_set(base_path + ['high-availability', 'name', failover_name]) self.cli_set(base_path + ['high-availability', 'remote', failover_remote]) self.cli_set(base_path + ['high-availability', 'status', 'secondary']) @@ -787,61 +1053,383 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): obj = loads(config) # Verify failover - self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL) + self.verify_config_value( + obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'standby', 'auto-failover': True}) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': os.uname()[1], + 'url': f'http://{failover_local}:647/', + 'role': 'standby', + 'auto-failover': True, + }, + ) self.verify_config_object( obj, - ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], - {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'primary', 'auto-failover': True}) + [ + 'Dhcp4', + 'hooks-libraries', + 0, + 'parameters', + 'high-availability', + 0, + 'peers', + ], + { + 'name': failover_name, + 'url': f'http://{failover_remote}:647/', + 'role': 'primary', + 'auto-failover': True, + }, + ) + + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}, + ) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}, + ) + + # Check for running process + self.verify_service_running() + + def test_dhcp_dynamic_dns_update(self): + shared_net_name = 'SMOKE-1DDNS' + + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + + self.cli_set(base_path + ['listen-interface', interface]) + + ddns = base_path + ['dynamic-dns-update'] + + self.cli_set(ddns + ['send-updates', 'enable']) + self.cli_set(ddns + ['conflict-resolution', 'enable']) + self.cli_set(ddns + ['override-no-update', 'enable']) + self.cli_set(ddns + ['override-client-update', 'enable']) + self.cli_set(ddns + ['replace-client-name', 'always']) + self.cli_set(ddns + ['update-on-renew', 'enable']) + + self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'algorithm', 'sha256']) + self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'secret', 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ==']) + self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'algorithm', 'sha256']) + self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'secret', 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ==']) + self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '1', 'address', '192.168.0.1']) + self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '2', 'address', '100.100.0.1']) + self.cli_set(ddns + ['forward-domain', 'domain.lan', 'key-name', 'domain-lan-updates']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'address', '192.168.0.1']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'port', '1053']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'address', '100.100.0.1']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'port', '1153']) + self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'key-name', 'reverse-0-168-192']) + + shared = base_path + ['shared-network-name', shared_net_name] + + self.cli_set(shared + ['dynamic-dns-update', 'send-updates', 'enable']) + self.cli_set(shared + ['dynamic-dns-update', 'conflict-resolution', 'enable']) + self.cli_set(shared + ['dynamic-dns-update', 'ttl-percent', '75']) + + pool = shared + [ 'subnet', subnet] + self.cli_set(pool + ['subnet-id', '1']) + + self.cli_set(pool + ['range', '0', 'start', range_0_start]) + self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) + + self.cli_set(pool + ['dynamic-dns-update', 'send-updates', 'enable']) + self.cli_set(pool + ['dynamic-dns-update', 'generated-prefix', 'myfunnyprefix']) + self.cli_set(pool + ['dynamic-dns-update', 'qualifying-suffix', 'suffix.lan']) + self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-set', 'xXyYzZ']) + self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-replacement', '_xXx_']) + + self.cli_commit() + + config = read_file(KEA4_CONF) + d2_config = read_file(KEA4_D2_CONF) + + obj = loads(config) + d2_obj = loads(d2_config) + + # Verify global DDNS parameters in the main config file + self.verify_config_value( + obj, + ['Dhcp4'], 'dhcp-ddns', + {'enable-updates': True, 'server-ip': '127.0.0.1', 'server-port': 53001, 'sender-ip': '', 'sender-port': 0, + 'max-queue-size': 1024, 'ncr-protocol': 'UDP', 'ncr-format': 'JSON'}) + + self.verify_config_value(obj, ['Dhcp4'], 'ddns-send-updates', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-use-conflict-resolution', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-no-update', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-client-update', True) + self.verify_config_value(obj, ['Dhcp4'], 'ddns-replace-client-name', 'always') + self.verify_config_value(obj, ['Dhcp4'], 'ddns-update-on-renew', True) + + # Verify scoped DDNS parameters in the main config file self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-send-updates', True) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-use-conflict-resolution', True) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-ttl-percent', 0.75) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-send-updates', True) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-generated-prefix', 'myfunnyprefix') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-qualifying-suffix', 'suffix.lan') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-set', 'xXyYzZ') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-replacement', '_xXx_') - # Verify options + # Verify keys and domains configuration in the D2 config self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], - {'name': 'routers', 'data': router}) + d2_obj, + ['DhcpDdns', 'tsig-keys'], + {'name': 'domain-lan-updates', 'algorithm': 'HMAC-SHA256', 'secret': 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='} + ) + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'tsig-keys'], + {'name': 'reverse-0-168-192', 'algorithm': 'HMAC-SHA256', 'secret': 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='} + ) - # Verify pools + self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'name', 'domain.lan') + self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'key-name', 'domain-lan-updates') self.verify_config_object( - obj, - ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + d2_obj, + ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '192.168.0.1'} + ) + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '100.100.0.1'} + ) + + self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'name', '0.168.192.in-addr.arpa') + self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'key-name', 'reverse-0-168-192') + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '192.168.0.1', 'port': 1053} + ) + self.verify_config_object( + d2_obj, + ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'], + {'ip-address': '100.100.0.1', 'port': 1153} + ) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - self.assertTrue(process_named_running(CTRL_PROCESS_NAME)) + self.assertTrue(process_named_running(D2_PROCESS_NAME)) def test_dhcp_on_interface_with_vrf(self): self.cli_set(['interfaces', 'ethernet', 'eth1', 'address', '10.1.1.1/30']) self.cli_set(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP']) - self.cli_set(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf', 'SMOKE-DHCP']) - self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'protocols', 'static', 'route', '10.1.10.0/24', 'next-hop', '10.1.1.2']) + self.cli_set( + [ + 'protocols', + 'static', + 'route', + '10.1.10.0/24', + 'interface', + 'eth1', + 'vrf', + 'SMOKE-DHCP', + ] + ) + self.cli_set( + [ + 'vrf', + 'name', + 'SMOKE-DHCP', + 'protocols', + 'static', + 'route', + '10.1.10.0/24', + 'next-hop', + '10.1.1.2', + ] + ) self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'table', '1000']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'subnet-id', '1']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'default-router', '10.1.10.1']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'name-server', '1.1.1.1']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'start', '10.1.10.10']) - self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'stop', '10.1.10.20']) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'subnet-id', + '1', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'option', + 'default-router', + '10.1.10.1', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'option', + 'name-server', + '1.1.1.1', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'range', + '1', + 'start', + '10.1.10.10', + ] + ) + self.cli_set( + base_path + + [ + 'shared-network-name', + 'SMOKE-DHCP-NETWORK', + 'subnet', + '10.1.10.0/24', + 'range', + '1', + 'stop', + '10.1.10.20', + ] + ) self.cli_set(base_path + ['listen-address', '10.1.1.1']) self.cli_commit() config = read_file(KEA4_CONF) obj = loads(config) - self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1']) + self.verify_config_value( + obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1'] + ) self.cli_delete(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP']) - self.cli_delete(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf']) + self.cli_delete( + ['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf'] + ) self.cli_delete(['vrf', 'name', 'SMOKE-DHCP']) self.cli_commit() + def test_dhcp_hostsd_lease_sync(self): + shared_net_name = 'SMOKE-LEASE-SYNC' + domain_name = 'sync.private' + + client_range = range(1, 4) + subnet_range_start = inc_ip(subnet, 10) + subnet_range_stop = inc_ip(subnet, 20) + + def internal_cleanup(): + for seq in client_range: + ip_addr = inc_ip(subnet, seq) + kea_delete_lease(4, ip_addr) + cmd( + f'{HOSTSD_CLIENT} --delete-hosts --tag dhcp-server-{ip_addr} --apply' + ) + + self.addClassCleanup(internal_cleanup) + + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + self.cli_set(pool + ['subnet-id', '1']) + self.cli_set(pool + ['option', 'domain-name', domain_name]) + self.cli_set(pool + ['range', '0', 'start', subnet_range_start]) + self.cli_set(pool + ['range', '0', 'stop', subnet_range_stop]) + + # commit changes + self.cli_commit() + + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet + ) + self.verify_config_value( + obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1 + ) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}, + ) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{subnet_range_start} - {subnet_range_stop}'}, + ) + + # Check for running process + self.verify_service_running() + + # All up and running, now test vyos-hostsd store + + # 1. Inject leases into kea + for seq in client_range: + client = f'client{seq}' + mac = f'00:50:00:00:00:{seq:02}' + ip = inc_ip(subnet, seq) + kea_add_lease(4, ip, host_name=client, mac_address=mac) + + # 2. Verify that leases are not available in vyos-hostsd + tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}') + host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}') + self.assertFalse(host_json.strip('{}')) + + # 3. Restart the service to trigger vyos-hostsd sync and wait for it to start + self.assertTrue(process_named_running(PROCESS_NAME, timeout=30)) + + # 4. Verify that leases are synced and available in vyos-hostsd + tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}') + host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}') + self.assertTrue(host_json) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 6ecf6c1cf..6535ca72d 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -108,6 +108,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['lease-time', 'default', lease_time]) self.cli_set(pool + ['lease-time', 'maximum', max_lease_time]) self.cli_set(pool + ['lease-time', 'minimum', min_lease_time]) + self.cli_set(pool + ['option', 'capwap-controller', dns_1]) self.cli_set(pool + ['option', 'name-server', dns_1]) self.cli_set(pool + ['option', 'name-server', dns_2]) self.cli_set(pool + ['option', 'name-server', dns_2]) @@ -157,6 +158,10 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.verify_config_object( obj, ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'capwap-ac-v6', 'data': dns_1}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'}) self.verify_config_object( obj, diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 28380f6d4..522102e67 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -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' @@ -38,13 +41,17 @@ ttl = '300' interface = 'eth0' class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): - def setUp(self): - # Always start with a clean CLI instance - self.cli_delete(base_path) + @classmethod + def setUpClass(cls): + super(TestServiceDDNS, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) def tearDown(self): # Check for running process - self.assertTrue(process_named_running(DDCLIENT_PNAME, timeout=5)) + self.assertTrue(process_named_running(DDCLIENT_PNAME, timeout=10)) # Delete DDNS configuration self.cli_delete(base_path) @@ -91,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] @@ -136,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) @@ -146,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}, @@ -335,9 +347,10 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check for process in VRF - systemd_override = cmd(f'cat {DDCLIENT_SYSTEMD_UNIT}') - self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient -file {DDCLIENT_CONF}', - systemd_override) + systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT) + self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient ' \ + 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_https.py b/smoketest/scripts/cli/test_service_https.py index 04c4a2e51..b4fe35d81 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -16,6 +16,7 @@ import unittest import json +import psutil from requests import request from urllib3.exceptions import InsecureRequestWarning @@ -113,6 +114,29 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # Check for stopped process self.assertFalse(process_named_running(PROCESS_NAME)) + def test_listen_address(self): + test_prefix = ['192.0.2.1/26', '2001:db8:1::ffff/64'] + test_addr = [ i.split('/')[0] for i in test_prefix ] + for i, addr in enumerate(test_prefix): + self.cli_set(['interfaces', 'dummy', f'dum{i}', 'address', addr]) + + key = 'MySuperSecretVyOS' + self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + # commit base config first, for testing update of listen-address + self.cli_commit() + + for addr in test_addr: + self.cli_set(base_path + ['listen-address', addr]) + self.cli_commit() + + res = set() + t = psutil.net_connections(kind="tcp") + for c in t: + if c.laddr.port == 443: + res.add(c.laddr.ip) + + self.assertEqual(res, set(test_addr)) + def test_certificate(self): cert_name = 'test_https' dh_name = 'dh-test' diff --git a/smoketest/scripts/cli/test_service_ids_ddos-protection.py b/smoketest/scripts/cli/test_service_ids_ddos-protection.py deleted file mode 100755 index 91b056eea..000000000 --- a/smoketest/scripts/cli/test_service_ids_ddos-protection.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2022 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -import unittest - -from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.configsession import ConfigSessionError -from vyos.utils.process import process_named_running -from vyos.utils.file import read_file - -PROCESS_NAME = 'fastnetmon' -FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf' -NETWORKS_CONF = '/run/fastnetmon/networks_list' -EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list' -base_path = ['service', 'ids', 'ddos-protection'] - -class TestServiceIDS(VyOSUnitTestSHIM.TestCase): - @classmethod - def setUpClass(cls): - super(TestServiceIDS, cls).setUpClass() - - # ensure we can also run this test on a live system - so lets clean - # out the current configuration :) - cls.cli_delete(cls, base_path) - - def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - # delete test config - self.cli_delete(base_path) - self.cli_commit() - - self.assertFalse(os.path.exists(FASTNETMON_CONF)) - self.assertFalse(process_named_running(PROCESS_NAME)) - - def test_fastnetmon(self): - networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64'] - excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128'] - interfaces = ['eth0', 'eth1'] - fps = '3500' - mbps = '300' - pps = '60000' - - self.cli_set(base_path + ['mode', 'mirror']) - # Required network! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in networks: - self.cli_set(base_path + ['network', tmp]) - - # optional excluded-network! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in excluded_networks: - self.cli_set(base_path + ['excluded-network', tmp]) - - # Required interface(s)! - with self.assertRaises(ConfigSessionError): - self.cli_commit() - for tmp in interfaces: - self.cli_set(base_path + ['listen-interface', tmp]) - - self.cli_set(base_path + ['direction', 'in']) - self.cli_set(base_path + ['threshold', 'general', 'fps', fps]) - self.cli_set(base_path + ['threshold', 'general', 'pps', pps]) - self.cli_set(base_path + ['threshold', 'general', 'mbps', mbps]) - - # commit changes - self.cli_commit() - - # Check configured port - config = read_file(FASTNETMON_CONF) - self.assertIn(f'mirror_afpacket = on', config) - self.assertIn(f'process_incoming_traffic = on', config) - self.assertIn(f'process_outgoing_traffic = off', config) - self.assertIn(f'ban_for_flows = on', config) - self.assertIn(f'threshold_flows = {fps}', config) - self.assertIn(f'ban_for_bandwidth = on', config) - self.assertIn(f'threshold_mbps = {mbps}', config) - self.assertIn(f'ban_for_pps = on', config) - self.assertIn(f'threshold_pps = {pps}', config) - # default - self.assertIn(f'enable_ban = on', config) - self.assertIn(f'enable_ban_ipv6 = on', config) - self.assertIn(f'ban_time = 1900', config) - - tmp = ','.join(interfaces) - self.assertIn(f'interfaces = {tmp}', config) - - - network_config = read_file(NETWORKS_CONF) - for tmp in networks: - self.assertIn(f'{tmp}', network_config) - - excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF) - for tmp in excluded_networks: - self.assertIn(f'{tmp}', excluded_network_config) - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py index ab0898d17..3b3c205cd 100755 --- a/smoketest/scripts/cli/test_service_ipoe-server.py +++ b/smoketest/scripts/cli/test_service_ipoe-server.py @@ -260,7 +260,7 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" tmp = ','.join(vlans) self.assertIn(f'{interface},{tmp}', conf['ipoe']['vlan-mon']) - def test_ipoe_server_static_client_ip(self): + def test_ipoe_server_static_client_ip_address(self): mac_address = '08:00:27:2f:d8:06' ip_address = '192.0.2.100' @@ -274,7 +274,7 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" interface, 'mac', mac_address, - 'static-ip', + 'ip-address', ip_address, ] ) @@ -295,6 +295,28 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" tmp = re.findall(regex, tmp) self.assertTrue(tmp) + def test_ipoe_server_start_session(self): + start_session = 'auto' + + # Configuration of local authentication for PPPoE server + self.basic_config() + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + # if 'start-session' option is not set the default value is 'dhcp' + self.assertIn(f'start=dhcpv4', conf['ipoe']['interface']) + + # change 'start-session' option to 'auto' + self.set(['interface', interface, 'start-session', start_session]) + self.cli_commit() + + # Validate changed configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + self.assertIn(f'start={start_session}', conf['ipoe']['interface']) + @unittest.skip("PPP is not a part of IPoE") def test_accel_ppp_options(self): pass diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py index 9d72ef78f..c73707e0d 100755 --- a/smoketest/scripts/cli/test_service_lldp.py +++ b/smoketest/scripts/cli/test_service_lldp.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 @@ -117,6 +117,8 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): config = read_file(LLDPD_CONF) self.assertIn(f'configure ports {interface} med location elin "{elin}"', config) + # This is the CLI default mode + self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config) self.assertIn(f'configure system interface pattern "{interface}"', config) def test_06_lldp_snmp(self): @@ -134,5 +136,50 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): self.cli_delete(['service', 'snmp']) + def test_07_lldp_interface_mode(self): + interfaces = Section.interfaces('ethernet', vlan=False) + + # set interface mode to 'tx' + self.cli_set(base_path + ['interface', 'all']) + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'disable']) + # commit changes + self.cli_commit() + + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status disable', config) + + # Change configuration to rx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'rx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status rx-only', config) + + # Change configuration to tx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'tx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status tx-only', config) + + # Change configuration to rx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'rx-tx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config) + if __name__ == '__main__': unittest.main(verbosity=2) 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_service_monitoring_prometheus.py b/smoketest/scripts/cli/test_service_monitoring_prometheus.py index dae103e4b..6e7f8c808 100755 --- a/smoketest/scripts/cli/test_service_monitoring_prometheus.py +++ b/smoketest/scripts/cli/test_service_monitoring_prometheus.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest from base_vyostest_shim import VyOSUnitTestSHIM @@ -22,12 +23,14 @@ from vyos.utils.file import read_file NODE_EXPORTER_PROCESS_NAME = 'node_exporter' FRR_EXPORTER_PROCESS_NAME = 'frr_exporter' +BLACKBOX_EXPORTER_PROCESS_NAME = 'blackbox_exporter' base_path = ['service', 'monitoring', 'prometheus'] listen_if = 'dum3421' listen_ip = '192.0.2.1' node_exporter_service_file = '/etc/systemd/system/node_exporter.service' frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' +blackbox_exporter_service_file = '/etc/systemd/system/blackbox_exporter.service' class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): @@ -53,6 +56,7 @@ class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): def test_01_node_exporter(self): self.cli_set(base_path + ['node-exporter', 'listen-address', listen_ip]) + self.cli_set(base_path + ['node-exporter', 'collectors', 'textfile']) # commit changes self.cli_commit() @@ -60,6 +64,11 @@ class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): file_content = read_file(node_exporter_service_file) self.assertIn(f'{listen_ip}:9100', file_content) + self.assertTrue(os.path.isdir('/run/node_exporter/collector')) + self.assertIn( + '--collector.textfile.directory=/run/node_exporter/collector', file_content + ) + # Check for running process self.assertTrue(process_named_running(NODE_EXPORTER_PROCESS_NAME)) @@ -75,6 +84,78 @@ class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(FRR_EXPORTER_PROCESS_NAME)) + def test_03_blackbox_exporter(self): + self.cli_set(base_path + ['blackbox-exporter', 'listen-address', listen_ip]) + + # commit changes + self.cli_commit() + + file_content = read_file(blackbox_exporter_service_file) + self.assertIn(f'{listen_ip}:9115', file_content) + + # Check for running process + self.assertTrue(process_named_running(BLACKBOX_EXPORTER_PROCESS_NAME)) + + def test_04_blackbox_exporter_with_config(self): + self.cli_set(base_path + ['blackbox-exporter', 'listen-address', listen_ip]) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'dns', + 'name', + 'dns_ip4', + 'preferred-ip-protocol', + 'ipv4', + ] + ) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'dns', + 'name', + 'dns_ip4', + 'query-name', + 'vyos.io', + ] + ) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'dns', + 'name', + 'dns_ip4', + 'query-type', + 'A', + ] + ) + self.cli_set( + base_path + + [ + 'blackbox-exporter', + 'modules', + 'icmp', + 'name', + 'icmp_ip6', + 'preferred-ip-protocol', + 'ipv6', + ] + ) + + # commit changes + self.cli_commit() + + file_content = read_file(blackbox_exporter_service_file) + self.assertIn(f'{listen_ip}:9115', file_content) + + # Check for running process + self.assertTrue(process_named_running(BLACKBOX_EXPORTER_PROCESS_NAME)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 6dbb6add4..33ce93ef4 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -252,6 +252,118 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('AdvIntervalOpt') self.assertEqual(tmp, 'off') + def test_auto_ignore(self): + isp_prefix = '2001:db8::/64' + ula_prefixes = ['fd00::/64', 'fd01::/64'] + + # configure wildcard prefix + self.cli_set(base_path + ['prefix', '::/64']) + + # test auto-ignore CLI behaviors with no prefix overrides + # set auto-ignore for all three prefixes + self.cli_set(base_path + ['auto-ignore', isp_prefix]) + + for ula_prefix in ula_prefixes: + self.cli_set(base_path + ['auto-ignore', ula_prefix]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # ensure autoignoreprefixes block is generated in config file + tmp = f'autoignoreprefixes' + ' {' + self.assertIn(tmp, config) + + # ensure all three prefixes are contained in the block + self.assertIn(f' {isp_prefix};', config) + for ula_prefix in ula_prefixes: + self.assertIn(f' {ula_prefix};', config) + + # remove a prefix and verify it's gone + self.cli_delete(base_path + ['auto-ignore', ula_prefixes[1]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + self.assertNotIn(f' {ula_prefixes[1]};', config) + + # ensure remaining two prefixes are still present + self.assertIn(f' {ula_prefixes[0]};', config) + self.assertIn(f' {isp_prefix};', config) + + # remove the remaining two prefixes and verify the config block is gone + self.cli_delete(base_path + ['auto-ignore', ula_prefixes[0]]) + self.cli_delete(base_path + ['auto-ignore', isp_prefix]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + tmp = f'autoignoreprefixes' + ' {' + self.assertNotIn(tmp, config) + + # test wildcard prefix overrides, with and without auto-ignore CLI configuration + newline = '\n' + left_curly = '{' + right_curly = '}' + + # override ULA prefixes + for ula_prefix in ula_prefixes: + self.cli_set(base_path + ['prefix', ula_prefix]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # ensure autoignoreprefixes block is generated in config file with both prefixes + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # remove a ULA prefix and ensure there is only one prefix in the config block + self.cli_delete(base_path + ['prefix', ula_prefixes[0]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # ensure autoignoreprefixes block is generated in config file with only one prefix + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # exclude a prefix with auto-ignore CLI syntax + self.cli_set(base_path + ['auto-ignore', ula_prefixes[0]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # verify that both prefixes appear in config block once again + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # override first ULA prefix again + # first ULA is auto-ignored in CLI, it must appear only once in config + self.cli_set(base_path + ['prefix', ula_prefixes[0]]) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # verify that both prefixes appear uniquely + tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};' + self.assertIn(tmp, config) + + # remove wildcard prefix and verify config block is gone + self.cli_delete(base_path + ['prefix', '::/64']) + + # commit and reload config + self.cli_commit() + config = read_file(RADVD_CONF) + + # verify config block is gone + tmp = f'autoignoreprefixes' + ' {' + self.assertNotIn(tmp, config) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py index 2b3f6d21c..ab4707a61 100755 --- a/smoketest/scripts/cli/test_service_webproxy.py +++ b/smoketest/scripts/cli/test_service_webproxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 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 @@ -297,6 +297,22 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_06_nocache_domain_proxy(self): + domains_nocache = ['test1.net', 'test2.net'] + self.cli_set(base_path + ['listen-address', listen_ip]) + for domain in domains_nocache: + self.cli_set(base_path + ['domain-noncache', domain]) + # commit changes + self.cli_commit() + + config = read_file(PROXY_CONF) + + for domain in domains_nocache: + self.assertIn(f'acl NOCACHE dstdomain {domain}', config) + self.assertIn(f'no_cache deny NOCACHE', config) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) 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_ip.py b/smoketest/scripts/cli/test_system_ip.py index 7d730f7b2..5b6ef2046 100755 --- a/smoketest/scripts/cli/test_system_ip.py +++ b/smoketest/scripts/cli/test_system_ip.py @@ -20,8 +20,6 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.system import sysctl_read from vyos.xml_ref import default_value -from vyos.frrender import mgmt_daemon -from vyos.frrender import zebra_daemon base_path = ['system', 'ip'] @@ -45,13 +43,13 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['disable-forwarding']) self.cli_commit() self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '0') - frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertIn('no ip forwarding', frrconfig) self.cli_delete(base_path + ['disable-forwarding']) self.cli_commit() self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '1') - frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertNotIn('no ip forwarding', frrconfig) def test_system_ip_multipath(self): @@ -91,7 +89,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ip protocol', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('ip protocol', end='') for protocol in protocols: self.assertIn(f'ip protocol {protocol} route-map route-map-{protocol}', frrconfig) @@ -102,7 +100,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ip protocol', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('ip protocol', end='') self.assertNotIn(f'ip protocol', frrconfig) def test_system_ip_protocol_non_existing_route_map(self): @@ -121,13 +119,13 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config applied to FRR - frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertIn(f'no ip nht resolve-via-default', frrconfig) self.cli_delete(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config removed to FRR - frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertNotIn(f'no ip nht resolve-via-default', frrconfig) if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py index ebf620204..26f281bb4 100755 --- a/smoketest/scripts/cli/test_system_ipv6.py +++ b/smoketest/scripts/cli/test_system_ipv6.py @@ -21,8 +21,6 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.system import sysctl_read from vyos.xml_ref import default_value -from vyos.frrender import mgmt_daemon -from vyos.frrender import zebra_daemon base_path = ['system', 'ipv6'] @@ -46,13 +44,13 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['disable-forwarding']) self.cli_commit() self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '0') - frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertIn('no ipv6 forwarding', frrconfig) self.cli_delete(base_path + ['disable-forwarding']) self.cli_commit() self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '1') - frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertNotIn('no ipv6 forwarding', frrconfig) def test_system_ipv6_strict_dad(self): @@ -109,7 +107,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('ipv6 protocol', end='') for protocol in protocols: # VyOS and FRR use a different name for OSPFv3 (IPv6) if protocol == 'ospfv3': @@ -123,7 +121,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify route-map properly applied to FRR - frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('ipv6 protocol', end='') self.assertNotIn(f'ipv6 protocol', frrconfig) def test_system_ipv6_protocol_non_existing_route_map(self): @@ -142,13 +140,13 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config applied to FRR - frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertIn(f'no ipv6 nht resolve-via-default', frrconfig) self.cli_delete(base_path + ['nht', 'no-resolve-via-default']) self.cli_commit() # Verify CLI config removed to FRR - frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig('', end='') self.assertNotIn(f'no ipv6 nht resolve-via-default', frrconfig) if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index 28abba012..71dec68d8 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-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,23 +14,37 @@ # 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 unittest +import jinja2 +import secrets +import string +import paramiko +import shutil from base_vyostest_shim import VyOSUnitTestSHIM from gzip import GzipFile -from subprocess import Popen, PIPE +from subprocess import Popen +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.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'] +weak_passwd_user = ['test_user', 'passWord1'] + +ssh_test_command = '/opt/vyatta/bin/vyatta-op-cmd-wrapper show version' ssh_pubkey = """ AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF @@ -44,6 +58,71 @@ pHJz8umqkxy3hfw0K7BRFtjWd63sbOP8Q/SDV7LPaIfIxenA9zv2rY7y+AIqTmSr 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 + listen = { port = 49 } + spawn = { + instances min = 1 + instances max = 10 + } + background = no +} + +id = tac_plus { + debug = ALL + log = stdout { + destination = /dev/stdout + } + authorization log group = yes + authentication log = stdout + authorization log = stdout + accounting log = stdout + + host = smoketest { + address = {{ source_address }}/32 + enable = clear enable + key = {{ tacacs_secret }} + } + + group = admin { + default service = permit + enable = permit + service = shell { + default command = permit + default attribute = permit + set priv-lvl = 15 + } + } + + user = {{ username }} { + password = clear {{ password }} + 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): @classmethod def setUpClass(cls): @@ -54,6 +133,37 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path + ['radius']) cls.cli_delete(cls, base_path + ['tacacs']) + # 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 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 for user in users: @@ -83,29 +193,28 @@ 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 = "VyOS Roxx " + user - home_dir = "/tmp/" + user + name = f'VyOS Roxx {user}' + passwd = f'{user}-pSWd-t3st' + home_dir = f'/tmp/smoketest/{user}' - self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', user]) - self.cli_set(base_path + ['user', user, 'full-name', 'VyOS Roxx']) + self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', passwd]) + self.cli_set(base_path + ['user', user, 'full-name', name]) self.cli_set(base_path + ['user', user, 'home-directory', home_dir]) self.cli_commit() for user in users: + passwd = f'{user}-pSWd-t3st' tmp = ['su','-', user] proc = Popen(tmp, stdin=PIPE, stdout=PIPE, stderr=PIPE) - tmp = "{}\nuname -a".format(user) + tmp = f'{passwd}\nuname -a' proc.stdin.write(tmp.encode()) proc.stdin.flush() (stdout, stderr) = proc.communicate() # stdout is something like this: - # b'Linux LR1.wue3 5.10.61-amd64-vyos #1 SMP Fri Aug 27 08:55:46 UTC 2021 x86_64 GNU/Linux\n' + # b'Linux vyos 6.6.66-vyos 6.6.66-vyos #1 SMP Mon Dec 30 19:05:15 UTC 2024 x86_64 GNU/Linux\n' self.assertTrue(len(stdout) > 40) locked_user = users[0] @@ -123,6 +232,16 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): tmp = cmd(f'sudo passwd -S {locked_user}') self.assertIn(f'{locked_user} P ', tmp) + def test_system_login_weak_password_warning(self): + self.cli_set(base_path + [ + 'user', weak_passwd_user[0], 'authentication', + 'plaintext-password', weak_passwd_user[1] + ]) + + out = self.cli_commit().strip() + + self.assertIn('WARNING: The password complexity is too low', out) + self.cli_delete(base_path + ['user', weak_passwd_user[0]]) def test_system_login_otp(self): otp_user = 'otp-test_user' @@ -172,17 +291,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_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) - radius_key = 'VyOSsecretVyOS' - radius_server = '172.16.100.10' - radius_source = '127.0.0.1' - radius_port = '2000' - radius_timeout = '1' + 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) + + 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)) + + default_port = default_value(base_path + ['radius', 'server', radius_servers[0], 'port']) + default_timeout = default_value(base_path + ['radius', 'server', radius_servers[0], 'timeout']) + + dummy_if = 'dum12760' + + # Load container image for FreeRADIUS server + radius_config = '/tmp/smoketest-radius-server' + radius_container_path = ['container', 'name', 'radius-1'] + + # 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}' + } + + tmpl = jinja2.Template(RADIUS_CLIENTS_TMPL_SRC) + write_file(f'{radius_config}/clients.cfg', tmpl.render(radius_test_user)) + + tmpl = jinja2.Template(RADIUS_USERS_TMPL_SRC) + write_file(f'{radius_config}/users', tmpl.render(radius_test_user)) + + # 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']) + + # 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)]) @@ -195,10 +368,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) @@ -225,59 +401,26 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf) self.assertTrue(tmp) - def test_system_login_radius_ipv6(self): - # Verify generated RADIUS configuration files + # 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) - radius_key = 'VyOS-VyOS' - radius_server = '2001:db8::1' - radius_source = '::1' - radius_port = '4000' - radius_timeout = '4' - - 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)]) - - # 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)]) + # 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() - # 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) - - # required, static options - self.assertIn('priv-lvl 15', pam_radius_auth_conf) - self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf) - - # PAM - pam_common_account = read_file('/etc/pam.d/common-account') - self.assertIn('pam_radius_auth.so', pam_common_account) - - pam_common_auth = read_file('/etc/pam.d/common-auth') - self.assertIn('pam_radius_auth.so', pam_common_auth) - - 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) - - # NSS - nsswitch_conf = read_file('/etc/nsswitch.conf') - tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf) - self.assertTrue(tmp) - - tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf) - self.assertTrue(tmp) + # Remove rendered tac_plus daemon configuration + shutil.rmtree(radius_config) def test_system_login_max_login_session(self): max_logins = '2' @@ -300,11 +443,46 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path + ['max-login-session']) def test_system_login_tacacs(self): - tacacs_secret = 'tac_plus_key' + tacacs_secret = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10)) tacacs_servers = ['100.64.0.11', '100.64.0.12'] + source_address = '100.64.0.1' + dummy_if = 'dum12759' + + # Load container image for lac_plus daemon + tac_plus_config = '/tmp/smoketest-tacacs-server' + tac_container_path = ['container', 'name', 'tacacs-1'] + + # Generate random string with 10 digits + username = 'tactest' + password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10)) + tac_test_user = { + 'username' : username, + 'password' : password, + 'tacacs_secret' : tacacs_secret, + 'source_address' : source_address, + } + + tmpl = jinja2.Template(TAC_PLUS_TMPL_SRC) + write_file(f'{tac_plus_config}/tac_plus.cfg', tmpl.render(tac_test_user)) + + # Start tac_plus container + self.cli_set(tac_container_path + ['allow-host-networks']) + self.cli_set(tac_container_path + ['image', tac_image]) + self.cli_set(tac_container_path + ['volume', 'config', 'destination', '/etc/tac_plus']) + self.cli_set(tac_container_path + ['volume', 'config', 'mode', 'ro']) + self.cli_set(tac_container_path + ['volume', 'config', 'source', tac_plus_config]) + + # Start container + self.cli_commit() + + # Define TACACS traffic source address + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{source_address}/32']) + self.cli_set(base_path + ['tacacs', 'source-address', source_address]) - # Enable TACACS + # Define TACACS servers for server in tacacs_servers: + # Use this system as "remote" TACACS server + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{server}/32']) self.cli_set(base_path + ['tacacs', 'server', server, 'key', tacacs_secret]) self.cli_commit() @@ -328,6 +506,11 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.assertIn('service=shell', pam_tacacs_conf) self.assertIn('protocol=ssh', pam_tacacs_conf) + # Verify configured TACACS source address + self.assertIn(f'source_ip={source_address}', pam_tacacs_conf) + self.assertIn(f'source_ip={source_address}', nss_tacacs_conf) + + # Verify configured TACACS servers for server in tacacs_servers: self.assertIn(f'secret={tacacs_secret}', pam_tacacs_conf) self.assertIn(f'server={server}', pam_tacacs_conf) @@ -335,6 +518,27 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.assertIn(f'secret={tacacs_secret}', nss_tacacs_conf) self.assertIn(f'server={server}', nss_tacacs_conf) + # 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 TACACS configuration + self.cli_delete(base_path + ['tacacs']) + # Remove tac_plus container + self.cli_delete(tac_container_path) + # Remove dummy interface + self.cli_delete(['interfaces', 'dummy', dummy_if]) + self.cli_commit() + + # Remove rendered tac_plus daemon configuration + shutil.rmtree(tac_plus_config) + def test_delete_current_user(self): current_user = get_current_user() diff --git a/smoketest/scripts/cli/test_system_option.py b/smoketest/scripts/cli/test_system_option.py index f3112cf0b..c7f8c1f3e 100755 --- a/smoketest/scripts/cli/test_system_option.py +++ b/smoketest/scripts/cli/test_system_option.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright (C) 2024-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 @@ -16,14 +16,18 @@ import os import unittest + from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.utils.cpu import get_cpus from vyos.utils.file import read_file from vyos.utils.process import is_systemd_service_active from vyos.utils.system import sysctl_read +from vyos.system import image base_path = ['system', 'option'] - class TestSystemOption(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) @@ -96,6 +100,60 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.assertFalse(os.path.exists(ssh_client_opt_file)) + def test_kernel_options(self): + amd_pstate_mode = 'active' + isolate_cpus = '1,2,3' + nohz_full = '2' + rcu_no_cbs = '1,2,4-5' + default_hp_size = '2M' + hp_size_1g = '1G' + hp_size_2m = '2M' + hp_count_1g = '2' + hp_count_2m = '512' + + self.cli_set(['system', 'option', 'kernel', 'cpu', 'disable-nmi-watchdog']) + self.cli_set(['system', 'option', 'kernel', 'cpu', 'isolate-cpus', isolate_cpus]) + self.cli_set(['system', 'option', 'kernel', 'cpu', 'nohz-full', nohz_full]) + self.cli_set(['system', 'option', 'kernel', 'cpu', 'rcu-no-cbs', rcu_no_cbs]) + self.cli_set(['system', 'option', 'kernel', 'disable-hpet']) + self.cli_set(['system', 'option', 'kernel', 'disable-mce']) + self.cli_set(['system', 'option', 'kernel', 'disable-mitigations']) + self.cli_set(['system', 'option', 'kernel', 'disable-power-saving']) + self.cli_set(['system', 'option', 'kernel', 'disable-softlockup']) + self.cli_set(['system', 'option', 'kernel', 'memory', 'disable-numa-balancing']) + self.cli_set(['system', 'option', 'kernel', 'memory', 'default-hugepage-size', default_hp_size]) + self.cli_set(['system', 'option', 'kernel', 'memory', 'hugepage-size', hp_size_1g, 'hugepage-count', hp_count_1g]) + self.cli_set(['system', 'option', 'kernel', 'memory', 'hugepage-size', hp_size_2m, 'hugepage-count', hp_count_2m]) + self.cli_set(['system', 'option', 'kernel', 'quiet']) + + self.cli_set(['system', 'option', 'kernel', 'amd-pstate-driver', amd_pstate_mode]) + cpu_vendor = get_cpus()[0]['vendor_id'] + if cpu_vendor != 'AuthenticAMD': + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['system', 'option', 'kernel', 'amd-pstate-driver']) + + self.cli_commit() + + # Read GRUB config file for current running image + tmp = read_file(f'{image.grub.GRUB_DIR_VYOS_VERS}/{image.get_running_image()}.cfg') + self.assertIn(' mitigations=off', tmp) + self.assertIn(' intel_idle.max_cstate=0 processor.max_cstate=1', tmp) + self.assertIn(' quiet', tmp) + self.assertIn(' nmi_watchdog=0', tmp) + self.assertIn(' hpet=disable', tmp) + self.assertIn(' mce=off', tmp) + self.assertIn(' nosoftlockup', tmp) + self.assertIn(f' isolcpus={isolate_cpus}', tmp) + self.assertIn(f' nohz_full={nohz_full}', tmp) + self.assertIn(f' rcu_nocbs={rcu_no_cbs}', tmp) + self.assertIn(f' default_hugepagesz={default_hp_size}', tmp) + self.assertIn(f' hugepagesz={hp_size_1g} hugepages={hp_count_1g}', tmp) + self.assertIn(f' hugepagesz={hp_size_2m} hugepages={hp_count_2m}', tmp) + self.assertIn(' numa_balancing=disable', tmp) + + if cpu_vendor == 'AuthenticAMD': + self.assertIn(f' initcall_blacklist=acpi_cpufreq_init amd_pstate={amd_pstate_mode}', tmp) if __name__ == '__main__': unittest.main(verbosity=2) 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_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py index a86711119..f3e1f65ea 100755 --- a/smoketest/scripts/cli/test_system_syslog.py +++ b/smoketest/scripts/cli/test_system_syslog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-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,24 +14,33 @@ # 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 re +import os import unittest 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 PROCESS_NAME = 'rsyslogd' -RSYSLOG_CONF = '/etc/rsyslog.d/00-vyos.conf' +RSYSLOG_CONF = '/run/rsyslog/rsyslog.conf' base_path = ['system', 'syslog'] -def get_config_value(key): - tmp = read_file(RSYSLOG_CONF) - tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) - return tmp[0] +dummy_interface = 'dum372874' + +def get_config(string=''): + """ + Retrieve current "running configuration" from FRR + string: search for a specific start string in the configuration + """ + command = 'cat /run/rsyslog/rsyslog.conf' + if string: + command += f' | sed -n "/^{string}$/,/}}/p"' # }} required to escape } in f-string + return cmd(command) class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): @classmethod @@ -41,6 +50,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['vrf']) def tearDown(self): # Check for running process @@ -50,82 +60,249 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + # The default syslog implementation should make syslog.service a + # symlink to itself + self.assertEqual(os.readlink('/etc/systemd/system/syslog.service'), + '/lib/systemd/system/rsyslog.service') + # Check for running process self.assertFalse(process_named_running(PROCESS_NAME)) - def test_syslog_basic(self): - host1 = '127.0.0.10' - host2 = '127.0.0.20' - - self.cli_set(base_path + ['host', host1, 'port', '999']) - self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all']) - self.cli_set(base_path + ['host', host2, 'facility', 'kern', 'level', 'err']) - self.cli_set(base_path + ['console', 'facility', 'all', 'level', 'warning']) - + def test_console(self): + level = 'warning' + self.cli_set(base_path + ['console', 'facility', 'all', 'level'], value=level) self.cli_commit() - # verify log level and facilities in config file - # *.warning /dev/console - # *.* @198.51.100.1:999 - # kern.err @192.0.2.1:514 + + rsyslog_conf = get_config() config = [ - get_config_value('\*.\*'), - get_config_value('kern.err'), - get_config_value('\*.warning'), + f'if prifilt("*.{level}") then {{', # {{ required to escape { in f-string + 'action(type="omfile" file="/dev/console")', ] - expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console'] + for tmp in config: + self.assertIn(tmp, rsyslog_conf) - for i in range(0, 3): - self.assertIn(expected[i], config[i]) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - def test_syslog_global(self): + def test_basic(self): hostname = 'vyos123' - domainname = 'example.local' - self.cli_set(['system', 'host-name', hostname]) - self.cli_set(['system', 'domain-name', domainname]) - self.cli_set(base_path + ['global', 'marker', 'interval', '600']) - self.cli_set(base_path + ['global', 'preserve-fqdn']) - self.cli_set(base_path + ['global', 'facility', 'kern', 'level', 'err']) + domain_name = 'example.local' + default_marker_interval = default_value(base_path + ['marker', 'interval']) + + facility = { + 'auth': {'level': 'info'}, + 'kern': {'level': 'debug'}, + 'all': {'level': 'notice'}, + } + + self.cli_set(['system', 'host-name'], value=hostname) + self.cli_set(['system', 'domain-name'], value=domain_name) + self.cli_set(base_path + ['preserve-fqdn']) + + for tmp, tmp_options in facility.items(): + level = tmp_options['level'] + self.cli_set(base_path + ['local', 'facility', tmp, 'level'], value=level) self.cli_commit() - config = read_file(RSYSLOG_CONF) + config = get_config('') expected = [ - '$MarkMessagePeriod 600', - '$PreserveFQDN on', - 'kern.err', - f'$LocalHostName {hostname}.{domainname}', + f'module(load="immark" interval="{default_marker_interval}")', + 'global(preserveFQDN="on")', + f'global(localHostname="{hostname}.{domain_name}")', ] - for e in expected: self.assertIn(e, config) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_syslog_remote(self): - rhost = '169.254.0.1' - default_port = default_value(base_path + ['host', rhost, 'port']) + config = get_config('#### GLOBAL LOGGING ####') + prifilt = [] + for tmp, tmp_options in facility.items(): + if tmp == 'all': + tmp = '*' + level = tmp_options['level'] + prifilt.append(f'{tmp}.{level}') + + prifilt.sort() + prifilt = ','.join(prifilt) - self.cli_set(base_path + ['global', 'facility', 'all', 'level', 'info']) - self.cli_set(base_path + ['global', 'facility', 'local7', 'level', 'debug']) - self.cli_set(base_path + ['host', rhost, 'facility', 'all', 'level', 'all']) - self.cli_set(base_path + ['host', rhost, 'protocol', 'tcp']) + self.assertIn(f'if prifilt("{prifilt}") then {{', config) + self.assertIn( ' action(', config) + self.assertIn( ' type="omfile"', config) + self.assertIn( ' file="/var/log/messages"', config) + self.assertIn( ' rotation.sizeLimit="524288"', config) + self.assertIn( ' rotation.sizeLimitCommand="/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog"', config) + self.cli_set(base_path + ['marker', 'disable']) self.cli_commit() - config = read_file(RSYSLOG_CONF) - self.assertIn(f'*.* @@{rhost}:{default_port}', config) + config = get_config('') + self.assertNotIn('module(load="immark"', config) + + def test_remote(self): + dummy_if_path = ['interfaces', 'dummy', dummy_interface] + rhosts = { + '169.254.0.1': { + 'facility': {'auth' : {'level': 'info'}}, + 'protocol': 'udp', + }, + '2001:db8::1': { + 'facility': {'all' : {'level': 'debug'}}, + 'port': '1514', + 'protocol': 'udp', + }, + 'syslog.vyos.net': { + 'facility': {'all' : {'level': 'debug'}}, + 'port': '1515', + 'protocol': 'tcp', + }, + '169.254.0.3': { + 'facility': {'auth' : {'level': 'info'}, + 'kern' : {'level': 'debug'}, + 'all' : {'level': 'notice'}, + }, + 'format': ['include-timezone', 'octet-counted'], + 'protocol': 'tcp', + 'port': '10514', + }, + } + default_port = default_value(base_path + ['remote', next(iter(rhosts)), 'port']) + default_protocol = default_value(base_path + ['remote', next(iter(rhosts)), 'protocol']) + + for remote, remote_options in rhosts.items(): + remote_base = base_path + ['remote', remote] + + if 'port' in remote_options: + self.cli_set(remote_base + ['port'], value=remote_options['port']) + + if 'facility' in remote_options: + for facility, facility_options in remote_options['facility'].items(): + level = facility_options['level'] + self.cli_set(remote_base + ['facility', facility, 'level'], + value=level) + + if 'format' in remote_options: + for format in remote_options['format']: + self.cli_set(remote_base + ['format'], value=format) + + if 'protocol' in remote_options: + protocol = remote_options['protocol'] + self.cli_set(remote_base + ['protocol'], value=protocol) + + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + self.cli_set(remote_base + ['source-address', source_address]) + + # check validate() - source address does not exist + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(dummy_if_path + ['address', f'{source_address}/32']) - # Change default port and enable "octet-counting" mode - port = '10514' - self.cli_set(base_path + ['host', rhost, 'port', port]) - self.cli_set(base_path + ['host', rhost, 'format', 'octet-counted']) self.cli_commit() config = read_file(RSYSLOG_CONF) - self.assertIn(f'*.* @@(o){rhost}:{port}', config) + for remote, remote_options in rhosts.items(): + config = get_config(f'# Remote syslog to {remote}') + prifilt = [] + if 'facility' in remote_options: + for facility, facility_options in remote_options['facility'].items(): + level = facility_options['level'] + if facility == 'all': + facility = '*' + prifilt.append(f'{facility}.{level}') + + prifilt.sort() + prifilt = ','.join(prifilt) + if not prifilt: + # Skip test - as we do not render anything if no facility is set + continue + + self.assertIn(f'if prifilt("{prifilt}") then {{', config) + self.assertIn( ' type="omfwd"', config) + self.assertIn(f' target="{remote}"', config) + + port = default_port + if 'port' in remote_options: + port = remote_options['port'] + self.assertIn(f'port="{port}"', config) + + protocol = default_protocol + if 'protocol' in remote_options: + protocol = remote_options['protocol'] + self.assertIn(f'protocol="{protocol}"', config) + + if 'format' in remote_options: + if 'include-timezone' in remote_options['format']: + self.assertIn( ' template="RSYSLOG_SyslogProtocol23Format"', config) + + if 'octet-counted' in remote_options['format']: + self.assertIn( ' TCP_Framing="octet-counted"', config) + else: + self.assertIn( ' TCP_Framing="traditional"', config) + + # cleanup dummy interface + self.cli_delete(dummy_if_path) + + def test_vrf_source_address(self): + rhosts = { + '169.254.0.10': { }, + '169.254.0.11': { + 'vrf': {'name' : 'red', 'table' : '12321'}, + 'source_address' : '169.254.0.11', + }, + '169.254.0.12': { + 'vrf': {'name' : 'green', 'table' : '12322'}, + 'source_address' : '169.254.0.12', + }, + '169.254.0.13': { + 'vrf': {'name' : 'blue', 'table' : '12323'}, + 'source_address' : '169.254.0.13', + }, + } + + for remote, remote_options in rhosts.items(): + remote_base = base_path + ['remote', remote] + self.cli_set(remote_base + ['facility', 'all']) + + vrf = None + if 'vrf' in remote_options: + vrf = remote_options['vrf']['name'] + self.cli_set(['vrf', 'name', vrf, 'table'], + value=remote_options['vrf']['table']) + self.cli_set(remote_base + ['vrf'], value=vrf) + + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + self.cli_set(remote_base + ['source-address'], + value=source_address) + + idx = source_address.split('.')[-1] + self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'address'], + value=f'{source_address}/32') + if vrf: + self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'vrf'], + value=vrf) + + self.cli_commit() + + for remote, remote_options in rhosts.items(): + config = get_config(f'# Remote syslog to {remote}') + + self.assertIn(f'target="{remote}"', config) + if 'vrf' in remote_options: + vrf = remote_options['vrf']['name'] + self.assertIn(f'Device="{vrf}"', config) + + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + self.assertIn(f'Address="{source_address}"', config) + + # Cleanup VRF/Dummy interfaces + for remote, remote_options in rhosts.items(): + if 'vrf' in remote_options: + vrf = remote_options['vrf']['name'] + self.cli_delete(['vrf', 'name', vrf]) + if 'source_address' in remote_options: + source_address = remote_options['source_address'] + idx = source_address.split('.')[-1] + self.cli_delete(['interfaces', 'dummy', f'dum{idx}']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index f2bea58d1..c1d943bde 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 @@ -352,25 +352,129 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() + def test_site_to_site_vti_ts_afi(self): + local_address = '192.0.2.10' + vti = 'vti10' + # IKE + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', ike_group, 'disable-mobike']) + # ESP + self.cli_set(base_path + ['esp-group', esp_group, 'compression']) + # VTI interface + self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24']) + + # vpn ipsec auth psk <tag> id <x.x.x.x> + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + + # Site to site + peer_base_path = base_path + ['site-to-site', 'peer', connection_name] + self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) + self.cli_set(peer_base_path + ['connection-type', 'none']) + self.cli_set(peer_base_path + ['force-udp-encapsulation']) + self.cli_set(peer_base_path + ['ike-group', ike_group]) + self.cli_set(peer_base_path + ['default-esp-group', esp_group]) + self.cli_set(peer_base_path + ['local-address', local_address]) + self.cli_set(peer_base_path + ['remote-address', peer_ip]) + self.cli_set(peer_base_path + ['vti', 'bind', vti]) + self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group]) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '0.0.0.0/0']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.1/32']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.3/32']) + + self.cli_commit() + + swanctl_conf = read_file(swanctl_file) + if_id = vti.lstrip('vti') + # The key defaults to 0 and will match any policies which similarly do + # not have a lookup key configuration - thus we shift the key by one + # to also support a vti0 interface + if_id = str(int(if_id) +1) + swanctl_conf_lines = [ + f'version = 2', + f'auth = psk', + f'proposals = aes128-sha1-modp1024', + f'esp_proposals = aes128-sha1-modp1024', + f'local_addrs = {local_address} # dhcp:no', + f'mobike = no', + f'remote_addrs = {peer_ip}', + f'mode = tunnel', + f'local_ts = 0.0.0.0/0', + f'remote_ts = 192.0.2.1/32,192.0.2.3/32', + f'ipcomp = yes', + f'start_action = none', + f'replay_window = 32', + f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one + f'if_id_out = {if_id}', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + # Check IPv6 TS + self.cli_delete(peer_base_path + ['vti', 'traffic-selector']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '::/0']) + self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '::/0']) + self.cli_commit() + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'local_ts = ::/0', + f'remote_ts = ::/0', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + # Check both TS (IPv4 + IPv6) + self.cli_delete(peer_base_path + ['vti', 'traffic-selector']) + self.cli_commit() + swanctl_conf = read_file(swanctl_file) + swanctl_conf_lines = [ + f'local_ts = 0.0.0.0/0,::/0', + f'remote_ts = 0.0.0.0/0,::/0', + f'updown = "/etc/ipsec.d/vti-up-down {vti}"' + ] + for line in swanctl_conf_lines: + self.assertIn(line, swanctl_conf) + + def test_dmvpn(self): - 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 +503,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/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index f4ed1a61f..30980f9ec 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# 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 @@ -18,12 +18,13 @@ import re import os import unittest -from base_vyostest_shim import VyOSUnitTestSHIM from json import loads from jmespath import search +from base_vyostest_shim import VyOSUnitTestSHIM +from base_vyostest_shim import CSTORE_GUARD_TIME + from vyos.configsession import ConfigSessionError -from vyos.frrender import mgmt_daemon from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.file import read_file @@ -52,6 +53,10 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): else: for tmp in Section.interfaces('ethernet', vlan=False): cls._interfaces.append(tmp) + + # Enable CSTORE guard time required by FRR related tests + cls._commit_guard_time = CSTORE_GUARD_TIME + # call base-classes classmethod super(VRFTest, cls).setUpClass() @@ -113,7 +118,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): regex = f'{table}\s+{vrf}\s+#\s+{description}' self.assertTrue(re.findall(regex, iproute2_config)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) self.assertEqual(int(table), get_vrf_tableid(vrf)) @@ -234,7 +239,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) self.assertIn(f' ip route {prefix} {next_hop}', frrconfig) @@ -318,7 +323,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) for protocol in v4_protocols: self.assertIn(f' ip protocol {protocol} route-map route-map-{vrf}-{protocol}', frrconfig) @@ -333,7 +338,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertNotIn(f' ip protocol', frrconfig) def test_vrf_ip_ipv6_protocol_non_existing_route_map(self): @@ -381,7 +386,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) for protocol in v6_protocols: # VyOS and FRR use a different name for OSPFv3 (IPv6) @@ -400,7 +405,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertNotIn(f' ipv6 protocol', frrconfig) def test_vrf_vni_duplicates(self): @@ -430,7 +435,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 1) @@ -452,7 +457,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 1) @@ -475,7 +480,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 2) @@ -495,7 +500,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Increment table ID for the next run table = str(int(table) + 2) @@ -503,7 +508,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify purple VRF/VNI self.assertTrue(interface_exists(purple)) table = str(int(table) + 10) - frrconfig = self.getFRRconfig(f'vrf {purple}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {purple}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) # Now delete all the VNIs @@ -518,12 +523,12 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertTrue(interface_exists(vrf)) - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertNotIn('vni', frrconfig) # Verify purple VNI remains self.assertTrue(interface_exists(purple)) - frrconfig = self.getFRRconfig(f'vrf {purple}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {purple}', endsection='^exit-vrf') self.assertIn(f' vni {table}', frrconfig) def test_vrf_ip_ipv6_nht(self): @@ -541,7 +546,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly applied to FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) self.assertIn(f' no ip nht resolve-via-default', frrconfig) self.assertIn(f' no ipv6 nht resolve-via-default', frrconfig) @@ -556,7 +561,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # Verify route-map properly is removed from FRR for vrf in vrfs: - frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon=mgmt_daemon) + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertNotIn(f' no ip nht resolve-via-default', frrconfig) self.assertNotIn(f' no ipv6 nht resolve-via-default', frrconfig) diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py index 700e4cec7..84e9c145d 100755 --- a/smoketest/scripts/system/test_kernel_options.py +++ b/smoketest/scripts/system/test_kernel_options.py @@ -128,5 +128,20 @@ 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) + + def test_amd_pstate(self): + # AMD pstate driver required as we have "set system option kernel amd-pstate-driver" + for option in ['CONFIG_X86_AMD_PSTATE']: + tmp = re.findall(f'{option}=y', self._config_data) + self.assertTrue(tmp) + for option in ['CONFIG_X86_AMD_PSTATE_DEFAULT_MODE']: + tmp = re.findall(f'{option}=3', self._config_data) + self.assertTrue(tmp) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 594de3eb0..94882fc14 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -22,6 +22,7 @@ from ipaddress import ip_address from ipaddress import ip_network from json import dumps as json_write +import psutil from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge @@ -223,6 +224,21 @@ def verify(container): if not os.path.exists(source): raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') + if 'tmpfs' in container_config: + for tmpfs, tmpfs_config in container_config['tmpfs'].items(): + if 'destination' not in tmpfs_config: + raise ConfigError(f'tmpfs "{tmpfs}" has no destination path configured!') + if 'size' in tmpfs_config: + free_mem_mb: int = psutil.virtual_memory().available / 1024 / 1024 + if int(tmpfs_config['size']) > free_mem_mb: + Warning(f'tmpfs "{tmpfs}" size is greater than the current free memory!') + + total_mem_mb: int = (psutil.virtual_memory().total / 1024 / 1024) / 2 + if int(tmpfs_config['size']) > total_mem_mb: + raise ConfigError(f'tmpfs "{tmpfs}" size should not be more than 50% of total system memory!') + else: + raise ConfigError(f'tmpfs "{tmpfs}" has no size configured!') + if 'port' in container_config: for tmp in container_config['port']: if not {'source', 'destination'} <= set(container_config['port'][tmp]): @@ -273,6 +289,13 @@ def verify(container): if 'registry' in container: for registry, registry_config in container['registry'].items(): + if 'mirror' in registry_config: + if 'host_name' in registry_config['mirror'] and 'address' in registry_config['mirror']: + raise ConfigError(f'Container registry mirror address/host-name are mutually exclusive!') + + if 'path' in registry_config['mirror'] and not registry_config['mirror']['path'].startswith('/'): + raise ConfigError('Container registry mirror path must start with "/"!') + if 'authentication' not in registry_config: continue if not {'username', 'password'} <= set(registry_config['authentication']): @@ -301,6 +324,11 @@ def generate_run_arguments(name, container_config): cap = cap.upper().replace('-', '_') capabilities += f' --cap-add={cap}' + # Grant root capabilities to the container + privileged = '' + if 'privileged' in container_config: + privileged = '--privileged' + # Add a host device to the container /dev/x:/dev/x device = '' if 'device' in container_config: @@ -362,6 +390,14 @@ def generate_run_arguments(name, container_config): prop = vol_config['propagation'] volume += f' --volume {svol}:{dvol}:{mode},{prop}' + # Mount tmpfs + tmpfs = '' + if 'tmpfs' in container_config: + for tmpfs_config in container_config['tmpfs'].values(): + dest = tmpfs_config['destination'] + size = tmpfs_config['size'] + tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}' + host_pid = '' if 'allow_host_pid' in container_config: host_pid = '--pid host' @@ -371,9 +407,9 @@ def generate_run_arguments(name, container_config): for ns in container_config['name_server']: name_server += f'--dns {ns}' - container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \ + container_base_cmd = f'--detach --interactive --tty --replace {capabilities} {privileged} --cpus {cpu_quota} {sysctl_opt} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {hostname} {device} {port} {name_server} {volume} {env_opt} {label} {uid} {host_pid}' + f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}' entrypoint = '' if 'entrypoint' in container_config: diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index ffbd915a2..274ca2ce6 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -18,7 +18,6 @@ import os import re from sys import exit - from vyos.base import Warning from vyos.config import Config from vyos.configdict import is_node_changed @@ -34,6 +33,8 @@ from vyos.utils.dict import dict_search_recursive from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import rc_cmd +from vyos.utils.network import get_vrf_members +from vyos.utils.network import get_interface_vrf from vyos import ConfigError from vyos import airbag from pathlib import Path @@ -43,7 +44,7 @@ airbag.enable() nftables_conf = '/run/nftables.conf' domain_resolver_usage = '/run/use-vyos-domain-resolver-firewall' -domain_resolver_usage_nat = '/run/use-vyos-domain-resolver-nat' +firewall_config_dir = "/config/firewall" sysctl_file = r'/run/sysctl/10-vyos-firewall.conf' @@ -53,7 +54,8 @@ valid_groups = [ 'network_group', 'port_group', 'interface_group', - ## Added for group ussage in bridge firewall + 'remote_group', + ## Added for group usage in bridge firewall 'ipv4_address_group', 'ipv6_address_group', 'ipv4_network_group', @@ -134,6 +136,27 @@ def get_config(config=None): fqdn_config_parse(firewall, 'firewall') + if not os.path.exists(nftables_conf): + firewall['first_install'] = True + + if 'zone' in firewall: + for local_zone, local_zone_conf in firewall['zone'].items(): + if 'local_zone' not in local_zone_conf: + # Get physical interfaces assigned to the zone if vrf is used: + if 'vrf' in local_zone_conf['member']: + local_zone_conf['vrf_interfaces'] = {} + for vrf_name in local_zone_conf['member']['vrf']: + local_zone_conf['vrf_interfaces'][vrf_name] = ','.join(get_vrf_members(vrf_name)) + continue + + local_zone_conf['from_local'] = {} + + for zone, zone_conf in firewall['zone'].items(): + if zone == local_zone or 'from' not in zone_conf: + continue + if local_zone in zone_conf['from']: + local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone] + set_dependents('conntrack', conf) return firewall @@ -182,7 +205,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): if 'jump' not in rule_conf['action']: raise ConfigError('jump-target defined, but action jump needed and it is not defined') target = rule_conf['jump_target'] - if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code. + if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code. verify_jump_target(firewall, hook, target, family, recursive=True) else: verify_jump_target(firewall, hook, target, family, recursive=False) @@ -245,12 +268,12 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): if dict_search_args(rule_conf, 'gre', 'flags', 'checksum') is None: # There is no builtin match in nftables for the GRE key, so we need to do a raw lookup. - # The offset of the key within the packet shifts depending on the C-flag. - # 99% of the time, nobody will have checksums enabled - it's usually a manual config option. - # We can either assume it is unset unless otherwise directed + # The offset of the key within the packet shifts depending on the C-flag. + # 99% of the time, nobody will have checksums enabled - it's usually a manual config option. + # We can either assume it is unset unless otherwise directed # (confusing, requires doco to explain why it doesn't work sometimes) - # or, demand an explicit selection to be made for this specific match rule. - # This check enforces the latter. The user is free to create rules for both cases. + # or, demand an explicit selection to be made for this specific match rule. + # This check enforces the latter. The user is free to create rules for both cases. raise ConfigError('Matching GRE tunnel key requires an explicit checksum flag match. For most cases, use "gre flags checksum unset"') if dict_search_args(rule_conf, 'gre', 'flags', 'key', 'unset') is not None: @@ -263,7 +286,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): if gre_inner_value < 0 or gre_inner_value > 65535: raise ConfigError('inner-proto outside valid ethertype range 0-65535') except ValueError: - pass # Symbolic constant, pre-validated before reaching here. + pass # Symbolic constant, pre-validated before reaching here. tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags: @@ -290,8 +313,8 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): raise ConfigError('Only one of address, fqdn or geoip can be specified') if 'group' in side_conf: - if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: - raise ConfigError('Only one address-group, network-group or domain-group can be specified') + if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: @@ -311,7 +334,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): error_group = fw_group.replace("_", "-") - if group in ['address_group', 'network_group', 'domain_group']: + if group in ['address_group', 'network_group', 'domain_group', 'remote_group']: types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf] if types: raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') @@ -414,6 +437,16 @@ def verify(firewall): for ifname in interfaces: verify_hardware_offload(ifname) + if 'offload' in firewall.get('global_options', {}).get('state_policy', {}): + offload_path = firewall['global_options']['state_policy']['offload'] + if 'offload_target' not in offload_path: + raise ConfigError('offload-target must be specified') + + offload_target = offload_path['offload_target'] + + if not dict_search_args(firewall, 'flowtable', offload_target): + raise ConfigError(f'Invalid offload-target. Flowtable "{offload_target}" does not exist on the system') + if 'group' in firewall: for group_type in nested_group_types: if group_type in firewall['group']: @@ -421,6 +454,11 @@ def verify(firewall): for group_name, group in groups.items(): verify_nested_group(group_name, group, groups, []) + if 'remote_group' in firewall['group']: + for group_name, group in firewall['group']['remote_group'].items(): + if 'url' not in group: + raise ConfigError(f'remote-group {group_name} must have a url configured') + for family in ['ipv4', 'ipv6', 'bridge']: if family in firewall: for chain in ['name','forward','input','output', 'prerouting']: @@ -442,28 +480,45 @@ def verify(firewall): local_zone = False zone_interfaces = [] + zone_vrf = [] if 'zone' in firewall: for zone, zone_conf in firewall['zone'].items(): - if 'local_zone' not in zone_conf and 'interface' not in zone_conf: + if 'local_zone' not in zone_conf and 'member' not in zone_conf: raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') if 'local_zone' in zone_conf: if local_zone: raise ConfigError('There cannot be multiple local zones') - if 'interface' in zone_conf: + if 'member' in zone_conf: raise ConfigError('Local zone cannot have interfaces assigned') if 'intra_zone_filtering' in zone_conf: raise ConfigError('Local zone cannot use intra-zone-filtering') local_zone = True - if 'interface' in zone_conf: - found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces] + if 'member' in zone_conf: + if 'interface' in zone_conf['member']: + for iface in zone_conf['member']['interface']: + + if iface in zone_interfaces: + raise ConfigError(f'Interfaces cannot be assigned to multiple zones') + + iface_vrf = get_interface_vrf(iface) + if iface_vrf != 'default': + Warning(f"Interface {iface} assigned to zone {zone} is in VRF {iface_vrf}. This might not work as expected.") + zone_interfaces.append(iface) - if found_duplicates: - raise ConfigError(f'Interfaces cannot be assigned to multiple zones') + if 'vrf' in zone_conf['member']: + for vrf in zone_conf['member']['vrf']: + if vrf in zone_vrf: + raise ConfigError(f'VRF cannot be assigned to multiple zones') + zone_vrf.append(vrf) - zone_interfaces += zone_conf['interface'] + if 'vrf_interfaces' in zone_conf: + for vrf_name, vrf_interfaces in zone_conf['vrf_interfaces'].items(): + if not vrf_interfaces: + raise ConfigError( + f'VRF "{vrf_name}" cannot be a member of any zone. It does not contain any interfaces.') if 'intra_zone_filtering' in zone_conf: intra_zone = zone_conf['intra_zone_filtering'] @@ -499,24 +554,17 @@ def verify(firewall): return None def generate(firewall): - if not os.path.exists(nftables_conf): - firewall['first_install'] = True - - if 'zone' in firewall: - for local_zone, local_zone_conf in firewall['zone'].items(): - if 'local_zone' not in local_zone_conf: - continue - - local_zone_conf['from_local'] = {} - - for zone, zone_conf in firewall['zone'].items(): - if zone == local_zone or 'from' not in zone_conf: - continue - if local_zone in zone_conf['from']: - local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone] - render(nftables_conf, 'firewall/nftables.j2', firewall) render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall) + + # Cleanup remote-group cache files + if os.path.exists(firewall_config_dir): + for fw_file in os.listdir(firewall_config_dir): + # Delete matching files in 'config/firewall' that no longer exist as a remote-group in config + if fw_file.startswith("R_") and fw_file.endswith(".txt"): + if 'group' not in firewall or 'remote_group' not in firewall['group'] or fw_file[2:-4] not in firewall['group']['remote_group'].keys(): + os.unlink(os.path.join(firewall_config_dir, fw_file)) + return None def parse_firewall_error(output): @@ -576,7 +624,7 @@ def apply(firewall): ## DOMAIN RESOLVER domain_action = 'restart' - if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items(): + if dict_search_args(firewall, 'group', 'remote_group') or dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items(): text = f'# Automatically generated by firewall.py\nThis file indicates that vyos-domain-resolver service is used by the firewall.\n' Path(domain_resolver_usage).write_text(text) else: @@ -589,7 +637,7 @@ def apply(firewall): # Call helper script to Update set contents if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']: print('Updating GeoIP. Please wait...') - geoip_update(firewall) + geoip_update(firewall=firewall) return None diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 4f1141dcb..84316c16e 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -126,9 +126,8 @@ def get_config(config=None): # Restore existing config level conf.set_level(old_level) - if dict_search('member.interface', bond): - for interface, interface_config in bond['member']['interface'].items(): - + if dict_search('member.interface', bond) is not None: + for interface in bond['member']['interface']: interface_ethernet_config = conf.get_config_dict( ['interfaces', 'ethernet', interface], key_mangling=('-', '_'), @@ -137,44 +136,45 @@ def get_config(config=None): with_defaults=False, with_recursive_defaults=False) - interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config) + bond['member']['interface'][interface].update({'config_paths' : + dict_to_paths_values(interface_ethernet_config)}) # Check if member interface is a new member if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]): bond['shutdown_required'] = {} - interface_config['new_added'] = {} + bond['member']['interface'][interface].update({'new_added' : {}}) # Check if member interface is disabled conf.set_level(['interfaces']) section = Section.section(interface) # this will be 'ethernet' for 'eth0' if conf.exists([section, interface, 'disable']): - interface_config['disable'] = '' + if tmp: bond['member']['interface'][interface].update({'disable': ''}) conf.set_level(old_level) # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp: interface_config['is_bridge_member'] = tmp + if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp}) # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') - for tmp in is_member(conf, interface, 'bonding'): - if bond['ifname'] == tmp: - continue - interface_config['is_bond_member'] = tmp + if ifname in tmp: + del tmp[ifname] + if tmp: bond['member']['interface'][interface].update({'is_bond_member' : tmp}) # Check if member interface is used as source-interface on another interface tmp = is_source_interface(conf, interface) - if tmp: interface_config['is_source_interface'] = tmp + if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp}) # bond members must not have an assigned address tmp = has_address_configured(conf, interface) - if tmp: interface_config['has_address'] = {} + if tmp: bond['member']['interface'][interface].update({'has_address' : ''}) # bond members must not have a VRF attached tmp = has_vrf_configured(conf, interface) - if tmp: interface_config['has_vrf'] = {} + if tmp: bond['member']['interface'][interface].update({'has_vrf' : ''}) + return bond diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index 637db442a..95dcc543e 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -25,6 +25,7 @@ from vyos.configdict import has_vlan_subinterface_configured from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import BridgeIf from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured @@ -74,8 +75,9 @@ def get_config(config=None): for interface in list(bridge['member']['interface']): # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp and bridge['ifname'] not in tmp: - bridge['member']['interface'][interface].update({'is_bridge_member' : tmp}) + if ifname in tmp: + del tmp[ifname] + if tmp: bridge['member']['interface'][interface].update({'is_bridge_member' : tmp}) # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') @@ -135,6 +137,7 @@ def verify(bridge): verify_dhcpv6(bridge) verify_vrf(bridge) + verify_mtu_ipv6(bridge) verify_mirror_redirect(bridge) ifname = bridge['ifname'] diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py index 007708d4a..1c5b4d0e7 100755 --- a/src/conf_mode/interfaces_geneve.py +++ b/src/conf_mode/interfaces_geneve.py @@ -47,7 +47,7 @@ def get_config(config=None): # GENEVE interfaces are picky and require recreation if certain parameters # change. But a GENEVE interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['remote', 'vni', 'parameters']: + for cli_option in ['remote', 'vni', 'parameters', 'port']: if is_node_changed(conf, base + [ifname, cli_option]): geneve.update({'rebuild_required': {}}) diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 8c1213e2b..a9b4e570d 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -32,6 +32,7 @@ from vyos.base import DeprecationWarning from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdiff import get_config_diff from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect @@ -94,6 +95,23 @@ def get_config(config=None): if 'deleted' in openvpn: return openvpn + if not is_node_changed(conf, base) and dict_search_args(openvpn, 'tls'): + diff = get_config_diff(conf) + if diff.get_child_nodes_diff(['pki'], recursive=True).get('add') == ['ca', 'certificate']: + crl_path = os.path.join(cfg_dir, f'{ifname}_crl.pem') + if os.path.exists(crl_path): + # do not restart service when changed only CRL and crl file already exist + openvpn.update({'no_restart_crl': True}) + for rec in diff.get_child_nodes_diff(['pki', 'ca'], recursive=True).get('add'): + if diff.get_child_nodes_diff(['pki', 'ca', rec], recursive=True).get('add') != ['crl']: + openvpn.update({'no_restart_crl': False}) + break + if openvpn.get('no_restart_crl'): + for rec in diff.get_child_nodes_diff(['pki', 'certificate'], recursive=True).get('add'): + if diff.get_child_nodes_diff(['pki', 'certificate', rec], recursive=True).get('add') != ['revoke']: + openvpn.update({'no_restart_crl': False}) + break + if is_node_changed(conf, base + [ifname, 'openvpn-option']): openvpn.update({'restart_required': {}}) if is_node_changed(conf, base + [ifname, 'enable-dco']): @@ -786,10 +804,12 @@ def apply(openvpn): # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - action = 'reload-or-restart' - if 'restart_required' in openvpn: - action = 'restart' - call(f'systemctl {action} openvpn@{interface}.service') + + if not openvpn.get('no_restart_crl'): + action = 'reload-or-restart' + if 'restart_required' in openvpn: + action = 'restart' + call(f'systemctl {action} openvpn@{interface}.service') o = VTunIf(**openvpn) o.update(openvpn) diff --git a/src/conf_mode/interfaces_pseudo-ethernet.py b/src/conf_mode/interfaces_pseudo-ethernet.py index 446beffd3..b066fd542 100755 --- a/src/conf_mode/interfaces_pseudo-ethernet.py +++ b/src/conf_mode/interfaces_pseudo-ethernet.py @@ -27,6 +27,7 @@ from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_source_interface from vyos.configverify import verify_vlan_config from vyos.configverify import verify_mtu_parent +from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.ifconfig import MACVLANIf from vyos.utils.network import interface_exists @@ -71,6 +72,7 @@ def verify(peth): verify_vrf(peth) verify_address(peth) verify_mtu_parent(peth, peth['parent']) + verify_mtu_ipv6(peth) verify_mirror_redirect(peth) # use common function to verify VLAN configuration verify_vlan_config(peth) 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_virtual-ethernet.py b/src/conf_mode/interfaces_virtual-ethernet.py index cb6104f59..59ce474fc 100755 --- a/src/conf_mode/interfaces_virtual-ethernet.py +++ b/src/conf_mode/interfaces_virtual-ethernet.py @@ -23,6 +23,7 @@ from vyos.configdict import get_interface_dict from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import VethIf from vyos.utils.network import interface_exists airbag.enable() @@ -62,6 +63,7 @@ def verify(veth): return None verify_vrf(veth) + verify_mtu_ipv6(veth) verify_address(veth) if 'peer_name' not in veth: diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py index 20629c6c1..915bde066 100755 --- a/src/conf_mode/interfaces_vti.py +++ b/src/conf_mode/interfaces_vti.py @@ -20,6 +20,7 @@ from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import VTIIf from vyos import ConfigError from vyos import airbag @@ -40,6 +41,7 @@ def get_config(config=None): def verify(vti): verify_vrf(vti) + verify_mtu_ipv6(vti) verify_mirror_redirect(vti) return None diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py index 68646e8ff..256b65708 100755 --- a/src/conf_mode/interfaces_vxlan.py +++ b/src/conf_mode/interfaces_vxlan.py @@ -95,6 +95,8 @@ def verify(vxlan): if 'group' in vxlan: if 'source_interface' not in vxlan: raise ConfigError('Multicast VXLAN requires an underlaying interface') + if 'remote' in vxlan: + raise ConfigError('Both group and remote cannot be specified') verify_source_interface(vxlan) if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan): diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index b6fd6b0b2..3ca6ecdca 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -19,6 +19,9 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdict import is_source_interface +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -29,8 +32,10 @@ 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() @@ -54,11 +59,31 @@ 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) + + # Check if interface is used as source-interface on VXLAN interface + tmp = is_source_interface(conf, ifname, 'vxlan') + if tmp: + if 'deleted' not in wireguard: + set_dependents('vxlan', conf, tmp) + else: + wireguard['is_source_interface'] = tmp + return wireguard + def verify(wireguard): if 'deleted' in wireguard: verify_bridge_delete(wireguard) + if 'is_source_interface' in wireguard: + raise ConfigError( + f'Interface "{wireguard["ifname"]}" cannot be deleted as it is used ' + f'as source interface for "{wireguard["is_source_interface"]}"!' + ) return None verify_mtu_ipv6(wireguard) @@ -72,7 +97,7 @@ def verify(wireguard): if 'port' in wireguard and 'port_changed' in wireguard: listen_port = int(wireguard['port']) - if check_port_availability('0.0.0.0', listen_port, 'udp') is not True: + if check_port_availability(None, listen_port, protocol='udp') is not True: raise ConfigError(f'UDP port {listen_port} is busy or unavailable and ' 'cannot be used for the interface!') @@ -82,28 +107,41 @@ 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']) + def generate(wireguard): return None + def apply(wireguard): check_kmod('wireguard') @@ -122,8 +160,28 @@ 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') + + call_dependents() + return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/interfaces_wwan.py b/src/conf_mode/interfaces_wwan.py index 230eb14d6..ddbebfb4a 100755 --- a/src/conf_mode/interfaces_wwan.py +++ b/src/conf_mode/interfaces_wwan.py @@ -26,6 +26,7 @@ from vyos.configverify import verify_authentication from vyos.configverify import verify_interface_exists from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import WWANIf from vyos.utils.dict import dict_search from vyos.utils.process import cmd @@ -98,6 +99,7 @@ def verify(wwan): verify_interface_exists(wwan, ifname) verify_authentication(wwan) verify_vrf(wwan) + verify_mtu_ipv6(wwan) verify_mirror_redirect(wwan) return None diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py index 45042dd52..504a90596 100644 --- a/src/conf_mode/load-balancing_haproxy.py +++ b/src/conf_mode/load-balancing_haproxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright (C) 2023-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,6 +19,7 @@ import os from sys import exit from shutil import rmtree +from vyos.defaults import systemd_services from vyos.config import Config from vyos.configverify import verify_pki_certificate from vyos.configverify import verify_pki_ca_certificate @@ -39,7 +40,6 @@ airbag.enable() load_balancing_dir = '/run/haproxy' load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg' -systemd_service = 'haproxy.service' systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf' def get_config(config=None): @@ -65,29 +65,39 @@ def verify(lb): return None if 'backend' not in lb or 'service' not in lb: - raise ConfigError(f'"service" and "backend" must be configured!') + raise ConfigError('Both "service" and "backend" must be configured!') for front, front_config in lb['service'].items(): if 'port' not in front_config: raise ConfigError(f'"{front} service port" must be configured!') # Check if bind address:port are used by another service - tmp_address = front_config.get('address', '0.0.0.0') + tmp_address = front_config.get('address', None) tmp_port = front_config['port'] if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \ not is_listen_port_bind_service(int(tmp_port), 'haproxy'): - raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service') + raise ConfigError(f'TCP port "{tmp_port}" is used by another service') + + if 'http_compression' in front_config: + if front_config['mode'] != 'http': + raise ConfigError(f'service {front} must be set to http mode to use http-compression!') + if len(front_config['http_compression']['mime_type']) == 0: + raise ConfigError(f'service {front} must have at least one mime-type configured to use' + f'http_compression!') + + for cert in dict_search('ssl.certificate', front_config) or []: + verify_pki_certificate(lb, cert) for back, back_config in lb['backend'].items(): if 'http_check' in back_config: http_check = back_config['http_check'] if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']: - raise ConfigError(f'"expect status" and "expect string" can not be configured together!') + raise ConfigError('"expect status" and "expect string" can not be configured together!') if 'health_check' in back_config: if back_config['mode'] != 'tcp': raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' + - f'health-check whilst in TCP mode!') + 'health-check whilst in TCP mode!') if 'http_check' in back_config: raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!') @@ -105,20 +115,15 @@ def verify(lb): if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']): raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') + tmp = dict_search('ssl.ca_certificate', back_config) + if tmp: verify_pki_ca_certificate(lb, tmp) + # Check if http-response-headers are configured in any frontend/backend where mode != http for group in ['service', 'backend']: for config_name, config in lb[group].items(): if 'http_response_headers' in config and config['mode'] != 'http': raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!') - for front, front_config in lb['service'].items(): - for cert in dict_search('ssl.certificate', front_config) or []: - verify_pki_certificate(lb, cert) - - for back, back_config in lb['backend'].items(): - tmp = dict_search('ssl.ca_certificate', back_config) - if tmp: verify_pki_ca_certificate(lb, tmp) - def generate(lb): if not lb: @@ -186,12 +191,11 @@ def generate(lb): return None def apply(lb): + action = 'stop' + if lb: + action = 'reload-or-restart' call('systemctl daemon-reload') - if not lb: - call(f'systemctl stop {systemd_service}') - else: - call(f'systemctl reload-or-restart {systemd_service}') - + call(f'systemctl {action} {systemd_services["haproxy"]}') return None diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py index 5da0b906b..92d9acfba 100755 --- a/src/conf_mode/load-balancing_wan.py +++ b/src/conf_mode/load-balancing_wan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-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,24 +14,16 @@ # 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 shutil import rmtree -from vyos.base import Warning from vyos.config import Config from vyos.configdep import set_dependents, call_dependents from vyos.utils.process import cmd -from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() -load_balancing_dir = '/run/load-balance' -load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf' -systemd_service = 'vyos-wan-load-balance.service' - +service = 'vyos-wan-load-balance.service' def get_config(config=None): if config: @@ -40,6 +32,7 @@ def get_config(config=None): conf = Config() base = ['load-balancing', 'wan'] + lb = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, @@ -59,87 +52,61 @@ def verify(lb): if not lb: return None - if 'interface_health' not in lb: - raise ConfigError( - 'A valid WAN load-balance configuration requires an interface with a nexthop!' - ) - - for interface, interface_config in lb['interface_health'].items(): - if 'nexthop' not in interface_config: - raise ConfigError( - f'interface-health {interface} nexthop must be specified!') - - if 'test' in interface_config: - for test_rule, test_config in interface_config['test'].items(): - if 'type' in test_config: - if test_config['type'] == 'user-defined' and 'test_script' not in test_config: - raise ConfigError( - f'test {test_rule} script must be defined for test-script!' - ) - - if 'rule' not in lb: - Warning( - 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!' - ) + if 'interface_health' in lb: + for ifname, health_conf in lb['interface_health'].items(): + if 'nexthop' not in health_conf: + raise ConfigError(f'Nexthop must be configured for interface {ifname}') + + if 'test' not in health_conf: + continue + + for test_id, test_conf in health_conf['test'].items(): + if 'type' not in test_conf: + raise ConfigError(f'No type configured for health test on interface {ifname}') + + if test_conf['type'] == 'user-defined' and 'test_script' not in test_conf: + raise ConfigError(f'Missing user-defined script for health test on interface {ifname}') else: - for rule, rule_config in lb['rule'].items(): - if 'inbound_interface' not in rule_config: - raise ConfigError(f'rule {rule} inbound-interface must be specified!') - if {'failover', 'exclude'} <= set(rule_config): - raise ConfigError(f'rule {rule} failover cannot be configured with exclude!') - if {'limit', 'exclude'} <= set(rule_config): - raise ConfigError(f'rule {rule} limit cannot be used with exclude!') - if 'interface' not in rule_config: - if 'exclude' not in rule_config: - Warning( - f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule' - ) - for direction in {'source', 'destination'}: - if direction in rule_config: - if 'protocol' in rule_config and 'port' in rule_config[ - direction]: - if rule_config['protocol'] not in {'tcp', 'udp'}: - raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"') + raise ConfigError('Interface health tests must be configured') + if 'rule' in lb: + for rule_id, rule_conf in lb['rule'].items(): + if 'interface' not in rule_conf and 'exclude' not in rule_conf: + raise ConfigError(f'Interface or exclude not specified on load-balancing wan rule {rule_id}') -def generate(lb): - if not lb: - # Delete /run/load-balance/wlb.conf - if os.path.isfile(load_balancing_conf_file): - os.unlink(load_balancing_conf_file) - # Delete old directories - if os.path.isdir(load_balancing_dir): - rmtree(load_balancing_dir, ignore_errors=True) - if os.path.exists('/var/run/load-balance/wlb.out'): - os.unlink('/var/run/load-balance/wlb.out') + if 'failover' in rule_conf and 'exclude' in rule_conf: + raise ConfigError(f'Failover cannot be configured with exclude on load-balancing wan rule {rule_id}') - return None + if 'limit' in rule_conf: + if 'exclude' in rule_conf: + raise ConfigError(f'Limit cannot be configured with exclude on load-balancing wan rule {rule_id}') - # Create load-balance dir - if not os.path.isdir(load_balancing_dir): - os.mkdir(load_balancing_dir) + if 'rate' in rule_conf['limit'] and 'period' not in rule_conf['limit']: + raise ConfigError(f'Missing "limit period" on load-balancing wan rule {rule_id}') - render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb) + if 'period' in rule_conf['limit'] and 'rate' not in rule_conf['limit']: + raise ConfigError(f'Missing "limit rate" on load-balancing wan rule {rule_id}') - return None + for direction in ['source', 'destination']: + if direction in rule_conf: + if 'port' in rule_conf[direction]: + if 'protocol' not in rule_conf: + raise ConfigError(f'Protocol required to specify port on load-balancing wan rule {rule_id}') + + if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + raise ConfigError(f'Protocol must be tcp, udp or tcp_udp to specify port on load-balancing wan rule {rule_id}') +def generate(lb): + return None def apply(lb): if not lb: - try: - cmd(f'systemctl stop {systemd_service}') - except Exception as e: - print(f"Error message: {e}") - + cmd(f'sudo systemctl stop {service}') else: - cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1') - cmd(f'systemctl restart {systemd_service}') + cmd(f'sudo systemctl restart {service}') call_dependents() - return None - - if __name__ == '__main__': try: c = get_config() 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/nat66.py b/src/conf_mode/nat66.py index 95dfae3a5..c65950c9e 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -92,6 +92,10 @@ def verify(nat): if prefix != None: if not is_ipv6(prefix): raise ConfigError(f'{err_msg} source-prefix not specified') + + if 'destination' in config and 'group' in config['destination']: + if len({'address_group', 'network_group', 'domain_group'} & set(config['destination']['group'])) > 1: + raise ConfigError('Only one address-group, network-group or domain-group can be specified') if dict_search('destination.rule', nat): for rule, config in dict_search('destination.rule', nat).items(): diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index acea2c9be..869518dd9 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.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 @@ -19,6 +19,7 @@ import os from sys import argv from sys import exit +from vyos.base import Message from vyos.config import Config from vyos.config import config_dict_merge from vyos.configdep import set_dependents @@ -27,6 +28,8 @@ from vyos.configdict import node_changed from vyos.configdiff import Diff from vyos.configdiff import get_config_diff from vyos.defaults import directories +from vyos.defaults import internal_ports +from vyos.defaults import systemd_services from vyos.pki import encode_certificate from vyos.pki import is_ca_certificate from vyos.pki import load_certificate @@ -42,9 +45,11 @@ from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.file import read_file +from vyos.utils.network import check_port_availability from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() @@ -128,8 +133,20 @@ def certbot_request(name: str, config: dict, dry_run: bool=True): f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\ f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\ f'{domains}' + + listen_address = None if 'listen_address' in config: - tmp += f' --http-01-address {config["listen_address"]}' + listen_address = config['listen_address'] + + # When ACME is used behind a reverse proxy, we always bind to localhost + # whatever the CLI listen-address is configured for. + if ('haproxy' in dict_search('used_by', config) and + is_systemd_service_running(systemd_services['haproxy']) and + not check_port_availability(listen_address, 80)): + tmp += f' --http-01-address 127.0.0.1 --http-01-port {internal_ports["certbot_haproxy"]}' + elif listen_address: + tmp += f' --http-01-address {listen_address}' + # verify() does not need to actually request a cert but only test for plausability if dry_run: tmp += ' --dry-run' @@ -150,14 +167,18 @@ def get_config(config=None): if len(argv) > 1 and argv[1] == 'certbot_renew': pki['certbot_renew'] = {} - changed_keys = ['ca', 'certificate', 'dh', 'key-pair', 'openssh', 'openvpn'] + # Walk through the list of sync_translate mapping and build a list + # which is later used to check if the node was changed in the CLI config + changed_keys = [] + for value in sync_translate.values(): + if value not in changed_keys: + changed_keys.append(value) + # Check for changes to said given keys in the CLI config for key in changed_keys: tmp = node_changed(conf, base + [key], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({key.replace('-', '_') : tmp}) # We only merge on the defaults of there is a configuration at all @@ -219,8 +240,8 @@ def get_config(config=None): continue path = search['path'] - path_str = ' '.join(path + found_path) - #print(f'PKI: Updating config: {path_str} {item_name}') + path_str = ' '.join(path + found_path).replace('_','-') + Message(f'Updating configuration: "{path_str} {item_name}"') if path[0] == 'interfaces': ifname = found_path[0] @@ -230,6 +251,24 @@ def get_config(config=None): if not D.node_changed_presence(path): set_dependents(path[1], conf) + # Check PKI certificates if they are auto-generated by ACME. If they are, + # traverse the current configuration and determine the service where the + # certificate is used by. + # Required to check if we might need to run certbot behing a reverse proxy. + if 'certificate' in pki: + for name, cert_config in pki['certificate'].items(): + if 'acme' not in cert_config: + continue + if not dict_search('system.load_balancing.haproxy', pki): + continue + used_by = [] + for cert_list, _ in dict_search_recursive( + pki['system']['load_balancing']['haproxy'], 'certificate'): + if name in cert_list: + used_by.append('haproxy') + if used_by: + pki['certificate'][name]['acme'].update({'used_by': used_by}) + return pki def is_valid_certificate(raw_data): @@ -321,6 +360,15 @@ def verify(pki): raise ConfigError(f'An email address is required to request '\ f'certificate for "{name}" via ACME!') + listen_address = None + if 'listen_address' in cert_conf['acme']: + listen_address = cert_conf['acme']['listen_address'] + + if 'used_by' not in cert_conf['acme']: + if not check_port_availability(listen_address, 80): + raise ConfigError('Port 80 is already in use and not available '\ + f'to provide ACME challenge for "{name}"!') + if 'certbot_renew' not in pki: # Only run the ACME command if something on this entity changed, # as this is time intensive @@ -374,27 +422,35 @@ def verify(pki): for search in sync_search: for key in search['keys']: changed_key = sync_translate[key] - if changed_key not in pki['changed']: continue - for item_name in pki['changed'][changed_key]: node_present = False if changed_key == 'openvpn': node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) else: node_present = dict_search_args(pki, changed_key, item_name) + # If the node is still present, we can skip the check + # as we are not deleting it + if node_present: + continue - if not node_present: - search_dict = dict_search_args(pki['system'], *search['path']) - - if not search_dict: - continue + search_dict = dict_search_args(pki['system'], *search['path']) + if not search_dict: + continue - for found_name, found_path in dict_search_recursive(search_dict, key): - if found_name == item_name: - path_str = " ".join(search['path'] + found_path) - raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"') + for found_name, found_path in dict_search_recursive(search_dict, key): + # Check if the name matches either by string compare, or beeing + # part of a list + if ((isinstance(found_name, str) and found_name == item_name) or + (isinstance(found_name, list) and item_name in found_name)): + # We do not support _ in CLI paths - this is only a convenience + # as we mangle all - to _, now it's time to reverse this! + path_str = ' '.join(search['path'] + found_path).replace('_','-') + object = changed_key.replace('_','-') + tmp = f'Embedded PKI {object} with name "{item_name}" is still '\ + f'in use by CLI path "{path_str}"' + raise ConfigError(tmp) return None @@ -440,13 +496,21 @@ def generate(pki): for name, cert_conf in pki['certificate'].items(): if 'acme' in cert_conf: certbot_list.append(name) - # generate certificate if not found on disk + # There is no ACME/certbot managed certificate presend on the + # system, generate it if name not in certbot_list_on_disk: certbot_request(name, cert_conf['acme'], dry_run=False) + # Now that the certificate was properly generated we have + # the PEM files on disk. We need to add the certificate to + # certbot_list_on_disk to automatically import the CA chain + certbot_list_on_disk.append(name) + # We alredy had an ACME managed certificate on the system, but + # something changed in the configuration elif changed_certificates != None and name in changed_certificates: - # when something for the certificate changed, we should delete it + # Delete old ACME certificate first if name in certbot_list_on_disk: certbot_delete(name) + # Request new certificate via certbot certbot_request(name, cert_conf['acme'], dry_run=False) # Cleanup certbot configuration and certificates if no longer in use by CLI @@ -482,7 +546,7 @@ def generate(pki): if not ca_cert_present: tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate') if not bool(tmp) or tmp != cert_chain_base64: - print(f'Adding/replacing automatically imported CA certificate for "{cert}" ...') + Message(f'Add/replace automatically imported CA certificate for "{cert}"...') add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64) return None diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py index 223175b8a..521764896 100755 --- a/src/conf_mode/policy_route.py +++ b/src/conf_mode/policy_route.py @@ -21,13 +21,16 @@ from sys import exit from vyos.base import Warning from vyos.config import Config +from vyos.configdiff import get_config_diff, Diff from vyos.template import render from vyos.utils.dict import dict_search_args +from vyos.utils.dict import dict_search_recursive from vyos.utils.process import cmd from vyos.utils.process import run from vyos.utils.network import get_vrf_tableid from vyos.defaults import rt_global_table from vyos.defaults import rt_global_vrf +from vyos.firewall import geoip_update from vyos import ConfigError from vyos import airbag airbag.enable() @@ -43,6 +46,43 @@ valid_groups = [ 'interface_group' ] +def geoip_updated(conf, policy): + diff = get_config_diff(conf) + node_diff = diff.get_child_nodes_diff(['policy'], expand_nodes=Diff.DELETE, recursive=True) + + out = { + 'name': [], + 'ipv6_name': [], + 'deleted_name': [], + 'deleted_ipv6_name': [] + } + updated = False + + for key, path in dict_search_recursive(policy, 'geoip'): + set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}' + if (path[0] == 'route'): + out['name'].append(set_name) + elif (path[0] == 'route6'): + set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}' + out['ipv6_name'].append(set_name) + + updated = True + + if 'delete' in node_diff: + for key, path in dict_search_recursive(node_diff['delete'], 'geoip'): + set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}' + if (path[0] == 'route'): + out['deleted_name'].append(set_name) + elif (path[0] == 'route6'): + set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}' + out['deleted_ipv6_name'].append(set_name) + updated = True + + if updated: + return out + + return False + def get_config(config=None): if config: conf = config @@ -60,6 +100,7 @@ def get_config(config=None): if 'dynamic_group' in policy['firewall_group']: del policy['firewall_group']['dynamic_group'] + policy['geoip_updated'] = geoip_updated(conf, policy) return policy def verify_rule(policy, name, rule_conf, ipv6, rule_id): @@ -203,6 +244,12 @@ def apply(policy): apply_table_marks(policy) + if policy['geoip_updated']: + # Call helper script to Update set contents + if 'name' in policy['geoip_updated'] or 'ipv6_name' in policy['geoip_updated']: + print('Updating GeoIP. Please wait...') + geoip_update(policy=policy) + return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index c4af717af..99d8eb9d1 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -413,15 +413,19 @@ def verify(config_dict): verify_route_map(afi_config['route_map'][tmp], bgp) if 'route_reflector_client' in afi_config: - peer_group_as = peer_config.get('remote_as') + peer_as = peer_config.get('remote_as') - if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): + if peer_as is not None and (peer_as != 'internal' and peer_as != bgp['system_as']): raise ConfigError('route-reflector-client only supported for iBGP peers') else: + # Check into the peer group for the remote as, if we are in a peer group, check in peer itself if 'peer_group' in peer_config: peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) - if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): - raise ConfigError('route-reflector-client only supported for iBGP peers') + elif neighbor == 'peer_group': + peer_group_as = peer_config.get('remote_as') + + if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): + raise ConfigError('route-reflector-client only supported for iBGP peers') # T5833 not all AFIs are supported for VRF if 'vrf' in bgp and 'address_family' in peer_config: @@ -523,12 +527,21 @@ def verify(config_dict): raise ConfigError( 'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!') + if (dict_search('route_map.vrf.import', afi_config) is not None + or dict_search('import.vrf', afi_config) is not None): # FRR error: please unconfigure vpn to vrf commands before # using import vrf commands - if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None: + if (dict_search('import.vpn', afi_config) is not None + or dict_search('export.vpn', afi_config) is not None): raise ConfigError('Please unconfigure VPN to VRF commands before '\ 'using "import vrf" commands!') + if (dict_search('route_map.vpn.import', afi_config) is not None + or dict_search('route_map.vpn.export', afi_config) is not None) : + raise ConfigError('Please unconfigure route-map VPN to VRF commands before '\ + 'using "import vrf" commands!') + + # Verify that the export/import route-maps do exist for export_import in ['export', 'import']: tmp = dict_search(f'route_map.vpn.{export_import}', afi_config) 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_console-server.py b/src/conf_mode/service_console-server.py index b112add3f..b83c6dfb1 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 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 @@ -98,6 +98,12 @@ def generate(proxy): return None def apply(proxy): + if not os.path.exists('/etc/dropbear/dropbear_rsa_host_key'): + call('dropbearkey -t rsa -s 4096 -f /etc/dropbear/dropbear_rsa_host_key') + + if not os.path.exists('/etc/dropbear/dropbear_ecdsa_host_key'): + call('dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key') + call('systemctl daemon-reload') call('systemctl stop dropbear@*.service conserver-server.service') diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index 9c59aa63d..99c7e6a1f 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,20 +38,21 @@ 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' ctrl_socket = '/run/kea/dhcp4-ctrl-socket' config_file = '/run/kea/kea-dhcp4.conf' +config_file_d2 = '/run/kea/kea-dhcp-ddns.conf' lease_file = '/config/dhcp/dhcp4-leases.csv' lease_file_glob = '/config/dhcp/dhcp4-leases*' -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,40 @@ 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_ddns_domain_servers(domain_type, domain): + if 'dns_server' in domain: + invalid_servers = [] + for server_no, server_config in domain['dns_server'].items(): + if 'address' not in server_config: + invalid_servers.append(server_no) + if len(invalid_servers) > 0: + raise ConfigError(f'{domain_type} DNS servers {", ".join(invalid_servers)} in DDNS configuration need to have an IP address') + return None + def verify(dhcp): # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: @@ -170,13 +188,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 +207,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 +227,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 +275,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 +293,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 +339,118 @@ 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') + if 'dynamic_dns_update' in dhcp: + ddns = dhcp['dynamic_dns_update'] + if 'tsig_key' in ddns: + invalid_keys = [] + for tsig_key_name, tsig_key_config in ddns['tsig_key'].items(): + if not ('algorithm' in tsig_key_config and 'secret' in tsig_key_config): + invalid_keys.append(tsig_key_name) + if len(invalid_keys) > 0: + raise ConfigError(f'Both algorithm and secret need to be set for TSIG keys: {", ".join(invalid_keys)}') + + if 'forward_domain' in ddns: + verify_ddns_domain_servers('Forward', ddns['forward_domain']) + + if 'reverse_domain' in ddns: + verify_ddns_domain_servers('Reverse', ddns['reverse_domain']) + return None + def generate(dhcp): # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: @@ -382,8 +482,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,19 +495,36 @@ 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( + config_file, + 'dhcp-server/kea-dhcp4.conf.j2', + dhcp, + user=user_group, + group=user_group, + ) + if 'dynamic_dns_update' in dhcp: + render( + config_file_d2, + 'dhcp-server/kea-dhcp-ddns.conf.j2', + dhcp, + user=user_group, + group=user_group + ) return None + def apply(dhcp): - services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server'] + services = ['kea-dhcp4-server', 'kea-dhcp-ddns-server'] if not dhcp or 'disable' in dhcp: for service in services: @@ -420,13 +541,11 @@ def apply(dhcp): if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp: action = 'stop' - if service == 'kea-ctrl-agent' and 'high_availability' not in dhcp: - action = 'stop' - call(f'systemctl {action} {service}.service') return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py index e3bdbc9f8..5636d6f83 100755 --- a/src/conf_mode/service_dns_forwarding.py +++ b/src/conf_mode/service_dns_forwarding.py @@ -366,6 +366,13 @@ def apply(dns): hc.add_name_server_tags_recursor(['dhcp-' + interface, 'dhcpv6-' + interface ]) + # add dhcp interfaces + if 'dhcp' in dns: + for interface in dns['dhcp']: + if interface_exists(interface): + hc.add_name_server_tags_recursor(['dhcp-' + interface, + 'dhcpv6-' + interface ]) + # hostsd will generate the forward-zones file # the list and keys() are required as get returns a dict, not list hc.delete_forward_zones(list(hc.get_forward_zones().keys())) diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index 9e58b4c72..2123823f4 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf from vyos.configverify import verify_pki_certificate from vyos.configverify import verify_pki_ca_certificate from vyos.configverify import verify_pki_dh_parameters +from vyos.configdiff import get_config_diff from vyos.defaults import api_config_state from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key @@ -79,6 +80,14 @@ def get_config(config=None): # merge CLI and default dictionary https = config_dict_merge(default_values, https) + + # some settings affecting nginx will require a restart: + # for example, a reload will not suffice when binding the listen address + # after nginx has started and dropped privileges; add flag here + diff = get_config_diff(conf) + children_changed = diff.node_changed_children(base) + https['nginx_restart_required'] = bool(set(children_changed) != set(['api'])) + return https def verify(https): @@ -208,7 +217,10 @@ def apply(https): elif is_systemd_service_active(http_api_service_name): call(f'systemctl stop {http_api_service_name}') - call(f'systemctl reload-or-restart {https_service_name}') + if https['nginx_restart_required']: + call(f'systemctl restart {https_service_name}') + else: + call(f'systemctl reload-or-restart {https_service_name}') if __name__ == '__main__': try: diff --git a/src/conf_mode/service_ids_ddos-protection.py b/src/conf_mode/service_ids_ddos-protection.py deleted file mode 100755 index 276a71fcb..000000000 --- a/src/conf_mode/service_ids_ddos-protection.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2023 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os - -from sys import exit - -from vyos.config import Config -from vyos.template import render -from vyos.utils.process import call -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -config_file = r'/run/fastnetmon/fastnetmon.conf' -networks_list = r'/run/fastnetmon/networks_list' -excluded_networks_list = r'/run/fastnetmon/excluded_networks_list' -attack_dir = '/var/log/fastnetmon_attacks' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['service', 'ids', 'ddos-protection'] - if not conf.exists(base): - return None - - fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - - return fastnetmon - -def verify(fastnetmon): - if not fastnetmon: - return None - - if 'mode' not in fastnetmon: - raise ConfigError('Specify operating mode!') - - if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon: - raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring") - - if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}): - raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'") - - if 'alert_script' in fastnetmon: - if os.path.isfile(fastnetmon['alert_script']): - # Check script permissions - if not os.access(fastnetmon['alert_script'], os.X_OK): - raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script'])) - else: - raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon)) - -def generate(fastnetmon): - if not fastnetmon: - for file in [config_file, networks_list]: - if os.path.isfile(file): - os.unlink(file) - - return None - - # Create dir for log attack details - if not os.path.exists(attack_dir): - os.mkdir(attack_dir) - - render(config_file, 'ids/fastnetmon.j2', fastnetmon) - render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon) - render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon) - return None - -def apply(fastnetmon): - systemd_service = 'fastnetmon.service' - if not fastnetmon: - # Stop fastnetmon service if removed - call(f'systemctl stop {systemd_service}') - else: - call(f'systemctl reload-or-restart {systemd_service}') - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/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/service_monitoring_prometheus.py b/src/conf_mode/service_monitoring_prometheus.py index e0a9fc4ef..9a07d8593 100755 --- a/src/conf_mode/service_monitoring_prometheus.py +++ b/src/conf_mode/service_monitoring_prometheus.py @@ -26,15 +26,18 @@ from vyos.utils.process import call from vyos import ConfigError from vyos import airbag - airbag.enable() node_exporter_service_file = '/etc/systemd/system/node_exporter.service' node_exporter_systemd_service = 'node_exporter.service' +node_exporter_collector_path = '/run/node_exporter/collector' frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' frr_exporter_systemd_service = 'frr_exporter.service' +blackbox_exporter_service_file = '/etc/systemd/system/blackbox_exporter.service' +blackbox_exporter_systemd_service = 'blackbox_exporter.service' + def get_config(config=None): if config: @@ -57,6 +60,12 @@ def get_config(config=None): if tmp: monitoring.update({'frr_exporter_restart_required': {}}) + tmp = False + for node in ['vrf', 'config-file']: + tmp = tmp or is_node_changed(conf, base + ['blackbox-exporter', node]) + if tmp: + monitoring.update({'blackbox_exporter_restart_required': {}}) + return monitoring @@ -70,6 +79,22 @@ def verify(monitoring): if 'frr_exporter' in monitoring: verify_vrf(monitoring['frr_exporter']) + if 'blackbox_exporter' in monitoring: + verify_vrf(monitoring['blackbox_exporter']) + + if ( + 'modules' in monitoring['blackbox_exporter'] + and 'dns' in monitoring['blackbox_exporter']['modules'] + and 'name' in monitoring['blackbox_exporter']['modules']['dns'] + ): + for mod_name, mod_config in monitoring['blackbox_exporter']['modules'][ + 'dns' + ]['name'].items(): + if 'query_name' not in mod_config: + raise ConfigError( + f'query name not specified in dns module {mod_name}' + ) + return None @@ -84,6 +109,11 @@ def generate(monitoring): if os.path.isfile(frr_exporter_service_file): os.unlink(frr_exporter_service_file) + if not monitoring or 'blackbox_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(blackbox_exporter_service_file): + os.unlink(blackbox_exporter_service_file) + if not monitoring: return None @@ -94,6 +124,13 @@ def generate(monitoring): 'prometheus/node_exporter.service.j2', monitoring['node_exporter'], ) + if ( + 'collectors' in monitoring['node_exporter'] + and 'textfile' in monitoring['node_exporter']['collectors'] + ): + # Create textcollector folder + if not os.path.isdir(node_exporter_collector_path): + os.makedirs(node_exporter_collector_path) if 'frr_exporter' in monitoring: # Render frr_exporter service_file @@ -103,6 +140,20 @@ def generate(monitoring): monitoring['frr_exporter'], ) + if 'blackbox_exporter' in monitoring: + # Render blackbox_exporter service_file + render( + blackbox_exporter_service_file, + 'prometheus/blackbox_exporter.service.j2', + monitoring['blackbox_exporter'], + ) + # Render blackbox_exporter config file + render( + '/run/blackbox_exporter/config.yml', + 'prometheus/blackbox_exporter.yml.j2', + monitoring['blackbox_exporter'], + ) + return None @@ -113,6 +164,8 @@ def apply(monitoring): call(f'systemctl stop {node_exporter_systemd_service}') if not monitoring or 'frr_exporter' not in monitoring: call(f'systemctl stop {frr_exporter_systemd_service}') + if not monitoring or 'blackbox_exporter' not in monitoring: + call(f'systemctl stop {blackbox_exporter_systemd_service}') if not monitoring: return @@ -133,6 +186,14 @@ def apply(monitoring): call(f'systemctl {systemd_action} {frr_exporter_systemd_service}') + if 'blackbox_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'blackbox_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {blackbox_exporter_systemd_service}') + if __name__ == '__main__': try: diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py index 1174b1238..c64c59af7 100755 --- a/src/conf_mode/service_snmp.py +++ b/src/conf_mode/service_snmp.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 @@ -22,6 +22,7 @@ from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_vrf +from vyos.defaults import systemd_services from vyos.snmpv3_hashgen import plaintext_to_md5 from vyos.snmpv3_hashgen import plaintext_to_sha1 from vyos.snmpv3_hashgen import random @@ -43,7 +44,7 @@ config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' default_script_dir = r'/config/user-data/' systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf' -systemd_service = 'snmpd.service' +systemd_service = systemd_services['snmpd'] def get_config(config=None): if config: @@ -146,6 +147,9 @@ def verify(snmp): return None if 'user' in snmp['v3']: + if 'engineid' not in snmp['v3']: + raise ConfigError(f'EngineID must be configured for SNMPv3!') + for user, user_config in snmp['v3']['user'].items(): if 'group' not in user_config: raise ConfigError(f'Group membership required for user "{user}"!') 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/system_host-name.py b/src/conf_mode/system_host-name.py index 3f245f166..de4accda2 100755 --- a/src/conf_mode/system_host-name.py +++ b/src/conf_mode/system_host-name.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 @@ -23,6 +23,7 @@ import vyos.hostsd_client from vyos.base import Warning from vyos.config import Config from vyos.configdict import leaf_node_changed +from vyos.defaults import systemd_services from vyos.ifconfig import Section from vyos.template import is_ip from vyos.utils.process import cmd @@ -174,11 +175,13 @@ def apply(config): # Restart services that use the hostname if hostname_new != hostname_old: - call("systemctl restart rsyslog.service") + tmp = systemd_services['syslog'] + call(f'systemctl restart {tmp}') # If SNMP is running, restart it too if process_named_running('snmpd') and config['snmpd_restart_reqired']: - call('systemctl restart snmpd.service') + tmp = systemd_services['snmpd'] + call(f'systemctl restart {tmp}') return None diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index d3a969d9b..4febb6494 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -24,10 +24,13 @@ from pwd import getpwuid from sys import exit from time import sleep +from vyos.base import Warning from vyos.config import Config from vyos.configverify import verify_vrf from vyos.template import render from vyos.template import is_ipv4 +from vyos.utils.auth import EPasswdStrength +from vyos.utils.auth import evaluate_strength from vyos.utils.auth import get_current_user from vyos.utils.configfs import delete_cli_node from vyos.utils.configfs import add_cli_node @@ -146,6 +149,19 @@ def verify(login): if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID: raise ConfigError(f'User "{user}" can not be created, conflict with local system account!') + # T6353: Check password for complexity using cracklib. + # A user password should be sufficiently complex + plaintext_password = dict_search( + path='authentication.plaintext_password', + dict_object=user_config + ) or None + + failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR] + if plaintext_password is not None: + result = evaluate_strength(plaintext_password) + if result['strength'] in failed_check_status: + Warning(result['error']) + for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items(): if 'type' not in pubkey_options: raise ConfigError(f'Missing type for public-key "{pubkey}"!') diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py index 5826d8042..cdd066649 100755 --- a/src/conf_mode/system_login_banner.py +++ b/src/conf_mode/system_login_banner.py @@ -95,8 +95,12 @@ def apply(banner): render(POSTLOGIN_FILE, 'login/default_motd.j2', banner, permission=0o644, user='root', group='root') - render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', banner, - permission=0o644, user='root', group='root') + if banner['version_data']['build_type'] != 'release': + render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', + banner, + permission=0o644, + user='root', + group='root') return None diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index e2832cde6..5acad6599 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-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,7 +86,7 @@ def verify(options): if 'source_address' in config: if not is_addr_assigned(config['source_address']): - raise ConfigError('No interface with give address specified!') + raise ConfigError('No interface with given address specified!') if 'ssh_client' in options: config = options['ssh_client'] @@ -122,7 +122,14 @@ def generate(options): render(ssh_config, 'system/ssh_config.j2', options) render(usb_autosuspend, 'system/40_usb_autosuspend.j2', options) + # XXX: This code path and if statements must be kept in sync with the Kernel + # option handling in image_installer.py:get_cli_kernel_options(). This + # occurance is used for having the appropriate options passed to GRUB + # when re-configuring options on the CLI. cmdline_options = [] + kernel_opts = options.get('kernel', {}) + k_cpu_opts = kernel_opts.get('cpu', {}) + k_memory_opts = kernel_opts.get('memory', {}) if 'kernel' in options: if 'disable_mitigations' in options['kernel']: cmdline_options.append('mitigations=off') @@ -131,8 +138,51 @@ def generate(options): if 'amd_pstate_driver' in options['kernel']: mode = options['kernel']['amd_pstate_driver'] cmdline_options.append( - f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}' - ) + f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}') + if 'quiet' in options['kernel']: + cmdline_options.append('quiet') + + if 'disable_hpet' in kernel_opts: + cmdline_options.append('hpet=disable') + + if 'disable_mce' in kernel_opts: + cmdline_options.append('mce=off') + + if 'disable_softlockup' in kernel_opts: + cmdline_options.append('nosoftlockup') + + # CPU options + isol_cpus = k_cpu_opts.get('isolate_cpus') + if isol_cpus: + cmdline_options.append(f'isolcpus={isol_cpus}') + + nohz_full = k_cpu_opts.get('nohz_full') + if nohz_full: + cmdline_options.append(f'nohz_full={nohz_full}') + + rcu_nocbs = k_cpu_opts.get('rcu_no_cbs') + if rcu_nocbs: + cmdline_options.append(f'rcu_nocbs={rcu_nocbs}') + + if 'disable_nmi_watchdog' in k_cpu_opts: + cmdline_options.append('nmi_watchdog=0') + + # Memory options + if 'disable_numa_balancing' in k_memory_opts: + cmdline_options.append('numa_balancing=disable') + + default_hp_size = k_memory_opts.get('default_hugepage_size') + if default_hp_size: + cmdline_options.append(f'default_hugepagesz={default_hp_size}') + + hp_sizes = k_memory_opts.get('hugepage_size') + if hp_sizes: + for size, settings in hp_sizes.items(): + cmdline_options.append(f'hugepagesz={size}') + count = settings.get('hugepage_count') + if count: + cmdline_options.append(f'hugepages={count}') + grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) return None diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py index 41119b494..a22dac36f 100755 --- a/src/conf_mode/system_sflow.py +++ b/src/conf_mode/system_sflow.py @@ -54,7 +54,7 @@ def verify(sflow): # Check if configured sflow agent-address exist in the system if 'agent_address' in sflow: tmp = sflow['agent_address'] - if not is_addr_assigned(tmp): + if not is_addr_assigned(tmp, include_vrf=True): raise ConfigError( f'Configured "sflow agent-address {tmp}" does not exist in the system!' ) diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py index eb2f02eb3..bdab09f3c 100755 --- a/src/conf_mode/system_syslog.py +++ b/src/conf_mode/system_syslog.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 @@ -20,17 +20,22 @@ from sys import exit from vyos.base import Warning from vyos.config import Config -from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf +from vyos.defaults import systemd_services +from vyos.utils.network import is_addr_assigned from vyos.utils.process import call from vyos.template import render +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 from vyos import ConfigError from vyos import airbag airbag.enable() -rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf' +rsyslog_conf = '/run/rsyslog/rsyslog.conf' logrotate_conf = '/etc/logrotate.d/vyos-rsyslog' -systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf' + +systemd_socket = 'syslog.socket' +systemd_service = systemd_services['syslog'] def get_config(config=None): if config: @@ -46,23 +51,17 @@ def get_config(config=None): syslog.update({ 'logrotate' : logrotate_conf }) - tmp = is_node_changed(conf, base + ['vrf']) - if tmp: syslog.update({'restart_required': {}}) - syslog = conf.merge_defaults(syslog, recursive=True) - if syslog.from_defaults(['global']): - del syslog['global'] - - if ( - 'global' in syslog - and 'preserve_fqdn' in syslog['global'] - and conf.exists(['system', 'host-name']) - and conf.exists(['system', 'domain-name']) - ): - hostname = conf.return_value(['system', 'host-name']) - domain = conf.return_value(['system', 'domain-name']) - fqdn = f'{hostname}.{domain}' - syslog['global']['local_host_name'] = fqdn + if syslog.from_defaults(['local']): + del syslog['local'] + + if 'preserve_fqdn' in syslog: + if conf.exists(['system', 'host-name']): + tmp = conf.return_value(['system', 'host-name']) + syslog['preserve_fqdn']['host_name'] = tmp + if conf.exists(['system', 'domain-name']): + tmp = conf.return_value(['system', 'domain-name']) + syslog['preserve_fqdn']['domain_name'] = tmp return syslog @@ -70,13 +69,33 @@ def verify(syslog): if not syslog: return None - if 'host' in syslog: - for host, host_options in syslog['host'].items(): - if 'protocol' in host_options and host_options['protocol'] == 'udp': - if 'format' in host_options and 'octet_counted' in host_options['format']: - Warning(f'Syslog UDP transport for "{host}" should not use octet-counted format!') - - verify_vrf(syslog) + if 'preserve_fqdn' in syslog: + if 'host_name' not in syslog['preserve_fqdn']: + Warning('No "system host-name" defined - cannot set syslog FQDN!') + if 'domain_name' not in syslog['preserve_fqdn']: + Warning('No "system domain-name" defined - cannot set syslog FQDN!') + + if 'remote' in syslog: + for remote, remote_options in syslog['remote'].items(): + if 'protocol' in remote_options and remote_options['protocol'] == 'udp': + if 'format' in remote_options and 'octet_counted' in remote_options['format']: + Warning(f'Syslog UDP transport for "{remote}" should not use octet-counted format!') + + if 'vrf' in remote_options: + verify_vrf(remote_options) + + if 'source_address' in remote_options: + vrf = None + if 'vrf' in remote_options: + vrf = remote_options['vrf'] + if not is_addr_assigned(remote_options['source_address'], vrf): + raise ConfigError('No interface with given address specified!') + + source_address = remote_options['source_address'] + if ((is_ipv4(remote) and is_ipv6(source_address)) or + (is_ipv6(remote) and is_ipv4(source_address))): + raise ConfigError(f'Source-address "{source_address}" does not match '\ + f'address-family of remote "{remote}"!') def generate(syslog): if not syslog: @@ -88,26 +107,15 @@ def generate(syslog): return None render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog) - render(systemd_override, 'rsyslog/override.conf.j2', syslog) render(logrotate_conf, 'rsyslog/logrotate.j2', syslog) - - # Reload systemd manager configuration - call('systemctl daemon-reload') return None def apply(syslog): - systemd_socket = 'syslog.socket' - systemd_service = 'syslog.service' if not syslog: call(f'systemctl stop {systemd_service} {systemd_socket}') return None - # we need to restart the service if e.g. the VRF name changed - systemd_action = 'reload-or-restart' - if 'restart_required' in syslog: - systemd_action = 'restart' - - call(f'systemctl {systemd_action} {systemd_service}') + call(f'systemctl reload-or-restart {systemd_service}') return None if __name__ == '__main__': diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index e22b7550c..2754314f7 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 @@ -64,6 +64,7 @@ swanctl_dir = '/etc/swanctl' charon_conf = '/etc/strongswan.d/charon.conf' charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf' +charon_systemd_conf = '/etc/strongswan.d/charon-systemd.conf' interface_conf = '/etc/strongswan.d/interfaces_use.conf' swanctl_conf = f'{swanctl_dir}/swanctl.conf' @@ -86,8 +87,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 +94,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 +122,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) @@ -151,6 +157,8 @@ def get_config(config=None): _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface) ipsec['vti_interface_dicts'][vti_interface] = vti + ipsec['vpp_ipsec_exists'] = conf.exists(['vpp', 'settings', 'ipsec']) + return ipsec def get_dhcp_address(iface): @@ -196,8 +204,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']: @@ -479,6 +487,17 @@ def verify(ipsec): else: raise ConfigError(f"Missing ike-group on site-to-site peer {peer}") + # verify encryption algorithm compatibility for IKE with VPP + if ipsec['vpp_ipsec_exists']: + ike_group = ipsec['ike_group'][peer_conf['ike_group']] + for proposal, proposal_config in ike_group.get('proposal', {}).items(): + algs = ['gmac', 'serpent', 'twofish'] + if any(alg in proposal_config['encryption'] for alg in algs): + raise ConfigError( + f'Encryption algorithm {proposal_config["encryption"]} cannot be used ' + f'for IKE proposal {proposal} for site-to-site peer {peer} with VPP' + ) + if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']: raise ConfigError(f"Missing authentication on site-to-site peer {peer}") @@ -557,7 +576,7 @@ def verify(ipsec): esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group'] - if esp_group_name not in ipsec['esp_group']: + if esp_group_name not in ipsec.get('esp_group'): raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}") esp_group = ipsec['esp_group'][esp_group_name] @@ -569,6 +588,18 @@ def verify(ipsec): if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']): raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") + # verify ESP encryption algorithm compatibility with VPP + # because Marvel plugin for VPP doesn't support all algorithms that Strongswan does + if ipsec['vpp_ipsec_exists']: + for proposal, proposal_config in esp_group.get('proposal', {}).items(): + algs = ['aes128', 'aes192', 'aes256', 'aes128gcm128', 'aes192gcm128', 'aes256gcm128'] + if proposal_config['encryption'] not in algs: + raise ConfigError( + f'Encryption algorithm {proposal_config["encryption"]} cannot be used ' + f'for ESP proposal {proposal} on tunnel {tunnel} for site-to-site peer {peer} with VPP' + ) + + def cleanup_pki_files(): for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]: if not os.path.exists(path): @@ -624,7 +655,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) @@ -715,21 +746,19 @@ def generate(ipsec): render(charon_conf, 'ipsec/charon.j2', ipsec) render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec) render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec) + render(charon_systemd_conf, 'ipsec/charon_systemd.conf.j2', ipsec) render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec) render(swanctl_conf, 'ipsec/swanctl.conf.j2', 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 +766,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 +775,6 @@ def apply(ipsec): # ConfigError("ConfigError('Interface ethN requires an IP address!')") pass - if __name__ == '__main__': try: ipsec = get_config() diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 74780b601..8baf55857 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -149,8 +149,9 @@ def verify(vrf): f'static routes installed!') if 'name' in vrf: - reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", - "get", "inet", "mtu", "link", "type", "vrf"] + reserved_names = ['add', 'all', 'broadcast', 'default', 'delete', 'dev', + 'down', 'get', 'inet', 'link', 'mtu', 'type', 'up', 'vrf'] + table_ids = [] vnis = [] for name, vrf_config in vrf['name'].items(): diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/06-vyos-nodefaultroute b/src/etc/dhcp/dhclient-enter-hooks.d/06-vyos-nodefaultroute new file mode 100644 index 000000000..38f674276 --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/06-vyos-nodefaultroute @@ -0,0 +1,20 @@ +# Don't add default route if no-default-route is configured for interface + +# As configuration is not available to cli-shell-api at the first boot, we must use vyos.config, which contains a workaround for this +function get_no_default_route { +python3 - <<PYEND +from vyos.config import Config +import os + +config = Config() +if config.exists('interfaces'): + iface_types = config.list_nodes('interfaces') + for iface_type in iface_types: + if config.exists("interfaces {} {} dhcp-options no-default-route".format(iface_type, os.environ['interface'])): + print("True") +PYEND +} + +if [[ "$(get_no_default_route)" == 'True' ]]; then + new_routers="" +fi diff --git a/src/etc/netplug/vyos-netplug-dhcp-client b/src/etc/netplug/vyos-netplug-dhcp-client index 55d15a163..a230fe900 100755 --- a/src/etc/netplug/vyos-netplug-dhcp-client +++ b/src/etc/netplug/vyos-netplug-dhcp-client @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2023 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 @@ -19,44 +19,53 @@ import sys from time import sleep -from vyos.configquery import ConfigTreeQuery +from vyos.config import Config from vyos.ifconfig import Section from vyos.utils.boot import boot_configuration_complete +from vyos.utils.process import cmd +from vyos.utils.process import is_systemd_service_active from vyos.utils.commit import commit_in_progress -from vyos.utils.process import call from vyos import airbag + airbag.enable() if len(sys.argv) < 3: - airbag.noteworthy("Must specify both interface and link status!") + airbag.noteworthy('Must specify both interface and link status!') sys.exit(1) if not boot_configuration_complete(): - airbag.noteworthy("System bootup not yet finished...") + airbag.noteworthy('System bootup not yet finished...') sys.exit(1) +interface = sys.argv[1] + while commit_in_progress(): - sleep(1) + sleep(0.250) -interface = sys.argv[1] in_out = sys.argv[2] -config = ConfigTreeQuery() +config = Config() interface_path = ['interfaces'] + Section.get_config_path(interface).split() -for _, interface_config in config.get_config_dict(interface_path).items(): - # Bail out early if we do not have an IP address configured - if 'address' not in interface_config: - continue - # Bail out early if interface ist administrative down - if 'disable' in interface_config: - continue - systemd_action = 'start' - if in_out == 'out': - systemd_action = 'stop' - # Start/Stop DHCP service - if 'dhcp' in interface_config['address']: - call(f'systemctl {systemd_action} dhclient@{interface}.service') - # Start/Stop DHCPv6 service - if 'dhcpv6' in interface_config['address']: - call(f'systemctl {systemd_action} dhcp6c@{interface}.service') +systemdV4_service = f'dhclient@{interface}.service' +systemdV6_service = f'dhcp6c@{interface}.service' +if in_out == 'out': + # Interface moved state to down + if is_systemd_service_active(systemdV4_service): + cmd(f'systemctl stop {systemdV4_service}') + if is_systemd_service_active(systemdV6_service): + cmd(f'systemctl stop {systemdV6_service}') +elif in_out == 'in': + if config.exists_effective(interface_path + ['address']): + tmp = config.return_effective_values(interface_path + ['address']) + # Always (re-)start the DHCP(v6) client service. If the DHCP(v6) client + # is already running - which could happen if the interface is re- + # configured in operational down state, it will have a backoff + # time increasing while not receiving a DHCP(v6) reply. + # + # To make the interface instantly available, and as for a DHCP(v6) lease + # we will re-start the service and thus cancel the backoff time. + if 'dhcp' in tmp: + cmd(f'systemctl restart {systemdV4_service}') + if 'dhcpv6' in tmp: + cmd(f'systemctl restart {systemdV6_service}') diff --git a/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb b/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb new file mode 100755 index 000000000..fff258afa --- /dev/null +++ b/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb @@ -0,0 +1,61 @@ +#!/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/>. + +# This is a Python hook script which is invoked whenever a PPPoE session goes +# "ip-up". It will call into our vyos.ifconfig library and will then execute +# common tasks for the PPPoE interface. The reason we have to "hook" this is +# that we can not create a pppoeX interface in advance in linux and then connect +# pppd to this already existing interface. + +import os +import signal + +from sys import argv +from sys import exit + +from vyos.defaults import directories + +# When the ppp link comes up, this script is called with the following +# parameters +# $1 the interface name used by pppd (e.g. ppp3) +# $2 the tty device name +# $3 the tty device speed +# $4 the local IP address for the interface +# $5 the remote IP address +# $6 the parameter specified by the 'ipparam' option to pppd + +if (len(argv) < 7): + exit(1) + +wlb_pid_file = '/run/wlb_daemon.pid' + +interface = argv[6] +nexthop = argv[5] + +if not os.path.exists(directories['ppp_nexthop_dir']): + os.mkdir(directories['ppp_nexthop_dir']) + +nexthop_file = os.path.join(directories['ppp_nexthop_dir'], interface) + +with open(nexthop_file, 'w') as f: + f.write(nexthop) + +# Trigger WLB daemon update +if os.path.exists(wlb_pid_file): + with open(wlb_pid_file, 'r') as f: + pid = int(f.read()) + + os.kill(pid, signal.SIGUSR2) diff --git a/src/etc/rsyslog.conf b/src/etc/rsyslog.conf deleted file mode 100644 index b3f41acb6..000000000 --- a/src/etc/rsyslog.conf +++ /dev/null @@ -1,67 +0,0 @@ -################# -#### MODULES #### -################# - -$ModLoad imuxsock # provides support for local system logging -$ModLoad imklog # provides kernel logging support (previously done by rklogd) -#$ModLoad immark # provides --MARK-- message capability - -$OmitLocalLogging off -$SystemLogSocketName /run/systemd/journal/syslog - -$KLogPath /proc/kmsg - -########################### -#### GLOBAL DIRECTIVES #### -########################### - -# Use traditional timestamp format. -# To enable high precision timestamps, comment out the following line. -# A modern-style logfile format similar to TraditionalFileFormat, buth with high-precision timestamps and timezone information -#$ActionFileDefaultTemplate RSYSLOG_FileFormat -# The "old style" default log file format with low-precision timestamps -$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat - -# Filter duplicated messages -$RepeatedMsgReduction on - -# -# Set the default permissions for all log files. -# -$FileOwner root -$FileGroup adm -$FileCreateMode 0640 -$DirCreateMode 0755 -$Umask 0022 - -# -# Stop excessive logging of sudo -# -:msg, contains, " pam_unix(sudo:session): session opened for user root(uid=0) by" stop -:msg, contains, "pam_unix(sudo:session): session closed for user root" stop - -# -# Include all config files in /etc/rsyslog.d/ -# -$IncludeConfig /etc/rsyslog.d/*.conf - -# The lines below cause all listed daemons/processes to be logged into -# /var/log/auth.log, then drops the message so it does not also go to the -# regular syslog so that messages are not duplicated - -$outchannel auth_log,/var/log/auth.log -if $programname == 'CRON' or - $programname == 'sudo' or - $programname == 'su' - then :omfile:$auth_log - -if $programname == 'CRON' or - $programname == 'sudo' or - $programname == 'su' - then stop - -############### -#### RULES #### -############### -# Emergencies are sent to everybody logged in. -*.emerg :omusrmsg:*
\ No newline at end of file diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 76be41ddc..ef81cebac 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -83,6 +83,16 @@ net.ipv4.conf.default.ignore_routes_with_linkdown=1 net.ipv6.conf.all.ignore_routes_with_linkdown=1 net.ipv6.conf.default.ignore_routes_with_linkdown=1 +# Disable IPv6 interface autoconfigurationnable packet forwarding for IPv6 +net.ipv6.conf.all.autoconf=0 +net.ipv6.conf.default.autoconf=0 +net.ipv6.conf.*.autoconf=0 + +# Disable IPv6 router advertisements +net.ipv6.conf.all.accept_ra=0 +net.ipv6.conf.default.accept_ra=0 +net.ipv6.conf.*.accept_ra=0 + # Enable packet forwarding for IPv6 net.ipv6.conf.all.forwarding=1 diff --git a/src/etc/systemd/system/fastnetmon.service.d/override.conf b/src/etc/systemd/system/fastnetmon.service.d/override.conf deleted file mode 100644 index 841666070..000000000 --- a/src/etc/systemd/system/fastnetmon.service.d/override.conf +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -RequiresMountsFor=/run -ConditionPathExists=/run/fastnetmon/fastnetmon.conf -After= -After=vyos-router.service - -[Service] -Type=simple -WorkingDirectory=/run/fastnetmon -PIDFile=/run/fastnetmon.pid -ExecStart= -ExecStart=/usr/sbin/fastnetmon --configuration_file /run/fastnetmon/fastnetmon.conf diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf index 614b4f7ed..a4a73ecd9 100644 --- a/src/etc/systemd/system/frr.service.d/override.conf +++ b/src/etc/systemd/system/frr.service.d/override.conf @@ -3,9 +3,11 @@ After=vyos-router.service [Service] LimitNOFILE=4096 -ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \ +ExecStartPre=/bin/bash -c 'if [ ! -f /run/frr/config/frr.conf ]; then \ + mkdir -p /run/frr/config; \ echo "log syslog" > /run/frr/config/frr.conf; \ echo "log facility local7" >> /run/frr/config/frr.conf; \ chown frr:frr /run/frr/config/frr.conf; \ chmod 664 /run/frr/config/frr.conf; \ - mount --bind /run/frr/config/frr.conf /etc/frr/frr.conf' + mount --bind /run/frr/config/frr.conf /etc/frr/frr.conf; \ +fi;' diff --git a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf deleted file mode 100644 index 0f5bf801e..000000000 --- a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -After= -After=vyos-router.service - -[Service] -ExecStart= -ExecStart=/usr/sbin/kea-ctrl-agent -c /run/kea/kea-ctrl-agent.conf -AmbientCapabilities=CAP_NET_BIND_SERVICE -CapabilityBoundingSet=CAP_NET_BIND_SERVICE diff --git a/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf new file mode 100644 index 000000000..cdfdea8eb --- /dev/null +++ b/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-dhcp-ddns -c /run/kea/kea-dhcp-ddns.conf diff --git a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf index 682e5bbce..4a04892c0 100644 --- a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf +++ b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf @@ -5,3 +5,5 @@ After=vyos-router.service [Service] ExecStart= ExecStart=/usr/sbin/kea-dhcp4 -c /run/kea/kea-dhcp4.conf +ExecStartPost=!/usr/bin/python3 /usr/libexec/vyos/system/sync-dhcp-lease-to-hosts.py --inet +Restart=on-failure diff --git a/src/etc/systemd/system/rsyslog.service.d/override.conf b/src/etc/systemd/system/rsyslog.service.d/override.conf new file mode 100644 index 000000000..665b994d9 --- /dev/null +++ b/src/etc/systemd/system/rsyslog.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +StartLimitIntervalSec=0 + +[Service] +ExecStart= +ExecStart=/usr/sbin/rsyslogd -n -iNONE -f /run/rsyslog/rsyslog.conf +Restart=always +RestartPreventExitStatus= +RestartSec=10 +RuntimeDirectoryPreserve=yes diff --git a/src/etc/udev/rules.d/90-vyos-serial.rules b/src/etc/udev/rules.d/90-vyos-serial.rules index 30c1d3170..f86b2258f 100644 --- a/src/etc/udev/rules.d/90-vyos-serial.rules +++ b/src/etc/udev/rules.d/90-vyos-serial.rules @@ -8,7 +8,7 @@ SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci" SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb" # /dev/serial/by-path/, /dev/serial/by-id/ for USB devices -KERNEL!="ttyUSB[0-9]*", GOTO="serial_end" +KERNEL!="ttyUSB[0-9]*|ttyACM[0-9]*", GOTO="serial_end" SUBSYSTEMS=="usb-serial", ENV{.ID_PORT}="$attr{port_number}" diff --git a/src/helpers/geoip-update.py b/src/helpers/geoip-update.py index 34accf2cc..061c95401 100755 --- a/src/helpers/geoip-update.py +++ b/src/helpers/geoip-update.py @@ -25,20 +25,19 @@ def get_config(config=None): conf = config else: conf = ConfigTreeQuery() - base = ['firewall'] - if not conf.exists(base): - return None - - return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) + return ( + conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) if conf.exists(['firewall']) else None, + conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) if conf.exists(['policy']) else None, + ) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("--force", help="Force update", action="store_true") args = parser.parse_args() - firewall = get_config() - - if not geoip_update(firewall, force=args.force): + firewall, policy = get_config() + if not geoip_update(firewall=firewall, policy=policy, force=args.force): sys.exit(1) diff --git a/src/helpers/show_commit_data.py b/src/helpers/show_commit_data.py new file mode 100755 index 000000000..d507ed9a4 --- /dev/null +++ b/src/helpers/show_commit_data.py @@ -0,0 +1,56 @@ +#!/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/>. +# +# +# This script is used to show the commit data of the configuration + +import sys +from pathlib import Path +from argparse import ArgumentParser + +from vyos.config_mgmt import ConfigMgmt +from vyos.configtree import ConfigTree +from vyos.configtree import show_commit_data + +cm = ConfigMgmt() + +parser = ArgumentParser( + description='Show commit priority queue; no options compares the last two commits' +) +parser.add_argument('--active-config', help='Path to the active configuration file') +parser.add_argument('--proposed-config', help='Path to the proposed configuration file') +args = parser.parse_args() + +active_arg = args.active_config +proposed_arg = args.proposed_config + +if active_arg and not proposed_arg: + print('--proposed-config is required when --active-config is specified') + sys.exit(1) + +if not active_arg and not proposed_arg: + active = cm.get_config_tree_revision(1) + proposed = cm.get_config_tree_revision(0) +else: + if active_arg: + active = ConfigTree(Path(active_arg).read_text()) + else: + active = cm.get_config_tree_revision(0) + + proposed = ConfigTree(Path(proposed_arg).read_text()) + +ret = show_commit_data(active, proposed) +print(ret) diff --git a/src/helpers/test_commit.py b/src/helpers/test_commit.py new file mode 100755 index 000000000..00a413687 --- /dev/null +++ b/src/helpers/test_commit.py @@ -0,0 +1,49 @@ +#!/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/>. +# +# +# This script is used to test execution of the commit algorithm by vyos-commitd + +from pathlib import Path +from argparse import ArgumentParser +from datetime import datetime + +from vyos.configtree import ConfigTree +from vyos.configtree import test_commit + + +parser = ArgumentParser( + description='Execute commit priority queue' +) +parser.add_argument( + '--active-config', help='Path to the active configuration file', required=True +) +parser.add_argument( + '--proposed-config', help='Path to the proposed configuration file', required=True +) +args = parser.parse_args() + +active_arg = args.active_config +proposed_arg = args.proposed_config + +active = ConfigTree(Path(active_arg).read_text()) +proposed = ConfigTree(Path(proposed_arg).read_text()) + + +time_begin_commit = datetime.now() +test_commit(active, proposed) +time_end_commit = datetime.now() +print(f'commit time: {time_end_commit - time_begin_commit}') diff --git a/src/helpers/vyos-certbot-renew-pki.sh b/src/helpers/vyos-certbot-renew-pki.sh index d0b663f7b..1c273d2fa 100755 --- a/src/helpers/vyos-certbot-renew-pki.sh +++ b/src/helpers/vyos-certbot-renew-pki.sh @@ -1,3 +1,3 @@ -#!/bin/sh +#!/bin/vbash source /opt/vyatta/etc/functions/script-template /usr/libexec/vyos/conf_mode/pki.py certbot_renew diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py deleted file mode 100755 index f5a1d9297..000000000 --- a/src/helpers/vyos-domain-resolver.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2022-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 json -import time - -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.utils.commit import commit_in_progress -from vyos.utils.dict import dict_search_args -from vyos.utils.process import cmd -from vyos.utils.process import run -from vyos.xml_ref import get_defaults - -base = ['firewall'] -timeout = 300 -cache = False -base_firewall = ['firewall'] -base_nat = ['nat'] - -domain_state = {} - -ipv4_tables = { - 'ip vyos_mangle', - 'ip vyos_filter', - 'ip vyos_nat', - 'ip raw' -} - -ipv6_tables = { - 'ip6 vyos_mangle', - 'ip6 vyos_filter', - 'ip6 raw' -} - -def get_config(conf, node): - node_config = conf.get_config_dict(node, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - default_values = get_defaults(node, get_first_key=True) - - node_config = dict_merge(default_values, node_config) - - global timeout, cache - - if 'resolver_interval' in node_config: - timeout = int(node_config['resolver_interval']) - - if 'resolver_cache' in node_config: - cache = True - - fqdn_config_parse(node_config, node[0]) - - return node_config - -def resolve(domains, ipv6=False): - global domain_state - - ip_list = set() - - for domain in domains: - resolved = fqdn_resolve(domain, ipv6=ipv6) - - if resolved and cache: - domain_state[domain] = resolved - elif not resolved: - if domain not in domain_state: - continue - resolved = domain_state[domain] - - ip_list = ip_list | resolved - return ip_list - -def nft_output(table, set_name, ip_list): - output = [f'flush set {table} {set_name}'] - if ip_list: - ip_str = ','.join(ip_list) - output.append(f'add element {table} {set_name} {{ {ip_str} }}') - return output - -def nft_valid_sets(): - try: - valid_sets = [] - sets_json = cmd('nft --json list sets') - sets_obj = json.loads(sets_json) - - for obj in sets_obj['nftables']: - if 'set' in obj: - family = obj['set']['family'] - table = obj['set']['table'] - name = obj['set']['name'] - valid_sets.append((f'{family} {table}', name)) - - return valid_sets - except: - return [] - -def update_fqdn(config, node): - conf_lines = [] - count = 0 - valid_sets = nft_valid_sets() - - if node == 'firewall': - domain_groups = dict_search_args(config, 'group', 'domain_group') - if domain_groups: - for set_name, domain_config in domain_groups.items(): - if 'address' not in domain_config: - continue - nft_set_name = f'D_{set_name}' - domains = domain_config['address'] - - ip_list = resolve(domains, ipv6=False) - for table in ipv4_tables: - if (table, nft_set_name) in valid_sets: - conf_lines += nft_output(table, nft_set_name, ip_list) - ip6_list = resolve(domains, ipv6=True) - for table in ipv6_tables: - if (table, nft_set_name) in valid_sets: - conf_lines += nft_output(table, nft_set_name, ip6_list) - count += 1 - - for set_name, domain in config['ip_fqdn'].items(): - table = 'ip vyos_filter' - nft_set_name = f'FQDN_{set_name}' - ip_list = resolve([domain], ipv6=False) - if (table, nft_set_name) in valid_sets: - conf_lines += nft_output(table, nft_set_name, ip_list) - count += 1 - - for set_name, domain in config['ip6_fqdn'].items(): - table = 'ip6 vyos_filter' - nft_set_name = f'FQDN_{set_name}' - ip_list = resolve([domain], ipv6=True) - if (table, nft_set_name) in valid_sets: - conf_lines += nft_output(table, nft_set_name, ip_list) - count += 1 - - else: - # It's NAT - for set_name, domain in config['ip_fqdn'].items(): - table = 'ip vyos_nat' - nft_set_name = f'FQDN_nat_{set_name}' - ip_list = resolve([domain], ipv6=False) - if (table, nft_set_name) in valid_sets: - conf_lines += nft_output(table, nft_set_name, ip_list) - count += 1 - - nft_conf_str = "\n".join(conf_lines) + "\n" - code = run(f'nft --file -', input=nft_conf_str) - - print(f'Updated {count} sets in {node} - result: {code}') - -if __name__ == '__main__': - print(f'VyOS domain resolver') - - count = 1 - while commit_in_progress(): - if ( count % 60 == 0 ): - print(f'Commit still in progress after {count}s - waiting') - count += 1 - time.sleep(1) - - conf = ConfigTreeQuery() - firewall = get_config(conf, base_firewall) - nat = get_config(conf, base_nat) - - print(f'interval: {timeout}s - cache: {cache}') - - while True: - update_fqdn(firewall, 'firewall') - update_fqdn(nat, 'nat') - time.sleep(timeout) diff --git a/src/helpers/vyos-load-balancer.py b/src/helpers/vyos-load-balancer.py new file mode 100755 index 000000000..30329fd5c --- /dev/null +++ b/src/helpers/vyos-load-balancer.py @@ -0,0 +1,312 @@ +#!/usr/bin/python3 + +# 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import json +import os +import signal +import sys +import time + +from vyos.config import Config +from vyos.template import render +from vyos.utils.commit import commit_in_progress +from vyos.utils.network import get_interface_address +from vyos.utils.process import rc_cmd +from vyos.utils.process import run +from vyos.xml_ref import get_defaults +from vyos.wanloadbalance import health_ping_host +from vyos.wanloadbalance import health_ping_host_ttl +from vyos.wanloadbalance import parse_dhcp_nexthop +from vyos.wanloadbalance import parse_ppp_nexthop + +nftables_wlb_conf = '/run/nftables_wlb.conf' +wlb_status_file = '/run/wlb_status.json' +wlb_pid_file = '/run/wlb_daemon.pid' +sleep_interval = 5 # Main loop sleep interval + +def health_check(ifname, conf, state, test_defaults): + # Run health tests for interface + + if get_ipv4_address(ifname) is None: + return False + + if 'test' not in conf: + resp_time = test_defaults['resp-time'] + target = conf['nexthop'] + + if target == 'dhcp': + target = state['dhcp_nexthop'] + + if not target: + return False + + return health_ping_host(target, ifname, wait_time=resp_time) + + for test_id, test_conf in conf['test'].items(): + check_type = test_conf['type'] + + if check_type == 'ping': + resp_time = test_conf['resp_time'] + target = test_conf['target'] + if not health_ping_host(target, ifname, wait_time=resp_time): + return False + elif check_type == 'ttl': + target = test_conf['target'] + ttl_limit = test_conf['ttl_limit'] + if not health_ping_host_ttl(target, ifname, ttl_limit=ttl_limit): + return False + elif check_type == 'user-defined': + script = test_conf['test_script'] + rc = run(script) + if rc != 0: + return False + + return True + +def on_state_change(lb, ifname, state): + # Run hook on state change + if 'hook' in lb: + script_path = os.path.join('/config/scripts/', lb['hook']) + env = { + 'WLB_INTERFACE_NAME': ifname, + 'WLB_INTERFACE_STATE': 'ACTIVE' if state else 'FAILED' + } + + code = run(script_path, env=env) + if code != 0: + print('WLB hook returned non-zero error code') + + print(f'INFO: State change: {ifname} -> {state}') + +def get_ipv4_address(ifname): + # Get primary ipv4 address on interface (for source nat) + addr_json = get_interface_address(ifname) + if addr_json and 'addr_info' in addr_json and len(addr_json['addr_info']) > 0: + for addr_info in addr_json['addr_info']: + if addr_info['family'] == 'inet': + if 'local' in addr_info: + return addr_json['addr_info'][0]['local'] + return None + +def dynamic_nexthop_update(lb, ifname): + # Update on DHCP/PPP address/nexthop changes + # Return True if nftables needs to be updated - IP change + + if 'dhcp_nexthop' in lb['health_state'][ifname]: + if ifname[:5] == 'pppoe': + dhcp_nexthop_addr = parse_ppp_nexthop(ifname) + else: + dhcp_nexthop_addr = parse_dhcp_nexthop(ifname) + + table_num = lb['health_state'][ifname]['table_number'] + + if dhcp_nexthop_addr and lb['health_state'][ifname]['dhcp_nexthop'] != dhcp_nexthop_addr: + lb['health_state'][ifname]['dhcp_nexthop'] = dhcp_nexthop_addr + run(f'ip route replace table {table_num} default dev {ifname} via {dhcp_nexthop_addr}') + + if_addr = get_ipv4_address(ifname) + if if_addr and if_addr != lb['health_state'][ifname]['if_addr']: + lb['health_state'][ifname]['if_addr'] = if_addr + return True + + return False + +def nftables_update(lb): + # Atomically reload nftables table from template + if not os.path.exists(nftables_wlb_conf): + lb['first_install'] = True + elif 'first_install' in lb: + del lb['first_install'] + + render(nftables_wlb_conf, 'load-balancing/nftables-wlb.j2', lb) + + rc, out = rc_cmd(f'nft -f {nftables_wlb_conf}') + + if rc != 0: + print('ERROR: Failed to apply WLB nftables config') + print('Output:', out) + return False + + return True + +def cleanup(lb): + if 'interface_health' in lb: + index = 1 + for ifname, health_conf in lb['interface_health'].items(): + table_num = lb['mark_offset'] + index + run(f'ip route del table {table_num} default') + run(f'ip rule del fwmark {hex(table_num)} table {table_num}') + index += 1 + + run(f'nft delete table ip vyos_wanloadbalance') + +def get_config(): + conf = Config() + base = ['load-balancing', 'wan'] + lb = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, with_recursive_defaults=True) + + lb['test_defaults'] = get_defaults(base + ['interface-health', 'A', 'test', 'B'], get_first_key=True) + + return lb + +if __name__ == '__main__': + while commit_in_progress(): + print("Notice: Waiting for commit to complete...") + time.sleep(1) + + lb = get_config() + + lb['health_state'] = {} + lb['mark_offset'] = 0xc8 + + # Create state dicts, interface address and nexthop, install routes and ip rules + if 'interface_health' in lb: + index = 1 + for ifname, health_conf in lb['interface_health'].items(): + table_num = lb['mark_offset'] + index + addr = get_ipv4_address(ifname) + lb['health_state'][ifname] = { + 'if_addr': addr, + 'failure_count': 0, + 'success_count': 0, + 'last_success': 0, + 'last_failure': 0, + 'state': addr is not None, + 'state_changed': False, + 'table_number': table_num, + 'mark': hex(table_num) + } + + if health_conf['nexthop'] == 'dhcp': + lb['health_state'][ifname]['dhcp_nexthop'] = None + + dynamic_nexthop_update(lb, ifname) + else: + run(f'ip route replace table {table_num} default dev {ifname} via {health_conf["nexthop"]}') + + run(f'ip rule add fwmark {hex(table_num)} table {table_num}') + + index += 1 + + nftables_update(lb) + + run('ip route flush cache') + + if 'flush_connections' in lb: + run('conntrack --delete') + run('conntrack -F expect') + + with open(wlb_status_file, 'w') as f: + f.write(json.dumps(lb['health_state'])) + + # Signal handler SIGUSR2 -> dhcpcd update + def handle_sigusr2(signum, frame): + for ifname, health_conf in lb['interface_health'].items(): + if 'nexthop' in health_conf and health_conf['nexthop'] == 'dhcp': + retval = dynamic_nexthop_update(lb, ifname) + + if retval: + nftables_update(lb) + + # Signal handler SIGTERM -> exit + def handle_sigterm(signum, frame): + if os.path.exists(wlb_status_file): + os.unlink(wlb_status_file) + + if os.path.exists(wlb_pid_file): + os.unlink(wlb_pid_file) + + if os.path.exists(nftables_wlb_conf): + os.unlink(nftables_wlb_conf) + + cleanup(lb) + sys.exit(0) + + signal.signal(signal.SIGUSR2, handle_sigusr2) + signal.signal(signal.SIGINT, handle_sigterm) + signal.signal(signal.SIGTERM, handle_sigterm) + + with open(wlb_pid_file, 'w') as f: + f.write(str(os.getpid())) + + # Main loop + + try: + while True: + ip_change = False + + if 'interface_health' in lb: + for ifname, health_conf in lb['interface_health'].items(): + state = lb['health_state'][ifname] + + result = health_check(ifname, health_conf, state=state, test_defaults=lb['test_defaults']) + + state_changed = result != state['state'] + state['state_changed'] = False + + if result: + state['failure_count'] = 0 + state['success_count'] += 1 + state['last_success'] = time.time() + if state_changed and state['success_count'] >= int(health_conf['success_count']): + state['state'] = True + state['state_changed'] = True + elif not result: + state['failure_count'] += 1 + state['success_count'] = 0 + state['last_failure'] = time.time() + if state_changed and state['failure_count'] >= int(health_conf['failure_count']): + state['state'] = False + state['state_changed'] = True + + if state['state_changed']: + state['if_addr'] = get_ipv4_address(ifname) + on_state_change(lb, ifname, state['state']) + + if dynamic_nexthop_update(lb, ifname): + ip_change = True + + if any(state['state_changed'] for ifname, state in lb['health_state'].items()): + if not nftables_update(lb): + break + + run('ip route flush cache') + + if 'flush_connections' in lb: + run('conntrack --delete') + run('conntrack -F expect') + + with open(wlb_status_file, 'w') as f: + f.write(json.dumps(lb['health_state'])) + elif ip_change: + nftables_update(lb) + + time.sleep(sleep_interval) + except Exception as e: + print('WLB ERROR:', e) + + if os.path.exists(wlb_status_file): + os.unlink(wlb_status_file) + + if os.path.exists(wlb_pid_file): + os.unlink(wlb_pid_file) + + if os.path.exists(nftables_wlb_conf): + os.unlink(nftables_wlb_conf) + + cleanup(lb) diff --git a/src/init/vyos-router b/src/init/vyos-router index 00136309b..6f1d386d6 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -459,8 +459,17 @@ start () nfct helper add tns inet6 tcp nft --file /usr/share/vyos/vyos-firewall-init.conf || log_failure_msg "could not initiate firewall rules" + # Ensure rsyslog is the default syslog daemon + SYSTEMD_SYSLOG="/etc/systemd/system/syslog.service" + SYSTEMD_RSYSLOG="/lib/systemd/system/rsyslog.service" + if [ ! -L ${SYSTEMD_SYSLOG} ] || [ "$(readlink -f ${SYSTEMD_SYSLOG})" != "${SYSTEMD_RSYSLOG}" ]; then + ln -sf ${SYSTEMD_RSYSLOG} ${SYSTEMD_SYSLOG} + systemctl daemon-reload + fi + # As VyOS does not execute commands that are not present in the CLI we call # the script by hand to have a single source for the login banner and MOTD + ${vyos_conf_scripts_dir}/system_syslog.py || log_failure_msg "could not reset syslog" ${vyos_conf_scripts_dir}/system_console.py || log_failure_msg "could not reset serial console" ${vyos_conf_scripts_dir}/system_login_banner.py || log_failure_msg "could not reset motd and issue files" ${vyos_conf_scripts_dir}/system_option.py || log_failure_msg "could not reset system option files" @@ -556,6 +565,9 @@ start () if [[ ! -z "$tmp" ]]; then vtysh -c "rpki start" fi + + # Start netplug daemon + systemctl start netplug.service } stop() @@ -573,8 +585,8 @@ stop() umount ${vyatta_configdir} log_action_end_msg $? + systemctl stop netplug.service systemctl stop vyconfd.service - systemctl stop frr.service unmount_encrypted_config diff --git a/src/migration-scripts/bgp/5-to-6 b/src/migration-scripts/bgp/5-to-6 new file mode 100644 index 000000000..e6fea6574 --- /dev/null +++ b/src/migration-scripts/bgp/5-to-6 @@ -0,0 +1,39 @@ +# 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/>. + +# T7163: migrate "address-family ipv4|6-unicast redistribute table" from a multi +# leafNode to a tagNode. This is needed to support per table definition of a +# route-map and/or metric + +from vyos.configtree import ConfigTree + +def migrate(config: ConfigTree) -> None: + bgp_base = ['protocols', 'bgp'] + if not config.exists(bgp_base): + return + + for address_family in ['ipv4-unicast', 'ipv6-unicast']: + # there is no non-main routing table beeing redistributed under this addres family + # bail out early and continue with next AFI + table_path = bgp_base + ['address-family', address_family, 'redistribute', 'table'] + if not config.exists(table_path): + continue + + tables = config.return_values(table_path) + config.delete(table_path) + + for table in tables: + config.set(table_path + [table]) + config.set_tag(table_path) diff --git a/src/migration-scripts/dhcp-server/7-to-8 b/src/migration-scripts/dhcp-server/7-to-8 index 7fcb62e86..d0f9455bb 100644 --- a/src/migration-scripts/dhcp-server/7-to-8 +++ b/src/migration-scripts/dhcp-server/7-to-8 @@ -41,9 +41,6 @@ def migrate(config: ConfigTree) -> None: for network in config.list_nodes(base + ['shared-network-name']): base_network = base + ['shared-network-name', network] - if config.exists(base_network + ['ping-check']): - config.delete(base_network + ['ping-check']) - if config.exists(base_network + ['shared-network-parameters']): config.delete(base_network +['shared-network-parameters']) @@ -57,9 +54,6 @@ def migrate(config: ConfigTree) -> None: if config.exists(base_subnet + ['enable-failover']): config.delete(base_subnet + ['enable-failover']) - if config.exists(base_subnet + ['ping-check']): - config.delete(base_subnet + ['ping-check']) - if config.exists(base_subnet + ['subnet-parameters']): config.delete(base_subnet + ['subnet-parameters']) diff --git a/src/migration-scripts/firewall/16-to-17 b/src/migration-scripts/firewall/16-to-17 index ad0706f04..ad0706f04 100755..100644 --- a/src/migration-scripts/firewall/16-to-17 +++ b/src/migration-scripts/firewall/16-to-17 diff --git a/src/migration-scripts/firewall/17-to-18 b/src/migration-scripts/firewall/17-to-18 new file mode 100755 index 000000000..34ce6aa07 --- /dev/null +++ b/src/migration-scripts/firewall/17-to-18 @@ -0,0 +1,41 @@ +# Copyright (C) 2024-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; 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/>. + +# From +# set firewall zone <zone> interface RED +# set firewall zone <zone> interface eth0 +# To +# set firewall zone <zone> member vrf RED +# set firewall zone <zone> member interface eth0 + +from vyos.configtree import ConfigTree + +base = ['firewall', 'zone'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + for zone in config.list_nodes(base): + zone_iface_base = base + [zone, 'interface'] + zone_member_base = base + [zone, 'member'] + if config.exists(zone_iface_base): + for iface in config.return_values(zone_iface_base): + if config.exists(['vrf', 'name', iface]): + config.set(zone_member_base + ['vrf'], value=iface, replace=False) + else: + config.set(zone_member_base + ['interface'], value=iface, replace=False) + config.delete(zone_iface_base) 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/ids/1-to-2 b/src/migration-scripts/ids/1-to-2 new file mode 100644 index 000000000..4c0333c88 --- /dev/null +++ b/src/migration-scripts/ids/1-to-2 @@ -0,0 +1,30 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# T: Migrate threshold and add new threshold types + +from vyos.configtree import ConfigTree + +# The old 'service ids' path was only used for FastNetMon +# Suricata is in 'service suricata', +# so this isn't an overreach +base = ['service', 'ids'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + else: + config.delete(base) diff --git a/src/migration-scripts/lldp/2-to-3 b/src/migration-scripts/lldp/2-to-3 new file mode 100644 index 000000000..93090756c --- /dev/null +++ b/src/migration-scripts/lldp/2-to-3 @@ -0,0 +1,31 @@ +# 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/>. + +# T7165: Migrate LLDP interface disable to 'mode disable' + +from vyos.configtree import ConfigTree + +base = ['service', 'lldp'] + +def migrate(config: ConfigTree) -> None: + interface_base = base + ['interface'] + if not config.exists(interface_base): + # Nothing to do + return + + for interface in config.list_nodes(interface_base): + if config.exists(interface_base + [interface, 'disable']): + config.delete(interface_base + [interface, 'disable']) + config.set(interface_base + [interface, 'mode'], value='disable') 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/migration-scripts/policy/8-to-9 b/src/migration-scripts/policy/8-to-9 new file mode 100644 index 000000000..355e48e00 --- /dev/null +++ b/src/migration-scripts/policy/8-to-9 @@ -0,0 +1,49 @@ +# Copyright (C) 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/>. + +# T7116: Remove unsupported "internet" community following FRR removal +# From + # set policy route-map <name> rule <ord> set community [add | replace] internet + # set policy community-list <name> rule <ord> regex internet +# To + # set policy route-map <name> rule <ord> set community [add | replace] 0:0 + # set policy community-list <name> rule <ord> regex _0:0_ + +# NOTE: In FRR expanded community-lists, without the '_' delimiters, a regex of +# "0:0" will match "65000:0" as well as "0:0". This doesn't line up with what +# we want when replacing "internet". + +from vyos.configtree import ConfigTree + +rm_base = ['policy', 'route-map'] +cl_base = ['policy', 'community-list'] + +def migrate(config: ConfigTree) -> None: + if config.exists(rm_base): + for policy_name in config.list_nodes(rm_base): + for rule_ord in config.list_nodes(rm_base + [policy_name, 'rule'], path_must_exist=False): + tmp_path = rm_base + [policy_name, 'rule', rule_ord, 'set', 'community'] + if config.exists(tmp_path + ['add']) and config.return_value(tmp_path + ['add']) == 'internet': + config.set(tmp_path + ['add'], '0:0') + if config.exists(tmp_path + ['replace']) and config.return_value(tmp_path + ['replace']) == 'internet': + config.set(tmp_path + ['replace'], '0:0') + + if config.exists(cl_base): + for policy_name in config.list_nodes(cl_base): + for rule_ord in config.list_nodes(cl_base + [policy_name, 'rule'], path_must_exist=False): + tmp_path = cl_base + [policy_name, 'rule', rule_ord, 'regex'] + if config.exists(tmp_path) and config.return_value(tmp_path) == 'internet': + config.set(tmp_path, '_0:0_') + diff --git a/src/migration-scripts/reverse-proxy/2-to-3 b/src/migration-scripts/reverse-proxy/2-to-3 new file mode 100755 index 000000000..ac539618e --- /dev/null +++ b/src/migration-scripts/reverse-proxy/2-to-3 @@ -0,0 +1,66 @@ +# 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/>. + +# T7429: logging facility "all" unavailable in code + +from vyos.configtree import ConfigTree + +base = ['load-balancing', 'haproxy'] +unsupported_facilities = ['all', 'authpriv', 'mark'] + +def config_migrator(config, config_path: list) -> None: + if not config.exists(config_path): + return + # Remove unsupported backend HAProxy syslog facilities form CLI + # Works for both backend and service CLI nodes + for service_backend in config.list_nodes(config_path): + log_path = config_path + [service_backend, 'logging', 'facility'] + if not config.exists(log_path): + continue + # Remove unsupported syslog facilities form CLI + for facility in config.list_nodes(log_path): + if facility in unsupported_facilities: + config.delete(log_path + [facility]) + continue + # Remove unsupported facility log level form CLI. VyOS will fallback + # to default log level if not set + if config.exists(log_path + [facility, 'level']): + tmp = config.return_value(log_path + [facility, 'level']) + if tmp == 'all': + config.delete(log_path + [facility, 'level']) + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + # Remove unsupported syslog facilities form CLI + global_path = base + ['global-parameters', 'logging', 'facility'] + if config.exists(global_path): + for facility in config.list_nodes(global_path): + if facility in unsupported_facilities: + config.delete(global_path + [facility]) + continue + # Remove unsupported facility log level form CLI. VyOS will fallback + # to default log level if not set + if config.exists(global_path + [facility, 'level']): + tmp = config.return_value(global_path + [facility, 'level']) + if tmp == 'all': + config.delete(global_path + [facility, 'level']) + + # Remove unsupported backend HAProxy syslog facilities from CLI + config_migrator(config, base + ['backend']) + # Remove unsupported service HAProxy syslog facilities from CLI + config_migrator(config, base + ['service']) diff --git a/src/migration-scripts/system/28-to-29 b/src/migration-scripts/system/28-to-29 new file mode 100644 index 000000000..ccf7056c4 --- /dev/null +++ b/src/migration-scripts/system/28-to-29 @@ -0,0 +1,71 @@ +# 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/>. + +# T6989: +# - remove syslog arbitrary file logging +# - remove syslog user console logging +# - move "global preserve-fqdn" one CLI level up +# - rename "host" to "remote" + +from vyos.configtree import ConfigTree + +base = ['system', 'syslog'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + return + # Drop support for custom file logging + if config.exists(base + ['file']): + config.delete(base + ['file']) + + # Drop support for logging to a user tty + # This should be dynamically added via an op-mode command like "terminal monitor" + if config.exists(base + ['user']): + config.delete(base + ['user']) + + # Move "global preserve-fqdn" one CLI level up, as it relates to all + # logging targets (console, global and remote) + preserve_fqdn_base = base + ['global', 'preserve-fqdn'] + if config.exists(preserve_fqdn_base): + config.delete(preserve_fqdn_base) + config.set(base + ['preserve-fqdn']) + + # Move "global marker" one CLI level up, as it relates to all + # logging targets (console, global and remote) + marker_base = base + ['global', 'marker'] + if config.exists(marker_base): + config.copy(marker_base, base + ['marker']) + config.delete(marker_base) + + # Rename "global" -> "local" as this describes what is logged locally + # on the router to a file on the filesystem + if config.exists(base + ['global']): + config.rename(base + ['global'], 'local') + + vrf = '' + if config.exists(base + ['vrf']): + vrf = config.return_value(base + ['vrf']) + config.delete(base + ['vrf']) + + # Rename host x.x.x.x -> remote x.x.x.x + if config.exists(base + ['host']): + config.set(base + ['remote']) + config.set_tag(base + ['remote']) + for remote in config.list_nodes(base + ['host']): + config.copy(base + ['host', remote], base + ['remote', remote]) + config.set_tag(base + ['remote']) + if vrf: + config.set(base + ['remote', remote, 'vrf'], value=vrf) + config.delete(base + ['host']) diff --git a/src/migration-scripts/vrf/1-to-2 b/src/migration-scripts/vrf/1-to-2 index 557a9ec58..89b0f708a 100644 --- a/src/migration-scripts/vrf/1-to-2 +++ b/src/migration-scripts/vrf/1-to-2 @@ -37,7 +37,10 @@ def migrate(config: ConfigTree) -> None: new_static_base = vrf_base + [vrf, 'protocols'] config.set(new_static_base) config.copy(static_base, new_static_base + ['static']) - config.set_tag(new_static_base + ['static', 'route']) + if config.exists(new_static_base + ['static', 'route']): + config.set_tag(new_static_base + ['static', 'route']) + if config.exists(new_static_base + ['static', 'route6']): + config.set_tag(new_static_base + ['static', 'route6']) # Now delete the old configuration config.delete(base) diff --git a/src/migration-scripts/vrf/2-to-3 b/src/migration-scripts/vrf/2-to-3 index acacffb41..5f396e7ed 100644 --- a/src/migration-scripts/vrf/2-to-3 +++ b/src/migration-scripts/vrf/2-to-3 @@ -76,7 +76,8 @@ def migrate(config: ConfigTree) -> None: # Get a list of all currently used VRFs and tables vrfs_current = {} for vrf in config.list_nodes(base): - vrfs_current[vrf] = int(config.return_value(base + [vrf, 'table'])) + if config.exists(base + [vrf, 'table']): + vrfs_current[vrf] = int(config.return_value(base + [vrf, 'table'])) # Check VRF names and table numbers name_regex = re.compile(r'^\d.*$') diff --git a/src/migration-scripts/wanloadbalance/3-to-4 b/src/migration-scripts/wanloadbalance/3-to-4 new file mode 100644 index 000000000..e49f46a5b --- /dev/null +++ b/src/migration-scripts/wanloadbalance/3-to-4 @@ -0,0 +1,33 @@ +# 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/>. + +from vyos.configtree import ConfigTree + +base = ['load-balancing', 'wan'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + if config.exists(base + ['rule']): + for rule in config.list_nodes(base + ['rule']): + rule_base = base + ['rule', rule] + + if config.exists(rule_base + ['inbound-interface']): + ifname = config.return_value(rule_base + ['inbound-interface']) + + if ifname.endswith('+'): + config.set(rule_base + ['inbound-interface'], value=ifname.replace('+', '*')) diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index 1429fd7b1..725bfc75b 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 @@ -30,137 +30,72 @@ 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 _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.utcfromtimestamp(expiry - lifetime) - lease['expire_timestamp'] = datetime.utcfromtimestamp(expiry) 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'): @@ -170,46 +105,67 @@ def _get_formatted_server_leases(raw_data, family='inet'): ipaddr = lease.get('ip') 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') - end = lease.get('end') - end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-' + start = datetime.fromtimestamp(lease.get('start'), timezone.utc) + end = ( + datetime.fromtimestamp(lease.get('end'), timezone.utc) + if lease.get('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') - end = lease.get('end') - end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') + start = datetime.fromtimestamp( + lease.get('last_communication'), timezone.utc + ) + end = ( + datetime.fromtimestamp(lease.get('end'), timezone.utc) + if lease.get('end') + else '-' + ) 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 +185,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): data_entries = [] for entry in pool_data: pool = entry.get('pool') @@ -259,67 +216,52 @@ 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'): + +def _get_formatted_server_static_mappings(raw_data): 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') + desc = entry.get('description', 'N/A') + data_entries.append([pool, subnet, hostname, ip_addr, mac_addr, duid, desc]) + + 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 +275,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 +297,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) + + +@_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) + return _get_formatted_server_static_mappings(static_mappings) + 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 +429,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,22 +458,29 @@ 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): + +def _get_formatted_client_leases(lease_data): from time import localtime from time import strftime @@ -481,30 +490,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,12 +526,14 @@ 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: return lease_data else: - return _get_formatted_client_leases(lease_data, family=family) + return _get_formatted_client_leases(lease_data) + @_verify_client def renew_client_lease(raw: bool, family: ArgFamily, interface: str): @@ -530,6 +545,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 +556,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/firewall.py b/src/op_mode/firewall.py index c197ca434..f3309ee34 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -18,6 +18,7 @@ import argparse import ipaddress import json import re +from signal import signal, SIGPIPE, SIG_DFL import tabulate import textwrap @@ -25,6 +26,9 @@ from vyos.config import Config from vyos.utils.process import cmd from vyos.utils.dict import dict_search_args +signal(SIGPIPE, SIG_DFL) + + def get_config_node(conf, node=None, family=None, hook=None, priority=None): if node == 'nat': if family == 'ipv6': @@ -148,6 +152,38 @@ def get_nftables_group_members(family, table, name): return out +def get_nftables_remote_group_members(family, table, name): + prefix = 'ip6' if family == 'ipv6' else 'ip' + out = [] + + try: + results_str = cmd(f'nft -j list set {prefix} {table} {name}') + results = json.loads(results_str) + except: + return out + + if 'nftables' not in results: + return out + + for obj in results['nftables']: + if 'set' not in obj: + continue + + set_obj = obj['set'] + if 'elem' in set_obj: + for elem in set_obj['elem']: + # search for single IP elements + if isinstance(elem, str): + out.append(elem) + # search for prefix elements + elif isinstance(elem, dict) and 'prefix' in elem: + out.append(f"{elem['prefix']['addr']}/{elem['prefix']['len']}") + # search for IP range elements + elif isinstance(elem, dict) and 'range' in elem: + out.append(f"{elem['range'][0]}-{elem['range'][1]}") + + return out + def output_firewall_vertical(rules, headers, adjust=True): for rule in rules: adjusted_rule = rule + [""] * (len(headers) - len(rule)) if adjust else rule # account for different header length, like default-action @@ -253,15 +289,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule if not source_addr: source_addr = dict_search_args(rule_conf, 'source', 'group', 'domain_group') if not source_addr: - source_addr = dict_search_args(rule_conf, 'source', 'fqdn') + source_addr = dict_search_args(rule_conf, 'source', 'group', 'remote_group') if not source_addr: - source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code') - if source_addr: - source_addr = str(source_addr)[1:-1].replace('\'','') - if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'): - source_addr = 'NOT ' + str(source_addr) + source_addr = dict_search_args(rule_conf, 'source', 'fqdn') if not source_addr: - source_addr = 'any' + source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code') + if source_addr: + source_addr = str(source_addr)[1:-1].replace('\'','') + if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'): + source_addr = 'NOT ' + str(source_addr) + if not source_addr: + source_addr = 'any' # Get destination dest_addr = dict_search_args(rule_conf, 'destination', 'address') @@ -272,15 +310,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule if not dest_addr: dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'domain_group') if not dest_addr: - dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn') + dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'remote_group') if not dest_addr: - dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code') - if dest_addr: - dest_addr = str(dest_addr)[1:-1].replace('\'','') - if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'): - dest_addr = 'NOT ' + str(dest_addr) + dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn') if not dest_addr: - dest_addr = 'any' + dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code') + if dest_addr: + dest_addr = str(dest_addr)[1:-1].replace('\'','') + if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'): + dest_addr = 'NOT ' + str(dest_addr) + if not dest_addr: + dest_addr = 'any' # Get inbound interface iiface = dict_search_args(rule_conf, 'inbound_interface', 'name') @@ -552,30 +592,8 @@ def show_firewall_group(name=None): header_tail = [] for group_type, group_type_conf in firewall['group'].items(): - ## - if group_type != 'dynamic_group': - - for group_name, group_conf in group_type_conf.items(): - if name and name != group_name: - continue - - references = find_references(group_type, group_name) - row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D'] - if 'address' in group_conf: - row.append("\n".join(sorted(group_conf['address']))) - elif 'network' in group_conf: - row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) - elif 'mac_address' in group_conf: - row.append("\n".join(sorted(group_conf['mac_address']))) - elif 'port' in group_conf: - row.append("\n".join(sorted(group_conf['port']))) - elif 'interface' in group_conf: - row.append("\n".join(sorted(group_conf['interface']))) - else: - row.append('N/D') - rows.append(row) - - else: + # interate over dynamic-groups + if group_type == 'dynamic_group': if not args.detail: header_tail = ['Timeout', 'Expires'] @@ -584,6 +602,9 @@ def show_firewall_group(name=None): prefix = 'DA_' if dynamic_type == 'address_group' else 'DA6_' if dynamic_type in firewall['group']['dynamic_group']: for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items(): + if name and name != dynamic_name: + continue + references = find_references(dynamic_type, dynamic_name) row = [dynamic_name, textwrap.fill(dynamic_conf.get('description') or '', 50), dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D'] @@ -622,6 +643,68 @@ def show_firewall_group(name=None): header_tail += [""] * (len(members) - 1) rows.append(row) + # iterate over remote-groups + elif group_type == 'remote_group': + for remote_name, remote_conf in group_type_conf.items(): + if name and name != remote_name: + continue + + references = find_references(group_type, remote_name) + row = [remote_name, textwrap.fill(remote_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D'] + members = get_nftables_remote_group_members("ipv4", 'vyos_filter', f'R_{remote_name}') + members6 = get_nftables_remote_group_members("ipv6", 'vyos_filter', f'R6_{remote_name}') + + if 'url' in remote_conf: + # display only the url if no members are found for both views + if not members and not members6: + if args.detail: + header_tail = ['IPv6 Members', 'Remote URL'] + row.append('N/D') + row.append('N/D') + row.append(remote_conf['url']) + else: + row.append(remote_conf['url']) + rows.append(row) + else: + # display all table elements in detail view + if args.detail: + header_tail = ['IPv6 Members', 'Remote URL'] + if members: + row.append(' '.join(members)) + else: + row.append('N/D') + if members6: + row.append(' '.join(members6)) + else: + row.append('N/D') + row.append(remote_conf['url']) + rows.append(row) + else: + row.append(remote_conf['url']) + rows.append(row) + + # catch the rest of the group types + else: + for group_name, group_conf in group_type_conf.items(): + if name and name != group_name: + continue + + references = find_references(group_type, group_name) + row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D'] + if 'address' in group_conf: + row.append("\n".join(sorted(group_conf['address']))) + elif 'network' in group_conf: + row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) + elif 'mac_address' in group_conf: + row.append("\n".join(sorted(group_conf['mac_address']))) + elif 'port' in group_conf: + row.append("\n".join(sorted(group_conf['port']))) + elif 'interface' in group_conf: + row.append("\n".join(sorted(group_conf['interface']))) + else: + row.append('N/D') + rows.append(row) + if rows: print('Firewall Groups\n') if args.detail: diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 1da112673..ac5a84419 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2025 VyOS maintainers and contributors <maintainers@vyos.io> # # This file is part of VyOS. # @@ -24,7 +24,9 @@ from glob import glob from sys import exit from os import environ from os import readlink -from os import getpid, getppid +from os import getpid +from os import getppid +from json import loads from typing import Union from urllib.parse import urlparse from passlib.hosts import linux_context @@ -32,12 +34,26 @@ from errno import ENOSPC from psutil import disk_partitions +from vyos.base import Warning from vyos.configtree import ConfigTree from vyos.remote import download -from vyos.system import disk, grub, image, compat, raid, SYSTEM_CFG_VER +from vyos.system import disk +from vyos.system import grub +from vyos.system import image +from vyos.system import compat +from vyos.system import raid +from vyos.system import SYSTEM_CFG_VER +from vyos.system import grub_util from vyos.template import render +from vyos.utils.auth import ( + DEFAULT_PASSWORD, + EPasswdStrength, + evaluate_strength +) +from vyos.utils.dict import dict_search from vyos.utils.io import ask_input, ask_yes_no, select_entry from vyos.utils.file import chmod_2775 +from vyos.utils.file import read_file from vyos.utils.process import cmd, run, rc_cmd from vyos.version import get_version_data @@ -46,7 +62,13 @@ MSG_ERR_NOT_LIVE: str = 'The system is already installed. Please use "add system MSG_ERR_LIVE: str = 'The system is in live-boot mode. Please use "install image" instead.' MSG_ERR_NO_DISK: str = 'No suitable disk was found. There must be at least one disk of 2GB or greater size.' MSG_ERR_IMPROPER_IMAGE: str = 'Missing sha256sum.txt.\nEither this image is corrupted, or of era 1.2.x (md5sum) and would downgrade image tools;\ndisallowed in either case.' -MSG_ERR_ARCHITECTURE_MISMATCH: str = 'Upgrading to a different image architecture will break your system.' +MSG_ERR_INCOMPATIBLE_IMAGE: str = 'Image compatibility check failed, aborting installation.' +MSG_ERR_ARCHITECTURE_MISMATCH: str = 'The current architecture is "{0}", the new image is for "{1}". Upgrading to a different image architecture will break your system.' +MSG_ERR_FLAVOR_MISMATCH: str = 'The current image flavor is "{0}", the new image is "{1}". Upgrading to a non-matching flavor can have unpredictable consequences.' +MSG_ERR_MISSING_ARCHITECTURE: str = 'The new image version data does not specify architecture, cannot check compatibility (is it a legacy release image?)' +MSG_ERR_MISSING_FLAVOR: str = 'The new image version data does not specify flavor, cannot check compatibility (is it a legacy release image?)' +MSG_ERR_CORRUPT_CURRENT_IMAGE: str = 'Version data in the current image is malformed: missing flavor and/or architecture fields. Upgrade compatibility cannot be checked.' +MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE: str = 'Unsupported signature type, signature cannot be verified.' MSG_INFO_INSTALL_WELCOME: str = 'Welcome to VyOS installation!\nThis command will install VyOS to your permanent storage.' MSG_INFO_INSTALL_EXIT: str = 'Exiting from VyOS installation' MSG_INFO_INSTALL_SUCCESS: str = 'The image installed successfully; please reboot now.' @@ -62,6 +84,7 @@ MSG_INPUT_CONFIG_FOUND: str = 'An active configuration was found. Would you like MSG_INPUT_CONFIG_CHOICE: str = 'The following config files are available for boot:' MSG_INPUT_CONFIG_CHOOSE: str = 'Which file would you like as boot config?' MSG_INPUT_IMAGE_NAME: str = 'What would you like to name this image?' +MSG_INPUT_IMAGE_NAME_TAKEN: str = 'There is already an installed image by that name; please choose again' MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set the new image as the default one for boot?' MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:' MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:' @@ -78,8 +101,10 @@ MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.' MSG_WARN_ROOT_SIZE_TOOSMALL: str = 'The size is too small. Try again' MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n'\ 'It must be between 1 and 64 characters long and contains only the next characters: .+-_ a-z A-Z 0-9' + +MSG_WARN_CHANGE_PASSWORD: str = 'Default password used. Consider changing ' \ + 'it on next login.' MSG_WARN_PASSWORD_CONFIRM: str = 'The entered values did not match. Try again' -MSG_WARN_FLAVOR_MISMATCH: str = 'The running image flavor is "{0}". The new image flavor is "{1}".\n' \ 'Installing a different image flavor may cause functionality degradation or break your system.\n' \ 'Do you want to continue with installation?' CONST_MIN_DISK_SIZE: int = 2147483648 # 2 GB @@ -95,7 +120,7 @@ DIR_ISO_MOUNT: str = f'{DIR_INSTALLATION}/iso_src' DIR_DST_ROOT: str = f'{DIR_INSTALLATION}/disk_dst' DIR_KERNEL_SRC: str = '/boot/' FILE_ROOTFS_SRC: str = '/usr/lib/live/mount/medium/live/filesystem.squashfs' -ISO_DOWNLOAD_PATH: str = '/tmp/vyos_installation.iso' +ISO_DOWNLOAD_PATH: str = '' external_download_script = '/usr/libexec/vyos/simple-download.py' external_latest_image_url_script = '/usr/libexec/vyos/latest-image-url.py' @@ -462,6 +487,29 @@ def setup_grub(root_dir: str) -> None: render(grub_cfg_menu, grub.TMPL_GRUB_MENU, {}) render(grub_cfg_options, grub.TMPL_GRUB_OPTS, {}) +def get_cli_kernel_options(config_file: str) -> list: + config = ConfigTree(read_file(config_file)) + config_dict = loads(config.to_json()) + kernel_options = dict_search('system.option.kernel', config_dict) + if kernel_options is None: + kernel_options = {} + cmdline_options = [] + + # XXX: This code path and if statements must be kept in sync with the Kernel + # option handling in system_options.py:generate(). This occurance is used + # for having the appropriate options passed to GRUB after an image upgrade! + if 'disable-mitigations' in kernel_options: + cmdline_options.append('mitigations=off') + if 'disable-power-saving' in kernel_options: + cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1') + if 'amd-pstate-driver' in kernel_options: + mode = kernel_options['amd-pstate-driver'] + cmdline_options.append( + f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}') + if 'quiet' in kernel_options: + cmdline_options.append('quiet') + + return cmdline_options def configure_authentication(config_file: str, password: str) -> None: """Write encrypted password to config file @@ -476,10 +524,7 @@ def configure_authentication(config_file: str, password: str) -> None: plaintext exposed """ encrypted_password = linux_context.hash(password) - - with open(config_file) as f: - config_string = f.read() - + config_string = read_file(config_file) config = ConfigTree(config_string) config.set([ 'system', 'login', 'user', 'vyos', 'authentication', @@ -501,7 +546,6 @@ def validate_signature(file_path: str, sign_type: str) -> None: """ print('Validating signature') signature_valid: bool = False - # validate with minisig if sign_type == 'minisig': pub_key_list = glob('/usr/share/vyos/keys/*.minisign.pub') for pubkey in pub_key_list: @@ -510,11 +554,8 @@ def validate_signature(file_path: str, sign_type: str) -> None: signature_valid = True break Path(f'{file_path}.minisig').unlink() - # validate with GPG - if sign_type == 'asc': - if run(f'gpg --verify ${file_path}.asc ${file_path}') == 0: - signature_valid = True - Path(f'{file_path}.asc').unlink() + else: + exit(MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE) # warn or pass if not signature_valid: @@ -524,21 +565,18 @@ def validate_signature(file_path: str, sign_type: str) -> None: print('Signature is valid') def download_file(local_file: str, remote_path: str, vrf: str, - username: str, password: str, progressbar: bool = False, check_space: bool = False): - environ['REMOTE_USERNAME'] = username - environ['REMOTE_PASSWORD'] = password + # Server credentials are implicitly passed in environment variables + # that are set by add_image if vrf is None: download(local_file, remote_path, progressbar=progressbar, check_space=check_space, raise_error=True) else: - remote_auth = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password}' vrf_cmd = f'ip vrf exec {vrf} {external_download_script} \ --local-file {local_file} --remote-path {remote_path}' - cmd(vrf_cmd, auth=remote_auth) + cmd(vrf_cmd, env=environ) def image_fetch(image_path: str, vrf: str = None, - username: str = '', password: str = '', no_prompt: bool = False) -> Path: """Fetch an ISO image @@ -548,13 +586,17 @@ def image_fetch(image_path: str, vrf: str = None, Returns: Path: a path to a local file """ + import os.path + from uuid import uuid4 + + global ISO_DOWNLOAD_PATH + # Latest version gets url from configured "system update-check url" if image_path == 'latest': command = external_latest_image_url_script if vrf: - command = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password} \ - ip vrf exec {vrf} ' + command - code, output = rc_cmd(command) + command = f'ip vrf exec {vrf} {command}' + code, output = rc_cmd(command, env=environ) if code: print(output) exit(MSG_INFO_INSTALL_EXIT) @@ -563,23 +605,25 @@ def image_fetch(image_path: str, vrf: str = None, try: # check a type of path if urlparse(image_path).scheme: - # download an image + # Download the image file + ISO_DOWNLOAD_PATH = os.path.join(os.path.expanduser("~"), '{0}.iso'.format(uuid4())) download_file(ISO_DOWNLOAD_PATH, image_path, vrf, - username, password, progressbar=True, check_space=True) - # download a signature + # Download the image signature + # VyOS only supports minisign signatures at the moment, + # but we keep the logic for multiple signatures + # in case we add something new in the future sign_file = (False, '') - for sign_type in ['minisig', 'asc']: + for sign_type in ['minisig']: try: download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}', - f'{image_path}.{sign_type}', vrf, - username, password) + f'{image_path}.{sign_type}', vrf) sign_file = (True, sign_type) break except Exception: - print(f'{sign_type} signature is not available') - # validate a signature if it is available + print(f'Could not download {sign_type} signature') + # Validate the signature if it is available if sign_file[0]: validate_signature(ISO_DOWNLOAD_PATH, sign_file[1]) else: @@ -701,30 +745,48 @@ def is_raid_install(install_object: Union[disk.DiskDetails, raid.RaidDetails]) - return False -def validate_compatibility(iso_path: str) -> None: +def validate_compatibility(iso_path: str, force: bool = False) -> None: """Check architecture and flavor compatibility with the running image Args: iso_path (str): a path to the mounted ISO image """ - old_data = get_version_data() - old_flavor = old_data.get('flavor', '') - old_architecture = old_data.get('architecture') or cmd('dpkg --print-architecture') + current_data = get_version_data() + current_flavor = current_data.get('flavor') + current_architecture = current_data.get('architecture') or cmd('dpkg --print-architecture') new_data = get_version_data(f'{iso_path}/version.json') - new_flavor = new_data.get('flavor', '') - new_architecture = new_data.get('architecture', '') + new_flavor = new_data.get('flavor') + new_architecture = new_data.get('architecture') - if not old_architecture == new_architecture: - print(MSG_ERR_ARCHITECTURE_MISMATCH) + if not current_flavor or not current_architecture: + # This may only happen if someone modified the version file. + # Unlikely but not impossible. + print(MSG_ERR_CORRUPT_CURRENT_IMAGE) cleanup() exit(MSG_INFO_INSTALL_EXIT) - if not old_flavor == new_flavor: - if not ask_yes_no(MSG_WARN_FLAVOR_MISMATCH.format(old_flavor, new_flavor), default=False): - cleanup() - exit(MSG_INFO_INSTALL_EXIT) + success = True + + if current_architecture != new_architecture: + success = False + if not new_architecture: + print(MSG_ERR_MISSING_ARCHITECTURE) + else: + print(MSG_ERR_ARCHITECTURE_MISMATCH.format(current_architecture, new_architecture)) + + if current_flavor != new_flavor: + if not force: + success = False + if not new_flavor: + print(MSG_ERR_MISSING_FLAVOR) + else: + print(MSG_ERR_FLAVOR_MISMATCH.format(current_flavor, new_flavor)) + if not success: + print(MSG_ERR_INCOMPATIBLE_IMAGE) + cleanup() + exit(MSG_INFO_INSTALL_EXIT) def install_image() -> None: """Install an image to a disk @@ -746,14 +808,25 @@ def install_image() -> None: break print(MSG_WARN_IMAGE_NAME_WRONG) + failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR] # ask for password while True: user_password: str = ask_input(MSG_INPUT_PASSWORD, no_echo=True, non_empty=True) + + if user_password == DEFAULT_PASSWORD: + Warning(MSG_WARN_CHANGE_PASSWORD) + else: + result = evaluate_strength(user_password) + if result['strength'] in failed_check_status: + Warning(result['error']) + confirm: str = ask_input(MSG_INPUT_PASSWORD_CONFIRM, no_echo=True, non_empty=True) + if user_password == confirm: break + print(MSG_WARN_PASSWORD_CONFIRM) # ask for default console @@ -849,8 +922,7 @@ def install_image() -> None: for disk_target in l: disk.partition_mount(disk_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi') grub.install(disk_target.name, f'{DIR_DST_ROOT}/boot/', - f'{DIR_DST_ROOT}/boot/efi', - id=f'VyOS (RAID disk {l.index(disk_target) + 1})') + f'{DIR_DST_ROOT}/boot/efi') disk.partition_umount(disk_target.partition['efi']) else: print('Installing GRUB to the drive') @@ -893,7 +965,7 @@ def install_image() -> None: @compat.grub_cfg_update def add_image(image_path: str, vrf: str = None, username: str = '', - password: str = '', no_prompt: bool = False) -> None: + password: str = '', no_prompt: bool = False, force: bool = False) -> None: """Add a new image Args: @@ -902,15 +974,18 @@ def add_image(image_path: str, vrf: str = None, username: str = '', if image.is_live_boot(): exit(MSG_ERR_LIVE) + environ['REMOTE_USERNAME'] = username + environ['REMOTE_PASSWORD'] = password + # fetch an image - iso_path: Path = image_fetch(image_path, vrf, username, password, no_prompt) + iso_path: Path = image_fetch(image_path, vrf, no_prompt) try: # mount an ISO Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True) disk.partition_mount(iso_path, DIR_ISO_MOUNT, 'iso9660') print('Validating image compatibility') - validate_compatibility(DIR_ISO_MOUNT) + validate_compatibility(DIR_ISO_MOUNT, force=force) # check sums print('Validating image checksums') @@ -936,8 +1011,12 @@ def add_image(image_path: str, vrf: str = None, username: str = '', f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed') if not no_prompt: + versions = grub.version_list() while True: image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name) + if image_name in versions: + print(MSG_INPUT_IMAGE_NAME_TAKEN) + continue if image.validate_name(image_name): break print(MSG_WARN_IMAGE_NAME_WRONG) @@ -959,7 +1038,7 @@ def add_image(image_path: str, vrf: str = None, username: str = '', Path(target_config_dir).mkdir(parents=True) chown(target_config_dir, group='vyattacfg') chmod_2775(target_config_dir) - copytree('/opt/vyatta/etc/config/', target_config_dir, + copytree('/opt/vyatta/etc/config/', target_config_dir, symlinks=True, copy_function=copy_preserve_owner, dirs_exist_ok=True) else: Path(target_config_dir).mkdir(parents=True) @@ -992,6 +1071,12 @@ def add_image(image_path: str, vrf: str = None, username: str = '', if set_as_default: grub.set_default(image_name, root_dir) + cmdline_options = get_cli_kernel_options( + f'{target_config_dir}/config.boot') + grub_util.update_kernel_cmdline_options(' '.join(cmdline_options), + root_dir=root_dir, + version=image_name) + except OSError as e: # if no space error, remove image dir and cleanup if e.errno == ENOSPC: @@ -1031,6 +1116,9 @@ def parse_arguments() -> Namespace: parser.add_argument('--image-path', help='a path (HTTP or local file) to an image that needs to be installed' ) + parser.add_argument('--force', action='store_true', + help='Ignore flavor compatibility requirements.' + ) # parser.add_argument('--image_new_name', help='a new name for image') args: Namespace = parser.parse_args() # Validate arguments @@ -1047,7 +1135,8 @@ if __name__ == '__main__': install_image() if args.action == 'add': add_image(args.image_path, args.vrf, - args.username, args.password, args.no_prompt) + args.username, args.password, + args.no_prompt, args.force) exit() diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index e7afc4caa..c97f3b129 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -29,6 +29,7 @@ from vyos.ifconfig import Section from vyos.ifconfig import Interface from vyos.ifconfig import VRRP from vyos.utils.process import cmd +from vyos.utils.network import interface_exists from vyos.utils.process import rc_cmd from vyos.utils.process import call @@ -84,6 +85,14 @@ def filtered_interfaces(ifnames: typing.Union[str, list], yield interface +def detailed_output(dataset, headers): + for data in dataset: + adjusted_rule = data + [""] * (len(headers) - len(data)) # account for different header length, like default-action + transformed_rule = [[header, adjusted_rule[i]] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char + + print(tabulate(transformed_rule, tablefmt="presto")) + print() + def _split_text(text, used=0): """ take a string and attempt to split it to fit with the width of the screen @@ -296,6 +305,114 @@ def _get_counter_data(ifname: typing.Optional[str], return ret +def _get_kernel_data(raw, ifname = None, detail = False): + if ifname: + # Check if the interface exists + if not interface_exists(ifname): + raise vyos.opmode.IncorrectValue(f"{ifname} does not exist!") + int_name = f'dev {ifname}' + else: + int_name = '' + + kernel_interface = json.loads(cmd(f'ip -j -d -s address show {int_name}')) + + # Return early if raw + if raw: + return kernel_interface, None + + # Format the kernel data + kernel_interface_out = _format_kernel_data(kernel_interface, detail) + + return kernel_interface, kernel_interface_out + +def _format_kernel_data(data, detail): + output_list = [] + tmpInfo = {} + + # Sort interfaces by name + for interface in sorted(data, key=lambda x: x.get('ifname', '')): + if interface.get('linkinfo', {}).get('info_kind') == 'vrf': + continue + + # Get the device model; ex. Intel Corporation Ethernet Controller I225-V + dev_model = interface.get('parentdev', '') + if 'parentdev' in interface: + parentdev = interface['parentdev'] + if re.match(r'^[0-9a-fA-F]{4}:', parentdev): + dev_model = cmd(f'lspci -nn -s {parentdev}').split(']:')[1].strip() + + # Get the IP addresses on interface + ip_list = [] + has_global = False + + for ip in interface['addr_info']: + if ip.get('scope') in ('global', 'host'): + has_global = True + local = ip.get('local', '-') + prefixlen = ip.get('prefixlen', '') + ip_list.append(f"{local}/{prefixlen}") + + + # If no global IP address, add '-'; indicates no IP address on interface + if not has_global: + ip_list.append('-') + + sl_status = ('A' if not 'UP' in interface['flags'] else 'u') + '/' + ('D' if interface['operstate'] == 'DOWN' else 'u') + + # Generate temporary dict to hold data + tmpInfo['ifname'] = interface.get('ifname', '') + tmpInfo['ip'] = ip_list + tmpInfo['mac'] = interface.get('address', '') + tmpInfo['mtu'] = interface.get('mtu', '') + tmpInfo['vrf'] = interface.get('master', 'default') + tmpInfo['status'] = sl_status + tmpInfo['description'] = interface.get('ifalias', '') + tmpInfo['device'] = dev_model + tmpInfo['alternate_names'] = interface.get('altnames', '') + tmpInfo['minimum_mtu'] = interface.get('min_mtu', '') + tmpInfo['maximum_mtu'] = interface.get('max_mtu', '') + rx_stats = interface.get('stats64', {}).get('rx') + tx_stats = interface.get('stats64', {}).get('tx') + tmpInfo['rx_packets'] = rx_stats.get('packets', "") + tmpInfo['rx_bytes'] = rx_stats.get('bytes', "") + tmpInfo['rx_errors'] = rx_stats.get('errors', "") + tmpInfo['rx_dropped'] = rx_stats.get('dropped', "") + tmpInfo['rx_over_errors'] = rx_stats.get('over_errors', '') + tmpInfo['multicast'] = rx_stats.get('multicast', "") + tmpInfo['tx_packets'] = tx_stats.get('packets', "") + tmpInfo['tx_bytes'] = tx_stats.get('bytes', "") + tmpInfo['tx_errors'] = tx_stats.get('errors', "") + tmpInfo['tx_dropped'] = tx_stats.get('dropped', "") + tmpInfo['tx_carrier_errors'] = tx_stats.get('carrier_errors', "") + tmpInfo['tx_collisions'] = tx_stats.get('collisions', "") + + # Generate output list; detail adds more fields + output_list.append([tmpInfo['ifname'], + '\n'.join(tmpInfo['ip']), + tmpInfo['mac'], + tmpInfo['vrf'], + tmpInfo['mtu'], + tmpInfo['status'], + tmpInfo['description'], + *([tmpInfo['device']] if detail else []), + *(['\n'.join(tmpInfo['alternate_names'])] if detail else []), + *([tmpInfo['minimum_mtu']] if detail else []), + *([tmpInfo['maximum_mtu']] if detail else []), + *([tmpInfo['rx_packets']] if detail else []), + *([tmpInfo['rx_bytes']] if detail else []), + *([tmpInfo['rx_errors']] if detail else []), + *([tmpInfo['rx_dropped']] if detail else []), + *([tmpInfo['rx_over_errors']] if detail else []), + *([tmpInfo['multicast']] if detail else []), + *([tmpInfo['tx_packets']] if detail else []), + *([tmpInfo['tx_bytes']] if detail else []), + *([tmpInfo['tx_errors']] if detail else []), + *([tmpInfo['tx_dropped']] if detail else []), + *([tmpInfo['tx_carrier_errors']] if detail else []), + *([tmpInfo['tx_collisions']] if detail else [])]) + + return output_list + @catch_broken_pipe def _format_show_data(data: list): unhandled = [] @@ -445,6 +562,27 @@ def _format_show_counters(data: list): print (output) return output +def show_kernel(raw: bool, intf_name: typing.Optional[str], detail: bool): + raw_data, data = _get_kernel_data(raw, intf_name, detail) + + # Return early if raw + if raw: + return raw_data + + # Normal headers; show interfaces kernel + headers = ['Interface', 'IP Address', 'MAC', 'VRF', 'MTU', 'S/L', 'Description'] + + # Detail headers; show interfaces kernel detail + detail_header = ['Interface', 'IP Address', 'MAC', 'VRF', 'MTU', 'S/L', 'Description', + 'Device', 'Alternate Names','Minimum MTU', 'Maximum MTU', 'RX_Packets', + 'RX_Bytes', 'RX_Errors', 'RX_Dropped', 'Receive Overrun Errors', 'Received Multicast', + 'TX_Packets', 'TX_Bytes', 'TX_Errors', 'TX_Dropped', 'Transmit Carrier Errors', + 'Transmit Collisions'] + + if detail: + detailed_output(data, detail_header) + else: + print(tabulate(data, headers)) def _show_raw(data: list, intf_name: str): if intf_name is not None and len(data) <= 1: 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/load-balancing_wan.py b/src/op_mode/load-balancing_wan.py new file mode 100755 index 000000000..9fa473802 --- /dev/null +++ b/src/op_mode/load-balancing_wan.py @@ -0,0 +1,117 @@ +#!/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 json +import re +import sys + +from datetime import datetime + +from vyos.config import Config +from vyos.utils.process import cmd + +import vyos.opmode + +wlb_status_file = '/run/wlb_status.json' + +status_format = '''Interface: {ifname} +Status: {status} +Last Status Change: {last_change} +Last Interface Success: {last_success} +Last Interface Failure: {last_failure} +Interface Failures: {failures} +''' + +def _verify(func): + """Decorator checks if WLB config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = Config() + if not config.exists(['load-balancing', 'wan']): + unconf_message = 'WAN load-balancing is not configured' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + return _wrapper + +def _get_raw_data(): + with open(wlb_status_file, 'r') as f: + data = json.loads(f.read()) + if not data: + return {} + return data + +def _get_formatted_output(raw_data): + for ifname, if_data in raw_data.items(): + latest_change = if_data['last_success'] if if_data['last_success'] > if_data['last_failure'] else if_data['last_failure'] + + change_dt = datetime.fromtimestamp(latest_change) if latest_change > 0 else None + success_dt = datetime.fromtimestamp(if_data['last_success']) if if_data['last_success'] > 0 else None + failure_dt = datetime.fromtimestamp(if_data['last_failure']) if if_data['last_failure'] > 0 else None + now = datetime.utcnow() + + fmt_data = { + 'ifname': ifname, + 'status': "active" if if_data['state'] else "failed", + 'last_change': change_dt.strftime("%Y-%m-%d %H:%M:%S") if change_dt else 'N/A', + 'last_success': str(now - success_dt) if success_dt else 'N/A', + 'last_failure': str(now - failure_dt) if failure_dt else 'N/A', + 'failures': if_data['failure_count'] + } + print(status_format.format(**fmt_data)) + +@_verify +def show_summary(raw: bool): + data = _get_raw_data() + + if raw: + return data + else: + return _get_formatted_output(data) + +@_verify +def show_connection(raw: bool): + res = cmd('sudo conntrack -L -n') + lines = res.split("\n") + filtered_lines = [line for line in lines if re.search(r' mark=[1-9]', line)] + + if raw: + return filtered_lines + + for line in lines: + print(line) + +@_verify +def show_status(raw: bool): + res = cmd('sudo nft list chain ip vyos_wanloadbalance wlb_mangle_prerouting') + lines = res.split("\n") + filtered_lines = [line.replace("\t", "") for line in lines[3:-2] if 'meta mark set' not in line] + + if raw: + return filtered_lines + + for line in filtered_lines: + print(line) + +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/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/qos.py b/src/op_mode/qos.py index b8ca149a0..464b552ee 100755 --- a/src/op_mode/qos.py +++ b/src/op_mode/qos.py @@ -38,7 +38,7 @@ def get_tc_info(interface_dict, interface_name, policy_type): if not policy_name: return None, None - class_dict = op_mode_config_dict(['qos', 'policy', policy_type, policy_name], key_mangling=('-', '_'), + class_dict = op_mode_config_dict(['qos', 'policy', policy_type, policy_name], get_first_key=True) if not class_dict: return None, None 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/restart.py b/src/op_mode/restart.py index 3b0031f34..efa835485 100755 --- a/src/op_mode/restart.py +++ b/src/op_mode/restart.py @@ -53,6 +53,10 @@ service_map = { 'systemd_service': 'strongswan', 'path': ['vpn', 'ipsec'], }, + 'load-balancing_wan': { + 'systemd_service': 'vyos-wan-load-balance', + 'path': ['load-balancing', 'wan'], + }, 'mdns_repeater': { 'systemd_service': 'avahi-daemon', 'path': ['service', 'mdns', 'repeater'], @@ -86,6 +90,7 @@ services = typing.Literal[ 'haproxy', 'igmp_proxy', 'ipsec', + 'load-balancing_wan', 'mdns_repeater', 'router_advert', 'snmp', diff --git a/src/op_mode/stp.py b/src/op_mode/stp.py new file mode 100755 index 000000000..fb57bd7ee --- /dev/null +++ b/src/op_mode/stp.py @@ -0,0 +1,185 @@ +#!/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 json +from tabulate import tabulate + +import vyos.opmode +from vyos.utils.process import cmd +from vyos.utils.network import interface_exists + +def detailed_output(dataset, headers): + for data in dataset: + adjusted_rule = data + [""] * (len(headers) - len(data)) # account for different header length, like default-action + transformed_rule = [[header, adjusted_rule[i]] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char + + print(tabulate(transformed_rule, tablefmt="presto")) + print() + +def _get_bridge_vlan_data(iface): + allowed_vlans = [] + native_vlan = None + vlanData = json.loads(cmd(f"bridge -j -d vlan show")) + for vlans in vlanData: + if vlans['ifname'] == iface: + for allowed in vlans['vlans']: + if "flags" in allowed and "PVID" in allowed["flags"]: + native_vlan = allowed['vlan'] + elif allowed.get('vlanEnd', None): + allowed_vlans.append(f"{allowed['vlan']}-{allowed['vlanEnd']}") + else: + allowed_vlans.append(str(allowed['vlan'])) + + if not allowed_vlans: + allowed_vlans = ["none"] + if not native_vlan: + native_vlan = "none" + + return ",".join(allowed_vlans), native_vlan + +def _get_stp_data(ifname, brInfo, brStatus): + tmpInfo = {} + + tmpInfo['bridge_name'] = brInfo.get('ifname') + tmpInfo['up_state'] = brInfo.get('operstate') + tmpInfo['priority'] = brInfo.get('linkinfo').get('info_data').get('priority') + tmpInfo['vlan_filtering'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('vlan_filtering') == 1 else "Disabled" + tmpInfo['vlan_protocol'] = brInfo.get('linkinfo').get('info_data').get('vlan_protocol') + + # The version of VyOS I tested had am issue with the "ip -d link show type bridge" + # output. The root_id was always the local bridge, even though the underlying system + # understood when it wasn't. Could be an upstream Bug. I pull from the "/sys/class/net" + # structure instead. This can be changed later if the "ip link" behavior is corrected. + + #tmpInfo['bridge_id'] = brInfo.get('linkinfo').get('info_data').get('bridge_id') + #tmpInfo['root_id'] = brInfo.get('linkinfo').get('info_data').get('root_id') + + tmpInfo['bridge_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/bridge_id").split('.') + tmpInfo['root_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/root_id").split('.') + + # The "/sys/class/net" structure stores the IDs without seperators like ':' or '.' + # This adds a ':' after every 2 characters to make it resemble a MAC Address + tmpInfo['bridge_id'][1] = ':'.join(tmpInfo['bridge_id'][1][i:i+2] for i in range(0, len(tmpInfo['bridge_id'][1]), 2)) + tmpInfo['root_id'][1] = ':'.join(tmpInfo['root_id'][1][i:i+2] for i in range(0, len(tmpInfo['root_id'][1]), 2)) + + tmpInfo['stp_state'] = "Enabled" if brInfo.get('linkinfo', {}).get('info_data', {}).get('stp_state') == 1 else "Disabled" + + # I don't call any of these values, but I created them to be called within raw output if desired + + tmpInfo['mcast_snooping'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('mcast_snooping') == 1 else "Disabled" + tmpInfo['rxbytes'] = brInfo.get('stats64').get('rx').get('bytes') + tmpInfo['rxpackets'] = brInfo.get('stats64').get('rx').get('packets') + tmpInfo['rxerrors'] = brInfo.get('stats64').get('rx').get('errors') + tmpInfo['rxdropped'] = brInfo.get('stats64').get('rx').get('dropped') + tmpInfo['rxover_errors'] = brInfo.get('stats64').get('rx').get('over_errors') + tmpInfo['rxmulticast'] = brInfo.get('stats64').get('rx').get('multicast') + tmpInfo['txbytes'] = brInfo.get('stats64').get('tx').get('bytes') + tmpInfo['txpackets'] = brInfo.get('stats64').get('tx').get('packets') + tmpInfo['txerrors'] = brInfo.get('stats64').get('tx').get('errors') + tmpInfo['txdropped'] = brInfo.get('stats64').get('tx').get('dropped') + tmpInfo['txcarrier_errors'] = brInfo.get('stats64').get('tx').get('carrier_errors') + tmpInfo['txcollosions'] = brInfo.get('stats64').get('tx').get('collisions') + + tmpStatus = [] + for members in brStatus: + if members.get('master') == brInfo.get('ifname'): + allowed_vlans, native_vlan = _get_bridge_vlan_data(members['ifname']) + tmpStatus.append({'interface': members.get('ifname'), + 'state': members.get('state').capitalize(), + 'mtu': members.get('mtu'), + 'pathcost': members.get('cost'), + 'bpduguard': "Enabled" if members.get('guard') == True else "Disabled", + 'rootguard': "Enabled" if members.get('root_block') == True else "Disabled", + 'mac_learning': "Enabled" if members.get('learning') == True else "Disabled", + 'neigh_suppress': "Enabled" if members.get('neigh_suppress') == True else "Disabled", + 'vlan_tunnel': "Enabled" if members.get('vlan_tunnel') == True else "Disabled", + 'isolated': "Enabled" if members.get('isolated') == True else "Disabled", + **({'allowed_vlans': allowed_vlans} if allowed_vlans else {}), + **({'native_vlan': native_vlan} if native_vlan else {})}) + + tmpInfo['members'] = tmpStatus + return tmpInfo + +def show_stp(raw: bool, ifname: typing.Optional[str], detail: bool): + rawList = [] + rawDict = {'stp': []} + + if ifname: + if not interface_exists(ifname): + raise vyos.opmode.Error(f"{ifname} does not exist!") + else: + ifname = "" + + bridgeInfo = json.loads(cmd(f"ip -j -d -s link show type bridge {ifname}")) + + if not bridgeInfo: + raise vyos.opmode.Error(f"No Bridges configured!") + + bridgeStatus = json.loads(cmd(f"bridge -j -s -d link show")) + + for bridges in bridgeInfo: + output_list = [] + amRoot = "" + bridgeDict = _get_stp_data(ifname, bridges, bridgeStatus) + + if bridgeDict['bridge_id'][1] == bridgeDict['root_id'][1]: + amRoot = " (This bridge is the root)" + + print('-' * 80) + print(f"Bridge interface {bridgeDict['bridge_name']} ({bridgeDict['up_state']}):\n") + print(f"Spanning Tree is {bridgeDict['stp_state']}") + print(f"Bridge ID {bridgeDict['bridge_id'][1]}, Priority {int(bridgeDict['bridge_id'][0], 16)}") + print(f"Root ID {bridgeDict['root_id'][1]}, Priority {int(bridgeDict['root_id'][0], 16)}{amRoot}") + print(f"VLANs {bridgeDict['vlan_filtering'].capitalize()}, Protocol {bridgeDict['vlan_protocol']}") + print() + + for members in bridgeDict['members']: + output_list.append([members['interface'], + members['state'], + *([members['pathcost']] if detail else []), + members['bpduguard'], + members['rootguard'], + members['mac_learning'], + *([members['neigh_suppress']] if detail else []), + *([members['vlan_tunnel']] if detail else []), + *([members['isolated']] if detail else []), + *([members['allowed_vlans']] if detail else []), + *([members['native_vlan']] if detail else [])]) + + if raw: + rawList.append(bridgeDict) + elif detail: + headers = ['Interface', 'State', 'Pathcost', 'BPDU_Guard', 'Root_Guard', 'Learning', 'Neighbor_Suppression', 'Q-in-Q', 'Port_Isolation', 'Allowed VLANs', 'Native VLAN'] + detailed_output(output_list, headers) + else: + headers = ['Interface', 'State', 'BPDU_Guard', 'Root_Guard', 'Learning'] + print(tabulate(output_list, headers)) + print() + + if raw: + rawDict['stp'] = rawList + return rawDict + +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/tech_support.py b/src/op_mode/tech_support.py index f60bb87ff..c4496dfa3 100644 --- a/src/op_mode/tech_support.py +++ b/src/op_mode/tech_support.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# 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 @@ -20,6 +20,7 @@ import json import vyos.opmode from vyos.utils.process import cmd +from vyos.base import Warning def _get_version_data(): from vyos.version import get_version_data @@ -51,7 +52,12 @@ def _get_storage(): def _get_devices(): devices = {} devices["pci"] = cmd("lspci") - devices["usb"] = cmd("lsusb") + + try: + devices["usb"] = cmd("lsusb") + except OSError: + Warning("Could not retrieve information about USB devices") + devices["usb"] = {} return devices @@ -97,21 +103,22 @@ def _get_boot_config(): return strip_config_source(config) def _get_config_scripts(): - from os import listdir + from os import walk from os.path import join from vyos.utils.file import read_file scripts = [] dir = '/config/scripts' - for f in listdir(dir): - script = {} - path = join(dir, f) - data = read_file(path) - script["path"] = path - script["data"] = data - - scripts.append(script) + for dirpath, _, filenames in walk(dir): + for filename in filenames: + script = {} + path = join(dirpath, filename) + data = read_file(path) + script["path"] = path + script["data"] = data + + scripts.append(script) return scripts 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/op_mode/zone.py b/src/op_mode/zone.py index 49fecdf28..df39549d2 100644 --- a/src/op_mode/zone.py +++ b/src/op_mode/zone.py @@ -56,10 +56,15 @@ def _convert_one_zone_data(zone: str, zone_config: dict) -> dict: from_zone_dict['firewall_v6'] = dict_search( 'firewall.ipv6_name', from_zone_config) list_of_rules.append(from_zone_dict) + zone_members =[] + interface_members = dict_search('member.interface', zone_config) + vrf_members = dict_search('member.vrf', zone_config) + zone_members += interface_members if interface_members is not None else [] + zone_members += vrf_members if vrf_members is not None else [] zone_dict = { 'name': zone, - 'interface': dict_search('interface', zone_config), + 'members': zone_members, 'type': 'LOCAL' if dict_search('local_zone', zone_config) is not None else None, } @@ -126,7 +131,7 @@ def output_zone_list(zone_conf: dict) -> list: if zone_conf['type'] == 'LOCAL': zone_info.append('LOCAL') else: - zone_info.append("\n".join(zone_conf['interface'])) + zone_info.append("\n".join(zone_conf['members'])) from_zone = [] firewall = [] @@ -175,7 +180,7 @@ def get_formatted_output(zone_policy: list) -> str: :rtype: str """ headers = ["Zone", - "Interfaces", + "Members", "From Zone", "Firewall IPv4", "Firewall IPv6" diff --git a/src/services/api/rest/models.py b/src/services/api/rest/models.py index 27d9fb5ee..dda50010f 100644 --- a/src/services/api/rest/models.py +++ b/src/services/api/rest/models.py @@ -293,6 +293,13 @@ class TracerouteModel(ApiModel): } +class InfoQueryParams(BaseModel): + model_config = {"extra": "forbid"} + + version: bool = True + hostname: bool = True + + class Success(BaseModel): success: bool data: Union[str, bool, Dict] diff --git a/src/services/vyos-commitd b/src/services/vyos-commitd new file mode 100755 index 000000000..e7f2d82c7 --- /dev/null +++ b/src/services/vyos-commitd @@ -0,0 +1,457 @@ +#!/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 os +import sys +import grp +import json +import signal +import socket +import typing +import logging +import traceback +import importlib.util +import io +from contextlib import redirect_stdout +from dataclasses import dataclass +from dataclasses import fields +from dataclasses import field +from dataclasses import asdict +from pathlib import Path + +import tomli + +from google.protobuf.json_format import MessageToDict +from google.protobuf.json_format import ParseDict + +from vyos.defaults import directories +from vyos.utils.boot import boot_configuration_complete +from vyos.configsource import ConfigSourceCache +from vyos.configsource import ConfigSourceError +from vyos.config import Config +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict +from vyos import ConfigError + +from vyos.proto import vycall_pb2 + + +@dataclass +class Status: + success: bool = False + out: str = '' + + +@dataclass +class Call: + script_name: str = '' + tag_value: str = None + arg_value: str = None + reply: Status = None + + def set_reply(self, success: bool, out: str): + self.reply = Status(success=success, out=out) + + +@dataclass +class Session: + # pylint: disable=too-many-instance-attributes + + session_id: str = '' + dry_run: bool = False + atomic: bool = False + background: bool = False + config: Config = None + init: Status = None + calls: list[Call] = field(default_factory=list) + + def set_init(self, success: bool, out: str): + self.init = Status(success=success, out=out) + + +@dataclass +class ServerConf: + commitd_socket: str = '' + session_dir: str = '' + running_cache: str = '' + session_cache: str = '' + + +server_conf = None +SOCKET_PATH = None +conf_mode_scripts = None +frr = None + +CFG_GROUP = 'vyattacfg' + +script_stdout_log = '/tmp/vyos-commitd-script-stdout' + +debug = True + +logger = logging.getLogger(__name__) +logs_handler = logging.StreamHandler() +logger.addHandler(logs_handler) + +if debug: + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.INFO) + + +vyos_conf_scripts_dir = directories['conf_mode'] +commitd_include_file = os.path.join(directories['data'], 'configd-include.json') + + +def key_name_from_file_name(f): + return os.path.splitext(f)[0] + + +def module_name_from_key(k): + return k.replace('-', '_') + + +def path_from_file_name(f): + return os.path.join(vyos_conf_scripts_dir, f) + + +def load_conf_mode_scripts(): + with open(commitd_include_file) as f: + try: + include = json.load(f) + except OSError as e: + logger.critical(f'configd include file error: {e}') + sys.exit(1) + except json.JSONDecodeError as e: + logger.critical(f'JSON load error: {e}') + sys.exit(1) + + # import conf_mode scripts + (_, _, filenames) = next(iter(os.walk(vyos_conf_scripts_dir))) + filenames.sort() + + # this is redundant, as all scripts are currently in the include file; + # leave it as an inexpensive check for future changes + load_filenames = [f for f in filenames if f in include] + imports = [key_name_from_file_name(f) for f in load_filenames] + module_names = [module_name_from_key(k) for k in imports] + paths = [path_from_file_name(f) for f in load_filenames] + to_load = list(zip(module_names, paths)) + + modules = [] + + for x in to_load: + spec = importlib.util.spec_from_file_location(x[0], x[1]) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + modules.append(module) + + scripts = dict(zip(imports, modules)) + + return scripts + + +def get_session_out(session: Session) -> str: + out = '' + if session.init and session.init.out: + out = f'{out} + init: {session.init.out} + \n' + for call in session.calls: + reply = call.reply + if reply and reply.out: + out = f'{out} + {call.script_name}: {reply.out} + \n' + return out + + +def write_stdout_log(file_name, session): + if boot_configuration_complete(): + return + with open(file_name, 'a') as f: + f.write(get_session_out(session)) + + +def msg_to_commit_data(msg: vycall_pb2.Commit) -> Session: + # pylint: disable=no-member + + d = MessageToDict(msg, preserving_proto_field_name=True) + + # wrap in dataclasses + session = Session(**d) + session.init = Status(**session.init) if session.init else None + session.calls = list(map(lambda x: Call(**x), session.calls)) + for call in session.calls: + call.reply = Status(**call.reply) if call.reply else None + + return session + + +def commit_data_to_msg(obj: Session) -> vycall_pb2.Commit: + # pylint: disable=no-member + + # avoid asdict attempt of deepcopy on Config obj + obj.config = None + + msg = vycall_pb2.Commit() + msg = ParseDict(asdict(obj), msg, ignore_unknown_fields=True) + + return msg + + +def initialization(session: Session) -> Session: + running_cache = os.path.join(server_conf.session_dir, server_conf.running_cache) + session_cache = os.path.join(server_conf.session_dir, server_conf.session_cache) + try: + configsource = ConfigSourceCache( + running_config_cache=running_cache, + session_config_cache=session_cache, + ) + except ConfigSourceError as e: + fail_msg = f'Failed to read config caches: {e}' + logger.critical(fail_msg) + session.set_init(False, fail_msg) + return session + + session.set_init(True, '') + + config = Config(config_source=configsource) + + dependent_func: dict[str, list[typing.Callable]] = {} + setattr(config, 'dependent_func', dependent_func) + + scripts_called = [] + setattr(config, 'scripts_called', scripts_called) + + dry_run = session.dry_run + config.set_bool_attr('dry_run', dry_run) + logger.debug(f'commit dry_run is {dry_run}') + + session.config = config + + return session + + +def run_script(script_name: str, config: Config, args: list) -> tuple[bool, str]: + # pylint: disable=broad-exception-caught + + script = conf_mode_scripts[script_name] + script.argv = args + config.set_level([]) + dry_run = config.get_bool_attr('dry_run') + try: + c = script.get_config(config) + script.verify(c) + if not dry_run: + script.generate(c) + script.apply(c) + else: + if hasattr(script, 'call_dependents'): + script.call_dependents() + except ConfigError as e: + logger.error(e) + return False, str(e) + except Exception: + tb = traceback.format_exc() + logger.error(tb) + return False, tb + + return True, '' + + +def process_call_data(call: Call, config: Config, last: bool = False) -> None: + # pylint: disable=too-many-locals + + script_name = key_name_from_file_name(call.script_name) + + if script_name not in conf_mode_scripts: + fail_msg = f'No such script: {call.script_name}' + logger.critical(fail_msg) + call.set_reply(False, fail_msg) + return + + config.dependency_list.clear() + + tag_value = call.tag_value if call.tag_value is not None else '' + os.environ['VYOS_TAGNODE_VALUE'] = tag_value + + args = call.arg_value.split() if call.arg_value else [] + args.insert(0, f'{script_name}.py') + + tag_ext = f'_{tag_value}' if tag_value else '' + script_record = f'{script_name}{tag_ext}' + scripts_called = getattr(config, 'scripts_called', []) + scripts_called.append(script_record) + + with redirect_stdout(io.StringIO()) as o: + success, err_out = run_script(script_name, config, args) + amb_out = o.getvalue() + o.close() + + out = amb_out + err_out + + call.set_reply(success, out) + + logger.info(f'[{script_name}] {out}') + + if last: + scripts_called = getattr(config, 'scripts_called', []) + logger.debug(f'scripts_called: {scripts_called}') + + if last and success: + tmp = get_frrender_dict(config) + if frr.generate(tmp): + # only apply a new FRR configuration if anything changed + # in comparison to the previous applied configuration + frr.apply() + + +def process_session_data(session: Session) -> Session: + if session.init is None or not session.init.success: + return session + + config = session.config + len_calls = len(session.calls) + for index, call in enumerate(session.calls): + process_call_data(call, config, last=len_calls == index + 1) + + return session + + +def read_message(msg: bytes) -> Session: + """Read message into Session instance""" + + message = vycall_pb2.Commit() # pylint: disable=no-member + message.ParseFromString(msg) + session = msg_to_commit_data(message) + + session = initialization(session) + session = process_session_data(session) + + write_stdout_log(script_stdout_log, session) + + return session + + +def write_reply(session: Session) -> bytearray: + """Serialize modified object to bytearray, prepending data length + header""" + + reply = commit_data_to_msg(session) + encoded_data = reply.SerializeToString() + byte_size = reply.ByteSize() + length_bytes = byte_size.to_bytes(4) + arr = bytearray(length_bytes) + arr.extend(encoded_data) + + return arr + + +def load_server_conf() -> ServerConf: + # pylint: disable=import-outside-toplevel + # pylint: disable=broad-exception-caught + from vyos.defaults import vyconfd_conf + + try: + with open(vyconfd_conf, 'rb') as f: + vyconfd_conf_d = tomli.load(f) + + except Exception as e: + logger.critical(f'Failed to open the vyconfd.conf file {vyconfd_conf}: {e}') + sys.exit(1) + + app = vyconfd_conf_d.get('appliance', {}) + + conf_data = { + k: v for k, v in app.items() if k in [_.name for _ in fields(ServerConf)] + } + + conf = ServerConf(**conf_data) + + return conf + + +def remove_if_exists(f: str): + try: + os.unlink(f) + except FileNotFoundError: + pass + + +def sig_handler(_signum, _frame): + logger.info('stopping server') + raise KeyboardInterrupt + + +def run_server(): + # pylint: disable=global-statement + + global server_conf + global SOCKET_PATH + global conf_mode_scripts + global frr + + signal.signal(signal.SIGTERM, sig_handler) + signal.signal(signal.SIGINT, sig_handler) + + logger.info('starting server') + + server_conf = load_server_conf() + SOCKET_PATH = server_conf.commitd_socket + conf_mode_scripts = load_conf_mode_scripts() + + cfg_group = grp.getgrnam(CFG_GROUP) + os.setgid(cfg_group.gr_gid) + + server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + + remove_if_exists(SOCKET_PATH) + server_socket.bind(SOCKET_PATH) + Path(SOCKET_PATH).chmod(0o775) + + # We only need one long-lived instance of FRRender + frr = FRRender() + + server_socket.listen(2) + while True: + try: + conn, _ = server_socket.accept() + logger.debug('connection accepted') + while True: + # receive size of data + data_length = conn.recv(4) + if not data_length: + logger.debug('no data') + # if no data break + break + + length = int.from_bytes(data_length) + # receive data + data = conn.recv(length) + + session = read_message(data) + reply = write_reply(session) + conn.sendall(reply) + + conn.close() + logger.debug('connection closed') + + except KeyboardInterrupt: + break + + server_socket.close() + sys.exit(0) + + +if __name__ == '__main__': + run_server() diff --git a/src/services/vyos-configd b/src/services/vyos-configd index d558e8c26..28acccd2c 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -28,6 +28,7 @@ import traceback import importlib.util import io from contextlib import redirect_stdout +from enum import Enum import zmq @@ -60,11 +61,14 @@ SOCKET_PATH = 'ipc:///run/vyos-configd.sock' MAX_MSG_SIZE = 65535 PAD_MSG_SIZE = 6 + # Response error codes -R_SUCCESS = 1 -R_ERROR_COMMIT = 2 -R_ERROR_DAEMON = 4 -R_PASS = 8 +class Response(Enum): + SUCCESS = 1 + ERROR_COMMIT = 2 + ERROR_DAEMON = 4 + PASS = 8 + vyos_conf_scripts_dir = directories['conf_mode'] configd_include_file = os.path.join(directories['data'], 'configd-include.json') @@ -73,12 +77,15 @@ configd_env_unset_file = os.path.join(directories['data'], 'vyos-configd-env-uns # sourced on entering config session configd_env_file = '/etc/default/vyos-configd-env' + def key_name_from_file_name(f): return os.path.splitext(f)[0] + def module_name_from_key(k): return k.replace('-', '_') + def path_from_file_name(f): return os.path.join(vyos_conf_scripts_dir, f) @@ -126,7 +133,7 @@ def write_stdout_log(file_name, msg): f.write(msg) -def run_script(script_name, config, args) -> tuple[int, str]: +def run_script(script_name, config, args) -> tuple[Response, str]: # pylint: disable=broad-exception-caught script = conf_mode_scripts[script_name] @@ -139,13 +146,13 @@ def run_script(script_name, config, args) -> tuple[int, str]: script.apply(c) except ConfigError as e: logger.error(e) - return R_ERROR_COMMIT, str(e) + return Response.ERROR_COMMIT, str(e) except Exception: tb = traceback.format_exc() logger.error(tb) - return R_ERROR_COMMIT, tb + return Response.ERROR_COMMIT, tb - return R_SUCCESS, '' + return Response.SUCCESS, '' def initialization(socket): @@ -195,8 +202,9 @@ def initialization(socket): os.environ['VYATTA_CHANGES_ONLY_DIR'] = changes_only_dir_string try: - configsource = ConfigSourceString(running_config_text=active_string, - session_config_text=session_string) + configsource = ConfigSourceString( + running_config_text=active_string, session_config_text=session_string + ) except ConfigSourceError as e: logger.debug(e) return None @@ -211,17 +219,14 @@ def initialization(socket): scripts_called = [] setattr(config, 'scripts_called', scripts_called) - if not hasattr(config, 'frrender_cls'): - setattr(config, 'frrender_cls', FRRender()) - return config -def process_node_data(config, data, _last: bool = False) -> tuple[int, str]: +def process_node_data(config, data, _last: bool = False) -> tuple[Response, str]: if not config: out = 'Empty config' logger.critical(out) - return R_ERROR_DAEMON, out + return Response.ERROR_DAEMON, out script_name = None os.environ['VYOS_TAGNODE_VALUE'] = '' @@ -237,7 +242,7 @@ def process_node_data(config, data, _last: bool = False) -> tuple[int, str]: if not script_name: out = 'Missing script_name' logger.critical(out) - return R_ERROR_DAEMON, out + return Response.ERROR_DAEMON, out if res.group(3): args = res.group(3).split() args.insert(0, f'{script_name}.py') @@ -249,7 +254,7 @@ def process_node_data(config, data, _last: bool = False) -> tuple[int, str]: scripts_called.append(script_record) if script_name not in include_set: - return R_PASS, '' + return Response.PASS, '' with redirect_stdout(io.StringIO()) as o: result, err_out = run_script(script_name, config, args) @@ -262,13 +267,15 @@ def process_node_data(config, data, _last: bool = False) -> tuple[int, str]: def send_result(sock, err, msg): + err_no = err.value + err_name = err.name msg = msg if msg else '' msg_size = min(MAX_MSG_SIZE, len(msg)) - err_rep = err.to_bytes(1) + err_rep = err_no.to_bytes(1) msg_size_rep = f'{msg_size:#0{PAD_MSG_SIZE}x}' - logger.debug(f'Sending reply: error_code {err} with output') + logger.debug(f'Sending reply: {err_name} with output') sock.send_multipart([err_rep, msg_size_rep.encode(), msg.encode()]) write_stdout_log(script_stdout_log, msg) @@ -312,8 +319,10 @@ if __name__ == '__main__': remove_if_file(configd_env_file) os.symlink(configd_env_set_file, configd_env_file) - config = None + # We only need one long-lived instance of FRRender + frr = FRRender() + config = None while True: # Wait for next request from client msg = socket.recv().decode() @@ -332,10 +341,11 @@ if __name__ == '__main__': scripts_called = getattr(config, 'scripts_called', []) logger.debug(f'scripts_called: {scripts_called}') - if hasattr(config, 'frrender_cls') and res == R_SUCCESS: - frrender_cls = getattr(config, 'frrender_cls') + if res == Response.SUCCESS: tmp = get_frrender_dict(config) - frrender_cls.generate(tmp) - frrender_cls.apply() + if frr.generate(tmp): + # only apply a new FRR configuration if anything changed + # in comparison to the previous applied configuration + frr.apply() else: logger.critical(f'Unexpected message: {message}') diff --git a/src/services/vyos-conntrack-logger b/src/services/vyos-conntrack-logger index 9c31b465f..ec0e1f717 100755 --- a/src/services/vyos-conntrack-logger +++ b/src/services/vyos-conntrack-logger @@ -15,10 +15,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import argparse -import grp import logging import multiprocessing -import os import queue import signal import socket diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver new file mode 100755 index 000000000..fb18724af --- /dev/null +++ b/src/services/vyos-domain-resolver @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022-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 json +import time +import logging +import os + +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.remote import download +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.file import makedir, chmod_775, write_file, read_file +from vyos.utils.network import is_valid_ipv4_address_or_range, is_valid_ipv6_address_or_range +from vyos.utils.process import cmd +from vyos.utils.process import run +from vyos.xml_ref import get_defaults + +base = ['firewall'] +timeout = 300 +cache = False +base_firewall = ['firewall'] +base_nat = ['nat'] +base_interfaces = ['interfaces'] + +firewall_config_dir = "/config/firewall" + +domain_state = {} + +ipv4_tables = { + 'ip vyos_mangle', + 'ip vyos_filter', + 'ip vyos_nat', + 'ip raw' +} + +ipv6_tables = { + 'ip6 vyos_mangle', + 'ip6 vyos_filter', + 'ip6 raw' +} + +logger = logging.getLogger(__name__) +logs_handler = logging.StreamHandler() +logger.addHandler(logs_handler) +logger.setLevel(logging.INFO) + +def get_config(conf, node): + node_config = conf.get_config_dict(node, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + default_values = get_defaults(node, get_first_key=True) + + node_config = dict_merge(default_values, node_config) + + if node == base_firewall and 'global_options' in node_config: + global_config = node_config['global_options'] + global timeout, cache + + if 'resolver_interval' in global_config: + timeout = int(global_config['resolver_interval']) + + if 'resolver_cache' in global_config: + cache = True + + fqdn_config_parse(node_config, node[0]) + + return node_config + +def resolve(domains, ipv6=False): + global domain_state + + ip_list = set() + + for domain in domains: + resolved = fqdn_resolve(domain, ipv6=ipv6) + + cache_key = f'{domain}_ipv6' if ipv6 else domain + + if resolved and cache: + domain_state[cache_key] = resolved + elif not resolved: + if cache_key not in domain_state: + continue + resolved = domain_state[cache_key] + + ip_list = ip_list | resolved + return ip_list + +def nft_output(table, set_name, ip_list): + output = [f'flush set {table} {set_name}'] + if ip_list: + ip_str = ','.join(ip_list) + output.append(f'add element {table} {set_name} {{ {ip_str} }}') + return output + +def nft_valid_sets(): + try: + valid_sets = [] + sets_json = cmd('nft --json list sets') + sets_obj = json.loads(sets_json) + + for obj in sets_obj['nftables']: + if 'set' in obj: + family = obj['set']['family'] + table = obj['set']['table'] + name = obj['set']['name'] + valid_sets.append((f'{family} {table}', name)) + + return valid_sets + except: + return [] + +def update_remote_group(config): + conf_lines = [] + count = 0 + valid_sets = nft_valid_sets() + + remote_groups = dict_search_args(config, 'group', 'remote_group') + if remote_groups: + # Create directory for list files if necessary + if not os.path.isdir(firewall_config_dir): + makedir(firewall_config_dir, group='vyattacfg') + chmod_775(firewall_config_dir) + + for set_name, remote_config in remote_groups.items(): + if 'url' not in remote_config: + continue + nft_ip_set_name = f'R_{set_name}' + nft_ip6_set_name = f'R6_{set_name}' + + # Create list file if necessary + list_file = os.path.join(firewall_config_dir, f"{nft_ip_set_name}.txt") + if not os.path.exists(list_file): + write_file(list_file, '', user="root", group="vyattacfg", mode=0o644) + + # Attempt to download file, use cached version if download fails + try: + download(list_file, remote_config['url'], raise_error=True) + except: + logger.error(f'Failed to download list-file for {set_name} remote group') + logger.info(f'Using cached list-file for {set_name} remote group') + + # Read list file + ip_list = [] + ip6_list = [] + invalid_list = [] + for line in read_file(list_file).splitlines(): + line_first_word = line.strip().partition(' ')[0] + + if is_valid_ipv4_address_or_range(line_first_word): + ip_list.append(line_first_word) + elif is_valid_ipv6_address_or_range(line_first_word): + ip6_list.append(line_first_word) + else: + if line_first_word[0].isalnum(): + invalid_list.append(line_first_word) + + # Load ip tables + for table in ipv4_tables: + if (table, nft_ip_set_name) in valid_sets: + conf_lines += nft_output(table, nft_ip_set_name, ip_list) + + # Load ip6 tables + for table in ipv6_tables: + if (table, nft_ip6_set_name) in valid_sets: + conf_lines += nft_output(table, nft_ip6_set_name, ip6_list) + + invalid_str = ", ".join(invalid_list) + if invalid_str: + logger.info(f'Invalid address for set {set_name}: {invalid_str}') + + count += 1 + + nft_conf_str = "\n".join(conf_lines) + "\n" + code = run(f'nft --file -', input=nft_conf_str) + + logger.info(f'Updated {count} remote-groups in firewall - result: {code}') + + +def update_fqdn(config, node): + conf_lines = [] + count = 0 + valid_sets = nft_valid_sets() + + if node == 'firewall': + domain_groups = dict_search_args(config, 'group', 'domain_group') + if domain_groups: + for set_name, domain_config in domain_groups.items(): + if 'address' not in domain_config: + continue + nft_set_name = f'D_{set_name}' + domains = domain_config['address'] + + ip_list = resolve(domains, ipv6=False) + for table in ipv4_tables: + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + ip6_list = resolve(domains, ipv6=True) + for table in ipv6_tables: + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip6_list) + count += 1 + + for set_name, domain in config['ip_fqdn'].items(): + table = 'ip vyos_filter' + nft_set_name = f'FQDN_{set_name}' + ip_list = resolve([domain], ipv6=False) + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + count += 1 + + for set_name, domain in config['ip6_fqdn'].items(): + table = 'ip6 vyos_filter' + nft_set_name = f'FQDN_{set_name}' + ip_list = resolve([domain], ipv6=True) + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + count += 1 + + else: + # It's NAT + for set_name, domain in config['ip_fqdn'].items(): + table = 'ip vyos_nat' + nft_set_name = f'FQDN_nat_{set_name}' + ip_list = resolve([domain], ipv6=False) + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + count += 1 + + nft_conf_str = "\n".join(conf_lines) + "\n" + code = run(f'nft --file -', input=nft_conf_str) + + 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') + if wg_interfaces: + + 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('VyOS domain resolver') + + count = 1 + while commit_in_progress(): + if ( count % 60 == 0 ): + logger.info(f'Commit still in progress after {count}s - waiting') + count += 1 + time.sleep(1) + + 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_remote_group(firewall) + update_interfaces(interfaces, 'interfaces') + time.sleep(timeout) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 1ba90471e..44f03586c 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -233,10 +233,7 @@ # } import os -import sys -import time import json -import signal import traceback import re import logging @@ -245,7 +242,6 @@ import zmq from voluptuous import Schema, MultipleInvalid, Required, Any from collections import OrderedDict from vyos.utils.file import makedir -from vyos.utils.permission import chown from vyos.utils.permission import chmod_755 from vyos.utils.process import popen from vyos.utils.process import process_named_running diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 558561182..be3dd5051 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -20,18 +20,22 @@ import grp import json import logging import signal +import traceback from time import sleep +from typing import Annotated -from fastapi import FastAPI +from fastapi import FastAPI, Query from fastapi.exceptions import RequestValidationError from uvicorn import Config as UvicornConfig from uvicorn import Server as UvicornServer from vyos.configsession import ConfigSession from vyos.defaults import api_config_state +from vyos.utils.file import read_file +from vyos.version import get_version from api.session import SessionState -from api.rest.models import error +from api.rest.models import error, InfoQueryParams, success CFG_GROUP = 'vyattacfg' @@ -57,11 +61,49 @@ app = FastAPI(debug=True, title="VyOS API", version="0.1.0") + @app.exception_handler(RequestValidationError) async def validation_exception_handler(_request, exc): return error(400, str(exc.errors()[0])) +@app.get('/info') +def info(q: Annotated[InfoQueryParams, Query()]): + show_version = q.version + show_hostname = q.hostname + + prelogin_file = r'/etc/issue' + hostname_file = r'/etc/hostname' + default = 'Welcome to VyOS' + + try: + res = { + 'banner': '', + 'hostname': '', + 'version': '' + } + if show_version: + res.update(version=get_version()) + + if show_hostname: + try: + hostname = read_file(hostname_file) + except Exception: + hostname = 'vyos' + res.update(hostname=hostname) + + banner = read_file(prelogin_file, defaultonfailure=default) + if banner == f'{default} - \\n \\l': + banner = banner.partition(default)[1] + + res.update(banner=banner) + except Exception: + LOG.critical(traceback.format_exc()) + return error(500, 'An internal error occured. Check the logs for details.') + + return success(res) + + ### # Modify uvicorn to allow reloading server within the configsession ### 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/system/sync-dhcp-lease-to-hosts.py b/src/system/sync-dhcp-lease-to-hosts.py new file mode 100755 index 000000000..5c8b18faf --- /dev/null +++ b/src/system/sync-dhcp-lease-to-hosts.py @@ -0,0 +1,112 @@ +#!/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 vyos.opmode +import vyos.hostsd_client + +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_server_leases + +# Configure logging +logger = logging.getLogger(__name__) +# set stream as output +logs_handler = logging.StreamHandler() +logger.addHandler(logs_handler) + + +def _get_all_server_leases(inet_suffix='4') -> list: + mappings = [] + try: + active_config = kea_get_active_config(inet_suffix) + except Exception: + raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration') + + try: + pools = kea_get_dhcp_pools(active_config, inet_suffix) + mappings = kea_get_server_leases( + active_config, inet_suffix, pools, state=[], origin=None + ) + except Exception: + raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server leases') + + return mappings + + +if __name__ == '__main__': + # Parse command arguments + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--inet', action='store_true', help='Use IPv4 DHCP leases') + group.add_argument('--inet6', action='store_true', help='Use IPv6 DHCP leases') + args = parser.parse_args() + + inet_suffix = '4' if args.inet else '6' + service_suffix = '' if args.inet else 'v6' + + if inet_suffix == '6': + raise vyos.opmode.UnsupportedOperation( + 'Syncing IPv6 DHCP leases are not supported yet' + ) + + # Load configuration + config = ConfigTreeQuery() + + # Check if DHCP server is configured + # Using warning instead of error since this check may fail during first-time + # DHCP server setup when the service is not yet configured in the config tree. + # This happens when called from systemd's ExecStartPost the first time. + if not config.exists(f'service dhcp{service_suffix}-server'): + logger.warning(f'DHCP{service_suffix} server is not configured') + + # Check if hostfile-update is enabled + if not config.exists(f'service dhcp{service_suffix}-server hostfile-update'): + logger.debug( + f'Hostfile update is disabled for DHCP{service_suffix} server, skipping hosts update' + ) + exit(0) + + lease_data = _get_all_server_leases(inet_suffix) + + try: + hc = vyos.hostsd_client.Client() + + for mapping in lease_data: + ip_addr = mapping.get('ip') + mac_addr = mapping.get('mac') + name = mapping.get('hostname') + name = name if name else f'host-{mac_addr.replace(":", "-")}' + domain = mapping.get('domain') + fqdn = f'{name}.{domain}' if domain else name + hc.add_hosts( + { + f'dhcp-server-{ip_addr}': { + fqdn: {'address': [ip_addr], 'aliases': []} + } + } + ) + + hc.apply() + + logger.debug('Hosts store updated successfully') + + except vyos.hostsd_client.VyOSHostsdError as e: + raise vyos.opmode.InternalError(str(e)) diff --git a/src/systemd/netplug.service b/src/systemd/netplug.service new file mode 100644 index 000000000..928c553e8 --- /dev/null +++ b/src/systemd/netplug.service @@ -0,0 +1,9 @@ +[Unit] +Description=Network cable hotplug management daemon +Documentation=man:netplugd(8) +After=vyos-router.service + +[Service] +Type=forking +PIDFile=/run/netplugd.pid +ExecStart=/sbin/netplugd -c /etc/netplug/netplugd.conf -p /run/netplugd.pid diff --git a/src/systemd/vyos-commitd.service b/src/systemd/vyos-commitd.service new file mode 100644 index 000000000..5b083f500 --- /dev/null +++ b/src/systemd/vyos-commitd.service @@ -0,0 +1,27 @@ +[Unit] +Description=VyOS commit daemon + +# Without this option, lots of default dependencies are added, +# among them network.target, which creates a dependency cycle +DefaultDependencies=no + +# Seemingly sensible way to say "as early as the system is ready" +# All vyos-configd needs is read/write mounted root +After=systemd-remount-fs.service +Before=vyos-router.service + +[Service] +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-commitd +Type=idle + +SyslogIdentifier=vyos-commitd +SyslogFacility=daemon + +Restart=on-failure + +# Does't work in Jessie but leave it here +User=root +Group=vyattacfg + +[Install] +WantedBy=vyos.target diff --git a/src/systemd/vyos-domain-resolver.service b/src/systemd/vyos-domain-resolver.service index e63ae5e34..87a4748f4 100644 --- a/src/systemd/vyos-domain-resolver.service +++ b/src/systemd/vyos-domain-resolver.service @@ -6,7 +6,9 @@ ConditionPathExistsGlob=/run/use-vyos-domain-resolver* [Service] Type=simple Restart=always -ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/vyos-domain-resolver.py +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-domain-resolver +SyslogIdentifier=vyos-domain-resolver +SyslogFacility=daemon StandardError=journal StandardOutput=journal 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/systemd/vyos-wan-load-balance.service b/src/systemd/vyos-wan-load-balance.service index 7d62a2ff6..a59f2c3ae 100644 --- a/src/systemd/vyos-wan-load-balance.service +++ b/src/systemd/vyos-wan-load-balance.service @@ -1,15 +1,11 @@ [Unit] -Description=VyOS WAN load-balancing service +Description=VyOS WAN Load Balancer After=vyos-router.service [Service] -ExecStart=/opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid -ExecReload=/bin/kill -s SIGTERM $MAINPID && sleep 5 && /opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid -ExecStop=/bin/kill -s SIGTERM $MAINPID -PIDFile=/var/run/vyatta/wlb.pid -KillMode=process -Restart=on-failure -RestartSec=5s +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-load-balancer.py [Install] WantedBy=multi-user.target diff --git a/src/systemd/vyos.target b/src/systemd/vyos.target index 47c91c1cc..ea1593fe9 100644 --- a/src/systemd/vyos.target +++ b/src/systemd/vyos.target @@ -1,3 +1,3 @@ [Unit] Description=VyOS target -After=multi-user.target +After=multi-user.target vyos-grub-update.service systemd-sysctl.service diff --git a/src/tests/test_config_diff.py b/src/tests/test_config_diff.py index 39e17613a..4017fff4d 100644 --- a/src/tests/test_config_diff.py +++ b/src/tests/test_config_diff.py @@ -31,11 +31,11 @@ class TestConfigDiff(TestCase): def test_unit(self): diff = vyos.configtree.DiffTree(self.config_left, self.config_null) sub = diff.sub - self.assertEqual(sub.to_string(), self.config_left.to_string()) + self.assertEqual(sub, self.config_left) diff = vyos.configtree.DiffTree(self.config_null, self.config_left) add = diff.add - self.assertEqual(add.to_string(), self.config_left.to_string()) + self.assertEqual(add, self.config_left) def test_symmetry(self): lr_diff = vyos.configtree.DiffTree(self.config_left, @@ -45,10 +45,10 @@ class TestConfigDiff(TestCase): sub = lr_diff.sub add = rl_diff.add - self.assertEqual(sub.to_string(), add.to_string()) + self.assertEqual(sub, add) add = lr_diff.add sub = rl_diff.sub - self.assertEqual(add.to_string(), sub.to_string()) + self.assertEqual(add, sub) def test_identity(self): lr_diff = vyos.configtree.DiffTree(self.config_left, @@ -61,6 +61,9 @@ class TestConfigDiff(TestCase): r_union = vyos.configtree.union(add, inter) l_union = vyos.configtree.union(sub, inter) + # here we must compare string representations instead of using + # dunder equal, as we assert equivalence of the values list, which + # is optionally ordered at render self.assertEqual(r_union.to_string(), self.config_right.to_string(ordered_values=True)) self.assertEqual(l_union.to_string(), diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py index 9a4f02859..1b4a57311 100644 --- a/src/tests/test_config_parser.py +++ b/src/tests/test_config_parser.py @@ -51,3 +51,7 @@ class TestConfigParser(TestCase): def test_rename_duplicate(self): with self.assertRaises(vyos.configtree.ConfigTreeError): self.config.rename(["top-level-tag-node", "foo"], "bar") + + def test_leading_slashes(self): + self.assertTrue(self.config.exists(["normal-node", "value-with-leading-slashes"])) + self.assertEqual(self.config.return_value(["normal-node", "value-with-leading-slashes"]), "//other-value") 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") diff --git a/src/tests/test_template.py b/src/tests/test_template.py index 6377f6da5..7cae867a0 100644 --- a/src/tests/test_template.py +++ b/src/tests/test_template.py @@ -190,3 +190,12 @@ class TestVyOSTemplate(TestCase): for group_name, group_config in data['ike_group'].items(): ciphers = vyos.template.get_esp_ike_cipher(group_config) self.assertIn(IKEv2_DEFAULT, ','.join(ciphers)) + + def test_get_default_port(self): + from vyos.defaults import internal_ports + + with self.assertRaises(RuntimeError): + vyos.template.get_default_port('UNKNOWN') + + self.assertEqual(vyos.template.get_default_port('certbot_haproxy'), + internal_ports['certbot_haproxy']) diff --git a/src/tests/test_utils_network.py b/src/tests/test_utils_network.py index d68dec16f..92fde447d 100644 --- a/src/tests/test_utils_network.py +++ b/src/tests/test_utils_network.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 @@ -43,3 +43,12 @@ class TestVyOSUtilsNetwork(TestCase): self.assertFalse(vyos.utils.network.is_loopback_addr('::2')) self.assertFalse(vyos.utils.network.is_loopback_addr('192.0.2.1')) + + def test_check_port_availability(self): + self.assertTrue(vyos.utils.network.check_port_availability('::1', 8080)) + self.assertTrue(vyos.utils.network.check_port_availability('127.0.0.1', 8080)) + self.assertTrue(vyos.utils.network.check_port_availability(None, 8080, protocol='udp')) + # We do not have 192.0.2.1 configured on this system + self.assertFalse(vyos.utils.network.check_port_availability('192.0.2.1', 443)) + # We do not have 2001:db8::1 configured on this system + self.assertFalse(vyos.utils.network.check_port_availability('2001:db8::1', 80, protocol='udp')) diff --git a/src/validators/base64 b/src/validators/base64 index e2b1e730d..a54168ef7 100755 --- a/src/validators/base64 +++ b/src/validators/base64 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 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 @@ -15,13 +15,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import base64 -from sys import argv +import argparse -if __name__ == '__main__': - if len(argv) != 2: - exit(1) - try: - base64.b64decode(argv[1]) - except: +parser = argparse.ArgumentParser(description="Validate base64 input.") +parser.add_argument("base64", help="Base64 encoded string to validate") +parser.add_argument("--decoded-len", type=int, help="Optional list of valid lengths for the decoded input") +args = parser.parse_args() + +try: + decoded = base64.b64decode(args.base64) + if args.decoded_len and len(decoded) != args.decoded_len: exit(1) - exit(0) +except: + exit(1) +exit(0) diff --git a/src/validators/cpu b/src/validators/cpu new file mode 100755 index 000000000..959a49248 --- /dev/null +++ b/src/validators/cpu @@ -0,0 +1,43 @@ +#!/usr/bin/python3 + +import re +import sys + +MAX_CPU = 511 + + +def validate_isolcpus(value): + pattern = re.compile(r'^(\d{1,3}(-\d{1,3})?)(,(\d{1,3}(-\d{1,3})?))*$') + if not pattern.fullmatch(value): + return False + + flat_list = [] + for part in value.split(','): + if '-' in part: + start, end = map(int, part.split('-')) + if start > end or start < 0 or end > MAX_CPU: + return False + flat_list.extend(range(start, end + 1)) + else: + num = int(part) + if num < 0 or num > MAX_CPU: + return False + flat_list.append(num) + + for i in range(1, len(flat_list)): + if flat_list[i] <= flat_list[i - 1]: + return False + + return True + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python3 cpu.py <cpu_list>") + sys.exit(1) + + input_value = sys.argv[1] + if validate_isolcpus(input_value): + sys.exit(0) + else: + sys.exit(1) diff --git a/src/validators/ethernet-interface b/src/validators/ethernet-interface new file mode 100644 index 000000000..2bf92812e --- /dev/null +++ b/src/validators/ethernet-interface @@ -0,0 +1,13 @@ +#!/bin/sh + +if ! [[ "$1" =~ ^(lan|eth|eno|ens|enp|enx)[0-9]+$ ]]; then + echo "Error: $1 is not an ethernet interface" + exit 1 +fi + +if ! [ -d "/sys/class/net/$1" ]; then + echo "Error: $1 interface does not exist in the system" + exit 1 +fi + +exit 0 diff --git a/tests/data/config.valid b/tests/data/config.valid index 1fbdd1505..024e5e05c 100644 --- a/tests/data/config.valid +++ b/tests/data/config.valid @@ -26,6 +26,7 @@ normal-node { } } option-with-quoted-value "some-value" + value-with-leading-slashes "//other-value" } trailing-leaf-node-option some-value @@ -35,5 +36,5 @@ empty-node { trailing-leaf-node-without-value -// Trailing comment -// Another trailing comment +// some version string info +// continued |