diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index 998b06c05..2857e7eab 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -499,7 +499,7 @@ Match devices by MAC when setting options like: `wakeonlan` or `*-offload`. > "XX:XX:XX:XX:XX:XX". The following special options are also accepted: > `permanent` and `random`. > In addition to these options, the NetworkManager renderer also accepts - > `stable` and `preserve`. + > `stable`, `stable-ssid` (Wi-Fi only) and `preserve`. > > **Note:** This will not work reliably for devices matched by name > only and rendered by networkd, due to interactions with device diff --git a/src/parse.c b/src/parse.c index 79c5c7145..6500c7a9c 100644 --- a/src/parse.c +++ b/src/parse.c @@ -392,7 +392,7 @@ handle_special_macaddress_option(NetplanParser* npp, yaml_node_t* node, void* en g_assert(entryptr != NULL); g_assert(node->type == YAML_SCALAR_NODE); - if (!_is_macaddress_special_nm_option(scalar(node)) && + if (!_is_macaddress_special_nm_option(npp->current.netdef, scalar(node)) && !_is_macaddress_special_nd_option(scalar(node))) return FALSE; @@ -695,7 +695,7 @@ handle_netdef_set_mac(NetplanParser* npp, yaml_node_t* node, const void* data, G if (!handle_special_macaddress_option(npp, node, npp->current.netdef, data, NULL)) { return yaml_error(npp, node, error, "Invalid MAC address '%s', must be XX:XX:XX:XX:XX:XX, XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX" - " or one of 'permanent', 'random', 'stable', 'preserve'.", + " or one of 'permanent', 'random', 'stable', 'preserve', 'stable-ssid' (Wi-Fi only).", scalar(node)); } } diff --git a/src/util-internal.h b/src/util-internal.h index bed36d493..bfc4dbebc 100644 --- a/src/util-internal.h +++ b/src/util-internal.h @@ -136,7 +136,7 @@ gboolean _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth); gboolean -_is_macaddress_special_nm_option(const char* value); +_is_macaddress_special_nm_option(const NetplanNetDefinition* netdef, const char* value); gboolean _is_macaddress_special_nd_option(const char* value); diff --git a/src/util.c b/src/util.c index 0fc06f222..294a2f457 100644 --- a/src/util.c +++ b/src/util.c @@ -1239,12 +1239,13 @@ _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth) } gboolean -_is_macaddress_special_nm_option(const char* value) +_is_macaddress_special_nm_option(const NetplanNetDefinition* netdef, const char* value) { return ( !g_strcmp0(value, "preserve") || !g_strcmp0(value, "permanent") || !g_strcmp0(value, "random") - || !g_strcmp0(value, "stable")); + || !g_strcmp0(value, "stable") + || (!g_strcmp0(value, "stable-ssid") && netdef->type == NETPLAN_DEF_TYPE_WIFI)); } gboolean diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 9f01fcf28..0e3ad0d5d 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -504,7 +504,21 @@ def test_eth_set_mac_special_values_error(self): error = ("Invalid MAC address 'preservetypo', must be XX:XX:XX:XX:XX:XX, " "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX or " - "one of 'permanent', 'random', 'stable', 'preserve'") + "one of 'permanent', 'random', 'stable', 'preserve', 'stable-ssid' (Wi-Fi only)") + self.assertIn(error, res) + + def test_eth_set_mac_special_values_ethernet_stable_ssid(self): + res = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + macaddress: stable-ssid + dhcp4: true''', expect_fail=True) + + error = ("Invalid MAC address 'stable-ssid', must be XX:XX:XX:XX:XX:XX, " + "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX or " + "one of 'permanent', 'random', 'stable', 'preserve', 'stable-ssid' (Wi-Fi only)") self.assertIn(error, res) def test_eth_match_by_driver(self): diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py index 6dc8df7f8..5809b40e7 100644 --- a/tests/generator/test_wifis.py +++ b/tests/generator/test_wifis.py @@ -1114,6 +1114,42 @@ def test_wifi_regdom(self): new_config = f.read() self.assertIn('ExecStart=/usr/sbin/iw reg set DE\n', new_config) + def test_wlan_set_mac_special_values(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wlan0: + macaddress: stable-ssid + dhcp4: true + access-points: + "mynetwork": + password: mypassword''') + + self.assert_networkd(None) + + self.assert_nm({'wlan0-mynetwork': '''[connection] +id=netplan-wlan0-mynetwork +type=wifi +interface-name=wlan0 + +[wifi] +cloned-mac-address=stable-ssid +ssid=mynetwork +mode=infrastructure + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi-security] +key-mgmt=wpa-psk +pmf=2 +psk=mypassword +'''}) + class TestConfigErrors(TestBase): diff --git a/tests/integration/wifi.py b/tests/integration/wifi.py index 26200d0c1..4e8079ad7 100644 --- a/tests/integration/wifi.py +++ b/tests/integration/wifi.py @@ -181,5 +181,45 @@ def test_wifi_ap_open(self): self.assertIn('ssid fake net', out) self.assert_iface_up(self.dev_w_ap, ['inet 10.']) + @unittest.skip("Test if flaky. NM might generate a different MAC address.") + def test_wifi_cloned_macaddress_stable_ssid(self): + self.setup_ap('''hw_mode=g +channel=1 +ssid=fake net +wpa=1 +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +wpa_passphrase=12345678 +''', None) + + with open(self.config, 'w') as f: + f.write('''network: + renderer: NetworkManager + wifis: + %(wc)s: + addresses: ["192.168.1.42/24"] + dhcp4: false + dhcp6: false + macaddress: stable-ssid + access-points: + "fake net": + password: 12345678''' % {'wc': self.dev_w_client}) + + subprocess.check_call(['systemctl', 'start', 'NetworkManager']) + + # Make the generated MAC address predictable + # See nm_utils_hw_addr_gen_stable_eth() in NM for details + # TODO: save and restore these files to avoid any impact on the + # entire test suite. + with open('/etc/machine-id', 'w') as f: + f.write('ee7ac3602b6306061bd984a41eb1c045\n') + with open('/var/lib/NetworkManager/secret_key', 'w') as f: + f.write('nm-v2:hnIHoHp4p9kaEWU5/+dO+gFREirN1AsMoO1MPaoYxCc=') + + subprocess.check_call(['systemctl', 'restart', 'NetworkManager']) + + self.generate_and_settle([self.state_up(self.dev_w_client)]) + self.assert_iface_up(self.dev_w_client, ['ether 5e:ba:fe:fd:89:03']) + unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))