From c5d987717237d001ae0cddd5c11109ab1f72882c Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:08:37 +0200 Subject: [PATCH 01/17] T7557: python3-nose -> python3-nose2 --- Makefile | 2 +- debian/control | 2 +- nose2.cfg | 9 +++++++++ python/vyos/airbag.py | 4 +++- test-requirements.txt | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 nose2.cfg diff --git a/Makefile b/Makefile index 244f465186d..15b6b8aebf8 100644 --- a/Makefile +++ b/Makefile @@ -107,7 +107,7 @@ clean: .PHONY: test test: generate-configd-include-json set -e; python3 -m compileall -q -x '/vmware-tools/scripts/' . - PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose + PYTHONPATH=python/ python3 -m nose2 -v .PHONY: check_migration_scripts_executable .ONESHELL: diff --git a/debian/control b/debian/control index f5853ba2c61..b8279116c73 100644 --- a/debian/control +++ b/debian/control @@ -31,7 +31,7 @@ Build-Depends: python3-hurry.filesize, python3-netaddr, python3-netifaces, - python3-nose, + python3-nose2, python3-jinja2, python3-paramiko, python3-passlib, diff --git a/nose2.cfg b/nose2.cfg new file mode 100644 index 00000000000..e2fbd610a1d --- /dev/null +++ b/nose2.cfg @@ -0,0 +1,9 @@ +[unittest] +start-dir = src +code-directories = conf_mode + op_mode + completion + validators + tests +test-file-pattern = test_*.py +test-method-prefix = test diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index 1dcccdd4769..1d00b6367e0 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -24,6 +24,8 @@ def enable(log=True): + if 'nose2' in sys.modules: + return if log: _intercepting_logger() _intercepting_exceptions() @@ -157,7 +159,7 @@ def _intercepting_exceptions(_singleton=[False]): {instructions} When reporting problems, please include as much information as possible: -- do not obfuscate any data (feel free to contact us privately if your +- do not obfuscate any data (feel free to contact us privately if your business policy requires it) - and include all the information presented below diff --git a/test-requirements.txt b/test-requirements.txt index a475e0a1672..10f52300eef 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,6 @@ python/ lxml pylint -nose +nose2 coverage jinja2 From 3f4be8ec60e39bc6f77e167970503402ad89494a Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:48:12 +0200 Subject: [PATCH 02/17] T7557: Check serial sessions without utmp --- python/vyos/utils/serial.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py index 36b483e9196..64e63e3ab92 100644 --- a/python/vyos/utils/serial.py +++ b/python/vyos/utils/serial.py @@ -24,7 +24,6 @@ RE_GETTY_DEVICES = re.compile(r'.+@(.+).service$') SD_UNIT_PATH = '/run/systemd/system' -UTMP_PATH = '/run/utmp' def get_serial_units(include_devices=[]): # Since we cannot depend on the current config for decommissioned ports, @@ -60,10 +59,10 @@ def get_authenticated_ports(units): # # We can safely skip blank or LOGIN sessions with valid device names. # - for line in cmd(f'utmpdump {UTMP_PATH}').splitlines(): - row = line.split('] [') - user_name = row[3].strip() - user_term = row[4].strip() + for line in cmd(f'who').splitlines(): + row = line.split() + user_name = row[0].strip() + user_term = row[1].strip() if user_name and user_name != 'LOGIN' and user_term in ports: connected.append(user_term) From 110be2f716a2cdab11cab37457c4a983dc2728ee Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:22:56 +0200 Subject: [PATCH 03/17] T7557: Use update-locale to change time format --- src/conf_mode/system_option.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index fbe7231df06..2c5b12903f1 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -251,7 +251,7 @@ def apply(options): # Time format 12|24-hour if 'time_format' in options: time_format = time_format_to_locale.get(options['time_format']) - cmd(f'localectl set-locale LC_TIME={time_format}') + cmd(f'update-locale LC_TIME={time_format}') # Reload UDEV, required for USB auto suspend cmd('udevadm control --reload-rules') From c3ff111564b7db3bfe12e36162fd1beba12cefed Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:09:26 +0200 Subject: [PATCH 04/17] T7557: Add e2fsprogs package for installer --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index b8279116c73..b59956e57f3 100644 --- a/debian/control +++ b/debian/control @@ -132,6 +132,7 @@ Depends: mokutil, shim-signed [amd64], sbsigntool [amd64], + e2fsprogs, # Image signature verification tool minisign, # Live filesystem tools From 59665ece6c30056fad9a3a46595565f8cbe33ca1 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:20:07 +0200 Subject: [PATCH 05/17] T7557: Update stunnel4 systemd unit --- src/conf_mode/service_stunnel.py | 4 ++-- src/systemd/{stunnel.service => stunnel4.service} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/systemd/{stunnel.service => stunnel4.service} (100%) diff --git a/src/conf_mode/service_stunnel.py b/src/conf_mode/service_stunnel.py index 5ea5b88b4a6..2781c5ceaf2 100644 --- a/src/conf_mode/service_stunnel.py +++ b/src/conf_mode/service_stunnel.py @@ -248,9 +248,9 @@ def generate(stunnel): def apply(stunnel): if not stunnel or ('client' not in stunnel and 'server' not in stunnel): - call('systemctl stop stunnel.service') + call('systemctl stop stunnel4.service') else: - call('systemctl restart stunnel.service') + call('systemctl restart stunnel4.service') if __name__ == '__main__': diff --git a/src/systemd/stunnel.service b/src/systemd/stunnel4.service similarity index 100% rename from src/systemd/stunnel.service rename to src/systemd/stunnel4.service From 657e552af3511305cd25650afc7cb1bb4ce3a75e Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:29:19 +0200 Subject: [PATCH 06/17] T7557: Add systemd units for sysv init scripts --- src/systemd/igmpproxy.service | 18 ++++++++++++++++++ src/systemd/qat_service.service | 18 ++++++++++++++++++ src/systemd/wide-dhcpv6-client.service | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/systemd/igmpproxy.service create mode 100644 src/systemd/qat_service.service create mode 100644 src/systemd/wide-dhcpv6-client.service diff --git a/src/systemd/igmpproxy.service b/src/systemd/igmpproxy.service new file mode 100644 index 00000000000..2c4de1ed09c --- /dev/null +++ b/src/systemd/igmpproxy.service @@ -0,0 +1,18 @@ +[Unit] +Description=IGMP Proxy +After=vyos-router.service + +[Service] +Type=forking +Restart=no +TimeoutSec=5min +IgnoreSIGPIPE=no +KillMode=process +GuessMainPID=no +RemainAfterExit=yes +SuccessExitStatus=5 6 +ExecStart=/etc/init.d/igmpproxy start +ExecStop=/etc/init.d/igmpproxy stop + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/qat_service.service b/src/systemd/qat_service.service new file mode 100644 index 00000000000..e62a476a057 --- /dev/null +++ b/src/systemd/qat_service.service @@ -0,0 +1,18 @@ +[Unit] +Description=Intel QAT service +After=vyos-router.service + +[Service] +Type=forking +Restart=no +TimeoutSec=5min +IgnoreSIGPIPE=no +KillMode=process +GuessMainPID=no +RemainAfterExit=yes +SuccessExitStatus=5 6 +ExecStart=/etc/init.d/qat_service start +ExecStop=/etc/init.d/qat_service stop + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/wide-dhcpv6-client.service b/src/systemd/wide-dhcpv6-client.service new file mode 100644 index 00000000000..69b005c06fd --- /dev/null +++ b/src/systemd/wide-dhcpv6-client.service @@ -0,0 +1,18 @@ +[Unit] +Description=WIDE DHCPv6 Client +After=vyos-router.service + +[Service] +Type=forking +Restart=no +TimeoutSec=5min +IgnoreSIGPIPE=no +KillMode=process +GuessMainPID=no +RemainAfterExit=yes +SuccessExitStatus=5 6 +ExecStart=/etc/init.d/wide-dhcpv6-client start +ExecStop=/etc/init.d/wide-dhcpv6-client stop + +[Install] +WantedBy=multi-user.target From b1542a23ec2e25c59614ffa8b3ed11ab20b618fa Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:25:04 +0200 Subject: [PATCH 07/17] T7557: Set dmesg tty loglevel to warning --- src/init/vyos-router | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/init/vyos-router b/src/init/vyos-router index 7a02ce5641a..3be946d431c 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -453,6 +453,9 @@ start () { log_success_msg "Starting VyOS router" + # Set dmesg tty loglevel to warning + dmesg -n 4 + # reset and clean config files security_reset || log_failure_msg "security reset failed" From f84a04638f8252625481d5c104de41ca220d4b61 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:20:39 +0200 Subject: [PATCH 08/17] T7557: DSA keys not supported in Trixie --- data/templates/ssh/sshd_config.j2 | 1 - .../include/version/ssh-version.xml.i | 2 +- .../include/version/system-version.xml.i | 2 +- interface-definitions/service_ssh.xml.in | 8 ++-- interface-definitions/system_login.xml.in | 8 +--- python/vyos/utils/auth.py | 4 +- smoketest/config-tests/basic-vyos | 2 + smoketest/configs/basic-vyos | 4 ++ smoketest/scripts/cli/test_service_ssh.py | 3 -- src/conf_mode/service_ssh.py | 4 -- src/migration-scripts/ssh/2-to-3 | 46 +++++++++++++++++++ src/migration-scripts/system/29-to-30 | 34 ++++++++++++++ 12 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 src/migration-scripts/ssh/2-to-3 create mode 100644 src/migration-scripts/system/29-to-30 diff --git a/data/templates/ssh/sshd_config.j2 b/data/templates/ssh/sshd_config.j2 index 1315bf2cbea..6d29468d245 100644 --- a/data/templates/ssh/sshd_config.j2 +++ b/data/templates/ssh/sshd_config.j2 @@ -7,7 +7,6 @@ # Protocol 2 HostKey /etc/ssh/ssh_host_rsa_key -HostKey /etc/ssh/ssh_host_dsa_key HostKey /etc/ssh/ssh_host_ecdsa_key HostKey /etc/ssh/ssh_host_ed25519_key SyslogFacility AUTH diff --git a/interface-definitions/include/version/ssh-version.xml.i b/interface-definitions/include/version/ssh-version.xml.i index 0f25caf98ff..05cf431a799 100644 --- a/interface-definitions/include/version/ssh-version.xml.i +++ b/interface-definitions/include/version/ssh-version.xml.i @@ -1,3 +1,3 @@ - + diff --git a/interface-definitions/include/version/system-version.xml.i b/interface-definitions/include/version/system-version.xml.i index 5cdece74a95..a9987e8e44e 100644 --- a/interface-definitions/include/version/system-version.xml.i +++ b/interface-definitions/include/version/system-version.xml.i @@ -1,3 +1,3 @@ - + diff --git a/interface-definitions/service_ssh.xml.in b/interface-definitions/service_ssh.xml.in index c659a7db7e7..f85c5a5e321 100644 --- a/interface-definitions/service_ssh.xml.in +++ b/interface-definitions/service_ssh.xml.in @@ -138,11 +138,11 @@ Allowed host key signature algorithms - ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512 ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 sk-ecdsa-sha2-nistp256@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa-cert-v01@openssh.com rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512-cert-v01@openssh.com ssh-dss-cert-v01@openssh.com ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com + ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512 ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 sk-ecdsa-sha2-nistp256@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa-cert-v01@openssh.com rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512-cert-v01@openssh.com ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com - (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ssh-rsa|rsa-sha2-256|rsa-sha2-512|ssh-dss|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ecdsa-sha2-nistp256@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512-cert-v01@openssh.com|ssh-dss-cert-v01@openssh.com|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com) + (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ssh-rsa|rsa-sha2-256|rsa-sha2-512|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ecdsa-sha2-nistp256@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512-cert-v01@openssh.com|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com) @@ -151,11 +151,11 @@ Allowed pubkey signature algorithms - ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384 ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521 ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-dss ssh-dss-cert-v01@openssh.com ssh-rsa ssh-rsa-cert-v01@openssh.com rsa-sha2-256 rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512 rsa-sha2-512-cert-v01@openssh.com + ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384 ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521 ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa ssh-rsa-cert-v01@openssh.com rsa-sha2-256 rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512 rsa-sha2-512-cert-v01@openssh.com - (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ecdsa-sha2-nistp256|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-dss|ssh-dss-cert-v01@openssh.com|ssh-rsa|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512|rsa-sha2-512-cert-v01@openssh.com) + (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ecdsa-sha2-nistp256|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512|rsa-sha2-512-cert-v01@openssh.com) diff --git a/interface-definitions/system_login.xml.in b/interface-definitions/system_login.xml.in index a13ba10ea1f..268f5c67e46 100644 --- a/interface-definitions/system_login.xml.in +++ b/interface-definitions/system_login.xml.in @@ -138,12 +138,8 @@ SSH public key type - ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ssh-ed25519@openssh.com + ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ssh-ed25519@openssh.com - - ssh-dss - Digital Signature Algorithm (DSA) key support - ssh-rsa Key pair based on RSA algorithm @@ -173,7 +169,7 @@ Elliptic curve 25519 security key - (ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh.com|sk-ssh-ed25519@openssh.com) + (ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh.com|sk-ssh-ed25519@openssh.com) diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py index 6e816af71b1..d29dd432988 100644 --- a/python/vyos/utils/auth.py +++ b/python/vyos/utils/auth.py @@ -104,8 +104,8 @@ def split_ssh_public_key(key_string, defaultname=""): else: key_type, key_data, key_name = parts[0], parts[1], defaultname - if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']: - raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type)) + if key_type not in ['ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']: + raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type)) return({"type": key_type, "data": key_data, "name": key_name}) diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos index aaf450e80c1..b62dbf2d867 100644 --- a/smoketest/config-tests/basic-vyos +++ b/smoketest/config-tests/basic-vyos @@ -86,12 +86,14 @@ set service ssh ciphers 'aes192-ctr' set service ssh ciphers 'aes256-ctr' set service ssh ciphers 'chacha20-poly1305@openssh.com' set service ssh ciphers 'rijndael-cbc@lysator.liu.se' +set service ssh hostkey-algorithm 'ssh-rsa' set service ssh key-exchange 'curve25519-sha256@libssh.org' set service ssh key-exchange 'diffie-hellman-group1-sha1' set service ssh key-exchange 'diffie-hellman-group-exchange-sha1' set service ssh key-exchange 'diffie-hellman-group-exchange-sha256' set service ssh listen-address '192.168.0.1' set service ssh port '22' +set service ssh pubkey-accepted-algorithm 'ssh-rsa' set system config-management commit-revisions '100' set system conntrack ignore ipv4 rule 1 destination address '192.0.2.2' set system conntrack ignore ipv4 rule 1 source address '192.0.2.1' diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index 5f7a71237cb..3bdf5d5b749 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -228,6 +228,10 @@ service { key-exchange curve25519-sha256@libssh.org key-exchange diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256 port 22 + hostkey-algorithm ssh-rsa + hostkey-algorithm ssh-dss + pubkey-accepted-algorithm ssh-rsa + pubkey-accepted-algorithm ssh-dss } } system { diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index 32bc3cc8937..7b92458f712 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -38,7 +38,6 @@ pki_path = ['pki'] key_rsa = '/etc/ssh/ssh_host_rsa_key' -key_dsa = '/etc/ssh/ssh_host_dsa_key' key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' trusted_user_ca = config_files['sshd_user_ca'] test_command = 'uname -a' @@ -156,7 +155,6 @@ def tearDown(self): self.cli_commit() self.assertTrue(os.path.isfile(key_rsa)) - self.assertTrue(os.path.isfile(key_dsa)) self.assertTrue(os.path.isfile(key_ed25519)) # Established SSH connections remains running after service is stopped. @@ -408,7 +406,6 @@ def test_ssh_pubkey_accepted_algorithm(self): 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', - 'ssh-dss', 'ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512', diff --git a/src/conf_mode/service_ssh.py b/src/conf_mode/service_ssh.py index bf8afe8b77e..22adac72506 100755 --- a/src/conf_mode/service_ssh.py +++ b/src/conf_mode/service_ssh.py @@ -42,7 +42,6 @@ sshguard_whitelist = '/etc/sshguard/whitelist' key_rsa = '/etc/ssh/ssh_host_rsa_key' -key_dsa = '/etc/ssh/ssh_host_dsa_key' key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' trusted_user_ca = config_files['sshd_user_ca'] @@ -116,9 +115,6 @@ def generate(ssh): if not os.path.isfile(key_rsa): syslog(LOG_INFO, 'SSH RSA host key not found, generating new key!') call(f'ssh-keygen -q -N "" -t rsa -f {key_rsa}') - if not os.path.isfile(key_dsa): - syslog(LOG_INFO, 'SSH DSA host key not found, generating new key!') - call(f'ssh-keygen -q -N "" -t dsa -f {key_dsa}') if not os.path.isfile(key_ed25519): syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!') call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}') diff --git a/src/migration-scripts/ssh/2-to-3 b/src/migration-scripts/ssh/2-to-3 new file mode 100644 index 00000000000..0e3cf4da864 --- /dev/null +++ b/src/migration-scripts/ssh/2-to-3 @@ -0,0 +1,46 @@ +# Copyright 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 . + +# T7557: OpenSSH on Trixie does not support DSS keys +# https://www.debian.org/releases/trixie/release-notes/issues.en.html#openssh-no-longer-supports-dsa-keys + + +from vyos.configtree import ConfigTree + +base = ['service', 'ssh'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + dss_algo = [ + 'ssh-dss', + 'ssh-dss-cert-v01@openssh.com', + ] + + path_hostkey = base + ['hostkey-algorithm'] + if config.exists(path_hostkey): + values = config.return_values(path_hostkey) + for algo in dss_algo: + if algo in values: + config.delete_value(path_hostkey, algo) + + path_pubkey = base + ['pubkey-accepted-algorithm'] + if config.exists(path_pubkey): + values = config.return_values(path_pubkey) + for algo in dss_algo: + if algo in values: + config.delete_value(path_pubkey, algo) diff --git a/src/migration-scripts/system/29-to-30 b/src/migration-scripts/system/29-to-30 new file mode 100644 index 00000000000..a5c2d0f3cbc --- /dev/null +++ b/src/migration-scripts/system/29-to-30 @@ -0,0 +1,34 @@ +# Copyright 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 . + +# T7557: OpenSSH does not support DSA keys in Debian Trixie + +from vyos.configtree import ConfigTree + +base = ['system', 'login', 'user'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + return + + for user in config.list_nodes(base): + key_base = base + [user, 'authentication', 'public-keys'] + if config.exists(key_base): + for key in config.list_nodes(key_base): + key_type = key_base + [key, 'type'] + if config.exists(key_type): + tmp = config.return_value(key_type) + if tmp == 'ssh-dss': + config.delete(key_base + [key]) From 5dab88456ba65135bef8ea7247e5d3c4b8b70487 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Fri, 27 Jun 2025 00:22:39 +0200 Subject: [PATCH 09/17] T7557: Use systemd quadlet for containers --- data/templates/container/quadlet-network.j2 | 8 + data/templates/container/quadlet-unit.j2 | 11 + data/templates/container/systemd-unit.j2 | 17 -- smoketest/scripts/cli/test_container.py | 23 +- src/conf_mode/container.py | 261 +++++++++----------- 5 files changed, 150 insertions(+), 170 deletions(-) create mode 100644 data/templates/container/quadlet-network.j2 create mode 100644 data/templates/container/quadlet-unit.j2 delete mode 100644 data/templates/container/systemd-unit.j2 diff --git a/data/templates/container/quadlet-network.j2 b/data/templates/container/quadlet-network.j2 new file mode 100644 index 00000000000..f253be557da --- /dev/null +++ b/data/templates/container/quadlet-network.j2 @@ -0,0 +1,8 @@ +### Autogenerated by container.py ### +[Unit] +Description=VyOS Network {{ name }} + +[Network] +{% for opt in opts %} +{{ opt }} +{% endfor %} diff --git a/data/templates/container/quadlet-unit.j2 b/data/templates/container/quadlet-unit.j2 new file mode 100644 index 00000000000..7546e3f1f82 --- /dev/null +++ b/data/templates/container/quadlet-unit.j2 @@ -0,0 +1,11 @@ +### Autogenerated by container.py ### +[Unit] +Description=VyOS Container {{ name }} + +[Container] +{% for opt in opts %} +{{ opt }} +{% endfor %} + +[Service] +Restart={{ restart }} diff --git a/data/templates/container/systemd-unit.j2 b/data/templates/container/systemd-unit.j2 deleted file mode 100644 index d379f0a0765..00000000000 --- a/data/templates/container/systemd-unit.j2 +++ /dev/null @@ -1,17 +0,0 @@ -### Autogenerated by container.py ### -[Unit] -Description=VyOS Container {{ name }} - -[Service] -Environment=PODMAN_SYSTEMD_UNIT=%n -Restart=on-failure -ExecStartPre=/bin/rm -f %t/%n.pid %t/%n.cid -ExecStart=/usr/bin/podman run \ - --conmon-pidfile %t/%n.pid --cidfile %t/%n.cid --cgroups=no-conmon \ - {{ run_args }} -ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n.cid -t 5 -ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n.cid -ExecStopPost=/bin/rm -f %t/%n.cid -PIDFile=%t/%n.pid -KillMode=control-group -Type=forking diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index 7590ed1cdef..5e265a39022 100755 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -28,7 +28,6 @@ base_path = ['container'] PROCESS_NAME = 'conmon' -PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid' busybox_image = 'busybox:stable' busybox_image_path = '/usr/share/vyos/busybox-stable.tar' @@ -68,9 +67,13 @@ def tearDown(self): self.assertIsNone(process_named_running(PROCESS_NAME)) # Ensure systemd units are removed - units = glob.glob('/run/systemd/system/vyos-container-*') + units = glob.glob('/run/containers/systemd/vyos*') self.assertEqual(units, []) + def is_running(self, name): + command = f'systemctl show vyos-{name} --property=ActiveState --value' + return cmd(command).strip() == 'active' + def test_basic(self): cont_name = 'c1' @@ -99,12 +102,7 @@ def test_basic(self): # commit changes self.cli_commit() - pid = 0 - with open(PROCESS_PIDFILE.format(cont_name), 'r') as f: - pid = int(f.read()) - - # Check for running process - self.assertEqual(process_named_running(PROCESS_NAME), pid) + self.assertTrue(self.is_running(cont_name)) # verify tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax') @@ -143,6 +141,8 @@ def test_name_server(self): self.cli_set(base_path + ['network', net_name, 'no-name-server']) self.cli_commit() + self.assertTrue(self.is_running(cont_name)) + n = cmd_to_json(f'sudo podman inspect {cont_name}') self.assertEqual(n['HostConfig']['Dns'][0], name_server) @@ -158,12 +158,7 @@ def test_cpu_limit(self): self.cli_commit() - pid = 0 - with open(PROCESS_PIDFILE.format(cont_name), 'r') as f: - pid = int(f.read()) - - # Check for running process - self.assertEqual(process_named_running(PROCESS_NAME), pid) + self.assertTrue(self.is_running(cont_name)) def test_ipv4_network(self): prefix = '192.0.2.0/24' diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 4ec9b8849ba..c0473d96b5e 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -50,7 +50,7 @@ config_containers = '/etc/containers/containers.conf' config_registry = '/etc/containers/registries.conf' config_storage = '/etc/containers/storage.conf' -systemd_unit_path = '/run/systemd/system' +quadlet_unit_path = '/run/containers/systemd' def _cmd(command): @@ -303,60 +303,106 @@ def verify(container): return None +def generate_network_options(name, network_config): + out = [ + f'NetworkName={name}', + 'Driver=bridge', + 'Internal=false', + 'IPAMDriver=host-local', + f'PodmanArgs=--interface-name=pod-{name}', + ] + + # TODO: if 'no_name_server' in network_config: + # DNS appears broken in trixie + out.append('DisableDNS=true') + + mtu = network_config['mtu'] if 'mtu' in network_config else '1500' + out.append(f'Options=mtu={mtu}') + + ipv6 = False + + for prefix in network_config['prefix']: + gw = inc_ip(prefix, 1) + out.append(f'Subnet={prefix}') + out.append(f'Gateway={gw}') + + if is_ipv6(prefix): + ipv6 = True + + if ipv6: + out.append('IPv6=true') + + return out + +def generate_quadlet_options(name, container_config): + out = [ + f'ContainerName={name}', + f'Image={container_config['image']}', + f'LogDriver={container_config['log_driver']}', + f'Memory={container_config['memory']}m', + f'ShmSize={container_config['shared_memory']}m', + f'PodmanArgs=--cpus={container_config['cpu_quota']}', + 'PodmanArgs=--no-healthcheck', + 'PodmanArgs=--interactive', + 'PodmanArgs=--tty', + ] -def generate_run_arguments(name, container_config): - image = container_config['image'] - cpu_quota = container_config['cpu_quota'] - memory = container_config['memory'] - shared_memory = container_config['shared_memory'] - restart = container_config['restart'] - log_driver = container_config['log_driver'] + if 'allow_host_networks' in container_config: + out.append('Network=host') - # Add sysctl options - sysctl_opt = '' - if 'sysctl' in container_config and 'parameter' in container_config['sysctl']: - for k, v in container_config['sysctl']['parameter'].items(): - sysctl_opt += f" --sysctl \"{k}={v['value']}\"" + if 'allow_host_pid' in container_config: + out.append('PodmanArgs=--pid host') - # Add capability options. Should be in uppercase - capabilities = '' if 'capability' in container_config: for cap in container_config['capability']: cap = cap.upper().replace('-', '_') - capabilities += f' --cap-add={cap}' + out.append(f'AddCapability={cap}') - # Grant root capabilities to the container - privileged = '' - if 'privileged' in container_config: - privileged = '--privileged' + if 'command' in container_config: + command = container_config['command'].strip() + + if 'arguments' in container_config: + command += ' ' + container_config['arguments'].strip() + + out.append(f'Exec={command}') - # Add a host device to the container /dev/x:/dev/x - device = '' if 'device' in container_config: for dev, dev_config in container_config['device'].items(): source_dev = dev_config['source'] dest_dev = dev_config['destination'] - device += f' --device={source_dev}:{dest_dev}' + out.append(f'AddDevice={source_dev}:{dest_dev}') + + if 'entrypoint' in container_config: + entrypoint = container_config['entrypoint'] + out.append(f'Entrypoint={entrypoint}') - # Check/set environment options "-e foo=bar" - env_opt = '' if 'environment' in container_config: for k, v in container_config['environment'].items(): - env_opt += f" --env \"{k}={v['value']}\"" + out.append(f'Environment={k}={v}') + + if 'host_name' in container_config: + hostname = container_config['host_name'] + out.append(f'HostName={hostname}') - # Check/set label options "--label foo=bar" - label = '' if 'label' in container_config: for k, v in container_config['label'].items(): - label += f" --label \"{k}={v['value']}\"" + out.append(f'Label={k}={v['value']}') - hostname = '' - if 'host_name' in container_config: - hostname = container_config['host_name'] - hostname = f'--hostname {hostname}' + if 'name_server' in container_config: + for ns in container_config['name_server']: + out.append(f'DNS={ns}') + + if 'network' in container_config: + for network in container_config['network']: + out.append(f'Network=vyos-{network}.network') + + if 'address' in container_config['network'][network]: + for address in container_config['network'][network]['address']: + if is_ipv6(address): + out.append(f'IP6={address}') + else: + out.append(f'IP={address}') - # Publish ports - port = '' if 'port' in container_config: protocol = '' for portmap in container_config['port']: @@ -368,80 +414,39 @@ def generate_run_arguments(name, container_config): # If listen_addresses is not empty, include them in the publish command if listen_addresses: for listen_address in listen_addresses: - port += f' --publish {bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}' + out.append(f'PublishPort={bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}') else: # If listen_addresses is empty, just include the standard publish command - port += f' --publish {sport}:{dport}/{protocol}' + out.append(f'PublishPort={sport}:{dport}/{protocol}') + + if 'privileged' in container_config: + out.append('PodmanArgs=--privileged') + + if 'sysctl' in container_config and 'parameter' in container_config['sysctl']: + for k, v in container_config['sysctl']['parameter'].items(): + out.append(f'Sysctl={k}={v['value']}') + + if 'tmpfs' in container_config: + for tmpfs_config in container_config['tmpfs'].values(): + dest = tmpfs_config['destination'] + size = tmpfs_config['size'] + out.append(f'Mount=type=tmpfs,tmpfs-size={size}M,destination={dest}') - # Set uid and gid - uid = '' if 'uid' in container_config: uid = container_config['uid'] if 'gid' in container_config: uid += ':' + container_config['gid'] - uid = f'--user {uid}' + out.append(f'User={uid}') - # Bind volume - volume = '' if 'volume' in container_config: - for vol, vol_config in container_config['volume'].items(): + for _, vol_config in container_config['volume'].items(): svol = vol_config['source'] dvol = vol_config['destination'] mode = vol_config['mode'] 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' - - name_server = '' - if 'name_server' in container_config: - for ns in container_config['name_server']: - name_server += f'--dns {ns}' - - 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} --log-driver={log_driver} ' \ - f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}' - - entrypoint = '' - if 'entrypoint' in container_config: - # it needs to be json-formatted with single quote on the outside - entrypoint = json_write(container_config['entrypoint'].split()).replace('"', """) - entrypoint = f'--entrypoint '{entrypoint}'' - - command = '' - if 'command' in container_config: - command = container_config['command'].strip() - - command_arguments = '' - if 'arguments' in container_config: - command_arguments = container_config['arguments'].strip() - - if 'allow_host_networks' in container_config: - return f'{container_base_cmd} --net host {entrypoint} {image} {command} {command_arguments}'.strip() - - ip_param = '' - networks = ",".join(container_config['network']) - for network in container_config['network']: - if 'address' not in container_config['network'][network]: - continue - for address in container_config['network'][network]['address']: - if is_ipv6(address): - ip_param += f' --ip6 {address}' - else: - ip_param += f' --ip {address}' - - return f'{container_base_cmd} --no-healthcheck --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip() + out.append(f'Volume={svol}:{dvol}:{mode},{prop}') + return out def generate(container): # bail out early - looks like removal from running config @@ -451,53 +456,25 @@ def generate(container): os.unlink(file) return None - if 'network' in container: - for network, network_config in container['network'].items(): - tmp = { - 'name': network, - 'id': sha256(f'{network}'.encode()).hexdigest(), - 'driver': 'bridge', - 'network_interface': f'pod-{network}', - 'subnets': [], - 'ipv6_enabled': False, - 'internal': False, - 'dns_enabled': True, - 'ipam_options': { - 'driver': 'host-local' - }, - 'options': { - 'mtu': '1500' - } - } - - if 'no_name_server' in network_config: - tmp['dns_enabled'] = False - - if 'mtu' in network_config: - tmp['options']['mtu'] = network_config['mtu'] - - for prefix in network_config['prefix']: - net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)} - tmp['subnets'].append(net) - - if is_ipv6(prefix): - tmp['ipv6_enabled'] = True - - write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2)) - render(config_containers, 'container/containers.conf.j2', container) render(config_registry, 'container/registries.conf.j2', container) render(config_storage, 'container/storage.conf.j2', container) + if 'network' in container: + for network, network_config in container['network'].items(): + file_path = os.path.join(quadlet_unit_path, f'vyos-{network}.network') + opts = generate_network_options(network, network_config) + render(file_path, 'container/quadlet-network.j2', {'name': network, 'opts': opts}) + if 'name' in container: for name, container_config in container['name'].items(): if 'disable' in container_config: continue - file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') - run_args = generate_run_arguments(name, container_config) - render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args, }, - formater=lambda _: _.replace(""", '"').replace("'", "'")) + file_path = os.path.join(quadlet_unit_path, f'vyos-{name}.container') + quadlet_opts = generate_quadlet_options(name, container_config) + restart = container_config['restart'] + render(file_path, 'container/quadlet-unit.j2', {'name': name, 'opts': quadlet_opts, 'restart': restart}) return None @@ -507,8 +484,8 @@ def apply(container): # Option "--force" allows to delete containers with any status if 'container_remove' in container: for name in container['container_remove']: - file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') - call(f'systemctl stop vyos-container-{name}.service') + file_path = os.path.join(quadlet_unit_path, f'vyos-{name}.container') + call(f'systemctl stop vyos-{name}') if os.path.exists(file_path): os.unlink(file_path) @@ -517,7 +494,13 @@ def apply(container): # Delete old networks if needed if 'network_remove' in container: for network in container['network_remove']: - call(f'podman network rm {network} >/dev/null 2>&1') + file_path = os.path.join(quadlet_unit_path, f'vyos-{network}.network') + call(f'systemctl stop vyos-{network}-network') + call(f'podman network rm {network}') + if os.path.exists(file_path): + os.unlink(file_path) + + call('systemctl daemon-reload') # Add container disabled_new = False @@ -534,15 +517,15 @@ def apply(container): # check if there is a container by that name running tmp = _cmd('podman ps -a --format "{{.Names}}"') if name in tmp: - file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') - call(f'systemctl stop vyos-container-{name}.service') + file_path = os.path.join(quadlet_unit_path, f'vyos-{name}.container') + call(f'systemctl stop vyos-{name}') if os.path.exists(file_path): disabled_new = True os.unlink(file_path) continue if 'container_restart' in container and name in container['container_restart']: - cmd(f'systemctl restart vyos-container-{name}.service') + cmd(f'systemctl restart vyos-{name}') if disabled_new: call('systemctl daemon-reload') From c67ae6c6e3b978c19024b4a84cac1ec8e45cea8a Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:54:10 +0200 Subject: [PATCH 10/17] T7557: Fix dns-forwarding, pending yaml re-write --- data/templates/dns-forwarding/override.conf.j2 | 2 +- data/templates/dns-forwarding/recursor.conf.lua.j2 | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/templates/dns-forwarding/override.conf.j2 b/data/templates/dns-forwarding/override.conf.j2 index 9d81a297796..a559da4f448 100644 --- a/data/templates/dns-forwarding/override.conf.j2 +++ b/data/templates/dns-forwarding/override.conf.j2 @@ -5,4 +5,4 @@ After=vyos-router.service [Service] RuntimeDirectoryPreserve=yes ExecStart= -ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir={{ config_dir }} +ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir={{ config_dir }} --enable-old-settings diff --git a/data/templates/dns-forwarding/recursor.conf.lua.j2 b/data/templates/dns-forwarding/recursor.conf.lua.j2 index 622283ad85a..48c9edbeaac 100644 --- a/data/templates/dns-forwarding/recursor.conf.lua.j2 +++ b/data/templates/dns-forwarding/recursor.conf.lua.j2 @@ -2,7 +2,9 @@ -- Do not edit, your changes will get overwritten -- -- Load DNSSEC root keys from dns-root-data package. -dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua") +-- dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua") +-- lua-config no longer present, copying line from file +readTrustAnchorsFromFile("/usr/share/dns/root.key") -- Load lua from vyos-hostsd -- dofile("{{ config_dir }}/recursor.vyos-hostsd.conf.lua") From 0fc8fb2b03749df766ac5d10881011af73534629 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:28:15 +0200 Subject: [PATCH 11/17] T7557: Add python systemd module for interface smoketests --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index b59956e57f3..d4f717d6db2 100644 --- a/debian/control +++ b/debian/control @@ -405,6 +405,7 @@ Description: VyOS configuration scripts and data for AWS Gateway Load Balancer Package: vyos-1x-smoketest Architecture: all Depends: + python3-systemd, skopeo, snmp, vyos-1x From 056958fb6fd8829577f8d6ba65af5091a2cd025b Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:28:47 +0200 Subject: [PATCH 12/17] T7557: Use systemd check for salt-minion process --- smoketest/scripts/cli/test_service_salt-minion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smoketest/scripts/cli/test_service_salt-minion.py b/smoketest/scripts/cli/test_service_salt-minion.py index 1ad1333f069..786d5fcb009 100755 --- a/smoketest/scripts/cli/test_service_salt-minion.py +++ b/smoketest/scripts/cli/test_service_salt-minion.py @@ -19,7 +19,7 @@ from socket import gethostname from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.utils.process import process_named_running +from vyos.utils.process import is_systemd_service_active from vyos.utils.file import read_file from vyos.utils.process import cmd @@ -47,7 +47,7 @@ def tearDownClass(cls): def tearDown(self): # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertTrue(is_systemd_service_active(PROCESS_NAME)) # delete testing SALT config self.cli_delete(base_path) @@ -57,7 +57,7 @@ def tearDown(self): # from the CI) salt-minion process is not killed by systemd. Apparently # no issue on VMWare. if cmd('systemd-detect-virt') != 'kvm': - self.assertFalse(process_named_running(PROCESS_NAME)) + self.assertFalse(is_systemd_service_active(PROCESS_NAME)) def test_default(self): servers = ['192.0.2.1', '192.0.2.2'] From 4ee0bce6d261669a0c28b184cabb2a6d3a3d85d7 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 30 Jun 2025 00:53:41 +0200 Subject: [PATCH 13/17] T7557: Fix netplug hook for interface flapping --- python/vyos/utils/network.py | 8 ++++++++ src/etc/netplug/vyos-netplug-dhcp-client | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 0e2cc58cfbe..4c3c095df28 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -663,3 +663,11 @@ def is_valid_ipv6_address_or_range(addr: str) -> bool: return ip_network(addr).version == 6 except: return False + +def is_carrier_up(ifname: str) -> bool | None: + try: + carrier_path = f'/sys/class/net/{ifname}/carrier' + with open(carrier_path, 'r') as f: + return f.read().strip() == '1' + except: + return None diff --git a/src/etc/netplug/vyos-netplug-dhcp-client b/src/etc/netplug/vyos-netplug-dhcp-client index 31a0cc6a725..57b5813c126 100755 --- a/src/etc/netplug/vyos-netplug-dhcp-client +++ b/src/etc/netplug/vyos-netplug-dhcp-client @@ -22,6 +22,7 @@ from time import sleep from vyos.config import Config from vyos.ifconfig import Section from vyos.utils.boot import boot_configuration_complete +from vyos.utils.network import is_carrier_up from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_active from vyos.utils.commit import commit_in_progress @@ -49,7 +50,8 @@ interface_path = ['interfaces'] + Section.get_config_path(interface).split() systemdV4_service = f'dhclient@{interface}.service' systemdV6_service = f'dhcp6c@{interface}.service' -if in_out == 'out': +# T7557 - Check the interface is still down at time of script +if in_out == 'out' and not is_carrier_up(interface): # Interface moved state to down if is_systemd_service_active(systemdV4_service): cmd(f'systemctl stop {systemdV4_service}') From 8693365bec1e178a180906c3587ef338ad285817 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:06:04 +0200 Subject: [PATCH 14/17] T7557: Fix bridge bpdu/root guard test --- smoketest/scripts/cli/test_interfaces_bridge.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 8502e6b2d2e..9bf5e2757ad 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -515,19 +515,16 @@ def test_bridge_root_bpdu_guard(self): self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard']) with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_discard() # Test if bpdu_guard configured - self.cli_set(['interfaces', 'bridge', 'br0', 'stp']) - self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'bpdu-guard']) + self.cli_delete(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard']) self.cli_commit() tmp = read_file(f'/sys/class/net/eth0/brport/bpdu_guard') self.assertEqual(tmp, '1') # Test if root_guard configured - self.cli_delete(['interfaces', 'bridge', 'br0']) - self.cli_set(['interfaces', 'bridge', 'br0', 'stp']) + self.cli_delete(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'bpdu-guard']) self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard']) self.cli_commit() From ede2bf3f6cc6df069ce1cf76e939336d3246db94 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:56:50 +0200 Subject: [PATCH 15/17] T7557: Prevent zombie process crashing pppoe smoketest --- .../scripts/cli/test_interfaces_pppoe.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index dbc70ebd4ba..834f1e9d7cc 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -15,8 +15,8 @@ # along with this program. If not, see . import unittest +import psutil -from psutil import process_iter from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError @@ -48,10 +48,13 @@ def tearDown(self): # Validate PPPoE client process for interface in self._interfaces: running = False - for proc in process_iter(): - if interface in proc.cmdline(): - running = True - break + for proc in psutil.process_iter(): + try: + if interface in proc.cmdline(): + running = True + break + except psutil.ZombieProcess: + pass self.assertTrue(running) self.cli_delete(base_path) @@ -110,10 +113,13 @@ def test_pppoe_client_disabled_interface(self): # Validate PPPoE client process - must not run as interfaces are disabled for interface in self._interfaces: running = False - for proc in process_iter(): - if interface in proc.cmdline(): - running = True - break + for proc in psutil.process_iter(): + try: + if interface in proc.cmdline(): + running = True + break + except psutil.ZombieProcess: + pass self.assertFalse(running) # enable PPPoE interfaces From a8931b1be46e8ec77b76744bf42a3c08ef02af70 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:37:22 +0200 Subject: [PATCH 16/17] T7557: Fix pylint detected errors --- python/vyos/ifconfig/interface.py | 2 +- src/conf_mode/firewall.py | 1 + src/conf_mode/interfaces_wireless.py | 2 +- src/conf_mode/protocols_bgp.py | 3 +- src/conf_mode/system_task-scheduler.py | 6 +- src/conf_mode/vpn_ipsec.py | 3 + src/migration-scripts/ipoe-server/1-to-2 | 1 + src/op_mode/conntrack_sync.py | 2 + src/op_mode/dhcp.py | 1 + src/op_mode/firewall.py | 75 ++++++++++++------------ src/op_mode/ikev2_profile_generator.py | 1 + src/op_mode/nat.py | 6 ++ src/op_mode/otp.py | 1 + src/op_mode/show_openconnect_otp.py | 1 + src/op_mode/show_wwan.py | 2 +- 15 files changed, 62 insertions(+), 45 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 3b3536301d1..39ddc914b5e 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1629,7 +1629,7 @@ def set_mirror_redirect(self): if direction == 'ingress': handle = 'ffff: ingress' parent = 'ffff:' - elif direction == 'egress': + else: # egress handle = '1: root prio' parent = '1:' diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 90fdded998d..7c47bf8d59d 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -589,6 +589,7 @@ def parse_firewall_error(output): # Parse the comment parsed_entries = comment.split('-') family = 'bridge' if parsed_entries[0] == 'bri' else parsed_entries[0] + chain = '' if parsed_entries[1] == 'NAM': chain = 'name' elif parsed_entries[1] == 'FWD': diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index b3b909046d2..d40f06efea3 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -170,7 +170,7 @@ def verify(wifi): # 802.11ax (WiFi-6e - HE) can use up to 160MHz bandwidth channels six_ghz_op_modes_he = ['131', '132', '133', '134', '135'] # 802.11be (WiFi-7 - EHT) can use up to 320MHz bandwidth channels - six_ghz_op_modes_eht = six_ghz_op_modes_he.append('137') + six_ghz_op_modes_he.append('137') if 'security' in wifi and 'wpa' in wifi['security'] and 'mode' in wifi['security']['wpa']: if wifi['security']['wpa']['mode'] == 'wpa3': if 'he' in wifi['capabilities']: diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index bc7925d2864..10d3d78c472 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -419,11 +419,12 @@ def verify(config_dict): 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 + peer_group_as = None if 'peer_group' in peer_config: peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) 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') diff --git a/src/conf_mode/system_task-scheduler.py b/src/conf_mode/system_task-scheduler.py index c0253006a41..e03163c9e9c 100755 --- a/src/conf_mode/system_task-scheduler.py +++ b/src/conf_mode/system_task-scheduler.py @@ -86,7 +86,7 @@ def verify(tasks): if task["interval"]: if task["spec"]: raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"])) - + if not re.match(r"^\d+[mdh]?$", task["interval"]): raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"]))) else: @@ -121,6 +121,7 @@ def generate(tasks): crontab_lines = [] for task in tasks: command = make_command(task["executable"], task["args"]) + line = None if task["spec"]: line = format_task(command=command, rawspec=task["spec"]) else: @@ -131,7 +132,8 @@ def generate(tasks): line = format_task(command=command, minute="0", hour="*/{0}".format(value)) elif suffix == "d": line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value)) - crontab_lines.append(line) + if line: + crontab_lines.append(line) with open(crontab_file, 'w') as f: f.write(crontab_header) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index dcb9f9c6909..e60b58e9ec5 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -400,6 +400,9 @@ def verify(ipsec): if 'prefix' in pool_config and 'range' in pool_config: raise ConfigError(f'Only one of prefix or range can be specified for pool "{pool}"!') + range_is_ipv4 = False + range_is_ipv6 = False + if 'prefix' in pool_config: range_is_ipv4 = is_ipv4(pool_config['prefix']) range_is_ipv6 = is_ipv6(pool_config['prefix']) diff --git a/src/migration-scripts/ipoe-server/1-to-2 b/src/migration-scripts/ipoe-server/1-to-2 index cf1cb5ca623..ff24144796d 100644 --- a/src/migration-scripts/ipoe-server/1-to-2 +++ b/src/migration-scripts/ipoe-server/1-to-2 @@ -68,6 +68,7 @@ def migrate(config: ConfigTree) -> None: namedpools_base = pool_base + ['name'] for pool_name in config.list_nodes(namedpools_base): + mask = None pool_path = namedpools_base + [pool_name] if config.exists(pool_path + ['subnet']): subnet = config.return_value(pool_path + ['subnet']) diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index 0da5b3b0b3e..9aeddc06633 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.py @@ -169,6 +169,8 @@ def show_status(raw: bool): ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface']) ct_sync_intf = ', '.join(ct_sync_intf) failover_state = "no transition yet!" + failover_mechanism = None + vrrp_sync_grp = None expect_sync_protocols = [] if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']): diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index f5b01a323d8..6061884f155 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -100,6 +100,7 @@ def _get_raw_server_leases( def _get_formatted_server_leases(raw_data, family='inet'): data_entries = [] + headers = [] if family == 'inet': for lease in raw_data: ipaddr = lease.get('ip') diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 6e9f8a01b76..f226b3652a9 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -192,7 +192,7 @@ def output_firewall_vertical(rules, headers, adjust=True): print(tabulate.tabulate(transformed_rule, tablefmt="presto")) print() -def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=None): +def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=None, detail=False): print(f'\n---------------------------------\n{family} Firewall "{hook} {priority}"\n') details = get_nftables_details(family, hook, priority) @@ -226,10 +226,10 @@ def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=N rows.append(row) if rows: - if args.rule: + if single_rule_id: rows.pop() - if args.detail: + if detail: header = ['Rule', 'Description', 'Action', 'Protocol', 'Packets', 'Bytes', 'Conditions'] output_firewall_vertical(rows, header) else: @@ -238,7 +238,7 @@ def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=N rows[rows.index(i)].pop(1) print(tabulate.tabulate(rows, header) + '\n') -def output_firewall_state_policy(family): +def output_firewall_state_policy(family, detail=None): if family == 'bridge': return {} print(f'\n---------------------------------\n{family} State Policy\n') @@ -254,10 +254,7 @@ def output_firewall_state_policy(family): rows.append(row) if rows: - if args.rule: - rows.pop() - - if args.detail: + if detail: header = ['State', 'Conditions', 'Packets', 'Bytes'] output_firewall_vertical(rows, header) else: @@ -266,7 +263,7 @@ def output_firewall_state_policy(family): rows[rows.index(i)].pop(1) print(tabulate.tabulate(rows, header) + '\n') -def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule_id=None): +def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule_id=None, detail=None): print(f'\n---------------------------------\n{family} Firewall "{hook} {prior}"\n') details = get_nftables_details(family, hook, prior) @@ -384,7 +381,7 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule rows.append(row) if rows: - if args.detail: + if detail: header = ['Rule', 'Description', 'Packets', 'Bytes', 'Action', 'Source', 'Destination', 'Inbound-Interface', 'Outbound-interface'] output_firewall_vertical(rows, header) else: @@ -393,7 +390,7 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule rows[rows.index(i)].pop(1) print(tabulate.tabulate(rows, header) + '\n') -def show_firewall(): +def show_firewall(detail=None): print('Rulesets Information') conf = Config() @@ -405,14 +402,14 @@ def show_firewall(): for family in ['ipv4', 'ipv6', 'bridge']: if 'global_options' in firewall: if 'state_policy' in firewall['global_options']: - output_firewall_state_policy(family) + output_firewall_state_policy(family, detail=detail) if family in firewall: for hook, hook_conf in firewall[family].items(): for prior, prior_conf in firewall[family][hook].items(): - output_firewall_name(family, hook, prior, prior_conf) + output_firewall_name(family, hook, prior, prior_conf, detail=detail) -def show_firewall_family(family): +def show_firewall_family(family, detail=None): print(f'Rulesets {family} Information') conf = Config() @@ -423,30 +420,30 @@ def show_firewall_family(family): if 'global_options' in firewall: if 'state_policy' in firewall['global_options']: - output_firewall_state_policy(family) + output_firewall_state_policy(family, detail=detail) if family in firewall: for hook, hook_conf in firewall[family].items(): for prior, prior_conf in firewall[family][hook].items(): - output_firewall_name(family, hook, prior, prior_conf) + output_firewall_name(family, hook, prior, prior_conf, detail=detail) -def show_firewall_name(family, hook, priority): +def show_firewall_name(family, hook, priority, detail=None): print('Ruleset Information') conf = Config() firewall = get_config_node(conf, 'firewall', family, hook, priority) if firewall: - output_firewall_name(family, hook, priority, firewall) + output_firewall_name(family, hook, priority, firewall, detail=detail) -def show_firewall_rule(family, hook, priority, rule_id): +def show_firewall_rule(family, hook, priority, rule_id, detail=None): print('Rule Information') conf = Config() firewall = get_config_node(conf, 'firewall', family, hook, priority) if firewall: - output_firewall_name(family, hook, priority, firewall, rule_id) + output_firewall_name(family, hook, priority, firewall, rule_id, detail=detail) -def show_firewall_group(name=None): +def show_firewall_group(name=None, detail=None): conf = Config() firewall = get_config_node(conf, node='firewall') @@ -594,7 +591,7 @@ def find_references(group_type, group_name): for group_type, group_type_conf in firewall['group'].items(): # interate over dynamic-groups if group_type == 'dynamic_group': - if not args.detail: + if not detail: header_tail = ['Timeout', 'Expires'] for dynamic_type in ['address_group', 'ipv6_address_group']: @@ -611,7 +608,7 @@ def find_references(group_type, group_name): members = get_nftables_group_members(family, 'vyos_filter', f'{prefix}{dynamic_name}') if not members: - if args.detail: + if detail: row.append('N/D') else: row += ["N/D"] * 3 @@ -629,7 +626,7 @@ def find_references(group_type, group_name): timeout = str(member.get('timeout', 'N/D')) expires = str(member.get('expires', 'N/D')) - if args.detail: + if detail: row.append(f'{val} (timeout: {timeout}, expires: {expires})') continue @@ -639,7 +636,7 @@ def find_references(group_type, group_name): row += [val, timeout, expires] rows.append(row) - if args.detail: + if detail: header_tail += [""] * (len(members) - 1) rows.append(row) @@ -657,7 +654,7 @@ def find_references(group_type, group_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: + if detail: header_tail = ['IPv6 Members', 'Remote URL'] row.append('N/D') row.append('N/D') @@ -667,7 +664,7 @@ def find_references(group_type, group_name): rows.append(row) else: # display all table elements in detail view - if args.detail: + if detail: header_tail = ['IPv6 Members', 'Remote URL'] if members: row.append(' '.join(members)) @@ -707,7 +704,7 @@ def find_references(group_type, group_name): if rows: print('Firewall Groups\n') - if args.detail: + if detail: header = ['Name', 'Description', 'Type', 'References', 'Members'] + header_tail output_firewall_vertical(rows, header, adjust=False) else: @@ -716,7 +713,7 @@ def find_references(group_type, group_name): rows[rows.index(i)].pop(1) print(tabulate.tabulate(rows, header)) -def show_summary(): +def show_summary(detail=None): print('Ruleset Summary') conf = Config() @@ -760,9 +757,9 @@ def show_summary(): print('\nBridge Ruleset:\n') print(tabulate.tabulate(br_out, header) + '\n') - show_firewall_group() + show_firewall_group(detail=detail) -def show_statistics(): +def show_statistics(detail=None): print('Rulesets Statistics') conf = Config() @@ -779,7 +776,7 @@ def show_statistics(): if family in firewall: for hook, hook_conf in firewall[family].items(): for prior, prior_conf in firewall[family][hook].items(): - output_firewall_name_statistics(family, hook,prior, prior_conf) + output_firewall_name_statistics(family, hook,prior, prior_conf, detail=detail) if __name__ == '__main__': parser = argparse.ArgumentParser() @@ -796,16 +793,16 @@ def show_statistics(): if args.action == 'show': if not args.rule: - show_firewall_name(args.family, args.hook, args.priority) + show_firewall_name(args.family, args.hook, args.priority, args.detail) else: - show_firewall_rule(args.family, args.hook, args.priority, args.rule) + show_firewall_rule(args.family, args.hook, args.priority, args.rule, args.detail) elif args.action == 'show_all': - show_firewall() + show_firewall(args.detail) elif args.action == 'show_family': - show_firewall_family(args.family) + show_firewall_family(args.family, args.detail) elif args.action == 'show_group': - show_firewall_group(args.name) + show_firewall_group(args.name, args.detail) elif args.action == 'show_statistics': - show_statistics() + show_statistics(args.detail) elif args.action == 'show_summary': - show_summary() + show_summary(args.detail) diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py index 0db9ef545e3..6c1c0781289 100755 --- a/src/op_mode/ikev2_profile_generator.py +++ b/src/op_mode/ikev2_profile_generator.py @@ -225,6 +225,7 @@ def transform_pfs(pfs, ike_dh_group): pfs_enabled = (pfs != 'disable') + pfs_dh_group = None if pfs == 'enable': pfs_dh_group = ike_dh_group elif pfs.startswith('dh-group'): diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index a65bcfd5975..61018b0bc49 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -36,6 +36,7 @@ def _get_xml_translation(direction, family, address=None): """ Get conntrack XML output --src-nat|--dst-nat """ + opt = '' if direction == 'source': opt = '--src-nat' if direction == 'destination': @@ -62,6 +63,7 @@ def _get_json_data(direction, family): """ Get NAT format JSON """ + chain = '' if direction == 'source': chain = 'POSTROUTING' if direction == 'destination': @@ -137,6 +139,7 @@ def _get_ports_for_output(rules): data_entries = [] for rule in data: + interface = 'any' if 'comment' in rule['rule']: comment = rule.get('rule').get('comment') rule_number = comment.split('-')[-1] @@ -239,6 +242,9 @@ def _get_ports_for_output(rules): def _get_formatted_output_statistics(data, direction): data_entries = [] for rule in data: + packets = '' + _bytes = '' + interface = 'any' if 'comment' in rule['rule']: comment = rule.get('rule').get('comment') rule_number = comment.split('-')[-1] diff --git a/src/op_mode/otp.py b/src/op_mode/otp.py index aceb7566067..f9fb9cc5a03 100755 --- a/src/op_mode/otp.py +++ b/src/op_mode/otp.py @@ -85,6 +85,7 @@ def _get_login_otp(username: str, info:str): result['otp_length'] = '6' result['interval'] = '30' result['token_type'] = 'hotp-time' + token_type_acrn = '' if result['token_type'] == 'hotp-time': token_type_acrn = 'totp' result['otp_url'] = ''.join(["otpauth://",token_type_acrn,"/",username,"@",\ diff --git a/src/op_mode/show_openconnect_otp.py b/src/op_mode/show_openconnect_otp.py index 36aa7bf1101..8366df3738e 100755 --- a/src/op_mode/show_openconnect_otp.py +++ b/src/op_mode/show_openconnect_otp.py @@ -63,6 +63,7 @@ def display_otp_ocserv(username, params, info): otp_length = params['otp']['otp_length'] interval = params['otp']['interval'] token_type = params['otp']['token_type'] + token_type_acrn = '' if token_type == 'hotp-time': token_type_acrn = 'totp' key_base32 = b32encode(bytes.fromhex(key_hex)).decode() diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py index 05e7d0e752c..4106051ab97 100755 --- a/src/op_mode/show_wwan.py +++ b/src/op_mode/show_wwan.py @@ -37,7 +37,7 @@ def qmi_cmd(device, command, silent=False): try: tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}') - tmp = tmp.replace(f'[{cdc}] ', '') + tmp = tmp.replace(f'[{device}] ', '') if not silent: # skip first line as this only holds the info headline for line in tmp.splitlines()[1:]: From c6bef37f7fe75310e63292e32a6d2735b1f282b4 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:38:02 +0200 Subject: [PATCH 17/17] T7557: Update locked user check without spwd module --- src/op_mode/show_users.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py index bccfaf99172..cc650780e08 100755 --- a/src/op_mode/show_users.py +++ b/src/op_mode/show_users.py @@ -47,13 +47,14 @@ def is_locked(user_name: str) -> bool: """Check if a given user has password in shadow db""" try: - import warnings - with warnings.catch_warnings(): - warnings.filterwarnings("ignore",category=DeprecationWarning) - import spwd - encrypted_password = spwd.getspnam(user_name)[1] - return encrypted_password == '*' or encrypted_password.startswith('!') - except (KeyError, PermissionError): + with open('/etc/shadow', 'r') as f: + for line in f.readlines(): + args = line.strip().split(':') + if len(args) < 2: + continue + if args[0] == user_name: + return len(args[1]) > 0 and args[1].startswith('!') + except PermissionError: print('Cannot access shadow database, ensure this script is run with sufficient permissions') sys.exit(1)