diff options
-rw-r--r-- | data/config-mode-dependencies/vyos-1x.json | 3 | ||||
-rw-r--r-- | data/configd-include.json | 2 | ||||
-rw-r--r-- | data/templates/wifi/hostapd.conf.j2 | 32 | ||||
-rw-r--r-- | interface-definitions/include/version/interfaces-version.xml.i | 2 | ||||
-rw-r--r-- | interface-definitions/interfaces_wireless.xml.in | 20 | ||||
-rw-r--r-- | interface-definitions/system_wireless.xml.in | 36 | ||||
-rw-r--r-- | python/vyos/configsession.py | 10 | ||||
-rw-r--r-- | python/vyos/defaults.py | 5 | ||||
-rw-r--r-- | python/vyos/utils/__init__.py | 1 | ||||
-rw-r--r-- | python/vyos/utils/auth.py | 12 | ||||
-rw-r--r-- | python/vyos/utils/configfs.py | 37 | ||||
-rw-r--r-- | smoketest/config-tests/wireless-basic | 25 | ||||
-rw-r--r-- | smoketest/configs/wireless-basic | 66 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_wireless.py | 190 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_wireless.py | 11 | ||||
-rwxr-xr-x | src/conf_mode/pki.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/system_login.py | 50 | ||||
-rw-r--r-- | src/conf_mode/system_wireless.py | 64 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/32-to-33 | 57 | ||||
-rwxr-xr-x | src/op_mode/pki.py | 22 |
20 files changed, 535 insertions, 112 deletions
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 3f381169b..9623948c2 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -59,5 +59,8 @@ "wireguard": ["interfaces_wireguard"], "wireless": ["interfaces_wireless"], "wwan": ["interfaces_wwan"] + }, + "system_wireless": { + "wireless": ["interfaces_wireless"] } } diff --git a/data/configd-include.json b/data/configd-include.json index dcee50306..633d898a5 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -92,6 +92,7 @@ "system_ip.py", "system_ipv6.py", "system_lcd.py", +"system_login.py", "system_login_banner.py", "system_logs.py", "system_option.py", @@ -102,6 +103,7 @@ "system_task-scheduler.py", "system_timezone.py", "system_update-check.py", +"system_wireless.py", "vpn_ipsec.py", "vpn_l2tp.py", "vpn_openconnect.py", diff --git a/data/templates/wifi/hostapd.conf.j2 b/data/templates/wifi/hostapd.conf.j2 index 769325b49..e1a08f7e4 100644 --- a/data/templates/wifi/hostapd.conf.j2 +++ b/data/templates/wifi/hostapd.conf.j2 @@ -383,23 +383,25 @@ vht_oper_chwidth={{ capabilities.vht.channel_set_width }} {% for short_gi in capabilities.vht.short_gi if capabilities.vht.short_gi is vyos_defined %} {% set output.value = output.value ~ '[SHORT-GI-' ~ short_gi | upper ~ ']' %} {% endfor %} -{% for beamform in capabilities.vht.beamform if capabilities.vht.beamform is vyos_defined %} -{% set output.value = output.value ~ '[SU-BEAMFORMER]' if beamform is vyos_defined('single-user-beamformer') else '' %} -{% set output.value = output.value ~ '[SU-BEAMFORMEE]' if beamform is vyos_defined('single-user-beamformee') else '' %} -{% set output.value = output.value ~ '[MU-BEAMFORMER]' if beamform is vyos_defined('multi-user-beamformer') else '' %} -{% set output.value = output.value ~ '[MU-BEAMFORMEE]' if beamform is vyos_defined('multi-user-beamformee') else '' %} -{% endfor %} -{% if capabilities.vht.antenna_count is vyos_defined and capabilities.vht.antenna_count | int > 1 %} -{% if capabilities.vht.beamform is vyos_defined %} -{% if capabilities.vht.beamform == 'single-user-beamformer' %} +{% if capabilities.vht.beamform is vyos_defined %} +{% for bf in capabilities.vht.beamform %} +{% set output.value = output.value ~ '[SU-BEAMFORMER]' if bf is vyos_defined('single-user-beamformer') else output.value %} +{% set output.value = output.value ~ '[SU-BEAMFORMEE]' if bf is vyos_defined('single-user-beamformee') else output.value %} +{% set output.value = output.value ~ '[MU-BEAMFORMER]' if bf is vyos_defined('multi-user-beamformer') else output.value %} +{% set output.value = output.value ~ '[MU-BEAMFORMEE]' if bf is vyos_defined('multi-user-beamformee') else output.value %} +{% endfor %} +{% if capabilities.vht.antenna_count is vyos_defined and capabilities.vht.antenna_count | int > 1 %} +{% if 'single-user-beamformer' in capabilities.vht.beamform %} {% if capabilities.vht.antenna_count is vyos_defined and capabilities.vht.antenna_count | int > 1 and capabilities.vht.antenna_count | int < 6 %} -{% set output.value = output.value ~ '[BF-ANTENNA-' ~ capabilities.vht.antenna_count | int -1 ~ ']' %} -{% set output.value = output.value ~ '[SOUNDING-DIMENSION-' ~ capabilities.vht.antenna_count | int -1 ~ ']' %} +{% set dimension = capabilities.vht.antenna_count | int - 1 %} +{% set output.value = output.value ~ '[BF-ANTENNA-' ~ dimension ~ ']' %} +{% set output.value = output.value ~ '[SOUNDING-DIMENSION-' ~ dimension ~ ']' %} +{% endif %} +{% else %} +{% if capabilities.vht.antenna_count is vyos_defined and capabilities.vht.antenna_count | int > 1 and capabilities.vht.antenna_count | int < 5 %} +{% set output.value = output.value ~ '[BF-ANTENNA-' ~ capabilities.vht.antenna_count ~ ']' %} +{% set output.value = output.value ~ '[SOUNDING-DIMENSION-' ~ capabilities.vht.antenna_count ~ ']' %} {% endif %} -{% endif %} -{% if capabilities.vht.antenna_count is vyos_defined and capabilities.vht.antenna_count | int > 1 and capabilities.vht.antenna_count | int < 5 %} -{% set output.value = output.value ~ '[BF-ANTENNA-' ~ capabilities.vht.antenna_count ~ ']' %} -{% set output.value = output.value ~ '[SOUNDING-DIMENSION-' ~ capabilities.vht.antenna_count ~ ']' %} {% endif %} {% endif %} {% endif %} diff --git a/interface-definitions/include/version/interfaces-version.xml.i b/interface-definitions/include/version/interfaces-version.xml.i index 854e60f4e..2915b318e 100644 --- a/interface-definitions/include/version/interfaces-version.xml.i +++ b/interface-definitions/include/version/interfaces-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/interfaces-version.xml.i --> -<syntaxVersion component='interfaces' version='32'></syntaxVersion> +<syntaxVersion component='interfaces' version='33'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_wireless.xml.in b/interface-definitions/interfaces_wireless.xml.in index 458f7ebb3..b3fc2302d 100644 --- a/interface-definitions/interfaces_wireless.xml.in +++ b/interface-definitions/interfaces_wireless.xml.in @@ -451,26 +451,6 @@ </properties> <defaultValue>0</defaultValue> </leafNode> - <leafNode name="country-code"> - <properties> - <help>Indicate country in which device is operating</help> - <completionHelp> - <list>00 ad ae af ai al am an ar as at au aw az ba bb bd be bf bg bh bl bm bn bo br bs bt by bz ca cf ch ci cl cn co cr cu cx cy cz de dk dm do dz ec ee eg es et fi fm fr gb gd ge gf gh gl gp gr gt gu gy hk hn hr ht hu id ie il in ir is it jm jo jp ke kh kn kp kr kw ky kz lb lc li lk ls lt lu lv ma mc md me mf mh mk mn mo mp mq mr mt mu mv mw mx my ng ni nl no np nz om pa pe pf pg ph pk pl pm pr pt pw py qa re ro rs ru rw sa se sg si sk sn sr sv sy tc td tg th tn tr tt tw tz ua ug us uy uz vc ve vi vn vu wf ws ye yt za zw</list> - </completionHelp> - <valueHelp> - <format>00</format> - <description>World regulatory domain</description> - </valueHelp> - <valueHelp> - <format>txt</format> - <description>ISO/IEC 3166-1 Country Code</description> - </valueHelp> - <constraint> - <regex>(00|ad|ae|af|ai|al|am|an|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|bh|bl|bm|bn|bo|br|bs|bt|by|bz|ca|cf|ch|ci|cl|cn|co|cr|cu|cx|cy|cz|de|dk|dm|do|dz|ec|ee|eg|es|et|fi|fm|fr|gb|gd|ge|gf|gh|gl|gp|gr|gt|gu|gy|hk|hn|hr|ht|hu|id|ie|il|in|ir|is|it|jm|jo|jp|ke|kh|kn|kp|kr|kw|ky|kz|lb|lc|li|lk|ls|lt|lu|lv|ma|mc|md|me|mf|mh|mk|mn|mo|mp|mq|mr|mt|mu|mv|mw|mx|my|ng|ni|nl|no|np|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pr|pt|pw|py|qa|re|ro|rs|ru|rw|sa|se|sg|si|sk|sn|sr|sv|sy|tc|td|tg|th|tn|tr|tt|tw|tz|ua|ug|us|uy|uz|vc|ve|vi|vn|vu|wf|ws|ye|yt|za|zw)</regex> - </constraint> - <constraintErrorMessage>Invalid ISO/IEC 3166-1 Country Code</constraintErrorMessage> - </properties> - </leafNode> #include <include/generic-description.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> diff --git a/interface-definitions/system_wireless.xml.in b/interface-definitions/system_wireless.xml.in new file mode 100644 index 000000000..834f8b624 --- /dev/null +++ b/interface-definitions/system_wireless.xml.in @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="wireless" owner="${vyos_conf_scripts_dir}/system_wireless.py"> + <properties> + <help>Wireless (IEEE-802.11) subsystem settings</help> + <!-- must be before interface wireless, check /opt/vyatta/sbin/priority.pl --> + <priority>317</priority> + </properties> + <children> + <leafNode name="country-code"> + <properties> + <help>Indicate country in which device is operating</help> + <completionHelp> + <list>00 ad ae af ai al am an ar as at au aw az ba bb bd be bf bg bh bl bm bn bo br bs bt by bz ca cf ch ci cl cn co cr cu cx cy cz de dk dm do dz ec ee eg es et fi fm fr gb gd ge gf gh gl gp gr gt gu gy hk hn hr ht hu id ie il in ir is it jm jo jp ke kh kn kp kr kw ky kz lb lc li lk ls lt lu lv ma mc md me mf mh mk mn mo mp mq mr mt mu mv mw mx my ng ni nl no np nz om pa pe pf pg ph pk pl pm pr pt pw py qa re ro rs ru rw sa se sg si sk sn sr sv sy tc td tg th tn tr tt tw tz ua ug us uy uz vc ve vi vn vu wf ws ye yt za zw</list> + </completionHelp> + <valueHelp> + <format>00</format> + <description>World regulatory domain</description> + </valueHelp> + <valueHelp> + <format>txt</format> + <description>ISO/IEC 3166-1 Country Code</description> + </valueHelp> + <constraint> + <regex>(00|ad|ae|af|ai|al|am|an|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|bh|bl|bm|bn|bo|br|bs|bt|by|bz|ca|cf|ch|ci|cl|cn|co|cr|cu|cx|cy|cz|de|dk|dm|do|dz|ec|ee|eg|es|et|fi|fm|fr|gb|gd|ge|gf|gh|gl|gp|gr|gt|gu|gy|hk|hn|hr|ht|hu|id|ie|il|in|ir|is|it|jm|jo|jp|ke|kh|kn|kp|kr|kw|ky|kz|lb|lc|li|lk|ls|lt|lu|lv|ma|mc|md|me|mf|mh|mk|mn|mo|mp|mq|mr|mt|mu|mv|mw|mx|my|ng|ni|nl|no|np|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pr|pt|pw|py|qa|re|ro|rs|ru|rw|sa|se|sg|si|sk|sn|sr|sv|sy|tc|td|tg|th|tn|tr|tt|tw|tz|ua|ug|us|uy|uz|vc|ve|vi|vn|vu|wf|ws|ye|yt|za|zw)</regex> + </constraint> + <constraintErrorMessage>Invalid ISO/IEC 3166-1 Country Code</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index beec6010b..ccf2ce8f2 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -1,5 +1,4 @@ -# configsession -- the write API for the VyOS running config -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 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; @@ -12,11 +11,14 @@ # 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 +# configsession -- the write API for the VyOS running config + import os import re import sys import subprocess +from vyos.defaults import directories from vyos.utils.process import is_systemd_service_running from vyos.utils.dict import dict_to_paths @@ -58,7 +60,7 @@ def inject_vyos_env(env): env['VYOS_HEADLESS_CLIENT'] = 'vyos_http_api' env['vyatta_bindir']= '/opt/vyatta/bin' env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates' - env['vyatta_configdir'] = '/opt/vyatta/config' + env['vyatta_configdir'] = directories['vyos_configdir'] env['vyatta_datadir'] = '/opt/vyatta/share' env['vyatta_datarootdir'] = '/opt/vyatta/share' env['vyatta_libdir'] = '/opt/vyatta/lib' @@ -70,7 +72,7 @@ def inject_vyos_env(env): env['vyos_bin_dir'] = '/usr/bin' env['vyos_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates' env['vyos_completion_dir'] = '/usr/libexec/vyos/completion' - env['vyos_configdir'] = '/opt/vyatta/config' + env['vyos_configdir'] = directories['vyos_configdir'] env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode' env['vyos_datadir'] = '/opt/vyatta/share' env['vyos_datarootdir']= '/opt/vyatta/share' diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index e7cd69a8b..9ccd925ce 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -1,4 +1,4 @@ -# Copyright 2018-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2018-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 @@ -35,6 +35,7 @@ directories = { 'vyos_udev_dir' : '/run/udev/vyos', 'isc_dhclient_dir' : '/run/dhclient', 'dhcp6_client_dir' : '/run/dhcp6c', + 'vyos_configdir' : '/opt/vyatta/config' } config_status = '/tmp/vyos-config-status' @@ -44,7 +45,7 @@ cfg_group = 'vyattacfg' cfg_vintage = 'vyos' -commit_lock = '/opt/vyatta/config/.lock' +commit_lock = os.path.join(directories['vyos_configdir'], '.lock') component_version_json = os.path.join(directories['data'], 'component-versions.json') diff --git a/python/vyos/utils/__init__.py b/python/vyos/utils/__init__.py index 1cd062a11..90620071b 100644 --- a/python/vyos/utils/__init__.py +++ b/python/vyos/utils/__init__.py @@ -17,6 +17,7 @@ from vyos.utils import assertion from vyos.utils import auth from vyos.utils import boot from vyos.utils import commit +from vyos.utils import configfs from vyos.utils import convert from vyos.utils import cpu from vyos.utils import dict diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py index a59858d72..d014f756f 100644 --- a/python/vyos/utils/auth.py +++ b/python/vyos/utils/auth.py @@ -1,6 +1,6 @@ # authutils -- miscelanneous functions for handling passwords and publis keys # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2023-2024 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; @@ -11,13 +11,12 @@ # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import re from vyos.utils.process import cmd - def make_password_hash(password): """ Makes a password hash for /etc/shadow using mkpasswd """ @@ -39,3 +38,10 @@ def split_ssh_public_key(key_string, defaultname=""): 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)) return({"type": key_type, "data": key_data, "name": key_name}) + +def get_current_user() -> str: + import os + current_user = 'nobody' + if 'SUDO_USER' in os.environ: + current_user = os.environ['SUDO_USER'] + return current_user diff --git a/python/vyos/utils/configfs.py b/python/vyos/utils/configfs.py new file mode 100644 index 000000000..8617f0129 --- /dev/null +++ b/python/vyos/utils/configfs.py @@ -0,0 +1,37 @@ +# Copyright 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/>. + +import os + +def delete_cli_node(cli_path: list): + from shutil import rmtree + for config_dir in ['VYATTA_TEMP_CONFIG_DIR', 'VYATTA_CHANGES_ONLY_DIR']: + tmp = os.path.join(os.environ[config_dir], '/'.join(cli_path)) + # delete CLI node + if os.path.exists(tmp): + rmtree(tmp) + +def add_cli_node(cli_path: list, value: str=None): + from vyos.utils.auth import get_current_user + from vyos.utils.file import write_file + + current_user = get_current_user() + for config_dir in ['VYATTA_TEMP_CONFIG_DIR', 'VYATTA_CHANGES_ONLY_DIR']: + # store new value + tmp = os.path.join(os.environ[config_dir], '/'.join(cli_path)) + write_file(f'{tmp}/node.val', value, user=current_user, group='vyattacfg', mode=0o664) + # mark CLI node as modified + if config_dir == 'VYATTA_CHANGES_ONLY_DIR': + write_file(f'{tmp}/.modified', '', user=current_user, group='vyattacfg', mode=0o664) diff --git a/smoketest/config-tests/wireless-basic b/smoketest/config-tests/wireless-basic new file mode 100644 index 000000000..77db29c2f --- /dev/null +++ b/smoketest/config-tests/wireless-basic @@ -0,0 +1,25 @@ +set interfaces ethernet eth0 duplex 'auto' +set interfaces ethernet eth0 speed 'auto' +set interfaces ethernet eth1 duplex 'auto' +set interfaces ethernet eth1 speed 'auto' +set interfaces wireless wlan0 address '192.168.0.1/24' +set interfaces wireless wlan0 channel '1' +set interfaces wireless wlan0 mode 'n' +set interfaces wireless wlan0 security wpa cipher 'CCMP' +set interfaces wireless wlan0 security wpa mode 'wpa2' +set interfaces wireless wlan0 security wpa passphrase '12345678' +set interfaces wireless wlan0 ssid 'VyOS' +set interfaces wireless wlan0 type 'access-point' +set interfaces wireless wlan1 address '192.168.1.1/24' +set interfaces wireless wlan1 channel '2' +set interfaces wireless wlan1 mode 'n' +set interfaces wireless wlan1 ssid 'VyOS-PUBLIC' +set interfaces wireless wlan1 type 'access-point' +set system config-management commit-revisions '200' +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 wireless country-code 'es' +set system syslog global facility all level 'info' +set system syslog global facility local7 level 'debug' diff --git a/smoketest/configs/wireless-basic b/smoketest/configs/wireless-basic new file mode 100644 index 000000000..9cc34f5bc --- /dev/null +++ b/smoketest/configs/wireless-basic @@ -0,0 +1,66 @@ +interfaces { + ethernet eth0 { + duplex "auto" + speed "auto" + } + ethernet eth1 { + duplex "auto" + speed "auto" + } + wireless wlan0 { + address 192.168.0.1/24 + channel 1 + country-code es + mode n + security { + wpa { + cipher CCMP + mode wpa2 + passphrase 12345678 + } + } + ssid VyOS + type access-point + } + wireless wlan1 { + address 192.168.1.1/24 + channel 2 + country-code de + mode n + ssid VyOS-PUBLIC + type access-point + } +} +system { + config-management { + commit-revisions "200" + } + console { + device ttyS0 { + speed 115200 + } + } + domain-name "dev.vyos.net" + host-name "WR1" + login { + user vyos { + authentication { + encrypted-password "$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0" + } + } + } + syslog { + global { + facility all { + level "info" + } + facility local7 { + level "debug" + } + } + } +} + +// 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/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index b45754cae..421ca7861 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-2023 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -32,19 +32,31 @@ def get_config_value(interface, key): tmp = re.findall(f'{key}=+(.*)', tmp) return tmp[0] +wifi_cc_path = ['system', 'wireless', 'country-code'] + class WirelessInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['interfaces', 'wireless'] cls._options = { - 'wlan0': ['physical-device phy0', 'ssid VyOS-WIFI-0', - 'type station', 'address 192.0.2.1/30'], - 'wlan1': ['physical-device phy0', 'ssid VyOS-WIFI-1', 'country-code se', - 'type access-point', 'address 192.0.2.5/30', 'channel 0'], - 'wlan10': ['physical-device phy1', 'ssid VyOS-WIFI-2', - 'type station', 'address 192.0.2.9/30'], - 'wlan11': ['physical-device phy1', 'ssid VyOS-WIFI-3', 'country-code se', - 'type access-point', 'address 192.0.2.13/30', 'channel 0'], + 'wlan0': ['physical-device phy0', + 'ssid VyOS-WIFI-0', + 'type station', + 'address 192.0.2.1/30'], + 'wlan1': ['physical-device phy0', + 'ssid VyOS-WIFI-1', + 'type access-point', + 'address 192.0.2.5/30', + 'channel 0'], + 'wlan10': ['physical-device phy1', + 'ssid VyOS-WIFI-2', + 'type station', + 'address 192.0.2.9/30'], + 'wlan11': ['physical-device phy1', + 'ssid VyOS-WIFI-3', + 'type access-point', + 'address 192.0.2.13/30', + 'channel 0'], } cls._interfaces = list(cls._options) # call base-classes classmethod @@ -54,6 +66,8 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): cls._test_ipv6 = False cls._test_vlan = False + cls.cli_set(cls, wifi_cc_path + ['es']) + def test_wireless_add_single_ip_address(self): # derived method to check if member interfaces are enslaved properly super().test_add_single_ip_address() @@ -74,7 +88,6 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): ssid = 'ssid' self.cli_set(self._base_path + [interface, 'ssid', ssid]) - self.cli_set(self._base_path + [interface, 'country-code', 'se']) self.cli_set(self._base_path + [interface, 'type', 'access-point']) # auto-powersave is special @@ -146,11 +159,149 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): # Check for running process self.assertTrue(process_named_running('hostapd')) + def test_wireless_hostapd_vht_mu_beamformer_config(self): + # Multi-User-Beamformer + interface = 'wlan1' + ssid = 'vht_mu-beamformer' + antennas = '3' + + self.cli_set(self._base_path + [interface, 'ssid', ssid]) + self.cli_set(self._base_path + [interface, 'type', 'access-point']) + self.cli_set(self._base_path + [interface, 'channel', '36']) + + ht_opt = { + # VyOS CLI option hostapd - ht_capab setting + 'channel-set-width ht20' : '[HT20]', + 'channel-set-width ht40-' : '[HT40-]', + 'channel-set-width ht40+' : '[HT40+]', + 'dsss-cck-40' : '[DSSS_CCK-40]', + 'short-gi 20' : '[SHORT-GI-20]', + 'short-gi 40' : '[SHORT-GI-40]', + 'max-amsdu 7935' : '[MAX-AMSDU-7935]', + } + for key in ht_opt: + self.cli_set(self._base_path + [interface, 'capabilities', 'ht'] + key.split()) + + vht_opt = { + # VyOS CLI option hostapd - ht_capab setting + 'max-mpdu 11454' : '[MAX-MPDU-11454]', + 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2]', + 'stbc tx' : '[TX-STBC-2BY1]', + 'stbc rx 12' : '[RX-STBC-12]', + 'ldpc' : '[RXLDPC]', + 'tx-powersave' : '[VHT-TXOP-PS]', + 'vht-cf' : '[HTC-VHT]', + 'antenna-pattern-fixed' : '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]', + 'link-adaptation both' : '[VHT-LINK-ADAPT3]', + 'short-gi 80' : '[SHORT-GI-80]', + 'short-gi 160' : '[SHORT-GI-160]', + 'beamform multi-user-beamformer' : '[MU-BEAMFORMER][BF-ANTENNA-3][SOUNDING-DIMENSION-3]', + } + + self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'channel-set-width', '1']) + self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'center-channel-freq', 'freq-1', '42']) + self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'antenna-count', antennas]) + for key in vht_opt: + self.cli_set(self._base_path + [interface, 'capabilities', 'vht'] + key.split()) + + self.cli_commit() + + # + # Validate Config + # + tmp = get_config_value(interface, 'interface') + self.assertEqual(interface, tmp) + + # ssid + tmp = get_config_value(interface, 'ssid') + self.assertEqual(ssid, tmp) + + # channel + tmp = get_config_value(interface, 'channel') + self.assertEqual('36', tmp) + + tmp = get_config_value(interface, 'ht_capab') + for key, value in ht_opt.items(): + self.assertIn(value, tmp) + + tmp = get_config_value(interface, 'vht_capab') + for key, value in vht_opt.items(): + self.assertIn(value, tmp) + + def test_wireless_hostapd_vht_su_beamformer_config(self): + # Single-User-Beamformer + interface = 'wlan1' + ssid = 'vht_su-beamformer' + antennas = '3' + + self.cli_set(self._base_path + [interface, 'ssid', ssid]) + self.cli_set(self._base_path + [interface, 'type', 'access-point']) + self.cli_set(self._base_path + [interface, 'channel', '36']) + + ht_opt = { + # VyOS CLI option hostapd - ht_capab setting + 'channel-set-width ht20' : '[HT20]', + 'channel-set-width ht40-' : '[HT40-]', + 'channel-set-width ht40+' : '[HT40+]', + 'dsss-cck-40' : '[DSSS_CCK-40]', + 'short-gi 20' : '[SHORT-GI-20]', + 'short-gi 40' : '[SHORT-GI-40]', + 'max-amsdu 7935' : '[MAX-AMSDU-7935]', + } + for key in ht_opt: + self.cli_set(self._base_path + [interface, 'capabilities', 'ht'] + key.split()) + + vht_opt = { + # VyOS CLI option hostapd - ht_capab setting + 'max-mpdu 11454' : '[MAX-MPDU-11454]', + 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2]', + 'stbc tx' : '[TX-STBC-2BY1]', + 'stbc rx 12' : '[RX-STBC-12]', + 'ldpc' : '[RXLDPC]', + 'tx-powersave' : '[VHT-TXOP-PS]', + 'vht-cf' : '[HTC-VHT]', + 'antenna-pattern-fixed' : '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]', + 'link-adaptation both' : '[VHT-LINK-ADAPT3]', + 'short-gi 80' : '[SHORT-GI-80]', + 'short-gi 160' : '[SHORT-GI-160]', + 'beamform single-user-beamformer' : '[SU-BEAMFORMER][BF-ANTENNA-2][SOUNDING-DIMENSION-2]', + } + + self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'channel-set-width', '1']) + self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'center-channel-freq', 'freq-1', '42']) + self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'antenna-count', antennas]) + for key in vht_opt: + self.cli_set(self._base_path + [interface, 'capabilities', 'vht'] + key.split()) + + self.cli_commit() + + # + # Validate Config + # + tmp = get_config_value(interface, 'interface') + self.assertEqual(interface, tmp) + + # ssid + tmp = get_config_value(interface, 'ssid') + self.assertEqual(ssid, tmp) + + # channel + tmp = get_config_value(interface, 'channel') + self.assertEqual('36', tmp) + + tmp = get_config_value(interface, 'ht_capab') + for key, value in ht_opt.items(): + self.assertIn(value, tmp) + + tmp = get_config_value(interface, 'vht_capab') + for key, value in vht_opt.items(): + self.assertIn(value, tmp) + def test_wireless_hostapd_wpa_config(self): # Only set the hostapd (access-point) options interface = 'wlan0' phy = 'phy0' - ssid = 'ssid' + ssid = 'VyOS-SMOKETEST' channel = '1' wpa_key = 'VyOSVyOSVyOS' mode = 'n' @@ -160,21 +311,20 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.cli_set(self._base_path + [interface, 'type', 'access-point']) self.cli_set(self._base_path + [interface, 'mode', mode]) + # Country-Code must be set + self.cli_delete(wifi_cc_path) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(wifi_cc_path + [country]) + # SSID must be set with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(self._base_path + [interface, 'ssid', ssid]) # Channel must be set - with self.assertRaises(ConfigSessionError): - self.cli_commit() self.cli_set(self._base_path + [interface, 'channel', channel]) - # Country-Code must be set - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_set(self._base_path + [interface, 'country-code', country]) - self.cli_set(self._base_path + [interface, 'security', 'wpa', 'mode', 'wpa2']) self.cli_set(self._base_path + [interface, 'security', 'wpa', 'passphrase', wpa_key]) @@ -222,7 +372,6 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.cli_set(bridge_path + ['member', 'interface', interface]) self.cli_set(self._base_path + [interface, 'ssid', ssid]) - self.cli_set(self._base_path + [interface, 'country-code', 'se']) self.cli_set(self._base_path + [interface, 'type', 'access-point']) self.cli_commit() @@ -260,7 +409,6 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): deny_mac = ['00:00:00:00:de:01', '00:00:00:00:de:02', '00:00:00:00:de:03', '00:00:00:00:de:04'] self.cli_set(self._base_path + [interface, 'ssid', ssid]) - self.cli_set(self._base_path + [interface, 'country-code', 'se']) self.cli_set(self._base_path + [interface, 'type', 'access-point']) self.cli_set(self._base_path + [interface, 'security', 'station-address', 'mode', 'accept']) @@ -295,4 +443,4 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): if __name__ == '__main__': check_kmod('mac80211_hwsim') - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index c0a17c0bc..998ff9dba 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -44,6 +44,8 @@ hostapd_conf = '/run/hostapd/{ifname}.conf' hostapd_accept_station_conf = '/run/hostapd/{ifname}_station_accept.conf' hostapd_deny_station_conf = '/run/hostapd/{ifname}_station_deny.conf' +country_code_path = ['system', 'wireless', 'country-code'] + def find_other_stations(conf, base, ifname): """ Only one wireless interface per phy can be in station mode - @@ -78,7 +80,11 @@ def get_config(config=None): conf = Config() base = ['interfaces', 'wireless'] - ifname, wifi = get_interface_dict(conf, base) + _, wifi = get_interface_dict(conf, base) + + # retrieve global Wireless regulatory domain setting + if conf.exists(country_code_path): + wifi['country_code'] = conf.return_value(country_code_path) if 'deleted' not in wifi: # then get_interface_dict provides default keys @@ -131,7 +137,8 @@ def verify(wifi): if wifi['type'] == 'access-point': if 'country_code' not in wifi: - raise ConfigError('Wireless country-code is mandatory') + raise ConfigError(f'Wireless country-code is mandatory, use: '\ + f'"set {" ".join(country_code_path)}"!') if 'channel' not in wifi: raise ConfigError('Wireless channel must be configured!') diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index f37cac524..4a0e86f32 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -232,7 +232,7 @@ def get_config(config=None): path = search['path'] path_str = ' '.join(path + found_path) - print(f'PKI: Updating config: {path_str} {item_name}') + #print(f'PKI: Updating config: {path_str} {item_name}') if path[0] == 'interfaces': ifname = found_path[0] diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index 20121f170..439fa645b 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -26,14 +26,15 @@ from time import sleep from vyos.config import Config from vyos.configverify import verify_vrf -from vyos.defaults import directories from vyos.template import render from vyos.template import is_ipv4 +from vyos.utils.auth import get_current_user +from vyos.utils.configfs import delete_cli_node +from vyos.utils.configfs import add_cli_node from vyos.utils.dict import dict_search from vyos.utils.file import chown from vyos.utils.process import cmd from vyos.utils.process import call -from vyos.utils.process import rc_cmd from vyos.utils.process import run from vyos.utils.process import DEVNULL from vyos import ConfigError @@ -125,10 +126,9 @@ def verify(login): # This check is required as the script is also executed from vyos-router # init script and there is no SUDO_USER environment variable available # during system boot. - if 'SUDO_USER' in os.environ: - cur_user = os.environ['SUDO_USER'] - if cur_user in login['rm_users']: - raise ConfigError(f'Attempting to delete current user: {cur_user}') + tmp = get_current_user() + if tmp in login['rm_users']: + raise ConfigError(f'Attempting to delete current user: {tmp}') if 'user' in login: system_users = getpwall() @@ -221,35 +221,13 @@ def generate(login): login['user'][user]['authentication']['encrypted_password'] = encrypted_password del login['user'][user]['authentication']['plaintext_password'] - # remove old plaintext password and set new encrypted password - env = os.environ.copy() - env['vyos_libexec_dir'] = directories['base'] - # Set default commands for re-adding user with encrypted password - del_user_plain = f"system login user {user} authentication plaintext-password" - add_user_encrypt = f"system login user {user} authentication encrypted-password '{encrypted_password}'" - - lvl = env['VYATTA_EDIT_LEVEL'] - # We're in config edit level, for example "edit system login" - # Change default commands for re-adding user with encrypted password - if lvl != '/': - # Replace '/system/login' to 'system login' - lvl = lvl.strip('/').split('/') - # Convert command str to list - del_user_plain = del_user_plain.split() - # New command exclude level, for example "edit system login" - del_user_plain = del_user_plain[len(lvl):] - # Convert string to list - del_user_plain = " ".join(del_user_plain) - - add_user_encrypt = add_user_encrypt.split() - add_user_encrypt = add_user_encrypt[len(lvl):] - add_user_encrypt = " ".join(add_user_encrypt) - - ret, out = rc_cmd(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env) - if ret: raise ConfigError(out) - ret, out = rc_cmd(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env) - if ret: raise ConfigError(out) + del_user_plain = ['system', 'login', 'user', user, 'authentication', 'plaintext-password'] + add_user_encrypt = ['system', 'login', 'user', user, 'authentication', 'encrypted-password'] + + delete_cli_node(del_user_plain) + add_cli_node(add_user_encrypt, value=encrypted_password) + else: try: if get_shadow_password(user) == dict_search('authentication.encrypted_password', user_config): @@ -283,8 +261,6 @@ def generate(login): if os.path.isfile(tacacs_nss_config_file): os.unlink(tacacs_nss_config_file) - - # NSS must always be present on the system render(nss_config_file, 'login/nsswitch.conf.j2', login, permission=0o644, user='root', group='root') diff --git a/src/conf_mode/system_wireless.py b/src/conf_mode/system_wireless.py new file mode 100644 index 000000000..e0ca0ab8e --- /dev/null +++ b/src/conf_mode/system_wireless.py @@ -0,0 +1,64 @@ +#!/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/>. + +from sys import exit + +from vyos.config import Config +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['system', 'wireless'] + interface_base = ['interfaces', 'wireless'] + + wireless = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) + + + if conf.exists(interface_base): + wireless['interfaces'] = conf.list_nodes(interface_base) + for interface in wireless['interfaces']: + set_dependents('wireless', conf, interface) + + return wireless + +def verify(wireless): + pass + +def generate(wireless): + pass + +def apply(wireless): + if 'interfaces' in wireless: + call_dependents() + pass + +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/migration-scripts/interfaces/32-to-33 b/src/migration-scripts/interfaces/32-to-33 new file mode 100755 index 000000000..caf588474 --- /dev/null +++ b/src/migration-scripts/interfaces/32-to-33 @@ -0,0 +1,57 @@ +#!/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/>. +# +# T6318: WiFi country-code should be set system-wide instead of per-device + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['interfaces', 'wireless'] + +config = ConfigTree(config_file) +if not config.exists(base): + # Nothing to do + exit(0) + +installed = False +for interface in config.list_nodes(base): + cc_path = base + [interface, 'country-code'] + if config.exists(cc_path): + tmp = config.return_value(cc_path) + config.delete(cc_path) + + # There can be only ONE wireless country-code per device, everything + # else makes no sense as a WIFI router can not operate in two + # different countries + if not installed: + config.set(['system', 'wireless', 'country-code'], value=tmp) + installed = True + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 361b60e0e..9ce166c7d 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -426,11 +426,15 @@ def generate_ca_certificate_sign(name, ca_name, install=False, file=False): return None cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=True, is_sub_ca=True) - passphrase = ask_passphrase() + + passphrase = None + if private_key is not None: + passphrase = ask_passphrase() if not install and not file: print(encode_certificate(cert)) - print(encode_private_key(private_key, passphrase=passphrase)) + if private_key is not None: + print(encode_private_key(private_key, passphrase=passphrase)) return None if install: @@ -438,7 +442,8 @@ def generate_ca_certificate_sign(name, ca_name, install=False, file=False): if file: write_file(f'{name}.pem', encode_certificate(cert)) - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + if private_key is not None: + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_certificate_sign(name, ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) @@ -492,11 +497,15 @@ def generate_certificate_sign(name, ca_name, install=False, file=False): return None cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False) - passphrase = ask_passphrase() + + passphrase = None + if private_key is not None: + passphrase = ask_passphrase() if not install and not file: print(encode_certificate(cert)) - print(encode_private_key(private_key, passphrase=passphrase)) + if private_key is not None: + print(encode_private_key(private_key, passphrase=passphrase)) return None if install: @@ -504,7 +513,8 @@ def generate_certificate_sign(name, ca_name, install=False, file=False): if file: write_file(f'{name}.pem', encode_certificate(cert)) - write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) + if private_key is not None: + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_certificate_selfsign(name, install=False, file=False): private_key, key_type = generate_private_key() |