From 9e22ab6b2aee48029d3455f65880e45c558cf1da Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sat, 15 Jun 2024 21:40:04 +0200 Subject: [PATCH] wireless: T6318: move country-code to a system wide configuration Wireless devices are subject to regulations issued by authorities. For any given AP or router, there will most likely be no case where one wireless NIC is located in one country and another wireless NIC in the same device is located in another country, resulting in different regulatory domains to apply to the same box. Currently, wireless regulatory domains in VyOS need to be configured per-NIC: set interfaces wireless wlan0 country-code us This leads to several side-effects: * When operating multiple WiFi NICs, they all can have different regulatory domains configured which might offend legislation. * Some NICs need additional entries to /etc/modprobe.d/cfg80211.conf to apply regulatory domain settings, such as: "options cfg80211 ieee80211_regdom=US" This is true for the Compex WLE600VX. This setting cannot be done per-interface. Migrate the first found wireless module country-code from the wireless interface CLI to: "system wireless country-code" --- data/config-mode-dependencies/vyos-1x.json | 3 + data/configd-include.json | 1 + .../include/version/interfaces-version.xml.i | 2 +- .../interfaces_wireless.xml.in | 20 ------ interface-definitions/system_wireless.xml.in | 36 ++++++++++ smoketest/config-tests/wireless-basic | 25 +++++++ smoketest/configs/wireless-basic | 66 +++++++++++++++++++ .../scripts/cli/test_interfaces_wireless.py | 50 ++++++++------ src/conf_mode/interfaces_wireless.py | 11 +++- src/conf_mode/system_wireless.py | 64 ++++++++++++++++++ src/migration-scripts/interfaces/32-to-33 | 57 ++++++++++++++++ 11 files changed, 292 insertions(+), 43 deletions(-) create mode 100644 interface-definitions/system_wireless.xml.in create mode 100644 smoketest/config-tests/wireless-basic create mode 100644 smoketest/configs/wireless-basic create mode 100644 src/conf_mode/system_wireless.py create mode 100755 src/migration-scripts/interfaces/32-to-33 diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 3f381169b7..9623948c24 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 dcee503064..a00eb4bcb8 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -102,6 +102,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/interface-definitions/include/version/interfaces-version.xml.i b/interface-definitions/include/version/interfaces-version.xml.i index 854e60f4e5..2915b318eb 100644 --- a/interface-definitions/include/version/interfaces-version.xml.i +++ b/interface-definitions/include/version/interfaces-version.xml.i @@ -1,3 +1,3 @@ - + diff --git a/interface-definitions/interfaces_wireless.xml.in b/interface-definitions/interfaces_wireless.xml.in index 458f7ebb3f..b3fc2302d9 100644 --- a/interface-definitions/interfaces_wireless.xml.in +++ b/interface-definitions/interfaces_wireless.xml.in @@ -451,26 +451,6 @@ 0 - - - Indicate country in which device is operating - - 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 - - - 00 - World regulatory domain - - - txt - ISO/IEC 3166-1 Country Code - - - (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) - - Invalid ISO/IEC 3166-1 Country Code - - #include #include #include diff --git a/interface-definitions/system_wireless.xml.in b/interface-definitions/system_wireless.xml.in new file mode 100644 index 0000000000..834f8b624a --- /dev/null +++ b/interface-definitions/system_wireless.xml.in @@ -0,0 +1,36 @@ + + + + + + + Wireless (IEEE-802.11) subsystem settings + + 317 + + + + + Indicate country in which device is operating + + 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 + + + 00 + World regulatory domain + + + txt + ISO/IEC 3166-1 Country Code + + + (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) + + Invalid ISO/IEC 3166-1 Country Code + + + + + + + diff --git a/smoketest/config-tests/wireless-basic b/smoketest/config-tests/wireless-basic new file mode 100644 index 0000000000..5d7bc8aaa0 --- /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 0000000000..9cc34f5bcc --- /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 b45754cae4..2806b7201c 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -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 @@ def setUpClass(cls): 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 @@ def test_wireless_hostapd_config(self): 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 @@ -150,7 +163,7 @@ 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 +173,20 @@ def test_wireless_hostapd_wpa_config(self): 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 +234,6 @@ def test_wireless_access_point_bridge(self): 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 +271,6 @@ def test_wireless_security_station_address(self): 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 +305,4 @@ def test_wireless_security_station_address(self): 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 c0a17c0bc3..998ff9dbae 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -44,6 +44,8 @@ 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/system_wireless.py b/src/conf_mode/system_wireless.py new file mode 100644 index 0000000000..e0ca0ab8ec --- /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 . + +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 0000000000..caf5884743 --- /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 . +# +# 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)